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:
+114
-8
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user