diff --git a/main.py b/main.py index 1bc8698..ea85972 100644 --- a/main.py +++ b/main.py @@ -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 管理面板 中...") diff --git a/services/tui_service.py b/services/tui_service.py index 61f4c19..23508eb 100644 --- a/services/tui_service.py +++ b/services/tui_service.py @@ -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() diff --git a/services/web_panel/routes/status.py b/services/web_panel/routes/status.py index 528b0a5..fa5f711 100644 --- a/services/web_panel/routes/status.py +++ b/services/web_panel/routes/status.py @@ -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: