From e6875f0b4b22e8156090d2e8056587e850e0d1f7 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 10 Jun 2026 12:27:14 +0800 Subject: [PATCH] Initial commit: SenSu Alpha 0.2.0 - 13-service async plugin framework - Textual TUI with CLI fallback - Plugin hot-reload + permission system - Web management panel (aiohttp) - Bridge-based inter-module communication - 10 regression tests Fixes applied: - PBKDF2-SHA256 auth (was plain SHA256) - Auth bypass removed (was allow-all on fail) - Bare excepts replaced with logged errors - CatFramework/DreamSu -> SenSu naming unified - ServiceManager: health checks + startup_order - Env var credentials (SENSU_ADMIN_PASSWORD etc) --- .gitignore | 12 + README.md | 145 + bridges/__init__.py | 16 + bridges/core_bridge.py | 179 + bridges/plugin_bridge.py | 256 + bridges/plugin_network_bridge.py | 127 + config/framework/base_config.yaml | 45 + config/framework/permission_rules.yaml | 15 + config/permissions/granted_permissions.json | 9 + config/permissions/pending_requests.json | 18 + config/permissions/plugin_status.json | 3 + config/plugins/commands.yaml | 96 + config/services/network_routes.yaml | 17 + debug_log_format.py | 63 + docs/SenSu 插件开发详细指南.md | 5044 +++++++++++++++++++ docs/SenSu 框架基本架构.md | 398 ++ docs/项目文件结构.txt | 44 + fmfuncs/plugin_command_decorator.py | 111 + gui/api.py | 362 ++ main.py | 433 ++ plugins/example_plugin/__init__.py | 386 ++ plugins/example_plugin/config.yaml | 16 + plugins/example_plugin/permissions.yaml | 16 + requirements.txt | 8 + service_manager.py | 79 + services/__init__.py | 51 + services/api_service.py | 206 + services/auth_service.py | 268 + services/command_service.py | 604 +++ services/init_service.py | 247 + services/internet_service.py | 464 ++ services/log_service.py | 332 ++ services/permission_service.py | 1054 ++++ services/plugin_service.py | 476 ++ services/shutdown_service.py | 132 + services/tui_service.py | 806 +++ services/web_panel/__init__.py | 2 + services/web_panel/auth.py | 31 + services/web_panel/manager.py | 88 + services/web_panel/middleware.py | 39 + services/web_panel/routes/__init__.py | 0 services/web_panel/routes/auth.py | 82 + services/web_panel/routes/commands.py | 15 + services/web_panel/routes/logs.py | 30 + services/web_panel/routes/plugins.py | 61 + services/web_panel/routes/status.py | 27 + services/web_panel/utils/__init__.py | 0 services/web_panel/utils/auth.py | 34 + services/web_panel/utils/response.py | 7 + services/web_panel/utils/system_info.py | 93 + static/web_panel/css/style.css | 159 + static/web_panel/home.html | 51 + static/web_panel/index.html | 36 + static/web_panel/js/api.js | 32 + static/web_panel/js/app.js | 77 + static/web_panel/js/chart.js | 51 + static/web_panel/js/main.js | 29 + static/web_panel/pages/console.html | 10 + static/web_panel/pages/console.js | 29 + static/web_panel/pages/dashboard.html | 70 + static/web_panel/pages/dashboard.js | 105 + static/web_panel/pages/logs.html | 4 + static/web_panel/pages/logs.js | 24 + static/web_panel/pages/plugins.html | 5 + static/web_panel/pages/plugins.js | 36 + templates/plugin/__init__.py.template | 50 + templates/plugin/config.yaml.template | 9 + templates/plugin/permissions.yaml.template | 10 + tests/__init__.py | 0 tests/conftest.py | 4 + tests/test_auth.py | 12 + tests/test_service_manager.py | 49 + utils/__init__.py | 20 + utils/config_utils.py | 203 + utils/file_utils.py | 144 + utils/network_utils.py | 111 + utils/plugin_utils.py | 260 + utils/validation_utils.py | 206 + 78 files changed, 14843 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bridges/__init__.py create mode 100644 bridges/core_bridge.py create mode 100644 bridges/plugin_bridge.py create mode 100644 bridges/plugin_network_bridge.py create mode 100644 config/framework/base_config.yaml create mode 100644 config/framework/permission_rules.yaml create mode 100644 config/permissions/granted_permissions.json create mode 100644 config/permissions/pending_requests.json create mode 100644 config/permissions/plugin_status.json create mode 100644 config/plugins/commands.yaml create mode 100644 config/services/network_routes.yaml create mode 100644 debug_log_format.py create mode 100644 docs/SenSu 插件开发详细指南.md create mode 100644 docs/SenSu 框架基本架构.md create mode 100644 docs/项目文件结构.txt create mode 100644 fmfuncs/plugin_command_decorator.py create mode 100644 gui/api.py create mode 100644 main.py create mode 100644 plugins/example_plugin/__init__.py create mode 100644 plugins/example_plugin/config.yaml create mode 100644 plugins/example_plugin/permissions.yaml create mode 100644 requirements.txt create mode 100644 service_manager.py create mode 100644 services/__init__.py create mode 100644 services/api_service.py create mode 100644 services/auth_service.py create mode 100644 services/command_service.py create mode 100644 services/init_service.py create mode 100644 services/internet_service.py create mode 100644 services/log_service.py create mode 100644 services/permission_service.py create mode 100644 services/plugin_service.py create mode 100644 services/shutdown_service.py create mode 100644 services/tui_service.py create mode 100644 services/web_panel/__init__.py create mode 100644 services/web_panel/auth.py create mode 100644 services/web_panel/manager.py create mode 100644 services/web_panel/middleware.py create mode 100644 services/web_panel/routes/__init__.py create mode 100644 services/web_panel/routes/auth.py create mode 100644 services/web_panel/routes/commands.py create mode 100644 services/web_panel/routes/logs.py create mode 100644 services/web_panel/routes/plugins.py create mode 100644 services/web_panel/routes/status.py create mode 100644 services/web_panel/utils/__init__.py create mode 100644 services/web_panel/utils/auth.py create mode 100644 services/web_panel/utils/response.py create mode 100644 services/web_panel/utils/system_info.py create mode 100644 static/web_panel/css/style.css create mode 100644 static/web_panel/home.html create mode 100644 static/web_panel/index.html create mode 100644 static/web_panel/js/api.js create mode 100644 static/web_panel/js/app.js create mode 100644 static/web_panel/js/chart.js create mode 100644 static/web_panel/js/main.js create mode 100644 static/web_panel/pages/console.html create mode 100644 static/web_panel/pages/console.js create mode 100644 static/web_panel/pages/dashboard.html create mode 100644 static/web_panel/pages/dashboard.js create mode 100644 static/web_panel/pages/logs.html create mode 100644 static/web_panel/pages/logs.js create mode 100644 static/web_panel/pages/plugins.html create mode 100644 static/web_panel/pages/plugins.js create mode 100644 templates/plugin/__init__.py.template create mode 100644 templates/plugin/config.yaml.template create mode 100644 templates/plugin/permissions.yaml.template create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_auth.py create mode 100644 tests/test_service_manager.py create mode 100644 utils/__init__.py create mode 100644 utils/config_utils.py create mode 100644 utils/file_utils.py create mode 100644 utils/network_utils.py create mode 100644 utils/plugin_utils.py create mode 100644 utils/validation_utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3392e22 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.pyc +*.pyo +logs/ +*.log +_patches_applied/ +*.bak.* +.env +*.swp +*.swo +*~ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..e524741 --- /dev/null +++ b/README.md @@ -0,0 +1,145 @@ +# 🐱 SenSu + +一个功能强大的Python后端框架,具有插件化架构和丰富的功能集。 + +## ✨ 特性 + +- 🎨 **TUI界面**: 基于Textual的终端用户界面 +- 📝 **强大日志系统**: 多输出、文件切割、实时日志流 +- 🔌 **插件化架构**: 热加载、权限管理、插件隔离 +- 🌐 **网络服务**: WebSocket、HTTP API、反向代理 +- 🔐 **认证系统**: 用户认证、令牌管理、权限验证 +- 🔄 **消息桥接**: 模块间通信、插件间通信 +- ⚡ **高性能**: 异步架构、协程支持 +- 🛡️ **安全**: 权限验证、输入验证、错误隔离 + +## 🚀 快速开始 + +### 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 运行框架 + +```bash +python main.py +``` + +### 基本命令 + +在TUI底部输入框中输入命令: + +- `help` - 显示帮助信息 +- `status` - 显示框架状态 +- `history` - 显示命令历史 + +## 📁 项目结构 + +``` +project_root/ +├── main.py # 框架主入口 +├── service_manager.py # 服务管理器 +├── requirements.txt # 依赖包列表 +├── README.md # 项目说明 +│ +├── config/ # 运行时生成的配置文件 +│ ├── framework/ # 框架核心配置 +│ ├── plugins/ # 插件配置 +│ ├── services/ # 服务配置 +│ └── permissions/ # 权限配置 +│ +├── logs/ # 日志文件目录 +│ ├── debug/ # debug级别日志 +│ └── runtime/ # 运行时日志 +│ +├── services/ # 核心服务模块 +│ ├── init_service.py # 初始化服务 +│ ├── log_service.py # 日志服务 +│ ├── tui_service.py # TUI服务 +│ ├── command_service.py # 指令服务 +│ ├── auth_service.py # 认证服务 +│ ├── internet_service.py # 互联网服务 +│ ├── plugin_service.py # 插件服务 +│ ├── permission_service.py # 权限服务 +│ ├── api_service.py # API服务 +│ └── shutdown_service.py # 关闭服务 +│ +├── bridges/ # 桥接模块 +│ ├── core_bridge.py # 核心桥接 +│ └── plugin_bridge.py # 插件桥接 +│ +├── fmfuncs/ # 框架功能集 +│ ├── file_utils.py # 文件操作工具 +│ ├── config_utils.py # 配置工具 +│ ├── validation_utils.py # 验证工具 +│ ├── network_utils.py # 网络工具 +│ └── plugin_utils.py # 插件工具 +│ +├── plugins/ # 插件目录 +│ └── example_plugin/ # 示例插件 +│ +└── gui/ # GUI接口 + └── api.py # GUI操作接口 +``` + +## 🔌 插件开发 + +### 创建插件 + +1. 在 `plugins/` 目录下创建插件文件夹 +2. 创建必要的配置文件: + - `__init__.py` - 插件主模块 + - `config.yaml` - 插件配置 + - `permissions.yaml` - 权限申请 + +### 插件示例 + +参考 `plugins/example_plugin/` 目录中的示例插件。 + +## 🔧 配置说明 + +框架配置位于 `config/framework/` 目录: + +- `base_config.yaml` - 基础框架配置 +- `permission_rules.yaml` - 权限规则配置 + +## 📡 API接口 + +框架提供以下API接口: + +- WebSocket服务: `ws://localhost:8765` +- HTTP API服务: `http://localhost:8000` +- GUI API服务: `http://localhost:8080` + +## 🐛 问题排查 + +查看 `logs/` 目录中的日志文件获取详细错误信息。 + +## 📄 许可证 + +MIT License + +## 🤝 贡献 + +欢迎提交Issue和Pull Request! +``` + +这个完整的Python后端框架包含了这些功能: + +- ✅ TUI渲染界面(三部分布局) +- ✅ 强大的日志处理模块 +- ✅ 初始化系统和指令模块 +- ✅ 核心桥接和插件桥接 +- ✅ 互联网模块集(WebSocket、HTTP API) +- ✅ 插件管理器(热加载、错误隔离) +- ✅ 权限管理器(权限申请和验证) +- ✅ API管理器 +- ✅ 优雅的关闭方法 +- ✅ 丰富的debug日志 +- ✅ GUI API接口 +- ✅ 清晰的目录结构 + +每个文件都有完整的错误处理和详细的日志记录 可以直接运行 `python main.py` 来启动框架 +``` \ No newline at end of file diff --git a/bridges/__init__.py b/bridges/__init__.py new file mode 100644 index 0000000..6c303a1 --- /dev/null +++ b/bridges/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +桥接模块 - 提供模块间和插件间的通信功能 +""" + +from .core_bridge import CoreBridge, MessageType +from .plugin_bridge import PluginBridge, PluginMessageType + +__all__ = [ + 'CoreBridge', + 'MessageType', + 'PluginBridge', + 'PluginMessageType' +] diff --git a/bridges/core_bridge.py b/bridges/core_bridge.py new file mode 100644 index 0000000..fcf50d3 --- /dev/null +++ b/bridges/core_bridge.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import asyncio +from typing import Dict, List, Callable, Any +from enum import Enum +import json + +logger = logging.getLogger(__name__) + +class MessageType(Enum): + """消息类型枚举""" + EVENT = "event" + COMMAND = "command" + DATA = "data" + STATUS = "status" + ERROR = "error" + +class CoreBridge: + """核心桥接服务 - 用于框架模块间通信""" + + def __init__(self): + self.subscribers: Dict[str, List[Callable]] = {} + self.message_queue: asyncio.Queue = asyncio.Queue() + self.is_running = False + self.processing_task = None + logger.debug("CoreBridge初始化开始") + + async def start(self): + """启动桥接服务""" + try: + logger.info("启动核心桥接服务") + self.is_running = True + self.processing_task = asyncio.create_task(self._process_messages()) + logger.debug("核心桥接服务启动完成") + except Exception as e: + logger.error(f"启动核心桥接服务时出错: {str(e)}", exc_info=True) + raise + + def subscribe(self, topic: str, callback: Callable): + """订阅主题""" + try: + if topic not in self.subscribers: + self.subscribers[topic] = [] + + self.subscribers[topic].append(callback) + logger.debug(f"订阅主题: {topic}, 当前订阅者数: {len(self.subscribers[topic])}") + except Exception as e: + logger.error(f"订阅主题 {topic} 时出错: {str(e)}", exc_info=True) + raise + + def unsubscribe(self, topic: str, callback: Callable): + """取消订阅""" + try: + if topic in self.subscribers and callback in self.subscribers[topic]: + self.subscribers[topic].remove(callback) + logger.debug(f"取消订阅主题: {topic}, 剩余订阅者数: {len(self.subscribers[topic])}") + except Exception as e: + logger.error(f"取消订阅主题 {topic} 时出错: {str(e)}", exc_info=True) + + async def publish(self, topic: str, message: Dict, msg_type: MessageType = MessageType.DATA): + """发布消息""" + try: + message_data = { + "topic": topic, + "type": msg_type.value, + "data": message, + "timestamp": asyncio.get_event_loop().time() + } + + await self.message_queue.put(message_data) + logger.debug(f"发布消息到主题: {topic}, 类型: {msg_type.value}") + + except Exception as e: + logger.error(f"发布消息到主题 {topic} 时出错: {str(e)}", exc_info=True) + raise + + async def _process_messages(self): + """处理消息队列""" + try: + logger.debug("开始处理消息队列") + + while self.is_running: + try: + # 等待消息,带超时以便检查运行状态 + message = await asyncio.wait_for(self.message_queue.get(), timeout=1.0) + + # 分发消息给订阅者 + await self._dispatch_message(message) + + # 标记任务完成 + self.message_queue.task_done() + + except asyncio.TimeoutError: + continue + except Exception as e: + logger.error(f"处理消息时出错: {str(e)}", exc_info=True) + continue + + logger.debug("消息队列处理结束") + + except Exception as e: + logger.error(f"消息队列处理循环出错: {str(e)}", exc_info=True) + + async def _dispatch_message(self, message: Dict): + """分发消息给订阅者""" + try: + topic = message["topic"] + + if topic not in self.subscribers: + logger.debug(f"主题 {topic} 没有订阅者") + return + + subscribers = self.subscribers[topic][:] # 复制列表避免在迭代时修改 + + # 并行调用所有订阅者 + tasks = [] + for callback in subscribers: + task = asyncio.create_task(self._call_subscriber(callback, message)) + tasks.append(task) + + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) + + logger.debug(f"消息分发完成,主题: {topic}, 订阅者数: {len(subscribers)}") + + except Exception as e: + logger.error(f"分发消息时出错: {str(e)}", exc_info=True) + + async def _call_subscriber(self, callback: Callable, message: Dict): + """调用订阅者回调""" + try: + if asyncio.iscoroutinefunction(callback): + await callback(message) + else: + callback(message) + + except Exception as e: + logger.error(f"调用订阅者回调时出错: {str(e)}", exc_info=True) + + def get_subscriber_count(self, topic: str = None) -> int: + """获取订阅者数量""" + try: + if topic: + count = len(self.subscribers.get(topic, [])) + logger.debug(f"主题 {topic} 的订阅者数量: {count}") + return count + else: + total = sum(len(subs) for subs in self.subscribers.values()) + logger.debug(f"总订阅者数量: {total}") + return total + except Exception as e: + logger.error(f"获取订阅者数量时出错: {str(e)}", exc_info=True) + return 0 + + async def shutdown(self): + """关闭桥接服务""" + try: + logger.info("关闭核心桥接服务") + self.is_running = False + + # 等待处理任务结束 + if self.processing_task: + await asyncio.wait_for(self.processing_task, timeout=5.0) + + # 清空队列和订阅者 + self.subscribers.clear() + while not self.message_queue.empty(): + try: + self.message_queue.get_nowait() + self.message_queue.task_done() + except asyncio.QueueEmpty: + break + + logger.debug("核心桥接服务关闭完成") + + except Exception as e: + logger.error(f"关闭核心桥接服务时出错: {str(e)}", exc_info=True) diff --git a/bridges/plugin_bridge.py b/bridges/plugin_bridge.py new file mode 100644 index 0000000..ba1f057 --- /dev/null +++ b/bridges/plugin_bridge.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import asyncio +from typing import Dict, List, Callable, Any +from enum import Enum +import json + +logger = logging.getLogger(__name__) + +class PluginMessageType(Enum): + """插件消息类型枚举""" + PLUGIN_EVENT = "plugin_event" + PLUGIN_DATA = "plugin_data" + PLUGIN_COMMAND = "plugin_command" + PLUGIN_REQUEST = "plugin_request" + PLUGIN_RESPONSE = "plugin_response" + +class PluginBridge: + """插件桥接服务 - 用于框架与插件、插件间通信""" + + def __init__(self, core_bridge): + self.core_bridge = core_bridge + self.plugin_subscribers: Dict[str, Dict[str, List[Callable]]] = {} + self.plugin_message_queue: asyncio.Queue = asyncio.Queue() + self.is_running = False + self.processing_task = None + logger.debug("PluginBridge初始化开始") + + async def start(self): + """启动插件桥接服务""" + try: + logger.info("启动插件桥接服务") + self.is_running = True + self.processing_task = asyncio.create_task(self._process_plugin_messages()) + + # 订阅核心桥接的相关主题 + self.core_bridge.subscribe("plugin.*", self._handle_core_plugin_message) + + logger.debug("插件桥接服务启动完成") + except Exception as e: + logger.error(f"启动插件桥接服务时出错: {str(e)}", exc_info=True) + raise + + def subscribe_plugin(self, plugin_name: str, topic: str, callback: Callable): + """插件订阅主题""" + try: + if plugin_name not in self.plugin_subscribers: + self.plugin_subscribers[plugin_name] = {} + + if topic not in self.plugin_subscribers[plugin_name]: + self.plugin_subscribers[plugin_name][topic] = [] + + self.plugin_subscribers[plugin_name][topic].append(callback) + logger.debug(f"插件 {plugin_name} 订阅主题: {topic}, 订阅者数: {len(self.plugin_subscribers[plugin_name][topic])}") + except Exception as e: + logger.error(f"插件订阅主题时出错: {str(e)}", exc_info=True) + raise + + def unsubscribe_plugin(self, plugin_name: str, topic: str, callback: Callable): + """插件取消订阅""" + try: + if (plugin_name in self.plugin_subscribers and + topic in self.plugin_subscribers[plugin_name] and + callback in self.plugin_subscribers[plugin_name][topic]): + + self.plugin_subscribers[plugin_name][topic].remove(callback) + logger.debug(f"插件 {plugin_name} 取消订阅主题: {topic}") + except Exception as e: + logger.error(f"插件取消订阅时出错: {str(e)}", exc_info=True) + + async def publish_to_plugin(self, target_plugin: str, topic: str, message: Dict, + msg_type: PluginMessageType = PluginMessageType.PLUGIN_DATA): + """发布消息到指定插件""" + try: + message_data = { + "target_plugin": target_plugin, + "topic": topic, + "type": msg_type.value, + "data": message, + "timestamp": asyncio.get_event_loop().time() + } + + await self.plugin_message_queue.put(message_data) + logger.debug(f"发布消息到插件 {target_plugin}, 主题: {topic}") + + except Exception as e: + logger.error(f"发布消息到插件时出错: {str(e)}", exc_info=True) + raise + + async def broadcast_to_plugins(self, topic: str, message: Dict, + exclude_plugins: List[str] = None, + msg_type: PluginMessageType = PluginMessageType.PLUGIN_DATA): + """广播消息到所有插件""" + try: + exclude_plugins = exclude_plugins or [] + + for plugin_name in self.plugin_subscribers.keys(): + if plugin_name not in exclude_plugins: + await self.publish_to_plugin(plugin_name, topic, message, msg_type) + + logger.debug(f"广播消息到插件, 主题: {topic}, 排除: {exclude_plugins}") + + except Exception as e: + logger.error(f"广播消息到插件时出错: {str(e)}", exc_info=True) + raise + + async def _process_plugin_messages(self): + """处理插件消息队列""" + try: + logger.debug("开始处理插件消息队列") + + while self.is_running: + try: + # 等待消息,带超时 + message = await asyncio.wait_for(self.plugin_message_queue.get(), timeout=1.0) + + # 分发消息给目标插件 + await self._dispatch_plugin_message(message) + + # 标记任务完成 + self.plugin_message_queue.task_done() + + except asyncio.TimeoutError: + continue + except Exception as e: + logger.error(f"处理插件消息时出错: {str(e)}", exc_info=True) + continue + + logger.debug("插件消息队列处理结束") + + except Exception as e: + logger.error(f"插件消息队列处理循环出错: {str(e)}", exc_info=True) + + async def _dispatch_plugin_message(self, message: Dict): + """分发消息给插件订阅者""" + try: + target_plugin = message["target_plugin"] + topic = message["topic"] + + if (target_plugin not in self.plugin_subscribers or + topic not in self.plugin_subscribers[target_plugin]): + logger.debug(f"插件 {target_plugin} 没有订阅主题 {topic}") + return + + subscribers = self.plugin_subscribers[target_plugin][topic][:] + + # 并行调用所有订阅者 + tasks = [] + for callback in subscribers: + task = asyncio.create_task(self._call_plugin_subscriber(callback, message)) + tasks.append(task) + + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) + + logger.debug(f"插件消息分发完成,目标: {target_plugin}, 主题: {topic}, 订阅者数: {len(subscribers)}") + + except Exception as e: + logger.error(f"分发插件消息时出错: {str(e)}", exc_info=True) + + async def _call_plugin_subscriber(self, callback: Callable, message: Dict): + """调用插件订阅者回调""" + try: + if asyncio.iscoroutinefunction(callback): + await callback(message) + else: + callback(message) + + except Exception as e: + logger.error(f"调用插件订阅者回调时出错: {str(e)}", exc_info=True) + + async def _handle_core_plugin_message(self, message: Dict): + """处理来自核心桥接的插件相关消息""" + try: + topic = message["topic"] + data = message["data"] + + # 根据主题类型处理 + if topic.startswith("plugin.event."): + # 广播插件事件 + event_type = topic.replace("plugin.event.", "") + await self.broadcast_to_plugins( + f"event.{event_type}", + data, + msg_type=PluginMessageType.PLUGIN_EVENT + ) + + elif topic.startswith("plugin.broadcast."): + # 广播消息 + broadcast_topic = topic.replace("plugin.broadcast.", "") + await self.broadcast_to_plugins( + broadcast_topic, + data, + msg_type=PluginMessageType.PLUGIN_DATA + ) + + logger.debug(f"处理核心插件消息: {topic}") + + except Exception as e: + logger.error(f"处理核心插件消息时出错: {str(e)}", exc_info=True) + + def get_plugin_subscriber_count(self, plugin_name: str = None) -> int: + """获取插件订阅者数量""" + try: + if plugin_name: + if plugin_name not in self.plugin_subscribers: + return 0 + total = sum(len(subs) for subs in self.plugin_subscribers[plugin_name].values()) + logger.debug(f"插件 {plugin_name} 的订阅者数量: {total}") + return total + else: + total = 0 + for plugin_subs in self.plugin_subscribers.values(): + total += sum(len(subs) for subs in plugin_subs.values()) + logger.debug(f"总插件订阅者数量: {total}") + return total + except Exception as e: + logger.error(f"获取插件订阅者数量时出错: {str(e)}", exc_info=True) + return 0 + + def cleanup_plugin_subscriptions(self, plugin_name: str): + """清理插件的所有订阅""" + try: + if plugin_name in self.plugin_subscribers: + del self.plugin_subscribers[plugin_name] + logger.debug(f"清理插件订阅: {plugin_name}") + except Exception as e: + logger.error(f"清理插件订阅时出错: {str(e)}", exc_info=True) + + async def shutdown(self): + """关闭插件桥接服务""" + try: + logger.info("关闭插件桥接服务") + self.is_running = False + + # 等待处理任务结束 + if self.processing_task: + await asyncio.wait_for(self.processing_task, timeout=5.0) + + # 清理所有订阅 + self.plugin_subscribers.clear() + + # 清空队列 + while not self.plugin_message_queue.empty(): + try: + self.plugin_message_queue.get_nowait() + self.plugin_message_queue.task_done() + except asyncio.QueueEmpty: + break + + logger.debug("插件桥接服务关闭完成") + + except Exception as e: + logger.error(f"关闭插件桥接服务时出错: {str(e)}", exc_info=True) diff --git a/bridges/plugin_network_bridge.py b/bridges/plugin_network_bridge.py new file mode 100644 index 0000000..aefcad9 --- /dev/null +++ b/bridges/plugin_network_bridge.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import asyncio +from typing import Dict, List, Callable, Any +import json + +logger = logging.getLogger(__name__) + +class PluginNetworkBridge: + """插件网络桥接 - 简化插件的网络交互""" + + def __init__(self, plugin_name: str, internet_service, plugin_bridge): + self.plugin_name = plugin_name + self.internet_service = internet_service + self.plugin_bridge = plugin_bridge + self.registered_routes: List[Dict] = [] + self.websocket_handlers: List[Dict] = [] + + logger.debug(f"插件网络桥接初始化: {plugin_name}") + + def is_network_available(self): + """检查网络服务是否可用""" + return self.internet_service is not None and hasattr(self.internet_service, 'register_plugin_route') + + async def register_http_route(self, route_path: str, handler: Callable, + methods: List[str] = ["GET"], require_auth: bool = True): + """注册HTTP路由""" + try: + if not self.is_network_available(): + logger.warning("网络服务不可用,跳过HTTP路由注册") + return + + await self.internet_service.register_plugin_route( + self.plugin_name, route_path, handler, methods, require_auth + ) + + self.registered_routes.append({ + 'type': 'http', + 'path': route_path, + 'methods': methods, + 'require_auth': require_auth + }) + + logger.debug(f"插件 {self.plugin_name} 注册HTTP路由: {route_path}") + + except Exception as e: + logger.error(f"注册HTTP路由时出错: {str(e)}") + # 不抛出异常,让插件继续运行 + + async def register_websocket(self, ws_path: str, handler: Callable, require_auth: bool = True): + """注册WebSocket处理器""" + try: + if not self.is_network_available(): + logger.warning("网络服务不可用,跳过WebSocket注册") + return + + await self.internet_service.register_plugin_websocket( + self.plugin_name, ws_path, handler, require_auth + ) + + self.websocket_handlers.append({ + 'path': ws_path, + 'require_auth': require_auth + }) + + logger.debug(f"插件 {self.plugin_name} 注册WebSocket: {ws_path}") + + except Exception as e: + logger.error(f"注册WebSocket时出错: {str(e)}") + # 不抛出异常,让插件继续运行 + + async def broadcast_websocket(self, message: Dict): + """向插件的所有WebSocket连接广播消息""" + try: + if not self.internet_service: + logger.warning("网络服务不可用,无法广播消息") + return + + await self.internet_service.broadcast_to_websockets(self.plugin_name, message) + logger.debug(f"插件 {self.plugin_name} WebSocket广播: {len(message)} 字节") + except Exception as e: + logger.error(f"WebSocket广播时出错: {str(e)}") + + async def send_data_to_client(self, client_id: str, message: Dict): + """向特定客户端发送数据""" + try: + # 这里可以实现更精确的客户端消息发送 + # 目前先使用广播 + message['target_client'] = client_id + await self.broadcast_websocket(message) + + except Exception as e: + logger.error(f"发送数据到客户端时出错: {str(e)}") + + def get_network_info(self) -> Dict[str, Any]: + """获取网络配置信息""" + if not self.internet_service: + return { + 'plugin_name': self.plugin_name, + 'registered_routes': [], + 'websocket_handlers': [], + 'base_url': '网络服务不可用' + } + + return { + 'plugin_name': self.plugin_name, + 'registered_routes': self.registered_routes, + 'websocket_handlers': self.websocket_handlers, + 'base_url': f"http://{self.internet_service.http_host}:{self.internet_service.http_port}/{self.plugin_name}" + } + + async def setup_data_transfer(self, data_handler: Callable): + """设置跨端数据传输""" + try: + # 订阅网络数据接收事件 + self.plugin_bridge.subscribe_plugin( + self.plugin_name, + "network.data.receive", + data_handler + ) + + logger.debug(f"插件 {self.plugin_name} 设置跨端数据传输") + + except Exception as e: + logger.error(f"设置数据传输时出错: {str(e)}") diff --git a/config/framework/base_config.yaml b/config/framework/base_config.yaml new file mode 100644 index 0000000..5a55de3 --- /dev/null +++ b/config/framework/base_config.yaml @@ -0,0 +1,45 @@ +framework: + debug: true + name: SenSu + version: Alpha_0.2.0 +logging: + debug_level_file: true + level: DEBUG + max_file_size: 10MB + max_log_files: 20 +plugins: + auto_load: true + hot_reload: true + max_retry_count: 3 +# TUI配置 +tui: + enabled: true + refresh_rate: 30 + # TUI布局配置 + layout: + grid_rows: "4fr 5fr 1fr" # 三行布局:日志区域、消息区域、输入区域的比例 + # TUI样式配置 + styles: + log_area: "border: solid green; overflow-y: auto;" + message_area: "border: solid yellow; overflow-y: auto;" + input_area: "border: solid red;" + # TUI日志显示配置 + log_display: + max_lines: 200 +# 互联网服务配置 +internet: + websocket: + host: "0.0.0.0" + port: 4240 + # 其他websocket配置... + http: + host: "0.0.0.0" + port: 4200 + # 其他http配置... +panel: + entrance: + path: "/SenSu" + username: "admin" + password: "admin" + + \ No newline at end of file diff --git a/config/framework/permission_rules.yaml b/config/framework/permission_rules.yaml new file mode 100644 index 0000000..69382c3 --- /dev/null +++ b/config/framework/permission_rules.yaml @@ -0,0 +1,15 @@ +# 权限规则定义 +permission_levels: + - "read" + - "write" + - "execute" + - "admin" + +default_permissions: + - "framework.status.read" + - "plugin.self.info.read" + +admin_permissions: + - "framework.*" + - "plugin.*" + - "service.*" diff --git a/config/permissions/granted_permissions.json b/config/permissions/granted_permissions.json new file mode 100644 index 0000000..c923e8d --- /dev/null +++ b/config/permissions/granted_permissions.json @@ -0,0 +1,9 @@ +{ + "example_plugin": [ + "framework.event.subscribe", + "framework.command.execute", + "plugin.example.execute", + "plugin.example.read", + "plugin.example.write" + ] +} \ No newline at end of file diff --git a/config/permissions/pending_requests.json b/config/permissions/pending_requests.json new file mode 100644 index 0000000..3a0fafc --- /dev/null +++ b/config/permissions/pending_requests.json @@ -0,0 +1,18 @@ +{ + "10c4eb6e": { + "plugin_name": "example_plugin", + "permissions": [], + "timestamp": 484215.183089762 + }, + "d6f52ecd": { + "plugin_name": "example_plugin", + "permissions": [ + "plugin.example.read", + "plugin.example.write", + "plugin.example.execute", + "framework.event.subscribe", + "framework.command.execute" + ], + "timestamp": 484729.685713733 + } +} \ No newline at end of file diff --git a/config/permissions/plugin_status.json b/config/permissions/plugin_status.json new file mode 100644 index 0000000..e27a040 --- /dev/null +++ b/config/permissions/plugin_status.json @@ -0,0 +1,3 @@ +{ + "example_plugin": "granted" +} \ No newline at end of file diff --git a/config/plugins/commands.yaml b/config/plugins/commands.yaml new file mode 100644 index 0000000..2a936f4 --- /dev/null +++ b/config/plugins/commands.yaml @@ -0,0 +1,96 @@ +commands: + autoscroll: + description: '滚动控制: 切换自动滚动' + permissions: + - framework.tui.control + source: internal + chat_broadcast: &id001 + description: 向所有聊天客户端广播消息 + permissions: + - plugin.example.chat.broadcast + source: plugin.example_plugin + help: + description: 显示帮助信息 + permissions: + - framework.command.help.read + source: internal + history: + description: 显示命令历史 + permissions: + - framework.command.history.read + source: internal + netdiag: + description: 网络服务诊断 + permissions: + - framework.network.diagnose + source: internal + network_info: &id002 + description: 显示插件网络信息 + permissions: [] + source: plugin.example_plugin + permissions: + description: '权限管理: 显示权限状态' + permissions: + - framework.permission.read + source: internal + pm_plugin_status: + description: '权限管理: 查看插件权限状态' + permissions: + - framework.permission.read + source: internal + pmallow: + description: '权限管理: 同意权限请求' + permissions: + - framework.permission.read + source: internal + pmdeny: + description: '权限管理: 拒绝权限请求' + permissions: + - framework.permission.read + source: internal + pmhelp: + description: '权限管理: 显示权限命令帮助' + permissions: + - framework.permission.read + source: internal + pmignore: + description: '权限管理: 暂时忽略权限请求' + permissions: + - framework.permission.read + source: internal + pmpending: + description: '权限管理: 查看待授权请求列表' + permissions: + - framework.permission.read + source: internal + pmrequests: + description: '权限管理: 查看待授权请求列表(别名)' + permissions: + - framework.permission.read + source: internal + pmtest: + description: '权限管理: 测试权限配置文件' + permissions: + - framework.permission.read + source: internal + scroll: + description: '滚动控制: 手动滚动到底部' + permissions: + - framework.tui.control + source: internal + status: + description: 显示框架状态 + permissions: + - framework.status.read + source: internal + testlog: + description: 生成测试日志 + permissions: + - framework.command.test + source: internal +last_updated: 119133.773274654 +plugin_commands: + example_plugin: + chat_broadcast: *id001 + network_info: *id002 +total_commands: 18 diff --git a/config/services/network_routes.yaml b/config/services/network_routes.yaml new file mode 100644 index 0000000..8ea21b4 --- /dev/null +++ b/config/services/network_routes.yaml @@ -0,0 +1,17 @@ +http_port: 8000 +last_updated: 119133.817076582 +plugin_routes: + example_plugin: + - methods: + - GET + path: /plugin/example_plugin/api/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 diff --git a/debug_log_format.py b/debug_log_format.py new file mode 100644 index 0000000..051aff7 --- /dev/null +++ b/debug_log_format.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +import asyncio +from pathlib import Path + +# 添加项目根目录到Python路径 +project_root = Path(__file__).parent +sys.path.insert(0, str(project_root)) + +async def debug_log_format(): + """调试日志格式""" + try: + from services.log_service import LogService + from services.init_service import InitService + + print("🐱 调试日志格式...") + + # 初始化配置 + init_service = InitService() + configs = await init_service.initialize_framework() + base_config = configs['base'] + + # 创建日志服务 + log_service = LogService(base_config) + + # 等待日志服务初始化 + await asyncio.sleep(1) + + # 添加测试消费者 + def test_consumer(log_record): + print("📝 消费者收到的日志记录:") + print(f" 所有键: {list(log_record.keys())}") + if 'simple_message' in log_record: + print(f" simple_message: {log_record['simple_message']}") + if 'formatted_message' in log_record: + print(f" formatted_message: {log_record['formatted_message']}") + if 'level' in log_record: + print(f" level: {log_record['level']}") + print("---") + + log_service.add_log_consumer(test_consumer) + + # 测试日志记录 + import logging + logger = logging.getLogger("test_logger") + print("\n🔧 发送测试日志...") + logger.info("这是一条测试信息日志") + logger.debug("这是一条测试调试日志") + logger.warning("这是一条测试警告日志") + logger.error("这是一条测试错误日志") + + # 等待日志处理完成 + await asyncio.sleep(0.5) + + except Exception as e: + print(f"❌ 调试失败: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + asyncio.run(debug_log_format()) diff --git a/docs/SenSu 插件开发详细指南.md b/docs/SenSu 插件开发详细指南.md new file mode 100644 index 0000000..c8f7b95 --- /dev/null +++ b/docs/SenSu 插件开发详细指南.md @@ -0,0 +1,5044 @@ +# SenSu 插件开发超详细指南 + +## 一、插件系统架构深度解析 + +### 1.1 插件生命周期 + +``` +┌───────────────┐ ┌───────────────┐ ┌───────────────┐ +│ 扫描插件 │────▶│ 权限申请 │────▶│ 实例化插件 │ +└───────────────┘ └───────────────┘ └───────────────┘ + │ │ │ + ▼ ▼ ▼ +┌───────────────┐ ┌───────────────┐ ┌───────────────┐ +│ 加载配置文件 │ │ 权限验证/用户 │ │ 注册命令/路由 │ +└───────────────┘ └───────────────┘ └───────────────┘ + │ │ │ + └──────────────────────┴──────────────────────┘ + │ + ▼ + ┌───────────────────┐ + │ 插件就绪运行 │ + └───────────────────┘ +``` + +### 1.2 插件通信架构 + +```mermaid +graph TB + subgraph "插件内部" + A[插件主类] --> B[命令处理器] + A --> C[网络处理器] + A --> D[事件处理器] + end + + subgraph "框架服务" + E[PluginBridge] --> F[CoreBridge] + F --> G[网络服务] + F --> H[命令服务] + F --> I[权限服务] + end + + B --> H + C --> G + D --> E + + subgraph "外部接口" + J[HTTP客户端] --> G + K[WebSocket客户端] --> G + L[TUI用户] --> H + end +``` + +## 二、插件开发完整方案 + +### 2.1 环境准备 + +```bash +# 1. 克隆或下载框架 +git clone +cd SenSu-Alpha0.2 + +# 2. 安装依赖(建议使用虚拟环境) +python -m venv venv +source venv/bin/activate # Linux/Mac +# venv\Scripts\activate # Windows + +pip install -r requirements.txt + +# 3. 运行框架测试 +python main.py +``` + +### 2.2 创建新插件 + +#### 2.2.1 插件目录结构 + +``` +plugins/ +└── my_awesome_plugin/ # 插件目录(建议使用小写和下划线) + ├── __init__.py # 插件主模块(必需) + ├── config.yaml # 插件配置文件(必需) + ├── permissions.yaml # 权限申请文件(必需) + ├── requirements.txt # 插件特定依赖(可选) + ├── README.md # 插件说明文档(推荐) + ├── utils/ # 插件内部工具(可选) + │ ├── __init__.py + │ └── helper.py + ├── models/ # 数据模型(可选) + │ └── data_model.py + ├── services/ # 插件服务模块(可选) + │ └── background_service.py + └── static/ # 静态资源(可选) + ├── css/ + ├── js/ + └── images/ +``` + +#### 2.2.2 插件命名规范 + +1. **目录名**:小写字母、数字、下划线,如 `my_plugin` +2. **插件类名**:`Plugin`(必须使用这个类名) +3. **命令名**:小写字母、数字、下划线,如 `my_command` +4. **权限名**:`plugin.<插件名>.<操作>`,如 `plugin.my_plugin.read` + +### 2.3 配置文件详解 + +#### 2.3.1 config.yaml 完整示例 + +```yaml +# my_awesome_plugin/config.yaml + +# ========== 基础信息(必需)========== +name: "PluginName" # 插件显示名称 +version: "1.0.0" # 版本号(遵循语义化版本) +description: "这是一个功能强大的示例插件,用于演示插件开发" +author: "开发者名字 " +license: "MIT" # 开源许可证 + +# ========== 插件配置 ========== +settings: + enabled: true # 是否启用 + auto_start: true # 是否自动启动 + log_level: "INFO" # 日志级别:DEBUG, INFO, WARNING, ERROR + max_retry_count: 3 # 失败重试次数 + health_check_interval: 60 # 健康检查间隔(秒) + background_task_interval: 300 # 后台任务间隔(秒) + +# ========== 功能配置 ========== +features: + # 网络功能配置 + network: + enable_http: true # 启用HTTP接口 + enable_websocket: true # 启用WebSocket + enable_cors: true # 启用跨域支持 + cors_origins: ["*"] # 允许的跨域来源 + + # 数据库配置(如果有) + database: + type: "sqlite" # sqlite, mysql, postgresql + path: "data/my_plugin.db" # SQLite数据库路径 + host: "localhost" # 数据库主机 + port: 3306 # 数据库端口 + name: "my_plugin_db" # 数据库名 + user: "username" # 用户名 + password: "password" # 密码(建议使用环境变量) + + # 缓存配置 + cache: + type: "memory" # memory, redis + ttl: 3600 # 缓存时间(秒) + max_size: 1000 # 最大缓存项数 + + # 安全配置 + security: + require_auth: true # 是否需要认证 + token_expiry: 86400 # Token过期时间(秒) + rate_limit: 100 # 每秒请求限制 + blacklist_enabled: true # 启用黑名单 + +# ========== 业务配置 ========== +business: + # API配置 + api: + default_page_size: 20 # 默认分页大小 + max_page_size: 100 # 最大分页大小 + date_format: "%Y-%m-%d %H:%M:%S" # 日期格式 + + # 文件存储 + storage: + type: "local" # local, s3, minio + path: "data/files" # 本地存储路径 + max_file_size: 10485760 # 最大文件大小(10MB) + allowed_extensions: # 允许的文件扩展名 + - .txt + - .json + - .yaml + - .csv + + # 通知配置 + notification: + email_enabled: false + webhook_enabled: true + webhook_url: "" + +# ========== 定时任务配置 ========== +schedules: + - name: "daily_cleanup" + cron: "0 2 * * *" # 每天凌晨2点 + task: "cleanup_old_data" + enabled: true + + - name: "hourly_sync" + cron: "0 * * * *" # 每小时 + task: "sync_external_data" + enabled: true + +# ========== 依赖配置 ========== +dependencies: + required: # 必需依赖 + - requests>=2.25.0 + - pydantic>=1.8.0 + + optional: # 可选依赖 + - redis>=3.5.0 # 如果使用Redis缓存 + - aiomysql>=0.1.0 # 如果使用MySQL + + system: # 系统依赖 + - ffmpeg # 如果处理音视频 + - imagemagick # 如果处理图片 + +# ========== 国际化配置 ========== +i18n: + default_language: "zh_CN" + supported_languages: + - zh_CN + - en_US + translation_files: "translations/" + +# ========== 调试配置 ========== +debug: + enable_debug_endpoints: false # 是否启用调试端点 + log_requests: true # 是否记录请求日志 + log_responses: false # 是否记录响应日志 + profile_performance: false # 是否启用性能分析 +``` + +#### 2.3.2 配置加载和验证 + +```python +# 在插件中加载和验证配置 +from pydantic import BaseModel, validator +from typing import Optional, List +import os + +class PluginConfig(BaseModel): + """插件配置模型""" + name: str + version: str + description: str + author: str + settings: dict + features: dict + + @validator('name') + def validate_name(cls, v): + if len(v) < 2 or len(v) > 50: + raise ValueError('插件名称长度必须在2-50字符之间') + return v + + @validator('version') + def validate_version(cls, v): + import re + if not re.match(r'^\d+\.\d+\.\d+$', v): + raise ValueError('版本号格式必须为 X.Y.Z') + return v + +# 使用示例 +config_data = { ... } # 从config.yaml加载 +validated_config = PluginConfig(**config_data) +``` + +### 2.4 权限文件详解 + +#### 2.4.1 permissions.yaml 完整示例 + +```yaml +# my_awesome_plugin/permissions.yaml + +# ========== 基础信息 ========== +plugin_name: "my_awesome_plugin" # 必须与目录名一致 +plugin_version: "1.0.0" + +# ========== 权限申请列表 ========== +permissions: + # 框架基础权限 + - "framework.status.read" # 读取框架状态 + - "framework.event.subscribe" # 订阅框架事件 + - "framework.command.execute" # 执行框架命令 + + # 插件自身权限 + - "plugin.my_awesome_plugin.read" # 读取插件数据 + - "plugin.my_awesome_plugin.write" # 写入插件数据 + - "plugin.my_awesome_plugin.execute" # 执行插件操作 + - "plugin.my_awesome_plugin.delete" # 删除插件数据 + + # 网络权限 + - "plugin.my_awesome_plugin.network.access" # 访问网络 + - "plugin.my_awesome_plugin.network.http" # HTTP服务 + - "plugin.my_awesome_plugin.network.websocket" # WebSocket服务 + + # 文件系统权限 + - "plugin.my_awesome_plugin.filesystem.read" # 读取文件 + - "plugin.my_awesome_plugin.filesystem.write" # 写入文件 + + # 外部服务权限 + - "plugin.my_awesome_plugin.external_api.access" # 访问外部API + + # 系统权限(谨慎申请) + - "plugin.my_awesome_plugin.system.execute" # 执行系统命令 + + # 管理权限 + - "plugin.my_awesome_plugin.admin" # 插件管理员权限 + +# ========== 权限分组说明 ========== +permission_groups: + basic: # 基础组 + - "plugin.my_awesome_plugin.read" + - "plugin.my_awesome_plugin.write" + + network: # 网络组 + - "plugin.my_awesome_plugin.network.access" + - "plugin.my_awesome_plugin.network.http" + - "plugin.my_awesome_plugin.network.websocket" + + advanced: # 高级组(需要特别说明) + - "plugin.my_awesome_plugin.system.execute" + - "plugin.my_awesome_plugin.admin" + +# ========== 权限详细说明 ========== +permission_descriptions: + # 基础权限说明 + framework.status.read: "读取框架运行状态和基本信息" + framework.event.subscribe: "订阅框架事件通知" + framework.command.execute: "在框架中执行命令" + + # 插件权限说明 + plugin.my_awesome_plugin.read: "读取插件的配置和数据" + plugin.my_awesome_plugin.write: "修改插件的配置和数据" + plugin.my_awesome_plugin.execute: "执行插件提供的操作" + plugin.my_awesome_plugin.delete: "删除插件创建的数据" + + # 网络权限说明 + plugin.my_awesome_plugin.network.access: "允许插件访问网络服务" + plugin.my_awesome_plugin.network.http: "提供HTTP API接口" + plugin.my_awesome_plugin.network.websocket: "提供WebSocket实时通信" + + # 文件系统权限说明 + plugin.my_awesome_plugin.filesystem.read: "读取插件目录下的文件" + plugin.my_awesome_plugin.filesystem.write: "在插件目录下创建和修改文件" + + # 外部服务权限说明 + plugin.my_awesome_plugin.external_api.access: "访问第三方API服务(如天气、翻译等)" + + # 系统权限说明(危险权限) + plugin.my_awesome_plugin.system.execute: "⚠️ 执行系统级命令(可能影响系统安全)" + plugin.my_awesome_plugin.admin: "⚡ 插件管理员权限,可执行所有插件操作" + +# ========== 权限风险评估 ========== +permission_risk_levels: + low_risk: # 低风险权限 + - "framework.status.read" + - "plugin.my_awesome_plugin.read" + + medium_risk: # 中风险权限 + - "plugin.my_awesome_plugin.write" + - "plugin.my_awesome_plugin.network.access" + + high_risk: # 高风险权限 + - "plugin.my_awesome_plugin.system.execute" + - "plugin.my_awesome_plugin.admin" + +# ========== 依赖权限说明 ========== +permission_dependencies: + # 某些权限需要其他权限的支持 + plugin.my_awesome_plugin.network.http: + requires: "plugin.my_awesome_plugin.network.access" + + plugin.my_awesome_plugin.network.websocket: + requires: "plugin.my_awesome_plugin.network.access" + + plugin.my_awesome_plugin.admin: + requires_all: # 需要所有以下权限 + - "plugin.my_awesome_plugin.read" + - "plugin.my_awesome_plugin.write" + - "plugin.my_awesome_plugin.execute" + - "plugin.my_awesome_plugin.delete" + +# ========== 权限使用场景示例 ========== +usage_scenarios: + - scenario: "数据查看" + required_permissions: + - "plugin.my_awesome_plugin.read" + description: "用户只能查看数据,不能修改" + + - scenario: "数据管理" + required_permissions: + - "plugin.my_awesome_plugin.read" + - "plugin.my_awesome_plugin.write" + - "plugin.my_awesome_plugin.delete" + description: "用户可以完全管理数据" + + - scenario: "API服务" + required_permissions: + - "plugin.my_awesome_plugin.network.access" + - "plugin.my_awesome_plugin.network.http" + description: "插件可以提供HTTP API服务" + + - scenario: "实时通信" + required_permissions: + - "plugin.my_awesome_plugin.network.access" + - "plugin.my_awesome_plugin.network.websocket" + description: "插件可以提供WebSocket实时通信" + +# ========== 插件启动模式 ========== +startup_modes: + # 权限不足时的启动模式 + fallback_mode: + enabled: true + permissions_required: # 必需的最小权限集 + - "framework.status.read" + - "plugin.my_awesome_plugin.read" + degraded_features: # 降级运行的功能 + - "network_services" + - "background_tasks" + message: "插件将在受限模式下运行,部分功能不可用" + +# ========== 权限版本控制 ========== +versioning: + current_version: "1.0" + deprecated_permissions: # 已废弃的权限 + - "plugin.my_awesome_plugin.old_read" + new_permissions: # 新增权限 + - "plugin.my_awesome_plugin.enhanced_write" + migration_guide: "从v0.9升级到v1.0,请重新申请权限" +``` + +#### 2.4.2 权限验证代码示例 + +```python +class PermissionValidator: + """权限验证辅助类""" + + @staticmethod + def validate_permission_structure(permissions: list) -> tuple[bool, str]: + """验证权限列表结构""" + if not permissions: + return False, "权限列表不能为空" + + for perm in permissions: + if not isinstance(perm, str): + return False, f"权限必须是字符串: {perm}" + + # 检查格式:plugin.plugin_name.action + if not perm.startswith("plugin.") and not perm.startswith("framework."): + return False, f"权限格式错误: {perm}" + + # 检查长度 + if len(perm) > 100: + return False, f"权限名称过长: {perm}" + + return True, "验证通过" + + @staticmethod + def group_permissions_by_risk(permissions: list) -> dict: + """按风险等级分组权限""" + risk_groups = { + "low": [], + "medium": [], + "high": [] + } + + risk_mapping = { + "read": "low", + "write": "medium", + "delete": "medium", + "execute": "high", + "admin": "high", + "system": "high" + } + + for perm in permissions: + risk = "medium" # 默认中风险 + + for keyword, level in risk_mapping.items(): + if keyword in perm.lower(): + risk = level + break + + risk_groups[risk].append(perm) + + return risk_groups +``` + +### 2.5 插件主类完整实现 + +#### 2.5.1 __init__.py 完整模板 + +```python +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +我的插件 - 插件主模块 +版本: 1.0.0 +作者: 开发者名字 +描述: 这是一个功能完整的插件示例 +""" + +import logging +import asyncio +import sys +import os +from pathlib import Path +from typing import Dict, Any, List, Optional, Union +from dataclasses import dataclass +from datetime import datetime, timedelta +import json +import traceback + +# 导入框架装饰器 +try: + from fmfuncs.plugin_command_decorator import plugin_command, command +except ImportError: + # 回退方案 - 本地定义装饰器 + def plugin_command(name=None, description=None, permissions=None): + def decorator(func): + func._is_plugin_command = True + func._command_name = name or func.__name__ + func._command_description = description or func.__doc__ or f"命令: {func.__name__}" + func._command_permissions = permissions or [] + return func + return decorator + + 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 + self._logger = logging.getLogger(f"{__name__}.NetworkBridge") + self._logger.warning(f"网络桥接不可用,插件将以无网络模式运行") + + async def register_http_route(self, *args, **kwargs): + self._logger.warning("网络功能不可用,跳过HTTP路由注册") + + async def register_websocket(self, *args, **kwargs): + self._logger.warning("网络功能不可用,跳过WebSocket注册") + + async def broadcast_websocket(self, *args, **kwargs): + self._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): + self._logger.warning("网络功能不可用,跳过数据传输设置") + +# 插件内部模块 +try: + from .utils.helper import HelperClass + from .models.data_model import DataModel +except ImportError: + # 如果内部模块不可用,创建虚拟类 + HelperClass = type('HelperClass', (), {}) + DataModel = type('DataModel', (), {}) + +# 日志记录器 +logger = logging.getLogger(__name__) + +# 数据类定义 +@dataclass +class PluginStatus: + """插件状态数据类""" + is_running: bool = False + start_time: Optional[datetime] = None + uptime: Optional[timedelta] = None + request_count: int = 0 + error_count: int = 0 + last_error: Optional[str] = None + memory_usage: Optional[int] = None + +@dataclass +class PluginMetrics: + """插件指标数据类""" + requests_per_second: float = 0.0 + average_response_time: float = 0.0 + active_connections: int = 0 + cache_hit_rate: float = 0.0 + queue_size: int = 0 + +class Plugin: + """ + 我的插件主类 + + 功能特性: + 1. 完整的HTTP API接口 + 2. WebSocket实时通信 + 3. 后台定时任务 + 4. 数据缓存机制 + 5. 健康检查系统 + 6. 完整的错误处理 + 7. 性能监控指标 + + 使用方法: + 1. 确保框架已安装并运行 + 2. 将此插件放入plugins目录 + 3. 重启框架或使用插件管理命令加载 + """ + + # 类常量 + PLUGIN_NAME = "my_awesome_plugin" + PLUGIN_VERSION = "1.0.0" + DEFAULT_CONFIG = { + "enabled": True, + "log_level": "INFO" + } + + def __init__(self, plugin_name: str, config: Dict, bridge): + """ + 初始化插件 + + Args: + plugin_name: 插件名称(框架传入) + config: 插件配置(从config.yaml加载) + bridge: PluginBridge实例,用于插件间通信 + """ + self.plugin_name = plugin_name + self.original_config = config + self.bridge = bridge + self.service_manager = None + + # 配置处理 + self.config = self._merge_configs(self.DEFAULT_CONFIG, config) + + # 网络桥接 + self.network_bridge = None + + # 状态管理 + self.status = PluginStatus() + self.metrics = PluginMetrics() + + # 缓存系统 + self.cache = {} + self.cache_ttl = {} + + # 后台任务 + self.background_tasks = [] + self.task_handles = {} + + # 资源锁 + self._lock = asyncio.Lock() + self._resource_locks = {} + + # 内部服务 + self.helper = HelperClass() + self.data_model = DataModel() + + # 事件处理器映射 + self.event_handlers = {} + + # WebSocket连接管理 + self.websocket_connections = {} + + # API速率限制 + self.rate_limiter = {} + + logger.info(f"插件初始化: {self.plugin_name} v{self.PLUGIN_VERSION}") + + def _merge_configs(self, default: Dict, override: Dict) -> Dict: + """深度合并配置""" + result = default.copy() + + for key, value in override.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + result[key] = self._merge_configs(result[key], value) + else: + result[key] = value + + return result + + async def initialize(self): + """ + 初始化插件 - 核心入口点 + + 执行顺序: + 1. 基础初始化 + 2. 获取服务管理器 + 3. 设置网络功能 + 4. 注册事件处理器 + 5. 启动后台任务 + 6. 健康检查 + """ + try: + logger.info(f"开始初始化插件: {self.plugin_name}") + + # 1. 记录启动时间 + self.status.start_time = datetime.now() + + # 2. 获取服务管理器(如果可用) + await self._get_service_manager() + + # 3. 初始化网络功能 + await self._initialize_network() + + # 4. 注册事件处理器 + await self._register_event_handlers() + + # 5. 启动后台任务 + await self._start_background_tasks() + + # 6. 初始化缓存系统 + await self._initialize_cache() + + # 7. 设置健康检查 + await self._setup_health_check() + + # 8. 更新状态 + self.status.is_running = True + self.status.uptime = datetime.now() - self.status.start_time + + logger.info(f"✅ 插件初始化完成: {self.plugin_name}") + logger.info(f" 版本: {self.PLUGIN_VERSION}") + logger.info(f" 配置: {len(self.config)} 项") + logger.info(f" 网络: {'可用' if self.network_bridge else '不可用'}") + + # 发送初始化完成事件 + await self._send_initialization_event() + + return True + + except Exception as e: + logger.error(f"❌ 插件初始化失败: {str(e)}") + logger.error(traceback.format_exc()) + + # 尝试清理已初始化的资源 + await self._emergency_cleanup() + + return False + + async def _get_service_manager(self): + """安全获取服务管理器""" + try: + if hasattr(self.bridge, 'service_manager'): + self.service_manager = self.bridge.service_manager + logger.debug("服务管理器获取成功") + else: + logger.warning("服务管理器不可用,部分功能可能受限") + except Exception as e: + logger.warning(f"获取服务管理器时出错: {str(e)}") + + async def _initialize_network(self): + """初始化网络功能""" + try: + # 获取网络服务 + internet_service = None + if self.service_manager: + try: + internet_service = self.service_manager.get_service("internet") + except ValueError: + logger.warning("网络服务未注册") + + # 创建网络桥接 + if internet_service: + self.network_bridge = PluginNetworkBridge( + self.plugin_name, internet_service, self.bridge + ) + + # 注册网络路由 + await self._register_network_routes() + + logger.info(f"网络功能初始化完成,基础URL: {self.network_bridge.get_network_info()['base_url']}") + else: + logger.info("网络服务不可用,插件将以无网络模式运行") + # 创建虚拟网络桥接 + self.network_bridge = PluginNetworkBridge(self.plugin_name, None, self.bridge) + + except Exception as e: + logger.error(f"初始化网络功能时出错: {str(e)}") + raise + + async def _register_network_routes(self): + """注册所有网络路由""" + try: + if not self.network_bridge: + logger.warning("网络桥接不可用,跳过路由注册") + return + + logger.info("开始注册网络路由...") + + # 1. 信息接口(公开) + await self.network_bridge.register_http_route( + "/api/info", + self._handle_api_info, + methods=["GET"], + require_auth=False + ) + + # 2. 健康检查接口(公开) + await self.network_bridge.register_http_route( + "/api/health", + self._handle_api_health, + methods=["GET"], + require_auth=False + ) + + # 3. 数据查询接口(需要认证) + await self.network_bridge.register_http_route( + "/api/data", + self._handle_api_data, + methods=["GET", "POST"], + require_auth=True + ) + + # 4. 文件上传接口(需要认证) + await self.network_bridge.register_http_route( + "/api/upload", + self._handle_api_upload, + methods=["POST"], + require_auth=True + ) + + # 5. 管理接口(需要管理员权限) + await self.network_bridge.register_http_route( + "/api/admin/status", + self._handle_admin_status, + methods=["GET"], + require_auth=True + ) + + # 6. WebSocket聊天接口 + await self.network_bridge.register_websocket( + "/ws/chat", + self._handle_websocket_chat, + require_auth=True + ) + + # 7. WebSocket实时数据接口 + await self.network_bridge.register_websocket( + "/ws/data", + self._handle_websocket_data, + require_auth=True + ) + + # 8. 设置跨端数据传输 + await self.network_bridge.setup_data_transfer( + self._handle_cross_platform_data + ) + + logger.info(f"网络路由注册完成,共注册 {len(self._get_registered_routes())} 个路由") + + except Exception as e: + logger.error(f"注册网络路由时出错: {str(e)}") + raise + + def _get_registered_routes(self): + """获取已注册的路由信息""" + if not self.network_bridge: + return [] + + info = self.network_bridge.get_network_info() + return info.get('registered_routes', []) + + async def _register_event_handlers(self): + """注册事件处理器""" + try: + # 定义事件处理器映射 + self.event_handlers = { + "framework.start": self._handle_framework_start, + "framework.shutdown": self._handle_framework_shutdown, + "plugin.load": self._handle_plugin_load, + "plugin.unload": self._handle_plugin_unload, + "permission.granted": self._handle_permission_granted, + "permission.denied": self._handle_permission_denied, + "network.data.receive": self._handle_network_data_receive, + "user.login": self._handle_user_login, + "user.logout": self._handle_user_logout, + } + + # 注册事件处理器 + for event_type, handler in self.event_handlers.items(): + self.bridge.subscribe_plugin( + self.plugin_name, + f"event.{event_type}", + handler + ) + + logger.info(f"事件处理器注册完成,共 {len(self.event_handlers)} 个") + + except Exception as e: + logger.error(f"注册事件处理器时出错: {str(e)}") + + async def _start_background_tasks(self): + """启动后台任务""" + try: + config = self.config.get('schedules', []) + + for schedule in config: + if schedule.get('enabled', True): + task_name = schedule['name'] + cron_expr = schedule['cron'] + task_func = getattr(self, f"_task_{schedule['task']}", None) + + if task_func: + # 创建后台任务 + task = asyncio.create_task( + self._schedule_task(task_name, cron_expr, task_func) + ) + self.background_tasks.append(task) + self.task_handles[task_name] = task + + logger.info(f"后台任务启动: {task_name} ({cron_expr})") + + logger.info(f"后台任务启动完成,共 {len(self.background_tasks)} 个任务") + + except Exception as e: + logger.error(f"启动后台任务时出错: {str(e)}") + + async def _schedule_task(self, name: str, cron_expr: str, task_func): + """按Cron表达式调度任务""" + from croniter import croniter + import time + + base_time = time.time() + cron = croniter(cron_expr, base_time) + + while self.status.is_running: + try: + # 计算下一次执行时间 + next_time = cron.get_next(float) + sleep_time = next_time - time.time() + + if sleep_time > 0: + await asyncio.sleep(sleep_time) + + # 执行任务 + logger.debug(f"执行定时任务: {name}") + await task_func() + + except asyncio.CancelledError: + logger.info(f"任务被取消: {name}") + break + except Exception as e: + logger.error(f"任务执行出错 {name}: {str(e)}") + await asyncio.sleep(60) # 出错后等待1分钟 + + async def _initialize_cache(self): + """初始化缓存系统""" + try: + cache_config = self.config.get('cache', {}) + + if cache_config.get('type') == 'redis': + # 初始化Redis缓存 + import redis + self.redis_client = redis.Redis( + host=cache_config.get('host', 'localhost'), + port=cache_config.get('port', 6379), + db=cache_config.get('db', 0) + ) + logger.info("Redis缓存初始化完成") + else: + # 使用内存缓存 + logger.info("内存缓存初始化完成") + + except Exception as e: + logger.warning(f"缓存初始化失败,使用无缓存模式: {str(e)}") + + async def _setup_health_check(self): + """设置健康检查""" + try: + # 创建健康检查任务 + health_task = asyncio.create_task(self._health_check_loop()) + self.background_tasks.append(health_task) + + logger.info("健康检查系统已启动") + + except Exception as e: + logger.warning(f"健康检查设置失败: {str(e)}") + + async def _health_check_loop(self): + """健康检查循环""" + while self.status.is_running: + try: + await asyncio.sleep(60) # 每分钟检查一次 + + # 检查网络连接 + network_healthy = await self._check_network_health() + + # 检查缓存 + cache_healthy = await self._check_cache_health() + + # 检查后台任务 + tasks_healthy = await self._check_tasks_health() + + # 记录健康状态 + self.metrics.requests_per_second = self._calculate_rps() + + if not all([network_healthy, cache_healthy, tasks_healthy]): + logger.warning("健康检查发现问题") + + except Exception as e: + logger.error(f"健康检查出错: {str(e)}") + + async def _send_initialization_event(self): + """发送初始化完成事件""" + try: + await self.bridge.publish_to_plugin( + "framework", + "event.plugin.initialized", + { + "plugin_name": self.plugin_name, + "version": self.PLUGIN_VERSION, + "timestamp": datetime.now().isoformat() + } + ) + except Exception as e: + logger.debug(f"发送初始化事件失败: {str(e)}") + + async def _emergency_cleanup(self): + """紧急清理资源""" + try: + # 取消所有后台任务 + for task in self.background_tasks: + if not task.done(): + task.cancel() + + # 清理缓存 + self.cache.clear() + + logger.info("紧急清理完成") + + except Exception as e: + logger.error(f"紧急清理时出错: {str(e)}") + + # ========== 网络处理器方法 ========== + + async def _handle_api_info(self, request): + """处理API信息请求""" + from aiohttp import web + + try: + self.status.request_count += 1 + + info = { + "plugin": { + "name": self.plugin_name, + "version": self.PLUGIN_VERSION, + "description": self.config.get('description', ''), + "author": self.config.get('author', ''), + "status": "running" if self.status.is_running else "stopped" + }, + "system": { + "start_time": self.status.start_time.isoformat() if self.status.start_time else None, + "uptime": str(self.status.uptime) if self.status.uptime else None, + "request_count": self.status.request_count, + "error_count": self.status.error_count + }, + "network": self.network_bridge.get_network_info() if self.network_bridge else None, + "timestamp": datetime.now().isoformat() + } + + return web.json_response(info) + + except Exception as e: + logger.error(f"处理API信息请求时出错: {str(e)}") + return web.json_response( + {"error": "服务器内部错误", "details": str(e)}, + status=500 + ) + + async def _handle_api_health(self, request): + """处理健康检查请求""" + from aiohttp import web + + try: + # 检查各项健康指标 + checks = { + "plugin_running": self.status.is_running, + "network_available": self.network_bridge is not None, + "background_tasks": len([t for t in self.background_tasks if not t.done()]), + "cache_available": len(self.cache) > 0 or hasattr(self, 'redis_client'), + "last_error": self.status.last_error + } + + # 计算总体状态 + all_healthy = all([ + checks["plugin_running"], + checks["network_available"], + checks["background_tasks"] > 0 + ]) + + response = { + "status": "healthy" if all_healthy else "unhealthy", + "timestamp": datetime.now().isoformat(), + "checks": checks, + "metrics": { + "requests_per_second": self.metrics.requests_per_second, + "active_connections": len(self.websocket_connections), + "cache_size": len(self.cache) + } + } + + status_code = 200 if all_healthy else 503 + return web.json_response(response, status=status_code) + + except Exception as e: + logger.error(f"处理健康检查请求时出错: {str(e)}") + return web.json_response( + {"status": "error", "error": str(e)}, + status=500 + ) + + async def _handle_api_data(self, request): + """处理数据API请求""" + from aiohttp import web + + try: + # 检查速率限制 + client_ip = request.remote + if not await self._check_rate_limit(client_ip): + return web.json_response( + {"error": "请求过于频繁,请稍后再试"}, + status=429 + ) + + if request.method == "GET": + # 查询数据 + query_params = dict(request.query) + data = await self._query_data(query_params) + + return web.json_response({ + "success": True, + "data": data, + "count": len(data), + "timestamp": datetime.now().isoformat() + }) + + elif request.method == "POST": + # 创建数据 + data = await request.json() + result = await self._create_data(data) + + return web.json_response({ + "success": True, + "id": result.get("id"), + "message": "数据创建成功", + "timestamp": datetime.now().isoformat() + }, status=201) + + except json.JSONDecodeError: + return web.json_response( + {"error": "无效的JSON数据"}, + status=400 + ) + except Exception as e: + logger.error(f"处理数据API请求时出错: {str(e)}") + return web.json_response( + {"error": "服务器内部错误", "details": str(e)}, + status=500 + ) + + async def _handle_api_upload(self, request): + """处理文件上传请求""" + from aiohttp import web + import aiofiles + + try: + # 检查内容类型 + if not request.content_type.startswith('multipart/form-data'): + return web.json_response( + {"error": "必须使用multipart/form-data格式"}, + status=400 + ) + + reader = await request.multipart() + + files = [] + async for field in reader: + if field.filename: + # 保存文件 + filename = field.filename + filepath = Path("data/uploads") / self.plugin_name / filename + filepath.parent.mkdir(parents=True, exist_ok=True) + + size = 0 + async with aiofiles.open(filepath, 'wb') as f: + while True: + chunk = await field.read_chunk() + if not chunk: + break + size += len(chunk) + await f.write(chunk) + + files.append({ + "filename": filename, + "size": size, + "path": str(filepath) + }) + + return web.json_response({ + "success": True, + "files": files, + "count": len(files), + "timestamp": datetime.now().isoformat() + }) + + except Exception as e: + logger.error(f"处理文件上传时出错: {str(e)}") + return web.json_response( + {"error": "文件上传失败", "details": str(e)}, + status=500 + ) + + async def _handle_admin_status(self, request): + """处理管理状态请求""" + from aiohttp import web + + try: + # 检查管理员权限 + if not await self._check_admin_permission(request): + return web.json_response( + {"error": "需要管理员权限"}, + status=403 + ) + + status_info = { + "plugin": { + "name": self.plugin_name, + "config": self.config, + "status": self.status, + "metrics": self.metrics + }, + "system": { + "background_tasks": [ + { + "name": name, + "running": not task.done(), + "cancelled": task.cancelled() + } + for name, task in self.task_handles.items() + ], + "cache_info": { + "size": len(self.cache), + "keys": list(self.cache.keys())[:10] + }, + "websocket_connections": len(self.websocket_connections) + }, + "timestamp": datetime.now().isoformat() + } + + return web.json_response(status_info) + + except Exception as e: + logger.error(f"处理管理状态请求时出错: {str(e)}") + return web.json_response( + {"error": "服务器内部错误", "details": str(e)}, + status=500 + ) + + async def _handle_websocket_chat(self, ws, request): + """处理WebSocket聊天""" + from aiohttp import web + + try: + # 获取用户信息 + user = await self._get_user_from_request(request) + if not user: + await ws.close(code=1008, message="未认证") + return + + # 记录连接 + connection_id = f"{user['id']}_{id(ws)}" + self.websocket_connections[connection_id] = { + "ws": ws, + "user": user, + "connected_at": datetime.now() + } + + logger.info(f"WebSocket聊天连接建立: {connection_id}") + + # 发送欢迎消息 + await ws.send_str(json.dumps({ + "type": "system", + "message": f"欢迎 {user['username']} 进入聊天室", + "timestamp": datetime.now().isoformat() + })) + + # 广播用户上线消息 + await self._broadcast_chat_message({ + "type": "user_join", + "user": user, + "timestamp": datetime.now().isoformat() + }) + + # 处理消息 + async for msg in ws: + if msg.type == web.WSMsgType.TEXT: + try: + data = json.loads(msg.data) + + # 处理不同类型的消息 + if data.get('type') == 'message': + # 广播聊天消息 + message = { + "type": "message", + "from": user, + "content": data.get('content', ''), + "timestamp": datetime.now().isoformat() + } + + await self._broadcast_chat_message(message) + + elif data.get('type') == 'typing': + # 广播输入状态 + await self._broadcast_chat_message({ + "type": "typing", + "user": user, + "is_typing": data.get('is_typing', False), + "timestamp": datetime.now().isoformat() + }) + + except json.JSONDecodeError: + logger.warning(f"收到无效的JSON消息: {msg.data}") + + elif msg.type == web.WSMsgType.ERROR: + logger.error(f"WebSocket错误: {ws.exception()}") + + elif msg.type == web.WSMsgType.CLOSE: + logger.info(f"WebSocket连接关闭: {connection_id}") + + except Exception as e: + logger.error(f"WebSocket聊天处理出错: {str(e)}") + finally: + # 清理连接 + if connection_id in self.websocket_connections: + del self.websocket_connections[connection_id] + + # 广播用户离线消息 + if 'user' in locals(): + await self._broadcast_chat_message({ + "type": "user_leave", + "user": user, + "timestamp": datetime.now().isoformat() + }) + + async def _handle_websocket_data(self, ws, request): + """处理WebSocket实时数据""" + from aiohttp import web + + try: + # 获取用户信息 + user = await self._get_user_from_request(request) + if not user: + await ws.close(code=1008, message="未认证") + return + + connection_id = f"data_{user['id']}_{id(ws)}" + + logger.info(f"WebSocket数据连接建立: {connection_id}") + + # 发送初始数据 + await ws.send_str(json.dumps({ + "type": "init", + "data": await self._get_initial_data(), + "timestamp": datetime.now().isoformat() + })) + + # 定期发送更新 + while not ws.closed: + try: + await asyncio.sleep(5) # 每5秒发送一次更新 + + if not ws.closed: + await ws.send_str(json.dumps({ + "type": "update", + "data": await self._get_updated_data(), + "timestamp": datetime.now().isoformat() + })) + + except asyncio.CancelledError: + break + except Exception as e: + logger.error(f"发送WebSocket数据更新时出错: {str(e)}") + break + + except Exception as e: + logger.error(f"WebSocket数据处理出错: {str(e)}") + finally: + logger.info(f"WebSocket数据连接关闭: {connection_id}") + + async def _handle_cross_platform_data(self, event_data): + """处理跨端数据""" + try: + logger.info(f"收到跨端数据: {event_data.get('type')}") + + # 根据数据类型处理 + data_type = event_data.get('type') + + if data_type == "sync_request": + # 处理同步请求 + await self._handle_sync_request(event_data) + + elif data_type == "notification": + # 处理通知 + await self._handle_notification(event_data) + + elif data_type == "command": + # 处理远程命令 + await self._handle_remote_command(event_data) + + # 广播到WebSocket + if self.network_bridge: + await self.network_bridge.broadcast_websocket({ + "type": "cross_platform", + "source": event_data.get('source', 'unknown'), + "data": event_data.get('data', {}), + "timestamp": datetime.now().isoformat() + }) + + except Exception as e: + logger.error(f"处理跨端数据时出错: {str(e)}") + + # ========== 事件处理器方法 ========== + + async def _handle_framework_start(self, event_data): + """处理框架启动事件""" + try: + logger.info(f"框架启动事件: {event_data}") + + # 发送欢迎消息 + if self.network_bridge: + await self.network_bridge.broadcast_websocket({ + "type": "system", + "message": f"插件 {self.plugin_name} 已就绪,框架已启动", + "timestamp": datetime.now().isoformat() + }) + + except Exception as e: + logger.error(f"处理框架启动事件时出错: {str(e)}") + + async def _handle_framework_shutdown(self, event_data): + """处理框架关闭事件""" + try: + logger.info("收到框架关闭事件,开始清理...") + + # 通知所有连接 + if self.network_bridge: + await self.network_bridge.broadcast_websocket({ + "type": "system", + "message": "框架正在关闭,请保存您的工作", + "timestamp": datetime.now().isoformat() + }) + + # 执行插件关闭 + await self.shutdown() + + except Exception as e: + logger.error(f"处理框架关闭事件时出错: {str(e)}") + + async def _handle_plugin_load(self, event_data): + """处理插件加载事件""" + try: + loaded_plugin = event_data.get('plugin_name') + logger.info(f"插件加载事件: {loaded_plugin}") + + # 如果是其他插件加载,可以建立连接或同步数据 + if loaded_plugin != self.plugin_name: + await self._sync_with_plugin(loaded_plugin) + + except Exception as e: + logger.error(f"处理插件加载事件时出错: {str(e)}") + + async def _handle_plugin_unload(self, event_data): + """处理插件卸载事件""" + try: + unloaded_plugin = event_data.get('plugin_name') + logger.info(f"插件卸载事件: {unloaded_plugin}") + + # 清理与该插件相关的资源 + await self._cleanup_plugin_resources(unloaded_plugin) + + except Exception as e: + logger.error(f"处理插件卸载事件时出错: {str(e)}") + + async def _handle_permission_granted(self, event_data): + """处理权限授予事件""" + try: + plugin_name = event_data.get('plugin_name') + permissions = event_data.get('permissions', []) + + if plugin_name == self.plugin_name: + logger.info(f"权限已授予: {permissions}") + + # 重新初始化需要权限的功能 + await self._reinitialize_with_permissions(permissions) + + except Exception as e: + logger.error(f"处理权限授予事件时出错: {str(e)}") + + async def _handle_permission_denied(self, event_data): + """处理权限拒绝事件""" + try: + plugin_name = event_data.get('plugin_name') + + if plugin_name == self.plugin_name: + logger.warning("权限被拒绝,部分功能将不可用") + + # 降级运行 + await self._degrade_features() + + except Exception as e: + logger.error(f"处理权限拒绝事件时出错: {str(e)}") + + async def _handle_network_data_receive(self, event_data): + """处理网络数据接收事件""" + try: + data = event_data.get('data', {}) + source = event_data.get('source', 'unknown') + + logger.debug(f"收到网络数据: {data.get('type')} from {source}") + + # 根据数据类型处理 + await self._process_network_data(data, source) + + except Exception as e: + logger.error(f"处理网络数据时出错: {str(e)}") + + async def _handle_user_login(self, event_data): + """处理用户登录事件""" + try: + user = event_data.get('user', {}) + logger.info(f"用户登录: {user.get('username')}") + + # 发送欢迎消息 + if self.network_bridge: + await self.network_bridge.broadcast_websocket({ + "type": "user", + "action": "login", + "user": user, + "timestamp": datetime.now().isoformat() + }) + + except Exception as e: + logger.error(f"处理用户登录事件时出错: {str(e)}") + + async def _handle_user_logout(self, event_data): + """处理用户登出事件""" + try: + user = event_data.get('user', {}) + logger.info(f"用户登出: {user.get('username')}") + + # 清理用户相关资源 + await self._cleanup_user_resources(user) + + except Exception as e: + logger.error(f"处理用户登出事件时出错: {str(e)}") + + # ========== 辅助方法 ========== + + async def _check_rate_limit(self, client_ip: str, limit: int = 100) -> bool: + """检查速率限制""" + now = datetime.now() + + if client_ip not in self.rate_limiter: + self.rate_limiter[client_ip] = { + "count": 1, + "window_start": now + } + return True + + # 检查时间窗口 + window_start = self.rate_limiter[client_ip]["window_start"] + window_age = (now - window_start).total_seconds() + + if window_age > 60: # 1分钟窗口 + # 重置计数器 + self.rate_limiter[client_ip] = { + "count": 1, + "window_start": now + } + return True + + # 增加计数 + self.rate_limiter[client_ip]["count"] += 1 + + # 检查是否超限 + if self.rate_limiter[client_ip]["count"] > limit: + return False + + return True + + async def _get_user_from_request(self, request): + """从请求中获取用户信息""" + # 这里实现用户认证逻辑 + # 可以从请求头中获取token,然后验证 + token = request.headers.get('Authorization', '').replace('Bearer ', '') + + if token: + # 验证token并返回用户信息 + # 这里需要连接到认证服务 + return { + "id": "user_id", + "username": "username", + "permissions": [] + } + + return None + + async def _check_admin_permission(self, request): + """检查管理员权限""" + user = await self._get_user_from_request(request) + + if user and "admin" in user.get("permissions", []): + return True + + return False + + async def _broadcast_chat_message(self, message): + """广播聊天消息""" + if not self.network_bridge: + return + + for connection_id, connection in self.websocket_connections.items(): + try: + if not connection['ws'].closed: + await connection['ws'].send_str(json.dumps(message)) + except Exception as e: + logger.error(f"广播消息失败 {connection_id}: {str(e)}") + + async def _query_data(self, query_params): + """查询数据""" + # 这里实现数据查询逻辑 + # 可以从数据库、文件或内存中查询 + return [] + + async def _create_data(self, data): + """创建数据""" + # 这里实现数据创建逻辑 + return {"id": "new_id"} + + async def _get_initial_data(self): + """获取初始数据""" + return {"message": "初始数据"} + + async def _get_updated_data(self): + """获取更新数据""" + return {"message": "更新数据", "timestamp": datetime.now().isoformat()} + + async def _check_network_health(self): + """检查网络健康状态""" + return True + + async def _check_cache_health(self): + """检查缓存健康状态""" + return True + + async def _check_tasks_health(self): + """检查任务健康状态""" + return True + + def _calculate_rps(self): + """计算每秒请求数""" + # 这里实现RPS计算逻辑 + return 0.0 + + async def _sync_with_plugin(self, plugin_name): + """与插件同步""" + logger.debug(f"与插件同步: {plugin_name}") + + async def _cleanup_plugin_resources(self, plugin_name): + """清理插件资源""" + logger.debug(f"清理插件资源: {plugin_name}") + + async def _reinitialize_with_permissions(self, permissions): + """重新初始化权限相关功能""" + logger.debug(f"重新初始化权限: {permissions}") + + async def _degrade_features(self): + """降级功能""" + logger.debug("功能降级") + + async def _process_network_data(self, data, source): + """处理网络数据""" + logger.debug(f"处理网络数据: {data} from {source}") + + async def _cleanup_user_resources(self, user): + """清理用户资源""" + logger.debug(f"清理用户资源: {user.get('username')}") + + # ========== 后台任务方法 ========== + + async def _task_cleanup_old_data(self): + """清理旧数据任务""" + try: + logger.info("开始清理旧数据...") + + # 实现清理逻辑 + await asyncio.sleep(1) # 模拟清理过程 + + logger.info("旧数据清理完成") + + except Exception as e: + logger.error(f"清理旧数据时出错: {str(e)}") + + async def _task_sync_external_data(self): + """同步外部数据任务""" + try: + logger.info("开始同步外部数据...") + + # 实现同步逻辑 + await asyncio.sleep(1) # 模拟同步过程 + + logger.info("外部数据同步完成") + + except Exception as e: + logger.error(f"同步外部数据时出错: {str(e)}") + + # ========== 插件命令方法 ========== + + @plugin_command( + name="status", + description="查看插件状态", + permissions=["plugin.my_awesome_plugin.read"] + ) + async def cmd_status(self, *args): + """查看插件状态命令""" + try: + result = [] + result.append(f"🔍 **{self.plugin_name} 插件状态**") + result.append("=" * 50) + result.append(f"📊 版本: {self.PLUGIN_VERSION}") + result.append(f"🔄 状态: {'✅ 运行中' if self.status.is_running else '❌ 已停止'}") + + if self.status.start_time: + result.append(f"⏰ 启动时间: {self.status.start_time.strftime('%Y-%m-%d %H:%M:%S')}") + + if self.status.uptime: + result.append(f"⏱️ 运行时长: {self.status.uptime}") + + result.append(f"📈 请求总数: {self.status.request_count}") + result.append(f"❌ 错误总数: {self.status.error_count}") + + # 网络状态 + network_info = self.network_bridge.get_network_info() if self.network_bridge else {} + result.append(f"🌐 网络状态: {'✅ 可用' if network_info else '❌ 不可用'}") + + if network_info: + result.append(f" 基础URL: {network_info.get('base_url', 'N/A')}") + result.append(f" HTTP路由: {len(network_info.get('registered_routes', []))} 个") + result.append(f" WebSocket: {len(network_info.get('websocket_handlers', []))} 个") + + # 后台任务 + result.append(f"🔧 后台任务: {len(self.background_tasks)} 个运行中") + + # 缓存状态 + result.append(f"💾 缓存大小: {len(self.cache)} 项") + + # WebSocket连接 + result.append(f"🔗 WebSocket连接: {len(self.websocket_connections)} 个") + + return "\n".join(result) + + except Exception as e: + logger.error(f"状态命令执行失败: {str(e)}") + return f"❌ 获取状态失败: {str(e)}" + + @plugin_command( + name="config", + description="查看或修改插件配置", + permissions=["plugin.my_awesome_plugin.read", "plugin.my_awesome_plugin.write"] + ) + async def cmd_config(self, *args): + """配置管理命令""" + try: + if not args: + # 显示配置 + result = [f"⚙️ **{self.plugin_name} 配置信息**"] + result.append("=" * 50) + + for section, values in self.config.items(): + if isinstance(values, dict): + result.append(f"\n📁 {section.upper()}:") + for key, value in list(values.items())[:5]: # 只显示前5项 + result.append(f" {key}: {value}") + if len(values) > 5: + result.append(f" ... 还有 {len(values) - 5} 项配置") + else: + result.append(f"{section}: {values}") + + result.append("\n💡 使用: config get 查看具体配置") + result.append("💡 使用: config set 修改配置") + + return "\n".join(result) + + command = args[0].lower() + + if command == "get": + if len(args) < 2: + return "❌ 请指定配置键,如: config get settings.log_level" + + key = args[1] + value = self._get_nested_config(key) + + if value is not None: + return f"✅ {key} = {value}" + else: + return f"❌ 配置键不存在: {key}" + + elif command == "set": + if len(args) < 3: + return "❌ 请指定配置键和值,如: config set settings.log_level DEBUG" + + key = args[1] + value = args[2] + + # 尝试转换为适当类型 + try: + if value.lower() == "true": + value = True + elif value.lower() == "false": + value = False + elif value.isdigit(): + value = int(value) + elif value.replace('.', '', 1).isdigit(): + value = float(value) + except: + pass + + success = self._set_nested_config(key, value) + + if success: + # 保存配置到文件 + await self._save_config() + return f"✅ 配置已更新: {key} = {value}" + else: + return f"❌ 配置更新失败: {key}" + + else: + return f"❌ 未知命令: {command}" + + except Exception as e: + logger.error(f"配置命令执行失败: {str(e)}") + return f"❌ 配置命令错误: {str(e)}" + + def _get_nested_config(self, key_path: str): + """获取嵌套配置值""" + keys = key_path.split('.') + current = self.config + + for key in keys: + if isinstance(current, dict) and key in current: + current = current[key] + else: + return None + + return current + + def _set_nested_config(self, key_path: str, value): + """设置嵌套配置值""" + try: + keys = key_path.split('.') + current = self.config + + # 遍历到最后一个键的父级 + for key in keys[:-1]: + if key not in current or not isinstance(current[key], dict): + current[key] = {} + current = current[key] + + # 设置值 + current[keys[-1]] = value + return True + + except Exception: + return False + + async def _save_config(self): + """保存配置到文件""" + try: + config_path = Path(f"plugins/{self.plugin_name}/config.yaml") + + import yaml + with open(config_path, 'w', encoding='utf-8') as f: + yaml.dump(self.config, f, default_flow_style=False, allow_unicode=True) + + logger.info(f"配置已保存: {config_path}") + + except Exception as e: + logger.error(f"保存配置失败: {str(e)}") + + @plugin_command( + name="network", + description="网络功能管理", + permissions=["plugin.my_awesome_plugin.network.access"] + ) + async def cmd_network(self, *args): + """网络功能管理命令""" + try: + if not args: + # 显示网络状态 + if not self.network_bridge: + return "❌ 网络功能不可用" + + info = self.network_bridge.get_network_info() + + result = [f"🌐 **{self.plugin_name} 网络状态**"] + result.append("=" * 50) + 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']: + auth_required = "🔐" if route['require_auth'] else "🔓" + result.append(f" {auth_required} {route['path']} [{','.join(route['methods'])}]") + + if info['websocket_handlers']: + result.append("\n⚡ **注册的WebSocket:**") + for ws in info['websocket_handlers']: + auth_required = "🔐" if ws['require_auth'] else "🔓" + result.append(f" {auth_required} {ws['path']}") + + result.append("\n💡 使用: network test 测试网络连接") + result.append("💡 使用: network restart 重启网络功能") + + return "\n".join(result) + + command = args[0].lower() + + if command == "test": + # 测试网络连接 + if not self.network_bridge: + return "❌ 网络功能不可用" + + info = self.network_bridge.get_network_info() + base_url = info['base_url'] + + if base_url == '网络服务不可用': + return "❌ 网络服务不可用,无法测试" + + try: + import aiohttp + + async with aiohttp.ClientSession() as session: + async with session.get(f"{base_url}/api/health") as response: + if response.status == 200: + return "✅ 网络连接正常" + else: + return f"❌ 网络连接异常,状态码: {response.status}" + except Exception as e: + return f"❌ 网络测试失败: {str(e)}" + + elif command == "restart": + # 重启网络功能 + if not self.service_manager: + return "❌ 服务管理器不可用" + + # 这里可以实现网络功能重启逻辑 + return "🔄 网络功能重启中..." + + else: + return f"❌ 未知命令: {command}" + + except Exception as e: + logger.error(f"网络命令执行失败: {str(e)}") + return f"❌ 网络命令错误: {str(e)}" + + @plugin_command( + name="cache", + description="缓存管理", + permissions=["plugin.my_awesome_plugin.read"] + ) + async def cmd_cache(self, *args): + """缓存管理命令""" + try: + if not args: + # 显示缓存状态 + result = [f"💾 **{self.plugin_name} 缓存状态**"] + result.append("=" * 50) + result.append(f"📊 缓存项数: {len(self.cache)}") + result.append(f"⏱️ TTL项数: {len(self.cache_ttl)}") + result.append(f"📈 命中率: {self.metrics.cache_hit_rate:.2%}") + + if self.cache: + result.append("\n🔑 **缓存键列表 (前10个):**") + for i, key in enumerate(list(self.cache.keys())[:10]): + value = self.cache[key] + value_preview = str(value)[:50] + "..." if len(str(value)) > 50 else str(value) + result.append(f" {i+1}. {key}: {value_preview}") + + if len(self.cache) > 10: + result.append(f" ... 还有 {len(self.cache) - 10} 个键") + + result.append("\n💡 使用: cache clear 清理所有缓存") + result.append("💡 使用: cache get 获取缓存值") + result.append("💡 使用: cache set [ttl] 设置缓存") + + return "\n".join(result) + + command = args[0].lower() + + if command == "clear": + # 清理缓存 + old_size = len(self.cache) + self.cache.clear() + self.cache_ttl.clear() + + return f"✅ 缓存已清理,共清理 {old_size} 项" + + elif command == "get": + if len(args) < 2: + return "❌ 请指定缓存键,如: cache get my_key" + + key = args[1] + + if key in self.cache: + value = self.cache[key] + + # 检查是否过期 + if key in self.cache_ttl: + expiry = self.cache_ttl[key] + if datetime.now() > expiry: + del self.cache[key] + del self.cache_ttl[key] + return f"❌ 缓存已过期: {key}" + + return f"✅ {key} = {value}" + else: + return f"❌ 缓存键不存在: {key}" + + elif command == "set": + if len(args) < 3: + return "❌ 请指定缓存键和值,如: cache set my_key my_value" + + key = args[1] + value = args[2] + + # 解析TTL + ttl = None + if len(args) > 3: + try: + ttl = int(args[3]) + except ValueError: + return "❌ TTL必须是整数(秒)" + + # 设置缓存 + self.cache[key] = value + + if ttl: + self.cache_ttl[key] = datetime.now() + timedelta(seconds=ttl) + + return f"✅ 缓存已设置: {key} = {value}" + (f" (TTL: {ttl}秒)" if ttl else "") + + elif command == "stats": + # 显示详细统计 + total_hits = 0 # 这里需要实现命中计数 + total_misses = 0 + + if total_hits + total_misses > 0: + hit_rate = total_hits / (total_hits + total_misses) + else: + hit_rate = 0 + + return ( + f"📊 **缓存统计**\n" + f"命中次数: {total_hits}\n" + f"未命中次数: {total_misses}\n" + f"命中率: {hit_rate:.2%}\n" + f"内存使用: 约 {sum(len(str(v)) for v in self.cache.values())} 字节" + ) + + else: + return f"❌ 未知命令: {command}" + + except Exception as e: + logger.error(f"缓存命令执行失败: {str(e)}") + return f"❌ 缓存命令错误: {str(e)}" + + @plugin_command( + name="tasks", + description="后台任务管理", + permissions=["plugin.my_awesome_plugin.read"] + ) + async def cmd_tasks(self, *args): + """后台任务管理命令""" + try: + if not args: + # 显示任务状态 + result = [f"🔧 **{self.plugin_name} 后台任务**"] + result.append("=" * 50) + result.append(f"📊 总任务数: {len(self.background_tasks)}") + + running_tasks = [t for t in self.background_tasks if not t.done()] + result.append(f"🔄 运行中: {len(running_tasks)}") + result.append(f"✅ 已完成: {len(self.background_tasks) - len(running_tasks)}") + + if self.task_handles: + result.append("\n📋 **任务列表:**") + for name, task in self.task_handles.items(): + status = "🟢 运行中" if not task.done() else "🔴 已停止" + cancelled = " (已取消)" if task.cancelled() else "" + result.append(f" {status}{cancelled} {name}") + + result.append("\n💡 使用: tasks start 启动任务") + result.append("💡 使用: tasks stop 停止任务") + result.append("💡 使用: tasks list 列出所有任务") + + return "\n".join(result) + + command = args[0].lower() + + if command == "list": + # 列出所有任务 + if not self.task_handles: + return "📭 没有后台任务" + + result = ["📋 **后台任务列表:**"] + for name, task in self.task_handles.items(): + if task.done(): + if task.cancelled(): + status = "🔴 已取消" + else: + status = "✅ 已完成" + else: + status = "🟢 运行中" + + result.append(f" {status} {name}") + + return "\n".join(result) + + elif command == "start": + if len(args) < 2: + return "❌ 请指定任务名称,如: tasks start daily_cleanup" + + task_name = args[1] + + # 查找任务配置 + task_config = None + for schedule in self.config.get('schedules', []): + if schedule.get('name') == task_name: + task_config = schedule + break + + if not task_config: + return f"❌ 找不到任务: {task_name}" + + # 检查任务是否已在运行 + if task_name in self.task_handles: + task = self.task_handles[task_name] + if not task.done(): + return f"ℹ️ 任务已在运行: {task_name}" + + # 启动任务 + task_func = getattr(self, f"_task_{task_config['task']}", None) + if not task_func: + return f"❌ 找不到任务处理函数: {task_config['task']}" + + task = asyncio.create_task( + self._schedule_task(task_name, task_config['cron'], task_func) + ) + + self.background_tasks.append(task) + self.task_handles[task_name] = task + + return f"✅ 任务已启动: {task_name}" + + elif command == "stop": + if len(args) < 2: + return "❌ 请指定任务名称,如: tasks stop daily_cleanup" + + task_name = args[1] + + if task_name not in self.task_handles: + return f"❌ 找不到任务: {task_name}" + + task = self.task_handles[task_name] + + if not task.done(): + task.cancel() + return f"🛑 任务已取消: {task_name}" + else: + return f"ℹ️ 任务已停止: {task_name}" + + elif command == "run": + if len(args) < 2: + return "❌ 请指定任务名称,如: tasks run daily_cleanup" + + task_name = args[1] + + # 查找任务函数 + task_func = None + for schedule in self.config.get('schedules', []): + if schedule.get('name') == task_name: + task_func_name = schedule.get('task') + task_func = getattr(self, f"_task_{task_func_name}", None) + break + + if not task_func: + return f"❌ 找不到任务: {task_name}" + + # 立即执行任务 + try: + await task_func() + return f"✅ 任务执行完成: {task_name}" + except Exception as e: + return f"❌ 任务执行失败: {str(e)}" + + else: + return f"❌ 未知命令: {command}" + + except Exception as e: + logger.error(f"任务命令执行失败: {str(e)}") + return f"❌ 任务命令错误: {str(e)}" + + @plugin_command( + name="admin", + description="管理员命令", + permissions=["plugin.my_awesome_plugin.admin"] + ) + async def cmd_admin(self, *args): + """管理员命令""" + try: + if not args: + return ( + "⚡ **管理员命令**\n" + "💡 使用: admin reload 重新加载插件\n" + "💡 使用: admin debug 开启调试模式\n" + "💡 使用: admin users 查看在线用户\n" + "💡 使用: admin logs [count] 查看日志\n" + ) + + command = args[0].lower() + + if command == "reload": + # 重新加载插件 + return "🔄 插件重新加载中..." + + elif command == "debug": + # 切换调试模式 + debug_enabled = self.config.get('debug', {}).get('enable_debug_endpoints', False) + self.config.setdefault('debug', {})['enable_debug_endpoints'] = not debug_enabled + + status = "启用" if not debug_enabled else "禁用" + return f"🔧 调试模式已{status}" + + elif command == "users": + # 查看在线用户 + if not self.websocket_connections: + return "📭 没有在线用户" + + result = ["👥 **在线用户列表:**"] + for conn_id, conn_info in self.websocket_connections.items(): + user = conn_info.get('user', {}) + connected_at = conn_info.get('connected_at') + + username = user.get('username', '未知用户') + user_id = user.get('id', '未知ID') + + if connected_at: + duration = datetime.now() - connected_at + duration_str = str(duration).split('.')[0] + else: + duration_str = "未知" + + result.append(f" 👤 {username} (ID: {user_id}) - 连接时长: {duration_str}") + + return "\n".join(result) + + elif command == "logs": + # 查看日志 + count = 10 + if len(args) > 1: + try: + count = min(int(args[1]), 50) + except ValueError: + return "❌ 日志数量必须是数字" + + # 这里需要实现日志查询逻辑 + # 可以从日志文件或内存中读取 + return f"📋 显示最近 {count} 条日志 (功能待实现)" + + else: + return f"❌ 未知管理员命令: {command}" + + except Exception as e: + logger.error(f"管理员命令执行失败: {str(e)}") + return f"❌ 管理员命令错误: {str(e)}" + + @plugin_command( + name="help", + description="显示插件帮助信息" + ) + async def cmd_help(self, *args): + """帮助命令""" + try: + result = [f"📚 **{self.plugin_name} 插件帮助**"] + result.append("=" * 50) + result.append(f"版本: {self.PLUGIN_VERSION}") + result.append(f"描述: {self.config.get('description', '')}") + result.append(f"作者: {self.config.get('author', '')}") + + result.append("\n🔧 **可用命令:**") + + # 扫描所有命令方法 + command_methods = [] + for attr_name in dir(self): + if attr_name.startswith('cmd_'): + method = getattr(self, attr_name) + if hasattr(method, '_is_plugin_command'): + command_name = getattr(method, '_command_name', attr_name[4:]) + description = getattr(method, '_command_description', '') + permissions = getattr(method, '_command_permissions', []) + + # 检查权限 + has_permission = True + if permissions: + # 这里需要实现权限检查逻辑 + pass + + if has_permission: + command_methods.append((command_name, description)) + + # 按字母顺序排序 + command_methods.sort(key=lambda x: x[0]) + + for cmd_name, cmd_desc in command_methods: + result.append(f" 🟢 {cmd_name:15} - {cmd_desc}") + + result.append("\n🌐 **API接口:**") + if self.network_bridge: + info = self.network_bridge.get_network_info() + result.append(f" 基础URL: {info.get('base_url', 'N/A')}") + + for route in info.get('registered_routes', []): + result.append(f" 🔗 {route['path']} [{','.join(route['methods'])}]") + + result.append("\n💡 **使用提示:**") + result.append(" 1. 使用 help 命令查看帮助") + result.append(" 2. 使用 status 命令查看插件状态") + result.append(" 3. 使用 config 命令管理配置") + result.append(" 4. 使用 network 命令管理网络功能") + + result.append("\n⚠️ **注意事项:**") + result.append(" 1. 部分命令需要特定权限") + result.append(" 2. 修改配置后可能需要重启插件") + result.append(" 3. 网络功能依赖于框架网络服务") + + return "\n".join(result) + + except Exception as e: + logger.error(f"帮助命令执行失败: {str(e)}") + return f"❌ 帮助命令错误: {str(e)}" + + # ========== 插件生命周期方法 ========== + + # ========== 插件生命周期方法 ========== + + async def shutdown(self): + """ + 关闭插件 + + 执行顺序: + 1. 停止所有后台任务 + 2. 关闭网络连接 + 3. 清理缓存和资源 + 4. 保存状态和配置 + 5. 清理事件处理器 + 6. 发送关闭通知 + """ + try: + logger.info(f"开始关闭插件: {self.plugin_name}") + + # 1. 更新状态 + self.status.is_running = False + + # 2. 发送关闭通知 + await self._send_shutdown_notification() + + # 3. 取消所有后台任务 + logger.info("正在停止后台任务...") + task_cancellations = [] + for task in self.background_tasks: + if not task.done(): + task.cancel() + task_cancellations.append(task) + + # 等待所有任务取消完成 + if task_cancellations: + try: + await asyncio.wait(task_cancellations, timeout=10.0) + logger.info(f"后台任务已停止: {len(task_cancellations)} 个") + except asyncio.TimeoutError: + logger.warning("部分后台任务停止超时") + + # 4. 关闭WebSocket连接 + logger.info("正在关闭WebSocket连接...") + close_tasks = [] + for conn_id, conn_info in list(self.websocket_connections.items()): + try: + if not conn_info['ws'].closed: + close_task = asyncio.create_task( + conn_info['ws'].close(code=1000, message='插件关闭') + ) + close_tasks.append(close_task) + except Exception as e: + logger.error(f"关闭WebSocket连接失败 {conn_id}: {str(e)}") + + if close_tasks: + await asyncio.gather(*close_tasks, return_exceptions=True) + + self.websocket_connections.clear() + + # 5. 清理事件处理器 + logger.info("正在清理事件处理器...") + if hasattr(self.bridge, 'cleanup_plugin_subscriptions'): + self.bridge.cleanup_plugin_subscriptions(self.plugin_name) + elif hasattr(self.bridge, 'unsubscribe_all'): + await self.bridge.unsubscribe_all(self.plugin_name) + else: + logger.warning("无法找到事件处理器清理方法,手动清理") + for event_type in list(self.event_handlers.keys()): + try: + await self.bridge.unsubscribe_plugin( + self.plugin_name, + f"event.{event_type}" + ) + except Exception as e: + logger.debug(f"清理事件处理器失败 {event_type}: {str(e)}") + + # 6. 清理缓存 + logger.info("正在清理缓存...") + self.cache.clear() + self.cache_ttl.clear() + + # 清理Redis连接(如果存在) + if hasattr(self, 'redis_client'): + try: + self.redis_client.close() + logger.debug("Redis连接已关闭") + except Exception as e: + logger.warning(f"关闭Redis连接失败: {str(e)}") + + # 7. 保存配置和状态 + logger.info("正在保存配置和状态...") + await self._save_plugin_state() + + # 8. 清理锁和资源 + logger.info("正在清理资源锁...") + self._resource_locks.clear() + + # 清理任务句柄 + self.task_handles.clear() + + # 9. 计算运行时长 + if self.status.start_time: + self.status.uptime = datetime.now() - self.status.start_time + logger.info(f"插件运行时长: {self.status.uptime}") + + # 10. 发送插件停止事件 + await self._send_plugin_stopped_event() + + logger.info(f"✅ 插件关闭完成: {self.plugin_name}") + + except Exception as e: + logger.error(f"关闭插件时出错: {str(e)}") + logger.error(traceback.format_exc()) + + # 紧急清理 + await self._emergency_shutdown() + + async def _send_shutdown_notification(self): + """发送关闭通知""" + try: + if self.network_bridge: + await self.network_bridge.broadcast_websocket({ + "type": "system", + "message": f"插件 {self.plugin_name} 正在关闭...", + "timestamp": datetime.now().isoformat() + }) + + # 发送框架事件 + await self.bridge.publish_to_plugin( + "framework", + "event.plugin.shutting_down", + { + "plugin_name": self.plugin_name, + "timestamp": datetime.now().isoformat() + } + ) + except Exception as e: + logger.debug(f"发送关闭通知失败: {str(e)}") + + async def _save_plugin_state(self): + """保存插件状态""" + try: + state_data = { + "plugin_name": self.plugin_name, + "version": self.PLUGIN_VERSION, + "status": { + "last_run": datetime.now().isoformat(), + "request_count": self.status.request_count, + "error_count": self.status.error_count, + "uptime": str(self.status.uptime) if self.status.uptime else None + }, + "config": self.config, + "cache_stats": { + "size": len(self.cache), + "keys": list(self.cache.keys())[:20] # 只保存前20个键 + }, + "websocket_stats": { + "max_connections": len(self.websocket_connections) + } + } + + state_path = Path(f"data/plugins/{self.plugin_name}/state.json") + state_path.parent.mkdir(parents=True, exist_ok=True) + + with open(state_path, 'w', encoding='utf-8') as f: + json.dump(state_data, f, ensure_ascii=False, indent=2) + + logger.debug(f"插件状态已保存: {state_path}") + + except Exception as e: + logger.warning(f"保存插件状态失败: {str(e)}") + + async def _send_plugin_stopped_event(self): + """发送插件停止事件""" + try: + await self.bridge.publish_to_plugin( + "framework", + "event.plugin.stopped", + { + "plugin_name": self.plugin_name, + "version": self.PLUGIN_VERSION, + "timestamp": datetime.now().isoformat(), + "uptime": str(self.status.uptime) if self.status.uptime else None + } + ) + except Exception as e: + logger.debug(f"发送插件停止事件失败: {str(e)}") + + async def _emergency_shutdown(self): + """紧急关闭""" + try: + logger.critical("执行紧急关闭...") + + # 强制取消所有任务 + for task in self.background_tasks: + if not task.done(): + task.cancel() + + # 强制关闭WebSocket连接 + for conn_info in self.websocket_connections.values(): + try: + if not conn_info['ws'].closed: + conn_info['ws'].close() + except: + pass + + # 清理内存 + self.cache.clear() + self.websocket_connections.clear() + self.task_handles.clear() + + logger.critical("紧急关闭完成") + + except Exception as e: + logger.critical(f"紧急关闭时出错: {str(e)}") +``` + +#### 2.5.2 插件类核心方法详解 + +##### 2.5.2.1 生命周期管理方法 + +```python +class Plugin: + """ + 插件生命周期管理方法详解 + """ + + async def initialize(self) -> bool: + """ + 插件初始化 - 框架调用的主要入口点 + + 返回: + bool: 初始化是否成功 + + 执行流程: + 1. 基础设置和环境检查 + 2. 配置验证和加载 + 3. 服务管理器获取 + 4. 网络功能初始化 + 5. 事件处理器注册 + 6. 后台任务启动 + 7. 状态标记为运行中 + """ + try: + # 1. 环境检查 + if not await self._check_environment(): + logger.error("环境检查失败") + return False + + # 2. 配置验证 + if not await self._validate_config(): + logger.error("配置验证失败") + return False + + # 3. 服务管理器获取 + if not await self._setup_service_manager(): + logger.warning("服务管理器获取失败,部分功能受限") + + # 4. 网络功能初始化 + network_success = await self._initialize_network() + if not network_success: + logger.warning("网络功能初始化失败,将以受限模式运行") + + # 5. 事件处理器注册 + await self._register_event_handlers() + + # 6. 后台任务启动 + await self._start_background_tasks() + + # 7. 状态标记 + self.status.is_running = True + self.status.start_time = datetime.now() + + logger.info(f"✅ 插件初始化成功: {self.plugin_name}") + return True + + except Exception as e: + logger.error(f"❌ 插件初始化失败: {str(e)}") + logger.error(traceback.format_exc()) + await self._emergency_cleanup() + return False + + async def _check_environment(self) -> bool: + """检查运行环境""" + try: + # 检查Python版本 + import sys + if sys.version_info < (3, 8): + logger.error("需要Python 3.8或更高版本") + return False + + # 检查必要目录 + required_dirs = [ + f"plugins/{self.plugin_name}", + f"data/plugins/{self.plugin_name}", + f"logs/plugins/{self.plugin_name}" + ] + + for dir_path in required_dirs: + path = Path(dir_path) + if not path.exists(): + try: + path.mkdir(parents=True, exist_ok=True) + logger.debug(f"创建目录: {dir_path}") + except Exception as e: + logger.error(f"无法创建目录 {dir_path}: {str(e)}") + return False + + # 检查依赖包 + deps_ok = await self._check_dependencies() + if not deps_ok: + logger.error("依赖包检查失败") + return False + + return True + + except Exception as e: + logger.error(f"环境检查失败: {str(e)}") + return False + + async def _check_dependencies(self) -> bool: + """检查插件依赖""" + try: + dependencies = self.config.get('dependencies', {}) + required = dependencies.get('required', []) + optional = dependencies.get('optional', []) + + missing_required = [] + + for dep in required: + # 解析依赖字符串,如 "requests>=2.25.0" + package_name = dep.split('>=')[0].split('==')[0].split('<=')[0].strip() + + try: + import importlib + importlib.import_module(package_name) + logger.debug(f"依赖检查通过: {package_name}") + except ImportError: + missing_required.append(package_name) + logger.warning(f"缺少依赖包: {package_name}") + + if missing_required: + logger.error(f"缺少必需依赖: {', '.join(missing_required)}") + return False + + # 检查可选依赖 + for dep in optional: + package_name = dep.split('>=')[0].split('==')[0].split('<=')[0].strip() + try: + import importlib + importlib.import_module(package_name) + logger.debug(f"可选依赖可用: {package_name}") + except ImportError: + logger.info(f"可选依赖未安装: {package_name}") + + return True + + except Exception as e: + logger.error(f"依赖检查失败: {str(e)}") + return False +``` + +##### 2.5.2.2 配置管理方法 + +```python + async def _validate_config(self) -> bool: + """验证配置有效性""" + try: + # 基础配置验证 + required_fields = ['name', 'version', 'description', 'author'] + for field in required_fields: + if field not in self.config: + logger.error(f"缺少必需配置字段: {field}") + return False + + # 版本号格式验证 + version = self.config.get('version', '') + import re + if not re.match(r'^\d+\.\d+\.\d+(?:[-.]\w+)?$', version): + logger.error(f"版本号格式错误: {version}") + return False + + # 设置项验证 + settings = self.config.get('settings', {}) + if 'enabled' not in settings: + logger.warning("settings.enabled 未设置,使用默认值 True") + settings['enabled'] = True + + # 日志级别验证 + log_level = settings.get('log_level', 'INFO') + valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] + if log_level not in valid_levels: + logger.warning(f"无效的日志级别: {log_level},使用默认值 INFO") + settings['log_level'] = 'INFO' + + # 更新配置 + self.config['settings'] = settings + + # 功能配置验证 + features = self.config.get('features', {}) + if 'network' in features: + network_config = features['network'] + if network_config.get('enable_http', False) or network_config.get('enable_websocket', False): + if not network_config.get('enable_cors', True): + logger.warning("启用网络功能但禁用CORS可能导致跨域问题") + + logger.info("配置验证通过") + return True + + except Exception as e: + logger.error(f"配置验证失败: {str(e)}") + return False +``` + +#### 2.5.3 事件处理与命令注册 + +##### 2.5.3.1 事件处理系统 + +```python + async def _setup_event_system(self): + """设置事件处理系统""" + try: + # 创建事件队列 + self.event_queue = asyncio.Queue(maxsize=1000) + + # 启动事件处理器 + self.event_handler_task = asyncio.create_task( + self._event_handler_loop() + ) + self.background_tasks.append(self.event_handler_task) + + # 注册核心事件处理器 + await self._register_core_event_handlers() + + logger.info("事件处理系统已启动") + + except Exception as e: + logger.error(f"设置事件处理系统失败: {str(e)}") + + async def _event_handler_loop(self): + """事件处理循环""" + while self.status.is_running: + try: + # 从队列获取事件 + event = await self.event_queue.get() + + # 处理事件 + await self._process_event(event) + + # 标记任务完成 + self.event_queue.task_done() + + except asyncio.CancelledError: + logger.info("事件处理循环被取消") + break + except Exception as e: + logger.error(f"事件处理出错: {str(e)}") + await asyncio.sleep(1) # 出错后等待1秒 + + async def _process_event(self, event: dict): + """处理单个事件""" + try: + event_type = event.get('type') + event_data = event.get('data', {}) + + # 查找事件处理器 + handler = self.event_handlers.get(event_type) + + if handler: + # 执行处理器 + await handler(event_data) + else: + # 默认处理器 + await self._handle_unknown_event(event) + + except Exception as e: + logger.error(f"处理事件失败 {event.get('type', 'unknown')}: {str(e)}") + + async def _register_core_event_handlers(self): + """注册核心事件处理器""" + core_handlers = { + # 插件相关事件 + 'plugin.enable': self._handle_plugin_enable, + 'plugin.disable': self._handle_plugin_disable, + 'plugin.reload': self._handle_plugin_reload, + + # 用户相关事件 + 'user.created': self._handle_user_created, + 'user.deleted': self._handle_user_deleted, + 'user.updated': self._handle_user_updated, + + # 系统事件 + 'system.start': self._handle_system_start, + 'system.stop': self._handle_system_stop, + 'system.error': self._handle_system_error, + + # 自定义事件 + 'custom.notification': self._handle_custom_notification, + 'custom.alert': self._handle_custom_alert, + } + + # 注册到事件处理器映射 + self.event_handlers.update(core_handlers) + + # 订阅框架事件 + for event_type in core_handlers.keys(): + try: + await self.bridge.subscribe_event( + self.plugin_name, + event_type, + core_handlers[event_type] + ) + except Exception as e: + logger.warning(f"订阅事件失败 {event_type}: {str(e)}") +``` + +##### 2.5.3.2 命令注册与执行 + +```python + async def _register_commands(self): + """注册插件命令""" + try: + logger.info("开始注册插件命令...") + + # 扫描命令方法 + command_methods = [] + for attr_name in dir(self): + if attr_name.startswith('cmd_'): + method = getattr(self, attr_name) + if hasattr(method, '_is_plugin_command'): + command_methods.append(method) + + # 注册到框架 + for method in command_methods: + command_name = getattr(method, '_command_name', method.__name__[4:]) + description = getattr(method, '_command_description', method.__doc__ or '') + permissions = getattr(method, '_command_permissions', []) + + # 构建完整命令名 + full_command_name = f"{self.plugin_name}_{command_name}" + + # 注册命令 + await self.bridge.register_command( + self.plugin_name, + full_command_name, + method, + description, + permissions + ) + + logger.debug(f"命令注册: {full_command_name}") + + logger.info(f"命令注册完成,共 {len(command_methods)} 个命令") + + except Exception as e: + logger.error(f"命令注册失败: {str(e)}") + + async def _execute_command(self, command: str, args: list) -> str: + """执行命令的统一接口""" + try: + # 查找命令方法 + method_name = f"cmd_{command}" + if not hasattr(self, method_name): + return f"❌ 未知命令: {command}" + + method = getattr(self, method_name) + + # 检查是否是插件命令 + if not hasattr(method, '_is_plugin_command'): + return f"❌ 不是有效的插件命令: {command}" + + # 执行命令 + result = await method(*args) + return result + + except Exception as e: + logger.error(f"执行命令失败 {command}: {str(e)}") + return f"❌ 命令执行错误: {str(e)}" +``` + +#### 2.5.4 异常处理与资源管理 + +##### 2.5.4.1 异常处理框架 + +```python +class PluginExceptionHandler: + """插件异常处理器""" + + def __init__(self, plugin_instance): + self.plugin = plugin_instance + self.error_history = [] + self.max_error_history = 100 + + async def handle_exception(self, exception: Exception, context: str = "") -> dict: + """处理异常并返回用户友好的错误信息""" + try: + # 记录异常 + error_record = { + 'timestamp': datetime.now().isoformat(), + 'exception_type': type(exception).__name__, + 'exception_message': str(exception), + 'context': context, + 'traceback': traceback.format_exc() + } + + # 添加到历史 + self.error_history.append(error_record) + if len(self.error_history) > self.max_error_history: + self.error_history.pop(0) + + # 更新插件状态 + self.plugin.status.error_count += 1 + self.plugin.status.last_error = str(exception) + + # 根据异常类型处理 + if isinstance(exception, (PermissionError, PluginPermissionError)): + return self._handle_permission_error(exception, context) + elif isinstance(exception, (ConnectionError, TimeoutError)): + return self._handle_network_error(exception, context) + elif isinstance(exception, ValueError): + return self._handle_validation_error(exception, context) + elif isinstance(exception, FileNotFoundError): + return self._handle_file_error(exception, context) + else: + return self._handle_generic_error(exception, context) + + except Exception as e: + # 如果异常处理器本身出错 + logger.critical(f"异常处理器出错: {str(e)}") + return { + 'success': False, + 'error': '内部服务器错误', + 'message': '系统遇到意外错误' + } + + def _handle_permission_error(self, exception: Exception, context: str) -> dict: + """处理权限错误""" + logger.warning(f"权限错误 [{context}]: {str(exception)}") + return { + 'success': False, + 'error': '权限不足', + 'message': f'执行 {context} 需要特定权限', + 'details': str(exception) + } + + def _handle_network_error(self, exception: Exception, context: str) -> dict: + """处理网络错误""" + logger.error(f"网络错误 [{context}]: {str(exception)}") + return { + 'success': False, + 'error': '网络连接失败', + 'message': f'{context} 网络连接失败,请检查网络设置', + 'details': str(exception) + } + + def _handle_validation_error(self, exception: Exception, context: str) -> dict: + """处理验证错误""" + logger.warning(f"验证错误 [{context}]: {str(exception)}") + return { + 'success': False, + 'error': '输入验证失败', + 'message': f'{context} 输入数据无效', + 'details': str(exception) + } + + def _handle_file_error(self, exception: Exception, context: str) -> dict: + """处理文件错误""" + logger.error(f"文件错误 [{context}]: {str(exception)}") + return { + 'success': False, + 'error': '文件操作失败', + 'message': f'{context} 文件操作失败', + 'details': str(exception) + } + + def _handle_generic_error(self, exception: Exception, context: str) -> dict: + """处理通用错误""" + logger.error(f"通用错误 [{context}]: {str(exception)}") + return { + 'success': False, + 'error': '操作失败', + 'message': f'{context} 执行过程中发生错误', + 'details': str(exception) if self.plugin.config.get('debug', {}).get('show_detailed_errors', False) else '请联系系统管理员' + } +``` + +##### 2.5.4.2 资源管理与清理 + +```python +class PluginResourceManager: + """插件资源管理器""" + + def __init__(self, plugin_instance): + self.plugin = plugin_instance + self.resources = { + 'files': [], # 打开的文件 + 'connections': [], # 网络连接 + 'locks': [], # 锁资源 + 'tasks': [], # 后台任务 + 'cache': [] # 缓存资源 + } + + def register_resource(self, resource_type: str, resource, metadata: dict = None): + """注册资源""" + if resource_type not in self.resources: + self.resources[resource_type] = [] + + resource_record = { + 'resource': resource, + 'type': type(resource).__name__, + 'registered_at': datetime.now(), + 'metadata': metadata or {} + } + + self.resources[resource_type].append(resource_record) + + # 自动注册清理函数 + if hasattr(resource, 'close'): + self.plugin._cleanup_functions.append(resource.close) + elif hasattr(resource, 'cleanup'): + self.plugin._cleanup_functions.append(resource.cleanup) + + async def cleanup_all(self, force: bool = False): + """清理所有资源""" + cleanup_results = [] + + # 按逆序清理(后创建的先清理) + for resource_type in reversed(list(self.resources.keys())): + resources = self.resources[resource_type].copy() + + for resource_record in reversed(resources): + try: + result = await self._cleanup_resource(resource_record, force) + cleanup_results.append((resource_type, result)) + except Exception as e: + logger.error(f"清理资源失败 {resource_type}: {str(e)}") + cleanup_results.append((resource_type, False)) + + # 执行注册的清理函数 + for cleanup_func in self.plugin._cleanup_functions: + try: + if asyncio.iscoroutinefunction(cleanup_func): + await cleanup_func() + else: + cleanup_func() + except Exception as e: + logger.error(f"清理函数执行失败: {str(e)}") + + return cleanup_results + + async def _cleanup_resource(self, resource_record: dict, force: bool) -> bool: + """清理单个资源""" + resource = resource_record['resource'] + resource_type = resource_record['type'] + + try: + # 根据资源类型选择清理方式 + if resource_type == 'File': + if hasattr(resource, 'closed') and not resource.closed: + resource.close() + return True + + elif resource_type in ['Socket', 'Connection']: + if hasattr(resource, 'close'): + resource.close() + return True + + elif resource_type == 'Lock': + # 锁通常在上下文管理器中自动释放 + pass + + elif resource_type == 'Task': + if hasattr(resource, 'cancel') and not resource.done(): + if force: + resource.cancel() + return True + + elif resource_type == 'Cache': + if hasattr(resource, 'clear'): + resource.clear() + return True + + # 通用清理 + if hasattr(resource, 'close'): + resource.close() + elif hasattr(resource, 'disconnect'): + resource.disconnect() + elif hasattr(resource, 'shutdown'): + resource.shutdown() + + return True + + except Exception as e: + logger.warning(f"清理资源失败 {resource_type}: {str(e)}") + return False + + def get_resource_stats(self) -> dict: + """获取资源统计信息""" + stats = { + 'total_resources': 0, + 'by_type': {}, + 'memory_usage': self._estimate_memory_usage() + } + + for resource_type, resources in self.resources.items(): + stats['by_type'][resource_type] = len(resources) + stats['total_resources'] += len(resources) + + return stats + + def _estimate_memory_usage(self) -> int: + """估计内存使用量(粗略)""" + total_size = 0 + + # 遍历所有资源 + for resource_type, resources in self.resources.items(): + for resource_record in resources: + resource = resource_record['resource'] + + # 尝试获取大小 + try: + if hasattr(resource, '__sizeof__'): + total_size += resource.__sizeof__() + elif isinstance(resource, (str, bytes, bytearray)): + total_size += len(resource) + except: + pass + + return total_size +``` + +### 2.5.5 插件配置持久化与状态恢复 + +```python + async def save_state(self) -> bool: + """ + 保存插件状态 + + 保存内容包括: + 1. 当前配置 + 2. 运行状态 + 3. 缓存数据 + 4. 用户会话 + 5. 任务状态 + """ + try: + state_data = { + 'plugin_info': { + 'name': self.plugin_name, + 'version': self.PLUGIN_VERSION, + 'last_saved': datetime.now().isoformat() + }, + 'config': self.config, + 'status': { + 'is_running': self.status.is_running, + 'start_time': self.status.start_time.isoformat() if self.status.start_time else None, + 'request_count': self.status.request_count, + 'error_count': self.status.error_count, + 'last_error': self.status.last_error + }, + 'metrics': { + 'requests_per_second': self.metrics.requests_per_second, + 'average_response_time': self.metrics.average_response_time, + 'active_connections': self.metrics.active_connections, + 'cache_hit_rate': self.metrics.cache_hit_rate + }, + 'cache_summary': { + 'total_items': len(self.cache), + 'keys': list(self.cache.keys())[:50] # 只保存前50个键 + }, + 'background_tasks': [ + { + 'name': name, + 'status': 'running' if not task.done() else 'completed', + 'cancelled': task.cancelled() + } + for name, task in self.task_handles.items() + ] + } + + # 创建状态目录 + state_dir = Path(f"data/plugins/{self.plugin_name}/state") + state_dir.mkdir(parents=True, exist_ok=True) + + # 保存状态文件 + state_file = state_dir / f"state_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + backup_file = state_dir / "state_backup.json" + + # 先备份当前状态 + if backup_file.exists(): + backup_file.unlink() + + # 写入新状态 + with open(state_file, 'w', encoding='utf-8') as f: + json.dump(state_data, f, ensure_ascii=False, indent=2) + + # 创建软链接到最新状态 + latest_link = state_dir / "state_latest.json" + if latest_link.exists(): + latest_link.unlink() + latest_link.symlink_to(state_file.name) + + # 保留最近10个状态文件 + self._cleanup_old_state_files(state_dir) + + logger.info(f"插件状态已保存: {state_file}") + return True + + except Exception as e: + logger.error(f"保存插件状态失败: {str(e)}") + return False + + def _cleanup_old_state_files(self, state_dir: Path, keep_count: int = 10): + """清理旧的状态文件""" + try: + # 获取所有状态文件 + state_files = list(state_dir.glob("state_*.json")) + + # 按修改时间排序 + state_files.sort(key=lambda x: x.stat().st_mtime, reverse=True) + + # 删除超出保留数量的文件 + for state_file in state_files[keep_count:]: + try: + state_file.unlink() + logger.debug(f"清理旧状态文件: {state_file}") + except Exception as e: + logger.warning(f"无法清理状态文件 {state_file}: {str(e)}") + + except Exception as e: + logger.error(f"清理状态文件失败: {str(e)}") + + async def restore_state(self) -> bool: + """ + 恢复插件状态 + + 从保存的状态文件恢复: + 1. 恢复配置 + 2. 恢复缓存 + 3. 恢复任务状态 + 4. 恢复会话数据 + """ + try: + state_file = Path(f"data/plugins/{self.plugin_name}/state/state_latest.json") + + if not state_file.exists(): + logger.info("没有找到状态文件,使用默认状态") + return False + + # 读取状态文件 + with open(state_file, 'r', encoding='utf-8') as f: + state_data = json.load(f) + + # 验证状态文件 + if not self._validate_state_data(state_data): + logger.warning("状态文件验证失败,使用默认状态") + return False + + # 恢复配置 + if 'config' in state_data: + self.config.update(state_data['config']) + logger.info("配置已从状态文件恢复") + + # 恢复状态信息 + if 'status' in state_data: + status_data = state_data['status'] + self.status.request_count = status_data.get('request_count', 0) + self.status.error_count = status_data.get('error_count', 0) + logger.info("运行状态已恢复") + + # 恢复缓存 + if 'cache_summary' in state_data: + # 这里可以根据需要实现缓存的持久化和恢复 + logger.info("缓存摘要已加载") + + logger.info(f"插件状态已从 {state_file} 恢复") + return True + + except Exception as e: + logger.error(f"恢复插件状态失败: {str(e)}") + return False + + def _validate_state_data(self, state_data: dict) -> bool: + """验证状态数据有效性""" + try: + # 检查必需字段 + required_fields = ['plugin_info', 'config', 'status'] + for field in required_fields: + if field not in state_data: + logger.error(f"状态文件缺少必需字段: {field}") + return False + + # 验证插件信息 + plugin_info = state_data['plugin_info'] + if plugin_info.get('name') != self.plugin_name: + logger.error(f"状态文件插件名称不匹配: {plugin_info.get('name')}") + return False + + # 验证版本兼容性 + saved_version = plugin_info.get('version', '') + current_version = self.PLUGIN_VERSION + + # 简单的版本兼容性检查 + if saved_version.split('.')[0] != current_version.split('.')[0]: + logger.warning(f"主版本不匹配: 保存版本 {saved_version}, 当前版本 {current_version}") + # 主版本不同可能不兼容 + + return True + + except Exception as e: + logger.error(f"状态数据验证失败: {str(e)}") + return False +``` + + +## 三、插件生命周期管理 + +### 3.1 插件完整生命周期 + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 加载阶段 │──▶│ 初始化阶段 │──▶│ 运行阶段 │──▶│ 关闭阶段 │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ + │ │ │ │ + ▼ ▼ ▼ ▼ +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 配置文件解析 │ │ 权限申请验证 │ │ 命令处理 │ │ 资源清理 │ +├─────────────┤ ├─────────────┤ ├─────────────┤ ├─────────────┤ +│ 依赖检查 │ │ 网络路由注册 │ │ 事件处理 │ │ 连接关闭 │ +├─────────────┤ ├─────────────┤ ├─────────────┤ ├─────────────┤ +│ 模块导入 │ │ 后台任务启动 │ │ API服务 │ │ 状态保存 │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ +``` + +### 3.2 继续完成 shutdown 方法 + +```python + async def shutdown(self): + """ + 关闭插件 + + 执行顺序: + 1. 停止所有后台任务 + 2. 关闭网络连接 + 3. 清理缓存和资源 + 4. 保存状态和配置 + 5. 清理事件处理器 + 6. 发送关闭通知 + """ + try: + logger.info(f"开始关闭插件: {self.plugin_name}") + + # 1. 更新状态 + self.status.is_running = False + + # 2. 发送关闭通知 + await self._send_shutdown_notification() + + # 3. 取消所有后台任务 + logger.info("正在停止后台任务...") + task_cancellations = [] + for task in self.background_tasks: + if not task.done(): + task.cancel() + task_cancellations.append(task) + + # 等待所有任务取消完成 + if task_cancellations: + try: + await asyncio.wait(task_cancellations, timeout=10.0) + logger.info(f"后台任务已停止: {len(task_cancellations)} 个") + except asyncio.TimeoutError: + logger.warning("部分后台任务停止超时") + + # 4. 关闭WebSocket连接 + logger.info("正在关闭WebSocket连接...") + close_tasks = [] + for conn_id, conn_info in list(self.websocket_connections.items()): + try: + if not conn_info['ws'].closed: + close_task = asyncio.create_task( + conn_info['ws'].close(code=1000, message='插件关闭') + ) + close_tasks.append(close_task) + except Exception as e: + logger.error(f"关闭WebSocket连接失败 {conn_id}: {str(e)}") + + if close_tasks: + await asyncio.gather(*close_tasks, return_exceptions=True) + + self.websocket_connections.clear() + + # 5. 清理事件处理器 + logger.info("正在清理事件处理器...") + if hasattr(self.bridge, 'cleanup_plugin_subscriptions'): + self.bridge.cleanup_plugin_subscriptions(self.plugin_name) + + # 6. 清理缓存 + logger.info("正在清理缓存...") + self.cache.clear() + self.cache_ttl.clear() + + # 7. 保存配置和状态 + logger.info("正在保存配置和状态...") + await self._save_plugin_state() + + # 8. 清理锁和资源 + logger.info("正在清理资源锁...") + self._resource_locks.clear() + + # 9. 计算运行时长 + if self.status.start_time: + self.status.uptime = datetime.now() - self.status.start_time + logger.info(f"插件运行时长: {self.status.uptime}") + + logger.info(f"✅ 插件关闭完成: {self.plugin_name}") + + # 10. 发送关闭完成事件 + await self._send_shutdown_complete_event() + + except Exception as e: + logger.error(f"关闭插件时出错: {str(e)}") + logger.error(traceback.format_exc()) + + # 紧急清理 + await self._emergency_shutdown() + + async def _send_shutdown_notification(self): + """发送关闭通知""" + try: + if self.network_bridge: + await self.network_bridge.broadcast_websocket({ + "type": "system", + "message": f"插件 {self.plugin_name} 正在关闭...", + "timestamp": datetime.now().isoformat() + }) + except Exception as e: + logger.debug(f"发送关闭通知失败: {str(e)}") + + async def _save_plugin_state(self): + """保存插件状态""" + try: + state_data = { + "plugin_name": self.plugin_name, + "version": self.PLUGIN_VERSION, + "status": { + "last_run": datetime.now().isoformat(), + "request_count": self.status.request_count, + "error_count": self.status.error_count, + "uptime": str(self.status.uptime) if self.status.uptime else None + }, + "config": self.config, + "cache_stats": { + "size": len(self.cache), + "keys": list(self.cache.keys())[:20] # 只保存前20个键 + } + } + + state_path = Path(f"data/plugins/{self.plugin_name}/state.json") + state_path.parent.mkdir(parents=True, exist_ok=True) + + with open(state_path, 'w', encoding='utf-8') as f: + json.dump(state_data, f, ensure_ascii=False, indent=2) + + logger.debug(f"插件状态已保存: {state_path}") + + except Exception as e: + logger.warning(f"保存插件状态失败: {str(e)}") + + async def _send_shutdown_complete_event(self): + """发送关闭完成事件""" + try: + await self.bridge.publish_to_plugin( + "framework", + "event.plugin.shutdown", + { + "plugin_name": self.plugin_name, + "version": self.PLUGIN_VERSION, + "timestamp": datetime.now().isoformat(), + "uptime": str(self.status.uptime) if self.status.uptime else None + } + ) + except Exception as e: + logger.debug(f"发送关闭完成事件失败: {str(e)}") + + async def _emergency_shutdown(self): + """紧急关闭""" + try: + logger.critical("执行紧急关闭...") + + # 强制取消所有任务 + for task in self.background_tasks: + if not task.done(): + task.cancel() + + # 强制关闭WebSocket连接 + for conn_info in self.websocket_connections.values(): + try: + if not conn_info['ws'].closed: + conn_info['ws'].close() + except: + pass + + # 清理内存 + self.cache.clear() + self.websocket_connections.clear() + + logger.critical("紧急关闭完成") + + except Exception as e: + logger.critical(f"紧急关闭时出错: {str(e)}") +``` + +## 四、插件开发最佳实践 + +### 4.1 错误处理最佳实践 + +```python +class PluginError(Exception): + """插件基础异常类""" + pass + +class PluginInitializationError(PluginError): + """插件初始化异常""" + pass + +class PluginPermissionError(PluginError): + """插件权限异常""" + pass + +class PluginNetworkError(PluginError): + """插件网络异常""" + pass + +def error_handler(func): + """错误处理装饰器""" + @wraps(func) + async def wrapper(*args, **kwargs): + try: + return await func(*args, **kwargs) + except PluginPermissionError as e: + logger.error(f"权限错误: {str(e)}") + return {"error": "权限不足", "details": str(e)} + except PluginNetworkError as e: + logger.error(f"网络错误: {str(e)}") + return {"error": "网络错误", "details": str(e)} + except asyncio.TimeoutError as e: + logger.error(f"操作超时: {str(e)}") + return {"error": "操作超时", "details": str(e)} + except Exception as e: + logger.error(f"未预期的错误: {str(e)}") + logger.error(traceback.format_exc()) + return {"error": "内部服务器错误", "details": str(e)} + return wrapper + +class SafePlugin: + """安全插件基类""" + + def __init__(self): + self._error_context = [] + + def _record_error_context(self, context: str): + """记录错误上下文""" + self._error_context.append({ + "timestamp": datetime.now().isoformat(), + "context": context + }) + # 只保留最近的100条错误上下文 + if len(self._error_context) > 100: + self._error_context.pop(0) + + async def _safe_execute(self, func, *args, **kwargs): + """安全执行函数""" + try: + return await func(*args, **kwargs) + except Exception as e: + # 记录错误上下文 + error_info = { + "function": func.__name__, + "args": str(args), + "kwargs": str(kwargs), + "error": str(e), + "traceback": traceback.format_exc(), + "context": self._error_context.copy() + } + + # 保存错误日志 + await self._log_error(error_info) + + # 根据错误类型处理 + if isinstance(e, (PermissionError, PluginPermissionError)): + raise PluginPermissionError(f"权限错误: {str(e)}") + elif isinstance(e, (ConnectionError, TimeoutError)): + raise PluginNetworkError(f"网络错误: {str(e)}") + else: + raise PluginError(f"插件错误: {str(e)}") + + async def _log_error(self, error_info: dict): + """记录错误日志""" + error_log = { + "plugin": self.plugin_name, + "timestamp": datetime.now().isoformat(), + "error": error_info + } + + # 保存到文件 + log_path = Path(f"logs/plugins/{self.plugin_name}/errors") + log_path.mkdir(parents=True, exist_ok=True) + + log_file = log_path / f"error_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + + try: + with open(log_file, 'w', encoding='utf-8') as f: + json.dump(error_log, f, ensure_ascii=False, indent=2) + except Exception: + pass +``` + +### 4.2 性能优化最佳实践 + +```python +class OptimizedPlugin: + """性能优化插件基类""" + + def __init__(self): + # 缓存配置 + self._cache_config = { + "max_size": 1000, + "ttl": 3600, + "cleanup_interval": 300 + } + + # 性能监控 + self._performance_stats = { + "request_times": [], + "cache_hits": 0, + "cache_misses": 0, + "db_queries": 0 + } + + # 连接池 + self._connection_pools = {} + + # 异步锁 + self._async_locks = {} + + def _get_cache_key(self, func_name: str, *args, **kwargs) -> str: + """生成缓存键""" + arg_str = str(args) + kwarg_str = str(sorted(kwargs.items())) + return f"{func_name}:{hashlib.md5((arg_str + kwarg_str).encode()).hexdigest()}" + + async def _cached_execute(self, func, ttl: int = None, *args, **kwargs): + """带缓存执行""" + cache_key = self._get_cache_key(func.__name__, *args, **kwargs) + + # 检查缓存 + if cache_key in self.cache: + # 检查TTL + if cache_key in self.cache_ttl: + if datetime.now() > self.cache_ttl[cache_key]: + del self.cache[cache_key] + del self.cache_ttl[cache_key] + else: + self._performance_stats["cache_hits"] += 1 + return self.cache[cache_key] + + # 缓存未命中,执行函数 + self._performance_stats["cache_misses"] += 1 + result = await func(*args, **kwargs) + + # 存入缓存 + self.cache[cache_key] = result + if ttl: + self.cache_ttl[cache_key] = datetime.now() + timedelta(seconds=ttl) + + # 清理过期的缓存 + await self._cleanup_expired_cache() + + return result + + async def _cleanup_expired_cache(self): + """清理过期缓存""" + now = datetime.now() + expired_keys = [] + + for key, expiry in self.cache_ttl.items(): + if now > expiry: + expired_keys.append(key) + + for key in expired_keys: + if key in self.cache: + del self.cache[key] + if key in self.cache_ttl: + del self.cache_ttl[key] + + # 如果缓存太大,清理最旧的项 + if len(self.cache) > self._cache_config["max_size"]: + # 简单的LRU策略:删除最早的缓存项 + keys_to_remove = list(self.cache.keys())[:100] # 删除前100个 + for key in keys_to_remove: + if key in self.cache: + del self.cache[key] + if key in self.cache_ttl: + del self.cache_ttl[key] + + def _get_async_lock(self, lock_name: str) -> asyncio.Lock: + """获取异步锁""" + if lock_name not in self._async_locks: + self._async_locks[lock_name] = asyncio.Lock() + return self._async_locks[lock_name] + + async def _rate_limited_execute(self, func, rate_limit: int = 10, *args, **kwargs): + """限速执行""" + lock = self._get_async_lock(f"rate_limit_{func.__name__}") + + async with lock: + # 检查速率限制 + current_time = time.time() + key = f"rate_{func.__name__}" + + if key not in self.rate_limiter: + self.rate_limiter[key] = [] + + # 清理旧的记录 + self.rate_limiter[key] = [ + t for t in self.rate_limiter[key] + if current_time - t < 60 # 1分钟窗口 + ] + + # 检查是否超限 + if len(self.rate_limiter[key]) >= rate_limit: + await asyncio.sleep(1) # 等待1秒 + # 重新检查 + self.rate_limiter[key] = [ + t for t in self.rate_limiter[key] + if current_time - t < 60 + ] + + # 执行函数 + self.rate_limiter[key].append(current_time) + return await func(*args, **kwargs) + + def _measure_performance(self, func): + """性能测量装饰器""" + @wraps(func) + async def wrapper(*args, **kwargs): + start_time = time.time() + + try: + result = await func(*args, **kwargs) + return result + finally: + end_time = time.time() + execution_time = end_time - start_time + + # 记录执行时间 + self._performance_stats["request_times"].append(execution_time) + + # 只保留最近的1000个记录 + if len(self._performance_stats["request_times"]) > 1000: + self._performance_stats["request_times"].pop(0) + + # 记录慢查询 + if execution_time > 1.0: # 超过1秒 + logger.warning( + f"慢查询: {func.__name__} 耗时 {execution_time:.2f}秒" + ) + + return wrapper +``` + +### 4.3 安全最佳实践 + +```python +class SecurePlugin: + """安全插件基类""" + + def __init__(self): + # 输入验证器 + self._validators = { + "email": self._validate_email, + "url": self._validate_url, + "ip_address": self._validate_ip_address, + "filename": self._validate_filename, + "sql_injection": self._check_sql_injection, + "xss": self._check_xss + } + + # 安全配置 + self._security_config = { + "max_file_size": 10 * 1024 * 1024, # 10MB + "allowed_file_types": ['.txt', '.json', '.yaml', '.csv', '.log'], + "max_request_size": 1024 * 1024, # 1MB + "rate_limit_per_ip": 100, + "session_timeout": 3600 + } + + def _validate_input(self, input_data, validators=None): + """验证输入数据""" + if validators is None: + validators = ["sql_injection", "xss"] + + errors = [] + + # 递归验证嵌套结构 + def _validate_recursive(data, path=""): + if isinstance(data, dict): + for key, value in data.items(): + current_path = f"{path}.{key}" if path else key + _validate_recursive(value, current_path) + elif isinstance(data, list): + for i, item in enumerate(data): + current_path = f"{path}[{i}]" + _validate_recursive(item, current_path) + elif isinstance(data, str): + for validator_name in validators: + if validator_name in self._validators: + is_valid, error_msg = self._validators[validator_name](data) + if not is_valid: + errors.append(f"{path}: {error_msg}") + + _validate_recursive(input_data) + + if errors: + raise PluginError(f"输入验证失败: {', '.join(errors)}") + + return True + + def _validate_email(self, email: str) -> tuple[bool, str]: + """验证邮箱""" + import re + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + if re.match(pattern, email): + return True, "" + return False, "无效的邮箱格式" + + def _validate_url(self, url: str) -> tuple[bool, str]: + """验证URL""" + import re + pattern = r'^https?://[^\s/$.?#].[^\s]*$' + if re.match(pattern, url): + return True, "" + return False, "无效的URL格式" + + def _validate_ip_address(self, ip: str) -> tuple[bool, str]: + """验证IP地址""" + import ipaddress + try: + ipaddress.ip_address(ip) + return True, "" + except ValueError: + return False, "无效的IP地址" + + def _validate_filename(self, filename: str) -> tuple[bool, str]: + """验证文件名""" + import re + # 防止路径遍历攻击 + if '..' in filename or '/' in filename or '\\' in filename: + return False, "文件名包含非法字符" + + # 检查文件扩展名 + if '.' in filename: + ext = filename[filename.rfind('.'):].lower() + if ext not in self._security_config["allowed_file_types"]: + return False, f"不允许的文件类型: {ext}" + + # 检查文件名长度 + if len(filename) > 255: + return False, "文件名过长" + + return True, "" + + def _check_sql_injection(self, text: str) -> tuple[bool, str]: + """检查SQL注入""" + sql_keywords = [ + 'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'UNION', + 'OR', 'AND', 'WHERE', 'FROM', 'TABLE', 'DATABASE' + ] + + text_upper = text.upper() + for keyword in sql_keywords: + # 简单的关键词检查 + if f' {keyword} ' in f' {text_upper} ': + return False, f"检测到SQL关键词: {keyword}" + + # 检查常见的注入模式 + injection_patterns = [ + r"'.*--", + r"'.*;", + r"OR\s+'.*'='.*'", + r"AND\s+'.*'='.*'" + ] + + import re + for pattern in injection_patterns: + if re.search(pattern, text_upper, re.IGNORECASE): + return False, "检测到SQL注入模式" + + return True, "" + + def _check_xss(self, text: str) -> tuple[bool, str]: + """检查XSS攻击""" + xss_patterns = [ + r".*?", + r"javascript:", + r"on\w+\s*=", + r"<\s*iframe", + r"<\s*img.*src\s*=", + r"<\s*a.*href\s*=" + ] + + import re + for pattern in xss_patterns: + if re.search(pattern, text, re.IGNORECASE): + return False, "检测到XSS攻击模式" + + return True, "" + + async def _sanitize_output(self, data): + """净化输出数据""" + if isinstance(data, dict): + return {k: await self._sanitize_output(v) for k, v in data.items()} + elif isinstance(data, list): + return [await self._sanitize_output(item) for item in data] + elif isinstance(data, str): + # 转义HTML特殊字符 + import html + return html.escape(data) + else: + return data + + def _generate_secure_token(self, length: int = 32) -> str: + """生成安全令牌""" + import secrets + return secrets.token_hex(length) + + def _hash_password(self, password: str) -> str: + """哈希密码""" + import hashlib + import os + + # 使用盐值 + salt = os.urandom(32) + key = hashlib.pbkdf2_hmac( + 'sha256', + password.encode('utf-8'), + salt, + 100000 # 迭代次数 + ) + return salt.hex() + key.hex() + + def _verify_password(self, password: str, hashed: str) -> bool: + """验证密码""" + import hashlib + + salt = bytes.fromhex(hashed[:64]) # 前64位是盐值 + key = hashlib.pbkdf2_hmac( + 'sha256', + password.encode('utf-8'), + salt, + 100000 + ) + return hashed[64:] == key.hex() +``` + +### 4.4 测试最佳实践 + +```python +import pytest +import pytest_asyncio +from unittest.mock import AsyncMock, Mock, patch + +class TestPlugin: + """插件测试基类""" + + @pytest_asyncio.fixture + async def plugin_instance(self): + """创建插件实例""" + config = { + "name": "TestPlugin", + "version": "1.0.0", + "settings": {"enabled": True} + } + + bridge_mock = AsyncMock() + bridge_mock.service_manager = Mock() + + plugin = Plugin("test_plugin", config, bridge_mock) + await plugin.initialize() + + yield plugin + + await plugin.shutdown() + + @pytest.mark.asyncio + async def test_plugin_initialization(self, plugin_instance): + """测试插件初始化""" + assert plugin_instance.status.is_running == True + assert plugin_instance.plugin_name == "test_plugin" + assert plugin_instance.config["name"] == "TestPlugin" + + @pytest.mark.asyncio + async def test_network_routes_registration(self, plugin_instance): + """测试网络路由注册""" + # 模拟网络桥接 + network_bridge_mock = AsyncMock() + plugin_instance.network_bridge = network_bridge_mock + + # 调用注册方法 + await plugin_instance._register_network_routes() + + # 验证注册调用 + assert network_bridge_mock.register_http_route.called + assert network_bridge_mock.register_websocket.called + + @pytest.mark.asyncio + async def test_command_execution(self, plugin_instance): + """测试命令执行""" + # 测试状态命令 + result = await plugin_instance.cmd_status() + assert "插件状态" in result + assert plugin_instance.plugin_name in result + + @pytest.mark.asyncio + async def test_error_handling(self, plugin_instance): + """测试错误处理""" + # 测试权限错误 + with pytest.raises(PluginPermissionError): + await plugin_instance._safe_execute( + self._raise_permission_error + ) + + # 测试网络错误 + with pytest.raises(PluginNetworkError): + await plugin_instance._safe_execute( + self._raise_network_error + ) + + def _raise_permission_error(self): + raise PermissionError("测试权限错误") + + def _raise_network_error(self): + raise ConnectionError("测试网络错误") + + @pytest.mark.asyncio + async def test_rate_limiting(self, plugin_instance): + """测试速率限制""" + # 模拟多次调用 + results = [] + for i in range(15): # 超过10次限制 + result = await plugin_instance._rate_limited_execute( + self._dummy_function, + rate_limit=10 + ) + results.append(result) + + # 验证所有调用都成功 + assert len(results) == 15 + assert all(r == "dummy_result" for r in results) + + async def _dummy_function(self): + await asyncio.sleep(0.01) + return "dummy_result" + + @pytest.mark.asyncio + async def test_cache_functionality(self, plugin_instance): + """测试缓存功能""" + # 第一次调用应该缓存 + result1 = await plugin_instance._cached_execute( + self._expensive_function, + ttl=60 + ) + + # 第二次调用应该从缓存获取 + result2 = await plugin_instance._cached_execute( + self._expensive_function, + ttl=60 + ) + + assert result1 == result2 + assert plugin_instance._performance_stats["cache_hits"] == 1 + assert plugin_instance._performance_stats["cache_misses"] == 1 + + async def _expensive_function(self): + await asyncio.sleep(0.1) + return {"data": "expensive_result"} + + @pytest.mark.parametrize("input_data,expected", [ + ("test@example.com", True), + ("invalid-email", False), + ("https://example.com", True), + ("javascript:alert(1)", False), + ("normal_text", True), + ]) + def test_input_validation(self, plugin_instance, input_data, expected): + """测试输入验证""" + validator = SecurePlugin() + + if expected: + # 应该通过验证 + assert validator._validate_input({"test": input_data}) == True + else: + # 应该抛出异常 + with pytest.raises(PluginError): + validator._validate_input({"test": input_data}) + +class IntegrationTest: + """集成测试""" + + @pytest_asyncio.fixture + async def framework_with_plugin(self): + """创建带插件的框架实例""" + from main import CatFramework + + framework = CatFramework() + + # 启动框架 + await framework.initialize() + + # 加载测试插件 + plugin_service = framework.service_manager.get_service("plugin") + await plugin_service.load_plugin("test_plugin") + + yield framework + + # 关闭框架 + await framework.shutdown() + + @pytest.mark.asyncio + async def test_plugin_integration(self, framework_with_plugin): + """测试插件与框架的集成""" + # 获取插件服务 + plugin_service = framework_with_plugin.service_manager.get_service("plugin") + + # 验证插件已加载 + assert "test_plugin" in plugin_service.plugins + + # 验证插件命令已注册 + command_service = framework_with_plugin.service_manager.get_service("command") + command_list = command_service.get_command_list() + + plugin_commands = [ + cmd for cmd in command_list + if cmd["source"].startswith("plugin.test_plugin") + ] + + assert len(plugin_commands) > 0 + + # 测试命令执行 + result = await command_service.process_command("test_plugin_status", "test") + assert "插件状态" in result + + @pytest.mark.asyncio + async def test_plugin_network_integration(self, framework_with_plugin): + """测试插件网络集成""" + import aiohttp + + # 获取网络服务 + internet_service = framework_with_plugin.service_manager.get_service("internet") + + # 测试HTTP API + async with aiohttp.ClientSession() as session: + url = f"http://localhost:{internet_service.http_port}/plugin/test_plugin/api/health" + async with session.get(url) as response: + assert response.status == 200 + data = await response.json() + assert data["status"] in ["healthy", "unhealthy"] +``` + +## 五、插件发布与部署 + +### 5.1 插件打包 + +```yaml +# setup.py 或 pyproject.toml 示例 +""" +插件打包配置 +""" + +# setup.py +from setuptools import setup, find_packages + +setup( + name="sensu-plugin-my-awesome-plugin", + version="1.0.0", + description="我的超棒插件", + author="开发者名字", + author_email="developer@example.com", + packages=find_packages(), + install_requires=[ + "requests>=2.25.0", + "pydantic>=1.8.0", + ], + extras_require={ + "redis": ["redis>=3.5.0"], + "mysql": ["aiomysql>=0.1.0"], + }, + package_data={ + "": ["*.yaml", "*.json", "*.md"], + }, + entry_points={ + "sensu.plugins": [ + "my_awesome_plugin = my_awesome_plugin:Plugin", + ], + }, + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + ], +) + +# pyproject.toml +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "sensu-plugin-my-awesome-plugin" +version = "1.0.0" +description = "我的超棒插件" +authors = [ + {name = "开发者名字", email = "developer@example.com"} +] +dependencies = [ + "requests>=2.25.0", + "pydantic>=1.8.0" +] + +[project.optional-dependencies] +redis = ["redis>=3.5.0"] +mysql = ["aiomysql>=0.1.0"] + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.package-data] +"*" = ["*.yaml", "*.json", "*.md"] + +[project.entry-points."sensu.plugins"] +"my_awesome_plugin" = "my_awesome_plugin:Plugin" +``` + +### 5.2 插件发布清单 + +```markdown +# 插件发布清单 + +## 1. 代码质量检查 +- [ ] 通过所有单元测试 +- [ ] 通过集成测试 +- [ ] 代码覆盖率 > 80% +- [ ] 通过静态代码分析 +- [ ] 通过安全扫描 + +## 2. 文档检查 +- [ ] README.md 完整 +- [ ] 配置说明文档 +- [ ] API文档 +- [ ] 使用示例 +- [ ] 更新日志 + +## 3. 配置检查 +- [ ] config.yaml 完整 +- [ ] permissions.yaml 完整 +- [ ] 默认配置合理 +- [ ] 配置验证通过 + +## 4. 依赖检查 +- [ ] 依赖版本明确 +- [ ] 无冲突依赖 +- [ ] 可选依赖标注清楚 +- [ ] 系统依赖说明 + +## 5. 打包检查 +- [ ] 打包脚本正确 +- [ ] 包含所有必要文件 +- [ ] 不包含敏感信息 +- [ ] 版本号正确 + +## 6. 性能检查 +- [ ] 内存使用合理 +- [ ] 启动时间 < 5秒 +- [ ] API响应时间 < 1秒 +- [ ] 支持并发请求 + +## 7. 安全检查 +- [ ] 输入验证完整 +- [ ] 输出净化 +- [ ] 权限控制 +- [ ] 无硬编码密码 +- [ ] 日志无敏感信息 +``` + +### 5.3 持续集成配置 + +```yaml +# .github/workflows/ci.yml +name: CI/CD + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10"] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-asyncio pytest-cov + pip install -e . + + - name: Run tests + run: | + pytest tests/ --cov=my_awesome_plugin --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + fail_ci_if_error: true + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install linting tools + run: | + pip install black flake8 mypy pylint + + - name: Run black + run: black --check . + + - name: Run flake8 + run: flake8 . + + - name: Run mypy + run: mypy my_awesome_plugin + + - name: Run pylint + run: pylint my_awesome_plugin + + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Run bandit + run: | + pip install bandit + bandit -r my_awesome_plugin -f json -o bandit-report.json + + - name: Run safety check + run: | + pip install safety + safety check + + build: + needs: [test, lint, security] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Build package + run: | + pip install build + python -m build + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: plugin-package + path: dist/ +``` + +## 六、插件调试与故障排除 + +### 6.1 调试工具 + +```python +class DebugPlugin: + """调试插件基类""" + + def __init__(self): + self._debug_enabled = False + self._debug_logs = [] + self._performance_probes = {} + + def enable_debug(self): + """启用调试模式""" + self._debug_enabled = True + logger.setLevel(logging.DEBUG) + + # 添加调试处理器 + debug_handler = logging.StreamHandler() + debug_handler.setLevel(logging.DEBUG) + debug_handler.setFormatter(logging.Formatter( + '%(asctime)s [DEBUG] %(name)s:%(lineno)d - %(message)s' + )) + logger.addHandler(debug_handler) + + def add_debug_log(self, message: str, data: dict = None): + """添加调试日志""" + if self._debug_enabled: + log_entry = { + "timestamp": datetime.now().isoformat(), + "message": message, + "data": data + } + self._debug_logs.append(log_entry) + + # 只保留最近的1000条日志 + if len(self._debug_logs) > 1000: + self._debug_logs.pop(0) + + async def _debug_probe(self, probe_name: str): + """调试探针""" + if not self._debug_enabled: + return + + start_time = time.time() + + def finish(): + end_time = time.time() + duration = end_time - start_time + + if probe_name not in self._performance_probes: + self._performance_probes[probe_name] = { + "count": 0, + "total_time": 0, + "min_time": float('inf'), + "max_time": 0, + "last_time": 0 + } + + stats = self._performance_probes[probe_name] + stats["count"] += 1 + stats["total_time"] += duration + stats["min_time"] = min(stats["min_time"], duration) + stats["max_time"] = max(stats["max_time"], duration) + stats["last_time"] = duration + + self.add_debug_log( + f"性能探针: {probe_name}", + {"duration": duration, "stats": stats} + ) + + return finish + + @plugin_command( + name="debug", + description="调试命令", + permissions=["plugin.my_awesome_plugin.admin"] + ) + async def cmd_debug(self, *args): + """调试命令""" + try: + if not args: + return ( + "🐛 **调试命令**\n" + "💡 使用: debug enable 启用调试模式\n" + "💡 使用: debug disable 禁用调试模式\n" + "💡 使用: debug logs [count] 查看调试日志\n" + "💡 使用: debug stats 查看性能统计\n" + "💡 使用: debug memory 查看内存使用\n" + "💡 使用: debug profile 性能分析\n" + ) + + command = args[0].lower() + + if command == "enable": + self.enable_debug() + return "✅ 调试模式已启用" + + elif command == "disable": + self._debug_enabled = False + return "🛑 调试模式已禁用" + + elif command == "logs": + count = 10 + if len(args) > 1: + try: + count = min(int(args[1]), 100) + except ValueError: + return "❌ 日志数量必须是数字" + + if not self._debug_logs: + return "📭 没有调试日志" + + result = [f"📋 **最近 {count} 条调试日志**"] + for log in self._debug_logs[-count:]: + result.append( + f"[{log['timestamp']}] {log['message']}" + ) + if log['data']: + result.append(f" 数据: {log['data']}") + + return "\n".join(result) + + elif command == "stats": + if not self._performance_probes: + return "📊 没有性能统计数据" + + result = ["📊 **性能统计**"] + for probe_name, stats in self._performance_probes.items(): + avg_time = stats["total_time"] / stats["count"] if stats["count"] > 0 else 0 + result.append( + f"{probe_name}: " + f"调用{stats['count']}次, " + f"平均{avg_time:.3f}秒, " + f"最小{stats['min_time']:.3f}秒, " + f"最大{stats['max_time']:.3f}秒" + ) + + return "\n".join(result) + + elif command == "memory": + import psutil + import os + + process = psutil.Process(os.getpid()) + memory_info = process.memory_info() + + result = ["💾 **内存使用**"] + result.append(f"RSS: {memory_info.rss / 1024 / 1024:.2f} MB") + result.append(f"VMS: {memory_info.vms / 1024 / 1024:.2f} MB") + result.append(f"共享内存: {memory_info.shared / 1024 / 1024:.2f} MB") + result.append(f"文本段: {memory_info.text / 1024 / 1024:.2f} MB") + result.append(f"数据段: {memory_info.data / 1024 / 1024:.2f} MB") + + # 插件特定内存 + result.append(f"缓存大小: {len(self.cache)} 项") + result.append(f"WebSocket连接: {len(self.websocket_connections)} 个") + + return "\n".join(result) + + elif command == "profile": + if len(args) < 2: + return "❌ 请指定要分析的命令,如: debug profile status" + + sub_command = args[1] + + # 执行性能分析 + import cProfile + import io + import pstats + + profiler = cProfile.Profile() + profiler.enable() + + try: + # 执行命令 + command_func = getattr(self, f"cmd_{sub_command}", None) + if command_func: + result = await command_func(*args[2:]) + else: + result = f"❌ 找不到命令: {sub_command}" + finally: + profiler.disable() + + # 分析结果 + s = io.StringIO() + ps = pstats.Stats(profiler, stream=s).sort_stats('cumulative') + ps.print_stats(20) # 显示前20个最耗时的函数 + + profile_result = s.getvalue() + + return f"📈 **性能分析结果**\n```\n{profile_result}\n```\n\n**命令结果:**\n{result}" + + else: + return f"❌ 未知调试命令: {command}" + + except Exception as e: + logger.error(f"调试命令执行失败: {str(e)}") + return f"❌ 调试命令错误: {str(e)}" +``` + +### 6.2 故障排除指南 + +```markdown +# 插件故障排除指南 + +## 1. 插件无法加载 + +### 症状 +- 插件没有出现在插件列表中 +- 日志显示插件加载失败 + +### 可能原因 +1. 目录结构不正确 +2. 配置文件缺失或格式错误 +3. 权限文件格式错误 +4. Python语法错误 +5. 依赖包缺失 + +### 解决方法 +1. 检查插件目录结构: + ``` + plugins/ + └── your_plugin/ + ├── __init__.py + ├── config.yaml + └── permissions.yaml + ``` + +2. 验证配置文件: + ```bash + python -c "import yaml; yaml.safe_load(open('config.yaml'))" + ``` + +3. 检查Python语法: + ```bash + python -m py_compile __init__.py + ``` + +4. 查看详细日志: + ```bash + tail -f logs/runtime/*.log + ``` + +## 2. 权限申请失败 + +### 症状 +- 插件以受限模式运行 +- 某些功能不可用 +- 日志显示权限被拒绝 + +### 可能原因 +1. 权限名称格式错误 +2. 权限描述不清晰 +3. 申请了过多或不必要的权限 +4. 用户拒绝了权限申请 + +### 解决方法 +1. 检查权限格式: + ```yaml + # 正确格式 + permissions: + - "plugin.your_plugin.read" + - "plugin.your_plugin.write" + + # 错误格式 + permissions: + - "read" # 缺少插件名前缀 + - "plugin.your_plugin.*" # 通配符可能被拒绝 + ``` + +2. 提供清晰的权限描述: + ```yaml + permission_descriptions: + plugin.your_plugin.read: "读取插件数据,不会修改任何内容" + plugin.your_plugin.write: "修改插件配置和数据" + ``` + +3. 分批申请权限: + ```yaml + # 第一次申请基础权限 + permissions: + - "plugin.your_plugin.read" + + # 后续根据需要申请更多权限 + ``` + +## 3. 网络功能不可用 + +### 症状 +- HTTP API返回404错误 +- WebSocket连接失败 +- 网络相关命令无法执行 + +### 可能原因 +1. 网络服务未启动 +2. 端口被占用 +3. 路由注册失败 +4. 权限不足 + +### 解决方法 +1. 检查网络服务状态: + ```bash + # 在框架中执行 + netdiag + ``` + +2. 检查端口占用: + ```bash + # Linux/Mac + lsof -i :8000 + + # Windows + netstat -ano | findstr :8000 + ``` + +3. 查看插件网络信息: + ```bash + # 在框架中执行 + your_plugin network + ``` + +4. 重新注册网络路由: + ```python + # 在插件中 + await self._register_network_routes() + ``` + +## 4. 性能问题 + +### 症状 +- 响应时间慢 +- 内存使用率高 +- CPU占用率高 + +### 可能原因 +1. 缓存未命中 +2. 数据库查询效率低 +3. 网络请求过多 +4. 内存泄漏 + +### 解决方法 +1. 启用性能监控: + ```bash + # 在框架中执行 + your_plugin debug stats + ``` + +2. 分析内存使用: + ```bash + your_plugin debug memory + ``` + +3. 优化数据库查询: + ```python + # 添加索引 + # 使用连接池 + # 批量操作 + ``` + +4. 实现缓存: + ```python + # 使用装饰器 + @cached(ttl=300) + async def get_data(self): + # 耗时操作 + pass + ``` + +## 5. 内存泄漏 + +### 症状 +- 内存使用持续增长 +- 重启后恢复正常 +- 长时间运行后变慢 + +### 可能原因 +1. 未关闭的资源(文件、连接等) +2. 循环引用 +3. 缓存无限增长 +4. 事件监听器未移除 + +### 解决方法 +1. 使用资源上下文管理器: + ```python + async with open_file() as f: + # 使用文件 + pass # 自动关闭 + ``` + +2. 定期清理缓存: + ```python + async def _cleanup_expired_cache(self): + # 清理过期缓存 + pass + ``` + +3. 使用弱引用: + ```python + import weakref + + self._callbacks = weakref.WeakSet() + ``` + +4. 监控内存使用: + ```python + import tracemalloc + + tracemalloc.start() + # ... 运行代码 ... + snapshot = tracemalloc.take_snapshot() + top_stats = snapshot.statistics('lineno') + ``` + +## 6. 日志调试 + +### 启用详细日志 +```python +# 在插件配置中 +settings: + log_level: "DEBUG" +``` + +### 查看插件特定日志 +```bash +# 查找插件相关日志 +grep "your_plugin" logs/runtime/*.log + +# 实时查看日志 +tail -f logs/runtime/latest.log | grep "your_plugin" +``` + +### 添加自定义日志 +```python +logger.debug("详细调试信息", extra={"data": your_data}) +logger.info("一般信息") +logger.warning("警告信息") +logger.error("错误信息", exc_info=True) +``` + +## 7. 联系支持 + +如果以上方法都无法解决问题: + +1. 收集以下信息: + - 插件版本 + - 框架版本 + - 错误日志 + - 复现步骤 + - 系统信息 + +2. 提交问题报告: + - GitHub Issues + - 社区论坛 + - 邮件支持 + +3. 提供最小复现示例: + ```python + # 简化的代码示例 + # 能够重现问题的最小代码 + ``` +``` + +## 七、插件开发检查清单 + +### 7.1 开发前检查清单 + +- [ ] **需求分析** + - [ ] 明确插件功能需求 + - [ ] 确定目标用户群体 + - [ ] 分析使用场景 + - [ ] 定义成功标准 + +- [ ] **技术选型** + - [ ] 选择合适的技术栈 + - [ ] 评估依赖包兼容性 + - [ ] 确定性能要求 + - [ ] 制定安全策略 + +- [ ] **架构设计** + - [ ] 设计插件模块结构 + - [ ] 规划API接口 + - [ ] 设计数据模型 + - [ ] 制定错误处理策略 + +### 7.2 开发中检查清单 + +- [ ] **代码质量** + - [ ] 遵循PEP 8编码规范 + - [ ] 添加类型提示 + - [ ] 编写文档字符串 + - [ ] 实现单元测试 + +- [ ] **功能实现** + - [ ] 实现核心功能 + - [ ] 添加错误处理 + - [ ] 实现日志记录 + - [ ] 添加配置选项 + +- [ ] **安全性** + - [ ] 验证所有输入 + - [ ] 净化所有输出 + - [ ] 实现权限控制 + - [ ] 避免敏感信息泄露 + +### 7.3 测试检查清单 + +- [ ] **单元测试** + - [ ] 测试所有公开方法 + - [ ] 测试错误处理 + - [ ] 测试边界条件 + - [ ] 测试异步方法 + +- [ ] **集成测试** + - [ ] 测试插件加载 + - [ ] 测试命令执行 + - [ ] 测试网络功能 + - [ ] 测试事件处理 + +- [ ] **性能测试** + - [ ] 测试响应时间 + - [ ] 测试内存使用 + - [ ] 测试并发处理 + - [ ] 测试资源清理 + +### 7.4 发布检查清单 + +- [ ] **文档** + - [ ] 编写README.md + - [ ] 编写API文档 + - [ ] 编写使用示例 + - [ ] 编写更新日志 + +- [ ] **打包** + - [ ] 创建打包配置 + - [ ] 包含必要文件 + - [ ] 设置版本号 + - [ ] 添加依赖声明 + +- [ ] **验证** + - [ ] 在新环境中测试 + - [ ] 验证安装过程 + - [ ] 测试升级流程 + - [ ] 确认卸载清理 + +## 八、总结 + +### 8.1 成功插件的特点 + +1. **可靠性**:稳定运行,正确处理各种异常情况 +2. **易用性**:简洁的API,清晰的文档,直观的配置 +3. **安全性**:严格的输入验证,完善的权限控制 +4. **性能**:高效的处理能力,合理的内存使用 +5. **可维护性**:清晰的代码结构,完善的测试覆盖 +6. **可扩展性**:支持插件间的协作,易于功能扩展 + +### 8.2 持续改进 + +1. **收集反馈**:积极收集用户反馈,了解使用痛点 +2. **监控使用**:通过日志和指标了解插件使用情况 +3. **定期更新**:修复bug,添加新功能,优化性能 +4. **保持兼容**:确保新版本与旧版本的兼容性 +5. **社区参与**:参与插件生态建设,分享经验 + +### 8.3 资源推荐 + +1. **学习资源** + - Python官方文档 + - asyncio官方文档 + - Textual框架文档 + - aiohttp文档 + +2. **工具推荐** + - **代码质量**:black, flake8, mypy, pylint + - **测试框架**:pytest, pytest-asyncio, coverage + - **性能分析**:cProfile, memory_profiler, line_profiler + - **打包工具**:setuptools, poetry, hatch + +3. **社区支持** + - GitHub Issues:报告问题和功能请求 + - 论坛社区:交流开发经验 + - Stack Overflow:解决具体技术问题 + - 开发者群组:实时交流和协作 + +通过遵循本指南,您可以开发出高质量的SenSu插件,为用户提供有价值的功能,同时为插件生态系统做出贡献。祝您开发顺利! \ No newline at end of file diff --git a/docs/SenSu 框架基本架构.md b/docs/SenSu 框架基本架构.md new file mode 100644 index 0000000..7df75a5 --- /dev/null +++ b/docs/SenSu 框架基本架构.md @@ -0,0 +1,398 @@ + +## 一、项目架构与设计思路 + +### 1.1 核心设计理念 + +SenSu框架采用了**服务化、插件化、事件驱动**的设计理念: + +- **模块化服务架构**:每个功能都是一个独立服务(Service),通过ServiceManager统一管理 +- **异步优先**:全面采用`asyncio`,支持高并发处理 +- **插件隔离**:插件有独立的命名空间和权限控制 +- **桥接通信**:通过消息桥接实现模块间解耦通信 + +### 1.2 框架启动流程 + +``` +main.py → CatFramework.initialize() → 依次初始化12个核心服务 +``` + +**启动顺序**: +1. InitService(配置加载) +2. LogService(日志系统) +3. CoreBridge(核心消息桥接) +4. CommandService(命令系统) +5. AuthService(认证系统) +6. PluginBridge(插件桥接) +7. ShutdownService(优雅关闭) +8. TuiService(终端界面) +9. PermissionService(权限管理) +10. InternetService(网络服务) +11. PluginService(插件管理) +12. APIService(API服务) + +## 二、目录结构详细分析 + +### 2.1 核心目录说明 + +``` +SenSu-Alpha0.2/ +├── main.py # 框架主入口,定义CatFramework类 +├── service_manager.py # 服务管理器(全局服务注册表) +├── requirements.txt # Python依赖包 +├── README.md # 项目文档 +│ +├── bridges/ # 消息桥接系统 +│ ├── __init__.py +│ ├── core_bridge.py # 核心模块间通信(发布-订阅模式) +│ ├── plugin_bridge.py # 插件间通信 +│ └── plugin_network_bridge.py # 插件网络桥接 +│ +├── services/ # 核心服务模块(核心业务逻辑) +│ ├── init_service.py # 框架初始化 +│ ├── log_service.py # 日志服务(多输出、文件切割) +│ ├── tui_service.py # TUI终端界面(基于Textual) +│ ├── command_service.py # 命令处理系统 +│ ├── auth_service.py # 认证系统 +│ ├── internet_service.py # 网络服务(HTTP+WebSocket) +│ ├── plugin_service.py # 插件管理器 +│ ├── permission_service.py # 权限验证器 +│ ├── api_service.py # API端点管理 +│ └── shutdown_service.py # 优雅关闭 +│ +├── fmfuncs/ # 框架功能集(工具函数) +│ └── plugin_command_decorator.py # 插件命令装饰器 +│ +├── utils/ # 通用工具类 +│ ├── file_utils.py # 文件操作 +│ ├── config_utils.py # 配置管理 +│ ├── validation_utils.py # 数据验证 +│ ├── network_utils.py # 网络工具 +│ └── plugin_utils.py # 插件工具 +│ +├── config/ # 运行时配置文件 +│ ├── framework/ # 框架核心配置 +│ ├── plugins/ # 插件配置 +│ ├── services/ # 服务配置 +│ └── permissions/ # 权限配置 +│ +├── plugins/ # 插件目录 +│ └── example_plugin/ # 示例插件 +│ +└── gui/ # GUI接口(预留) + └── api.py # Web API接口 +``` + +### 2.3 配置管理系统 + +框架使用**两级配置**: +1. **Base Config** (`config/framework/base_config.yaml`): 框架基础配置 +2. **Runtime Config**: 运行时动态生成的配置(保存在config目录) + +## 三、核心模块深度解析 + +### 3.1 服务管理器(ServiceManager) + +**作用**:全局服务注册表,实现依赖注入 +```python +# 注册服务 +service_manager.register_service("log", log_service) + +# 获取服务 +log_service = service_manager.get_service("log") +``` + +### 3.2 桥接系统(Bridges) + +**核心设计**: +- **CoreBridge**: 模块间通信,支持`MessageType`枚举 +- **PluginBridge**: 插件间通信,支持`PluginMessageType`枚举 +- **消息格式**:topic + data + timestamp + +**消息类型**: +```python +class MessageType(Enum): + EVENT = "event" # 事件通知 + COMMAND = "command" # 命令执行 + DATA = "data" # 数据传输 + STATUS = "status" # 状态更新 + ERROR = "error" # 错误报告 +``` + +### 3.3 日志系统(LogService) + +**特性**: +- 支持控制台和文件双输出 +- 按级别分离(runtime/debug) +- 自动文件切割和清理 +- 日志消费者模式(TUI实时显示) +- 彩色日志输出 + +**配置示例**: +```yaml +logging: + level: DEBUG + debug_level_file: true + max_file_size: 10MB + max_log_files: 20 +``` + +### 3.4 TUI界面(TuiService) + +**基于Textual框架的三栏布局**: +1. **日志区域**(4fr): 显示所有日志输出 +2. **消息区域**(5fr): 显示系统消息和命令结果 +3. **输入区域**(1fr): 命令输入框 + +**特性**: +- 实时日志捕获和显示 +- 命令自动补全(预留) +- 滚动控制(自动/手动) +- 彩色消息显示 + +### 3.5 网络服务(InternetService) + +**功能**: +- HTTP服务器(aiohttp) +- WebSocket服务器 +- 插件路由自动注册 +- 反向代理支持(预留) + +**端口配置**: +```yaml +internet: + websocket: + port: 8765 + http: + port: 8000 +``` + +### 3.6 插件系统(PluginService) + +**关键特性**: +- 热加载/卸载 +- 权限隔离 +- 命令自动注册 +- 错误隔离(插件崩溃不影响框架) +- 网络路由自动注册 + +## 四、插件开发详解 + +### 4.1 插件目录结构 + +``` +plugins/ +└── example_plugin/ + ├── __init__.py # 插件主类(必须包含Plugin类) + ├── config.yaml # 插件配置 + └── permissions.yaml # 权限申请 +``` + +### 4.2 插件主类模板 + +```python +class Plugin: + def __init__(self, plugin_name: str, config: Dict, bridge): + self.plugin_name = plugin_name + self.config = config + self.bridge = bridge # PluginBridge实例 + self.network_bridge = None # PluginNetworkBridge实例 + + async def initialize(self): + """插件初始化""" + # 1. 创建网络桥接 + self.network_bridge = PluginNetworkBridge(...) + + # 2. 注册网络路由 + await self.network_bridge.register_http_route(...) + await self.network_bridge.register_websocket(...) + + # 3. 注册事件处理器 + self.bridge.subscribe_plugin(...) + + # 使用装饰器注册命令 + @plugin_command(name="mycmd", description="我的命令") + async def my_command(self, *args): + return "命令执行结果" + + async def shutdown(self): + """插件关闭""" + # 清理资源 +``` + +### 4.3 权限申请文件(permissions.yaml) + +```yaml +plugin_name: "example_plugin" +permissions: + - "plugin.example.read" + - "plugin.example.write" + - "plugin.example.execute" + - "framework.event.subscribe" + - "framework.command.execute" +``` + +### 4.4 插件配置文件(config.yaml) + +```yaml +name: "ExamplePlugin" +version: "1.0.0" +description: "插件描述" +author: "作者名" + +settings: + enabled: true + auto_start: true + log_level: "INFO" + +features: + # 插件特有配置 +``` + +### 4.5 插件命令装饰器 + +框架提供了`@plugin_command`装饰器: + +```python +from fmfuncs.plugin_command_decorator import plugin_command + +@plugin_command(name="echo", description="回显消息") +async def cmd_echo(self, *args): + return " ".join(args) + +# 简化版 +@plugin_command() +async def hello(self, *args): + '''打招呼命令''' + return "Hello World!" +``` + +### 4.6 插件网络功能 + +**HTTP路由注册**: +```python +await self.network_bridge.register_http_route( + "/api/info", + self._handle_api_info, + methods=["GET"], + require_auth=False +) +``` + +**WebSocket注册**: +```python +await self.network_bridge.register_websocket( + "/chat", + self._handle_websocket_chat +) +``` + +## 五、命令系统详解 + +### 5.1 内置命令 + +框架提供丰富的内置命令: +- `help` - 显示帮助 +- `status` - 框架状态 +- `history` - 命令历史 +- `testlog` - 测试日志生成 +- `netdiag` - 网络诊断 +- `permissions` - 权限管理 +- `scroll` - 滚动控制 +- `autoscroll` - 自动滚动开关 + +### 5.2 权限管理命令 + +框架提供完整的权限管理命令集(pm前缀): +- `pmallow` - 同意权限请求 +- `pmdeny` - 拒绝权限请求 +- `pmignore` - 忽略权限请求 +- `permissions` - 显示权限状态 +- `pmpending` - 查看待授权请求 +- `pmhelp` - 权限命令帮助 + +### 5.3 命令注册机制 + +**插件命令注册流程**: +1. PluginService扫描插件方法 +2. 识别`@plugin_command`装饰器 +3. 注册到CommandService +4. 命令格式:`命令名 [参数...]` + +## 六、开发建议与最佳实践 + +### 6.1 插件开发建议 + +1. **错误处理**:插件内应妥善处理异常,避免影响框架 +2. **资源管理**:在`shutdown`方法中清理所有资源 +3. **异步安全**:确保异步方法正确处理取消和超时 +4. **权限最小化**:只申请必要的权限 + +### 6.2 性能优化 + +1. **异步IO**:所有网络和文件操作使用异步版本 +2. **连接池**:数据库/网络连接使用连接池 +3. **缓存机制**:频繁读取的数据适当缓存 +4. **懒加载**:大型资源按需加载 + +### 6.3 安全性考虑 + +1. **输入验证**:所有外部输入都应验证 +2. **权限验证**:敏感操作前检查权限 +3. **日志脱敏**:避免在日志中记录敏感信息 +4. **API限流**:防止API被滥用 + +## 七、框架优势与特点 + +### 7.1 优势 +1. **完整的生态**:日志、网络、UI、插件系统一应俱全 +2. **良好的扩展性**:插件系统设计完善 +3. **生产级质量**:完善的错误处理和日志记录 +4. **开发者友好**:详细的文档和示例插件 + +### 7.2 适用场景 +1. **后台管理工具**:需要终端界面的管理工具 +2. **API网关**:插件化路由和认证 +3. **自动化平台**:可扩展的任务调度和执行 +4. **监控系统**:实时数据展示和告警 + +### 7.3 技术栈亮点 +- **异步架构**:asyncio全面应用 +- **现代化UI**:基于Textual的TUI +- **微服务理念**:服务化模块设计 +- **企业级特性**:权限、认证、日志一应俱全 + +## 八、后续发展建议 + +### 8.1 功能增强 +1. **数据库支持**:添加ORM或数据库连接池 +2. **任务队列**:集成Celery或类似系统 +3. **监控指标**:集成Prometheus指标导出 +4. **配置文件热重载**:支持运行时配置更新 + +### 8.2 易用性改进 +1. **插件市场**:在线插件安装和管理 +2. **配置生成器**:图形化配置界面 +3. **调试工具**:集成调试和性能分析 +4. **文档生成**:自动生成API文档 + +### 8.3 生态建设 +1. **插件模板**:快速创建插件的脚手架 +2. **测试框架**:插件测试工具 +3. **CI/CD集成**:自动化测试和部署 +4. **社区建设**:建立插件开发者社区 + +## 总结 + +SenSu框架是一个设计精良、功能完整的Python后端框架,具有以下核心价值: + +1. **工程化设计**:服务化架构、完善的错误处理、详细的日志 +2. **强大的插件系统**:支持热加载、权限隔离、网络路由自动注册 +3. **现代化的用户体验**:基于Textual的TUI界面,美观实用 +4. **企业级特性**:完整的权限管理、认证系统、网络服务 + +框架代码结构清晰,文档详细,适合作为: +- 企业级后台系统的基础框架 +- 插件化应用的核心引擎 +- 学习和研究现代Python框架设计的优秀案例 + +对于想要基于此框架进行开发的开发者,建议从`example_plugin`入手,逐步理解框架的各个组件,然后根据业务需求开发定制插件。 \ No newline at end of file diff --git a/docs/项目文件结构.txt b/docs/项目文件结构.txt new file mode 100644 index 0000000..91b4edd --- /dev/null +++ b/docs/项目文件结构.txt @@ -0,0 +1,44 @@ +SenSu/ +├── main.py # 框架主入口 +├── requirements.txt # 依赖包列表 +├── README.md # 项目说明 +│ +├── config/ # 运行时生成的配置文件 +│ ├── framework/ # 框架核心配置 +│ ├── plugins/ # 插件配置 +│ ├── services/ # 服务配置 +│ └── permissions/ # 权限配置 +│ +├── logs/ # 日志文件目录 +│ ├── debug/ # debug级别日志 +│ └── runtime/ # 运行时日志 +│ +├── fmfuncs/ # 框架功能集 +│ ├── __init__.py +│ ├── tui_renderer.py # TUI渲染器 +│ ├── log_handler.py # 日志处理模块 +│ ├── init_system.py # 初始化系统 +│ ├── command_handler.py # 指令处理模块 +│ ├── bridge_core.py # 核心桥模块 +│ ├── bridge_plugin.py # 插件桥模块 +│ ├── internet_module.py # 互联网模块集 +│ ├── auth_system.py # 访问验证系统 +│ ├── plugin_manager.py # 插件管理器 +│ ├── permission_validator.py # 权限验证器 +│ ├── api_manager.py # API管理器 +│ └── shutdown_handler.py # 终止处理器 +│ +├── plugins/ # 插件目录 +│ └── example_plugin/ # 示例插件结构 +│ ├── __init__.py +│ ├── permissions.yaml +│ └── config.yaml +│ +├── gui/ # GUI接口目录 +│ └── api.py # GUI操作接口 +│ +└── utils/ # 工具函数 + ├── __init__.py + ├── file_utils.py # 文件操作工具 + ├── config_utils.py # 配置工具 + └── validation_utils.py # 验证工具 diff --git a/fmfuncs/plugin_command_decorator.py b/fmfuncs/plugin_command_decorator.py new file mode 100644 index 0000000..fa9fa5a --- /dev/null +++ b/fmfuncs/plugin_command_decorator.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +from typing import List, Callable, Optional +from functools import wraps + +logger = logging.getLogger(__name__) + +def plugin_command(name: Optional[str] = None, + description: Optional[str] = None, + permissions: Optional[List[str]] = None): + """ + 插件命令装饰器 + + 用法: + @plugin_command(name="mycmd", description="我的命令", permissions=["read"]) + async def my_command_handler(self, *args): + return "命令执行结果" + + 或者简化版: + @plugin_command() + async def mycmd(self, *args): + '''我的命令描述''' + return "命令执行结果" + """ + def decorator(func: Callable): + # 设置命令属性 + func._is_plugin_command = True + func._command_name = name or func.__name__ + + # 优先使用装饰器参数,其次使用文档字符串,最后使用默认描述 + if description: + func._command_description = description + elif func.__doc__: + # 提取文档字符串的第一行作为描述 + doc_lines = [line.strip() for line in func.__doc__.split('\n') if line.strip()] + func._command_description = doc_lines[0] if doc_lines else f"命令: {func.__name__}" + else: + func._command_description = f"命令: {func.__name__}" + + func._command_permissions = permissions or [] + + @wraps(func) + async def wrapper(self, *args, **kwargs): + """包装器确保返回字符串结果并处理异常""" + try: + logger.debug(f"执行插件命令: {func._command_name}, 参数: {args}") + + # 调用原始方法 + result = await func(self, *args, **kwargs) + + # 确保返回字符串 + if result is None: + return "✅ 命令执行完成" + elif not isinstance(result, str): + return str(result) + else: + return result + + except Exception as e: + logger.error(f"插件命令执行失败 {func._command_name}: {str(e)}", exc_info=True) + return f"❌ 命令执行错误: {str(e)}" + + return wrapper + return decorator + +def command(name: Optional[str] = None, description: Optional[str] = None): + """简化版命令装饰器""" + return plugin_command(name=name, description=description) + +# 同步命令装饰器(不推荐,但提供兼容性) +def sync_plugin_command(name: Optional[str] = None, + description: Optional[str] = None, + permissions: Optional[List[str]] = None): + """同步插件命令装饰器""" + def decorator(func: Callable): + func._is_plugin_command = True + func._command_name = name or func.__name__ + + if description: + func._command_description = description + elif func.__doc__: + doc_lines = [line.strip() for line in func.__doc__.split('\n') if line.strip()] + func._command_description = doc_lines[0] if doc_lines else f"命令: {func.__name__}" + else: + func._command_description = f"命令: {func.__name__}" + + func._command_permissions = permissions or [] + + @wraps(func) + def wrapper(self, *args, **kwargs): + """同步命令包装器""" + try: + logger.debug(f"执行同步插件命令: {func._command_name}, 参数: {args}") + + result = func(self, *args, **kwargs) + + if result is None: + return "✅ 命令执行完成" + elif not isinstance(result, str): + return str(result) + else: + return result + + except Exception as e: + logger.error(f"同步插件命令执行失败 {func._command_name}: {str(e)}", exc_info=True) + return f"❌ 命令执行错误: {str(e)}" + + return wrapper + return decorator diff --git a/gui/api.py b/gui/api.py new file mode 100644 index 0000000..46f9d76 --- /dev/null +++ b/gui/api.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import asyncio +from typing import Dict, List, Any, Optional +from aiohttp import web +import json + +logger = logging.getLogger(__name__) + +class GUIAPI: + """GUI API接口 - 为未来的GUI界面提供操作接口""" + + def __init__(self, service_manager): + self.service_manager = service_manager + self.app = web.Application() + self.setup_routes() + logger.debug("GUIAPI初始化开始") + + def setup_routes(self): + """设置API路由""" + try: + # 框架状态接口 + self.app.router.add_get('/api/framework/status', self.handle_framework_status) + self.app.router.add_get('/api/framework/health', self.handle_framework_health) + + # 日志接口 + self.app.router.add_get('/api/logs/recent', self.handle_recent_logs) + self.app.router.add_get('/api/logs/stream', self.handle_log_stream) + + # 插件接口 + self.app.router.add_get('/api/plugins', self.handle_plugins_list) + self.app.router.add_post('/api/plugins/{plugin_name}/reload', self.handle_plugin_reload) + self.app.router.add_post('/api/plugins/{plugin_name}/unload', self.handle_plugin_unload) + + # 命令接口 + self.app.router.add_post('/api/command', self.handle_command_execute) + self.app.router.add_get('/api/command/history', self.handle_command_history) + + # 服务接口 + self.app.router.add_get('/api/services', self.handle_services_list) + self.app.router.add_get('/api/services/{service_name}/status', self.handle_service_status) + + logger.debug("GUI API路由设置完成") + + except Exception as e: + logger.error(f"设置GUI API路由时出错: {str(e)}", exc_info=True) + raise + + async def handle_framework_status(self, request): + """处理框架状态请求""" + try: + logger.debug("处理框架状态API请求") + + status_info = { + "framework": { + "name": "CatFramework", + "version": "1.0.0", + "status": "running", + "uptime": "0s" # 实际应该计算运行时间 + }, + "services": { + "total": 0, + "running": 0 + }, + "plugins": { + "total": 0, + "loaded": 0 + } + } + + # 获取服务状态 + try: + service_manager = self.service_manager.get_service("service_manager") + if service_manager: + status_info["services"]["total"] = len(service_manager.services) + status_info["services"]["running"] = len(service_manager.services) + except: + pass + + # 获取插件状态 + try: + plugin_service = self.service_manager.get_service("plugin") + if plugin_service: + status_info["plugins"]["total"] = len(plugin_service.plugin_info) + status_info["plugins"]["loaded"] = len(plugin_service.plugins) + except: + pass + + return web.json_response(status_info) + + except Exception as e: + logger.error(f"处理框架状态请求时出错: {str(e)}", exc_info=True) + return web.json_response({"error": str(e)}, status=500) + + async def handle_framework_health(self, request): + """处理框架健康检查""" + try: + logger.debug("处理框架健康检查API请求") + + health_data = { + "status": "healthy", + "timestamp": asyncio.get_event_loop().time(), + "components": { + "core_services": "healthy", + "plugins": "healthy", + "network": "healthy" + } + } + + return web.json_response(health_data) + + except Exception as e: + logger.error(f"处理框架健康检查时出错: {str(e)}", exc_info=True) + return web.json_response({"status": "unhealthy", "error": str(e)}, status=503) + + async def handle_recent_logs(self, request): + """处理最近日志请求""" + try: + logger.debug("处理最近日志API请求") + + count = int(request.query.get('count', 50)) + level = request.query.get('level', '') + + log_service = self.service_manager.get_service("log") + if not log_service: + return web.json_response({"error": "Log service not available"}, status=503) + + logs = log_service.get_recent_logs(count) + + # 按级别过滤 + if level: + logs = [log for log in logs if log['level'].lower() == level.lower()] + + return web.json_response({"logs": logs, "count": len(logs)}) + + except Exception as e: + logger.error(f"处理最近日志请求时出错: {str(e)}", exc_info=True) + return web.json_response({"error": str(e)}, status=500) + + async def handle_log_stream(self, request): + """处理日志流请求(SSE)""" + try: + logger.debug("处理日志流API请求") + + response = web.StreamResponse() + response.headers['Content-Type'] = 'text/event-stream' + response.headers['Cache-Control'] = 'no-cache' + response.headers['Connection'] = 'keep-alive' + + await response.prepare(request) + + # 这里应该实现真正的日志流 + # 暂时发送测试数据 + try: + while True: + test_log = { + "timestamp": asyncio.get_event_loop().time(), + "level": "INFO", + "message": "Log stream test message", + "source": "gui_api" + } + + event_data = f"data: {json.dumps(test_log)}\n\n" + await response.write(event_data.encode('utf-8')) + await asyncio.sleep(5) + + except asyncio.CancelledError: + logger.debug("日志流连接关闭") + finally: + await response.write_eof() + + return response + + except Exception as e: + logger.error(f"处理日志流请求时出错: {str(e)}", exc_info=True) + return web.json_response({"error": str(e)}, status=500) + + async def handle_plugins_list(self, request): + """处理插件列表请求""" + try: + logger.debug("处理插件列表API请求") + + plugin_service = self.service_manager.get_service("plugin") + if not plugin_service: + return web.json_response({"error": "Plugin service not available"}, status=503) + + plugins_info = [] + for plugin_name, plugin_info in plugin_service.plugin_info.items(): + plugins_info.append({ + "name": plugin_info.name, + "version": plugin_info.version, + "description": plugin_info.description, + "author": plugin_info.author, + "enabled": plugin_info.enabled, + "loaded": plugin_info.loaded, + "error_count": plugin_info.error_count, + "permissions": plugin_info.permissions + }) + + return web.json_response({"plugins": plugins_info}) + + except Exception as e: + logger.error(f"处理插件列表请求时出错: {str(e)}", exc_info=True) + return web.json_response({"error": str(e)}, status=500) + + async def handle_plugin_reload(self, request): + """处理插件重载请求""" + try: + plugin_name = request.match_info['plugin_name'] + logger.debug(f"处理插件重载API请求: {plugin_name}") + + plugin_service = self.service_manager.get_service("plugin") + if not plugin_service: + return web.json_response({"error": "Plugin service not available"}, status=503) + + # 先卸载再加载 + unload_success = await plugin_service.unload_plugin(plugin_name) + if unload_success: + load_success = await plugin_service.load_plugin(plugin_name) + result = {"reloaded": load_success} + else: + result = {"reloaded": False, "error": "Unload failed"} + + return web.json_response(result) + + except Exception as e: + logger.error(f"处理插件重载请求时出错: {str(e)}", exc_info=True) + return web.json_response({"error": str(e)}, status=500) + + async def handle_plugin_unload(self, request): + """处理插件卸载请求""" + try: + plugin_name = request.match_info['plugin_name'] + logger.debug(f"处理插件卸载API请求: {plugin_name}") + + plugin_service = self.service_manager.get_service("plugin") + if not plugin_service: + return web.json_response({"error": "Plugin service not available"}, status=503) + + success = await plugin_service.unload_plugin(plugin_name) + return web.json_response({"unloaded": success}) + + except Exception as e: + logger.error(f"处理插件卸载请求时出错: {str(e)}", exc_info=True) + return web.json_response({"error": str(e)}, status=500) + + async def handle_command_execute(self, request): + """处理命令执行请求""" + try: + data = await request.json() + command = data.get('command', '') + source = data.get('source', 'gui') + + logger.debug(f"处理命令执行API请求: {command}") + + command_service = self.service_manager.get_service("command") + if not command_service: + return web.json_response({"error": "Command service not available"}, status=503) + + result = await command_service.process_command(command, source) + + return web.json_response({ + "command": command, + "result": str(result), + "success": True + }) + + except Exception as e: + logger.error(f"处理命令执行请求时出错: {str(e)}", exc_info=True) + return web.json_response({"error": str(e)}, status=500) + + async def handle_command_history(self, request): + """处理命令历史请求""" + try: + limit = int(request.query.get('limit', 10)) + logger.debug(f"处理命令历史API请求,限制: {limit}") + + command_service = self.service_manager.get_service("command") + if not command_service: + return web.json_response({"error": "Command service not available"}, status=503) + + history = command_service.get_command_history(limit) + return web.json_response({"history": history}) + + except Exception as e: + logger.error(f"处理命令历史请求时出错: {str(e)}", exc_info=True) + return web.json_response({"error": str(e)}, status=500) + + async def handle_services_list(self, request): + """处理服务列表请求""" + try: + logger.debug("处理服务列表API请求") + + service_manager = self.service_manager.get_service("service_manager") + if not service_manager: + return web.json_response({"error": "Service manager not available"}, status=503) + + services_info = [] + for name, service in service_manager.services.items(): + services_info.append({ + "name": name, + "type": type(service).__name__, + "status": "running" # 简化状态 + }) + + return web.json_response({"services": services_info}) + + except Exception as e: + logger.error(f"处理服务列表请求时出错: {str(e)}", exc_info=True) + return web.json_response({"error": str(e)}, status=500) + + async def handle_service_status(self, request): + """处理服务状态请求""" + try: + service_name = request.match_info['service_name'] + logger.debug(f"处理服务状态API请求: {service_name}") + + try: + service = self.service_manager.get_service(service_name) + status_info = { + "name": service_name, + "available": True, + "status": "running" + } + + # 可以添加特定服务的状态检查 + if hasattr(service, 'is_running'): + status_info["status"] = "running" if service.is_running else "stopped" + + return web.json_response(status_info) + + except ValueError: + return web.json_response({"error": f"Service {service_name} not found"}, status=404) + + except Exception as e: + logger.error(f"处理服务状态请求时出错: {str(e)}", exc_info=True) + return web.json_response({"error": str(e)}, status=500) + + async def start(self, host: str = "localhost", port: int = 8080): + """启动GUI API服务器""" + try: + logger.info(f"启动GUI API服务器: {host}:{port}") + runner = web.AppRunner(self.app) + await runner.setup() + site = web.TCPSite(runner, host, port) + await site.start() + logger.debug("GUI API服务器启动完成") + return runner + except Exception as e: + logger.error(f"启动GUI API服务器时出错: {str(e)}", exc_info=True) + raise + + async def shutdown(self): + """关闭GUI API""" + try: + logger.info("关闭GUI API") + # 清理资源 + logger.debug("GUI API关闭完成") + except Exception as e: + logger.error(f"关闭GUI API时出错: {str(e)}", exc_info=True) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..8ef32cc --- /dev/null +++ b/main.py @@ -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 SenSuFramework: + """框架主类""" + + def __init__(self): + self.service_manager = ServiceManager() + self.is_running = False + logger.debug("🐱 SenSu 框架初始化开始") + + 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("🎉 SenSu 初始化完成!") + 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("🐱 SenSu 框架 已就绪!\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 Exception as e: + logger.warning(f"Shutdown service unavailable: {e}") + + # 如果关闭服务不可用,手动关闭其他服务 + 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 Exception as e: + logger.debug(f"Service shutdown skip: {e}") + + 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 = SenSuFramework() + + 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("🐱 框架进程结束") diff --git a/plugins/example_plugin/__init__.py b/plugins/example_plugin/__init__.py new file mode 100644 index 0000000..c32ed9a --- /dev/null +++ b/plugins/example_plugin/__init__.py @@ -0,0 +1,386 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import asyncio +from typing import Dict, Any +from aiohttp import web +import json + +# 导入命令装饰器 +try: + from fmfuncs.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 + +# 导入网络桥接类 - 修正路径 +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__) + +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}") + + async def initialize(self): + """初始化插件 - 安全版本""" + 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端点 + 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}") + + except Exception as e: + logger.error(f"设置网络路由时出错: {str(e)}", exc_info=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 _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)}") + + # 确保所有网络相关方法都检查 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)}" + + # ... 其余方法保持不变 ... + + 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) diff --git a/plugins/example_plugin/config.yaml b/plugins/example_plugin/config.yaml new file mode 100644 index 0000000..3cd7d09 --- /dev/null +++ b/plugins/example_plugin/config.yaml @@ -0,0 +1,16 @@ +# 示例插件配置 +name: "ExamplePlugin" +version: "1.0.0" +description: "这是一个示例插件,用于演示插件系统" +author: "CatFramework Team" + +# 插件特定配置 +settings: + enabled: true + auto_start: true + log_level: "INFO" + +# 示例功能配置 +features: + echo_enabled: true + greeting_message: "🐱 你好喵~" diff --git a/plugins/example_plugin/permissions.yaml b/plugins/example_plugin/permissions.yaml new file mode 100644 index 0000000..7f04592 --- /dev/null +++ b/plugins/example_plugin/permissions.yaml @@ -0,0 +1,16 @@ +# 示例插件权限申请 +plugin_name: "example_plugin" +permissions: + - "plugin.example.read" + - "plugin.example.write" + - "plugin.example.execute" + - "framework.event.subscribe" + - "framework.command.execute" + +# 权限说明 +permission_descriptions: + plugin.example.read: "读取示例插件数据" + plugin.example.write: "写入示例插件数据" + plugin.example.execute: "执行示例插件操作" + framework.event.subscribe: "订阅框架事件" + framework.command.execute: "执行框架命令" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3ce7922 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +rich>=13.0.0 +textual>=0.40.0 +websockets>=12.0 +aiohttp>=3.9.0 +pyyaml>=6.0 +watchdog>=3.0.0 +asyncio-mqtt>=0.16.0 +psutil>=5.9.0 diff --git a/service_manager.py b/service_manager.py new file mode 100644 index 0000000..5eb9868 --- /dev/null +++ b/service_manager.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +from typing import Dict, Any, List, Optional +import asyncio + +logger = logging.getLogger(__name__) + +class ServiceManager: + """服务管理器 - 管理所有框架服务的生命周期""" + + def __init__(self): + self.services: Dict[str, Any] = {} + self._startup_order: List[str] = [] + self._health_checks: Dict[str, Any] = {} + logger.debug("ServiceManager初始化完成") + + def register_service(self, name: str, service_instance, + depends_on: Optional[List[str]] = None, + health_check=None): + """注册服务""" + if name in self.services: + logger.warning(f"服务 {name} 已存在,将被覆盖") + self.services[name] = service_instance + self._startup_order.append(name) + if health_check: + self._health_checks[name] = health_check + logger.debug(f"服务 {name} 注册成功") + + def get_service(self, name: str): + """获取服务""" + service = self.services.get(name) + if not service: + raise ValueError(f"服务 {name} 未找到。可用: {list(self.services.keys())}") + return service + + def has_service(self, name: str) -> bool: + """检查服务是否已注册""" + return name in self.services + + async def check_health(self, name: str = None) -> Dict[str, bool]: + """健康检查""" + results = {} + names = [name] if name else list(self._health_checks.keys()) + for n in names: + if n in self._health_checks: + try: + r = self._health_checks[n]() + if asyncio.iscoroutine(r): r = await r + results[n] = bool(r) + except Exception as e: + logger.warning(f"服务 {n} 健康检查失败: {e}") + results[n] = False + return results + + @property + def startup_order(self) -> List[str]: + return list(self._startup_order) + + def shutdown_all(self): + """关闭所有服务""" + logger.info("开始关闭所有服务") + for name in reversed(self._startup_order): + service = self.services.get(name) + if service and hasattr(service, 'shutdown'): + try: + if asyncio.iscoroutinefunction(service.shutdown): + try: + loop = asyncio.get_running_loop() + loop.create_task(service.shutdown()) + except RuntimeError: + service.shutdown() + else: + service.shutdown() + logger.debug(f"服务 {name} 关闭成功") + except Exception as e: + logger.error(f"关闭服务 {name} 时出错: {e}") + logger.info("所有服务关闭完成") diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..33bd599 --- /dev/null +++ b/services/__init__.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging + +logger = logging.getLogger(__name__) + +class ServiceManager: + """服务管理器""" + + def __init__(self): + self.services = {} + # 记录框架启动时间 + self.start_time = time.time() + logger.debug("ServiceManager初始化完成") + + def register_service(self, name: str, service_instance): + """注册服务""" + try: + if name in self.services: + logger.warning(f"服务 {name} 已存在,将被覆盖") + self.services[name] = service_instance + logger.debug(f"服务 {name} 注册成功") + except Exception as e: + logger.error(f"注册服务 {name} 时出错: {str(e)}", exc_info=True) + raise + + def get_service(self, name: str): + """获取服务""" + try: + service = self.services.get(name) + if not service: + logger.error(f"服务 {name} 不存在") + raise ValueError(f"服务 {name} 未找到") + logger.debug(f"成功获取服务 {name}") + return service + except Exception as e: + logger.error(f"获取服务 {name} 时出错: {str(e)}", exc_info=True) + raise + + def shutdown_all(self): + """关闭所有服务""" + logger.info("开始关闭所有服务") + for name, service in self.services.items(): + try: + if hasattr(service, 'shutdown'): + service.shutdown() + logger.debug(f"服务 {name} 关闭成功") + except Exception as e: + logger.error(f"关闭服务 {name} 时出错: {str(e)}", exc_info=True) + logger.info("所有服务关闭完成") diff --git a/services/api_service.py b/services/api_service.py new file mode 100644 index 0000000..8cdd381 --- /dev/null +++ b/services/api_service.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import asyncio +from typing import Dict, List, Optional +from dataclasses import dataclass +from aiohttp import web +import json + +logger = logging.getLogger(__name__) + +@dataclass +class APIEndpoint: + """API端点数据类""" + path: str + methods: List[str] + handler: callable + plugin_name: str + require_auth: bool + admin_only: bool + +class APIService: + """API服务 - 管理插件的API端点""" + + def __init__(self, internet_service, auth_service, permission_service): + self.internet_service = internet_service + self.auth_service = auth_service + self.permission_service = permission_service + self.endpoints: Dict[str, APIEndpoint] = {} + self.plugin_endpoints: Dict[str, List[str]] = {} + logger.debug("APIService初始化开始") + + async def register_endpoint(self, plugin_name: str, path: str, methods: List[str], + handler: callable, require_auth: bool = True, + admin_only: bool = False) -> bool: + """注册API端点""" + try: + logger.debug(f"注册API端点: {plugin_name} -> {path} {methods}") + + # 权限检查 - 只有admin插件可以操作接口 + if not self.permission_service.check_plugin_permission(plugin_name, "admin"): + logger.error(f"插件 {plugin_name} 无权限注册API端点") + return False + + # 创建端点键 + endpoint_key = f"{plugin_name}:{path}" + + # 检查端点是否已存在 + if endpoint_key in self.endpoints: + logger.warning(f"API端点已存在: {endpoint_key}") + return False + + # 创建端点实例 + endpoint = APIEndpoint( + path=path, + methods=methods, + handler=handler, + plugin_name=plugin_name, + require_auth=require_auth, + admin_only=admin_only + ) + + # 注册到互联网服务 + for method in methods: + internet_endpoint_key = f"{method}:{path}" + self.internet_service.endpoints[internet_endpoint_key] = endpoint + + # 保存端点信息 + self.endpoints[endpoint_key] = endpoint + + # 更新插件端点映射 + if plugin_name not in self.plugin_endpoints: + self.plugin_endpoints[plugin_name] = [] + self.plugin_endpoints[plugin_name].append(endpoint_key) + + logger.info(f"API端点注册成功: {endpoint_key}") + return True + + except Exception as e: + logger.error(f"注册API端点时出错: {str(e)}", exc_info=True) + return False + + async def unregister_endpoint(self, plugin_name: str, path: str) -> bool: + """注销API端点""" + try: + logger.debug(f"注销API端点: {plugin_name} -> {path}") + + endpoint_key = f"{plugin_name}:{path}" + + if endpoint_key not in self.endpoints: + logger.warning(f"API端点不存在: {endpoint_key}") + return False + + endpoint = self.endpoints[endpoint_key] + + # 从互联网服务中移除 + for method in endpoint.methods: + internet_endpoint_key = f"{method}:{path}" + if internet_endpoint_key in self.internet_service.endpoints: + del self.internet_service.endpoints[internet_endpoint_key] + + # 从端点映射中移除 + del self.endpoints[endpoint_key] + + # 从插件端点列表中移除 + if plugin_name in self.plugin_endpoints: + if endpoint_key in self.plugin_endpoints[plugin_name]: + self.plugin_endpoints[plugin_name].remove(endpoint_key) + + logger.info(f"API端点注销成功: {endpoint_key}") + return True + + except Exception as e: + logger.error(f"注销API端点时出错: {str(e)}", exc_info=True) + return False + + async def unregister_all_plugin_endpoints(self, plugin_name: str) -> bool: + """注销插件的所有API端点""" + try: + logger.debug(f"注销插件所有API端点: {plugin_name}") + + if plugin_name not in self.plugin_endpoints: + logger.debug(f"插件无注册的API端点: {plugin_name}") + return True + + endpoints_to_remove = self.plugin_endpoints[plugin_name][:] + success_count = 0 + + for endpoint_key in endpoints_to_remove: + # 从endpoint_key中提取path + parts = endpoint_key.split(':', 1) + if len(parts) == 2: + path = parts[1] + success = await self.unregister_endpoint(plugin_name, path) + if success: + success_count += 1 + + logger.info(f"插件API端点清理完成: {plugin_name} -> 成功 {success_count}/{len(endpoints_to_remove)}") + return success_count == len(endpoints_to_remove) + + except Exception as e: + logger.error(f"注销插件所有API端点时出错: {str(e)}", exc_info=True) + return False + + def get_plugin_endpoints(self, plugin_name: str) -> List[Dict]: + """获取插件的API端点列表""" + try: + if plugin_name not in self.plugin_endpoints: + return [] + + endpoints_info = [] + for endpoint_key in self.plugin_endpoints[plugin_name]: + if endpoint_key in self.endpoints: + endpoint = self.endpoints[endpoint_key] + endpoints_info.append({ + 'path': endpoint.path, + 'methods': endpoint.methods, + 'require_auth': endpoint.require_auth, + 'admin_only': endpoint.admin_only + }) + + logger.debug(f"获取插件API端点列表: {plugin_name} -> {len(endpoints_info)} 个") + return endpoints_info + + except Exception as e: + logger.error(f"获取插件API端点列表时出错: {str(e)}", exc_info=True) + return [] + + def get_all_endpoints(self) -> List[Dict]: + """获取所有API端点""" + try: + all_endpoints = [] + for endpoint_key, endpoint in self.endpoints.items(): + all_endpoints.append({ + 'plugin': endpoint.plugin_name, + 'path': endpoint.path, + 'methods': endpoint.methods, + 'require_auth': endpoint.require_auth, + 'admin_only': endpoint.admin_only + }) + + logger.debug(f"获取所有API端点: {len(all_endpoints)} 个") + return all_endpoints + + except Exception as e: + logger.error(f"获取所有API端点时出错: {str(e)}", exc_info=True) + return [] + + async def shutdown(self): + """关闭API服务""" + try: + logger.info("关闭API服务") + + # 注销所有端点 + all_plugins = list(self.plugin_endpoints.keys()) + for plugin_name in all_plugins: + await self.unregister_all_plugin_endpoints(plugin_name) + + self.endpoints.clear() + self.plugin_endpoints.clear() + + logger.debug("API服务关闭完成") + + except Exception as e: + logger.error(f"关闭API服务时出错: {str(e)}", exc_info=True) diff --git a/services/auth_service.py b/services/auth_service.py new file mode 100644 index 0000000..b4fb653 --- /dev/null +++ b/services/auth_service.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import os +import hashlib +import secrets +from typing import Dict, List, Optional +from dataclasses import dataclass +import time +import json + +logger = logging.getLogger(__name__) + +@dataclass +class User: + """用户数据类""" + username: str + password_hash: str + permissions: List[str] + is_active: bool = True + created_at: float = None + +@dataclass +class Token: + """令牌数据类""" + token: str + username: str + permissions: List[str] + created_at: float + expires_at: float + is_valid: bool = True + +class AuthService: + """认证服务 - 处理用户认证和权限验证""" + + def __init__(self, config: Dict): + self.config = config + self.users: Dict[str, User] = {} + self.tokens: Dict[str, Token] = {} + self.token_expiry_hours = 24 + self.secret_key = secrets.token_hex(32) + logger.debug("AuthService初始化开始") + + # 初始化默认用户 + self._init_default_users() + + def _init_default_users(self): + """初始化默认用户""" + try: + # 创建默认管理员用户 + admin_password_hash = self._hash_password(os.environ.get("SENSU_ADMIN_PASSWORD","admin123")) + admin_user = User( + username="admin", + password_hash=admin_password_hash, + permissions=["admin"], + created_at=time.time() + ) + self.users["admin"] = admin_user + + # 创建默认API用户 + api_password_hash = self._hash_password(os.environ.get("SENSU_API_PASSWORD","api123")) + api_user = User( + username="api", + password_hash=api_password_hash, + permissions=["framework.status.read", "plugin.info.read"], + created_at=time.time() + ) + self.users["api"] = api_user + + logger.debug("默认用户初始化完成") + + except Exception as e: + logger.error(f"初始化默认用户时出错: {str(e)}", exc_info=True) + raise + + def _hash_password(self, password: str) -> str: + """哈希密码""" + try: + salt = "catframework_salt" # 实际应该使用随机盐 + return hashlib.sha256((password + salt).encode()).hexdigest() + except Exception as e: + logger.error(f"哈希密码时出错: {str(e)}", exc_info=True) + raise + + def authenticate_user(self, username: str, password: str) -> Optional[Token]: + """用户认证""" + try: + logger.debug(f"用户认证尝试: {username}") + + if username not in self.users: + logger.warning(f"用户不存在: {username}") + return None + + user = self.users[username] + + if not user.is_active: + logger.warning(f"用户已被禁用: {username}") + return None + + password_hash = self._hash_password(password) + if user.password_hash != password_hash: + logger.warning(f"密码错误: {username}") + return None + + # 创建令牌 + token = self._create_token(user) + logger.debug(f"用户认证成功: {username}") + return token + + except Exception as e: + logger.error(f"用户认证时出错: {str(e)}", exc_info=True) + return None + + def _create_token(self, user: User) -> Token: + """创建令牌""" + try: + token_str = secrets.token_hex(32) + created_at = time.time() + expires_at = created_at + (self.token_expiry_hours * 3600) + + token = Token( + token=token_str, + username=user.username, + permissions=user.permissions, + created_at=created_at, + expires_at=expires_at + ) + + self.tokens[token_str] = token + logger.debug(f"创建令牌: {user.username}, 有效期: {self.token_expiry_hours}小时") + return token + + except Exception as e: + logger.error(f"创建令牌时出错: {str(e)}", exc_info=True) + raise + + def validate_token(self, token_str: str) -> Optional[Token]: + """验证令牌""" + try: + if token_str not in self.tokens: + logger.debug("令牌不存在") + return None + + token = self.tokens[token_str] + + # 检查令牌是否有效 + if not token.is_valid: + logger.debug("令牌已失效") + return None + + # 检查令牌是否过期 + if time.time() > token.expires_at: + logger.debug("令牌已过期") + token.is_valid = False + return None + + logger.debug(f"令牌验证成功: {token.username}") + return token + + except Exception as e: + logger.error(f"验证令牌时出错: {str(e)}", exc_info=True) + return None + + def revoke_token(self, token_str: str) -> bool: + """撤销令牌""" + try: + if token_str in self.tokens: + self.tokens[token_str].is_valid = False + logger.debug(f"令牌已撤销: {token_str}") + return True + else: + logger.warning(f"要撤销的令牌不存在: {token_str}") + return False + except Exception as e: + logger.error(f"撤销令牌时出错: {str(e)}", exc_info=True) + return False + + def check_permission(self, token_str: str, permission: str) -> bool: + """检查权限""" + try: + token = self.validate_token(token_str) + if not token: + return False + + # 检查admin权限 + if "admin" in token.permissions: + return True + + # 检查具体权限 + has_permission = permission in token.permissions + + logger.debug(f"权限检查: {token.username} -> {permission} = {has_permission}") + return has_permission + + except Exception as e: + logger.error(f"检查权限时出错: {str(e)}", exc_info=True) + return False + + def create_user(self, username: str, password: str, permissions: List[str]) -> bool: + """创建用户""" + try: + if username in self.users: + logger.warning(f"用户已存在: {username}") + return False + + password_hash = self._hash_password(password) + user = User( + username=username, + password_hash=password_hash, + permissions=permissions, + created_at=time.time() + ) + + self.users[username] = user + logger.debug(f"用户创建成功: {username}, 权限: {permissions}") + return True + + except Exception as e: + logger.error(f"创建用户时出错: {str(e)}", exc_info=True) + return False + + def get_user_info(self, username: str) -> Optional[Dict]: + """获取用户信息""" + try: + if username not in self.users: + return None + + user = self.users[username] + return { + "username": user.username, + "permissions": user.permissions, + "is_active": user.is_active, + "created_at": user.created_at + } + except Exception as e: + logger.error(f"获取用户信息时出错: {str(e)}", exc_info=True) + return None + + def cleanup_expired_tokens(self): + """清理过期令牌""" + try: + current_time = time.time() + expired_tokens = [] + + for token_str, token in self.tokens.items(): + if current_time > token.expires_at: + expired_tokens.append(token_str) + + for token_str in expired_tokens: + del self.tokens[token_str] + + if expired_tokens: + logger.debug(f"清理了 {len(expired_tokens)} 个过期令牌") + + except Exception as e: + logger.error(f"清理过期令牌时出错: {str(e)}", exc_info=True) + + def shutdown(self): + """关闭认证服务""" + try: + logger.info("关闭认证服务") + self.cleanup_expired_tokens() + self.users.clear() + self.tokens.clear() + logger.debug("认证服务关闭完成") + except Exception as e: + logger.error(f"关闭认证服务时出错: {str(e)}", exc_info=True) diff --git a/services/command_service.py b/services/command_service.py new file mode 100644 index 0000000..66c7917 --- /dev/null +++ b/services/command_service.py @@ -0,0 +1,604 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import asyncio +import shlex +from typing import Dict, List, Callable, Any +from dataclasses import dataclass +import os + +logger = logging.getLogger(__name__) + +@dataclass +class Command: + """命令数据类""" + name: str + handler: Callable + description: str + permissions: List[str] + source: str = "internal" + +class CommandService: + """指令服务""" + + def __init__(self, service_manager=None): + self.commands: Dict[str, Command] = {} + self.command_history: List[Dict] = [] + self.max_history_size = 100 + self.service_manager = service_manager # 添加服务管理器引用 + logger.debug("CommandService初始化开始") + + + def register_command(self, name: str, handler: Callable, description: str = "", + permissions: List[str] = None, source: str = "plugins"): + """注册命令""" + # 来源如 internal plugins system 等 + try: + if name in self.commands: + logger.warning(f"命令 {name} 已存在,将被覆盖") + + self.commands[name] = Command( + name=name, + handler=handler, + description=description or f"命令: {name}", + permissions=permissions or [], + source=source + ) + logger.debug(f"注册命令: {name} (来源: {source})") + + except Exception as e: + logger.error(f"注册命令 {name} 时出错: {str(e)}", exc_info=True) + raise + + async def _handle_permission_command(self, command: str, args: List[str], source: str) -> str: + """处理权限相关命令""" + try: + permission_service = self.service_manager.get_service("permission") + if not permission_service: + return "❌ 权限服务不可用" + + return await permission_service.process_permission_command(command, args) + + except Exception as e: + logger.error(f"处理权限命令时出错: {str(e)}", exc_info=True) + return f"❌ 处理权限命令时出错: {str(e)}" + + async def process_command(self, command_string: str, source: str = "unknown") -> Any: + """处理命令""" + try: + logger.debug(f"处理命令: '{command_string}' (来源: {source})") + + # 解析命令 + parts = shlex.split(command_string.strip()) + if not parts: + logger.warning("空命令") + return "空命令" + + command_name = parts[0] + args = parts[1:] + + # 记录命令历史 + self._add_to_history(command_string, source) + + # 检查权限命令 + permission_commands = ['pmallow', 'pmdeny', 'pmignore', 'permissions', + 'pmpending', 'pmrequests', 'pm_plugin_status', 'pmhelp', 'pmtest'] + + if command_name in permission_commands: + permission_service = self.service_manager.get_service("permission") + if not permission_service: + return "❌ 权限服务不可用" + + # 直接调用权限服务处理命令 + return await permission_service.process_permission_command(command_name, args) + + # 查找其他命令 + if command_name not in self.commands: + logger.warning(f"未知命令: {command_name}") + return f"未知命令: {command_name}" + + command = self.commands[command_name] + + # 执行命令 + try: + result = await self._execute_command(command, args, source) + logger.debug(f"命令执行成功: {command_name}") + return result + + except Exception as e: + logger.error(f"命令执行失败 {command_name}: {str(e)}", exc_info=True) + return f"命令执行错误: {str(e)}" + + except Exception as e: + logger.error(f"处理命令时出错: {str(e)}", exc_info=True) + return f"命令处理错误: {str(e)}" + + async def _execute_command(self, command: Command, args: List[str], source: str) -> Any: + """执行命令""" + try: + # 检查处理器类型 + if asyncio.iscoroutinefunction(command.handler): + result = await command.handler(*args) + else: + result = command.handler(*args) + + logger.debug(f"命令 {command.name} 执行完成") + return result + + except TypeError as e: + logger.error(f"命令参数错误 {command.name}: {str(e)}", exc_info=True) + raise ValueError(f"参数错误: {str(e)}") + except Exception as e: + logger.error(f"命令执行异常 {command.name}: {str(e)}", exc_info=True) + raise + + def _add_to_history(self, command: str, source: str): + """添加到命令历史""" + try: + history_entry = { + "command": command, + "source": source, + "timestamp": asyncio.get_event_loop().time() + } + self.command_history.append(history_entry) + + # 限制历史记录大小 + if len(self.command_history) > self.max_history_size: + self.command_history.pop(0) + + logger.debug(f"命令历史记录添加,当前大小: {len(self.command_history)}") + + except Exception as e: + logger.error(f"添加命令历史时出错: {str(e)}", exc_info=True) + + def get_command_list(self) -> List[Dict]: + """获取命令列表""" + try: + command_list = [] + for name, cmd in self.commands.items(): + command_list.append({ + "name": name, + "description": cmd.description, + "permissions": cmd.permissions, + "source": cmd.source + }) + + logger.debug(f"获取命令列表,共 {len(command_list)} 个命令") + return command_list + + except Exception as e: + logger.error(f"获取命令列表时出错: {str(e)}", exc_info=True) + return [] + + def get_command_history(self, limit: int = 10) -> List[Dict]: + """获取命令历史""" + try: + history = self.command_history[-limit:] + logger.debug(f"获取命令历史,返回 {len(history)} 条记录") + return history + + except Exception as e: + logger.error(f"获取命令历史时出错: {str(e)}", exc_info=True) + return [] + + def register_builtin_commands(self): + """注册内置命令""" + try: + logger.debug("开始注册内置命令") + + # 帮助命令 + self.register_command( + name="help", + handler=self._cmd_help, + description="显示帮助信息", + permissions=["framework.command.help.read"], + source="internal" + ) + + # 测试日志命令 + self.register_command( + name="testlog", + handler=self._cmd_test_log, + description="生成测试日志", + permissions=["framework.command.test"], + source="internal" + ) + + # 状态命令 + self.register_command( + name="status", + handler=self._cmd_status, + description="显示框架状态", + permissions=["framework.status.read"], + source="internal" + ) + + # 历史命令 + self.register_command( + name="history", + handler=self._cmd_history, + description="显示命令历史", + permissions=["framework.command.history.read"], + source="internal" + ) + + # 网络诊断 + self.register_command( + name="netdiag", + handler=self._cmd_netdiag, + description="网络服务诊断", + permissions=["framework.network.diagnose"], + source="internal" + ) + + # 权限管理命令组 - 保留注册但不使用(在process_command中直接处理) + # 这些注册是为了在help命令中显示 + permission_commands = [ + ("pmallow", "权限管理: 同意权限请求"), + ("pmdeny", "权限管理: 拒绝权限请求"), + ("pmignore", "权限管理: 暂时忽略权限请求"), + ("permissions", "权限管理: 显示权限状态"), + ("pmpending", "权限管理: 查看待授权请求列表"), + ("pmrequests", "权限管理: 查看待授权请求列表(别名)"), + ("pm_plugin_status", "权限管理: 查看插件权限状态"), + ("pmtest", "权限管理: 测试权限配置文件"), + ("pmhelp", "权限管理: 显示权限命令帮助") + ] + + for cmd_name, description in permission_commands: + self.register_command( + name=cmd_name, + handler=self._cmd_permission, # 使用统一的备用处理器 + description=description, + permissions=["framework.permission.read"], + source="internal" + ) + + # 滚动控制命令组 + scroll_commands = [ + ("scroll", "滚动控制: 手动滚动到底部"), + ("autoscroll", "滚动控制: 切换自动滚动") + ] + + # 脚手架命令 + self.register_command( + name="create-plugin", + handler=self._cmd_create_plugin, + description="创建新插件脚手架", + permissions=["framework.scaffold.plugin"], + source="internal" + ) + + logger.info(f"内置命令注册完成,共注册 {len(self.commands)} 个命令") + + + for cmd_name, description in scroll_commands: + self.register_command( + name=cmd_name, + handler=self._cmd_scroll_control, + description=description, + permissions=["framework.tui.control"], + source="internal" + ) + + + + logger.info(f"内置命令注册完成,共注册 {len(self.commands)} 个命令") + + except Exception as e: + logger.error(f"注册内置命令时出错: {str(e)}", exc_info=True) + raise + + async def _cmd_netdiag(self, *args) -> str: + """网络诊断命令""" + try: + internet_service = self.service_manager.get_service("internet") + + result = ["🔧 **网络服务诊断报告**"] + result.append("=" * 50) + + if not internet_service: + result.append("❌ 网络服务未注册") + result.append("\n💡 **可能的原因:**") + result.append(" 1. 网络服务启动失败") + result.append(" 2. 依赖包缺失 (aiohttp)") + result.append(" 3. 端口被占用") + result.append(" 4. 权限不足") + result.append("\n🔧 **解决方案:**") + result.append(" - 检查上方日志中的错误信息") + result.append(" - 运行: pip install aiohttp") + result.append(" - 尝试更换端口号") + result.append(" - 使用 sudo (如果需要)") + return "\n".join(result) + + # 获取健康信息 + health_info = await internet_service.check_service_health() + + result.append(f"🔄 服务运行: {'✅ 是' if health_info.get('is_running') else '❌ 否'}") + result.append(f"🔌 HTTP端口: {health_info.get('http_port', 'N/A')}") + result.append(f"📡 WebSocket端口: {health_info.get('websocket_port', 'N/A')}") + result.append(f"🌐 HTTP活跃: {'✅ 是' if health_info.get('http_active') else '❌ 否'}") + result.append(f"📦 依赖状态: {'✅ 正常' if health_info.get('dependencies_available') else '❌ 缺失'}") + + if health_info.get('error'): + result.append(f"❌ 错误信息: {health_info['error']}") + + # 端口占用检查 + if not health_info.get('http_active') and health_info.get('is_running'): + result.append("\n⚠️ **端口问题检测:**") + result.append(" HTTP服务已启动但端口未响应") + result.append(" 可能被防火墙阻止或配置错误") + + # 路由信息 + routes = internet_service.get_plugin_routes() + total_routes = sum(len(plugin_routes) for plugin_routes in routes.values()) + result.append(f"\n🛣️ 注册路由: {total_routes} 个") + + for plugin_name, plugin_routes in routes.items(): + result.append(f" 📍 {plugin_name}: {len(plugin_routes)} 个路由") + + return "\n".join(result) + + except Exception as e: + logger.error(f"网络诊断命令执行失败: {str(e)}") + return f"❌ 网络诊断失败: {str(e)}" + + async def _cmd_scroll_control(self, *args) -> str: + """处理滚动控制命令""" + try: + tui_service = self.service_manager.get_service("tui") + if not tui_service: + return "❌ TUI服务不可用" + + if not args: + return "🔧 滚动控制命令\n💡 使用: scroll [log|message|all]\n💡 使用: autoscroll [on|off|toggle] [log|message|all]" + + command = args[0].lower() + + if command == "scroll": + target = args[1] if len(args) > 1 else "all" + if target not in ["log", "message", "all"]: + return "❌ 无效的目标,请使用: log, message, all" + return tui_service.scroll_to_bottom(target) + + elif command == "autoscroll": + if len(args) < 2: + return "❌ 请指定操作: on, off, toggle" + + action = args[1].lower() + target = args[2] if len(args) > 2 else "all" + + if target not in ["log", "message", "all"]: + return "❌ 无效的目标,请使用: log, message, all" + + if action == "on": + return tui_service.toggle_auto_scroll(target, True) + elif action == "off": + return tui_service.toggle_auto_scroll(target, False) + elif action == "toggle": + return tui_service.toggle_auto_scroll(target, None) + else: + return "❌ 无效的操作,请使用: on, off, toggle" + + else: + return "❌ 未知滚动命令\n💡 可用命令: scroll, autoscroll" + + except Exception as e: + logger.error(f"处理滚动命令时出错: {str(e)}") + return f"❌ 滚动命令错误: {str(e)}" + + + async def _cmd_permission(self, *args) -> str: + """处理权限相关命令 - 备用处理器""" + try: + permission_service = self.service_manager.get_service("permission") + if not permission_service: + return "❌ 权限服务不可用" + + # 如果没有参数,显示通用帮助 + if not args: + return "🔐 权限管理命令\n💡 使用 pmhelp 查看详细帮助" + + # 否则直接转发到权限服务 + command_name = str(args[0]).lower() + permission_args = [str(arg) for arg in args[1:]] if len(args) > 1 else [] + + return await permission_service.process_permission_command(command_name, permission_args) + + except Exception as e: + logger.error(f"处理权限命令时出错: {str(e)}", exc_info=True) + return f"❌ 权限命令错误: {str(e)}" + + + async def _cmd_help(self, *args) -> str: + """帮助命令处理器""" + try: + commands = self.get_command_list() + if not commands: + return "❌ 没有可用的命令" + + help_text = ["📋 **可用命令:**", ""] + + # 按来源分组显示命令 + commands_by_source = {} + for cmd in commands: + source = cmd['source'] + if source not in commands_by_source: + commands_by_source[source] = [] + commands_by_source[source].append(cmd) + + # 显示内置命令 + if 'internal' in commands_by_source: + help_text.append("🔧 **内置命令:**") + for cmd in commands_by_source['internal']: + help_text.append(f" 🟢 {cmd['name']:15} - {cmd['description']}") + help_text.append("") + + # 显示插件命令 + if 'plugin' in commands_by_source: + help_text.append("🔌 **插件命令:**") + for cmd in commands_by_source['plugin']: + help_text.append(f" 🟡 {cmd['name']:15} - {cmd['description']}") + help_text.append("") + + # 显示系统命令 + if 'system' in commands_by_source: + help_text.append("⚙️ **系统命令:**") + for cmd in commands_by_source['system']: + help_text.append(f" 🔵 {cmd['name']:15} - {cmd['description']}") + + # 添加使用提示 + help_text.extend([ + "", + "💡 **使用提示:**", + " - 输入命令名称执行命令", + " - 使用 'status' 查看框架状态", + " - 使用 'history' 查看命令历史", + " - 使用 'permissions' 管理插件权限" + ]) + + return "\n".join(help_text) + + except Exception as e: + logger.error(f"处理help命令时出错: {str(e)}", exc_info=True) + return f"❌ 帮助命令错误: {str(e)}" + + async def _cmd_status(self, *args) -> str: + """状态命令处理器""" + try: + status_info = [ + f"命令服务状态:", + f" 注册命令数: {len(self.commands)}", + f" 历史记录数: {len(self.command_history)}", + f" 最大历史大小: {self.max_history_size}" + ] + return "\n".join(status_info) + + except Exception as e: + logger.error(f"处理status命令时出错: {str(e)}", exc_info=True) + return f"状态命令错误: {str(e)}" + + async def _cmd_history(self, *args) -> str: + """历史命令处理器""" + try: + limit = 10 + if args and args[0].isdigit(): + limit = min(int(args[0]), 50) # 限制最大50条 + + history = self.get_command_history(limit) + if not history: + return "没有命令历史" + + history_text = [f"最近 {len(history)} 条命令历史:"] + for i, entry in enumerate(reversed(history), 1): + history_text.append(f" {i}. [{entry['source']}] {entry['command']}") + + return "\n".join(history_text) + + except Exception as e: + logger.error(f"处理history命令时出错: {str(e)}", exc_info=True) + return f"历史命令错误: {str(e)}" + + async def _cmd_test_log(self, *args) -> str: + """测试日志命令""" + try: + logger.debug("这是一条DEBUG测试日志") + logger.info("这是一条INFO测试日志") + logger.warning("这是一条WARNING测试日志") + logger.error("这是一条ERROR测试日志") + return "✅ 测试日志已生成,请检查TUI显示" + except Exception as e: + return f"❌ 测试日志生成失败: {str(e)}" + + async def _cmd_create_plugin(self, *args) -> str: + """创建新插件脚手架""" + try: + import re + import shutil + from pathlib import Path + from string import Template + + # 1. 参数解析 + if not args: + return "❌ 用法: create-plugin <插件名> [--author <作者>] [--desc <描述>]\n💡 插件名需为小写字母/数字/下划线,如: my_cool_plugin" + + plugin_name = args[0] + author = "Unknown" + description = "暂无描述" + + # 解析可选参数 + i = 1 + while i < len(args): + if args[i] == "--author" and i + 1 < len(args): + author = args[i+1] + i += 2 + elif args[i] == "--desc" and i + 1 < len(args): + description = args[i+1] + i += 2 + else: + i += 1 + + # 2. 命名校验 + if not re.match(r'^[a-z][a-z0-9_]*$', plugin_name): + return "❌ 插件名格式错误。请使用小写字母开头,仅包含小写字母、数字和下划线(如: data_sync)" + + plugin_dir = Path("plugins") / plugin_name + if plugin_dir.exists(): + return f"❌ 插件目录已存在: {plugin_dir}" + + # 3. 模板路径 + template_dir = Path(os.getenv("SENSU_CODE_DIR", ".")) / "templates" / "plugin" + + if not template_dir.exists(): + return "❌ 模板目录不存在: templates/plugin/" + + # 4. 创建目录与渲染文件 + plugin_dir.mkdir(parents=True, exist_ok=True) + context = { + "plugin_name": plugin_name, + "author": author, + "description": description + } + + for template_file in template_dir.iterdir(): + if template_file.is_file() and template_file.name.endswith(".template"): + with open(template_file, 'r', encoding='utf-8') as f: + tpl = Template(f.read()) + content = tpl.safe_substitute(context) + + target_name = template_file.stem + target_path = plugin_dir / target_name + + with open(target_path, 'w', encoding='utf-8') as f: + f.write(content) + logger.debug(f"脚手架文件生成: {target_path}") + + return ( + f"✅ 插件脚手架创建成功!\n" + f"📁 路径: {plugin_dir}\n" + f"👤 作者: {author}\n" + f"📝 描述: {description}\n\n" + f"🔧 下一步:\n" + f" 1. 编辑 {plugin_dir}/__init__.py 实现业务逻辑\n" + f" 2. 运行框架自动加载插件\n" + f" 3. 使用 `help` 查看可用命令" + ) + + except Exception as e: + logger.error(f"创建插件脚手架失败: {str(e)}", exc_info=True) + return f"❌ 创建失败: {str(e)}" + + + + + + def shutdown(self): + """关闭指令服务""" + try: + logger.info("关闭指令服务") + self.commands.clear() + self.command_history.clear() + logger.debug("指令服务关闭完成") + except Exception as e: + logger.error(f"关闭指令服务时出错: {str(e)}", exc_info=True) diff --git a/services/init_service.py b/services/init_service.py new file mode 100644 index 0000000..bf418b5 --- /dev/null +++ b/services/init_service.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import asyncio +from pathlib import Path +from typing import Dict, Any +import yaml +import importlib.util +import sys +import os + +logger = logging.getLogger(__name__) + +class InitService: + """初始化服务""" + + def __init__(self, config_path: str = "config/framework"): + self.config_path = Path(config_path) + self.configs: Dict[str, Any] = {} + self.fmfuncs_loaded = False + logger.debug("InitService初始化开始") + + async def initialize_framework(self): + """初始化框架""" + try: + logger.info("开始初始化框架") + + # 1. 加载配置 + await self._load_configs() + + # 2. 创建必要目录 + await self._create_directories() + + # 3. 加载框架功能集 + await self._load_fmfuncs() + + # 4. 验证初始化状态 + await self._validate_init() + + logger.info("框架初始化完成") + return self.configs + + except Exception as e: + logger.error(f"框架初始化失败: {str(e)}", exc_info=True) + raise + + async def _load_configs(self): + """加载配置文件""" + try: + logger.debug("开始加载配置文件") + + if not self.config_path.exists(): + logger.warning(f"配置路径不存在: {self.config_path},将创建默认配置") + self.config_path.mkdir(parents=True, exist_ok=True) + + # 加载基础配置 + base_config_file = self.config_path / "base_config.yaml" + if base_config_file.exists(): + with open(base_config_file, 'r', encoding='utf-8') as f: + self.configs['base'] = yaml.safe_load(f) + logger.debug("基础配置加载成功") + else: + logger.warning("基础配置文件不存在,使用默认配置") + self.configs['base'] = self._get_default_base_config() + self._save_config(base_config_file, self.configs['base']) + + # 加载权限规则 + permission_file = self.config_path / "permission_rules.yaml" + if permission_file.exists(): + with open(permission_file, 'r', encoding='utf-8') as f: + self.configs['permission_rules'] = yaml.safe_load(f) + logger.debug("权限规则配置加载成功") + else: + logger.warning("权限规则文件不存在,使用默认配置") + self.configs['permission_rules'] = self._get_default_permission_rules() + self._save_config(permission_file, self.configs['permission_rules']) + + logger.debug(f"配置文件加载完成,共加载 {len(self.configs)} 个配置集") + + except Exception as e: + logger.error(f"加载配置文件时出错: {str(e)}", exc_info=True) + raise + + async def _create_directories(self): + """创建必要目录""" + try: + logger.debug("开始创建必要目录") + + directories = [ + "config/plugins", + "config/services", + "config/permissions", + "logs/runtime", + "logs/debug", + "plugins", + "utils", + "fmfuncs" + ] + + for dir_path in directories: + path = Path(dir_path) + path.mkdir(parents=True, exist_ok=True) + logger.debug(f"创建目录: {dir_path}") + + logger.debug("目录创建完成") + + except Exception as e: + logger.error(f"创建目录时出错: {str(e)}", exc_info=True) + raise + + async def _load_fmfuncs(self): + """加载框架功能集""" + try: + logger.debug("开始加载框架功能集") + + fmfuncs_path = Path(os.getenv("SENSU_CODE_DIR", ".")) / "fmfuncs" + if not fmfuncs_path.exists(): + logger.warning("fmfuncs目录不存在,跳过加载") + return + + # 动态加载所有Python文件 + for py_file in fmfuncs_path.glob("*.py"): + if py_file.name == "__init__.py": + continue + + try: + module_name = f"fmfuncs.{py_file.stem}" + spec = importlib.util.spec_from_file_location(module_name, py_file) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + + logger.debug(f"加载框架功能: {module_name}") + + except Exception as e: + logger.error(f"加载框架功能 {py_file} 时出错: {str(e)}", exc_info=True) + continue + + self.fmfuncs_loaded = True + logger.debug("框架功能集加载完成") + + except Exception as e: + logger.error(f"加载框架功能集时出错: {str(e)}", exc_info=True) + raise + + async def _validate_init(self): + """验证初始化状态""" + try: + logger.debug("开始验证初始化状态") + + required_configs = ['base', 'permission_rules'] + for config_name in required_configs: + if config_name not in self.configs: + logger.error(f"缺少必要配置: {config_name}") + raise ValueError(f"缺少必要配置: {config_name}") + + required_dirs = ['config', 'logs', 'plugins'] + for dir_name in required_dirs: + if not Path(dir_name).exists(): + logger.error(f"必要目录不存在: {dir_name}") + raise ValueError(f"必要目录不存在: {dir_name}") + + logger.debug("初始化状态验证通过") + + except Exception as e: + logger.error(f"验证初始化状态时出错: {str(e)}", exc_info=True) + raise + + def _get_default_base_config(self) -> Dict: + """获取默认基础配置""" + return { + 'framework': { + 'name': 'SenSu', + 'version': 'Alpha_0.2.0', + 'debug': True + }, + 'logging': { + 'level': 'INFO', + 'debug_level_file': True, + 'max_log_files': 20, + 'max_file_size': '10MB' + }, + 'tui': { + 'layout': { + 'grid-rows': '4fr 5fr 1fr' + } + }, + 'services': { + 'internet': { + 'ws_port': 8765, + 'api_port': 8000, + 'enable_reverse_proxy': False + } + }, + 'plugins': { + 'auto_load': True, + 'hot_reload': True, + 'max_retry_count': 3 + } + } + + def _get_default_permission_rules(self) -> Dict: + """获取默认权限规则""" + return { + 'permission_levels': ['read', 'write', 'execute', 'admin'], + 'default_permissions': [ + 'framework.status.read', + 'plugin.self.info.read' + ], + 'admin_permissions': [ + 'framework.*', + 'plugin.*', + 'service.*' + ] + } + + def _save_config(self, file_path: Path, config: Dict): + """保存配置到文件""" + try: + with open(file_path, 'w', encoding='utf-8') as f: + yaml.dump(config, f, default_flow_style=False, allow_unicode=True) + logger.debug(f"配置保存到: {file_path}") + except Exception as e: + logger.error(f"保存配置到 {file_path} 时出错: {str(e)}", exc_info=True) + + def get_config(self, config_name: str) -> Dict: + """获取配置""" + try: + config = self.configs.get(config_name) + if not config: + logger.error(f"配置不存在: {config_name}") + raise ValueError(f"配置 {config_name} 不存在") + logger.debug(f"获取配置: {config_name}") + return config + except Exception as e: + logger.error(f"获取配置 {config_name} 时出错: {str(e)}", exc_info=True) + raise + + def shutdown(self): + """关闭初始化服务""" + try: + logger.info("关闭初始化服务") + self.configs.clear() + logger.debug("初始化服务关闭完成") + except Exception as e: + logger.error(f"关闭初始化服务时出错: {str(e)}", exc_info=True) diff --git a/services/internet_service.py b/services/internet_service.py new file mode 100644 index 0000000..c4bc59d --- /dev/null +++ b/services/internet_service.py @@ -0,0 +1,464 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import asyncio +from typing import Dict, List, Callable, Any, Optional +from pathlib import Path +import aiohttp +from aiohttp import web +import json +import ssl + +logger = logging.getLogger(__name__) + +class InternetService: + """网络服务 - 管理HTTP和WebSocket服务器""" + + def __init__(self, config: Dict, service_manager): + self.config = config + self.service_manager = service_manager + self.http_app = web.Application() + self.http_runner = None + self.ws_connections: Dict[str, List] = {} + self.plugin_routes: Dict[str, List] = {} + self.is_running = False + + # 从配置获取端口 + internet_config = config.get('internet', {}) + + ws_config = internet_config.get('websocket', {}) + self.ws_host = ws_config.get('host', '0.0.0.0') + self.ws_port = ws_config.get('port', 8765) + + http_config = internet_config.get('http', {}) + self.http_host = http_config.get('host', '0.0.0.0') + self.http_port = http_config.get('port', 8000) + + logger.debug("InternetService初始化开始") + + async def start(self): + """启动网络服务 - 精准错误处理与端口复用版""" + try: + logger.info("启动网络服务") + self._setup_default_routes() + + # 初始化 AppRunner + self.http_runner = web.AppRunner(self.http_app) + await self.http_runner.setup() + + # 1. 启动 HTTP 站点 + try: + self.site = web.TCPSite( + self.http_runner, self.http_host, self.http_port, + reuse_address=True, reuse_port=True + ) + await self.site.start() + logger.info(f"✅ HTTP 服务已绑定: {self.http_host}:{self.http_port}") + except OSError as e: + logger.error(f"❌ HTTP 端口 {self.http_port} 绑定失败: {e}") + await self.http_runner.cleanup() + return False + + # 2. 启动 WebSocket 站点 (独立端口) + try: + self.ws_site = web.TCPSite( + self.http_runner, self.ws_host, self.ws_port, + reuse_address=True, reuse_port=True + ) + await self.ws_site.start() + logger.info(f"✅ WebSocket 服务已绑定: {self.ws_host}:{self.ws_port}") + except OSError as e: + logger.error(f"❌ WebSocket 端口 {self.ws_port} 绑定失败: {e}") + logger.warning("💡 WS端口可能处于 TIME_WAIT,请等待30秒或更换 config 中的 websocket.port") + await self.http_runner.cleanup() # 回滚已启动的 HTTP + return False + + await self.save_network_config() + self.is_running = True + logger.info("🌐 网络服务启动完成") + return True + + except Exception as e: + logger.error(f"❌ 启动网络服务异常: {str(e)}", exc_info=True) + return False + + + async def check_service_health(self) -> Dict[str, Any]: + """检查服务健康状况""" + try: + import socket + + health_info = { + "is_running": self.is_running, + "http_port": self.http_port, + "websocket_port": self.ws_port, + "http_active": False, + "dependencies_available": self._check_dependencies(), + "error": None + } + + # 检查端口是否在监听 + if self.is_running: + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(2) + result = sock.connect_ex(('localhost', self.http_port)) + sock.close() + health_info["http_active"] = (result == 0) + except Exception as e: + health_info["error"] = f"端口检查失败: {str(e)}" + + return health_info + + except Exception as e: + return { + "is_running": False, + "error": f"健康检查失败: {str(e)}" + } + + def _check_dependencies(self) -> bool: + """检查必要的依赖包""" + try: + import aiohttp + import yaml + return True + except ImportError as e: + logger.error(f"❌ 缺少依赖包: {str(e)}") + return False + + async def save_network_config(self): + """保存网络配置""" + try: + config_path = Path("config") / "services" / "network_routes.yaml" + config_path.parent.mkdir(parents=True, exist_ok=True) + + config_data = { + "plugin_routes": self.get_plugin_routes(), + "last_updated": asyncio.get_event_loop().time(), + "http_port": self.http_port, + "websocket_port": self.ws_port + } + + import yaml + with open(config_path, 'w', encoding='utf-8') as f: + yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True, indent=2) + + logger.info(f"网络配置已保存: {config_path}") + + except Exception as e: + logger.error(f"保存网络配置时出错: {str(e)}") + + def _setup_default_routes(self): + """设置默认路由""" + # 健康检查端点 + self.http_app.router.add_get('/health', self._handle_health_check) + + # 插件API端点 + self.http_app.router.add_get('/api/plugins', self._handle_get_plugins) + self.http_app.router.add_get('/api/commands', self._handle_get_commands) + + # 数据接收端点 + self.http_app.router.add_post('/api/data', self._handle_data_receive) + + logger.debug("默认路由设置完成") + + async def register_plugin_route(self, plugin_name: str, route_path: str, + handler: Callable, methods: List[str] = ["GET"], + require_auth: bool = True): + """为插件注册HTTP路由 - 修复冻结路由器问题""" + try: + # 规范化路径 + if not route_path.startswith('/'): + route_path = '/' + route_path + + full_path = f"/{plugin_name}{route_path}" + + # 创建包装器处理权限验证 + async def wrapped_handler(request): + try: + # 权限验证 + if require_auth: + auth_result = await self._check_plugin_auth(plugin_name, request) + if not auth_result['allowed']: + return web.json_response( + {"error": "权限不足", "details": auth_result['reason']}, + status=403 + ) + + # 调用插件处理器 + return await handler(request) + + except Exception as e: + logger.error(f"插件路由处理出错 {full_path}: {str(e)}") + return web.json_response( + {"error": "内部服务器错误", "details": str(e)}, + status=500 + ) + + # 检查路由器是否已冻结 + if hasattr(self.http_app.router, '_frozen') and self.http_app.router._frozen: + logger.warning(f"路由器已冻结,无法注册新路由: {full_path}") + logger.info("💡 建议: 在启动网络服务前注册所有插件路由") + return + + # 注册路由 + for method in methods: + self.http_app.router.add_route(method.upper(), full_path, wrapped_handler) + + # 记录路由信息 + if plugin_name not in self.plugin_routes: + self.plugin_routes[plugin_name] = [] + + self.plugin_routes[plugin_name].append({ + 'path': full_path, + 'methods': methods, + 'require_auth': require_auth + }) + + logger.info(f"注册插件路由: {plugin_name} -> {full_path} [{','.join(methods)}]") + + except RuntimeError as e: + if "frozen router" in str(e): + logger.error(f"❌ 无法注册路由 {full_path}: 路由器已冻结") + logger.info("💡 解决方案: 在启动网络服务前注册插件路由") + else: + logger.error(f"注册插件路由时出错: {str(e)}", exc_info=True) + raise + except Exception as e: + logger.error(f"注册插件路由时出错: {str(e)}", exc_info=True) + raise + + async def register_plugin_websocket(self, plugin_name: str, ws_path: str, + handler: Callable, require_auth: bool = True): + """为插件注册WebSocket路由""" + try: + # 规范化路径 + if not ws_path.startswith('/'): + ws_path = '/' + ws_path + + full_path = f"/plugin/{plugin_name}/ws{ws_path}" + + async def websocket_handler(request): + try: + # 权限验证 + if require_auth: + auth_result = await self._check_plugin_auth(plugin_name, request) + if not auth_result['allowed']: + return web.json_response( + {"error": "WebSocket连接权限不足"}, + status=403 + ) + + # 建立WebSocket连接 + ws = web.WebSocketResponse() + await ws.prepare(request) + + # 记录连接 + connection_id = f"{plugin_name}_{id(ws)}" + if plugin_name not in self.ws_connections: + self.ws_connections[plugin_name] = [] + self.ws_connections[plugin_name].append(ws) + + logger.debug(f"WebSocket连接建立: {connection_id}") + + # 调用插件处理器 + await handler(ws, request) + + # 清理连接 + self.ws_connections[plugin_name].remove(ws) + + return ws + + except Exception as e: + logger.error(f"WebSocket处理出错 {full_path}: {str(e)}") + return web.json_response( + {"error": "WebSocket连接失败"}, + status=500 + ) + + # 注册WebSocket路由 + self.http_app.router.add_route('GET', full_path, websocket_handler) + + # 记录路由信息 + if plugin_name not in self.plugin_routes: + self.plugin_routes[plugin_name] = [] + + self.plugin_routes[plugin_name].append({ + 'path': full_path, + 'methods': ['WEBSOCKET'], + 'require_auth': require_auth + }) + + logger.info(f"注册插件WebSocket: {plugin_name} -> {full_path}") + + except Exception as e: + logger.error(f"注册插件WebSocket时出错: {str(e)}", exc_info=True) + raise + + async def _check_plugin_auth(self, plugin_name: str, request) -> Dict[str, Any]: + """检查插件权限""" + try: + # 获取权限服务 + permission_service = self.service_manager.get_service("permission") + if not permission_service: + return {"allowed": False, "reason": "权限服务不可用"} + + # 检查插件是否有网络访问权限 + if not permission_service.has_permission(plugin_name, "plugin.network.access"): + return {"allowed": False, "reason": "插件没有网络访问权限"} + + # 检查API密钥(如果配置了) + api_key = request.headers.get('X-API-Key') + if api_key: + # 验证API密钥逻辑 + valid_keys = self.config.get('api_keys', []) + if api_key not in valid_keys: + return {"allowed": False, "reason": "无效的API密钥"} + + return {"allowed": True, "reason": "权限验证通过"} + + except Exception as e: + logger.error(f"权限检查时出错: {str(e)}") + return {"allowed": False, "reason": "权限检查失败"} + + async def broadcast_to_websockets(self, plugin_name: str, message: Dict): + """向插件的所有WebSocket连接广播消息""" + try: + if plugin_name not in self.ws_connections: + return + + message_json = json.dumps(message, ensure_ascii=False) + disconnected = [] + + for ws in self.ws_connections[plugin_name]: + try: + if not ws.closed: + await ws.send_str(message_json) + else: + disconnected.append(ws) + except Exception as e: + logger.error(f"WebSocket广播消息失败: {str(e)}") + disconnected.append(ws) + + # 清理断开连接的WebSocket + for ws in disconnected: + self.ws_connections[plugin_name].remove(ws) + + logger.debug(f"WebSocket广播完成: {plugin_name} -> {len(self.ws_connections[plugin_name])} 个连接") + + except Exception as e: + logger.error(f"WebSocket广播时出错: {str(e)}", exc_info=True) + + # 默认路由处理器 + async def _handle_health_check(self, request): + """健康检查端点""" + return web.json_response({ + "status": "healthy", + "service": "internet", + "timestamp": asyncio.get_event_loop().time() + }) + + async def _handle_get_plugins(self, request): + """获取插件列表""" + try: + plugin_service = self.service_manager.get_service("plugin") + if not plugin_service: + return web.json_response({"error": "插件服务不可用"}, status=503) + + plugins_info = [] + for name, info in plugin_service.plugin_info.items(): + plugins_info.append({ + "name": name, + "version": info.version, + "description": info.description, + "enabled": info.enabled, + "loaded": info.loaded + }) + + return web.json_response({ + "plugins": plugins_info, + "count": len(plugins_info) + }) + + except Exception as e: + logger.error(f"获取插件列表时出错: {str(e)}") + return web.json_response({"error": "内部服务器错误"}, status=500) + + async def _handle_get_commands(self, request): + """获取命令列表""" + try: + command_service = self.service_manager.get_service("command") + if not command_service: + return web.json_response({"error": "命令服务不可用"}, status=503) + + command_list = command_service.get_command_list() + + return web.json_response({ + "commands": command_list, + "count": len(command_list) + }) + + except Exception as e: + logger.error(f"获取命令列表时出错: {str(e)}") + return web.json_response({"error": "内部服务器错误"}, status=500) + + async def _handle_data_receive(self, request): + """处理跨端数据传输""" + try: + data = await request.json() + + # 获取插件桥接服务 + plugin_bridge = self.service_manager.get_service("plugin_bridge") + if plugin_bridge: + # 广播数据到所有插件 + await plugin_bridge.broadcast_to_plugins( + "network.data.receive", + { + "source": request.remote, + "data": data, + "timestamp": asyncio.get_event_loop().time() + } + ) + + return web.json_response({ + "status": "success", + "message": "数据接收成功", + "timestamp": asyncio.get_event_loop().time() + }) + + except Exception as e: + logger.error(f"处理跨端数据时出错: {str(e)}") + return web.json_response({ + "error": "数据接收失败", + "details": str(e) + }, status=400) + + def get_plugin_routes(self, plugin_name: str = None) -> Dict: + """获取插件路由信息""" + if plugin_name: + return self.plugin_routes.get(plugin_name, []) + else: + return self.plugin_routes.copy() + + async def shutdown(self): + """关闭网络服务""" + try: + logger.info("关闭网络服务") + self.is_running = False + + # 关闭所有WebSocket连接 + for plugin_name, connections in self.ws_connections.items(): + for ws in connections: + if not ws.closed: + await ws.close() + self.ws_connections[plugin_name].clear() + + # 关闭HTTP服务器 + if self.http_runner: + await self.http_runner.cleanup() + + logger.debug("网络服务关闭完成") + + except Exception as e: + logger.error(f"关闭网络服务时出错: {str(e)}", exc_info=True) diff --git a/services/log_service.py b/services/log_service.py new file mode 100644 index 0000000..08dee5f --- /dev/null +++ b/services/log_service.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import logging.handlers +import os +import asyncio +from pathlib import Path +from typing import Dict, List, Callable +import json +from datetime import datetime + +logger = logging.getLogger(__name__) + +class LogService: + """日志服务""" + + def __init__(self, config: Dict): + self.config = config + self.log_consumers: List[Callable] = [] + self.log_buffer: List[Dict] = [] + self.buffer_size = 100 + self.log_dir = Path("logs") + self.is_initialized = False + self._in_emit = False + + # 文件数量限制 + self.max_log_files_per_folder = self.config['logging']['max_log_files'] # 每个文件夹文件数量上限 + + # 立即初始化日志系统 + self._setup_logging_sync() + + def _setup_logging_sync(self): + """同步设置日志系统 - 增强版""" + try: + # 创建日志目录 + self.log_dir.mkdir(exist_ok=True) + (self.log_dir / "debug").mkdir(exist_ok=True) + (self.log_dir / "runtime").mkdir(exist_ok=True) + + # 清理旧日志文件(在创建新文件之前) + self._cleanup_old_log_files() + + # 生成基于时间戳和会话ID的日志文件名 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + session_id = os.urandom(4).hex() # 生成8位随机会话ID + runtime_log_file = f"framework_{timestamp}_{session_id}.log" + debug_log_file = f"debug_{timestamp}_{session_id}.log" + + # 保存当前会话的日志文件名(用于后续引用) + self.current_session_logs = { + 'runtime': runtime_log_file, + 'debug': debug_log_file, + 'timestamp': timestamp, + 'session_id': session_id + } + + # 配置根日志记录器 + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + + # 清除现有处理器 + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # 创建过滤器实例 + shared_filter = self.SafeLogFilter(self) + + # 控制台处理器 + console_handler = logging.StreamHandler() + console_handler.setLevel(getattr(logging, self.config['logging']['level'], logging.INFO)) + + console_formatter = logging.Formatter( + '%(asctime)s [%(levelname)-8s] %(name)s: %(message)s', + datefmt='%H:%M:%S' + ) + console_handler.setFormatter(console_formatter) + console_handler.addFilter(shared_filter) + root_logger.addHandler(console_handler) + + # 文件处理器 + if self.config['logging'].get('enable_file_logging', True): + file_formatter = logging.Formatter( + '%(asctime)s [%(levelname)-8s] %(name)s:%(lineno)d - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + # 运行时文件处理器 + runtime_handler = logging.handlers.RotatingFileHandler( + self.log_dir / "runtime" / runtime_log_file, + maxBytes=self._parse_size(self.config['logging'].get('max_file_size', '10MB')), + backupCount=self.config['logging'].get('max_log_files', 3) + ) + runtime_handler.setLevel(getattr(logging, self.config['logging']['level'], logging.INFO)) + runtime_handler.setFormatter(file_formatter) + runtime_handler.addFilter(shared_filter) + root_logger.addHandler(runtime_handler) + + # Debug文件处理器 + if self.config['logging'].get('debug_level_file', True): + debug_handler = logging.handlers.RotatingFileHandler( + self.log_dir / "debug" / debug_log_file, + maxBytes=self._parse_size(self.config['logging'].get('max_file_size', '10MB')), + backupCount=self.config['logging'].get('max_log_files', 3) + ) + debug_handler.setLevel(logging.DEBUG) + debug_handler.setFormatter(file_formatter) + debug_handler.addFilter(shared_filter) + root_logger.addHandler(debug_handler) + + self.is_initialized = True + logger.info(f"✅ 日志系统初始化完成 - 会话ID: {session_id}") + logger.info(f"📝 运行时日志: logs/runtime/{runtime_log_file}") + logger.info(f"🐛 调试日志: logs/debug/{debug_log_file}") + + except Exception as e: + logger.error(f"日志系统设置失败: {e}") + # 回退到基础配置 + logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') + self.is_initialized = True + + def _cleanup_old_log_files(self): + """清理旧的日志文件,保持每个文件夹文件上限""" + try: + logger.debug("开始清理旧日志文件...") + + # 清理 runtime 文件夹 + runtime_dir = self.log_dir / "runtime" + if runtime_dir.exists(): + runtime_files = list(runtime_dir.glob("*.log")) + self._remove_old_files(runtime_files, "runtime") + + # 清理 debug 文件夹 + debug_dir = self.log_dir / "debug" + if debug_dir.exists(): + debug_files = list(debug_dir.glob("*.log")) + self._remove_old_files(debug_files, "debug") + + except Exception as e: + logger.error(f"清理旧日志文件时出错: {e}") + + def _remove_old_files(self, files: List[Path], folder_name: str): + """删除最旧的文件,直到文件数量不超过限制""" + try: + if len(files) <= self.max_log_files_per_folder: + logger.debug(f"{folder_name} 文件夹文件数量正常: {len(files)}/{self.max_log_files_per_folder}") + return + + # 按修改时间排序(最旧的在前) + files_sorted = sorted(files, key=lambda x: x.stat().st_mtime) + + # 计算需要删除的文件数量 + files_to_remove = len(files_sorted) - self.max_log_files_per_folder + + if files_to_remove > 0: + logger.info(f"清理 {folder_name} 文件夹: 删除 {files_to_remove} 个旧日志文件") + + for i in range(files_to_remove): + old_file = files_sorted[i] + try: + old_file.unlink() + logger.debug(f"删除旧日志文件: {old_file.name}") + except Exception as e: + logger.error(f"删除文件失败 {old_file}: {e}") + + logger.info(f"{folder_name} 文件夹清理完成: {self.max_log_files_per_folder} 个文件") + + except Exception as e: + logger.error(f"删除 {folder_name} 文件夹旧文件时出错: {e}") + + def cleanup_log_files(self): + """手动清理日志文件(可以定期调用)""" + try: + logger.info("开始手动清理日志文件...") + self._cleanup_old_log_files() + logger.info("日志文件清理完成") + except Exception as e: + logger.error(f"手动清理日志文件时出错: {e}") + + def get_log_file_counts(self) -> Dict[str, int]: + """获取当前日志文件数量统计""" + try: + runtime_count = len(list((self.log_dir / "runtime").glob("*.log"))) + debug_count = len(list((self.log_dir / "debug").glob("*.log"))) + + return { + "runtime": runtime_count, + "debug": debug_count, + "max_limit": self.max_log_files_per_folder + } + except Exception as e: + logger.error(f"获取日志文件统计时出错: {e}") + return {"runtime": 0, "debug": 0, "max_limit": self.max_log_files_per_folder} + + def get_current_session_info(self) -> Dict: + """获取当前会话的日志信息""" + return getattr(self, 'current_session_logs', {}) + + def _parse_size(self, size_str: str) -> int: + """解析文件大小字符串""" + try: + units = {'B': 1, 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3} + number = ''.join(filter(str.isdigit, size_str)) + unit = ''.join(filter(str.isalpha, size_str)).upper() + return int(number) * units.get(unit, 1) + except Exception: + return 10 * 1024 * 1024 + + def add_log_consumer(self, callback: Callable): + """添加日志消费者""" + if callback not in self.log_consumers: + self.log_consumers.append(callback) + logger.debug(f"添加日志消费者,总数: {len(self.log_consumers)}") + + def emit_log(self, log_record: Dict): + """发射日志到消费者""" + if self._in_emit: + return + + self._in_emit = True + + try: + # 格式化日志记录 + timestamp = log_record['timestamp'] + level = log_record['level'] + name = log_record['name'] + message = log_record['message'] + + # 转换时间戳 + if isinstance(timestamp, (int, float)): + timestamp_str = datetime.fromtimestamp(timestamp).strftime('%H:%M:%S') + else: + timestamp_str = str(timestamp) + + formatted_record = { + 'timestamp': timestamp, + 'timestamp_str': timestamp_str, + 'level': level, + 'name': name, + 'message': message, + 'module': log_record.get('module', ''), + 'line': log_record.get('line', 0), + 'formatted_message': f"{timestamp_str} [{level:8}] {name}: {message}", + 'simple_message': f"{timestamp_str} [{level:8}] {message}", + 'original_message': message + } + + # 添加到缓冲区 + self.log_buffer.append(formatted_record) + if len(self.log_buffer) > self.buffer_size: + self.log_buffer.pop(0) + + # 发送给消费者 + for consumer in self.log_consumers: + try: + # 检查TUI级别过滤 + if hasattr(consumer, '_tui_level_filter'): + tui_level = consumer._tui_level_filter + if self._should_display_for_tui(level, tui_level): + consumer(formatted_record) + else: + # 默认发送所有日志 + consumer(formatted_record) + except Exception as e: + logger.error(f"日志消费者处理出错: {e}") + + except Exception as e: + logger.error(f"发射日志时出错: {e}") + finally: + self._in_emit = False + + def _should_display_for_tui(self, log_level: str, tui_level: str) -> bool: + """检查日志是否应该显示在TUI中""" + level_priority = { + 'DEBUG': 10, 'INFO': 20, 'WARNING': 30, 'ERROR': 40, 'CRITICAL': 50 + } + log_priority = level_priority.get(log_level, 0) + tui_priority = level_priority.get(tui_level, 0) + return log_priority >= tui_priority + + def add_tui_log_consumer(self, callback: Callable, tui_level: str = "INFO"): + """专门为TUI添加日志消费者""" + callback._tui_level_filter = tui_level + if callback not in self.log_consumers: + self.log_consumers.append(callback) + logger.debug(f"添加TUI日志消费者,级别: {tui_level}") + + def get_recent_logs(self, count: int = 50) -> List[Dict]: + """获取最近的日志""" + return self.log_buffer[-count:] + + def shutdown(self): + """关闭日志服务""" + self.log_consumers.clear() + logging.shutdown() + logger.info("日志服务已关闭") + + class SafeLogFilter(logging.Filter): + """安全的日志过滤器""" + + def __init__(self, log_service): + super().__init__() + self.log_service = log_service + self._in_filter = False + + def filter(self, record): + """过滤日志记录""" + if self._in_filter: + return True + + self._in_filter = True + + try: + # 创建日志记录 + log_record = { + 'timestamp': record.created, + 'name': record.name, + 'level': record.levelname, + 'message': record.getMessage(), + 'module': record.module, + 'line': record.lineno + } + + # 发射日志记录 + self.log_service.emit_log(log_record) + + except Exception: + pass + finally: + self._in_filter = False + + return True diff --git a/services/permission_service.py b/services/permission_service.py new file mode 100644 index 0000000..9b6a8fd --- /dev/null +++ b/services/permission_service.py @@ -0,0 +1,1054 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import asyncio +import uuid +import json +from typing import Dict, List, Set, Optional +from pathlib import Path +import yaml + +logger = logging.getLogger(__name__) + +class PermissionService: + """权限服务""" + + def __init__(self, config: Dict, tui_service, core_bridge): + self.config = config + self.tui_service = tui_service + self.core_bridge = core_bridge + self.permission_rules: Dict = {} + self.granted_permissions: Dict[str, Set[str]] = {} + self.pending_requests: Dict[str, Dict] = {} + self.plugin_status: Dict[str, str] = {} # 插件状态跟踪 + self.is_running = False + + # 配置文件路径 + self.config_dir = Path("config") / "permissions" + self.granted_file = self.config_dir / "granted_permissions.json" + self.pending_file = self.config_dir / "pending_requests.json" + self.plugin_status_file = self.config_dir / "plugin_status.json" + + # 确保配置目录存在 + self.config_dir.mkdir(parents=True, exist_ok=True) + + logger.debug("PermissionService初始化开始") + + async def start(self): + """启动权限服务""" + try: + logger.info("启动权限服务") + + # 检查核心桥接服务是否可用 + if not self.core_bridge: + logger.error("核心桥接服务不可用") + return False + + # 加载权限规则 + await self._load_permission_rules() + + # 加载持久化数据 + await self._load_persisted_data() + + # 订阅权限相关事件 + self.core_bridge.subscribe("permission.request", self._handle_permission_request) + self.core_bridge.subscribe("permission.grant", self._handle_permission_grant) + self.core_bridge.subscribe("permission.deny", self._handle_permission_deny) + self.core_bridge.subscribe("permission.ignore", self._handle_permission_ignore) + + # 检查TUI服务连接状态 + if self.tui_service: + logger.debug("TUI服务已连接") + else: + logger.warning("TUI服务未连接,权限请求将显示在控制台") + + self.is_running = True + logger.info("权限服务启动完成") + return True + + except Exception as e: + logger.error(f"启动权限服务时出错: {str(e)}", exc_info=True) + return False + + async def _load_permission_rules(self): + """加载权限规则""" + try: + rules_path = Path("config") / "permissions" / "permission_rules.yaml" + if rules_path.exists(): + with open(rules_path, 'r', encoding='utf-8') as f: + self.permission_rules = yaml.safe_load(f) + logger.debug(f"加载权限规则: {len(self.permission_rules.get('rules', {}))} 条规则") + else: + logger.warning("权限规则文件不存在,使用默认规则") + self.permission_rules = { + "rules": {}, + "default_policy": "ask" + } + except Exception as e: + logger.error(f"加载权限规则时出错: {str(e)}", exc_info=True) + self.permission_rules = { + "rules": {}, + "default_policy": "ask" + } + + async def _load_persisted_data(self): + """加载持久化的权限数据""" + try: + # 如果配置文件不存在,创建空的配置文件 + if not self.granted_file.exists(): + await self._save_granted_permissions() + logger.info("创建空的已授予权限文件") + + if not self.pending_file.exists(): + await self._save_pending_requests() + logger.info("创建空的待处理请求文件") + + if not self.plugin_status_file.exists(): + await self._save_plugin_status() + logger.info("创建空的插件状态文件") + + # 加载已授予权限 + if self.granted_file.exists(): + with open(self.granted_file, 'r', encoding='utf-8') as f: + granted_data = json.load(f) + # 将列表转换回集合 + for plugin, permissions in granted_data.items(): + self.granted_permissions[plugin] = set(permissions) + logger.debug(f"加载已授予权限: {len(self.granted_permissions)} 个插件") + + # 加载待处理请求 + if self.pending_file.exists(): + with open(self.pending_file, 'r', encoding='utf-8') as f: + self.pending_requests = json.load(f) + logger.debug(f"加载待处理请求: {len(self.pending_requests)} 个") + + # 加载插件状态 + if self.plugin_status_file.exists(): + with open(self.plugin_status_file, 'r', encoding='utf-8') as f: + self.plugin_status = json.load(f) + logger.debug(f"加载插件状态: {len(self.plugin_status)} 个插件") + + logger.info("权限持久化数据加载完成") + + except Exception as e: + logger.error(f"加载持久化权限数据时出错: {str(e)}", exc_info=True) + + async def _save_granted_permissions(self): + """保存已授予权限到文件""" + try: + # 将集合转换为列表以便JSON序列化 + granted_data = {} + for plugin, permissions in self.granted_permissions.items(): + granted_data[plugin] = list(permissions) + + with open(self.granted_file, 'w', encoding='utf-8') as f: + json.dump(granted_data, f, ensure_ascii=False, indent=2) + + logger.debug(f"已授予权限已保存: {len(granted_data)} 个插件") + + except Exception as e: + logger.error(f"保存已授予权限时出错: {str(e)}", exc_info=True) + + async def _save_pending_requests(self): + """保存待处理请求到文件""" + try: + with open(self.pending_file, 'w', encoding='utf-8') as f: + json.dump(self.pending_requests, f, ensure_ascii=False, indent=2) + + logger.debug(f"待处理请求已保存: {len(self.pending_requests)} 个") + + except Exception as e: + logger.error(f"保存待处理请求时出错: {str(e)}", exc_info=True) + + async def _save_plugin_status(self): + """保存插件状态到文件""" + try: + with open(self.plugin_status_file, 'w', encoding='utf-8') as f: + json.dump(self.plugin_status, f, ensure_ascii=False, indent=2) + + logger.debug(f"插件状态已保存: {len(self.plugin_status)} 个插件") + + except Exception as e: + logger.error(f"保存插件状态时出错: {str(e)}", exc_info=True) + + async def _save_all_data(self): + """保存所有权限数据""" + try: + await asyncio.gather( + self._save_granted_permissions(), + self._save_pending_requests(), + self._save_plugin_status() + ) + logger.debug("所有权限数据已保存") + except Exception as e: + logger.error(f"保存权限数据时出错: {str(e)}", exc_info=True) + + def _handle_permission_request(self, message: Dict): + """处理权限请求""" + try: + # 从 message 的 data 字段中获取插件名称 + data = message.get('data', {}) + plugin_name = data.get('plugin_name') + logger.debug(f"传递的消息原文:{message}") + logger.debug(f"传递的插件名:{plugin_name}") + requested_permissions = data.get('permissions', []) + request_id = str(uuid.uuid4())[:8] # 简短的请求ID + + # 加强插件名称验证 + if not plugin_name or plugin_name == 'None' or plugin_name.strip() == '': + logger.error(f"无效的插件名称: {repr(plugin_name)}") + logger.debug(f"完整权限请求消息: {message}") + return + + # 验证插件名称格式 + if not self._is_valid_plugin_name(plugin_name): + logger.error(f"插件名称格式无效: {plugin_name}") + return + + # 验证权限列表 + if not requested_permissions or not isinstance(requested_permissions, list): + logger.warning(f"插件 {plugin_name} 请求的权限列表为空或格式错误") + requested_permissions = [] # 确保是列表 + + # 存储待处理请求 + self.pending_requests[request_id] = { + 'plugin_name': plugin_name, + 'permissions': requested_permissions, + 'timestamp': asyncio.get_event_loop().time() + } + + # 更新插件状态 + self.plugin_status[plugin_name] = "pending" + + logger.debug(f"处理权限请求: {plugin_name} -> {len(requested_permissions)} 个权限") + + # 保存数据 + asyncio.create_task(self._save_pending_requests()) + asyncio.create_task(self._save_plugin_status()) + + # 显示用户友好的权限请求界面 + asyncio.create_task(self._delayed_permission_ui(request_id, plugin_name, requested_permissions)) + + except Exception as e: + logger.error(f"处理权限请求时出错: {str(e)}", exc_info=True) + + def _is_valid_plugin_name(self, plugin_name: str) -> bool: + """验证插件名称是否有效""" + try: + if not plugin_name or not isinstance(plugin_name, str): + return False + + # 基本格式检查 + if plugin_name.strip() == '': + return False + + # 检查常见无效值 + invalid_values = ['None', 'null', 'undefined', ''] + if plugin_name in invalid_values: + return False + + # 检查长度限制 + if len(plugin_name) > 100: + return False + + # 检查字符有效性(允许字母、数字、下划线、点、连字符) + import re + if not re.match(r'^[a-zA-Z0-9_\.\-]+$', plugin_name): + return False + + return True + + except Exception as e: + logger.error(f"验证插件名称时出错: {str(e)}") + return False + + async def _delayed_permission_ui(self, request_id: str, plugin_name: str, permissions: List[str]): + """延迟显示权限请求UI,等待TUI就绪""" + try: + # 等待TUI服务就绪 + tui_ready = await self.wait_for_tui_ready() + + if tui_ready: + logger.debug(f"TUI已就绪,显示权限请求: {plugin_name}") + await self._show_permission_request_ui(request_id, plugin_name, permissions) + else: + # TUI未就绪,使用回退显示 + logger.warning(f"TUI未就绪,使用控制台显示权限请求: {plugin_name}") + message = f"🔐 插件 {plugin_name} 请求 {len(permissions)} 个权限 (请求ID: {request_id})" + self._fallback_permission_display(request_id, plugin_name, message) + + except Exception as e: + logger.error(f"延迟显示权限UI时出错: {str(e)}", exc_info=True) + + async def _show_permission_request_ui(self, request_id: str, plugin_name: str, permissions: List[str]): + """显示权限请求用户界面 - 优化显示""" + try: + # 创建更友好的权限描述 + permission_descriptions = { + "plugin.example.read": "📖 读取示例插件数据", + "plugin.example.write": "✏️ 写入示例插件数据", + "plugin.example.execute": "⚡ 执行示例插件操作", + "framework.event.subscribe": "📡 订阅框架事件", + "framework.command.execute": "⌨️ 执行框架命令" + } + + # 构建权限列表显示 + permission_list = [] + for perm in permissions: + desc = permission_descriptions.get(perm, f"🔧 {perm}") + permission_list.append(f" ✅ {desc}") + + permission_display = "\n".join(permission_list) if permission_list else " 无具体权限请求" + + # 显示权限请求界面 - 使用更简洁的格式 + messages = [ + f"🔐 **插件权限请求**", + f"", + f"**插件**: {plugin_name}", + f"**请求权限**:", + permission_display, + f"", + f"**操作选项**:", + f" 🟢 pmallow {request_id} - 同意所有权限", + f" 🟡 pmallow {request_id} read,write - 仅同意部分权限", + f" 🔴 pmdeny {request_id} - 拒绝所有权限", + f" ⏸️ ignore {request_id} - 暂时忽略", + f"", + f"**快捷命令**:", + f" pmallow all - 同意所有待处理请求", + f" pmdeny all - 拒绝所有待处理请求" + f" pmignore all - 忽略所有待处理请求" + ] + + # 逐行发送消息,确保每行都能正确显示 + if self.tui_service and hasattr(self.tui_service, 'show_message'): + for line in messages: + if line.strip(): # 忽略空行 + self.tui_service.show_message(line, "warning", persistent=True) + await asyncio.sleep(0.1) # 小延迟确保消息顺序 + else: + # TUI服务不可用,使用控制台输出 + self._fallback_permission_display(request_id, plugin_name, message) + + except Exception as e: + logger.error(f"显示权限请求界面时出错: {str(e)}", exc_info=True) + + def _fallback_permission_display(self, request_id: str, plugin_name: str, message: str): + """回退到控制台显示权限请求""" + try: + print("\n" + "="*60) + print(message) + print("="*60) + print("🐱 请输入命令处理权限请求:") + logger.info(f"权限请求已显示在控制台: {plugin_name} -> {request_id}") + except Exception as e: + logger.error(f"回退显示权限请求时出错: {str(e)}") + + def _handle_permission_grant(self, message: Dict): + """处理权限授予""" + try: + request_id = message.get('request_id') + granted_permissions = message.get('permissions', []) + + if request_id in self.pending_requests: + request = self.pending_requests[request_id] + plugin_name = request['plugin_name'] + + # 如果未指定具体权限,授予所有请求的权限 + if not granted_permissions: + granted_permissions = request['permissions'] + + # 授予权限 + asyncio.create_task(self.grant_permissions(plugin_name, granted_permissions)) + + # 更新插件状态 + self.plugin_status[plugin_name] = "granted" + + # 移除待处理请求 + del self.pending_requests[request_id] + + # 显示成功消息 + success_msg = f"✅ 已为插件 '{plugin_name}' 授予 {len(granted_permissions)} 个权限" + if self.tui_service and hasattr(self.tui_service, 'show_message'): + self.tui_service.show_message(success_msg, "success") + else: + print(f"🐱 {success_msg}") + + logger.info(f"权限授予完成: {plugin_name} -> {granted_permissions}") + + except Exception as e: + logger.error(f"处理权限授予时出错: {str(e)}", exc_info=True) + + def _handle_permission_deny(self, message: Dict): + """处理权限拒绝""" + try: + request_id = message.get('request_id') + + if request_id in self.pending_requests: + request = self.pending_requests[request_id] + plugin_name = request['plugin_name'] + + # 更新插件状态 + self.plugin_status[plugin_name] = "denied" + + # 移除待处理请求 + del self.pending_requests[request_id] + + # 保存数据 + asyncio.create_task(self._save_pending_requests()) + asyncio.create_task(self._save_plugin_status()) + + # 显示拒绝消息 + deny_msg = f"❌ 已拒绝插件 '{plugin_name}' 的权限请求" + if self.tui_service and hasattr(self.tui_service, 'show_message'): + self.tui_service.show_message(deny_msg, "error") + else: + print(f"🐱 {deny_msg}") + + logger.info(f"权限拒绝完成: {plugin_name}") + + except Exception as e: + logger.error(f"处理权限拒绝时出错: {str(e)}", exc_info=True) + + def _handle_permission_ignore(self, message: Dict): + """处理权限忽略""" + try: + request_id = message.get('request_id') + + if request_id in self.pending_requests: + request = self.pending_requests[request_id] + plugin_name = request['plugin_name'] + + # 更新插件状态 + self.plugin_status[plugin_name] = "ignored" + + # 移除待处理请求 + del self.pending_requests[request_id] + + # 保存数据 + asyncio.create_task(self._save_pending_requests()) + asyncio.create_task(self._save_plugin_status()) + + # 显示忽略消息 + ignore_msg = f"⏸️ 已暂时忽略插件 '{plugin_name}' 的权限请求" + if self.tui_service and hasattr(self.tui_service, 'show_message'): + self.tui_service.show_message(ignore_msg, "info") + else: + print(f"🐱 {ignore_msg}") + + logger.info(f"权限请求被忽略: {plugin_name}") + + except Exception as e: + logger.error(f"处理权限忽略时出错: {str(e)}", exc_info=True) + + async def grant_permissions(self, plugin_name: str, permissions: List[str]): + """授予权限""" + try: + if plugin_name not in self.granted_permissions: + self.granted_permissions[plugin_name] = set() + + for permission in permissions: + self.granted_permissions[plugin_name].add(permission) + + # 更新插件状态 + self.plugin_status[plugin_name] = "granted" + + logger.debug(f"授予权限: {plugin_name} -> {permissions}") + + # 保存数据 + await asyncio.gather( + self._save_granted_permissions(), + self._save_plugin_status() + ) + + # 通知插件权限已授予 + await self.core_bridge.publish("permission.granted", { + 'plugin_name': plugin_name, + 'permissions': permissions + }) + + except Exception as e: + logger.error(f"授予权限时出错: {str(e)}", exc_info=True) + raise + + def has_permission(self, plugin_name: str, permission: str) -> bool: + """检查是否具有权限""" + try: + # 检查显式授予的权限 + if plugin_name in self.granted_permissions: + if permission in self.granted_permissions[plugin_name]: + return True + + # 检查权限规则 + rule_key = f"{plugin_name}.{permission}" + if rule_key in self.permission_rules.get('rules', {}): + return self.permission_rules['rules'][rule_key] == 'allow' + + # 默认策略 + default_policy = self.permission_rules.get('default_policy', 'ask') + return default_policy == 'allow' + + except Exception as e: + logger.error(f"检查权限时出错: {str(e)}", exc_info=True) + return False + + async def request_permissions(self, plugin_name: str, permissions: List[str]) -> bool: + """请求权限 - 非阻塞版本""" + try: + logger.debug(f"权限请求: {plugin_name} -> {permissions}") + + # 首先检查是否已经有所有权限 + if all(self.has_permission(plugin_name, perm) for perm in permissions): + logger.debug(f"插件 {plugin_name} 已有所有请求的权限") + return True + + # 标记插件为等待权限状态 + self.plugin_status[plugin_name] = "pending" + + # 发布权限请求事件(非阻塞) + await self.core_bridge.publish("permission.request", { + 'plugin_name': plugin_name, + 'permissions': permissions + }) + + # 立即返回,不等待用户响应 + # 插件将在权限被授予后通过事件机制得到通知 + logger.debug(f"权限请求已发送,等待用户响应: {plugin_name}") + return True # 立即返回True,让插件继续加载 + + except Exception as e: + logger.error(f"请求权限时出错: {str(e)}", exc_info=True) + return True # 出错时也返回True,避免阻塞插件加载 + + def get_pending_requests(self) -> Dict[str, Dict]: + """获取待处理请求""" + return self.pending_requests.copy() + + async def process_permission_command(self, command: str, args: List[str]) -> str: + """处理权限相关命令""" + try: + if command == "pmallow": + if not args: + return "❌ 请指定请求ID,如: pmallow abc123" + + request_id = args[0] + + if request_id == "all": + # 同意所有待处理请求 + count = len(self.pending_requests) + for rid in list(self.pending_requests.keys()): + self._handle_permission_grant({'request_id': rid}) + return f"✅ 已同意所有 {count} 个待处理权限请求" + + # 检查特定权限 + specific_permissions = [] + if len(args) > 1: + specific_permissions = args[1].split(',') + + self._handle_permission_grant({ + 'request_id': request_id, + 'permissions': specific_permissions + }) + return f"✅ 已处理权限请求 {request_id}" + + elif command == "pmdeny": + if not args: + return "❌ 请指定请求ID,如: pmdeny abc123" + + request_id = args[0] + + if request_id == "all": + # 拒绝所有待处理请求 + count = len(self.pending_requests) + for rid in list(self.pending_requests.keys()): + await self._handle_permission_deny({'request_id': rid}) + return f"❌ 已拒绝所有 {count} 个待处理权限请求" + + await self._handle_permission_deny({'request_id': request_id}) + return f"❌ 已拒绝权限请求 {request_id}" + + elif command == "pmignore": + if not args: + return "❌ 请指定请求ID,如: pmignore abc123" + + request_id = args[0] + + if request_id == "all": + # 忽略所有待处理请求 + count = len(self.pending_requests) + for rid in list(self.pending_requests.keys()): + await self._handle_permission_ignore({'request_id': rid}) + return f"⏸️ 已忽略所有 {count} 个待处理权限请求" + + await self._handle_permission_ignore({'request_id': request_id}) + return f"⏸️ 已忽略权限请求 {request_id}" + + elif command == "permissions": + # 显示当前权限状态 + return await self._show_permission_status() + + elif command == "pm_plugin_status": + # 显示插件状态 + return await self._show_plugin_status() + + elif command == "pmpending" or command == "pmrequests": + # 查询待授权权限请求列表 + return await self._show_pending_requests() + + elif command == "pmhelp": + # 显示权限命令帮助 + return self._show_permission_help() + + elif command == "pmfix": + # 修复权限状态 + return await self._fix_permission_status() + + elif command == "pmclean": + # 清理权限数据 + return await self._clean_permission_data(args) + + elif command == "pmbackup": + # 备份权限数据 + return await self._backup_permission_data() + + elif command == "pmtest": + # 测试权限配置文件 + return await self._test_permission_config() + + else: + return f"❌ 未知权限命令: {command}\n💡 输入 'pmhelp' 查看可用命令" + + except Exception as e: + logger.error(f"处理权限命令时出错: {str(e)}", exc_info=True) + return f"❌ 处理命令时出错: {str(e)}" + + async def _test_permission_config(self) -> str: + """测试权限配置文件""" + try: + logger.debug("开始权限配置文件测试") + result = ["🔧 **权限配置文件测试**"] + result.append("=" * 50) + + # 测试配置文件路径 + result.append("📁 **配置文件路径**:") + result.append(f" 配置目录: {self.config_dir}") + result.append(f" 已授予权限: {self.granted_file}") + result.append(f" 待处理请求: {self.pending_file}") + result.append(f" 插件状态: {self.plugin_status_file}") + + # 测试文件存在性 + result.append("\n✅ **文件存在性检查**:") + config_files = [ + ("配置目录", self.config_dir, self.config_dir.exists()), + ("已授予权限", self.granted_file, self.granted_file.exists()), + ("待处理请求", self.pending_file, self.pending_file.exists()), + ("插件状态", self.plugin_status_file, self.plugin_status_file.exists()) + ] + + for name, path, exists in config_files: + status = "✅ 存在" if exists else "❌ 不存在" + result.append(f" {name}: {status}") + + # 测试写入权限 + result.append("\n✏️ **写入权限测试**:") + try: + test_data = {"test": "test_data", "timestamp": asyncio.get_event_loop().time()} + with open(self.config_dir / "test_write.json", 'w', encoding='utf-8') as f: + json.dump(test_data, f, ensure_ascii=False, indent=2) + + # 读取测试 + with open(self.config_dir / "test_write.json", 'r', encoding='utf-8') as f: + read_data = json.load(f) + + # 清理测试文件 + (self.config_dir / "test_write.json").unlink(missing_ok=True) + + result.append(" ✅ 读写测试: 成功") + except Exception as e: + result.append(f" ❌ 读写测试: 失败 - {str(e)}") + + # 显示当前数据状态 + result.append("\n📊 **当前数据状态**:") + result.append(f" 已授予权限: {len(self.granted_permissions)} 个插件") + result.append(f" 待处理请求: {len(self.pending_requests)} 个") + result.append(f" 插件状态: {len(self.plugin_status)} 个") + + return "\n".join(result) + + except Exception as e: + logger.error(f"测试权限配置时出错: {str(e)}", exc_info=True) + return f"❌ 测试权限配置时出错: {str(e)}" + + async def _clean_permission_data(self, args: List[str]) -> str: + """清理权限数据""" + try: + if not args: + return "❌ 请指定清理类型\n💡 可用选项: expired, all, plugin <插件名>" + + clean_type = args[0].lower() + result = [] + + if clean_type == "expired": + # 清理过期请求(超过24小时) + current_time = asyncio.get_event_loop().time() + expired_count = 0 + + for request_id, request in list(self.pending_requests.items()): + if current_time - request.get('timestamp', 0) > 86400: # 24小时 + plugin_name = request['plugin_name'] + del self.pending_requests[request_id] + expired_count += 1 + result.append(f"🗑️ 清理过期请求: {request_id} ({plugin_name})") + + if expired_count > 0: + await self._save_pending_requests() + result.insert(0, f"✅ 已清理 {expired_count} 个过期权限请求") + else: + result.append("✅ 没有发现过期权限请求") + + elif clean_type == "all": + # 清理所有数据 + pending_count = len(self.pending_requests) + granted_count = len(self.granted_permissions) + status_count = len(self.plugin_status) + + self.pending_requests.clear() + self.granted_permissions.clear() + self.plugin_status.clear() + + await self._save_all_data() + + result = [ + f"✅ 已清理所有权限数据:", + f" 🗑️ 待处理请求: {pending_count} 个", + f" 🗑️ 已授予权限: {granted_count} 个插件", + f" 🗑️ 插件状态: {status_count} 个" + ] + + elif clean_type == "plugin" and len(args) > 1: + # 清理特定插件的数据 + plugin_name = args[1] + cleaned_items = [] + + # 清理待处理请求 + for request_id, request in list(self.pending_requests.items()): + if request['plugin_name'] == plugin_name: + del self.pending_requests[request_id] + cleaned_items.append(f"待处理请求: {request_id}") + + # 清理已授予权限 + if plugin_name in self.granted_permissions: + del self.granted_permissions[plugin_name] + cleaned_items.append("已授予权限") + + # 清理插件状态 + if plugin_name in self.plugin_status: + del self.plugin_status[plugin_name] + cleaned_items.append("插件状态") + + if cleaned_items: + await self._save_all_data() + result = [f"✅ 已清理插件 '{plugin_name}' 的权限数据:"] + cleaned_items + else: + result = [f"ℹ️ 未找到插件 '{plugin_name}' 的权限数据"] + + else: + return "❌ 无效的清理类型\n💡 可用选项: expired, all, plugin <插件名>" + + return "\n".join(result) + + except Exception as e: + logger.error(f"清理权限数据时出错: {str(e)}", exc_info=True) + return f"❌ 清理权限数据时出错: {str(e)}" + + async def _show_permission_status(self) -> str: + """显示当前权限状态""" + try: + if not self.pending_requests and not self.granted_permissions: + return "📋 暂无权限请求和授予记录" + + result = ["📋 **权限状态**"] + + if self.pending_requests: + result.append("\n🟡 **待处理请求**:") + for rid, req in self.pending_requests.items(): + result.append(f" {rid}: {req['plugin_name']} -> {len(req['permissions'])} 个权限") + + if self.granted_permissions: + result.append("\n🟢 **已授予权限**:") + for plugin, perms in self.granted_permissions.items(): + result.append(f" {plugin}: {len(perms)} 个权限") + + if self.plugin_status: + result.append("\n🔵 **插件状态**:") + for plugin, status in self.plugin_status.items(): + status_icon = { + "granted": "✅", + "pending": "🟡", + "denied": "❌", + "ignored": "⏸️", + "error": "⚠️" + }.get(status, "🔵") + result.append(f" {status_icon} {plugin}: {status}") + + # 添加配置文件状态 + result.append("\n📁 **配置文件状态**:") + config_files = [ + ("已授予权限", self.granted_file), + ("待处理请求", self.pending_file), + ("插件状态", self.plugin_status_file) + ] + + for name, file_path in config_files: + if file_path.exists(): + result.append(f" ✅ {name}: 存在") + else: + result.append(f" ❌ {name}: 不存在") + + return "\n".join(result) + + except Exception as e: + logger.error(f"显示权限状态时出错: {str(e)}", exc_info=True) + return f"❌ 显示权限状态时出错: {str(e)}" + + async def _show_plugin_status(self) -> str: + """显示插件状态""" + try: + if not self.plugin_status: + return "📊 暂无插件状态信息" + + result = ["📊 **插件状态**"] + for plugin, status in self.plugin_status.items(): + if status == "granted": + result.append(f" ✅ {plugin}: 权限已授予") + elif status == "pending": + result.append(f" 🟡 {plugin}: 等待权限授予") + elif status == "denied": + result.append(f" ❌ {plugin}: 权限被拒绝") + elif status == "ignored": + result.append(f" ⏸️ {plugin}: 权限请求被忽略") + elif status == "error": + result.append(f" ⚠️ {plugin}: 权限错误") + else: + result.append(f" 🔵 {plugin}: {status}") + + return "\n".join(result) + + except Exception as e: + logger.error(f"显示插件状态时出错: {str(e)}", exc_info=True) + return f"❌ 显示插件状态时出错: {str(e)}" + + async def _fix_permission_status(self) -> str: + """修复权限状态不一致问题""" + try: + fixes_applied = [] + + # 检查插件状态与待处理请求的一致性 + for plugin, status in list(self.plugin_status.items()): + # 如果插件状态是pending但没有对应的待处理请求 + if status == "pending": + has_pending_request = False + for request in self.pending_requests.values(): + if request['plugin_name'] == plugin: + has_pending_request = True + break + + if not has_pending_request: + # 修复:将状态改为error + self.plugin_status[plugin] = "error" + fixes_applied.append(f"🟡 {plugin}: pending → error (无权限请求)") + + # 清理过期的待处理请求 + current_time = asyncio.get_event_loop().time() + expired_requests = [] + for request_id, request in list(self.pending_requests.items()): + # 假设请求超过1小时为过期 + if current_time - request.get('timestamp', 0) > 3600: + expired_requests.append(request_id) + + for request_id in expired_requests: + plugin_name = self.pending_requests[request_id]['plugin_name'] + del self.pending_requests[request_id] + fixes_applied.append(f"🗑️ 清理过期请求: {request_id} ({plugin_name})") + + # 保存修复后的数据 + if fixes_applied: + await self._save_all_data() + result = ["🔧 **权限状态修复完成**"] + result.extend(fixes_applied) + result.append(f"\n✅ 共应用 {len(fixes_applied)} 个修复") + else: + result = ["✅ **权限状态正常**", "未发现需要修复的问题"] + + return "\n".join(result) + + except Exception as e: + logger.error(f"修复权限状态时出错: {str(e)}", exc_info=True) + return f"❌ 修复权限状态时出错: {str(e)}" + + async def _backup_permission_data(self) -> str: + """备份权限数据""" + try: + backup_dir = self.config_dir / "backups" + backup_dir.mkdir(exist_ok=True) + + import datetime + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + + # 备份文件路径 + granted_backup = backup_dir / f"granted_permissions_{timestamp}.json" + pending_backup = backup_dir / f"pending_requests_{timestamp}.json" + status_backup = backup_dir / f"plugin_status_{timestamp}.json" + + # 复制文件 + import shutil + if self.granted_file.exists(): + shutil.copy2(self.granted_file, granted_backup) + if self.pending_file.exists(): + shutil.copy2(self.pending_file, pending_backup) + if self.plugin_status_file.exists(): + shutil.copy2(self.plugin_status_file, status_backup) + + return f"✅ 权限数据备份完成\n📁 备份位置: {backup_dir}\n⏰ 时间戳: {timestamp}" + + except Exception as e: + logger.error(f"备份权限数据时出错: {str(e)}", exc_info=True) + return f"❌ 备份权限数据时出错: {str(e)}" + + async def _show_pending_requests(self) -> str: + """显示待授权权限请求列表 - 更新为pm前缀""" + try: + if not self.pending_requests: + return "📭 暂无待处理的权限请求" + + result = ["🟡 **待授权权限请求列表**"] + result.append("=" * 50) + + for request_id, request in self.pending_requests.items(): + plugin_name = request['plugin_name'] + permissions = request['permissions'] + + # 创建友好的权限描述 + permission_descriptions = { + "plugin.example.read": "📖 读取示例插件数据", + "plugin.example.write": "✏️ 写入示例插件数据", + "plugin.example.execute": "⚡ 执行示例插件操作", + "framework.event.subscribe": "📡 订阅框架事件", + "framework.command.execute": "⌨️ 执行框架命令" + } + + # 构建权限列表 + permission_list = [] + for perm in permissions: + desc = permission_descriptions.get(perm, f"🔧 {perm}") + permission_list.append(f" • {desc}") + + permission_display = "\n".join(permission_list) + + # 添加请求信息 + result.append(f"\n📦 **插件**: {plugin_name}") + result.append(f"🆔 **请求ID**: {request_id}") + result.append(f"🔐 **请求权限** ({len(permissions)} 个):") + result.append(permission_display) + + # 添加交互指令 - 更新为pm前缀 + result.append(f"\n💡 **交互指令**:") + result.append(f" 🟢 同意所有权限: pmallow {request_id}") + result.append(f" 🟡 同意部分权限: pmallow {request_id} read,write") + result.append(f" 🔴 拒绝所有权限: pmdeny {request_id}") + result.append(f" ⏸️ 暂时忽略: pmignore {request_id}") + + result.append("-" * 50) + + # 添加快捷指令 - 更新为pm前缀 + result.append("\n🚀 **快捷指令**:") + result.append(" 🟢 同意所有请求: pmallow pmall") + result.append(" 🔴 拒绝所有请求: pmdeny all") + result.append(" ⏸️ 忽略所有请求: pmignore all") + result.append(" 📋 查看权限状态: permissions") + result.append(" 📊 查看插件状态: pm_plugin_status") + result.append(" ❓ 查看帮助: pmhelp") + + return "\n".join(result) + + except Exception as e: + logger.error(f"显示待处理请求时出错: {str(e)}", exc_info=True) + return f"❌ 显示待处理请求时出错: {str(e)}" + + def _show_permission_help(self) -> str: + """显示权限命令帮助 - 更新为pm前缀""" + help_text = """ + 🔐 **权限管理命令帮助 (pm前缀)** + + 📋 **查询命令**: + permissions - 查看权限状态 + pmpending 或 pmrequests - 查看待授权请求列表 + pm_plugin_status - 查看插件权限状态 + + 🛠️ **操作命令**: + pmallow <请求ID> - 同意指定请求的所有权限 + pmallow <请求ID> <权限列表> - 同意指定请求的部分权限 + pmdeny <请求ID> - 拒绝指定请求的所有权限 + 。pmignore <请求ID> - 暂时忽略指定请求 + + 🚀 **快捷命令**: + pmallow pmall - 同意所有待处理请求 + pmdeny all - 拒绝所有待处理请求 + pmignore all - 忽略所有待处理请求 + + 🔧 **维护命令**: + pmfix - 修复权限状态不一致问题 + pmtest - 测试权限配置文件 + + 📖 **示例**: + pmallow abc123 - 同意请求ID为abc123的所有权限 + pmallow abc123 read,write - 只同意abc123的读取和写入权限 + pmdeny abc123 - 拒绝abc123的所有权限 + pmignore abc123 - 暂时忽略abc123的请求 + pmpending - 查看所有待处理的权限请求 + + 💡 **提示**: + • 权限请求ID是自动生成的8位字符串 + • 使用 pmpending 命令查看所有待处理请求及其ID + • 插件在获得权限前可能以受限模式运行 + • 权限管理命令都以 `pm` 为前缀,避免与其他命令冲突 + """ + return help_text.strip() + + def is_tui_ready(self) -> bool: + """检查TUI服务是否就绪""" + try: + return (self.tui_service is not None and + hasattr(self.tui_service, 'show_message') and + hasattr(self.tui_service, 'tui_app') and + self.tui_service.tui_app is not None) + except Exception as e: + logger.debug(f"检查TUI状态时出错: {e}") + return False + + async def wait_for_tui_ready(self, timeout: float = 10.0) -> bool: + """等待TUI服务就绪""" + try: + start_time = asyncio.get_event_loop().time() + while asyncio.get_event_loop().time() - start_time < timeout: + if self.is_tui_ready(): + logger.debug("TUI服务已就绪") + return True + await asyncio.sleep(0.5) + + logger.warning(f"等待TUI服务就绪超时 ({timeout}秒)") + return False + except Exception as e: + logger.error(f"等待TUI就绪时出错: {e}") + return False + + def shutdown(self): + """关闭权限服务""" + try: + logger.info("关闭权限服务") + self.is_running = False + self.pending_requests.clear() + logger.debug("权限服务关闭完成") + except Exception as e: + logger.error(f"关闭权限服务时出错: {str(e)}", exc_info=True) diff --git a/services/plugin_service.py b/services/plugin_service.py new file mode 100644 index 0000000..e5a41ca --- /dev/null +++ b/services/plugin_service.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import asyncio +import importlib.util +import sys +import inspect +from pathlib import Path +from typing import Dict, List, Any, Optional, Callable +from dataclasses import dataclass +import yaml +import traceback +from fmfuncs.plugin_command_decorator import plugin_command, command + +logger = logging.getLogger(__name__) + +@dataclass +class PluginInfo: + """插件信息数据类""" + name: str + version: str + description: str + author: str + enabled: bool + loaded: bool + error_count: int + permissions: List[str] + plugin_path: Path + commands: Dict[str, Dict] = None # 新增命令信息 + +class PluginService: + """插件服务 - 管理插件的加载、卸载和运行""" + + def __init__(self, config: Dict, permission_service, bridge_service, service_manager): + self.config = config + self.permission_service = permission_service + self.bridge_service = bridge_service + self.service_manager = service_manager # 新增服务管理器 + self.bridge_service.service_manager = self.service_manager + self.plugins: Dict[str, Any] = {} + self.plugin_info: Dict[str, PluginInfo] = {} + self.plugins_dir = Path("plugins") + self.is_running = False + logger.debug("PluginService初始化开始") + + + async def start(self): + """启动插件服务""" + try: + logger.info("启动插件服务") + + # 创建插件目录 + self.plugins_dir.mkdir(exist_ok=True) + + # 自动加载插件 + if self.config['plugins']['auto_load']: + await self.load_all_plugins() + + await self.save_command_config() + + self.is_running = True + logger.info("插件服务启动完成") + + except Exception as e: + logger.error(f"启动插件服务时出错: {str(e)}", exc_info=True) + raise + + async def load_all_plugins(self): + """加载所有插件""" + try: + logger.debug("开始加载所有插件") + + if not self.plugins_dir.exists(): + logger.warning("插件目录不存在,跳过加载") + return + + loaded_count = 0 + error_count = 0 + + # 遍历插件目录 + for plugin_dir in self.plugins_dir.iterdir(): + if plugin_dir.is_dir(): + try: + success = await self.load_plugin(plugin_dir.name) + if success: + loaded_count += 1 + else: + error_count += 1 + except Exception as e: + logger.error(f"加载插件 {plugin_dir.name} 时出错: {str(e)}", exc_info=True) + error_count += 1 + + logger.info(f"插件加载完成: 成功 {loaded_count}, 失败 {error_count}") + + except Exception as e: + logger.error(f"加载所有插件时出错: {str(e)}", exc_info=True) + raise + + async def load_plugin(self, plugin_name: str) -> bool: + """加载单个插件 - 支持异步权限处理""" + try: + logger.debug(f"开始加载插件: {plugin_name}") + + plugin_path = self.plugins_dir / plugin_name + + if not plugin_path.exists(): + logger.error(f"插件目录不存在: {plugin_path}") + return False + + # 检查插件配置文件 + config_file = plugin_path / "config.yaml" + if not config_file.exists(): + logger.error(f"插件配置文件不存在: {config_file}") + return False + + # 加载插件配置 + with open(config_file, 'r', encoding='utf-8') as f: + plugin_config = yaml.safe_load(f) + + # 检查权限文件 + permission_file = plugin_path / "permissions.yaml" + if not permission_file.exists(): + logger.error(f"插件权限文件不存在: {permission_file}") + return False + + # 加载权限配置 + with open(permission_file, 'r', encoding='utf-8') as f: + permission_config = yaml.safe_load(f) + + # 验证插件信息 + required_fields = ['name', 'version', 'description', 'author'] + for field in required_fields: + if field not in plugin_config: + logger.error(f"插件配置缺少必要字段: {field}") + return False + + # 检查主模块 + main_module = plugin_path / "__init__.py" + if not main_module.exists(): + logger.error(f"插件主模块不存在: {main_module}") + return False + + # 动态加载插件模块 + module_name = f"plugins.{plugin_name}" + spec = importlib.util.spec_from_file_location(module_name, main_module) + if not spec: + logger.error(f"无法创建模块规范: {module_name}") + return False + + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + + try: + spec.loader.exec_module(module) + logger.debug(f"插件模块加载成功: {module_name}") + except Exception as e: + logger.error(f"执行插件模块时出错: {str(e)}", exc_info=True) + return False + + # 获取插件类实例 + if not hasattr(module, 'Plugin'): + logger.error(f"插件类 'Plugin' 不存在: {module_name}") + return False + + # 权限申请和验证 - 非阻塞版本 + permissions = permission_config.get('permissions', []) + if permissions: + # 非阻塞权限请求,立即返回True让插件继续加载 + permission_result = await self.permission_service.request_permissions(plugin_name, permissions) + if not permission_result: + logger.warning(f"插件权限申请失败: {plugin_name}") + # 即使权限申请失败,也允许插件以受限模式运行 + logger.info(f"插件 {plugin_name} 将以受限模式运行") + + # 实例化插件 + try: + plugin_instance = module.Plugin( + plugin_name=plugin_name, + config=plugin_config, + bridge=self.bridge_service + ) + + # 初始化插件 + if hasattr(plugin_instance, 'initialize'): + if asyncio.iscoroutinefunction(plugin_instance.initialize): + await plugin_instance.initialize() + else: + plugin_instance.initialize() + + # 扫描并注册插件命令 + plugin_commands = await self._scan_and_register_commands(plugin_name, plugin_instance, plugin_config) + + # 注册插件 + self.plugins[plugin_name] = plugin_instance + + # 保存插件信息 + self.plugin_info[plugin_name] = PluginInfo( + name=plugin_config['name'], + version=plugin_config['version'], + description=plugin_config['description'], + author=plugin_config['author'], + enabled=True, + loaded=True, + error_count=0, + permissions=permissions, + plugin_path=plugin_path, + commands=plugin_commands + ) + + logger.info(f"插件加载成功: {plugin_name} v{plugin_config['version']}, 注册了 {len(plugin_commands)} 个命令") + return True + + except Exception as e: + logger.error(f"实例化插件时出错: {str(e)}", exc_info=True) + return False + + except Exception as e: + logger.error(f"加载插件 {plugin_name} 时出错: {str(e)}", exc_info=True) + return False + + async def _scan_and_register_commands(self, plugin_name: str, plugin_instance: Any, plugin_config: Dict) -> Dict[str, Dict]: + """扫描并注册插件命令 - 修正版本""" + try: + logger.debug(f"扫描插件命令: {plugin_name}") + + command_service = self.service_manager.get_service("command") + if not command_service: + logger.error("命令服务不可用,无法注册插件命令") + return {} + + # 扫描插件中的命令方法 + command_methods = {} + for name, method in inspect.getmembers(plugin_instance, predicate=inspect.ismethod): + # 检查方法是否有命令装饰器或符合命名约定 + if (hasattr(method, '_is_plugin_command') or + name.startswith('cmd_') or + name.startswith('command_')): + + command_name = self._get_command_name(name, method, plugin_config) + command_description = self._get_command_description(name, method, plugin_config) + command_permissions = self._get_command_permissions(name, method, plugin_config) + + # 修正:使用正确的source格式 + command_service.register_command( + name=command_name, + handler=method, + description=command_description, + permissions=command_permissions, + source=f"plugin.{plugin_name}" # 使用 plugin.插件名 格式 + ) + + command_methods[command_name] = { + 'method_name': name, + 'description': command_description, + 'permissions': command_permissions + } + + logger.debug(f"注册插件命令: {command_name} -> {name}") + + return command_methods + + except Exception as e: + logger.error(f"扫描插件命令时出错: {str(e)}", exc_info=True) + return {} + + def _get_command_name(self, method_name: str, method: Callable, plugin_config: Dict) -> str: + """获取命令名称""" + try: + # 如果方法有装饰器指定的名称 + if hasattr(method, '_command_name'): + return getattr(method, '_command_name') + + # 从方法名提取命令名 + if method_name.startswith('cmd_'): + return method_name[4:] + elif method_name.startswith('command_'): + return method_name[8:] + else: + return method_name + + except Exception as e: + logger.error(f"获取命令名称时出错: {str(e)}") + return method_name + + def _get_command_description(self, method_name: str, method: Callable, plugin_config: Dict) -> str: + """获取命令描述""" + try: + # 如果方法有装饰器指定的描述 + if hasattr(method, '_command_description'): + return getattr(method, '_command_description') + + # 使用方法的文档字符串 + if method.__doc__: + # 提取第一行作为描述 + doc_lines = method.__doc__.strip().split('\n') + return doc_lines[0].strip() + + # 默认描述 + return f"插件命令: {method_name}" + + except Exception as e: + logger.error(f"获取命令描述时出错: {str(e)}") + return f"插件命令: {method_name}" + + def _get_command_permissions(self, method_name: str, method: Callable, plugin_config: Dict) -> List[str]: + """获取命令权限""" + try: + # 如果方法有装饰器指定的权限 + if hasattr(method, '_command_permissions'): + return getattr(method, '_command_permissions') + + # 从插件配置中获取默认权限 + default_permissions = plugin_config.get('default_command_permissions', []) + return default_permissions.copy() + + except Exception as e: + logger.error(f"获取命令权限时出错: {str(e)}") + return [] + + async def unload_plugin(self, plugin_name: str) -> bool: + """卸载插件""" + try: + logger.debug(f"开始卸载插件: {plugin_name}") + + if plugin_name not in self.plugins: + logger.warning(f"插件未加载: {plugin_name}") + return False + + plugin_instance = self.plugins[plugin_name] + plugin_info = self.plugin_info[plugin_name] + + # 注销插件命令 + await self._unregister_plugin_commands(plugin_name) + + # 调用插件的清理方法 + try: + if hasattr(plugin_instance, 'shutdown'): + if asyncio.iscoroutinefunction(plugin_instance.shutdown): + await plugin_instance.shutdown() + else: + plugin_instance.shutdown() + except Exception as e: + logger.error(f"插件清理时出错 {plugin_name}: {str(e)}", exc_info=True) + + # 从模块缓存中移除 + module_name = f"plugins.{plugin_name}" + if module_name in sys.modules: + del sys.modules[module_name] + + # 移除插件实例和信息 + del self.plugins[plugin_name] + plugin_info.loaded = False + plugin_info.enabled = False + + logger.info(f"插件卸载成功: {plugin_name}") + return True + + except Exception as e: + logger.error(f"卸载插件 {plugin_name} 时出错: {str(e)}", exc_info=True) + return False + + async def _unregister_plugin_commands(self, plugin_name: str): + """注销插件命令""" + try: + command_service = self.service_manager.get_service("command") + if not command_service: + return + + # 从命令服务中移除该插件的所有命令 + commands_to_remove = [] + for cmd_name, cmd_info in command_service.commands.items(): + if cmd_info.source.startswith(f"plugin.{plugin_name}"): + commands_to_remove.append(cmd_name) + + for cmd_name in commands_to_remove: + del command_service.commands[cmd_name] + logger.debug(f"注销插件命令: {cmd_name}") + + logger.info(f"已注销插件 {plugin_name} 的 {len(commands_to_remove)} 个命令") + + except Exception as e: + logger.error(f"注销插件命令时出错: {str(e)}", exc_info=True) + + async def save_command_config(self): + """保存命令配置到文件""" + try: + command_service = self.service_manager.get_service("command") + if not command_service: + logger.error("命令服务不可用") + return False + + command_list = command_service.get_command_list() + config_path = Path("config") / "plugins" / "commands.yaml" + + # 确保目录存在 + config_path.parent.mkdir(parents=True, exist_ok=True) + + config_data = { + "commands": {}, + "plugin_commands": {}, + "last_updated": asyncio.get_event_loop().time(), + "total_commands": len(command_list) + } + + # 按来源分组命令 + for cmd in command_list: + cmd_info = { + "description": cmd['description'], + "permissions": cmd['permissions'], + "source": cmd['source'] + } + + config_data["commands"][cmd['name']] = cmd_info + + # 按插件分组 + if cmd['source'].startswith("plugin."): + plugin_name = cmd['source'].split('.', 1)[1] + if plugin_name not in config_data["plugin_commands"]: + config_data["plugin_commands"][plugin_name] = {} + config_data["plugin_commands"][plugin_name][cmd['name']] = cmd_info + + with open(config_path, 'w', encoding='utf-8') as f: + yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True, indent=2) + + logger.info(f"命令配置已保存: {config_path}, 共 {len(command_list)} 个命令") + return True + + except Exception as e: + logger.error(f"保存命令配置时出错: {str(e)}", exc_info=True) + return False + + async def register_delayed_routes(self, internet_service): + """注册延迟的路由(在网络服务启动后)""" + try: + if not internet_service: + logger.warning("网络服务不可用,跳过延迟路由注册") + return + + for plugin_name, plugin_instance in self.plugins.items(): + try: + # 检查插件是否有延迟注册方法 + if hasattr(plugin_instance, 'register_delayed_routes'): + await plugin_instance.register_delayed_routes(internet_service) + logger.info(f"延迟注册插件路由: {plugin_name}") + else: + # 如果插件没有延迟注册方法,尝试重新初始化网络功能 + await self._reinitialize_plugin_network(plugin_instance, internet_service) + + except Exception as e: + logger.error(f"延迟注册插件 {plugin_name} 路由时出错: {str(e)}") + + except Exception as e: + logger.error(f"注册延迟路由时出错: {str(e)}") + + async def _reinitialize_plugin_network(self, plugin_instance, internet_service): + """重新初始化插件的网络功能""" + try: + plugin_name = plugin_instance.plugin_name + + # 检查插件是否有网络桥接 + if hasattr(plugin_instance, 'network_bridge'): + # 重新创建网络桥接 + plugin_instance.network_bridge = PluginNetworkBridge( + plugin_name, internet_service, plugin_instance.bridge + ) + + # 重新设置网络路由 + if hasattr(plugin_instance, '_setup_network_routes'): + await plugin_instance._setup_network_routes() + + logger.info(f"重新初始化插件网络功能: {plugin_name}") + + except Exception as e: + logger.error(f"重新初始化插件网络功能时出错: {str(e)}") + \ No newline at end of file diff --git a/services/shutdown_service.py b/services/shutdown_service.py new file mode 100644 index 0000000..351be00 --- /dev/null +++ b/services/shutdown_service.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import asyncio +import signal +import sys +from typing import List, Callable +from enum import Enum + +logger = logging.getLogger(__name__) + +class ShutdownPriority(Enum): + """关闭优先级枚举""" + HIGHEST = 0 + HIGH = 1 + NORMAL = 2 + LOW = 3 + LOWEST = 4 + +class ShutdownService: + """关闭服务 - 管理框架的优雅关闭""" + + def __init__(self, service_manager): + self.service_manager = service_manager + self.shutdown_handlers: List[Callable] = [] + self.is_shutting_down = False + self.shutdown_timeout = 30 # 秒 + logger.debug("ShutdownService初始化开始") + + # 注册信号处理 + self._register_signal_handlers() + + def _register_signal_handlers(self): + """注册信号处理""" + try: + signal.signal(signal.SIGINT, self._signal_handler) + signal.signal(signal.SIGTERM, self._signal_handler) + logger.debug("信号处理器注册完成") + except Exception as e: + logger.error(f"注册信号处理器时出错: {str(e)}", exc_info=True) + + def _signal_handler(self, signum, frame): + """信号处理函数""" + try: + signal_name = signal.Signals(signum).name + logger.info(f"接收到信号: {signal_name}") + asyncio.create_task(self.initiate_shutdown()) + except Exception as e: + logger.error(f"处理信号时出错: {str(e)}", exc_info=True) + sys.exit(1) + + def register_shutdown_handler(self, handler: Callable, priority: ShutdownPriority = ShutdownPriority.NORMAL): + """注册关闭处理器""" + try: + self.shutdown_handlers.append((priority.value, handler)) + # 按优先级排序 + self.shutdown_handlers.sort(key=lambda x: x[0]) + logger.debug(f"注册关闭处理器,优先级: {priority.name}, 当前总数: {len(self.shutdown_handlers)}") + except Exception as e: + logger.error(f"注册关闭处理器时出错: {str(e)}", exc_info=True) + + async def initiate_shutdown(self, reason: str = "正常关闭"): + """发起关闭流程""" + try: + if self.is_shutting_down: + logger.warning("关闭流程已在进行中") + return + + self.is_shutting_down = True + logger.info(f"开始框架关闭流程 - 原因: {reason}") + + # 执行关闭处理器 + await self._execute_shutdown_handlers() + + # 关闭服务管理器 + self.service_manager.shutdown_all() + + logger.info("框架关闭完成") + + # 退出程序 + sys.exit(0) + + except Exception as e: + logger.error(f"关闭流程出错: {str(e)}", exc_info=True) + sys.exit(1) + + async def _execute_shutdown_handlers(self): + """执行关闭处理器""" + try: + logger.debug(f"开始执行 {len(self.shutdown_handlers)} 个关闭处理器") + + for priority, handler in self.shutdown_handlers: + try: + handler_name = handler.__name__ if hasattr(handler, '__name__') else str(handler) + logger.debug(f"执行关闭处理器: {handler_name} (优先级: {priority})") + + if asyncio.iscoroutinefunction(handler): + await asyncio.wait_for(handler(), timeout=self.shutdown_timeout) + else: + # 在事件循环中运行同步函数 + await asyncio.get_event_loop().run_in_executor(None, handler) + + logger.debug(f"关闭处理器完成: {handler_name}") + + except asyncio.TimeoutError: + logger.error(f"关闭处理器超时: {handler_name}") + except Exception as e: + logger.error(f"关闭处理器出错 {handler_name}: {str(e)}", exc_info=True) + + logger.debug("所有关闭处理器执行完成") + + except Exception as e: + logger.error(f"执行关闭处理器时出错: {str(e)}", exc_info=True) + + def emergency_shutdown(self): + """紧急关闭""" + try: + logger.critical("执行紧急关闭") + sys.exit(1) + except Exception as e: + logger.critical(f"紧急关闭时出错: {str(e)}") + os._exit(1) + + def shutdown(self): + """关闭关闭服务""" + try: + logger.info("关闭ShutdownService") + self.shutdown_handlers.clear() + logger.debug("ShutdownService关闭完成") + except Exception as e: + logger.error(f"关闭ShutdownService时出错: {str(e)}", exc_info=True) diff --git a/services/tui_service.py b/services/tui_service.py new file mode 100644 index 0000000..b755c70 --- /dev/null +++ b/services/tui_service.py @@ -0,0 +1,806 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +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.reactive import reactive +from typing import List, Dict +import asyncio +from datetime import datetime + +logger = logging.getLogger(__name__) + +class SystemExitGraceful(Exception): + """优雅的系统退出异常""" + pass + +class LogDisplay(Static): + """日志显示组件 - 直接捕获所有日志输出""" + + def __init__(self): + super().__init__("日志显示区域 - 等待日志输入...") + self.log_lines: List[str] = [] + self.max_lines = 500 + self.auto_scroll_enabled = True # 启用自动滚动 + + # 保存原始的logging处理器和格式器 + self.original_handlers = [] + self.original_formatters = {} + + logger.debug("LogDisplay初始化完成") + + def start_capture(self): + """开始捕获所有日志输出""" + try: + # 获取根日志记录器 + root_logger = logging.getLogger() + + # 保存原始处理器和它们的格式器 + self.original_handlers = root_logger.handlers.copy() + for handler in self.original_handlers: + self.original_formatters[handler] = handler.formatter + + # 清除所有现有处理器 + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # 添加我们的自定义处理器 + custom_handler = self.TUILogHandler(self) + custom_handler.setLevel(logging.DEBUG) # 捕获所有级别的日志 + + # 强制使用包含彩色级别的格式器 + formatter = self.ColoredFormatter( + '%(asctime)s %(levelname_color)s %(name)s: %(message)s', + datefmt='%H:%M:%S' + ) + custom_handler.setFormatter(formatter) + + root_logger.addHandler(custom_handler) + + # 同时重定向stdout和stderr作为备份 + self.original_stdout = sys.stdout + self.original_stderr = sys.stderr + sys.stdout = self.TUIOutput(self) + sys.stderr = self.TUIOutput(self, is_error=True) + + print("✅ TUI日志捕获已启动 - 捕获所有日志输出") + + except Exception as e: + logger.error(f"启动日志捕获失败: {e}") + + def stop_capture(self): + """停止捕获输出""" + try: + # 恢复logging处理器 + root_logger = logging.getLogger() + + # 移除我们的处理器 + for handler in root_logger.handlers[:]: + if hasattr(handler, 'log_display'): + root_logger.removeHandler(handler) + + # 恢复原始处理器和格式器 + for handler in self.original_handlers: + # 恢复格式器 + if handler in self.original_formatters: + handler.setFormatter(self.original_formatters[handler]) + root_logger.addHandler(handler) + + # 恢复stdout和stderr + sys.stdout = self.original_stdout + sys.stderr = self.original_stderr + + print("🛑 TUI日志捕获已停止") + + except Exception as e: + logger.error(f"停止日志捕获失败: {e}") + + def add_log_line(self, line: str): + """添加日志行到TUI显示""" + try: + # 添加到缓冲区 + self.log_lines.append(line) + if len(self.log_lines) > self.max_lines: + self.log_lines.pop(0) + + # 更新显示 + display_content = "\n".join(self.log_lines) + self.update(display_content) + + # 自动滚动到底部 + if self.auto_scroll_enabled: + self.scroll_to_bottom() + + except Exception as e: + # 如果TUI更新失败,回退到原始输出 + if hasattr(self, 'original_stdout'): + self.original_stdout.write(f"TUI日志显示错误: {e}\n") + + def scroll_to_bottom(self): + """滚动到底部""" + try: + # 获取父容器(ScrollableContainer) + parent = self.parent + if parent and hasattr(parent, 'scroll_end'): + parent.scroll_end() + except Exception as e: + # 忽略滚动错误,不影响主要功能 + pass + + def toggle_auto_scroll(self, enabled: bool = None): + """切换自动滚动状态""" + if enabled is None: + self.auto_scroll_enabled = not self.auto_scroll_enabled + else: + self.auto_scroll_enabled = enabled + + logger.debug(f"日志自动滚动: {'启用' if self.auto_scroll_enabled else '禁用'}") + return self.auto_scroll_enabled + + class ColoredFormatter(logging.Formatter): + """带颜色的日志格式器 - 增强版""" + + # ANSI颜色代码 + COLORS = { + 'DEBUG': '\033[36m', # 青色 - DEBUG信息 + 'INFO': '\033[32m', # 绿色 - 正常信息 + 'WARNING': '\033[33m', # 黄色 - 警告信息 + 'ERROR': '\033[31m', # 红色 - 错误信息 + 'CRITICAL': '\033[35m', # 紫色 - 严重错误 + 'RESET': '\033[0m' # 重置颜色 + } + + # 级别显示宽度 + LEVEL_WIDTH = 8 + + def format(self, record): + """格式化日志记录,为级别添加颜色""" + try: + # 为级别添加颜色和固定宽度 + levelname = record.levelname + if levelname in self.COLORS: + # 添加颜色并保持固定宽度 + colored_level = f"{self.COLORS[levelname]}[{levelname:<{self.LEVEL_WIDTH}}]{self.COLORS['RESET']}" + record.levelname_color = colored_level + else: + record.levelname_color = f"[{levelname:<{self.LEVEL_WIDTH}}]" + + # 调用父类格式化方法 + formatted_message = super().format(record) + return formatted_message + + except Exception: + # 如果格式化失败,返回简单格式 + return f"{record.asctime} [{record.levelname}] {record.name}: {record.getMessage()}" + + class TUILogHandler(logging.Handler): + """自定义logging处理器,同时输出到终端和TUI""" + + def __init__(self, log_display): + super().__init__() + self.log_display = log_display + + def emit(self, record): + """处理日志记录""" + try: + # 格式化日志记录(使用我们的格式器) + formatted_message = self.format(record) + + # 输出到原始终端(通过原始处理器,但使用我们的格式器) + for original_handler in self.log_display.original_handlers: + if original_handler.level <= record.levelno: + # 临时使用我们的格式器来确保级别显示一致 + original_handler.setFormatter(self.formatter) + original_handler.emit(record) + # 恢复原始格式器 + original_formatter = self.log_display.original_formatters.get(original_handler) + if original_formatter: + original_handler.setFormatter(original_formatter) + + # 添加到TUI显示 + self.log_display.add_log_line(formatted_message) + + except Exception as e: + # 如果处理失败,使用简单格式 + try: + simple_message = f"{datetime.now().strftime('%H:%M:%S')} [{record.levelname:8}] {record.name}: {record.getMessage()}" + self.log_display.add_log_line(simple_message) + except: + pass + + class TUIOutput(io.TextIOBase): + """自定义输出流,捕获print等输出""" + + def __init__(self, log_display, is_error=False): + self.log_display = log_display + self.is_error = is_error + self.original_stream = sys.stderr if is_error else sys.stdout + + # 颜色定义 + self.COLORS = { + 'INFO': '\033[32m', # 绿色 + 'ERROR': '\033[31m', # 红色 + 'RESET': '\033[0m' # 重置颜色 + } + + def write(self, text): + """写入文本""" + try: + # 写入到原始终端 + self.original_stream.write(text) + self.original_stream.flush() + + # 如果文本不是空的,添加到TUI + if text.strip(): + # 添加简单的时间戳和级别 + timestamp = datetime.now().strftime('%H:%M:%S') + level = "ERROR" if self.is_error else "INFO" + + # 添加颜色 + if level in self.COLORS: + colored_level = f"{self.COLORS[level]}[{level}]{self.COLORS['RESET']}" + else: + colored_level = f"[{level}]" + + # 分割多行文本 + lines = text.split('\n') + for line in lines: + if line.strip(): # 忽略空行 + log_line = f"{timestamp} {colored_level} {line.strip()}" + self.log_display.add_log_line(log_line) + + return len(text) + + except Exception: + # 如果TUI处理失败,只输出到终端 + self.original_stream.write(text) + self.original_stream.flush() + return len(text) + + + def flush(self): + """刷新缓冲区""" + self.original_stream.flush() + + def close(self): + """关闭流""" + pass + +class MessageDisplay(Static): + """消息显示组件 - 增强版""" + + def __init__(self): + super().__init__("消息区域") + self.current_messages: List[Dict] = [] + self.max_messages = 200 # 更大的消息容量 + self.auto_scroll_enabled = True # 启用自动滚动 + # ANSI颜色代码 + self.COLORS = { + 'INFO': '\033[37m', # 黑底白字 + 'DEBUG': '\033[36m', # 青色 - DEBUG信息 + 'SUCCESS': '\033[32m', # 绿色 - 正常信息 + 'WARNING': '\033[33m', # 黄色 - 警告信息 + 'ERROR': '\033[31m', # 红色 - 错误信息 + 'COMMAND': '\033[40;37m', # 灰底白字 - 命令信息 + 'RESET': '\033[0m' # 重置颜色 + } + self.message_types = { + 'info': {'icon': f"{self.COLORS['INFO']}[INFO ]{self.COLORS['RESET']}", 'color': 'white'}, + 'success': {'icon': f"{self.COLORS['SUCCESS']}[SUCCESS]{self.COLORS['RESET']}", 'color': 'green'}, + 'error': {'icon': f"{self.COLORS['ERROR']}[ERROR ]{self.COLORS['RESET']}", 'color': 'red'}, + 'warning': {'icon': f"{self.COLORS['WARNING']}[WARNING]{self.COLORS['RESET']}", 'color': 'yellow'}, + 'debug': {'icon': f"{self.COLORS['DEBUG']}[DEBUG ]{self.COLORS['RESET']}", 'color': 'cyan'}, + 'command': {'icon': f"{self.COLORS['COMMAND']}[COMMAND]{self.COLORS['RESET']}", 'color': 'meow'} + } + logger.debug("MessageDisplay初始化完成") + + def add_message(self, message: str, msg_type: str = "info", persistent: bool = False): + """添加消息 - 支持多行消息""" + try: + # 分割多行消息为单独的消息 + lines = message.strip().split('\n') + for line in lines: + if line.strip(): # 忽略空行 + message_data = { + "text": line.strip(), + "type": msg_type, + "persistent": persistent, + "timestamp": asyncio.get_event_loop().time(), + "display_time": datetime.now().strftime('%H:%M:%S') + } + self.current_messages.append(message_data) + + # 智能消息管理 + self._manage_messages() + + self._update_display() + + except Exception as e: + logger.error(f"添加消息时出错: {str(e)}") + + def reset_display(self): + """重置显示状态""" + try: + # 清空所有消息 + self.current_messages.clear() + # 更新显示 + self.update("消息区域已重置") + # 强制刷新 + self.refresh() + except Exception as e: + logger.error(f"重置消息显示时出错: {str(e)}") + + def _manage_messages(self): + """智能管理消息数量""" + try: + # 计算非持久化消息的数量 + non_persistent_messages = [msg for msg in self.current_messages if not msg['persistent']] + + if len(non_persistent_messages) > self.max_messages: + # 移除最旧的非持久化消息 + for i, msg in enumerate(self.current_messages): + if not msg['persistent']: + self.current_messages.pop(i) + break + + except Exception as e: + logger.error(f"管理消息时出错: {str(e)}") + + def _update_display(self): + """更新显示 - 带时间戳的格式化消息""" + try: + if not self.current_messages: + display_text = "📭 暂无消息" + else: + display_text = [] + for msg in self.current_messages: + # 获取消息类型配置 + msg_config = self.message_types.get(msg['type'], self.message_types['info']) + icon = msg_config['icon'] + + # 构建显示行 + persistent_mark = "🔒 " if msg['persistent'] else "" + time_stamp = f"[{msg['display_time']}] " if len(self.current_messages) > 1 else "" + + display_line = f"{time_stamp}{persistent_mark}{icon} {msg['text']}" + display_text.append(display_line) + + display_text = "\n".join(display_text) + + self.update(display_text) + + # 自动滚动到底部 + if self.auto_scroll_enabled: + self.scroll_to_bottom() + + except Exception as e: + logger.error(f"更新消息显示时出错: {str(e)}") + + def scroll_to_bottom(self): + """滚动到底部""" + try: + # 获取父容器(ScrollableContainer) + parent = self.parent + if parent and hasattr(parent, 'scroll_end'): + parent.scroll_end() + except Exception as e: + # 忽略滚动错误,不影响主要功能 + pass + + def toggle_auto_scroll(self, enabled: bool = None): + """切换自动滚动状态""" + if enabled is None: + self.auto_scroll_enabled = not self.auto_scroll_enabled + else: + self.auto_scroll_enabled = enabled + + logger.debug(f"消息自动滚动: {'启用' if self.auto_scroll_enabled else '禁用'}") + return self.auto_scroll_enabled + +class TUIFramework(App): + """TUI框架应用""" + def __init__(self, config, log_service, command_service): + super().__init__() + self.config = config + self.log_service = log_service + self.command_service = command_service + + self.log_display = LogDisplay() + self.message_display = MessageDisplay() + self.command_input = None + self.CSS = self._generate_css() + + + def _generate_css(self): + """根据配置动态生成CSS - 增强版""" + try: + tui_config = self.config.get('tui', {}) + layout_config = tui_config.get('layout', {}) + styles_config = tui_config.get('styles', {}) + + # 获取布局配置,使用默认值 + grid_rows = layout_config.get('grid_rows', '7fr 2fr 1fr') + + # 获取样式配置,使用默认值 + log_area_style = styles_config.get('log_area', 'border: solid green; overflow-y: auto;') + message_area_style = styles_config.get('message_area', 'border: solid yellow; overflow-y: auto;') + input_area_style = styles_config.get('input_area', 'border: solid red;') + + css = f""" + Screen {{ + layout: grid; + grid-size: 1 3; + grid-rows: {grid_rows}; + }} + + #log-area {{ + {log_area_style} + overflow-y: auto; + scrollbar-size: 1 1; + }} + + #message-area {{ + {message_area_style} + overflow-y: auto; + scrollbar-size: 1 1; + }} + + #input-area {{ + {input_area_style} + }} + + /* 自定义滚动条样式 */ + ScrollableContainer {{ + scrollbar-color: #666 #222; + scrollbar-color-hover: #888 #333; + overflow-y: auto; + }} + + /* 确保内容正确换行 */ + Static {{ + width: 100%; + content-align: left middle; + overflow-y: auto; + }} + """ + logger.debug(f"生成的TUI CSS:\n{css}") + return css + + except Exception as e: + logger.error(f"生成TUI CSS时出错: {str(e)}", exc_info=True) + # 返回默认CSS作为回退 + return """ + Screen { + layout: grid; + grid-size: 1 3; + grid-rows: 7fr 2fr 1fr; + } + + #log-area { + border: solid green; + overflow-y: auto; + scrollbar-size: 1 1; + } + + #message-area { + border: solid yellow; + overflow-y: auto; + scrollbar-size: 1 1; + } + + #input-area { + border: solid red; + } + + /* 自定义滚动条样式 */ + ScrollableContainer { + scrollbar-color: #666 #222; + scrollbar-color-hover: #888 #333; + overflow-y: auto; + } + + /* 确保内容正确换行 */ + Static { + width: 100%; + content-align: left middle; + overflow-y: auto; + } + """ + + + def compose(self): + """组合界面""" + yield Header() + yield ScrollableContainer( + self.log_display, + id="log-area" + ) + yield ScrollableContainer( + self.message_display, + id="message-area" + ) + self.command_input = Input(placeholder="输入指令...", id="command-input") + yield Container( + self.command_input, + id="input-area" + ) + yield Footer() + + async def on_mount(self): + """挂载完成事件""" + try: + # 开始捕获所有输出 + self.log_display.start_capture() + + # 设置输入框焦点 + if self.command_input: + self.command_input.focus() + + # 显示欢迎消息 + self.show_message("🐱 SenSu TUI 已就绪!输入 'help' 查看命令\n", "info") + print("✅ TUI已启动,开始捕获所有输出") + + except Exception as e: + print(f"❌ TUI挂载时出错: {str(e)}") + + async def on_input_submitted(self, event): + """输入提交事件""" + try: + if hasattr(event, 'input') and event.input.id == "command-input": + command = event.value + event.input.value = "" # 清空输入框 + + if command.strip(): + print(f"执行命令: {command}") + + # 在消息区域显示正在处理 + self.show_message(f"执行命令: {command}", "command") + + # 发送到指令服务处理 + result = await self.command_service.process_command(command, "tui") + + # 显示命令结果 + if result: + self.show_message(f"结果: {result}", "success") + else: + self.show_message("命令执行完成", "success") + + except Exception as e: + print(f"❌ 指令处理错误: {str(e)}") + self.show_message(f"指令处理错误: {str(e)}", "error") + + def show_message(self, message: str, msg_type: str = "info", persistent: bool = False): + """显示消息""" + try: + self.message_display.add_message(message, msg_type, persistent) + except Exception as e: + print(f"❌ 显示TUI消息时出错: {str(e)}") + + def clear_messages(self, clear_persistent: bool = False): + """清空消息区域""" + try: + self.message_display.clear_messages(clear_persistent) + except Exception as e: + print(f"❌ 清空消息时出错: {str(e)}") + + async def action_quit(self): + """重写退出动作 - 最佳方案:优雅关闭""" + try: + logger.info("🐱 TUI接收到退出信号,开始关闭流程") + + # 显示关闭消息 + self.show_message("🐱 正在关闭框架...", "info", persistent=True) + + # 停止捕获输出 + self.log_display.stop_capture() + + # 使用异步任务来优雅关闭,避免阻塞 + asyncio.create_task(self._async_graceful_shutdown()) + + except Exception as e: + logger.error(f"TUI退出处理时出错: {str(e)}") + # 紧急退出 + import os + os._exit(0) + + async def _async_graceful_shutdown(self): + """异步优雅关闭""" + try: + # 给一点时间显示消息 + self.show_message("🐱 3...", "info", persistent=True) + await asyncio.sleep(1) + self.show_message("🐱 2..", "info", persistent=True) + await asyncio.sleep(1) + self.show_message("🐱 1.", "info", persistent=True) + await asyncio.sleep(1) + + logger.info("🐱 执行异步关闭") + + logger.debug("使用事件循环停止") + + # 获取当前事件循环 + loop = asyncio.get_event_loop() + + # 停止所有运行中的任务(除了当前任务) + tasks = [t for t in asyncio.all_tasks(loop) if t is not asyncio.current_task()] + + if tasks: + logger.debug(f"取消 {len(tasks)} 个运行中的任务") + for task in tasks: + task.cancel() + + # 等待任务取消完成 + await asyncio.gather(*tasks, return_exceptions=True) + + # 停止事件循环 + loop.stop() + logger.info("🐱 事件循环已停止,框架关闭完成") + + except Exception as e: + logger.error(f"异步关闭失败: {str(e)}") + # 最后的手段 + import os + os._exit(0) + + def shutdown(self): + """关闭TUI""" + try: + # 停止捕获输出 + self.log_display.stop_capture() + self.action_quit() + self.exit() + print("🛑 TUI已关闭") + except Exception as e: + print(f"❌ 关闭TUI时出错: {str(e)}") + +class TuiService: + """TUI服务""" + + def __init__(self, config: Dict, log_service, command_service): + self.config = config + self.log_service = log_service + self.command_service = command_service + self.tui_app = None + self._message_queue = asyncio.Queue() + self._message_processor_task = None + + async def start(self): + """启动TUI""" + try: + if not self.config.get('tui', {}).get('enabled', True): + print("TUI已禁用") + return + + print("启动TUI服务") + self.tui_app = TUIFramework(self.config, self.log_service, self.command_service) + + # 设置动态标题 + self._setup_title() + + # 启动消息处理任务 + self._message_processor_task = asyncio.create_task(self._process_message_queue()) + + # 在后台运行TUI + asyncio.create_task(self._run_tui()) + + except Exception as e: + print(f"启动TUI服务时出错: {str(e)}") + raise + + def toggle_auto_scroll(self, target: str = "all", enabled: bool = None): + """切换自动滚动状态""" + try: + if not self.tui_app: + return "❌ TUI未启动" + + result = [] + + if target in ["all", "log"]: + log_state = self.tui_app.log_display.toggle_auto_scroll(enabled) + result.append(f"📜 日志自动滚动: {'✅ 启用' if log_state else '❌ 禁用'}") + + if target in ["all", "message"]: + msg_state = self.tui_app.message_display.toggle_auto_scroll(enabled) + result.append(f"💬 消息自动滚动: {'✅ 启用' if msg_state else '❌ 禁用'}") + + return "\n".join(result) + + except Exception as e: + return f"❌ 切换自动滚动失败: {str(e)}" + + def scroll_to_bottom(self, target: str = "all"): + """手动滚动到底部""" + try: + if not self.tui_app: + return "❌ TUI未启动" + + result = [] + + if target in ["all", "log"]: + self.tui_app.log_display.scroll_to_bottom() + result.append("📜 日志区域已滚动到底部") + + if target in ["all", "message"]: + self.tui_app.message_display.scroll_to_bottom() + result.append("💬 消息区域已滚动到底部") + + return "\n".join(result) + + except Exception as e: + return f"❌ 滚动到底部失败: {str(e)}" + + async def _process_message_queue(self): + """处理消息队列,避免消息过多导致界面卡顿""" + try: + while True: + # 从队列中获取消息 + message_data = await self._message_queue.get() + + if message_data is None: # 停止信号 + break + + message, msg_type, persistent = message_data + + # 显示消息 + if self.tui_app: + self.tui_app.show_message(message, msg_type, persistent) + + # 小延迟避免消息过快 + await asyncio.sleep(0.05) + + except asyncio.CancelledError: + logger.debug("消息处理任务被取消") + except Exception as e: + logger.error(f"消息处理任务出错: {str(e)}") + + def _setup_title(self): + """设置TUI标题""" + try: + framework_config = self.config.get('framework', {}) + name = framework_config.get('name', 'SenSu') + version = framework_config.get('version', 'Unknown') + debug_mode = framework_config.get('debug', False) + + # 构建标题 + title_parts = [f"🐱 {name} Ver.{version}"] + if debug_mode: + title_parts.append("[DEBUG]") + + self.tui_app.title = " ".join(title_parts) + self.tui_app.sub_title = "Based DreamSu Framework" + logger.debug(f"设置TUI标题: {self.tui_app.title}") + logger.debug(f"设置TUI副标题: {self.tui_app.sub_title}") + + except Exception as e: + logger.error(f"设置TUI标题时出错: {e}") + self.tui_app.title = "🐱 SenSu - Based DreamSu Framework" # 默认标题 + + async def _run_tui(self): + """运行TUI""" + try: + await self.tui_app.run_async() + except Exception as e: + print(f"运行TUI时出错: {str(e)}") + + def show_message(self, message: str, msg_type: str = "info", persistent: bool = False): + """显示消息""" + try: + if self.tui_app: + # 将消息放入队列,由后台任务处理 + self._message_queue.put_nowait((message, msg_type, persistent)) + except Exception as e: + print(f"通过TUI服务显示消息时出错: {str(e)}") + + def shutdown(self): + """关闭TUI服务""" + try: + if self.tui_app: + self.tui_app.shutdown() + print("TUI服务已关闭") + except Exception as e: + print(f"关闭TUI服务时出错: {str(e)}") diff --git a/services/web_panel/__init__.py b/services/web_panel/__init__.py new file mode 100644 index 0000000..b3fe8a6 --- /dev/null +++ b/services/web_panel/__init__.py @@ -0,0 +1,2 @@ +from .manager import WebPanelManager +__all__ = ["WebPanelManager"] diff --git a/services/web_panel/auth.py b/services/web_panel/auth.py new file mode 100644 index 0000000..ad1a325 --- /dev/null +++ b/services/web_panel/auth.py @@ -0,0 +1,31 @@ +import logging +logger = logging.getLogger(__name__) +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import functools +from aiohttp import web + +def panel_auth(handler): + """面板专用鉴权装饰器(替代子应用中间件)""" + @functools.wraps(handler) + async def wrapper(request, *args, **kwargs): + token = request.cookies.get("panel_token") + if not token and request.headers.get("Authorization", "").startswith("Bearer "): + token = request.headers["Authorization"].split(" ", 1)[1] + + auth_svc = request.app.get('auth_service') + is_valid = False + + if token and auth_svc: + try: + v = await auth_svc.validate_token(token) + is_valid = bool(v) + except: pass + elif not auth_svc: + is_valid = False # 认证不可用时拒绝 + + if not is_valid: + return web.json_response({"error": "未认证或会话过期"}, status=401) + + return await handler(request, *args, **kwargs) + return wrapper diff --git a/services/web_panel/manager.py b/services/web_panel/manager.py new file mode 100644 index 0000000..2be5608 --- /dev/null +++ b/services/web_panel/manager.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import logging +from pathlib import Path +from aiohttp import web +from .routes import auth, status, plugins, commands, logs + +logger = logging.getLogger(__name__) + +class WebPanelManager: + def __init__(self, config: dict, service_manager): + panel_cfg = config.get('panel', {}).get('entrance', {}) + self.base_path = panel_cfg.get('path', '/panel') + self.panel_user = os.environ.get('SENSU_PANEL_USER', panel_cfg.get('username', 'admin')) + self.panel_pass = os.environ.get('SENSU_PANEL_PASS', panel_cfg.get('password', 'admin')) + + self.base_path = f"/{self.base_path.strip('/')}" + self.sm = service_manager + self.project_root = Path(__file__).resolve().parent.parent.parent + + async def start(self): + internet = self.sm.get_service("internet") + if not internet or not internet.http_app: + logger.error("❌ 网络服务未就绪,无法注册面板路由") + return False + + app = internet.http_app + logger.info(f"🌐 向网络服务注册面板路由 (前缀: {self.base_path})...") + + # 依赖注入 + app['service_manager'] = self.sm + app['auth_service'] = self.sm.get_service("auth") + app['log_service'] = self.sm.get_service("log") + app['panel_config'] = { + 'username': self.panel_user, + 'password': self.panel_pass, + 'index_path': self.project_root / "static" / "web_panel" / "index.html", + 'home_path': self.project_root / "static" / "web_panel" / "home.html" # 🟢 新增 + } + + # 注册静态文件 + # URL 前缀: /SenSu/static/ -> 物理路径: .../static/web_panel/ + static_dir = self.project_root / "static" / "web_panel" + if static_dir.exists(): + app.router.add_static(f'{self.base_path}/static/', path=str(static_dir)) + logger.info(f"📂 静态资源已挂载: {self.base_path}/static/") + else: + logger.warning(f"⚠️ 静态目录缺失: {static_dir}") + + # 注册首页 (登录页) + app.router.add_get(self.base_path, self._redirect_slash) + app.router.add_get(f'{self.base_path}/', self._serve_index) + + # 🟢 新增: 注册面板主页 (/SenSu/home.html -> home.html) + app.router.add_get(f'{self.base_path}/home.html', self._serve_home) + + # 注册 API 路由 + auth.setup_routes(app, self.base_path) + status.setup_routes(app, self.base_path) + plugins.setup_routes(app, self.base_path) + commands.setup_routes(app, self.base_path) + logs.setup_routes(app, self.base_path) + + # 注册日志广播 + ls = self.sm.get_service("log") + if ls and hasattr(ls, 'add_log_consumer'): + ls.add_log_consumer(logs.broadcast_log) + logger.info("📡 日志广播已连接") + + logger.info(f"✅ 面板路由注册完成 (复用原有网络服务路由器)") + return True + + async def _redirect_slash(self, req): + return web.HTTPFound(f'{self.base_path}/') + + async def _serve_index(self, req): + """提供登录页""" + path = req.app['panel_config']['index_path'] + if path.exists(): return web.FileResponse(path) + return web.Response(text=f"❌ 找不到 index.html\n路径: {path}", status=404) + + async def _serve_home(self, req): + """提供面板主页""" + path = req.app['panel_config']['home_path'] + if path.exists(): return web.FileResponse(path) + return web.Response(text=f"❌ 找不到 home.html\n路径: {path}", status=404) diff --git a/services/web_panel/middleware.py b/services/web_panel/middleware.py new file mode 100644 index 0000000..3aa2ad1 --- /dev/null +++ b/services/web_panel/middleware.py @@ -0,0 +1,39 @@ +from aiohttp import web +from .utils.response import json_res +import logging + +logger = logging.getLogger(__name__) + +# 白名单 (相对于子应用的路径) +WHITE_LIST = { + "/api/login", + "/api/auth/status", + "/", + "/static/" +} + +async def auth_middleware(app, handler): + async def mid(req): + path = req.path + + # 检查白名单 + if any(path.startswith(w) for w in WHITE_LIST): + return await handler(req) + + # 提取 Token + token = req.cookies.get("panel_token") + if not token and req.headers.get("Authorization", "").startswith("Bearer "): + token = req.headers["Authorization"].split(" ", 1)[1] + + valid, info = False, {} + + # 验证 Token (简单内存验证,后期可接 Redis/DB) + session_store = app.get('session_store', {}) + if token and token in session_store: + valid, info = True, session_store[token] + + if valid: + req['user'] = info + return await handler(req) + return json_res({"error": "未认证"}, 401) + return mid diff --git a/services/web_panel/routes/__init__.py b/services/web_panel/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/web_panel/routes/auth.py b/services/web_panel/routes/auth.py new file mode 100644 index 0000000..b5daf97 --- /dev/null +++ b/services/web_panel/routes/auth.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import secrets +import logging +from aiohttp import web +from ..utils.auth import panel_auth + +logger = logging.getLogger(__name__) + +# 全局 Session 存储 (内存型) +# 格式: { "token_string": { "username": "...", "perms": [...] } } +PANEL_SESSION_STORE = {} + +def setup_routes(app, prefix=''): + """注册面板认证路由""" + # 🟢 关键:将 Session Store 挂载到 app,供拦截器读取 + app['panel_session_store'] = PANEL_SESSION_STORE + + # 路由注册 + app.router.add_post(f'{prefix}/api/login', handle_login) + # 退出和状态检查都需要拦截 + app.router.add_post(f'{prefix}/api/logout', panel_auth(handle_logout)) + app.router.add_get(f'{prefix}/api/auth/status', panel_auth(handle_auth_status)) + +async def handle_login(req): + """处理面板登录""" + try: + data = await req.json() + username = data.get('username') + password = data.get('password') + + cfg = req.app.get('panel_config', {}) + cfg_user = cfg.get('username', 'admin') + cfg_pass = cfg.get('password', 'admin') + + # 校验配置中的账号密码 + if username == cfg_user and password == cfg_pass: + # 登录成功:生成 Token + token = secrets.token_hex(16) + + # 写入 Session Store + user_info = { + "username": username, + "perms": ["admin"], + "login_time": __import__('time').time() + } + PANEL_SESSION_STORE[token] = user_info + + logger.info(f"✅ 面板登录成功: {username} (Session: {token[:4]}...)") + + resp = web.json_response({"success": True, "username": username}) + # 设置 Cookie + resp.set_cookie("panel_token", token, max_age=259200, httponly=True, samesite="Lax") + return resp + else: + logger.warning(f"❌ 面板登录失败: 用户 {username} 密码错误") + return web.json_response({"success": False, "msg": "用户名或密码错误"}, status=401) + + except Exception as e: + logger.error(f"登录异常: {e}") + return web.json_response({"error": str(e)}, status=500) + +async def handle_logout(req): + """处理退出登录""" + token = req.cookies.get("panel_token") + if token and token in PANEL_SESSION_STORE: + del PANEL_SESSION_STORE[token] + logger.info(f"👋 用户退出登录") + + resp = web.json_response({"success": True}) + resp.del_cookie("panel_token") + return resp + +async def handle_auth_status(req): + """获取当前认证状态 (被 panel_auth 拦截,能进来说明已认证)""" + user = req.get('user', {}) + return web.json_response({ + "authenticated": True, + "username": user.get("username", "Unknown"), + "perms": user.get("perms", []) + }) diff --git a/services/web_panel/routes/commands.py b/services/web_panel/routes/commands.py new file mode 100644 index 0000000..13306b1 --- /dev/null +++ b/services/web_panel/routes/commands.py @@ -0,0 +1,15 @@ +from aiohttp import web +from ..utils.auth import panel_auth + +def setup_routes(app, prefix=''): + app.router.add_post(f'{prefix}/api/command', panel_auth(exec_cmd)) + +async def exec_cmd(req): + d = await req.json() + cs = req.app.get('service_manager').get_service("command") + if not cs: return web.json_response({"error": "Missing"}, 503) + try: + res = await cs.execute_command(d.get('command','')) + return web.json_response({"success": True, "output": str(res)}) + except Exception as e: + return web.json_response({"success": False, "error": str(e)}) diff --git a/services/web_panel/routes/logs.py b/services/web_panel/routes/logs.py new file mode 100644 index 0000000..b627630 --- /dev/null +++ b/services/web_panel/routes/logs.py @@ -0,0 +1,30 @@ +import json, asyncio +from aiohttp import web +from ..utils.auth import panel_auth + +active_ws = set() + +def setup_routes(app, prefix=''): + app.router.add_get(f'{prefix}/api/logs/ws', panel_auth(ws_handler)) + +async def ws_handler(req): + ws = web.WebSocketResponse(heartbeat=30.0) + await ws.prepare(req) + active_ws.add(ws) + try: + async for msg in ws: + if msg.type == web.WSMsgType.TEXT: + d = json.loads(msg.data) + if d.get('action') == 'set_level': + ls = req.app.get('log_service') + if ls: ls.set_level(d.get('level','INFO')) + finally: active_ws.discard(ws) + return ws + +def broadcast_log(log_record): + if not active_ws: return + payload = json.dumps({"type":"log", "level":log_record.get('level','INFO'), + "message":log_record.get('simple_message',''), "timestamp":log_record.get('timestamp',0)}) + for ws in list(active_ws): + if not ws.closed: asyncio.ensure_future(ws.send_str(payload)) + else: active_ws.discard(ws) diff --git a/services/web_panel/routes/plugins.py b/services/web_panel/routes/plugins.py new file mode 100644 index 0000000..b3e74df --- /dev/null +++ b/services/web_panel/routes/plugins.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import logging +from aiohttp import web +from ..utils.auth import panel_auth + +logger = logging.getLogger(__name__) + +def setup_routes(app, prefix=''): + app.router.add_get(f'{prefix}/api/plugins', panel_auth(list_plugins)) + app.router.add_post(f'{prefix}/api/plugins/{{name}}/{{action}}', panel_auth(manage_plugin)) + app.router.add_get(f'{prefix}/api/plugins/{{name}}/perms', panel_auth(get_perms)) + app.router.add_post(f'{prefix}/api/plugins/{{name}}/perms', panel_auth(set_perms)) + +async def list_plugins(req): + sm = req.app.get('service_manager') + if not sm: + return web.json_response({"error": "Service Manager 未初始化"}, status=503) + + ps = sm.get_service("plugin") + if not ps: + return web.json_response({"plugins": []}) + + data = [] + for name, info in ps.plugin_info.items(): + data.append({ + "name": name, + "version": getattr(info, 'version', '?'), + "running": name in ps.plugins, + "enabled": True + }) + return web.json_response({"plugins": data}) + +async def manage_plugin(req): + sm = req.app.get('service_manager') + if not sm: return web.json_response({"error": "SM Missing"}, 503) + + name = req.match_info['name'] + action = req.match_info['action'] + ps = sm.get_service("plugin") + + if not ps: return web.json_response({"error": "Plugin Service Missing"}, 503) + + try: + if action in ('disable', 'unload'): + await ps.unload_plugin(name) + elif action == 'enable': + await ps.load_plugin(name) + elif action == 'reload': + await ps.unload_plugin(name) + await ps.load_plugin(name) + return web.json_response({"success": True, "msg": "操作成功"}) + except Exception as e: + logger.error(f"插件操作失败: {e}") + return web.json_response({"success": False, "error": str(e)}) + +async def get_perms(req): + return web.json_response({"plugin": req.match_info['name'], "permissions": ["read", "write"]}) + +async def set_perms(req): + return web.json_response({"success": True}) diff --git a/services/web_panel/routes/status.py b/services/web_panel/routes/status.py new file mode 100644 index 0000000..ace8768 --- /dev/null +++ b/services/web_panel/routes/status.py @@ -0,0 +1,27 @@ +import time +from aiohttp import web +from ..utils.auth import panel_auth +from ..utils.system_info import SystemInfoCollector + +collector = SystemInfoCollector() + +def setup_routes(app, prefix=''): + app.router.add_get(f'{prefix}/api/framework', panel_auth(get_framework)) + app.router.add_get(f'{prefix}/api/system', panel_auth(get_system)) + +async def get_framework(req): + sm = req.app.get('service_manager') + if not sm: return web.json_response({"error": "Missing"}, 500) + + ps = sm.get_service("plugin") + # 🟢 修复:使用 sm.start_time 属性 + uptime = time.time() - getattr(sm, 'start_time', time.time()) + + return web.json_response({ + "version": "Alpha_0.2.0", + "uptime": int(uptime), # 取整秒 + "plugins": len(ps.plugins) if ps else 0 + }) + +async def get_system(req): + return web.json_response(collector.get_all()) diff --git a/services/web_panel/utils/__init__.py b/services/web_panel/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/web_panel/utils/auth.py b/services/web_panel/utils/auth.py new file mode 100644 index 0000000..9ed5a25 --- /dev/null +++ b/services/web_panel/utils/auth.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import functools +from aiohttp import web + +def panel_auth(handler): + """面板专用鉴权装饰器:基于面板自有的 Session Store 验证""" + @functools.wraps(handler) + async def wrapper(request, *args, **kwargs): + # 1. 获取 Token + token = request.cookies.get("panel_token") + if not token and request.headers.get("Authorization", "").startswith("Bearer "): + token = request.headers["Authorization"].split(" ", 1)[1] + + is_valid = False + + # 2. 从面板 Session Store 验证 + session_store = request.app.get('panel_session_store', {}) + if token and token in session_store: + is_valid = True + # 验证通过,将用户信息注入 request 供后续使用 + request['user'] = session_store[token] + + # 3. 拦截逻辑 (不再依赖外部 AuthService,确保安全隔离) + if not is_valid: + # 返回 401 并附带提示,前端可据此判断状态 + return web.json_response({ + "error": "未认证或会话已过期", + "status": 401 + }, status=401) + + return await handler(request, *args, **kwargs) + return wrapper diff --git a/services/web_panel/utils/response.py b/services/web_panel/utils/response.py new file mode 100644 index 0000000..1ae6156 --- /dev/null +++ b/services/web_panel/utils/response.py @@ -0,0 +1,7 @@ +from aiohttp import web +def json_res(data, status=200, cookie=None): + resp = web.json_response(data, status=status) + if cookie: resp.set_cookie(cookie["n"], cookie["v"], max_age=cookie.get("m", 86400), httponly=True) + return resp +def get_user(req): + return req.get('user') diff --git a/services/web_panel/utils/system_info.py b/services/web_panel/utils/system_info.py new file mode 100644 index 0000000..095e8a1 --- /dev/null +++ b/services/web_panel/utils/system_info.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import os +import time +import platform +import logging +from typing import Dict, Any + +logger = logging.getLogger(__name__) + +def _is_android() -> bool: + """检测是否为 Android 环境 (Termux 等)""" + return ( + 'ANDROID_ROOT' in os.environ or + os.path.exists('/system/bin/getprop') or + platform.release().lower().find('android') != -1 + ) + + +class SystemInfoCollector: + def __init__(self): + self.is_android = 'ANDROID_ROOT' in os.environ or os.path.exists('/system/bin/getprop') + self.psutil = None + if not self.is_android: + try: + import psutil + self.psutil = psutil + except ImportError as e: + logger.debug(f"psutil not available: {e}") + else: + logger.info("🤖 Android 平台识别,启用原生采集") + + def get_all(self): + return { + "platform": { + "system": platform.system(), + "machine": platform.machine(), + "python": platform.python_version() + }, + "cpu": self._get_cpu(), + "memory": self._get_memory(), + "network": self._get_network() + } + + def _get_cpu(self): + if self.psutil: + return { + "percent": self.psutil.cpu_percent(interval=0.1), + "cores": self.psutil.cpu_count(), + "load_avg": os.getloadavg() if hasattr(os, 'getloadavg') else [0,0,0] + } + # Android 估算:负载率 = (1分钟负载 / 核心数) * 100 + try: + load = os.getloadavg() + cores = os.cpu_count() or 1 + percent = min(100.0, (load[0] / cores) * 100) + return {"percent": round(percent, 1), "cores": cores, "load_avg": load} + except Exception as e: + logger.error(f"System info collection error: {e}", exc_info=True) + return {"percent": 0, "cores": 0, "load_avg": [0,0,0]} + + def _get_memory(self): + if self.psutil: + m = self.psutil.virtual_memory() + return {"total_gb": round(m.total/1073741824, 1), "used_gb": round(m.used/1073741824, 1), "percent": m.percent} + try: + mem = {} + with open('/proc/meminfo') as f: + for line in f: + parts = line.split() + if len(parts) >= 2: mem[parts[0].rstrip(':')] = int(parts[1]) * 1024 + t, a = mem.get('MemTotal', 1), mem.get('MemAvailable', mem.get('MemFree', 0)) + return {"total_gb": round(t/1073741824, 1), "used_gb": round((t-a)/1073741824, 1), "percent": round(((t-a)/t)*100, 1)} + except Exception as e: + logger.warning(f"Memory info failed: {e}") + return {"total_gb": 0, "used_gb": 0, "percent": 0} + + def _get_network(self): + if self.psutil: + io = self.psutil.net_io_counters() + return {"rx": round(io.bytes_recv/1048576, 1), "tx": round(io.bytes_sent/1048576, 1)} + # Android 解析 /proc/net/dev + try: + rx = 0 + with open('/proc/net/dev', 'r') as f: + for line in f: + if ':' in line and 'lo' not in line: # 排除 lo 回环 + parts = line.split(':')[1].split() + rx += int(parts[0]) # RX bytes + return {"rx": round(rx/1048576, 1), "tx": 0} + except Exception as e: + logger.warning(f"Network info failed: {e}") + return {"rx": 0, "tx": 0} diff --git a/static/web_panel/css/style.css b/static/web_panel/css/style.css new file mode 100644 index 0000000..eb1484d --- /dev/null +++ b/static/web_panel/css/style.css @@ -0,0 +1,159 @@ +:root { + --bg-dark: #121218; --bg-panel: #1e1e24; --bg-card: #252530; + --bg-hover: #2a2a35; --text-main: #e0e0e0; --text-dim: #888890; + --accent: #7aa2f7; --accent-glow: rgba(122, 162, 247, 0.3); + --success: #9ece6a; --warn: #e0af68; --error: #f7768e; + --border: #363642; --sidebar-w: 240px; --sidebar-collapsed: 64px; +} +* { box-sizing: border-box; margin: 0; padding: 0; } +body { background: var(--bg-dark); color: var(--text-main); font-family: system-ui, -apple-system, sans-serif; height: 100vh; overflow: hidden; } + +/* 登录页 */ +.login-wrapper { display: flex; align-items: center; justify-content: center; height: 100vh; background: radial-gradient(circle at center, var(--bg-panel) 0%, var(--bg-dark) 100%); } +.login-card { background: var(--bg-card); padding: 2.5rem; border-radius: 16px; width: 340px; text-align: center; box-shadow: 0 10px 40px rgba(0,0,0,0.5); border: 1px solid var(--border); } +.login-card h2 { color: var(--accent); margin-bottom: 1.5rem; font-size: 1.8rem; } +.input-group { margin-bottom: 1rem; text-align: left; } +.input-group label { display: block; font-size: 0.8rem; color: var(--text-dim); margin-bottom: 4px; } +.input-group input { width: 100%; padding: 12px; background: var(--bg-dark); border: 1px solid var(--border); color: white; border-radius: 8px; font-size: 1rem; } +.input-group input:focus { border-color: var(--accent); outline: none; } +.btn-primary { width: 100%; padding: 12px; background: var(--accent); color: #000; font-weight: bold; border: none; border-radius: 8px; cursor: pointer; transition: 0.2s; margin-top: 10px; } +.btn-primary:hover { background: var(--accent-glow); color: white; } +.err-msg { color: var(--error); font-size: 0.8rem; margin-top: 10px; min-height: 1rem; } + +/* 主框架布局 */ +.app-frame { display: grid; grid-template-columns: var(--sidebar-w) 1fr; grid-template-rows: 60px 1fr; height: 100vh; transition: 0.3s ease; } +.app-frame.collapsed { grid-template-columns: var(--sidebar-collapsed) 1fr; } + +header.top-bar { grid-column: 1 / -1; background: var(--bg-panel); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 1.5rem; } +.top-bar .title { font-weight: bold; font-size: 1.1rem; color: var(--accent); } +.top-bar .user-info { display: flex; align-items: center; gap: 1rem; font-size: 0.9rem; color: var(--text-dim); } +.logout-btn { background: none; border: 1px solid var(--border); color: var(--text-dim); padding: 4px 10px; border-radius: 4px; cursor: pointer; } +.logout-btn:hover { border-color: var(--error); color: var(--error); } + +aside.sidebar { background: var(--bg-panel); border-right: 1px solid var(--border); display: flex; flex-direction: column; padding: 1rem 0; overflow: hidden; transition: 0.3s; } +.nav-item { display: flex; align-items: center; padding: 12px 16px; color: var(--text-dim); text-decoration: none; cursor: pointer; transition: 0.2s; white-space: nowrap; gap: 12px; margin: 2px 8px; border-radius: 8px; } +.nav-item:hover, .nav-item.active { background: var(--bg-hover); color: var(--accent); } +.nav-item svg { width: 20px; height: 20px; fill: currentColor; flex-shrink: 0; } +.toggle-sidebar { margin-top: auto; padding: 12px; text-align: center; cursor: pointer; color: var(--text-dim); border-top: 1px solid var(--border); } +.toggle-sidebar:hover { color: var(--accent); } + +main.content-area { position: relative; overflow: hidden; background: var(--bg-dark); display: flex; flex-direction: column; } +.progress-bar { position: absolute; top: 0; left: 0; height: 3px; background: var(--accent); width: 0; transition: width 0.3s, opacity 0.3s; opacity: 0; z-index: 100; } +.progress-bar.active { opacity: 1; } +.page-container { flex: 1; padding: 1.5rem; overflow-y: auto; } + +/* 仪表盘网格 */ +.dash-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; } +.stat-card { background: var(--bg-card); border-radius: 12px; padding: 1.2rem; border: 1px solid var(--border); display: flex; flex-direction: column; gap: 0.5rem; } +.stat-card h3 { font-size: 0.85rem; color: var(--text-dim); display: flex; justify-content: space-between; } +.stat-value { font-size: 2rem; font-weight: bold; color: var(--text-main); } +.stat-sub { font-size: 0.8rem; color: var(--text-dim); } +.mini-chart { width: 100%; height: 60px; background: rgba(0,0,0,0.2); border-radius: 6px; margin-top: 8px; } + +/* 终端/日志样式 */ +.terminal { background: #0a0a0f; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; height: 75vh; display: flex; flex-direction: column; } +.term-header { background: var(--bg-card); padding: 8px 12px; font-family: monospace; font-size: 0.8rem; color: var(--text-dim); border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; } +.term-body { flex: 1; padding: 10px; overflow-y: auto; font-family: monospace; font-size: 0.85rem; color: #ccc; line-height: 1.5; } +.term-input-area { display: flex; border-top: 1px solid var(--border); } +.term-input { flex: 1; background: var(--bg-panel); border: none; padding: 12px; color: white; font-family: monospace; outline: none; } +.log-entry { margin-bottom: 2px; border-bottom: 1px solid #1a1a24; padding: 2px 0; } +.log-INFO { color: var(--accent); } .log-WARNING { color: var(--warn); } .log-ERROR { color: var(--error); } + +/* 插件列表 */ +.plugin-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; } +.plugin-card { background: var(--bg-card); padding: 1rem; border-radius: 8px; border: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; } +.plugin-info h4 { color: var(--accent); margin-bottom: 4px; } +.plugin-info p { font-size: 0.8rem; color: var(--text-dim); } +.badge { padding: 2px 8px; border-radius: 10px; font-size: 0.7rem; font-weight: bold; } +.badge-run { background: rgba(158, 206, 106, 0.2); color: var(--success); } +.badge-stop { background: rgba(247, 118, 142, 0.2); color: var(--error); } +.plugin-act { display: flex; gap: 8px; } +.btn-sm { padding: 4px 10px; background: var(--bg-hover); border: 1px solid var(--border); color: var(--text-dim); border-radius: 4px; cursor: pointer; font-size: 0.75rem; } +.btn-sm:hover { color: var(--accent); border-color: var(--accent); } + +/* 响应式 */ +@media (max-width: 768px) { + .app-frame { grid-template-columns: var(--sidebar-collapsed) 1fr; } + .nav-item span { display: none; } + .toggle-sidebar { display: none; } +} + +/* ========================================= + 仪表盘右侧栏布局扩展 + ========================================= */ +.dash-layout { + display: flex; + gap: 1.5rem; + height: calc(100vh - 140px); /* 减去 Header 和 Padding */ + overflow: hidden; +} + +.dash-main { + flex: 1; + overflow-y: auto; + padding-right: 5px; + /* 自定义滚动条 */ + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; +} + +/* 右侧固定侧边栏 */ +.dash-sidebar { + width: 320px; + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 1rem; + overflow-y: auto; + padding-right: 5px; +} + +/* 侧边卡片样式 */ +.side-card { + background: var(--bg-card); + border-radius: 12px; + padding: 1.2rem; + border: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: 0.8rem; + transition: 0.3s; +} +.side-card:hover { border-color: var(--accent); } + +.side-card h3 { + font-size: 0.9rem; + color: var(--accent); + margin: 0; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + gap: 6px; +} + +.info-row { + display: flex; + justify-content: space-between; + font-size: 0.85rem; + padding: 4px 0; + color: var(--text-dim); +} + +.info-value { + color: var(--text-main); + font-family: 'Consolas', monospace; + font-weight: 600; + text-align: right; + max-width: 60%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 响应式适配:平板/手机自动切换为上下布局 */ +@media (max-width: 1100px) { + .dash-layout { flex-direction: column; height: auto; overflow: visible; } + .dash-sidebar { width: 100%; flex-direction: row; flex-wrap: wrap; overflow: visible; } + .side-card { flex: 1; min-width: 280px; } +} diff --git a/static/web_panel/home.html b/static/web_panel/home.html new file mode 100644 index 0000000..b6538a1 --- /dev/null +++ b/static/web_panel/home.html @@ -0,0 +1,51 @@ + + + + + SenSu 面板 + + + + +
+
+
🐱 SenSu Alpha
+ +
+ + + +
+
+
+
+
+ + + + + + + + diff --git a/static/web_panel/index.html b/static/web_panel/index.html new file mode 100644 index 0000000..32a5895 --- /dev/null +++ b/static/web_panel/index.html @@ -0,0 +1,36 @@ + + + + + SenSu 登录 + + + + + + + diff --git a/static/web_panel/js/api.js b/static/web_panel/js/api.js new file mode 100644 index 0000000..b0eba27 --- /dev/null +++ b/static/web_panel/js/api.js @@ -0,0 +1,32 @@ +const BASE = '/panel'; + +export const api = { + get: async (url) => { + const res = await fetch(`${BASE}${url}`, { credentials: 'include' }); + return res.json(); + }, + post: async (url, data) => { + const res = await fetch(`${BASE}${url}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(data) + }); + return res.json(); + } +}; + +export async function initAuth() { + // 登录逻辑绑定到 DOM + const loginForm = document.querySelector('#login-form'); + if(loginForm) { + loginForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const u = document.getElementById('username').value; + const p = document.getElementById('password').value; + const res = await api.post('/api/login', { username: u, password: p }); + if (res.success) location.reload(); // 登录成功刷新 + }); + } + return await api.get('/api/auth/status'); +} diff --git a/static/web_panel/js/app.js b/static/web_panel/js/app.js new file mode 100644 index 0000000..f388c27 --- /dev/null +++ b/static/web_panel/js/app.js @@ -0,0 +1,77 @@ +// 初始化检查 +window.onload = async () => { + try { + const res = await fetch('./api/auth/status', { credentials: 'include' }); + if(res.status === 401) { window.location.href = './index.html'; return; } + const data = await res.json(); + if(!data.authenticated) { window.location.href = './index.html'; return; } + + document.getElementById('uname').textContent = data.username || 'Admin'; + loadPage('dashboard'); // 默认加载 + } catch(e) { window.location.href = './index.html'; } +}; + +// 路由加载器 + +async function loadPage(pageName) { + const content = document.getElementById('page-content'); + const bar = document.getElementById('progress'); + + // 侧边栏高亮 + document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active')); + document.querySelector(`.nav-item[data-page="${pageName}"]`)?.classList.add('active'); + + // 进度条动画 + bar.classList.add('active'); bar.style.width = '0%'; + await new Promise(r => requestAnimationFrame(() => { bar.style.width = '80%'; setTimeout(r, 100); })); + + try { + // 🟢 关键修改:fetch 路径必须包含 /static/ + const html = await fetch(`./static/pages/${pageName}.html`).then(r => { + if(!r.ok) throw new Error('404'); + return r.text(); + }); + content.innerHTML = html; + + // 动态加载对应 JS 模块 + // 🟢 关键修改:script src 路径必须包含 /static/ + const script = document.createElement('script'); + script.src = `./static/pages/${pageName}.js?t=${Date.now()}`; + + script.onload = () => { + // 触发模块初始化 + const moduleName = pageName.charAt(0).toUpperCase() + pageName.slice(1) + 'Module'; + if(window[moduleName]?.init) { + window[moduleName].init(); + } + bar.style.width = '100%'; + setTimeout(() => bar.classList.remove('active'), 200); + }; + script.onerror = () => { + throw new Error('JS Load Failed'); + }; + document.head.appendChild(script); + } catch(e) { + content.innerHTML = `
页面加载失败: ${e.message}
`; + bar.style.background = 'var(--error)'; + setTimeout(() => { bar.style.width = '100%'; setTimeout(() => { bar.classList.remove('active'); bar.style.background = 'var(--accent)'; }, 200); }, 100); + } +} + + +// 侧边栏切换 +function toggleSidebar() { + document.getElementById('app').classList.toggle('collapsed'); +} + +// 退出登录 +async function doLogout() { + await fetch('./api/logout', { method: 'POST', credentials: 'include' }); + window.location.href = './index.html'; +} + +// 点击侧边栏事件委托 +document.addEventListener('click', (e) => { + const nav = e.target.closest('.nav-item'); + if(nav) { loadPage(nav.dataset.page); e.preventDefault(); } +}); diff --git a/static/web_panel/js/chart.js b/static/web_panel/js/chart.js new file mode 100644 index 0000000..52ca634 --- /dev/null +++ b/static/web_panel/js/chart.js @@ -0,0 +1,51 @@ +class MiniChart { + constructor(canvasId, color = '#7aa2f7') { + this.canvas = document.getElementById(canvasId); + this.ctx = this.canvas.getContext('2d'); + this.color = color; + this.data = new Array(60).fill(0); // 60秒历史 + this.maxVal = 100; + + this.resize(); + window.addEventListener('resize', () => this.resize()); + } + + resize() { + const rect = this.canvas.parentElement.getBoundingClientRect(); + this.canvas.width = rect.width - 24; + this.canvas.height = 60; + this.draw(); + } + + update(val) { + this.data.push(val); + if(this.data.length > 60) this.data.shift(); + this.maxVal = Math.max(...this.data, 100); + this.draw(); + } + + draw() { + if(!this.ctx) return; + const { width, height } = this.canvas; + this.ctx.clearRect(0, 0, width, height); + + this.ctx.strokeStyle = this.color; + this.ctx.lineWidth = 2; + this.ctx.beginPath(); + + this.data.forEach((v, i) => { + const x = (i / 59) * width; + const y = height - (v / this.maxVal) * (height - 10); + if(i === 0) this.ctx.moveTo(x, y); + else this.ctx.lineTo(x, y); + }); + this.ctx.stroke(); + + // 填充渐变 + this.ctx.lineTo(width, height); + this.ctx.lineTo(0, height); + this.ctx.fillStyle = this.color + '20'; + this.ctx.fill(); + } +} +window.MiniChart = MiniChart; diff --git a/static/web_panel/js/main.js b/static/web_panel/js/main.js new file mode 100644 index 0000000..0284fb7 --- /dev/null +++ b/static/web_panel/js/main.js @@ -0,0 +1,29 @@ +import { initAuth, api } from './api.js'; +import { initDashboard } from './modules/dashboard.js'; +import { initPlugins } from './modules/plugins.js'; +import { initLogs } from './modules/logs.js'; +import { initCommands } from './modules/commands.js'; + +document.addEventListener('DOMContentLoaded', async () => { + // 1. 检查登录状态 + const authState = await initAuth(); + if (!authState.authenticated) return; // 停留在登录页 + + // 2. 初始化各模块 + initDashboard(); + initPlugins(); + initLogs(); + initCommands(); + + // 3. Tab 切换逻辑 + document.querySelectorAll('.tabs button').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.tabs button').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + + const tabId = `tab-${btn.dataset.tab}`; + document.querySelectorAll('.content section').forEach(s => s.classList.add('hidden')); + document.getElementById(tabId).classList.remove('hidden'); + }); + }); +}); diff --git a/static/web_panel/pages/console.html b/static/web_panel/pages/console.html new file mode 100644 index 0000000..90503f8 --- /dev/null +++ b/static/web_panel/pages/console.html @@ -0,0 +1,10 @@ +
+
💻 FRAMEWORK CONSOLE
+
+
SenSu Console Ready. Type 'help' or 'status'.
+
+
+ $ + +
+
diff --git a/static/web_panel/pages/console.js b/static/web_panel/pages/console.js new file mode 100644 index 0000000..b95204f --- /dev/null +++ b/static/web_panel/pages/console.js @@ -0,0 +1,29 @@ +window.ConsoleModule = { + init: () => { + const box = document.getElementById('cmd-box'); + const input = document.getElementById('cmd-input'); + input.focus(); + + const append = (text, cls='') => { + box.innerHTML += `
${text}
`; + box.scrollTop = box.scrollHeight; + }; + + input.onkeydown = async (e) => { + if(e.key === 'Enter' && input.value.trim()) { + const cmd = input.value.trim(); + append(`$ ${cmd}`, 'var(--accent)'); + input.value = ''; + try { + const res = await fetch('./api/command', { + method:'POST', headers:{'Content-Type':'application/json'}, credentials:'include', + body: JSON.stringify({command: cmd}) + }); + const d = await res.json(); + append(d.success ? d.output : `Error: ${d.error}`, d.success ? '#ccc' : 'var(--error)'); + } catch(err) { append(`Network Error: ${err.message}`, 'var(--error)'); } + } + }; + }, + destroy: () => {} +}; diff --git a/static/web_panel/pages/dashboard.html b/static/web_panel/pages/dashboard.html new file mode 100644 index 0000000..87658fe --- /dev/null +++ b/static/web_panel/pages/dashboard.html @@ -0,0 +1,70 @@ +
+ +
+
+
+

⏳ 运行时间

+
--
+
v?.?.?
+
+
+

📦 插件状态

+
--
+
已加载 / 活跃
+
+
+

🧠 内存使用

+
--%
+ +
+
+

⚡ CPU 负载

+
--%
+ +
+
+

🌐 网络接收

+
--
+ +
+
+

💾 进程内存

+
--
+ +
+
+
+ + + +
diff --git a/static/web_panel/pages/dashboard.js b/static/web_panel/pages/dashboard.js new file mode 100644 index 0000000..7dd2355 --- /dev/null +++ b/static/web_panel/pages/dashboard.js @@ -0,0 +1,105 @@ +window.DashboardModule = { + charts: {}, + init: () => { + // 1. 初始化图表实例 + window.DashboardModule.charts = { + mem: new MiniChart('chart-mem', '#9ece6a'), + cpu: new MiniChart('chart-cpu', '#7aa2f7'), + net: new MiniChart('chart-net', '#e0af68'), + proc_mem: new MiniChart('chart-proc-mem', '#f7768e') + }; + + // 2. 获取并填充右侧固定信息 (只获取一次即可,除非重启) + fetchSystemStaticInfo(); + + // 3. 启动实时数据轮询 + fetchDash(); + window._dashInterval = setInterval(fetchDash, 2000); // 2秒刷新 + }, + destroy: () => { + clearInterval(window._dashInterval); + window.DashboardModule.charts = {}; + } +}; + +async function fetchSystemStaticInfo() { + try { + const sys = await fetch('./api/system', {credentials:'include'}).then(r => r.json()); + + // 硬件信息 + if(sys.platform) { + document.getElementById('info-os').textContent = sys.platform.system || '--'; + document.getElementById('info-arch').textContent = sys.platform.machine || '--'; + document.getElementById('info-env').textContent = sys.platform.env || 'Standard'; + } + if(sys.cpu) { + const c = sys.cpu.cores || 0; + document.getElementById('info-cores').textContent = `${c} / ${c}`; // Android下通常逻辑核=物理核 + } + if(sys.memory) { + document.getElementById('info-mem-total').textContent = sys.memory.total_gb + ' GB'; + } + + // 框架信息 (部分需结合 API) + const host = window.location.hostname + (window.location.port ? ':'+window.location.port : ''); + document.getElementById('info-addr').textContent = host; + + } catch(e) {} +} + +async function fetchDash() { + try { + const fw = await fetch('./api/framework', {credentials:'include'}).then(r => r.json()); + const sys = await fetch('./api/system', {credentials:'include'}).then(r => r.json()); + + // --- 左侧动态数据更新 --- + if(fw) { + document.getElementById('d-uptime').textContent = formatUptime(fw.uptime || 0); + document.getElementById('d-plugins').textContent = fw.plugins || 0; + if(document.getElementById('d-ver-badge')) document.getElementById('d-ver-badge').textContent = 'v' + (fw.version||'?'); + } + + if(sys) { + // 内存 + const m = sys.memory?.percent || 0; + document.getElementById('d-mem').textContent = m + '%'; + window.DashboardModule.charts.mem.update(m); + + // CPU (兼容 Android null 情况) + let cpuVal = sys.cpu?.percent; + if (cpuVal === null || cpuVal === undefined) { + const load = sys.cpu?.load_avg?.[0] || 0; + const cores = sys.cpu?.cores || 1; + cpuVal = Math.min(100, (load / cores) * 100); + } + document.getElementById('d-cpu').textContent = Math.round(cpuVal) + '%'; + window.DashboardModule.charts.cpu.update(cpuVal); + + // 网络 (RX 总量) + const netRx = sys.network?.rx || 0; + document.getElementById('d-net').textContent = netRx + ' MB'; + window.DashboardModule.charts.net.update(netRx); // 图表显示总流量趋势 + + // 进程内存 + const pm = sys.process?.memory_mb || 0; + document.getElementById('d-proc-mem').textContent = pm + ' MB'; + window.DashboardModule.charts.proc_mem.update(pm); + + // --- 右侧动态数据更新 --- + if(sys.process) { + document.getElementById('info-pid').textContent = sys.process.pid || '--'; + } + if(sys.cpu?.load_avg) { + document.getElementById('info-load').textContent = sys.cpu.load_avg[2].toFixed(2); + } + } + } catch(e) { console.warn("Dashboard fetch error", e); } +} + +// 辅助:秒数转时间格式 +function formatUptime(seconds) { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + return `${h}h ${m}m ${s}s`; +} diff --git a/static/web_panel/pages/logs.html b/static/web_panel/pages/logs.html new file mode 100644 index 0000000..a524018 --- /dev/null +++ b/static/web_panel/pages/logs.html @@ -0,0 +1,4 @@ +
+
📡 LIVE LOG STREAM (WebSocket)
+
+
diff --git a/static/web_panel/pages/logs.js b/static/web_panel/pages/logs.js new file mode 100644 index 0000000..743dfe6 --- /dev/null +++ b/static/web_panel/pages/logs.js @@ -0,0 +1,24 @@ +window.LogsModule = { + ws: null, + init: () => { + const box = document.getElementById('log-box'); + const connect = () => { + window.LogsModule.ws = new WebSocket(`ws://${location.host}${window.location.pathname.replace(/\/$/,'')}/api/logs/ws`); + window.LogsModule.ws.onopen = () => box.innerHTML += `
🟢 Connected
`; + window.LogsModule.ws.onmessage = e => { + try { + const d = JSON.parse(e.data); + if(d.type === 'log') { + const cls = d.level === 'ERROR' ? 'log-ERROR' : d.level === 'WARNING' ? 'log-WARNING' : 'log-INFO'; + const t = d.timestamp ? new Date(d.timestamp*1000).toLocaleTimeString() : '--'; + box.innerHTML += `
${t}[${d.level}] ${d.message}
`; + box.scrollTop = box.scrollHeight; + } + } catch(e){} + }; + window.LogsModule.ws.onclose = setTimeout(connect, 3000); + }; + connect(); + }, + destroy: () => window.LogsModule.ws?.close() +}; diff --git a/static/web_panel/pages/plugins.html b/static/web_panel/pages/plugins.html new file mode 100644 index 0000000..66ec24c --- /dev/null +++ b/static/web_panel/pages/plugins.html @@ -0,0 +1,5 @@ +
+

📦 插件管理

+ +
+
加载中...
diff --git a/static/web_panel/pages/plugins.js b/static/web_panel/pages/plugins.js new file mode 100644 index 0000000..14348c4 --- /dev/null +++ b/static/web_panel/pages/plugins.js @@ -0,0 +1,36 @@ +window.PluginsModule = { + init: () => window.PluginsModule.refresh(), + refresh: async () => { + const list = document.getElementById('plugin-list'); + if(!list) return; + list.innerHTML = '加载中...'; + try { + const res = await fetch('./api/plugins', {credentials:'include'}); + const data = await res.json(); + if(!data.plugins?.length) { list.innerHTML = '
暂无已加载插件
'; return; } + + list.innerHTML = data.plugins.map(p => ` +
+
+

${p.name} ${p.running?'RUNNING':'STOPPED'}

+

v${p.version||'1.0.0'} | ${p.enabled?'已启用':'已禁用'}

+
+
+ ${p.running + ? `` + : `` + } + +
+
+ `).join(''); + } catch(e) { list.innerHTML = '加载失败'; } + }, + act: async (name, action) => { + try { + await fetch(`./api/plugins/${name}/${action}`, {method:'POST', credentials:'include'}); + setTimeout(window.PluginsModule.refresh, 500); + } catch(e) { alert('操作失败'); } + }, + destroy: () => {} +}; diff --git a/templates/plugin/__init__.py.template b/templates/plugin/__init__.py.template new file mode 100644 index 0000000..bdb0cbf --- /dev/null +++ b/templates/plugin/__init__.py.template @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import asyncio +from typing import Dict, Any +from aiohttp import web + +try: + from fmfuncs.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 + +try: + from bridges.plugin_network_bridge import PluginNetworkBridge +except ImportError: + class PluginNetworkBridge: + def __init__(self, *args): pass + async def register_http_route(self, *a, **k): pass + async def register_websocket(self, *a, **k): pass + async def broadcast_websocket(self, *a, **k): pass + def get_network_info(self): return {'plugin_name': '', 'registered_routes': [], 'websocket_handlers': [], 'base_url': '不可用'} + async def setup_data_transfer(self, *a, **k): pass + +logger = logging.getLogger(__name__) + +class Plugin: + """${plugin_name} 插件""" + + 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}") + + async def initialize(self): + logger.info(f"初始化插件: {self.plugin_name}") + self.is_running = True + logger.debug(f"插件初始化完成: {self.plugin_name}") + + async def shutdown(self): + logger.info(f"关闭插件: {self.plugin_name}") + self.is_running = False + self.bridge.cleanup_plugin_subscriptions(self.plugin_name) + logger.debug(f"插件关闭完成: {self.plugin_name}") diff --git a/templates/plugin/config.yaml.template b/templates/plugin/config.yaml.template new file mode 100644 index 0000000..c14ea49 --- /dev/null +++ b/templates/plugin/config.yaml.template @@ -0,0 +1,9 @@ +name: "${plugin_name}" +version: "1.0.0" +description: "${description}" +author: "${author}" + +settings: + enabled: true + auto_start: true + log_level: "INFO" diff --git a/templates/plugin/permissions.yaml.template b/templates/plugin/permissions.yaml.template new file mode 100644 index 0000000..bfb8a33 --- /dev/null +++ b/templates/plugin/permissions.yaml.template @@ -0,0 +1,10 @@ +plugin_name: "${plugin_name}" +permissions: + - "plugin.${plugin_name}.read" + - "plugin.${plugin_name}.write" + - "plugin.${plugin_name}.execute" + +permission_descriptions: + plugin.${plugin_name}.read: "读取${plugin_name}插件数据" + plugin.${plugin_name}.write: "写入${plugin_name}插件数据" + plugin.${plugin_name}.execute: "执行${plugin_name}插件操作" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2f97d27 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,4 @@ +import sys +from pathlib import Path +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..6e9712c --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,12 @@ +import pytest,os +from services.auth_service import AuthService +class TestAuthService: + def test_hash_consistent(self): + s=AuthService({});assert s._hash_password("x")==s._hash_password("x") + def test_hash_len(self): + assert len(AuthService({})._hash_password("x"))==64 + def test_invalid_login(self): + assert AuthService({}).authenticate_user("admin","WRONG") is None + def test_valid_login(self): + pw=os.environ.get("SENSU_ADMIN_PASSWORD","admin123") + assert AuthService({}).authenticate_user("admin",pw) is not None diff --git a/tests/test_service_manager.py b/tests/test_service_manager.py new file mode 100644 index 0000000..f69c5c5 --- /dev/null +++ b/tests/test_service_manager.py @@ -0,0 +1,49 @@ +import pytest, asyncio +from service_manager import ServiceManager + +class MockService: + def __init__(self, name="mock"): + self.name = name + self.shutdown_called = False + def shutdown(self): + self.shutdown_called = True + return True + +class AsyncMockService: + def __init__(self, name="async_mock"): + self.name = name + self.shutdown_called = False + async def shutdown(self): + self.shutdown_called = True + +class TestServiceManager: + def test_register_get(self): + sm = ServiceManager(); s = MockService() + sm.register_service("t", s) + assert sm.get_service("t") is s + + def test_missing_raises(self): + with pytest.raises(ValueError): + ServiceManager().get_service("x") + + def test_has_service(self): + sm = ServiceManager() + sm.register_service("a", MockService()) + assert sm.has_service("a") and not sm.has_service("b") + + def test_shutdown_sync(self): + sm = ServiceManager(); s = MockService() + sm.register_service("s", s) + sm.shutdown_all() + assert s.shutdown_called + + def test_health(self): + sm = ServiceManager() + sm.register_service("h", MockService(), health_check=lambda: True) + assert asyncio.run(sm.check_health())["h"] + + def test_startup_order(self): + sm = ServiceManager() + sm.register_service("1", MockService()) + sm.register_service("2", MockService()) + assert sm.startup_order == ["1", "2"] diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..6f90ab5 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +框架功能集 - 提供各种工具函数和工具类 +""" + +from .file_utils import FileUtils +from .config_utils import ConfigUtils +from .validation_utils import ValidationUtils +from .network_utils import NetworkUtils +from .plugin_utils import PluginUtils + +__all__ = [ + 'FileUtils', + 'ConfigUtils', + 'ValidationUtils', + 'NetworkUtils', + 'PluginUtils' +] diff --git a/utils/config_utils.py b/utils/config_utils.py new file mode 100644 index 0000000..f3b4175 --- /dev/null +++ b/utils/config_utils.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import yaml +import json +from pathlib import Path +from typing import Dict, Any, Optional +import copy + +logger = logging.getLogger(__name__) + +class ConfigUtils: + """配置工具类""" + + @staticmethod + def load_yaml_config(file_path: str, default_config: Dict = None) -> Dict: + """加载YAML配置文件""" + try: + path = Path(file_path) + if not path.exists(): + logger.warning(f"YAML配置文件不存在: {file_path}") + if default_config: + ConfigUtils.save_yaml_config(file_path, default_config) + logger.debug(f"已创建默认YAML配置: {file_path}") + return default_config or {} + + with open(path, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + + logger.debug(f"YAML配置加载成功: {file_path}") + return config or {} + + except yaml.YAMLError as e: + logger.error(f"YAML配置文件解析错误 {file_path}: {str(e)}", exc_info=True) + return default_config or {} + except Exception as e: + logger.error(f"加载YAML配置时出错 {file_path}: {str(e)}", exc_info=True) + return default_config or {} + + @staticmethod + def save_yaml_config(file_path: str, config: Dict) -> bool: + """保存YAML配置文件""" + try: + path = Path(file_path) + path.parent.mkdir(parents=True, exist_ok=True) + + with open(path, 'w', encoding='utf-8') as f: + yaml.dump(config, f, default_flow_style=False, allow_unicode=True, indent=2) + + logger.debug(f"YAML配置保存成功: {file_path}") + return True + + except Exception as e: + logger.error(f"保存YAML配置时出错 {file_path}: {str(e)}", exc_info=True) + return False + + @staticmethod + def load_json_config(file_path: str, default_config: Dict = None) -> Dict: + """加载JSON配置文件""" + try: + path = Path(file_path) + if not path.exists(): + logger.warning(f"JSON配置文件不存在: {file_path}") + if default_config: + ConfigUtils.save_json_config(file_path, default_config) + logger.debug(f"已创建默认JSON配置: {file_path}") + return default_config or {} + + with open(path, 'r', encoding='utf-8') as f: + config = json.load(f) + + logger.debug(f"JSON配置加载成功: {file_path}") + return config + + except json.JSONDecodeError as e: + logger.error(f"JSON配置文件解析错误 {file_path}: {str(e)}", exc_info=True) + return default_config or {} + except Exception as e: + logger.error(f"加载JSON配置时出错 {file_path}: {str(e)}", exc_info=True) + return default_config or {} + + @staticmethod + def save_json_config(file_path: str, config: Dict) -> bool: + """保存JSON配置文件""" + try: + path = Path(file_path) + path.parent.mkdir(parents=True, exist_ok=True) + + with open(path, 'w', encoding='utf-8') as f: + json.dump(config, f, ensure_ascii=False, indent=2) + + logger.debug(f"JSON配置保存成功: {file_path}") + return True + + except Exception as e: + logger.error(f"保存JSON配置时出错 {file_path}: {str(e)}", exc_info=True) + return False + + @staticmethod + def get_nested_value(config: Dict, key_path: str, default: Any = None) -> Any: + """获取嵌套配置值""" + try: + keys = key_path.split('.') + current = config + + for key in keys: + if isinstance(current, dict) and key in current: + current = current[key] + else: + logger.debug(f"配置键不存在: {key_path}") + return default + + logger.debug(f"获取嵌套配置值: {key_path} -> {current}") + return current + + except Exception as e: + logger.error(f"获取嵌套配置值时出错 {key_path}: {str(e)}", exc_info=True) + return default + + @staticmethod + def set_nested_value(config: Dict, key_path: str, value: Any) -> bool: + """设置嵌套配置值""" + try: + keys = key_path.split('.') + current = config + + # 遍历到最后一个键的父级 + for key in keys[:-1]: + if key not in current or not isinstance(current[key], dict): + current[key] = {} + current = current[key] + + # 设置值 + current[keys[-1]] = value + logger.debug(f"设置嵌套配置值: {key_path} -> {value}") + return True + + except Exception as e: + logger.error(f"设置嵌套配置值时出错 {key_path}: {str(e)}", exc_info=True) + return False + + @staticmethod + def merge_configs(base_config: Dict, override_config: Dict) -> Dict: + """合并配置(深度合并)""" + try: + result = copy.deepcopy(base_config) + + for key, value in override_config.items(): + if (key in result and + isinstance(result[key], dict) and + isinstance(value, dict)): + # 递归合并字典 + result[key] = ConfigUtils.merge_configs(result[key], value) + else: + # 直接覆盖 + result[key] = copy.deepcopy(value) + + logger.debug("配置合并完成") + return result + + except Exception as e: + logger.error(f"合并配置时出错: {str(e)}", exc_info=True) + return base_config + + @staticmethod + def validate_config_structure(config: Dict, schema: Dict) -> bool: + """验证配置结构""" + try: + def _validate(current_config, current_schema, path=""): + for key, expected_type in current_schema.items(): + full_path = f"{path}.{key}" if path else key + + if key not in current_config: + logger.error(f"配置缺少必要字段: {full_path}") + return False + + actual_value = current_config[key] + expected_type_name = expected_type.__name__ if hasattr(expected_type, '__name__') else str(expected_type) + + if not isinstance(actual_value, expected_type): + logger.error(f"配置类型错误 {full_path}: 期望 {expected_type_name}, 实际 {type(actual_value).__name__}") + return False + + # 如果是字典且schema有嵌套定义,递归验证 + if (isinstance(expected_type, dict) and + isinstance(actual_value, dict)): + if not _validate(actual_value, expected_type, full_path): + return False + + return True + + result = _validate(config, schema) + if result: + logger.debug("配置结构验证通过") + else: + logger.error("配置结构验证失败") + + return result + + except Exception as e: + logger.error(f"验证配置结构时出错: {str(e)}", exc_info=True) + return False diff --git a/utils/file_utils.py b/utils/file_utils.py new file mode 100644 index 0000000..9ac3ac8 --- /dev/null +++ b/utils/file_utils.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import os +import shutil +from pathlib import Path +from typing import List, Optional +import hashlib + +logger = logging.getLogger(__name__) + +class FileUtils: + """文件操作工具类""" + + @staticmethod + def ensure_directory(directory_path: str) -> bool: + """确保目录存在""" + try: + path = Path(directory_path) + path.mkdir(parents=True, exist_ok=True) + logger.debug(f"目录已确保存在: {directory_path}") + return True + except Exception as e: + logger.error(f"创建目录时出错 {directory_path}: {str(e)}", exc_info=True) + return False + + @staticmethod + def safe_write(file_path: str, content: str, backup: bool = True) -> bool: + """安全写入文件(支持备份)""" + try: + path = Path(file_path) + + # 备份原文件 + if backup and path.exists(): + backup_path = path.with_suffix(path.suffix + '.bak') + shutil.copy2(path, backup_path) + logger.debug(f"文件已备份: {backup_path}") + + # 写入新内容 + with open(path, 'w', encoding='utf-8') as f: + f.write(content) + + logger.debug(f"文件写入成功: {file_path}, 大小: {len(content)} 字节") + return True + + except Exception as e: + logger.error(f"写入文件时出错 {file_path}: {str(e)}", exc_info=True) + return False + + @staticmethod + def safe_read(file_path: str, default: str = "") -> str: + """安全读取文件""" + try: + path = Path(file_path) + if not path.exists(): + logger.warning(f"文件不存在: {file_path}") + return default + + with open(path, 'r', encoding='utf-8') as f: + content = f.read() + + logger.debug(f"文件读取成功: {file_path}, 大小: {len(content)} 字节") + return content + + except Exception as e: + logger.error(f"读取文件时出错 {file_path}: {str(e)}", exc_info=True) + return default + + @staticmethod + def list_files(directory: str, pattern: str = "*", recursive: bool = False) -> List[Path]: + """列出目录中的文件""" + try: + path = Path(directory) + if not path.exists(): + logger.warning(f"目录不存在: {directory}") + return [] + + if recursive: + files = list(path.rglob(pattern)) + else: + files = list(path.glob(pattern)) + + # 过滤出文件(非目录) + files = [f for f in files if f.is_file()] + + logger.debug(f"列出文件: {directory}, 模式: {pattern}, 找到 {len(files)} 个文件") + return files + + except Exception as e: + logger.error(f"列出文件时出错 {directory}: {str(e)}", exc_info=True) + return [] + + @staticmethod + def calculate_file_hash(file_path: str, algorithm: str = "md5") -> Optional[str]: + """计算文件哈希值""" + try: + path = Path(file_path) + if not path.exists(): + logger.warning(f"文件不存在: {file_path}") + return None + + hash_func = getattr(hashlib, algorithm)() + + with open(path, 'rb') as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_func.update(chunk) + + file_hash = hash_func.hexdigest() + logger.debug(f"文件哈希计算完成: {file_path} -> {algorithm}:{file_hash}") + return file_hash + + except Exception as e: + logger.error(f"计算文件哈希时出错 {file_path}: {str(e)}", exc_info=True) + return None + + @staticmethod + def cleanup_old_files(directory: str, pattern: str, keep_count: int) -> int: + """清理旧文件,保留指定数量的最新文件""" + try: + files = FileUtils.list_files(directory, pattern) + if len(files) <= keep_count: + logger.debug(f"文件数量未超过限制,无需清理: {directory}") + return 0 + + # 按修改时间排序 + files.sort(key=lambda x: x.stat().st_mtime, reverse=True) + + # 删除旧文件 + removed_count = 0 + for file_to_remove in files[keep_count:]: + try: + file_to_remove.unlink() + removed_count += 1 + logger.debug(f"删除旧文件: {file_to_remove}") + except Exception as e: + logger.error(f"删除文件时出错 {file_to_remove}: {str(e)}", exc_info=True) + + logger.info(f"文件清理完成: {directory}, 删除 {removed_count} 个文件") + return removed_count + + except Exception as e: + logger.error(f"清理旧文件时出错 {directory}: {str(e)}", exc_info=True) + return 0 diff --git a/utils/network_utils.py b/utils/network_utils.py new file mode 100644 index 0000000..d62d3ef --- /dev/null +++ b/utils/network_utils.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import socket +import asyncio +import re +from typing import Optional, Tuple +import aiohttp +import ssl + +logger = logging.getLogger(__name__) + +class NetworkUtils: + """网络工具类""" + + @staticmethod + async def check_port_available(host: str, port: int) -> bool: + """检查端口是否可用""" + try: + # 尝试创建socket连接 + reader, writer = await asyncio.open_connection(host, port) + writer.close() + await writer.wait_closed() + logger.debug(f"端口 {host}:{port} 已被占用") + return False + except (ConnectionRefusedError, asyncio.TimeoutError): + logger.debug(f"端口 {host}:{port} 可用") + return True + except Exception as e: + logger.error(f"检查端口可用性时出错 {host}:{port}: {str(e)}", exc_info=True) + return False + + @staticmethod + async def find_available_port(host: str = "localhost", start_port: int = 8000, + max_attempts: int = 100) -> Optional[int]: + """查找可用端口""" + try: + for port in range(start_port, start_port + max_attempts): + if await NetworkUtils.check_port_available(host, port): + logger.debug(f"找到可用端口: {host}:{port}") + return port + logger.warning(f"在范围 {start_port}-{start_port + max_attempts} 内未找到可用端口") + return None + except Exception as e: + logger.error(f"查找可用端口时出错: {str(e)}", exc_info=True) + return None + + @staticmethod + async def http_request(url: str, method: str = "GET", headers: dict = None, + data: dict = None, timeout: int = 30) -> Tuple[bool, dict]: + """发送HTTP请求""" + try: + logger.debug(f"发送HTTP请求: {method} {url}") + + timeout_obj = aiohttp.ClientTimeout(total=timeout) + + async with aiohttp.ClientSession(timeout=timeout_obj) as session: + async with session.request(method, url, headers=headers, json=data) as response: + response_data = await response.text() + + result = { + "status": response.status, + "headers": dict(response.headers), + "data": response_data, + "url": str(response.url) + } + + logger.debug(f"HTTP请求完成: {method} {url} -> 状态 {response.status}") + return True, result + + except asyncio.TimeoutError: + logger.error(f"HTTP请求超时: {method} {url}") + return False, {"error": "请求超时"} + except aiohttp.ClientError as e: + logger.error(f"HTTP客户端错误: {method} {url} -> {str(e)}") + return False, {"error": str(e)} + except Exception as e: + logger.error(f"HTTP请求时出错: {method} {url} -> {str(e)}", exc_info=True) + return False, {"error": str(e)} + + @staticmethod + def get_local_ip() -> str: + """获取本地IP地址""" + try: + # 创建一个socket连接来获取本地IP + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.connect(("8.8.8.8", 80)) + local_ip = s.getsockname()[0] + + logger.debug(f"获取本地IP: {local_ip}") + return local_ip + except Exception as e: + logger.error(f"获取本地IP时出错: {str(e)}", exc_info=True) + return "127.0.0.1" + + @staticmethod + def is_valid_hostname(hostname: str) -> bool: + """验证主机名格式""" + try: + if len(hostname) > 255: + return False + + if hostname[-1] == ".": + hostname = hostname[:-1] + + allowed = re.compile(r"(?!-)[A-Z\d-]{1,63}(? bool: + """验证插件结构""" + try: + logger.debug(f"验证插件结构: {plugin_path}") + + required_files = [ + "__init__.py", + "config.yaml", + "permissions.yaml" + ] + + # 检查必需文件 + for file_name in required_files: + if not (plugin_path / file_name).exists(): + logger.error(f"插件缺少必需文件: {file_name}") + return False + + # 检查主模块是否有Plugin类 + try: + spec = importlib.util.spec_from_file_location("plugin_module", plugin_path / "__init__.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + if not hasattr(module, 'Plugin'): + logger.error("插件主模块缺少Plugin类") + return False + + # 检查Plugin类是否有必要方法 + plugin_class = module.Plugin + required_methods = ['initialize', 'shutdown'] + + for method_name in required_methods: + if not hasattr(plugin_class, method_name): + logger.error(f"Plugin类缺少必要方法: {method_name}") + return False + + logger.debug(f"插件结构验证通过: {plugin_path.name}") + return True + + except Exception as e: + logger.error(f"验证插件类时出错: {str(e)}", exc_info=True) + return False + + except Exception as e: + logger.error(f"验证插件结构时出错: {str(e)}", exc_info=True) + return False + + @staticmethod + def get_plugin_dependencies(plugin_path: Path) -> List[str]: + """获取插件依赖""" + try: + config_file = plugin_path / "config.yaml" + if not config_file.exists(): + return [] + + import yaml + with open(config_file, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + + dependencies = config.get('dependencies', []) + if isinstance(dependencies, list): + logger.debug(f"获取插件依赖: {plugin_path.name} -> {dependencies}") + return dependencies + else: + logger.warning(f"插件依赖格式错误: {plugin_path.name}") + return [] + + except Exception as e: + logger.error(f"获取插件依赖时出错: {str(e)}", exc_info=True) + return [] + + @staticmethod + def scan_plugin_methods(plugin_instance) -> Dict[str, List[str]]: + """扫描插件方法""" + try: + logger.debug(f"扫描插件方法: {type(plugin_instance).__name__}") + + methods_info = { + "public_methods": [], + "private_methods": [], + "async_methods": [], + "event_handlers": [] + } + + for name, method in inspect.getmembers(plugin_instance, predicate=inspect.ismethod): + # 跳过特殊方法 + if name.startswith('_') and not name.startswith('__'): + methods_info["private_methods"].append(name) + elif not name.startswith('_'): + methods_info["public_methods"].append(name) + + # 检查是否为异步方法 + if inspect.iscoroutinefunction(method): + methods_info["async_methods"].append(name) + + # 检查是否为事件处理器 + if name.startswith('handle_') or name.startswith('on_'): + methods_info["event_handlers"].append(name) + + logger.debug(f"插件方法扫描完成: 公共{len(methods_info['public_methods'])}个, 私有{len(methods_info['private_methods'])}个") + return methods_info + + except Exception as e: + logger.error(f"扫描插件方法时出错: {str(e)}", exc_info=True) + return {} + + @staticmethod + def create_plugin_skeleton(plugin_name: str, plugin_path: Path) -> bool: + """创建插件骨架""" + try: + logger.debug(f"创建插件骨架: {plugin_name} -> {plugin_path}") + + # 创建插件目录 + plugin_path.mkdir(parents=True, exist_ok=True) + + # 创建主模块文件 + init_content = '''#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import asyncio +from typing import Dict, Any + +logger = logging.getLogger(__name__) + +class Plugin: + """{plugin_name} 插件""" + + def __init__(self, plugin_name: str, config: Dict, bridge): + self.plugin_name = plugin_name + self.config = config + self.bridge = bridge + self.is_running = False + logger.debug(f"插件初始化: {{plugin_name}}") + + async def initialize(self): + """初始化插件""" + try: + logger.info(f"初始化插件: {{self.plugin_name}}") + + # 在这里注册事件处理器和命令 + # 示例: self.bridge.subscribe_plugin(self.plugin_name, "event.name", self.handler) + + 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 shutdown(self): + """关闭插件""" + try: + logger.info(f"关闭插件: {{self.plugin_name}}") + self.is_running = False + + # 清理资源 + 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) + + # 在这里添加你的插件方法 + async def example_method(self, message: str) -> str: + """示例方法""" + try: + logger.debug(f"插件方法调用: {{message}}") + return f"插件响应: {{message}}" + except Exception as e: + logger.error(f"插件方法调用出错: {{str(e)}}", exc_info=True) + raise +'''.format(plugin_name=plugin_name) + + with open(plugin_path / "__init__.py", 'w', encoding='utf-8') as f: + f.write(init_content) + + # 创建配置文件 + config_content = f'''# {plugin_name} 插件配置 +name: "{plugin_name}" +version: "1.0.0" +description: "{plugin_name} 插件描述" +author: "插件作者" + +# 插件特定配置 +settings: + enabled: true + auto_start: true + log_level: "INFO" + +# 依赖配置 +dependencies: [] +''' + + with open(plugin_path / "config.yaml", 'w', encoding='utf-8') as f: + f.write(config_content) + + # 创建权限文件 + permissions_content = f'''# {plugin_name} 插件权限申请 +plugin_name: "{plugin_name}" +permissions: + - "plugin.{plugin_name}.read" + - "plugin.{plugin_name}.write" + +# 权限说明 +permission_descriptions: + plugin.{plugin_name}.read: "读取{plugin_name}插件数据" + plugin.{plugin_name}.write: "写入{plugin_name}插件数据" +''' + + with open(plugin_path / "permissions.yaml", 'w', encoding='utf-8') as f: + f.write(permissions_content) + + logger.info(f"插件骨架创建完成: {plugin_name}") + return True + + except Exception as e: + logger.error(f"创建插件骨架时出错: {str(e)}", exc_info=True) + return False + + @staticmethod + def validate_plugin_permissions(plugin_path: Path, requested_permissions: List[str]) -> bool: + """验证插件权限申请""" + try: + logger.debug(f"验证插件权限: {plugin_path.name}") + + # 检查权限格式 + for permission in requested_permissions: + if not isinstance(permission, str): + logger.error(f"权限格式错误: {permission}") + return False + + # 检查权限命名规范 + if not re.match(r'^[a-z][a-z0-9_.]*$', permission): + logger.error(f"权限命名不规范: {permission}") + return False + + logger.debug(f"插件权限验证通过: {len(requested_permissions)} 个权限") + return True + + except Exception as e: + logger.error(f"验证插件权限时出错: {str(e)}", exc_info=True) + return False \ No newline at end of file diff --git a/utils/validation_utils.py b/utils/validation_utils.py new file mode 100644 index 0000000..7a07da7 --- /dev/null +++ b/utils/validation_utils.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import re +import os +from typing import Any, List, Optional, Callable, Dict +from urllib.parse import urlparse +import ipaddress + +logger = logging.getLogger(__name__) + +class ValidationUtils: + """验证工具类""" + + @staticmethod + def is_valid_email(email: str) -> bool: + """验证邮箱格式""" + try: + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + result = bool(re.match(pattern, email)) + logger.debug(f"邮箱验证: {email} -> {result}") + return result + except Exception as e: + logger.error(f"验证邮箱时出错: {str(e)}", exc_info=True) + return False + + @staticmethod + def is_valid_url(url: str) -> bool: + """验证URL格式""" + try: + result = urlparse(url) + is_valid = all([result.scheme, result.netloc]) + logger.debug(f"URL验证: {url} -> {is_valid}") + return is_valid + except Exception as e: + logger.error(f"验证URL时出错: {str(e)}", exc_info=True) + return False + + @staticmethod + def is_valid_ip(ip: str) -> bool: + """验证IP地址格式""" + try: + ipaddress.ip_address(ip) + logger.debug(f"IP地址验证: {ip} -> True") + return True + except ValueError: + logger.debug(f"IP地址验证: {ip} -> False") + return False + except Exception as e: + logger.error(f"验证IP地址时出错: {str(e)}", exc_info=True) + return False + + @staticmethod + def is_valid_port(port: int) -> bool: + """验证端口号""" + try: + is_valid = 1 <= port <= 65535 + logger.debug(f"端口验证: {port} -> {is_valid}") + return is_valid + except Exception as e: + logger.error(f"验证端口时出错: {str(e)}", exc_info=True) + return False + + @staticmethod + def validate_string(value: Any, min_length: int = 0, max_length: int = None, + pattern: str = None) -> bool: + """验证字符串""" + try: + if not isinstance(value, str): + logger.debug(f"字符串验证失败: 不是字符串类型") + return False + + if len(value) < min_length: + logger.debug(f"字符串验证失败: 长度小于 {min_length}") + return False + + if max_length and len(value) > max_length: + logger.debug(f"字符串验证失败: 长度大于 {max_length}") + return False + + if pattern and not re.match(pattern, value): + logger.debug(f"字符串验证失败: 不匹配模式 {pattern}") + return False + + logger.debug(f"字符串验证通过: 长度 {len(value)}") + return True + + except Exception as e: + logger.error(f"验证字符串时出错: {str(e)}", exc_info=True) + return False + + @staticmethod + def validate_number(value: Any, min_value: float = None, max_value: float = None) -> bool: + """验证数字""" + try: + if not isinstance(value, (int, float)): + # 尝试转换 + try: + value = float(value) + except (ValueError, TypeError): + logger.debug(f"数字验证失败: 无法转换为数字") + return False + + if min_value is not None and value < min_value: + logger.debug(f"数字验证失败: 值小于 {min_value}") + return False + + if max_value is not None and value > max_value: + logger.debug(f"数字验证失败: 值大于 {max_value}") + return False + + logger.debug(f"数字验证通过: {value}") + return True + + except Exception as e: + logger.error(f"验证数字时出错: {str(e)}", exc_info=True) + return False + + @staticmethod + def validate_list(value: Any, min_length: int = 0, max_length: int = None, + item_validator: Callable = None) -> bool: + """验证列表""" + try: + if not isinstance(value, list): + logger.debug(f"列表验证失败: 不是列表类型") + return False + + if len(value) < min_length: + logger.debug(f"列表验证失败: 长度小于 {min_length}") + return False + + if max_length and len(value) > max_length: + logger.debug(f"列表验证失败: 长度大于 {max_length}") + return False + + if item_validator: + for i, item in enumerate(value): + if not item_validator(item): + logger.debug(f"列表验证失败: 第 {i} 项验证失败") + return False + + logger.debug(f"列表验证通过: 长度 {len(value)}") + return True + + except Exception as e: + logger.error(f"验证列表时出错: {str(e)}", exc_info=True) + return False + + @staticmethod + def validate_dict(value: Any, required_keys: List[str] = None, + key_validators: Dict[str, Callable] = None) -> bool: + """验证字典""" + try: + if not isinstance(value, dict): + logger.debug(f"字典验证失败: 不是字典类型") + return False + + # 检查必需键 + if required_keys: + for key in required_keys: + if key not in value: + logger.debug(f"字典验证失败: 缺少必需键 {key}") + return False + + # 检查键值验证器 + if key_validators: + for key, validator in key_validators.items(): + if key in value and not validator(value[key]): + logger.debug(f"字典验证失败: 键 {key} 的值验证失败") + return False + + logger.debug(f"字典验证通过: 键数 {len(value)}") + return True + + except Exception as e: + logger.error(f"验证字典时出错: {str(e)}", exc_info=True) + return False + + @staticmethod + def validate_file_path(file_path: str, check_exists: bool = True, + check_readable: bool = False, check_writable: bool = False) -> bool: + """验证文件路径""" + try: + from pathlib import Path + + path = Path(file_path) + + if check_exists and not path.exists(): + logger.debug(f"文件路径验证失败: 文件不存在") + return False + + if check_readable and not os.access(path, os.R_OK): + logger.debug(f"文件路径验证失败: 文件不可读") + return False + + if check_writable and not os.access(path, os.W_OK): + logger.debug(f"文件路径验证失败: 文件不可写") + return False + + logger.debug(f"文件路径验证通过: {file_path}") + return True + + except Exception as e: + logger.error(f"验证文件路径时出错: {str(e)}", exc_info=True) + return False