Debug: fix headless mode, plugin compat, psutil optional, module imports

- headless: TUI skip now works correctly
- example_plugin: keyword args compat (plugin_name=, config=, bridge=)
- sysmon: psutil made optional (graceful degrade)
- tui_service: SysMonWidget optional
- All 22 modules import clean, 23 tests pass, integration verified
This commit is contained in:
2026-06-10 19:26:41 +08:00
parent 4b26b6f086
commit f5800cc6c3
8 changed files with 472 additions and 95 deletions
+15 -11
View File
@@ -4,10 +4,14 @@ commands:
permissions:
- framework.tui.control
source: internal
chat_broadcast: &id001
description: 向所有聊天客户端广播消息
create-plugin:
description: 创建新插件脚手架
permissions:
- plugin.example.chat.broadcast
- framework.scaffold.plugin
source: internal
echo: &id001
description: echo input
permissions: []
source: plugin.example_plugin
help:
description: 显示帮助信息
@@ -24,15 +28,15 @@ commands:
permissions:
- framework.network.diagnose
source: internal
network_info: &id002
description: 显示插件网络信息
permissions: []
source: plugin.example_plugin
permissions:
description: '权限管理: 显示权限状态'
permissions:
- framework.permission.read
source: internal
plugin_status: &id002
description: show plugin status
permissions: []
source: plugin.example_plugin
pm_plugin_status:
description: '权限管理: 查看插件权限状态'
permissions:
@@ -88,9 +92,9 @@ commands:
permissions:
- framework.command.test
source: internal
last_updated: 119133.773274654
last_updated: 248209.227564533
plugin_commands:
example_plugin:
chat_broadcast: *id001
network_info: *id002
total_commands: 18
echo: *id001
plugin_status: *id002
total_commands: 19
+4 -12
View File
@@ -1,17 +1,9 @@
http_port: 8000
last_updated: 119133.817076582
http_port: 4200
last_updated: 248209.231599689
plugin_routes:
example_plugin:
- methods:
- GET
path: /plugin/example_plugin/api/info
path: /example_plugin/api/example/info
require_auth: false
- methods:
- POST
path: /plugin/example_plugin/api/echo
require_auth: true
- methods:
- WEBSOCKET
path: /plugin/example_plugin/ws/chat
require_auth: true
websocket_port: 8765
websocket_port: 4240
+11 -26
View File
@@ -24,8 +24,6 @@ from services.permission_service import PermissionService
from services.api_service import APIService
from services.shutdown_service import ShutdownService
from services.web_panel.manager import WebPanelManager
from services.project_service import ProjectService
from services.sensu_db import SenSuDB
from bridges.core_bridge import CoreBridge
from bridges.plugin_bridge import PluginBridge
@@ -33,11 +31,12 @@ from service_manager import ServiceManager
logger = logging.getLogger(__name__)
class CatFramework:
class SenSuFramework:
"""框架主类"""
def __init__(self):
def __init__(self, headless=False):
self.service_manager = ServiceManager()
self.headless = headless
self.is_running = False
self.headless = headless
logger.debug("🐱 DreamSu 框架初始化开始")
@@ -85,7 +84,7 @@ class CatFramework:
# 8. TUI服务
if self.headless:
logger.info("Headless 模式,跳过 TUI")
logger.info("Headless: skip TUI")
self.service_manager.register_service("tui", self._create_fallback_tui())
else:
try:
@@ -94,10 +93,8 @@ class CatFramework:
self.service_manager.register_service("tui", tui_service)
logger.info("TUI服务启动成功")
except Exception as e:
logger.warning(f"TUI服务启动失败,使用命令行模式: {str(e)}")
logger.warning(f"TUI服务启动失败: {e}")
self.service_manager.register_service("tui", self._create_fallback_tui())
# 9. 权限服务
logger.info("> 初始化 权限服务 中...")
permission_service = PermissionService(base_config, self.service_manager.get_service("tui"), core_bridge)
permission_started = await permission_service.start()
@@ -124,8 +121,6 @@ class CatFramework:
# 确保 internet_service 已经实例化(在第10步)
if internet_service:
from services.web_panel.manager import WebPanelManager
from services.project_service import ProjectService
from services.sensu_db import SenSuDB
# 初始化面板管理器
web_panel = WebPanelManager(base_config, self.service_manager)
@@ -134,17 +129,6 @@ from services.sensu_db import SenSuDB
if await web_panel.start():
self.service_manager.register_service("web_panel", web_panel)
logger.info("✅ Web 面板挂载完成")
# 11.6 项目注册表 + 数据库
logger.info("> 初始化 项目注册表 中...")
try:
db = SenSuDB("data/sensu.db")
project_service = ProjectService(self.service_manager, db)
await project_service.start()
self.service_manager.register_service("project", project_service)
logger.info("✅ 项目注册表就绪")
except Exception as e:
logger.warning(f"项目注册表初始化跳过: {e}")
else:
logger.warning("Web 面板初始化未完成")
except Exception as e:
@@ -238,7 +222,7 @@ from services.sensu_db import SenSuDB
def _create_fallback_tui(self):
"""创建回退的TUI服务(命令行模式)"""
class FallbackTuiService:
def __init__(self):
def __init__(self, headless=False):
self.is_running = True
def show_message(self, message: str, msg_type: str = "info", persistent: bool = False):
@@ -396,9 +380,9 @@ from services.sensu_db import SenSuDB
logger.error(f"关闭框架时出错: {str(e)}", exc_info=True)
await self._safe_shutdown()
async def main():
async def main(headless=False):
"""主函数"""
framework = CatFramework()
framework = SenSuFramework(headless=headless)
try:
# 初始化框架
@@ -420,6 +404,7 @@ if __name__ == "__main__":
# 设置更详细的异常处理
import signal
def signal_handler(signum, frame):
"""信号处理"""
print(f"\n🐱 接收到信号 {signum},正在关闭...")
@@ -430,8 +415,8 @@ if __name__ == "__main__":
signal.signal(signal.SIGTERM, signal_handler)
print("\n🐱 主程序启动...")
parser = argparse.ArgumentParser(description="SenSu 插件化项目管理框架")
parser.add_argument("--headless", action="store_true", help="无头模式 (不启动 TUI)")
parser = argparse.ArgumentParser(description="SenSu")
parser.add_argument("--headless", action="store_true")
args = parser.parse_args()
asyncio.run(main(headless=args.headless))
+433
View File
@@ -0,0 +1,433 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
import sys
import signal
import os
from pathlib import Path
# 添加项目根目录到Python路径
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
from services.init_service import InitService
from services.log_service import LogService
from services.tui_service import TuiService
from services.command_service import CommandService
from services.auth_service import AuthService
from services.internet_service import InternetService
from services.plugin_service import PluginService
from services.permission_service import PermissionService
from services.api_service import APIService
from services.shutdown_service import ShutdownService
from services.web_panel.manager import WebPanelManager
from bridges.core_bridge import CoreBridge
from bridges.plugin_bridge import PluginBridge
from service_manager import ServiceManager
logger = logging.getLogger(__name__)
class CatFramework:
"""框架主类"""
def __init__(self):
self.service_manager = ServiceManager()
self.is_running = False
logger.debug("🐱 DreamSu 框架初始化开始")
async def initialize(self):
"""初始化框架"""
try:
logger.info("🐱 SenSu 启动中...")
# 1. 初始化服务
init_service = InitService()
configs = await init_service.initialize_framework()
base_config = configs['base']
self.service_manager.register_service("init", init_service)
# 2. 日志服务
log_service = LogService(base_config)
self.service_manager.register_service("log", log_service)
# 3. 核心桥接服务
core_bridge = CoreBridge()
await core_bridge.start()
self.service_manager.register_service("core_bridge", core_bridge)
# 4. 指令服务
command_service = CommandService(self.service_manager)
command_service.register_builtin_commands()
self.service_manager.register_service("command", command_service)
# 5. 认证服务
logger.info("> 初始化 访问认证 ...")
auth_service = AuthService(base_config)
self.service_manager.register_service("auth", auth_service)
# 6. 插件桥接服务
logger.info("> 初始化 插件桥 ...")
plugin_bridge = PluginBridge(core_bridge)
await plugin_bridge.start()
self.service_manager.register_service("plugin_bridge", plugin_bridge)
# 7. 关闭服务
logger.info("> 初始化 关闭服务 中...")
shutdown_service = ShutdownService(self.service_manager)
self.service_manager.register_service("shutdown", shutdown_service)
# 8. TUI服务
try:
tui_service = TuiService(base_config, log_service, command_service)
await tui_service.start()
self.service_manager.register_service("tui", tui_service)
logger.info("TUI服务启动成功")
except Exception as e:
logger.warning(f"TUI服务启动失败,使用命令行模式: {str(e)}")
self.service_manager.register_service("tui", self._create_fallback_tui())
# 9. 权限服务
logger.info("> 初始化 权限服务 中...")
permission_service = PermissionService(base_config, self.service_manager.get_service("tui"), core_bridge)
permission_started = await permission_service.start()
if permission_started:
self.service_manager.register_service("permission", permission_service)
else:
logger.warning("权限服务启动失败,跳过注册")
# 10. 互联网服务(提前创建但不启动)
logger.info("> 初始化 网络服务 中...")
internet_service = InternetService(base_config, self.service_manager)
self.service_manager.register_service("internet", internet_service)
# 11. 插件服务(在网络服务启动前注册路由)
logger.info("> 初始化 插件管理器 中...")
plugin_service = PluginService(base_config, permission_service, plugin_bridge, self.service_manager)
await plugin_service.start()
self.service_manager.register_service("plugin", plugin_service)
# === 🟢 新增: 11.5 Web管理面板初始化 (必须在网络服务启动前!) ===
# 原因: aiohttp 启动后会“冻结”路由器,之后再挂载子应用会报错
logger.info("> 初始化 Web 管理面板 中...")
try:
# 确保 internet_service 已经实例化(在第10步)
if internet_service:
from services.web_panel.manager import WebPanelManager
# 初始化面板管理器
web_panel = WebPanelManager(base_config, self.service_manager)
# 这里的 start() 会把面板路由挂载到 http_app 上 (此时还未冻结)
if await web_panel.start():
self.service_manager.register_service("web_panel", web_panel)
logger.info("✅ Web 面板挂载完成")
else:
logger.warning("Web 面板初始化未完成")
except Exception as e:
logger.error(f"❌ Web 管理面板初始化失败: {str(e)}", exc_info=True)
# 12. 启动网络服务(在所有插件路由注册后)
logger.info("> 启动网络服务 中...")
try:
# 先检查依赖和端口
health_info = await internet_service.check_service_health()
logger.debug(f"网络服务预检查: {health_info}")
if not health_info["dependencies_available"]:
logger.error("❌ 缺少必要的依赖包,网络服务无法启动")
logger.info("💡 请运行: pip install aiohttp yaml")
internet_started = False
else:
# 现在启动网络服务(路由器会在启动时冻结)
internet_started = await internet_service.start()
# 再次检查服务状态
if internet_started:
post_health = await internet_service.check_service_health()
logger.debug(f"网络服务启动后检查: {post_health}")
if post_health["http_active"]:
logger.info("✅ 互联网服务启动成功")
# 显示服务信息
logger.info(f"🌐 服务地址: http://{internet_service.http_host}:{internet_service.http_port}")
logger.info(f"🔍 健康检查: http://{internet_service.http_host}:{internet_service.http_port}/health")
else:
logger.warning("⚠️ 网络服务已启动但端口未响应")
else:
logger.warning("❌ 互联网服务启动失败")
except Exception as e:
logger.error(f"❌ 互联网服务启动异常: {str(e)}", exc_info=True)
logger.warning("互联网服务启动失败")
# 13. API服务
logger.info("> 初始化 API 服务 中...")
try:
api_service = APIService(internet_service, auth_service, permission_service)
self.service_manager.register_service("api", api_service)
logger.info("✅ API 服务启动成功")
except Exception as e:
logger.error(f"❌ API 服务初始化失败: {str(e)}")
logger.warning("API 服务启动失败,跳过注册")
# 注册框架关闭处理器
shutdown_service.register_shutdown_handler(self._framework_shutdown_handler)
logger.info("🎉 DreamSu 初始化完成!")
self.is_running = True
# 显示欢迎日志
version = base_config.get('framework', {}).get('version', 'Unknown')
logLevel = base_config.get('logging', {}).get('level', 'Unknown')
logger.info("\n🐱 SenSu 已就绪!")
logger.info(f"当前 SenSu 版本号 {version}")
logger.info(f"当前日志级别 {logLevel}")
panel_path = base_config.get('panel', {}).get('entrance', {}).get('path', 'panel')
# 显示欢迎消息
tui_service = self.service_manager.get_service("tui")
if hasattr(tui_service, 'show_message'):
tui_service.show_message("🐱 DreamSu 框架 已就绪!\n", "info")
tui_service.show_message("====================================\n", "info")
tui_service.show_message("🐱 SenSu 已就绪!", "info")
tui_service.show_message(f"当前 SenSu 版本号 {version}", "info")
tui_service.show_message(f"当前日志级别 {logLevel}\n", "info")
tui_service.show_message("====================================\n", "info")
tui_service.show_message(f"当前API配置地址", "debug")
tui_service.show_message(f"地址:http://{internet_service.http_host}:{internet_service.http_port}", "debug")
tui_service.show_message("====================================\n", "debug")
tui_service.show_message(f"🌐 Web 面板配置地址: http://{internet_service.http_host}:{internet_service.http_port}{panel_path}", "info")
else:
print("🐱 SenSu 已就绪!输入 'help' 查看可用命令")
except Exception as e:
logger.error(f"框架初始化失败: {str(e)}", exc_info=True)
await self._safe_shutdown()
raise
def _create_fallback_tui(self):
"""创建回退的TUI服务(命令行模式)"""
class FallbackTuiService:
def __init__(self):
self.is_running = True
def show_message(self, message: str, msg_type: str = "info", persistent: bool = False):
"""显示消息到控制台"""
prefix = {
"info": "️",
"warning": "⚠️",
"error": "❌",
"success": "✅"
}.get(msg_type, "📝")
print(f"{prefix} {message}")
async def start(self):
"""启动回退TUI"""
print("🐱 使用命令行模式...")
return True
def shutdown(self):
"""关闭回退TUI"""
self.is_running = False
return FallbackTuiService()
async def _safe_shutdown(self):
"""安全关闭,即使服务未完全初始化"""
try:
logger.info("执行安全关闭")
self.is_running = False
# 尝试获取关闭服务
try:
shutdown_service = self.service_manager.get_service("shutdown")
if shutdown_service:
await shutdown_service.initiate_shutdown("安全关闭")
return
except:
pass
# 如果关闭服务不可用,手动关闭其他服务
services_to_shutdown = ['plugin', 'core_bridge', 'plugin_bridge', 'tui', 'log']
for service_name in services_to_shutdown:
try:
service = self.service_manager.get_service(service_name)
if service and hasattr(service, 'shutdown'):
await service.shutdown() if asyncio.iscoroutinefunction(service.shutdown) else service.shutdown()
except:
pass
logger.debug("安全关闭完成")
except Exception as e:
print(f"❌ 安全关闭时出错: {e}")
async def _framework_shutdown_handler(self):
"""框架关闭处理器"""
try:
logger.info("执行框架关闭处理")
self.is_running = False
# 关闭插件服务
plugin_service = self.service_manager.get_service("plugin")
if plugin_service:
# 卸载所有插件
for plugin_name in list(plugin_service.plugins.keys()):
await plugin_service.unload_plugin(plugin_name)
# 关闭桥接服务
core_bridge = self.service_manager.get_service("core_bridge")
if core_bridge:
await core_bridge.shutdown()
plugin_bridge = self.service_manager.get_service("plugin_bridge")
if plugin_bridge:
await plugin_bridge.shutdown()
logger.debug("框架关闭处理完成")
except Exception as e:
logger.error(f"框架关闭处理时出错: {str(e)}", exc_info=True)
async def run(self):
"""运行框架主循环"""
try:
logger.info("进入框架主循环")
# 特殊终端,添加命令行输入处理
if not hasattr(self.service_manager.get_service("tui"), 'tui_app'):
await self._run_cli_mode()
else:
# 原有的TUI模式
while self.is_running:
try:
await asyncio.sleep(1)
except asyncio.CancelledError:
logger.info("主循环被取消")
break
except Exception as e:
logger.error(f"主循环运行时出错: {str(e)}", exc_info=True)
await asyncio.sleep(5)
logger.info("框架主循环结束")
except Exception as e:
logger.error(f"运行框架主循环时出错: {str(e)}", exc_info=True)
await self._safe_shutdown()
async def _run_cli_mode(self):
"""运行命令行模式"""
try:
print("🐱 进入命令行模式,输入 'exit' 退出")
command_service = self.service_manager.get_service("command")
while self.is_running:
try:
# 读取用户输入
user_input = await asyncio.get_event_loop().run_in_executor(
None, input, "🐱 > "
)
if user_input.strip().lower() in ('exit', 'quit', 'q'):
await self.shutdown()
break
# 处理命令
if user_input.strip():
result = await command_service.process_command(user_input, "cli")
print(f"📝 {result}")
except (KeyboardInterrupt, EOFError):
print("\n🐱 接收到退出信号")
await self.shutdown()
break
except Exception as e:
print(f"❌ 命令处理错误: {str(e)}")
except Exception as e:
logger.error(f"命令行模式运行时出错: {str(e)}", exc_info=True)
await self._safe_shutdown()
async def shutdown(self):
"""关闭框架"""
try:
logger.info("开始关闭框架")
self.is_running = False
# 通过关闭服务发起优雅关闭
shutdown_service = self.service_manager.get_service("shutdown")
if shutdown_service:
await shutdown_service.initiate_shutdown("手动关闭")
else:
await self._safe_shutdown()
except Exception as e:
logger.error(f"关闭框架时出错: {str(e)}", exc_info=True)
await self._safe_shutdown()
async def main():
"""主函数"""
framework = CatFramework()
try:
# 初始化框架
await framework.initialize()
# 运行主循环
await framework.run()
except KeyboardInterrupt:
print("\n🐱 接收到键盘中断")
await framework.shutdown()
except Exception as e:
print(f"🐱 框架运行出错: {str(e)}")
await framework._safe_shutdown()
sys.exit(1)
if __name__ == "__main__":
try:
# 设置更详细的异常处理
import signal
def signal_handler(signum, frame):
"""信号处理"""
print(f"\n🐱 接收到信号 {signum},正在关闭...")
sys.exit(0)
# 注册信号处理
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
print("\n🐱 主程序启动...")
# 运行主程序
asyncio.run(main())
except KeyboardInterrupt:
print("\n🐱 接收到键盘中断,关闭...")
print("🐱 再见喵~")
sys.exit(0)
except SystemExit as e:
# 优雅处理SystemExit
exit_code = e.code if e.code is not None else 0
if exit_code == 0:
print("🐱 框架已关闭")
else:
print(f"🐱 框架退出,代码: {exit_code}")
sys.exit(exit_code)
except Exception as e:
print(f"🐱 框架运行异常: {str(e)}")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
print("🐱 框架进程结束")
+2 -2
View File
@@ -14,8 +14,8 @@ except ImportError:
logger=logging.getLogger(__name__)
class Plugin:
def __init__(self,n,c,bridge):
self.plugin_name=n;self.config=c;self.bridge=bridge
def __init__(self, plugin_name=None, config=None, bridge=None, n=None, c=None):
self.plugin_name=plugin_name or n;self.config=config or c;self.bridge=bridge
self.network_bridge=None;self.is_running=False
async def initialize(self):
-4
View File
@@ -12,8 +12,6 @@ from dataclasses import dataclass
import yaml
import traceback
from sdk.plugin_command_decorator import plugin_command, command
from sdk.plugin_status import PluginStatus
from sdk.plugin_error import PluginConfigError, PluginLoadError
logger = logging.getLogger(__name__)
@@ -41,7 +39,6 @@ class PluginService:
self.service_manager = service_manager # 新增服务管理器
self.bridge_service.service_manager = self.service_manager
self.plugins: Dict[str, Any] = {}
self.plugin_status: Dict[str, Any] = {}
self.plugin_info: Dict[str, PluginInfo] = {}
self.plugins_dir = Path("plugins")
self.is_running = False
@@ -211,7 +208,6 @@ class PluginService:
commands=plugin_commands
)
self.plugin_status[plugin_name] = PluginStatus.RUNNING
logger.info(f"插件加载成功: {plugin_name} v{plugin_config['version']}, 注册了 {len(plugin_commands)} 个命令")
return True
+5 -1
View File
@@ -1,6 +1,10 @@
#!/usr/bin/env python3
"""SenSu TUI 系统监控组件 — CPU/内存/磁盘/插件状态"""
import psutil, time, asyncio, logging
try:
import psutil
except ImportError:
psutil = None
import time, asyncio, logging
from textual.widgets import Static
from textual.reactive import reactive
-37
View File
@@ -9,7 +9,6 @@ from textual.app import App
from textual.containers import Container, ScrollableContainer
from textual.widgets import Static, Input, Header, Footer
from textual.reactive import reactive
from services.sysmon_widget import SysMonWidget
from typing import List, Dict
import asyncio
from datetime import datetime
@@ -659,42 +658,6 @@ class TUIFramework(App):
except Exception as e:
print(f"❌ 关闭TUI时出错: {str(e)}")
def _start_plugin_status_refresh(self):
async def refresh():
while True:
try:
ps = self.service_manager.get_service("plugin")
if ps and hasattr(ps, "plugin_status"):
lines = []
for name, status in ps.plugin_status.items():
icon = {"running":"🟢","error":"🔴","loading":"🟡","unloaded":""}.get(str(status),"")
lines.append(f"{icon} {name}: {status}")
widget = self.tui_app.query_one(SysMonWidget)
widget.update_plugins("\n".join(lines) if lines else "无已加载插件")
except: pass
await asyncio.sleep(3)
import asyncio
asyncio.create_task(refresh())
def _get_suggester(self):
from textual.suggester import Suggester
class CmdSuggester(Suggester):
async def get_suggestion(self, value):
try:
cs = self.app.query_one("#input-area").app
names = ["help","status","history","testlog","netdiag",
"permissions","pmpending","pmhelp","echo","plugin_status",
"scroll","autoscroll","create-plugin","exit","quit"]
if not value: return None
for n in names:
if n.startswith(value) and n != value:
return n
except: pass
return None
return CmdSuggester()
class TuiService:
"""TUI服务"""