118 lines
9.0 KiB
Markdown
118 lines
9.0 KiB
Markdown
# Plugin Command 与 Settings 开发说明
|
||
|
||
> 更新日期:2026-09-03。本文记录第二阶段阶段 D 已实现的 Plugin Command Contribution、Plugin Settings Contribution、Secret 边界及前端闭环。当前回归基线为后端 136 项测试、前端 32 项测试,TypeScript 类型检查和生产构建通过。
|
||
|
||
## 1. 阶段目标
|
||
|
||
阶段 D 在阶段 C 的 Plugin Runtime 与隔离 MCP Host 上补齐两类宿主贡献:
|
||
|
||
- Command:插件声明命令,宿主负责注册、展示、校验、执行和返回白名单 effect;
|
||
- Settings:插件声明设置 Schema,宿主负责动态表单 Contract、非敏感值持久化和 Secret 加密引用;
|
||
- Frontend:Plugin 详情页提供 Host 状态、动态设置、Secret 管理与命令执行,全局命令面板加载 command_palette Contribution。
|
||
|
||
第三方代码不会导入 FastAPI 进程。操作系统级安全沙箱仍按规划在第三阶段桌面基础集成完成后、Tauri/Rust 沙箱正式构建前处理。
|
||
|
||
## 2. 包内声明
|
||
|
||
Plugin 在 `plugin.yaml` 的 `contributes.commands` 与 `contributes.settings_sections` 声明贡献标识,并分别提供 `commands.yaml`、`settings.yaml`。安装时宿主要求声明集合与文件内容完全一致,拒绝重复项、越过 Plugin 命名空间的 ID、未声明权限和无效 Schema。
|
||
|
||
`commands.yaml` 的首批字段包括:
|
||
|
||
- `command_id`、标题、描述、宿主图标;
|
||
- `locations`:`command_palette`、`context_menu` 或 `toolbar`;
|
||
- `when` 与允许传入执行器的 Context 字段;
|
||
- 参数 JSON Schema、可选权限、执行目标和超时;
|
||
- 执行目标必须在宿主白名单 `handler` 与当前插件命名空间的 `mcp_tool` 之间二选一。
|
||
- 可选 `secrets` 字段:只声明当前 Command 允许按需读取的 Secret Setting Key,不暴露给前端 DTO。
|
||
|
||
`settings.yaml` 采用递增 `schema_version`,首批字段类型固定为 `string`、`number`、`boolean`、`select`、`secret`。宿主会校验默认值、必填项、有限数值边界、Select 选项,以及 Secret 不得携带默认明文;`NaN` 与正负无穷不能用作上下界。
|
||
|
||
仓库内 `text-tools` 是联调 Fixture,覆盖 Command 和五种 Settings 字段类型。
|
||
|
||
## 3. Command 运行链路
|
||
|
||
`CommandRegistry` 只发布处于启用状态的 Plugin Command。Plugin 禁用、Host 不可用或重启时,Command 与 Tool 使用同样的注销/重新注册生命周期,避免前端看到实际不可执行的命令。
|
||
|
||
执行顺序如下:
|
||
|
||
1. 查找已注册 Command;
|
||
2. 使用 Draft 2020-12 JSON Schema 校验 arguments;
|
||
3. 根据 `when` 检查必要上下文;
|
||
4. 仅向执行器传递声明过的 Context 字段;
|
||
5. 在超时范围内调用宿主受控 handler,或调用独立的 MCP Command Target;
|
||
6. 按 effect 类型校验专属 payload、可序列化性和 64 KiB 大小上限;
|
||
7. 返回统一 `PluginCommandResult`。
|
||
|
||
首批 effect 为 `none`、`notification`、`navigate`、`refresh` 和 `job`。后端分别限制通知级别与消息、宿主路由名、刷新范围和 Job ID;Pydantic 与 TypeScript 均使用同一判别语义,前端不得把 effect 当作任意代码执行。
|
||
|
||
Command 执行器通过受控 Resolver 按需读取 `commands.yaml` 已声明且确实属于当前 Plugin Schema 的 Secret;使用 Secret 的 Plugin 还必须声明并获授 `secrets.use` 权限。读取未声明字段返回 `PLUGIN_SECRET_ACCESS_DENIED`,必填 Secret 未配置则返回 `PLUGIN_SECRET_REQUIRED`。Secret 不会并入普通 Settings 字典。Command 审计使用 500 条有界内存队列,仅保留 `command_id`、`plugin_id`、成功/失败状态、耗时、错误码和时间。arguments、正文选区、文件路径、effect 与 Secret 均不进入审计事件。
|
||
|
||
MCP Command Target 是专用执行目标,不注册进 Agent `ToolRegistry`,因此模型无法绕过 Command 权限与 Context 裁剪直接调用。宿主通过 `_notesagent` 保留包装传入 `command_id`、已校验 arguments、已裁剪 Context、已校验的非敏感 Settings 和声明过的 Secret。启用阶段只检查目标 `inputSchema` 在顶层 `properties` 中直接声明 `_notesagent: { type: object }`,不自行求解 JSON Schema,也不用空对象伪造业务数据;引用和组合约束可以放在 `_notesagent` 对象内部。执行阶段再用保留的完整 Schema 和官方 Draft 2020-12 Validator 校验真实信封。MCP Server 必须返回结构化的白名单 effect。远程原始错误不直接透传给 HTTP 调用方。插件仍不能把模块路径或 Shell 字符串作为执行器。
|
||
|
||
Command 与 Tool 的 JSON Schema 只允许当前文档内的 Fragment 引用(`#...`);宿主在注册前递归拒绝 `$ref` / `$dynamicRef` 指向的文件、HTTP 或其他外部资源,避免 Schema 校验触发未授权 I/O。文档内引用使用 Draft 2020-12 Resource Resolver 预检,嵌套 `$id` 创建的新资源及其 Anchor 按各自作用域解析,无法解析的引用在注册阶段返回稳定错误。
|
||
|
||
## 4. Settings 与 Secret 边界
|
||
|
||
普通 Settings 以 Plugin 为命名空间持久化到:
|
||
|
||
```text
|
||
APP_DATA_DIR/plugins/settings.json
|
||
```
|
||
|
||
该文件只包含:
|
||
|
||
- 当前 Schema 版本;
|
||
- 非敏感字段值;
|
||
- Secret 的确定性定长引用,格式为 `plugin.<sha256(plugin_id\\0setting_key)>`。
|
||
|
||
Secret 写入必须调用专用端点。后端通过 `SecretStr` 接收明文,再交给现有 `EncryptedCredentialStore`;普通 Settings API 只返回 `{ configured: true|false }`,不会返回 Secret 值。`plugin.*` 是保留命名空间,通用凭据 API、Provider 配置、Provider 临时测试凭据和 Provider Resolver 均不得访问,防止覆盖、删除或外发 Plugin Secret。卸载 Plugin 时同时删除普通设置命名空间和对应加密凭据。
|
||
|
||
没有默认值的 `required` 普通字段必须在启用 Plugin 前配置。Enable 和每次 Command Execute 都会重新检查有效设置;缺失时返回 `PLUGIN_SETTINGS_REQUIRED`,不启动 MCP Host,也不调用 Command handler。
|
||
|
||
读取持久化引用时,宿主会重新计算并核对 `plugin.<sha256(...)>`,引用不匹配即按损坏存储拒绝处理,不能借由篡改 `settings.json` 读取或删除 Provider 等其他命名空间的凭据。删除单个 Secret 或卸载 Plugin 时先原子更新 Settings 引用,再删除加密凭据;底层删除失败会恢复原引用。多 Secret 卸载使用一次凭据表原子替换,避免分批删除部分删除。
|
||
|
||
开发阶段凭据文件由本机 Fernet Key 加密。桌面端落地后,应由 Tauri Host 将同一引用语义迁移到 Stronghold 或系统 Keychain,HTTP Contract 无需因此改变。
|
||
|
||
## 5. HTTP 与前端 Service
|
||
|
||
后端已实现:
|
||
|
||
```text
|
||
GET /api/plugin-contributions/commands?location=command_palette
|
||
POST /api/plugin-contributions/commands/{command_id}/execute
|
||
GET /api/plugins/{plugin_id}/settings
|
||
PUT /api/plugins/{plugin_id}/settings
|
||
PUT /api/plugins/{plugin_id}/settings/{key}/secret
|
||
DELETE /api/plugins/{plugin_id}/settings/{key}/secret
|
||
```
|
||
|
||
前端 `pluginService` 提供对应方法及 Wire DTO,Plugin 详情页据此展示 MCP Host 状态和重启入口、动态生成五类设置字段、独立写入或删除 Secret,并执行带参数的 Plugin Command。全局命令面板打开时获取 `command_palette` 命令;需要必填参数的命令会引导用户进入详情页填写。调用方必须使用服务层,不自行拼接路径。
|
||
|
||
Secret 明文仅存在于当前密码输入框绑定的组件内存,提交后立即清空;不得写入 Pinia、LocalStorage、普通 Settings 请求或调试日志。前端不会读取 Secret 明文,只展示后端返回的 `configured` 状态。
|
||
|
||
## 6. 主要错误边界
|
||
|
||
- Command 未注册、冲突、参数或 Context 无效;
|
||
- 执行超时、执行器异常、effect 无效或过大;
|
||
- Settings Schema 无效、版本冲突、字段类型/边界错误或运行时必填值缺失;
|
||
- Secret 字段不存在、空 Secret、凭据存储异常;
|
||
- Settings JSON 根结构或 Plugin 命名空间损坏。
|
||
|
||
以上错误统一转换为 `ExtensionError` 和稳定业务错误码,HTTP 层不暴露内部堆栈、Secret 或插件返回的原始异常。
|
||
|
||
## 7. 验证
|
||
|
||
```powershell
|
||
cd backend
|
||
uv run pytest
|
||
|
||
cd ../frontend
|
||
pnpm test -- --run
|
||
pnpm type-check
|
||
pnpm build
|
||
```
|
||
|
||
阶段 D 测试覆盖注册/注销生命周期、位置过滤、参数与 Context 校验、上下文裁剪、设置影响命令执行、声明式 Secret Resolver 与越权拒绝、真实 MCP Command Target 与 Agent Tool 隔离、必填 Secret 传递、外部 Schema 引用拒绝、定长 Secret Reference、篡改引用的跨命名空间阻断、Secret 删除与卸载失败回滚、Provider/通用凭据命名空间隔离、五类设置字段、Schema 版本冲突、Secret 密文与清理、损坏存储、空 Command 列表等无效贡献文件、OpenAPI 路径、前端 Service 请求格式、Host 状态展示和动态 Secret 表单。
|
||
|
||
生产构建仍会报告现有大 Chunk 警告,不影响构建成功;该问题属于前端按路由和 Markdown 依赖拆包的后续性能任务。
|