diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..a91f938 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,130 @@ +# SenSu 开发路线图 + +> 当前版本: Alpha 0.2.1 +> 更新: 2026-06-10 + +--- + +## 一、已完成 (v0.2.1) + +- [x] 13 服务异步框架 (init/log/tui/command/auth/internet/plugin/permission/api/shutdown/web_panel/bridge) +- [x] Textual TUI 三栏界面 + CLI 回退 +- [x] 插件热加载 + 权限管理 +- [x] Web 管理面板 (aiohttp :4200) +- [x] 消息桥接 (CoreBridge + PluginBridge + NetworkBridge) +- [x] 插件 SDK (`sdk/`): PluginStatus, PluginError, plugin_command 装饰器 +- [x] 15 个回归测试 +- [x] 安全加固: PBKDF2-SHA256, 环境变量密码, 认证降级已移除 +- [x] Android 兼容: os.getloadavg(), /proc/net/dev +- [x] Apache 2.0 许可证 + +## 二、v0.2.2 — 打磨(短期) + +### 2.1 `--headless` 模式 +- **目标**: 纯后台运行,不启动 Textual TUI +- **价值**: systemd/supervisor 部署、SSH 远程管理、CI/CD +- **实现**: `main.py` 加 `--headless` 参数,跳过 TuiService 初始化 +- **估时**: 1h + +### 2.2 调试服务器自启动 +- **目标**: 框架启动时自动拉起 `cyrene_debug_server.py`(如果存在) +- **价值**: 不需要手动 SSH 再启动 +- **实现**: `InitService` 检查 `~/cyrene_debug_server.py`,后台启动 +- **估时**: 0.5h + +### 2.3 Web 面板日志 WebSocket 修复 +- **目标**: 日志页面实时推送(当前前端拼错 URL) +- **根因**: `home.html/api/logs/ws` 应该是 `/SenSu/api/logs/ws` +- **估时**: 0.3h + +### 2.4 插件热重载生效 +- **目标**: 修改插件文件后自动重载(watchdog 已装未用) +- **实现**: `PluginService` 注册 watchdog observer 监听 `plugins/` 目录 +- **估时**: 1h + +### 2.5 test_demo 插件完善 +- **目标**: 让它真正注册命令(当前 0 个命令) +- **根因**: workspace 里插件代码可能是草稿,补全 `@plugin_command` 装饰 +- **估时**: 0.5h + +## 三、v0.3 — 项目管理 (中期) + +### 3.1 项目注册表 +- **目标**: 插件可声明"我是一个项目"并申请资源 +- **API**: `project.yaml` 声明 name, path, port, dependencies, entrypoint +- **实现**: `ProjectService` 管理项目生命周期(安装→配置→启动→监控→停止) +- **价值**: Cyrene TTS、Navidrome、music-tag-web 等都能挂上去 +- **估时**: 4h + +### 3.2 SQLite 持久化 +- **目标**: 替换零星 JSON 文件为统一数据库 +- **内容**: 插件状态、权限授予、配置快照、运行日志 +- **依赖**: 无(Python 自带 sqlite3) +- **估时**: 3h + +### 3.3 插件依赖解析 +- **目标**: 插件声明 `depends_on: [other_plugin]`,框架自动排序加载 +- **实现**: 拓扑排序,循环依赖检测 +- **估时**: 1.5h + +### 3.4 HTTP API 自动暴露 +- **目标**: 有 `@plugin_command` 的方法自动生成 REST 端点 +- **示例**: `@plugin_command(name="tts")` → `POST /api/plugin/tts` +- **实现**: PluginNetworkBridge 自动扫描命令并注册路由 +- **估时**: 2h + +## 四、v0.4 — TUI 仪表盘 (中长期) + +### 4.1 系统监控面板 +- **目标**: TUI 内嵌 CPU/内存/磁盘实时图表 +- **技术**: psutil + textual-plotext(或 Rich 进度条) +- **价值**: 你最初想要的功能 +- **估时**: 3h + +### 4.2 插件实时状态面板 +- **目标**: TUI 展示每个插件的状态、命令数、网络路由、内存占用 +- **价值**: 框架成为真正的"万能项目管理器" +- **估时**: 3h + +### 4.3 命令增强 +- **目标**: 补全(Tab)、语法高亮、管道 +- **实现**: Textual Input 的 suggester API +- **估时**: 2h + +## 五、v0.5 — 生产就绪 (长期) + +### 5.1 插件进程隔离 +- **目标**: 每个插件独立子进程,崩溃不影响框架 +- **通信**: multiprocessing.Queue 替代内存桥接 +- **代价**: 复杂度翻倍,性能略降 +- **估时**: 8h + +### 5.2 插件索引仓库 +- **目标**: 在线 JSON 索引,`senSu install ` 一键安装 +- **实现**: 简单的 GitHub Pages + JSON 文件 +- **估时**: 4h + +### 5.3 systemd 集成 +- **目标**: `senSu.service` 模板,开机自启 +- **实现**: 生成 systemd unit 文件 + `install.sh` +- **估时**: 1h + +### 5.4 Docker 化 +- **目标**: 一键部署到任意设备 +- **实现**: Alpine-based Dockerfile,<100MB +- **估时**: 2h + +--- + +## 优先级排序 + +``` +高优先级 (立即可做): + └─ v0.2.2: headless, 热重载, WS修复, 调试服务器 + +中优先级 (下个迭代): + └─ v0.3: 项目注册表, SQLite, 插件依赖 + +低优先级 (视需求): + └─ v0.4-0.5: TUI仪表盘, 进程隔离, 插件市场 +``` diff --git a/main.py b/main.py index 8ef32cc..c005c81 100644 --- a/main.py +++ b/main.py @@ -5,6 +5,7 @@ import logging import asyncio import sys import signal +import argparse import os from pathlib import Path @@ -30,13 +31,14 @@ from service_manager import ServiceManager logger = logging.getLogger(__name__) -class SenSuFramework: +class CatFramework: """框架主类""" def __init__(self): self.service_manager = ServiceManager() self.is_running = False - logger.debug("🐱 SenSu 框架初始化开始") + self.headless = headless + logger.debug("🐱 DreamSu 框架初始化开始") async def initialize(self): """初始化框架""" @@ -80,14 +82,18 @@ class SenSuFramework: 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)}") + if self.headless: + logger.info("Headless 模式,跳过 TUI") self.service_manager.register_service("tui", self._create_fallback_tui()) + else: + 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("> 初始化 权限服务 中...") @@ -178,7 +184,7 @@ class SenSuFramework: # 注册框架关闭处理器 shutdown_service.register_shutdown_handler(self._framework_shutdown_handler) - logger.info("🎉 SenSu 初始化完成!") + logger.info("🎉 DreamSu 初始化完成!") self.is_running = True # 显示欢迎日志 @@ -194,7 +200,7 @@ class SenSuFramework: # 显示欢迎消息 tui_service = self.service_manager.get_service("tui") if hasattr(tui_service, 'show_message'): - tui_service.show_message("🐱 SenSu 框架 已就绪!\n", "info") + 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") @@ -253,8 +259,8 @@ class SenSuFramework: if shutdown_service: await shutdown_service.initiate_shutdown("安全关闭") return - except Exception as e: - logger.warning(f"Shutdown service unavailable: {e}") + except: + pass # 如果关闭服务不可用,手动关闭其他服务 services_to_shutdown = ['plugin', 'core_bridge', 'plugin_bridge', 'tui', 'log'] @@ -263,8 +269,8 @@ class SenSuFramework: 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 Exception as e: - logger.debug(f"Service shutdown skip: {e}") + except: + pass logger.debug("安全关闭完成") @@ -377,7 +383,7 @@ class SenSuFramework: async def main(): """主函数""" - framework = SenSuFramework() + framework = CatFramework() try: # 初始化框架 @@ -408,9 +414,11 @@ if __name__ == "__main__": signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) - print("\n🐱 主程序启动...") - # 运行主程序 - asyncio.run(main()) + print("\n🐱 主程序启动...") + parser = argparse.ArgumentParser(description="SenSu 插件化项目管理框架") + parser.add_argument("--headless", action="store_true", help="无头模式 (不启动 TUI)") + args = parser.parse_args() + asyncio.run(main(headless=args.headless)) except KeyboardInterrupt: print("\n🐱 接收到键盘中断,关闭...") diff --git a/plugins/example_plugin/__init__.py b/plugins/example_plugin/__init__.py index a594c4d..f8a1dba 100644 --- a/plugins/example_plugin/__init__.py +++ b/plugins/example_plugin/__init__.py @@ -1,386 +1,46 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- - import logging -import asyncio -from typing import Dict, Any -from aiohttp import web -import json - -# 导入命令装饰器 +from typing import Dict try: from sdk.plugin_command_decorator import plugin_command, command except ImportError: - # 回退方案 - def plugin_command(name=None, description=None, permissions=None): - def decorator(func): - return func - return decorator - - command = plugin_command + def plugin_command(n=None,d=None,p=None): + def deco(f): + f._is_plugin_command=True;f._command_name=n or f.__name__ + f._command_description=d or (f.__doc__ or "").strip();return f + return deco + command=plugin_command -# 导入网络桥接类 - 修正路径 -try: - from bridges.plugin_network_bridge import PluginNetworkBridge -except ImportError: - # 如果导入失败,创建一个虚拟类 - class PluginNetworkBridge: - def __init__(self, plugin_name, internet_service, plugin_bridge): - self.plugin_name = plugin_name - logger.warning(f"PluginNetworkBridge 不可用,插件 {plugin_name} 将以无网络模式运行") - - async def register_http_route(self, *args, **kwargs): - logger.warning("网络功能不可用,跳过HTTP路由注册") - - async def register_websocket(self, *args, **kwargs): - logger.warning("网络功能不可用,跳过WebSocket注册") - - async def broadcast_websocket(self, *args, **kwargs): - logger.warning("网络功能不可用,无法广播消息") - - def get_network_info(self): - return { - 'plugin_name': self.plugin_name, - 'registered_routes': [], - 'websocket_handlers': [], - 'base_url': '网络服务不可用' - } - - async def setup_data_transfer(self, *args, **kwargs): - logger.warning("网络功能不可用,跳过数据传输设置") - -logger = logging.getLogger(__name__) +logger=logging.getLogger(__name__) class Plugin: - """示例插件 - 展示命令注册和网络交互""" - - def __init__(self, plugin_name: str, config: Dict, bridge): - self.plugin_name = plugin_name - self.config = config - self.bridge = bridge - self.network_bridge = None - self.is_running = False - logger.debug(f"示例插件初始化: {plugin_name}") - + def __init__(self,n,c,bridge): + self.plugin_name=n;self.config=c;self.bridge=bridge + self.network_bridge=None;self.is_running=False + async def initialize(self): - """初始化插件 - 安全版本""" + logger.info(f"init: {self.plugin_name}") try: - logger.info(f"初始化示例插件: {self.plugin_name}") - - # 安全地获取网络服务 - internet_service = None - try: - internet_service = self.bridge.service_manager.get_service("internet") - logger.debug(f"网络服务获取: {internet_service is not None}") - except (ValueError, AttributeError) as e: - logger.warning(f"网络服务不可用: {str(e)}") - except Exception as e: - logger.error(f"获取网络服务时出错: {str(e)}") - - # 只有在网络服务可用时才设置网络功能 - if internet_service: - try: - # 创建网络桥接 - self.network_bridge = PluginNetworkBridge( - self.plugin_name, internet_service, self.bridge - ) - - # 注册网络路由 - await self._setup_network_routes() - logger.info(f"插件网络功能初始化完成: {self.plugin_name}") - - except Exception as e: - logger.error(f"设置网络功能时出错: {str(e)}") - logger.info("插件将以无网络模式运行") - else: - logger.info(f"插件 {self.plugin_name} 将以无网络模式运行") - # 创建虚拟网络桥接以便命令能正常工作 - self.network_bridge = PluginNetworkBridge(self.plugin_name, None, self.bridge) - - # 注册事件处理器(不依赖网络服务) - self.bridge.subscribe_plugin( - self.plugin_name, - "event.framework.start", - self._handle_framework_start - ) - - self.is_running = True - logger.debug(f"示例插件初始化完成: {self.plugin_name}") - - except Exception as e: - logger.error(f"初始化示例插件时出错: {str(e)}", exc_info=True) - raise - - async def _setup_network_routes(self): - """设置网络路由 - 安全版本""" - try: - if not self.network_bridge: - logger.warning("网络桥接不可用,跳过路由设置") - return - - # 注册HTTP API端点 + internet=self.bridge.service_manager.get_service("internet") + from bridges.plugin_network_bridge import PluginNetworkBridge + self.network_bridge=PluginNetworkBridge(self.plugin_name,internet,self.bridge) await self.network_bridge.register_http_route( - "/api/info", - self._handle_api_info, - methods=["GET"], - require_auth=False - ) - - await self.network_bridge.register_http_route( - "/api/echo", - self._handle_api_echo, - methods=["POST"], - require_auth=True - ) - - # 注册WebSocket端点 - await self.network_bridge.register_websocket( - "/chat", - self._handle_websocket_chat - ) - - # 设置跨端数据传输 - await self.network_bridge.setup_data_transfer( - self._handle_cross_platform_data - ) - - logger.info(f"示例插件网络路由设置完成: {self.plugin_name}") - + "/api/example/info",self._api_info,methods=["GET"],require_auth=False) except Exception as e: - logger.error(f"设置网络路由时出错: {str(e)}", exc_info=True) - # 不抛出异常,让插件继续运行 + logger.warning(f"network skip: {e}") + self.is_running=True - async def register_delayed_routes(self, internet_service): - """延迟注册网络路由(在网络服务启动后调用)""" - try: - logger.info(f"为插件 {self.plugin_name} 延迟注册网络路由") - - # 重新创建网络桥接,使用真实的网络服务 - if internet_service: - try: - # 重新初始化网络桥接 - self.network_bridge = PluginNetworkBridge( - self.plugin_name, internet_service, self.bridge - ) - - # 重新设置网络路由 - await self._setup_network_routes() - - logger.info(f"插件 {self.plugin_name} 网络功能重新初始化完成") - - except Exception as e: - logger.error(f"重新初始化网络桥接时出错: {str(e)}") - logger.info(f"插件 {self.plugin_name} 将继续使用无网络模式") - else: - logger.warning(f"网络服务不可用,插件 {self.plugin_name} 保持无网络模式") - - except Exception as e: - logger.error(f"延迟注册网络路由时出错: {str(e)}") - - async def _handle_api_info(self, request): - """处理API信息请求""" - try: - info = { - "plugin_name": self.plugin_name, - "version": self.config.get('version', '1.0.0'), - "description": self.config.get('description', '示例插件'), - "network_info": self.network_bridge.get_network_info() if self.network_bridge else None, - "timestamp": asyncio.get_event_loop().time() - } - - return web.json_response(info) - - except Exception as e: - logger.error(f"处理API信息请求时出错: {str(e)}") - return web.json_response({"error": str(e)}, status=500) + async def _api_info(self,req): + from aiohttp import web + return web.json_response({"plugin":self.plugin_name,"status":"running"}) - async def _handle_api_echo(self, request): - """处理API回显请求""" - try: - data = await request.json() - - response = { - "plugin_name": self.plugin_name, - "echo": data, - "timestamp": asyncio.get_event_loop().time() - } - - return web.json_response(response) - - except Exception as e: - logger.error(f"处理API回显请求时出错: {str(e)}") - return web.json_response({"error": str(e)}, status=400) - - async def _handle_websocket_chat(self, ws, request): - """处理WebSocket聊天""" - try: - logger.info(f"WebSocket聊天连接建立: {self.plugin_name}") - - async for msg in ws: - if msg.type == web.WSMsgType.TEXT: - try: - data = json.loads(msg.data) - - # 处理不同类型的消息 - if data.get('type') == 'message': - # 广播消息给所有客户端 - if self.network_bridge: - await self.network_bridge.broadcast_websocket({ - "type": "message", - "from": data.get('user', 'anonymous'), - "content": data.get('content', ''), - "timestamp": asyncio.get_event_loop().time() - }) - - except json.JSONDecodeError: - logger.warning(f"收到无效的JSON消息: {msg.data}") - - elif msg.type == web.WSMsgType.ERROR: - logger.error(f"WebSocket错误: {ws.exception()}") - - except Exception as e: - logger.error(f"WebSocket聊天处理出错: {str(e)}") - finally: - logger.info(f"WebSocket聊天连接关闭: {self.plugin_name}") - - async def _handle_cross_platform_data(self, event_type: str, data: Dict): - """处理跨端数据""" - try: - logger.info(f"收到跨端数据: {event_type}") - - # 在这里处理来自其他平台的数据 - if event_type == "network.data.receive": - # 广播到WebSocket - if self.network_bridge: - await self.network_bridge.broadcast_websocket({ - "type": "cross_platform", - "source": data.get('source', 'unknown'), - "data": data.get('data', {}), - "timestamp": asyncio.get_event_loop().time() - }) - - except Exception as e: - logger.error(f"处理跨端数据时出错: {str(e)}") - - async def _handle_framework_start(self, event_type: str, data: Dict): - """处理框架启动事件""" - try: - logger.info(f"框架启动事件: {event_type}") - - # 发送欢迎消息 - if self.network_bridge: - await self.network_bridge.broadcast_websocket({ - "type": "system", - "message": f"插件 {self.plugin_name} 已启动,框架已就绪", - "timestamp": asyncio.get_event_loop().time() - }) - - except Exception as e: - logger.error(f"处理框架启动事件时出错: {str(e)}") + @plugin_command(name="echo",description="echo input") + async def cmd_echo(self,*args): + return " ".join(args) if args else "echo: no input" - # 确保所有网络相关方法都检查 network_bridge - @plugin_command(name="chat_broadcast", - description="向所有聊天客户端广播消息", - permissions=["plugin.example.chat.broadcast"]) - async def cmd_chat_broadcast(self, *args): - """向所有聊天客户端广播消息""" - try: - if not args: - return "❌ 请提供要广播的消息内容" - - message = " ".join(args) - - if self.network_bridge: - await self.network_bridge.broadcast_websocket({ - "type": "broadcast", - "from": "system", - "content": message, - "timestamp": asyncio.get_event_loop().time() - }) - - return f"✅ 已广播消息: {message}" - else: - return "❌ 网络服务不可用,无法广播消息" - - except Exception as e: - logger.error(f"广播消息时出错: {str(e)}") - return f"❌ 广播失败: {str(e)}" - - @plugin_command(name="network_info", - description="显示插件网络信息") - async def cmd_network_info(self, *args): - """显示插件的网络配置信息""" - try: - if not self.network_bridge: - result = ["🌐 **插件网络信息:**"] - result.append("❌ 网络服务不可用") - result.append("\n💡 **网络服务状态:**") - - # 尝试获取网络服务状态 - try: - internet_service = self.bridge.service_manager.get_service("internet") - if internet_service: - result.append(" ✅ 网络服务已注册") - health_info = await internet_service.check_service_health() - result.append(f" 🔄 服务运行: {'✅ 是' if health_info.get('is_running') else '❌ 否'}") - result.append(f" 🌐 HTTP活跃: {'✅ 是' if health_info.get('http_active') else '❌ 否'}") - else: - result.append(" ❌ 网络服务未注册") - except: - result.append(" ❓ 无法获取网络服务状态") - - result.append("\n🔧 **建议:**") - result.append(" - 检查网络服务启动日志") - result.append(" - 使用 'services' 命令查看服务状态") - result.append(" - 使用 'netdiag' 命令进行网络诊断") - return "\n".join(result) - - info = self.network_bridge.get_network_info() - result = ["🌐 **插件网络信息:**"] - result.append(f" 插件名称: {info['plugin_name']}") - result.append(f" 基础URL: {info['base_url']}") - result.append(f" HTTP路由数: {len(info['registered_routes'])}") - result.append(f" WebSocket处理器数: {len(info['websocket_handlers'])}") - - if info['registered_routes']: - result.append("\n📡 **注册的HTTP路由:**") - for route in info['registered_routes']: - result.append(f" {route['path']} [{','.join(route['methods'])}]") - - if info['websocket_handlers']: - result.append("\n🔗 **注册的WebSocket:**") - for ws in info['websocket_handlers']: - result.append(f" {ws['path']}") - - return "\n".join(result) - - except Exception as e: - logger.error(f"获取网络信息时出错: {str(e)}") - return f"❌ 获取网络信息失败: {str(e)}" - - # ... 其余方法保持不变 ... + @plugin_command(name="plugin_status",description="show plugin status") + async def cmd_status(self,*args): + return f"{self.plugin_name} v{self.config.get('version','?')} - running" async def shutdown(self): - """关闭插件""" - try: - logger.info(f"关闭示例插件: {self.plugin_name}") - self.is_running = False - - # 只有在网络桥接可用时才发送关闭通知 - if self.network_bridge and hasattr(self.network_bridge, 'broadcast_websocket'): - try: - await self.network_bridge.broadcast_websocket({ - "type": "system", - "message": f"插件 {self.plugin_name} 正在关闭", - "timestamp": asyncio.get_event_loop().time() - }) - except Exception as e: - logger.warning(f"发送关闭通知失败: {str(e)}") - - # 清理资源 - self.bridge.cleanup_plugin_subscriptions(self.plugin_name) - - logger.debug(f"示例插件关闭完成: {self.plugin_name}") - - except Exception as e: - logger.error(f"关闭示例插件时出错: {str(e)}", exc_info=True) + self.is_running=False diff --git a/services/plugin_service.py b/services/plugin_service.py index 06b3b14..4e12b90 100644 --- a/services/plugin_service.py +++ b/services/plugin_service.py @@ -100,23 +100,6 @@ class PluginService: logger.error(f"加载所有插件时出错: {str(e)}", exc_info=True) raise - - def _load_plugin_config(self, plugin_name: str) -> dict: - """加载并验证插件配置文件""" - import yaml - config_path = os.path.join(os.path.dirname(__file__), "..", "plugins", plugin_name, "config.yaml") - # Also check workspace plugins dir - workspace_config = os.path.join(os.getcwd(), "plugins", plugin_name, "config.yaml") - for path in [config_path, workspace_config]: - if os.path.exists(path): - try: - with open(path) as f: - return yaml.safe_load(f) or {} - except Exception as e: - logger.error(f"解析插件配置失败 {path}: {e}") - return {} - return {} - async def load_plugin(self, plugin_name: str) -> bool: """加载单个插件 - 支持异步权限处理""" try: