From 74411c8a76bcd6678f35db4a52bde3663eed3c67 Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Fri, 13 Mar 2026 06:37:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(architecture):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E5=8E=9F=E5=88=99=EF=BC=8C=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E6=A8=A1=E5=9D=97=E9=9A=94=E7=A6=BB=E5=92=8C?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E5=AF=BC=E5=87=BA=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ARCHITECTURE.md | 110 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 4 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6d77d54f7..e7f888438 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -417,7 +417,21 @@ __all__ = [ ] ``` -**设计说明**: loader/bootstrap 等编排细节保留在子模块中,不作为根级稳定契约。 +**设计原则:** + +- **精简导出**: 仅暴露相对稳定的运行时构件 +- **高级原语**: `Peer`、`Transport`、`CapabilityRouter`、`HandlerDispatcher` +- **子模块隔离**: loader/bootstrap 编排细节保留在子模块中,不作为根级稳定契约 +- **插件作者指引**: 大多数插件应使用顶层 `astrbot_sdk` 或 `astrbot_sdk.api` + +**不在根级导出的类型:** + +| 类型 | 所在子模块 | 原因 | +|------|-----------|------| +| `LoadedPlugin`, `LoadedHandler` | `loader` | 加载器内部数据结构 | +| `PluginSpec`, `PluginDiscoveryResult` | `loader` | 插件发现元数据 | +| `SupervisorRuntime`, `WorkerSession` | `bootstrap` | 运行时编排细节 | +| `run_supervisor`, `run_plugin_worker` | `bootstrap` | 启动函数 | --- @@ -594,6 +608,50 @@ shared_legacy_context: LegacyContext | None = None {plugin_name}:{module}.{ClassName}.{method_name} ``` +**插件模块隔离 (重要):** + +```python +def import_string(path: str, plugin_dir: Path | None = None) -> Any: + """导入模块属性,支持插件目录隔离""" + module_name, attr = path.split(":", 1) + _prepare_plugin_import(module_name, plugin_dir) + module = import_module(module_name) + return getattr(module, attr) + +def _prepare_plugin_import(module_name: str, plugin_dir: Path | None) -> None: + """准备插件导入环境,处理模块缓存冲突""" + # 1. 将 plugin_dir 加入 sys.path + # 2. 检测缓存模块是否属于当前插件 + # 3. 若缓存模块属于其他插件,清理冲突的根包 + +def _purge_module_root(root_name: str) -> None: + """清理冲突的模块缓存""" + for module_name in list(sys.modules): + if module_name == root_name or module_name.startswith(f"{root_name}."): + sys.modules.pop(module_name, None) + +def _module_belongs_to_plugin(module: Any, plugin_dir: Path) -> bool: + """检查模块是否属于指定插件目录""" + # 通过 __file__ 或 __path__ 判断模块位置 +``` + +**设计说明**: 当多个插件使用相同的顶层包名(如 `commands.*`)时,`import_string` 会自动清理冲突的缓存模块,确保每个插件加载自己的代码而非其他插件的缓存。 + +**Legacy Handler 顺序保持:** + +```python +def _iter_discoverable_names(instance: Any) -> list[str]: + """返回可发现的名称列表,保持声明顺序""" + # 1. 优先返回 __handlers__ 中声明的名称(保持顺序) + handler_names = list(dict.fromkeys(_iter_handler_names(instance))) + # 2. 然后返回 dir() 中发现的额外名称(按字母排序) + known_names = set(handler_names) + extra_names = sorted(name for name in dir(instance) if name not in known_names) + return [*handler_names, *extra_names] +``` + +**设计说明**: 旧版插件期望 handler 按声明顺序注册。使用 `sorted()` 或 `dir()` 遍历会改变顺序,导致命令冲突时错误的 handler 被优先触发。 + --- #### `handler_dispatcher.py` - Handler 分发器 @@ -1461,7 +1519,7 @@ class MyTransport(Transport): | **运行时层** | `runtime/` | ✅ 完成 | | | | `peer.py` | ✅ | 对称通信端点 + 取消 + 流式 + 远程元数据缓存 | | | `transport.py` | ✅ | Stdio + WebSocket Server/Client | -| | `loader.py` | ✅ | 插件发现 + 环境管理 + 配置规范化 + Capability 支持 | +| | `loader.py` | ✅ | 插件发现 + 环境管理 + 配置规范化 + 模块隔离 + Handler 顺序保持 | | | `handler_dispatcher.py` | ✅ | Handler 分发 + CapabilityDispatcher + 参数注入增强 | | | `capability_router.py` | ✅ | 能力路由 + 15 个内置能力 + SessionRef 支持 | | | `bootstrap.py` | ✅ | Supervisor + Worker + WebSocket + 连接关闭处理 | @@ -1513,7 +1571,7 @@ tests_v4/ ├── test_transport.py # Transport 测试 ├── test_capability_router.py # CapabilityRouter 测试 ├── test_handler_dispatcher.py # HandlerDispatcher 测试 -├── test_loader.py # 加载器测试 +├── test_loader.py # 加载器测试(含模块隔离、Handler 顺序) ├── test_bootstrap.py # Bootstrap 测试 ├── test_runtime.py # 运行时测试 ├── test_runtime_integration.py # 运行时集成测试 @@ -1543,7 +1601,7 @@ tests_v4/ ├── test_script_migrations.py # 脚本迁移测试 ├── test_supervisor_migration.py # Supervisor 迁移测试 ├── test_legacy_plugin_integration.py # 旧版插件集成测试 -├── test_top_level_modules.py # 顶层模块测试 +├── test_top_level_modules.py # 顶层模块测试(含 runtime 导出验证) │ ├── # 入口点测试 ├── test_entrypoints.py # 入口点测试 @@ -1551,6 +1609,14 @@ tests_v4/ └── __init__.py # 包初始化文件 ``` +**测试重点:** + +| 测试文件 | 重点测试内容 | +|---------|-------------| +| `test_loader.py` | 插件模块隔离(多插件同名包)、Legacy Handler 顺序保持 | +| `test_top_level_modules.py` | `runtime.__init__` 导出验证、确保不暴露内部数据结构 | +| `test_legacy_plugin_integration.py` | 旧版插件完整兼容性、LegacyContext 共享 | + --- ## 版本兼容性 @@ -1584,3 +1650,39 @@ from astrbot_sdk.context import Context from astrbot_sdk.api.star import Context, Star from astrbot_sdk.compat import LegacyContext, CommandComponent ``` + +--- + +## 关键设计决策 + +### 运行时模块导出策略 + +`runtime/__init__.py` 仅暴露高级运行时原语(`Peer`, `Transport`, `CapabilityRouter`, `HandlerDispatcher`),而将 loader/bootstrap 等编排细节保留在子模块中。 + +**原因:** +- loader/bootstrap 数据结构(`LoadedPlugin`, `WorkerSession` 等)属于内部实现细节 +- 避免意外的 API 契约,便于未来重构 +- 插件作者通常不需要直接使用这些低级原语 + +### 插件模块隔离 + +当多个插件使用相同的顶层包名时,`import_string()` 会自动清理冲突的缓存模块。 + +**问题:** 插件 A 定义 `commands.hello`,插件 B 也定义 `commands.world`。若插件 A 先加载,`sys.modules["commands"]` 指向插件 A 的目录,插件 B 会错误地使用插件 A 的代码。 + +**解决:** `_prepare_plugin_import()` 检测缓存模块是否属于当前插件目录,若不属于则清理冲突的根包。 + +### Legacy Handler 顺序保持 + +`_iter_discoverable_names()` 保持 handler 的声明顺序,而非使用 `sorted()` 或 `set()` 遍历。 + +**原因:** 旧版 `StarManager` 按声明顺序注册 handler。当多个 handler 匹配同一命令时,第一个注册的 handler 被优先触发。改变顺序会导致不同的 handler 被触发。 + +**实现:** +```python +# 保持 __handlers__ 中的顺序 +handler_names = list(dict.fromkeys(_iter_handler_names(instance))) +# 额外发现的名称按字母排序 +extra_names = sorted(name for name in dir(instance) if name not in known_names) +return [*handler_names, *extra_names] +```