diff --git a/openspec/changes/abp-plugin-loader/.openspec.yaml b/openspec/changes/abp-plugin-loader/.openspec.yaml new file mode 100644 index 000000000..a61e7c112 --- /dev/null +++ b/openspec/changes/abp-plugin-loader/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-27 diff --git a/openspec/changes/abp-plugin-loader/design.md b/openspec/changes/abp-plugin-loader/design.md new file mode 100644 index 000000000..f7102bcce --- /dev/null +++ b/openspec/changes/abp-plugin-loader/design.md @@ -0,0 +1,106 @@ +## Context + +ABP 协议第一阶段完成了核心握手、传输层和事件系统。当前 `OutOfProcessPluginLoader` 已实现,但: +1. `PluginLoader` trait 未定义,InProcess 模式缺失 +2. `tools/list` 端点未实现,工具发现依赖它 +3. Python FFI 绑定未完成,Python 层无法调用 Rust 核心 +4. 配置集成未落地,无 PluginRegistry + +## Goals / Non-Goals + +**Goals:** +- 实现 `PluginLoader` trait,统一进程内/外加载逻辑 +- 实现 `InProcessPluginLoader`,Python 模块直接加载 +- 实现 `tools/list` 端点和工具 Schema 验证 +- 完成 Python FFI 绑定链路 +- 落地 config.yaml 插件配置和 PluginRegistry + +**Non-Goals:** +- 不实现 Stars → ABP 插件迁移(下一阶段) +- 不实现 WebUI 插件配置界面(config.yaml 落地即可) +- 不改变 Rust 核心源码(仅调用已有接口) + +## Decisions + +### 1. PluginLoader Trait 设计 + +**决定**:定义抽象 trait,包含 `load()`、`unload()`、`reload()`、`get_plugin_info()` 方法。 + +```python +class PluginLoader(ABC): + @abstractmethod + async def load(self, config: PluginConfig) -> PluginInstance: ... + + @abstractmethod + async def unload(self, plugin_id: str) -> None: ... + + @abstractmethod + async def reload(self, plugin_id: str) -> PluginInstance: ... +``` + +**替代方案**: +- 工厂模式(被否定):增加抽象层级,当前场景不需要 +- 统一接口参数(被否定):进程内/外行为差异大,分离更清晰 + +### 2. InProcessPluginLoader 实现 + +**决定**:Python 模块直接导入,插件实例化后存入 `Dict[str, PluginInstance]`。 + +**理由**: +- 无进程通信开销,性能最优 +- 与项目 "Rust 核心 + Python 胶水层" 架构一致 +- 插件需遵循 `ABPPluginProtocol` 接口约定 + +### 3. 工具发现架构 + +**决定**:`ToolRegistry` 统一管理,`tools/list` 聚合所有插件工具。 + +``` +ToolRegistry +├── register(plugin_id, tools) +├── unregister(plugin_id) +├── list_tools() -> List[ToolDef] +└── call_tool(name, args) -> ToolResult +``` + +**理由**: +- 中心化管理避免冲突 +- Schema 验证在注册时执行 +- 跨插件工具调用统一入口 + +### 4. FFI 绑定链路 + +**决定**:Python → Rust `_core.so` → ABP PluginLoader。 + +``` +Python PluginManager + → ctypes / cffi 调用 _core.so + → Rust abp_plugin_loader_* 函数 + → 返回 Python 对象(通过 .pyi 类型提示) +``` + +**理由**: +- Rust 源码待提交,当前仅使用编译产物 +- `_core.pyi` 提供类型检查 +- anyio 异步调用通过 `run_in_executor` 封装 + +## Risks / Trade-offs + +| 风险 | 影响 | 缓解措施 | +|------|------|----------| +| Rust 源码未提交,接口可能变 | FFI 调用失败 | 接口版本化,核心保持向后兼容 | +| InProcess 插件崩溃影响主进程 | 稳定性下降 | 进程内插件加超时保护 + 错误隔离 | +| 工具 Schema 验证复杂度 | 开发体验下降 | JSON Schema 仅校验格式,不校验业务逻辑 | + +## Migration Plan + +1. **Phase 1**:PluginLoader trait + InProcessPluginLoader(对现有 OutOfProcess 透明) +2. **Phase 2**:tools/list + ToolRegistry +3. **Phase 3**:FFI 绑定 + config.yaml 集成 +4. **Phase 4**:测试覆盖 + 文档 + +## Open Questions + +- Q1: InProcess 插件是否需要独立的沙箱隔离?(当前:无) +- Q2: 工具注册时 Schema 校验严格程度?(当前:格式校验 + 必需字段) +- Q3: PluginRegistry 是否需要持久化?(当前:内存,进程重启丢失) diff --git a/openspec/changes/abp-plugin-loader/proposal.md b/openspec/changes/abp-plugin-loader/proposal.md new file mode 100644 index 000000000..7ce49a890 --- /dev/null +++ b/openspec/changes/abp-plugin-loader/proposal.md @@ -0,0 +1,32 @@ +## Why + +ABP 协议实现第一阶段完成了核心握手、传输层和事件系统。当前 PluginLoader trait 和 InProcess 插件加载尚未实现,工具发现依赖 `tools/list`,FFI 绑定也未完成。这些是 ABP 插件系统真正可用的最后几块拼图。 + +## What Changes + +- **PluginLoader trait**:定义插件加载抽象,支持进程内/外两种模式 +- **InProcessPluginLoader**:实现 Python 模块直接加载(无需进程通信) +- **tools/list 端点**:插件暴露可用工具列表 +- **工具 Schema 验证**:JSON Schema Draft-07 校验 +- **跨插件工具发现**:统一聚合所有插件的工具 +- **FFI 绑定**:Python 胶水层调用 Rust ABP 核心 +- **配置集成**:config.yaml 插件配置落地,PluginRegistry 实现 +- **测试完善**:单元测试 + 集成测试覆盖 + +## Capabilities + +### New Capabilities + +- `abp-plugin-loader`: 插件加载器 trait 和实现(InProcess/OutOfProcess) +- `abp-tool-discovery`: 工具注册、Schema 验证、跨插件发现 + +### Modified Capabilities + +- `abp-protocol`: FFI 绑定补充(Python → Rust 核心调用链路) +- `abp-tool-router`: 增加 `tools/list` 端点实现 + +## Impact + +- **新增**:`astrbot/core/plugin/` 扩展(loader 模块) +- **修改**:`astrbot/core/plugin/plugin_manager.py`(FFI 绑定) +- **依赖**:Rust 核心 `_core.so` 已编译,源码待提交 `astrbot/rust/src/` diff --git a/openspec/changes/abp-plugin-loader/specs/abp-plugin-loader/spec.md b/openspec/changes/abp-plugin-loader/specs/abp-plugin-loader/spec.md new file mode 100644 index 000000000..9308067b8 --- /dev/null +++ b/openspec/changes/abp-plugin-loader/specs/abp-plugin-loader/spec.md @@ -0,0 +1,57 @@ +## ADDED Requirements + +### Requirement: PluginLoader Trait +ABP 插件加载器抽象接口,定义进程内/外插件的统一加载方式。 + +#### Scenario: Load In-Process Plugin +- **WHEN** 配置指定 `load_mode: "in_process"` +- **THEN** `InProcessPluginLoader` 直接导入 Python 模块 +- **AND** 实例化插件,传入 context、user_config、data_dirs +- **AND** 将插件实例注册到 `PluginRegistry` + +#### Scenario: Load Out-of-Process Plugin +- **WHEN** 配置指定 `load_mode: "out_of_process"` +- **THEN** `OutOfProcessPluginLoader` 启动插件子进程 +- **AND** 建立传输层连接(Stdio/Unix Socket/HTTP) +- **AND** 执行握手协议交换配置 + +#### Scenario: Unload Plugin +- **WHEN** 调用 `loader.unload(plugin_id)` +- **THEN** 关闭插件连接/释放模块引用 +- **AND** 从 `PluginRegistry` 注销插件 + +#### Scenario: Reload Plugin +- **WHEN** 调用 `loader.reload(plugin_id)` +- **THEN** 卸载现有插件实例 +- **AND** 重新加载并初始化插件 + +### Requirement: In-Process Plugin Loading +进程内插件直接加载为 Python 模块,由 Python 胶水层管理生命周期。 + +#### Scenario: In-Process Plugin Invocation +- **WHEN** 调用进程内插件方法(如 `handle_event`) +- **THEN** 直接通过 Python 对象调用 +- **AND** 不经过序列化/反序列化 +- **AND** 无进程通信开销 + +#### Scenario: In-Process Plugin Context +- **WHEN** 插件实例化时 +- **THEN** 主进程传入 context 对象 +- **AND** 传入 user_config(来自握手) +- **AND** 传入 data_dirs(数据目录路径) + +### Requirement: PluginRegistry +全局插件实例注册表,管理所有已加载插件。 + +#### Scenario: Register Plugin Instance +- **WHEN** 插件加载成功时 +- **THEN** `PluginRegistry` 存储 plugin_id → PluginInstance 映射 +- **AND** 插件暴露的工具注册到 `ToolRegistry` + +#### Scenario: Query Plugin by ID +- **WHEN** 调用 `registry.get_plugin(plugin_id)` +- **THEN** 返回插件实例或 None + +#### Scenario: List All Plugins +- **WHEN** 调用 `registry.list_plugins()` +- **THEN** 返回所有已注册插件的元数据列表 diff --git a/openspec/changes/abp-plugin-loader/specs/abp-tool-discovery/spec.md b/openspec/changes/abp-plugin-loader/specs/abp-tool-discovery/spec.md new file mode 100644 index 000000000..3de744c95 --- /dev/null +++ b/openspec/changes/abp-plugin-loader/specs/abp-tool-discovery/spec.md @@ -0,0 +1,58 @@ +## ADDED Requirements + +### Requirement: tools/list Endpoint +插件通过 `tools/list` 端点暴露可用工具列表。 + +#### Scenario: List Plugin Tools +- **WHEN** 主进程调用 `tools/list` 方法 +- **THEN** 插件返回 `{ "tools": [ToolDef, ...] }` +- **AND** 每个 ToolDef 包含 name、description、parameters(JSON Schema) + +#### Scenario: Empty Tools List +- **WHEN** 插件无可用工具 +- **THEN** 返回 `{ "tools": [] }` + +### Requirement: Tool Schema Validation +注册工具时进行 JSON Schema Draft-07 格式校验。 + +#### Scenario: Valid Tool Schema +- **WHEN** 工具定义通过 Schema 校验 +- **THEN** 工具注册到 `ToolRegistry` +- **AND** 可被 `tools/call` 调用 + +#### Scenario: Invalid Tool Schema +- **WHEN** 工具定义 Schema 校验失败 +- **THEN** 记录警告日志 +- **AND** 跳过该工具(不注册) + +### Requirement: ToolRegistry +中心化工具注册表,支持跨插件工具发现和调用。 + +#### Scenario: Register Tool +- **WHEN** 插件调用 `registry.register(plugin_id, tools)` +- **THEN** 工具按 plugin_id 隔离存储 +- **AND** 执行 Schema 校验 + +#### Scenario: Discover All Tools +- **WHEN** 调用 `registry.list_tools()` +- **THEN** 返回所有插件注册的工具 +- **AND** 每个工具标记来源 plugin_id + +#### Scenario: Call Tool +- **WHEN** 调用 `registry.call_tool(tool_name, args)` +- **THEN** 查找工具所属插件 +- **AND** 转发调用到对应插件 +- **AND** 返回执行结果 + +#### Scenario: Tool Not Found +- **WHEN** 调用不存在的工具 +- **THEN** 抛出 `ToolNotFoundError` 错误 +- **AND** 错误码 -32203 + +### Requirement: Cross-Plugin Tool Discovery +聚合多个插件的工具,提供统一发现接口。 + +#### Scenario: Aggregate Tools from Multiple Plugins +- **WHEN** 多个插件注册了工具 +- **THEN** `ToolRegistry` 聚合所有工具 +- **AND** 工具名可配置为 `plugin_name/tool_name` 格式避免冲突 diff --git a/openspec/changes/abp-plugin-loader/tasks.md b/openspec/changes/abp-plugin-loader/tasks.md new file mode 100644 index 000000000..3660b2cb7 --- /dev/null +++ b/openspec/changes/abp-plugin-loader/tasks.md @@ -0,0 +1,45 @@ +## 1. PluginLoader Trait & InProcessPluginLoader + +- [ ] 1.1 Define `PluginLoader` abstract base class in `astrbot/core/plugin/loader.py` +- [ ] 1.2 Define `PluginInstance` dataclass (plugin_id, instance, metadata) +- [ ] 1.3 Implement `InProcessPluginLoader.load()` (Python module import) +- [ ] 1.4 Implement `InProcessPluginLoader.unload()` (remove from registry) +- [ ] 1.5 Implement `InProcessPluginLoader.reload()` (reload module) +- [ ] 1.6 Create `PluginRegistry` class (Dict[str, PluginInstance] + singleton) +- [ ] 1.7 Add `register()` and `unregister()` methods to PluginRegistry + +## 2. Tool Discovery & Registry + +- [ ] 2.1 Create `astrbot/core/plugin/tool_registry.py` +- [ ] 2.2 Implement `ToolDef` dataclass (name, description, parameters schema) +- [ ] 2.3 Implement `ToolRegistry.register(plugin_id, tools)` with Schema validation +- [ ] 2.4 Implement `ToolRegistry.list_tools()` (aggregate all plugins) +- [ ] 2.5 Implement `ToolRegistry.call_tool(tool_name, args)` +- [ ] 2.6 Implement `tools/list` JSON-RPC endpoint in plugin base class +- [ ] 2.7 Add JSON Schema Draft-07 validation (jsonschema library) + +## 3. FFI Bindings + +- [ ] 3.1 Audit existing `_core.pyi` for missing ABP types +- [ ] 3.2 Add `plugin_loader_*` FFI function signatures to rust-ffi.md (if missing) +- [ ] 3.3 Implement Python → Rust plugin loader calls via ctypes +- [ ] 3.4 Add async wrapper using `run_in_executor` for FFI calls +- [ ] 3.5 Update `plugin_manager.py` to use FFI bindings + +## 4. Configuration Integration + +- [ ] 4.1 Define `plugins` section in `config.yaml` schema +- [ ] 4.2 Implement `PluginConfig` dataclass parsing +- [ ] 4.3 Integrate PluginRegistry initialization into AstrBot startup +- [ ] 4.4 Add connection pooling for Unix Socket transport (task 3.4 from original) +- [ ] 4.5 Write integration test for config → plugin loading flow + +## 5. Testing + +- [ ] 5.1 Write unit tests for PluginLoader trait +- [ ] 5.2 Write unit tests for InProcessPluginLoader +- [ ] 5.3 Write unit tests for ToolRegistry +- [ ] 5.4 Write integration tests for in-process plugin loading +- [ ] 5.5 Write integration tests for tools/list + tools/call flow +- [ ] 5.6 Run `ruff check .` and `ruff format .` +- [ ] 5.7 Run `uvx ty check` for type validation