feat(openspec): add abp-plugin-loader change for remaining ABP tasks

This commit is contained in:
LIghtJUNction
2026-03-27 22:25:41 +08:00
parent 0c194576c4
commit b4512f26e6
6 changed files with 300 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-27

View File

@@ -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 是否需要持久化?(当前:内存,进程重启丢失)

View File

@@ -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/`

View File

@@ -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** 返回所有已注册插件的元数据列表

View File

@@ -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、parametersJSON 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` 格式避免冲突

View File

@@ -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