diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5958261
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+__pycache__/
+*.pyc
+*.pyo
+logs/
+*.log
+_patches_applied/
+*.bak.*
+.env
+*.swp
+*.swo
+*~
+.DS_Store
+docs/
+data/
+config/framework/base_config.yaml
+!config/framework/base_config.yaml.example
+*.orig
+*.clean
+config/framework/permission_rules.yaml
+config/permissions/
+config/plugins/
+config/services/
+!docs/API_Auth_Flow.md
+!docs/SenSu 插件开发详细指南.md
+!docs/SenSu 框架基本架构.md
+plugins/*/config.yaml
+!plugins/*/config.yaml.example
diff --git a/LICENSE b/LICENSE
index a95568b..a78b074 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,73 +1,201 @@
-Apache License
-Version 2.0, January 2004
-http://www.apache.org/licenses/
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
-TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-1. Definitions.
+ 1. Definitions.
-"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
-"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
-"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
-"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
-"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
-"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
-"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
-"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
-"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
-"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
-2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
-3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
-4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
- (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
- (b) You must cause any modified files to carry prominent notices stating that You changed the files; and
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
- (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
- (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
- You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
-5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
-6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
-7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
-8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
-9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
-END OF TERMS AND CONDITIONS
+ END OF TERMS AND CONDITIONS
-APPENDIX: How to apply the Apache License to your work.
+ APPENDIX: How to apply the Apache License to your work.
-To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Do not include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
-Copyright 2026 AskaEth
+ Copyright [2026] [AskaEth]
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
-http://www.apache.org/licenses/LICENSE-2.0
+ http://www.apache.org/licenses/LICENSE-2.0
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
index 235b28d..93cae00 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,114 @@
-# SenSu
+# 🐱 SenSu
-一个轻量化 Python TUI 插件框架。
\ No newline at end of file
+万能 Python TUI 项目管理器 — 插件化架构,任何项目都能挂载运行。
+
+[](LICENSE)
+[]()
+
+---
+
+## ✨ 特性
+
+- 🎨 **Textual TUI** — 系统监控面板 + 命令补全 + CLI 回退
+- 🔌 **插件系统** — 热加载、权限隔离、依赖解析、进程隔离
+- 📁 **文件管理器** — 全文件系统浏览、编辑、上传、右键菜单,Windows/Linux/macOS 兼容
+- 🗄️ **项目引擎** — 接管任何 Python/命令行项目 (subprocess + 日志 + stdin)
+- 🔀 **反向代理** — HTTP/WS 代理,本地/局域网/公网端口映射
+- 📊 **SQLite 持久化** — 插件状态、权限、审计日志统一存储
+- 🌐 **Web 管理面板** — aiohttp MD3 WebUI,日夜主题切换,WebSocket 实时推送
+- 🔐 **安全认证** — PBKDF2-SHA256、环境变量密码
+- 🐳 **Docker 部署** — Alpine 镜像,systemd 服务文件
+- 🧪 **28 个回归测试** — pytest,零失败
+- 🔑 **API Key 系统** — 权限模板(readonly/monitor/full)、过期时间、用量追踪
+- 🛡️ **Sentinel 集群监控** — 多设备实时性能仪表盘 + 节点管理
+- 🔄 **版本更新检测** — 自动检查/下载/备份/回滚
+- 🛡️ **安全加固** — 全端点认证、CSRF防护、SSRF防护、错误脱敏
+- 🛠️ **插件 SDK** — 命令装饰器 + Web 面板 + API + SSE 推送 + 文件选择器
+
+## 🚀 快速开始
+
+```bash
+pip install -r requirements.txt
+./start.sh # 一键启动(依赖检测+后台)
+python main.py # TUI 模式
+python main.py --headless # 后台模式 (Web 面板 :4200)
+python -m pytest tests/ -v # 运行测试
+```
+
+## 🏗️ 架构
+
+```
+main.py → SenSuFramework (15+ 服务)
+├── UpdateService ← 版本检测 + 备份 + 回滚
+├── ProjectEngine ← 接管任何项目 (subprocess + 日志)
+├── ProxyService ← 反向代理 (HTTP+WS)
+├── PyEnvManager ← Python 版本 + venv + git clone
+├── SenSuDB ← SQLite 持久化 (5表)
+├── PluginWebMixin ← 插件 Web 控制面板 SDK
+├── PluginService ← 热加载 + 状态追踪
+├── SenSuDB ← SQLite 5 表
+├── TuiService ← Textual + 系统监控
+├── InternetService ← HTTP :4200 + WS :4240
+└── WebPanelManager ← /SenSu 管理面板 (路由 + WS + 文件管理)
+```
+
+## 📁 目录
+
+```
+SenSu/
+├── main.py, service_manager.py
+├── sdk/ plugin_command, plugin_status, plugin_error, plugin_web
+├── services/ 15+ 核心服务 + web_panel/ (路由 + 工具)
+├── bridges/ CoreBridge + PluginBridge
+├── plugins/ 插件目录 + example_plugin
+├── static/ MD3 CSS + JS + 页面
+├── deploy/ systemd + Dockerfile
+├── tests/ 28 个测试
+└── docs/ 开发文档
+```
+
+## 🌐 Web 面板
+
+```
+http://0.0.0.0:4200/SenSu/
+├── /dashboard 仪表盘 (WS 实时推送 CPU/内存/网速双线图)
+├── /pages/files 文件管理器 (全系统浏览/编辑/上传/右键菜单)
+├── /pages/projects 项目管理 (添加/启动/Git部署)
+├── /pages/proxy 反向代理管理
+├── /pages/plugins 插件管理 (启用/停用/重载)
+├── /pages/logs 实时日志 (WS 推送/500行上限/批量渲染)
+├── /pages/console Web 终端
+└── /plugin/{name} 插件控制面板
+```
+
+## 🔌 插件开发
+
+```python
+from sdk.plugin_command_decorator import plugin_command
+from sdk.plugin_web import PluginWebMixin
+
+class Plugin(PluginWebMixin):
+ def __init__(self, plugin_name=None, config=None, bridge=None):
+ PluginWebMixin.__init__(self)
+ self.plugin_name = plugin_name
+
+ async def initialize(self):
+ self.register_web_page("my_plugin", "My Panel", "
Hello
")
+
+ @plugin_command(name="hello")
+ async def cmd_hello(self, *args):
+ return "Hello World!"
+```
+
+## 🔧 环境变量
+
+| 变量 | 默认值 |
+|------|------|
+| `SENSU_ADMIN_PASSWORD` | `admin123` |
+| `SENSU_API_PASSWORD` | `api123` |
+| `SENSU_PANEL_USER` | `admin` |
+| `SENSU_PANEL_PASS` | `admin` |
+
+## 📄 许可证
+
+Apache License 2.0 · Copyright 2026 AskaEth
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 0000000..9bf52c1
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,49 @@
+# SenSu 开发路线图
+
+> 当前版本: Alpha 0.7.0
+> 更新: 2026-06-13
+
+---
+
+## 一、已完成 (v0.7.0)
+
+- [x] 13 服务异步框架 (init/log/tui/command/auth/internet/plugin/permission/api/shutdown/web_panel/bridge)
+- [x] Textual TUI 三栏界面 + CLI 回退
+- [x] 插件热加载 + 权限管理
+- [x] Web 管理面板 (aiohttp :4200)
+- [x] 消息桥接 (CoreBridge + PluginBridge + NetworkBridge)
+- [x] 插件 SDK (`sdk/`): PluginStatus, PluginError, plugin_command 装饰器
+- [x] 15 个回归测试
+- [x] 安全加固: PBKDF2-SHA256, 环境变量密码, 认证降级已移除
+- [x] Android 兼容: os.getloadavg(), /proc/net/dev
+- [x] Apache 2.0 许可证
+
+## 二、v0.2.2 — 打磨 ✅ 全部完成
+
+- [x] `--headless` 模式 — 纯后台运行,不启动 Textual TUI
+- [x] 调试服务器自启动 — `auto_start_scripts` 通用机制
+- [x] Web 面板日志 WebSocket 修复 — 动态 base 路径
+- [x] 插件热重载 — watchdog 监听 plugins/ 目录
+- [x] test_demo 插件完善 — echo + plugin_status 命令
+
+## 三、v0.3 — 项目管理 ✅ 全部完成
+
+- [x] 项目注册表 — `project.yaml` 声明, ProjectService 管理生命周期
+- [x] SQLite 持久化 — SenSuDB 5 表 (plugins/permissions/config_kv/audit_log)
+- [x] 插件依赖解析 — 拓扑排序, 循环依赖检测
+- [x] HTTP API 自动暴露 — PluginNetworkBridge.register_command_routes()
+
+## 四、v0.4 — TUI 仪表盘 ✅ 全部完成
+
+- [x] 系统监控面板 — CPU/MEM 彩色进度条 + 网络速率 (复用 SystemInfoCollector)
+- [x] 插件实时状态面板 — 名称/版本/命令数展示
+- [x] 命令补全 — Textual Suggester API, Tab 前缀匹配
+
+## 五、v0.5 — 生产就绪 ✅ 全部完成
+
+- [x] 插件进程隔离 — IsolatedPlugin (multiprocessing.Process) + PluginService 集成
+- [x] 插件索引仓库 — PluginIndex + `install ` 命令
+- [x] systemd 集成 — `deploy/sensu.service`
+- [x] Docker 化 — `deploy/Dockerfile` (Alpine, <100MB)
+
+---
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..9246a68
--- /dev/null
+++ b/bridges/plugin_bridge.py
@@ -0,0 +1,266 @@
+#!/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初始化开始")
+
+
+ def subscribe_plugin(self, topic: str, handler, plugin_name: str = None):
+ if plugin_name:
+ 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(handler)
+ self.core_bridge.subscribe(topic, handler)
+ logger.debug(f"plugin {plugin_name or '?'} subscribed: {topic}")
+ 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..f84577c
--- /dev/null
+++ b/bridges/plugin_network_bridge.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import logging
+import asyncio
+import inspect
+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_command_routes(self, plugin_instance, require_auth: bool = True):
+ """自动扫描插件 @plugin_command/cmd_* 方法并注册 REST 端点
+
+ 每个命令 → POST /api/plugin/{cmd_name}
+ 参数以 JSON body 传入: {"args": [...]}
+ 返回: {"ok": true, "result": "..."} 或 {"ok": false, "error": "..."}
+ """
+ try:
+ import inspect
+
+ registered = 0
+ for attr_name in dir(plugin_instance):
+ if attr_name.startswith("__"):
+ continue
+
+ method = getattr(plugin_instance, attr_name, None)
+ if not callable(method):
+ continue
+
+ # 识别 @plugin_command 装饰或 cmd_ 前缀
+ cmd_name = None
+ if hasattr(method, "_is_plugin_command"):
+ cmd_name = getattr(method, "_command_name", attr_name[4:] if attr_name.startswith("cmd_") else attr_name)
+ elif attr_name.startswith("cmd_"):
+ cmd_name = attr_name[4:]
+
+ if not cmd_name:
+ continue
+
+ route_path = f"/api/plugin/{cmd_name}"
+
+ # 创建闭包捕获 method 和 cmd_name
+ async def _make_handler(_method=method, _cmd_name=cmd_name):
+ from aiohttp import web
+
+ async def _handler(request):
+ try:
+ body = {}
+ try:
+ body = await request.json()
+ except Exception:
+ pass
+ args = body.get("args", [])
+ if isinstance(args, str):
+ args = [args]
+ kwargs = body.get("kwargs", {})
+ result = _method(*args, **kwargs)
+ if asyncio.iscoroutine(result):
+ result = await result
+ return web.json_response({"ok": True, "result": str(result)})
+ except Exception as e:
+ logger.error(f"命令 {_cmd_name} REST 调用失败: {e}")
+ return web.json_response({"ok": False, "error": str(e)}, status=500)
+
+ return _handler
+
+ await self.register_http_route(
+ route_path, await _make_handler(),
+ methods=["POST"], require_auth=require_auth,
+ )
+ logger.debug(f" 自动暴露 REST: POST {route_path}")
+ registered += 1
+
+ if registered:
+ logger.info(f"插件 {self.plugin_name} 自动暴露 {registered} 个命令 REST 端点")
+
+ except Exception as e:
+ logger.warning(f"自动注册命令路由失败 {self.plugin_name}: {e}")
+
+ 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.example b/config/framework/base_config.yaml.example
new file mode 100644
index 0000000..7a92c0f
--- /dev/null
+++ b/config/framework/base_config.yaml.example
@@ -0,0 +1,53 @@
+framework:
+ debug: true
+ name: SenSu
+ version: v0.8.0
+ branch: dev
+ repo_url: https://git.yeij.top/AskaEth/SenSu
+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
+ isolation: false
+# 自动启动脚本 — 框架启动时后台拉起
+auto_start_scripts:
+ enabled: true
+ scripts: []
+ # 示例:
+ # - name: cyrene_debug
+ # path: ~/cyrene_debug_server.py
+ # enabled: true
+ # args: []
+ # cwd: ~
+# API Keys — 服务器间通信凭证 (在 WebUI 设置页管理)
+# api_keys: []
+# TUI配置
+tui:
+ enabled: true
+ refresh_rate: 30
+ layout:
+ grid_rows: "4fr 5fr 1fr"
+ styles:
+ log_area: "border: solid green; overflow-y: auto;"
+ message_area: "border: solid yellow; overflow-y: auto;"
+ input_area: "border: solid red;"
+ log_display:
+ max_lines: 200
+# 互联网服务配置
+internet:
+ websocket:
+ host: "0.0.0.0"
+ port: 4240
+ http:
+ host: "0.0.0.0"
+ port: 4200
+panel:
+ entrance:
+ path: "/SenSu"
+ username: "admin"
+ password: "CHANGE_ME" # 首次启动请修改!
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/deploy/Dockerfile b/deploy/Dockerfile
new file mode 100644
index 0000000..d07f217
--- /dev/null
+++ b/deploy/Dockerfile
@@ -0,0 +1,10 @@
+FROM python:3.12-alpine
+WORKDIR /app
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+COPY . .
+RUN mkdir -p data logs
+EXPOSE 4200 4240
+ENV SENSU_ADMIN_PASSWORD=changeme
+ENV SENSU_API_PASSWORD=changeme
+CMD ["python3", "main.py", "--headless"]
diff --git a/deploy/sensu.service b/deploy/sensu.service
new file mode 100644
index 0000000..c7a8399
--- /dev/null
+++ b/deploy/sensu.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=SenSu Plugin Framework
+After=network.target
+
+[Service]
+Type=simple
+User=aska
+WorkingDirectory=/data/data/com.termux/files/home/Proj/SenSu_workspace
+ExecStart=/data/data/com.termux/files/usr/bin/python3 /data/data/com.termux/files/home/Proj/SenSu_workspace/start.py --headless
+Restart=on-failure
+RestartSec=5
+Environment=SENSU_ADMIN_PASSWORD=changeme
+Environment=SENSU_API_PASSWORD=changeme
+Environment=SENSU_PANEL_USER=admin
+Environment=SENSU_PANEL_PASS=changeme
+
+[Install]
+WantedBy=multi-user.target
diff --git a/docs/API_Auth_Flow.md b/docs/API_Auth_Flow.md
new file mode 100644
index 0000000..38c6384
--- /dev/null
+++ b/docs/API_Auth_Flow.md
@@ -0,0 +1,314 @@
+# SenSu API 认证与数据访问流程
+
+> 版本: v0.7.0+
+> 更新: 2026-06-13
+
+---
+
+## 一、概述
+
+所有 API 端点(除 `/health` 和 `/SenSu/api/login`)均需要认证。认证基于 **Panel Session Token** 机制:
+
+- 登录后获取 32 字符随机 token
+- 后续请求通过 Cookie (`panel_token`) 或 WebSocket URL 参数 (`?token=`) 传递
+- 写操作额外需要 `X-CSRF-Token` 请求头
+
+---
+
+## 二、登录流程
+
+```
+POST /SenSu/api/login
+Content-Type: application/json
+
+{"username": "admin", "password": "admin"}
+```
+
+### 处理步骤
+
+| 步骤 | 说明 | 代码位置 |
+|------|------|---------|
+| 1 | **频率检查** — IP 是否因 5 次失败被锁定 60s | `auth.py:_check_rate_limit()` |
+| 2 | **哈希比较** — `SHA256(password + salt) == stored_hash` | `manager.py:_hash_pw()` |
+| 3 | **生成 token** — `secrets.token_hex(16)` → 32 字符 | `auth.py:handle_login()` |
+| 4 | **存入内存** — `PANEL_SESSION_STORE[token] = {username, perms, login_time}` | 同上 |
+| 5 | **持久化到 DB** — `sensu_db.config_kv['panel_sessions']` JSON | `auth.py:_save_sessions_to_db()` |
+| 6 | **设置 Cookie** — `panel_token={token}; HttpOnly; Max-Age=259200; SameSite=Lax` | 同上 |
+
+### 响应
+
+```json
+{"success": true, "username": "admin"}
+```
+
+### 安全特性
+
+- 密码使用 `secrets.compare_digest()` 时序安全比较
+- 每用户独立随机盐 `secrets.token_hex(16)`
+- 5 次/IP 失败后锁定 60 秒,返回 `429 Too Many Requests`
+- Cookie 设置 `HttpOnly` (JS 不可读) + `SameSite=Lax`
+
+---
+
+## 三、HTTP API 请求(读操作)
+
+```
+GET /SenSu/api/files/list
+Cookie: panel_token=f93f18546742b6c4...
+```
+
+### 鉴权流程 (`panel_auth()`)
+
+```
+request
+ │
+ ├─ 1. 从 Cookie 取 panel_token
+ │ (或 Authorization: Bearer xxx header)
+ │
+ ├─ 2. 查 PANEL_SESSION_STORE[token]
+ │ ├─ 找到 → 注入 request['user']
+ │ └─ 未找到 → 401 {"error": "未认证或会话已过期"}
+ │
+ └─ 3. handler(request) → JSON 响应
+```
+
+### 受保护的读端点
+
+| 端点 | 说明 |
+|------|------|
+| `GET /SenSu/api/files/list` | 文件列表 |
+| `GET /SenSu/api/files/info` | 文件信息 |
+| `GET /SenSu/api/files/read` | 读取文本 |
+| `GET /SenSu/api/files/download` | 下载文件 |
+| `GET /SenSu/api/files/picker` | 文件选择器 |
+| `GET /SenSu/api/projects` | 项目列表 |
+| `GET /SenSu/api/projects/{name}/logs` | 项目日志 |
+| `GET /SenSu/api/proxy` | 代理列表 |
+| `GET /SenSu/api/system` | 系统状态 |
+| `GET /SenSu/api/framework` | 框架状态 |
+| `GET /SenSu/api/plugins` | 插件列表 |
+| `GET /SenSu/api/commands` | 命令列表 |
+| `GET /SenSu/api/auth/status` | 认证状态 |
+| `GET /plugin/{name}` | 插件页面 |
+| `GET /plugin/{name}/sse` | 插件 SSE |
+
+---
+
+## 四、HTTP API 请求(写操作 — CSRF 保护)
+
+```
+POST /SenSu/api/files/delete
+Cookie: panel_token=f93f18546742b6c4...
+X-CSRF-Token: f93f18546742b6c4...
+Content-Type: application/json
+
+{"path": "/some/file"}
+```
+
+### 鉴权流程 (`panel_auth(csrf_protect=True)`)
+
+```
+request
+ │
+ ├─ 1-2. 同上 (token 验证)
+ │
+ ├─ 3. CSRF 检查
+ │ X-CSRF-Token header == panel_token cookie ?
+ │ ├─ 匹配 → 继续
+ │ └─ 不匹配 → 403 {"error": "CSRF 验证失败"}
+ │
+ └─ 4. handler(request) → JSON 响应
+```
+
+### CSRF 保护的写端点
+
+| 端点 | 说明 |
+|------|------|
+| `POST /SenSu/api/files/delete` | 删除文件/目录 |
+| `POST /SenSu/api/files/write` | 写入文件 |
+| `POST /SenSu/api/files/mkdir` | 创建目录 |
+| `POST /SenSu/api/files/touch` | 创建文件 |
+| `POST /SenSu/api/files/rename` | 重命名 |
+| `POST /SenSu/api/files/upload` | 上传文件 |
+| `POST /SenSu/api/projects/run` | 启动项目 |
+| `POST /SenSu/api/projects/{name}/stop` | 停止项目 |
+| `POST /SenSu/api/projects/{name}/stdin` | 向项目发送输入 |
+| `POST /SenSu/api/proxy` | 添加代理 |
+| `DELETE /SenSu/api/proxy/{path}` | 删除代理 |
+
+---
+
+## 五、WebSocket 连接
+
+```
+GET /SenSu/api/system/ws?token=f93f18546742b6c4...
+Upgrade: websocket
+```
+
+### 鉴权流程 (`_ws_auth_wrapper`)
+
+```
+WebSocket 握手请求
+ │
+ ├─ 1. 从 query string 取 token
+ │
+ ├─ 2. 查 PANEL_SESSION_STORE[token]
+ │ ├─ 找到 → 建立 WS 连接 → 每 2s 推送
+ │ └─ 未找到 → {"error":"Unauthorized"} → close(4001)
+ │
+ └─ 3. 连接建立后持续推送
+ {"type":"sys", "system":{cpu,memory,network}, "framework":{...}}
+```
+
+### 前端调用方式
+
+```javascript
+// app.js 提供的工具函数
+function getCookie(name) {
+ var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
+ return match ? match[2] : '';
+}
+
+// 仪表盘 WebSocket
+var tok = getCookie("panel_token");
+var ws = new WebSocket("ws://" + location.host + base + "/api/system/ws?token=" + (tok || ""));
+
+// 日志 WebSocket
+var ws = new WebSocket("ws://" + location.host + base + "/api/logs/ws?token=" + (tok || ""));
+```
+
+### WebSocket 端点
+
+| 端点 | 鉴权方式 | 用途 |
+|------|---------|------|
+| `/SenSu/api/system/ws?token=` | query param | 系统监控实时推送 |
+| `/SenSu/api/logs/ws` | Cookie (同源自动带) | 日志实时推送 |
+
+---
+
+## 六、插件路由(双层鉴权)
+
+插件通过 `PluginNetworkBridge.register_http_route()` 注册的路由使用双层鉴权:
+
+```
+GET /example_plugin/api/example/info
+Cookie: panel_token=f93f18546742b6c4...
+```
+
+### 鉴权流程 (`_check_plugin_auth()`)
+
+```
+request
+ │
+ ├─ 第1层: 用户身份验证
+ │ ├─ 从 Cookie 取 panel_token
+ │ ├─ (或 Authorization: Bearer xxx header)
+ │ ├─ 查 PANEL_SESSION_STORE
+ │ └─ 失败 → 403 {"reason": "未认证"}
+ │
+ ├─ 第2层: 插件权限检查
+ │ ├─ 查 permission_service
+ │ ├─ 插件是否有 plugin.network.access ?
+ │ └─ 失败 → 403 {"reason": "插件没有网络访问权限"}
+ │
+ └─ handler(request) → 响应
+```
+
+### 插件命令 REST 端点(自动暴露)
+
+```
+POST /example_plugin/api/plugin/echo
+Content-Type: application/json
+
+{"args": ["hello", "world"]}
+```
+
+每个 `@plugin_command` 方法自动生成 REST 端点:
+- 路径: `POST /api/plugin/{command_name}`
+- 请求体: `{"args": [...], "kwargs": {...}}`
+- 响应: `{"ok": true, "result": "..."}`
+
+---
+
+## 七、公开端点(无需认证)
+
+| 端点 | 说明 | 安全措施 |
+|------|------|---------|
+| `GET /health` | 健康检查 | 仅返回 `{"status":"healthy"}` |
+| `POST /SenSu/api/login` | 登录 | 频率限制 + 哈希比较 |
+
+---
+
+
+## 十一、API Key 认证 (服务器间通信)
+
+### 创建 API Key
+在 WebUI "框架设置" → "API Key 管理" 中创建,支持三种权限模板:
+| 模板 | 权限 |
+|------|------|
+| `readonly` | framework.status.read, plugin.info.read |
+| `monitor` | + framework.event.subscribe |
+| `full` | admin (全部) |
+
+每个 Key 可选 TTL 过期时间,格式 `sk-` + 48 hex chars。
+
+### 使用方式
+```
+GET /SenSu/api/system
+Authorization: Bearer sk-xxxxxxxx...
+```
+
+### 验证流程
+```
+panel_auth() 拦截
+ ├─ Cookie panel_token → Session Store
+ ├─ 失败 → API Key Store (validate_api_key)
+ ├─ 检查过期时间
+ └─ 注入 key 自身权限范围
+```
+
+## 八、安全响应头
+
+所有响应自动添加:
+
+| Header | 值 |
+|--------|-----|
+| `X-Content-Type-Options` | `nosniff` |
+| `X-Frame-Options` | `DENY` |
+| `X-XSS-Protection` | `1; mode=block` |
+| `Referrer-Policy` | `strict-origin-when-cross-origin` |
+
+---
+
+## 九、错误响应格式
+
+### 认证失败
+```json
+{"error": "未认证或会话已过期", "status": 401}
+```
+
+### CSRF 失败
+```json
+{"error": "CSRF 验证失败", "status": 403}
+```
+
+### 频率限制
+```json
+{"success": false, "msg": "尝试次数过多,请 60 秒后重试"}
+```
+HTTP 状态码: `429`
+
+### 服务器错误(脱敏)
+```json
+{"error": "Internal server error"}
+```
+详细信息仅写入日志,不返回客户端。
+
+---
+
+## 十、会话持久化
+
+- 登录时 session 写入 `sensu.db` → `config_kv` 表
+- 框架重启时从 DB 恢复: `auth.py:_load_sessions_from_db()`
+- 登出时从 DB 清除
+- Token 有效期: Cookie `max-age=259200` (3 天)
diff --git a/docs/SenSu 插件开发详细指南.md b/docs/SenSu 插件开发详细指南.md
new file mode 100644
index 0000000..fe5eddc
--- /dev/null
+++ b/docs/SenSu 插件开发详细指南.md
@@ -0,0 +1,5343 @@
+# SenSu 插件开发超详细指南
+
+# 目录
+
+- [SenSu 插件开发超详细指南](#sensu-插件开发超详细指南)
+ - [一、插件系统架构深度解析](#一插件系统架构深度解析)
+ - [二、插件开发完整方案](#二插件开发完整方案)
+ - [2.6 插件 WebUI 页面](#26-插件-webui-页面-v060)
+ - [三、插件生命周期管理](#三插件生命周期管理)
+ - [四、插件开发最佳实践](#四插件开发最佳实践)
+ - [4.1 错误处理最佳实践](#41-错误处理最佳实践)
+ - [4.2 性能优化最佳实践](#42-性能优化最佳实践)
+ - [4.3 安全最佳实践](#43-安全最佳实践)
+ - [4.4 测试最佳实践](#44-测试最佳实践)
+ - [五、插件发布与部署](#五插件发布与部署)
+ - [5.1 插件打包](#51-插件打包)
+ - [5.2 插件发布清单](#52-插件发布清单)
+ - [5.3 持续集成配置](#53-持续集成配置)
+ - [六、插件调试与故障排除](#六插件调试与故障排除)
+ - [6.1 调试工具](#61-调试工具)
+ - [6.2 故障排除指南](#62-故障排除指南)
+- [在插件配置中](#在插件配置中)
+- [查找插件相关日志](#查找插件相关日志)
+- [实时查看日志](#实时查看日志)
+ - [七、插件开发检查清单](#七插件开发检查清单)
+ - [7.1 开发前检查清单](#71-开发前检查清单)
+ - [7.2 开发中检查清单](#72-开发中检查清单)
+ - [7.3 测试检查清单](#73-测试检查清单)
+ - [7.4 发布检查清单](#74-发布检查清单)
+ - [八、文件管理器集成 (v0.6.0)](#八文件管理器集成-v060-新增)
+ - [九、总结](#九总结)
+ - [8.1 成功插件的特点](#81-成功插件的特点)
+ - [8.2 持续改进](#82-持续改进)
+ - [8.3 资源推荐](#83-资源推荐)
+
+---
+
+
+## 一、插件系统架构深度解析
+
+### 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 # 是否自动启动
+ isolation: false # 进程隔离: true=独立子进程(无WebUI), false=框架内运行(支持WebUI/网络路由)
+ 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 core.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
+```
+
+
+### 2.6 插件 WebUI 页面 (v0.6.0)
+
+### 2.7 插件导入安装 (v0.7.0)
+
+框架支持在 WebUI 插件管理页直接导入安装插件:
+
+**ZIP 导入**:上传 .zip 包 → 自动解压到 plugins/ → pip install -r requirements.txt → 加载
+
+**Git Clone 导入**:输入仓库 URL → git clone → pip install → 加载
+
+安装过程在子进程中执行,失败不影响框架主进程。安装失败的插件在统计面板中显示。
+
+
+插件可以注册 WebUI 页面,自动出现在 SenSu 管理面板侧边栏的「插件面板」折叠项中。
+
+#### 2.6.1 注册页面
+
+```python
+from sdk.plugin_web import PluginWebMixin
+
+class MyPlugin(PluginWebMixin):
+ async def initialize(self):
+ self.register_web_page(
+ path="mypanel", # 访问路径: /plugin/{plugin_name}
+ title="我的面板", # 侧边栏显示名称
+ html_content="Hello
", # 完整 HTML 页面
+ icon="M" # 侧边栏图标 (单字符)
+ )
+```
+
+#### 2.6.2 页面渲染机制
+
+插件页面在 SenSu 主面板的 `page-container` 区域内联加载(不弹新窗口):
+
+1. 用户点击侧边栏「插件面板」→ 展开子项 → 点击插件页面
+2. 前端 fetch `/plugin/{plugin_name}` 获取完整 HTML
+3. 渲染管线自动处理:
+ - 提取 `
+插件面板
+
+
+