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:
qinglong
2026-06-13 11:05:56 +08:00
parent f402af58d0
commit 2d0a37ba19
3 changed files with 144 additions and 11 deletions
+14 -1
View File
@@ -10,6 +10,7 @@ import argparse
from services.project_engine import ProjectEngine
from services.pyenv_manager import PyEnvManager
from services.proxy_service import ProxyService
from services.web_panel.utils.system_info import SystemInfoCollector
import os
from pathlib import Path
@@ -62,6 +63,10 @@ class SenSuFramework:
# 2.5 自动启动脚本 (日志服务就绪后)
await init_service.start_auto_scripts()
# 2.6 系统信息采集器 (共享实例,TUI + Web面板共用)
sys_collector = SystemInfoCollector()
self.service_manager.register_service("sys_collector", sys_collector)
# 3. 核心桥接服务
core_bridge = CoreBridge()
await core_bridge.start()
@@ -94,7 +99,10 @@ class SenSuFramework:
self.service_manager.register_service("tui", self._create_fallback_tui())
else:
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()
self.service_manager.register_service("tui", tui_service)
logger.info("TUI服务启动成功")
@@ -122,6 +130,11 @@ class SenSuFramework:
await plugin_service.start()
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管理面板初始化 (必须在网络服务启动前!) ===
# 原因: aiohttp 启动后会“冻结”路由器,之后再挂载子应用会报错
logger.info("> 初始化 Web 管理面板 中...")
+114 -8
View File
@@ -6,12 +6,14 @@ import sys
import io
import time
from textual.app import App
from textual.containers import Container, ScrollableContainer
from textual.widgets import Static, Input, Header, Footer
from textual.containers import Container, ScrollableContainer, Horizontal
from textual.widgets import Static, Input, Header, Footer, LoadingIndicator
from textual.reactive import reactive
from typing import List, Dict
import asyncio
from datetime import datetime
import psutil
import os
logger = logging.getLogger(__name__)
@@ -403,16 +405,107 @@ class MessageDisplay(Static):
logger.debug(f"消息自动滚动: {'启用' if self.auto_scroll_enabled else '禁用'}")
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):
"""TUI框架应用"""
def __init__(self, config, log_service, command_service):
"""TUI框架应用"""
def __init__(self, config, log_service, command_service, plugin_service=None):
super().__init__()
self.config = config
self.log_service = log_service
self.command_service = command_service
self._plugin_service = plugin_service
self.log_display = LogDisplay()
self.message_display = MessageDisplay()
self.system_monitor = SystemMonitor()
self.plugin_panel = PluginStatusPanel(plugin_service)
self.command_input = None
self.CSS = self._generate_css()
@@ -517,6 +610,11 @@ class TUIFramework(App):
def compose(self):
"""组合界面"""
yield Header()
yield Container(
self.system_monitor,
self.plugin_panel,
id="monitor-area",
)
yield ScrollableContainer(
self.log_display,
id="log-area"
@@ -661,11 +759,13 @@ class TUIFramework(App):
class TuiService:
"""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.log_service = log_service
self.command_service = command_service
self.sys_collector = sys_collector
self.plugin_service = plugin_service
self.tui_app = None
self._message_queue = asyncio.Queue()
self._message_processor_task = None
@@ -678,7 +778,13 @@ class TuiService:
return
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()
+16 -2
View File
@@ -47,8 +47,21 @@ async def get_framework(req):
return web.json_response({"error": "Missing"}, status=500)
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):
return web.json_response(collector.get_all())
c = _get_collector(req)
return web.json_response(c.get_all())
async def sys_ws_handler(req):
"""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)"""
try:
sm = req.app.get('service_manager')
c = _get_collector(req)
payload = json.dumps({
"type": "sys",
"system": collector.get_all(),
"system": c.get_all(),
"framework": _get_framework_data(sm)
}, ensure_ascii=False)
if not ws.closed: