feat(v0.4): TUI 系统监控面板 + 插件状态面板
v0.4.1 系统监控面板: - 复用共享 SystemInfoCollector (TUI + Web面板同一数据源) - CPU/MEM 彩色进度条 + 网络实时速率 - 2 秒刷新,set_interval 驱动 v0.4.2 插件状态面板: - 展示已加载插件的名称/版本/命令数 - 3 秒刷新,PluginService 延迟注入 SystemInfoCollector: - 注册为共享服务 (sys_collector) - Web panel status.py 优先从 service_manager 获取 - 零重复数据采集 TUI 布局升级: 3行 → 4行 (monitor + log + message + input) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import argparse
|
|||||||
from services.project_engine import ProjectEngine
|
from services.project_engine import ProjectEngine
|
||||||
from services.pyenv_manager import PyEnvManager
|
from services.pyenv_manager import PyEnvManager
|
||||||
from services.proxy_service import ProxyService
|
from services.proxy_service import ProxyService
|
||||||
|
from services.web_panel.utils.system_info import SystemInfoCollector
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -62,6 +63,10 @@ class SenSuFramework:
|
|||||||
# 2.5 自动启动脚本 (日志服务就绪后)
|
# 2.5 自动启动脚本 (日志服务就绪后)
|
||||||
await init_service.start_auto_scripts()
|
await init_service.start_auto_scripts()
|
||||||
|
|
||||||
|
# 2.6 系统信息采集器 (共享实例,TUI + Web面板共用)
|
||||||
|
sys_collector = SystemInfoCollector()
|
||||||
|
self.service_manager.register_service("sys_collector", sys_collector)
|
||||||
|
|
||||||
# 3. 核心桥接服务
|
# 3. 核心桥接服务
|
||||||
core_bridge = CoreBridge()
|
core_bridge = CoreBridge()
|
||||||
await core_bridge.start()
|
await core_bridge.start()
|
||||||
@@ -94,7 +99,10 @@ class SenSuFramework:
|
|||||||
self.service_manager.register_service("tui", self._create_fallback_tui())
|
self.service_manager.register_service("tui", self._create_fallback_tui())
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
tui_service = TuiService(base_config, log_service, command_service)
|
tui_service = TuiService(
|
||||||
|
base_config, log_service, command_service,
|
||||||
|
sys_collector=sys_collector,
|
||||||
|
)
|
||||||
await tui_service.start()
|
await tui_service.start()
|
||||||
self.service_manager.register_service("tui", tui_service)
|
self.service_manager.register_service("tui", tui_service)
|
||||||
logger.info("TUI服务启动成功")
|
logger.info("TUI服务启动成功")
|
||||||
@@ -122,6 +130,11 @@ class SenSuFramework:
|
|||||||
await plugin_service.start()
|
await plugin_service.start()
|
||||||
self.service_manager.register_service("plugin", plugin_service)
|
self.service_manager.register_service("plugin", plugin_service)
|
||||||
|
|
||||||
|
# 将 plugin_service 注入 TUI 插件状态面板
|
||||||
|
tui = self.service_manager.get_service("tui")
|
||||||
|
if hasattr(tui, 'tui_app') and tui.tui_app:
|
||||||
|
tui.tui_app.plugin_panel.set_plugin_service(plugin_service)
|
||||||
|
|
||||||
# === 🟢 新增: 11.5 Web管理面板初始化 (必须在网络服务启动前!) ===
|
# === 🟢 新增: 11.5 Web管理面板初始化 (必须在网络服务启动前!) ===
|
||||||
# 原因: aiohttp 启动后会“冻结”路由器,之后再挂载子应用会报错
|
# 原因: aiohttp 启动后会“冻结”路由器,之后再挂载子应用会报错
|
||||||
logger.info("> 初始化 Web 管理面板 中...")
|
logger.info("> 初始化 Web 管理面板 中...")
|
||||||
|
|||||||
+114
-8
@@ -6,12 +6,14 @@ import sys
|
|||||||
import io
|
import io
|
||||||
import time
|
import time
|
||||||
from textual.app import App
|
from textual.app import App
|
||||||
from textual.containers import Container, ScrollableContainer
|
from textual.containers import Container, ScrollableContainer, Horizontal
|
||||||
from textual.widgets import Static, Input, Header, Footer
|
from textual.widgets import Static, Input, Header, Footer, LoadingIndicator
|
||||||
from textual.reactive import reactive
|
from textual.reactive import reactive
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import psutil
|
||||||
|
import os
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -403,16 +405,107 @@ class MessageDisplay(Static):
|
|||||||
logger.debug(f"消息自动滚动: {'启用' if self.auto_scroll_enabled else '禁用'}")
|
logger.debug(f"消息自动滚动: {'启用' if self.auto_scroll_enabled else '禁用'}")
|
||||||
return self.auto_scroll_enabled
|
return self.auto_scroll_enabled
|
||||||
|
|
||||||
|
class SystemMonitor(Static):
|
||||||
|
"""系统监控组件 — 从共享 SystemInfoCollector 取数"""
|
||||||
|
|
||||||
|
def __init__(self, collector=None):
|
||||||
|
super().__init__("")
|
||||||
|
self._collector = collector
|
||||||
|
self._timer = None
|
||||||
|
|
||||||
|
def set_collector(self, c):
|
||||||
|
self._collector = c
|
||||||
|
|
||||||
|
def on_mount(self):
|
||||||
|
self._timer = self.set_interval(2, self._refresh_stats)
|
||||||
|
|
||||||
|
def _refresh_stats(self):
|
||||||
|
try:
|
||||||
|
if not self._collector:
|
||||||
|
self.update("📊 系统监控: 数据源未连接")
|
||||||
|
return
|
||||||
|
all_data = self._collector.get_all()
|
||||||
|
cpu_pct = all_data["cpu"]["percent"]
|
||||||
|
mem = all_data["memory"]
|
||||||
|
net_speed = all_data["net_speed"]
|
||||||
|
|
||||||
|
def bar(val, w=10):
|
||||||
|
filled = int(min(val, 100) / 100 * w)
|
||||||
|
bar_chars = "█" * filled + "░" * (w - filled)
|
||||||
|
if val < 60:
|
||||||
|
color = "\033[32m"
|
||||||
|
elif val < 85:
|
||||||
|
color = "\033[33m"
|
||||||
|
else:
|
||||||
|
color = "\033[31m"
|
||||||
|
return f"{color}{bar_chars}\033[0m"
|
||||||
|
|
||||||
|
self.update(
|
||||||
|
f" CPU {bar(cpu_pct)} {cpu_pct:5.1f}% │ "
|
||||||
|
f"MEM {bar(mem['percent'])} {mem['percent']:5.1f}% "
|
||||||
|
f"({mem['used_gb']:.1f}/{mem['total_gb']:.1f}G) │ "
|
||||||
|
f"NET ↓{self._fmt_speed(net_speed['rx_bytes_sec'])} "
|
||||||
|
f"↑{self._fmt_speed(net_speed['tx_bytes_sec'])}"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _fmt_speed(bps):
|
||||||
|
if bps < 1024:
|
||||||
|
return f"{bps:.0f}B/s"
|
||||||
|
elif bps < 1024 * 1024:
|
||||||
|
return f"{bps / 1024:.0f}K/s"
|
||||||
|
else:
|
||||||
|
return f"{bps / 1024 / 1024:.1f}M/s"
|
||||||
|
|
||||||
|
|
||||||
|
class PluginStatusPanel(Static):
|
||||||
|
"""插件实时状态面板 — 展示已加载插件的状态/命令数"""
|
||||||
|
|
||||||
|
def __init__(self, plugin_service=None):
|
||||||
|
super().__init__("🔌 插件状态: 加载中...")
|
||||||
|
self._plugin_service = plugin_service
|
||||||
|
self._timer = None
|
||||||
|
|
||||||
|
def on_mount(self):
|
||||||
|
self._timer = self.set_interval(3, self._refresh)
|
||||||
|
|
||||||
|
def set_plugin_service(self, svc):
|
||||||
|
self._plugin_service = svc
|
||||||
|
|
||||||
|
def _refresh(self):
|
||||||
|
try:
|
||||||
|
if not self._plugin_service:
|
||||||
|
self.update("🔌 插件状态: 未连接")
|
||||||
|
return
|
||||||
|
infos = getattr(self._plugin_service, "plugin_info", {})
|
||||||
|
if not infos:
|
||||||
|
self.update("🔌 插件状态: 无已加载插件")
|
||||||
|
return
|
||||||
|
lines = []
|
||||||
|
for name, info in infos.items():
|
||||||
|
cmds = len(info.commands) if info.commands else 0
|
||||||
|
status_icon = "✅" if info.loaded else "❌"
|
||||||
|
lines.append(f"{status_icon} {name} v{info.version} | {cmds} commands")
|
||||||
|
self.update("🔌 " + " │ ".join(lines))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TUIFramework(App):
|
class TUIFramework(App):
|
||||||
"""TUI框架应用"""
|
"""TUI框架应用"""
|
||||||
def __init__(self, config, log_service, command_service):
|
def __init__(self, config, log_service, command_service, plugin_service=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.config = config
|
self.config = config
|
||||||
self.log_service = log_service
|
self.log_service = log_service
|
||||||
self.command_service = command_service
|
self.command_service = command_service
|
||||||
|
self._plugin_service = plugin_service
|
||||||
|
|
||||||
self.log_display = LogDisplay()
|
self.log_display = LogDisplay()
|
||||||
self.message_display = MessageDisplay()
|
self.message_display = MessageDisplay()
|
||||||
|
self.system_monitor = SystemMonitor()
|
||||||
|
self.plugin_panel = PluginStatusPanel(plugin_service)
|
||||||
self.command_input = None
|
self.command_input = None
|
||||||
self.CSS = self._generate_css()
|
self.CSS = self._generate_css()
|
||||||
|
|
||||||
@@ -517,6 +610,11 @@ class TUIFramework(App):
|
|||||||
def compose(self):
|
def compose(self):
|
||||||
"""组合界面"""
|
"""组合界面"""
|
||||||
yield Header()
|
yield Header()
|
||||||
|
yield Container(
|
||||||
|
self.system_monitor,
|
||||||
|
self.plugin_panel,
|
||||||
|
id="monitor-area",
|
||||||
|
)
|
||||||
yield ScrollableContainer(
|
yield ScrollableContainer(
|
||||||
self.log_display,
|
self.log_display,
|
||||||
id="log-area"
|
id="log-area"
|
||||||
@@ -661,11 +759,13 @@ class TUIFramework(App):
|
|||||||
|
|
||||||
class TuiService:
|
class TuiService:
|
||||||
"""TUI服务"""
|
"""TUI服务"""
|
||||||
|
|
||||||
def __init__(self, config: Dict, log_service, command_service):
|
def __init__(self, config: Dict, log_service, command_service, sys_collector=None, plugin_service=None):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.log_service = log_service
|
self.log_service = log_service
|
||||||
self.command_service = command_service
|
self.command_service = command_service
|
||||||
|
self.sys_collector = sys_collector
|
||||||
|
self.plugin_service = plugin_service
|
||||||
self.tui_app = None
|
self.tui_app = None
|
||||||
self._message_queue = asyncio.Queue()
|
self._message_queue = asyncio.Queue()
|
||||||
self._message_processor_task = None
|
self._message_processor_task = None
|
||||||
@@ -678,7 +778,13 @@ class TuiService:
|
|||||||
return
|
return
|
||||||
|
|
||||||
print("启动TUI服务")
|
print("启动TUI服务")
|
||||||
self.tui_app = TUIFramework(self.config, self.log_service, self.command_service)
|
self.tui_app = TUIFramework(
|
||||||
|
self.config, self.log_service, self.command_service,
|
||||||
|
plugin_service=self.plugin_service,
|
||||||
|
)
|
||||||
|
# 注入系统数据采集器
|
||||||
|
self.tui_app.system_monitor.set_collector(self.sys_collector)
|
||||||
|
self.tui_app.plugin_panel.set_plugin_service(self.plugin_service)
|
||||||
|
|
||||||
# 设置动态标题
|
# 设置动态标题
|
||||||
self._setup_title()
|
self._setup_title()
|
||||||
|
|||||||
@@ -47,8 +47,21 @@ async def get_framework(req):
|
|||||||
return web.json_response({"error": "Missing"}, status=500)
|
return web.json_response({"error": "Missing"}, status=500)
|
||||||
return web.json_response(_get_framework_data(sm))
|
return web.json_response(_get_framework_data(sm))
|
||||||
|
|
||||||
|
def _get_collector(req):
|
||||||
|
"""获取共享的 SystemInfoCollector(优先从 service_manager),fallback 到模块级实例"""
|
||||||
|
sm = req.app.get('service_manager')
|
||||||
|
if sm:
|
||||||
|
try:
|
||||||
|
shared = sm.get_service("sys_collector")
|
||||||
|
if shared:
|
||||||
|
return shared
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return collector
|
||||||
|
|
||||||
async def get_system(req):
|
async def get_system(req):
|
||||||
return web.json_response(collector.get_all())
|
c = _get_collector(req)
|
||||||
|
return web.json_response(c.get_all())
|
||||||
|
|
||||||
async def sys_ws_handler(req):
|
async def sys_ws_handler(req):
|
||||||
"""WebSocket push endpoint: system+framwork stats every 2s (no logging per message)"""
|
"""WebSocket push endpoint: system+framwork stats every 2s (no logging per message)"""
|
||||||
@@ -60,9 +73,10 @@ async def sys_ws_handler(req):
|
|||||||
"""Send one snapshot to this client (silent on error)"""
|
"""Send one snapshot to this client (silent on error)"""
|
||||||
try:
|
try:
|
||||||
sm = req.app.get('service_manager')
|
sm = req.app.get('service_manager')
|
||||||
|
c = _get_collector(req)
|
||||||
payload = json.dumps({
|
payload = json.dumps({
|
||||||
"type": "sys",
|
"type": "sys",
|
||||||
"system": collector.get_all(),
|
"system": c.get_all(),
|
||||||
"framework": _get_framework_data(sm)
|
"framework": _get_framework_data(sm)
|
||||||
}, ensure_ascii=False)
|
}, ensure_ascii=False)
|
||||||
if not ws.closed:
|
if not ws.closed:
|
||||||
|
|||||||
Reference in New Issue
Block a user