docs(plugins): 依赖与存储文档(L8/L9)+ 检验器入库并升级契约 0.3.0 规则
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# L8 插件依赖与服务互调
|
||||
|
||||
> 插件不是孤岛:A 依赖 B/C 时,声明、激活顺序、可用性判定、调用规范由宿主统一保证。
|
||||
> 契约 0.3.0 起;全部机制在 `dependencies` / `optionalDependencies` + `api.service` / `api.requirePlugin` 四个名字里。
|
||||
|
||||
## 1. 声明依赖(manifest.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "wiki-assistant",
|
||||
"version": "1.2.0",
|
||||
"minContract": "0.3.0",
|
||||
"dependencies": {
|
||||
"fetch-tools": "^1.0.0",
|
||||
"browser": "*"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"embed-store": "~2.1.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 值是 **semver 范围**,支持五种写法:`*`(任意)、`^1.2.0`(同主版本)、`~1.2.0`(同次版本)、`>=1.2.0`(下限)、`1.2.0`(精确)。
|
||||
- `dependencies` 是**必需依赖**:缺失 / 已停用 / 版本不符 / 自身装载失败 → 本插件激活失败。
|
||||
- `optionalDependencies` 是**可选依赖**:不就绪只降级,`api.requirePlugin(id)` 返回 `undefined`。
|
||||
- **依赖声明即访问授权**:`requirePlugin` 只对声明过的 id 放行,未声明直接抛错。
|
||||
|
||||
## 2. 运行时判定(软绑定,无强行绑定)
|
||||
|
||||
宿主在每次激活轮(启停/重载/重建运行时)做三步:
|
||||
|
||||
1. **静态预检**:逐项检查必需依赖——
|
||||
|
||||
| 状态 | 判定 | 插件行展示的错误 |
|
||||
|---|---|---|
|
||||
| missing | resolved 表里没有该 id | `缺少依赖插件 fetch-tools(^1.0.0),请安装后重载` |
|
||||
| disabled | 在禁用列表里 | `依赖插件 browser 已停用,请先启用` |
|
||||
| version | 不满足范围 | `依赖插件 fetch-tools 需要 ^1.0.0,当前 v0.9.0` |
|
||||
| error | 依赖自身装载失败 | `依赖插件 fetch-tools 装载失败,无法激活` |
|
||||
|
||||
2. **环检测**:必需依赖图存在环 → 整环不激活,每个成员报 `循环依赖: a → b → a`。
|
||||
3. **拓扑波次激活**:依赖先激活、依赖者后激活,无依赖关系的插件**同波并行**(异步工厂限时 10s,挂死不拖垮后续波次)。依赖激活失败时,依赖方报 `依赖插件 x 未能激活`。
|
||||
|
||||
行为语义(有意为之):
|
||||
|
||||
- A 声明依赖 B:**禁用 B → A 激活失败**(错误栏写明原因);**重新启用 B → A 自动恢复**(重建运行时天然重放)。
|
||||
- **禁用 A 不影响 B**——依赖不级联、不偷改用户的选择。
|
||||
- **不自动安装**:缺什么报什么,装不装由用户决定(错误文案就是行动指引)。
|
||||
|
||||
## 3. 服务互调(api.service / api.requirePlugin)
|
||||
|
||||
所有插件的 main 工厂运行在同一 pi 上下文,互调是**同进程直调**(无 IPC、无序列化),宿主只提供注册表与门控:
|
||||
|
||||
```ts
|
||||
// fetch-tools 的 main.mjs —— 暴露服务
|
||||
export default definePlugin((api) => {
|
||||
api.service('fetch', async (url: string, init?: RequestInit) => {
|
||||
const res = await fetch(url, init)
|
||||
return res.text()
|
||||
})
|
||||
})
|
||||
|
||||
// wiki-assistant 的 main.mjs —— 消费服务
|
||||
export default definePlugin(async (api) => {
|
||||
const fetchTools = api.requirePlugin('fetch-tools') as
|
||||
| { fetch: (url: string, init?: RequestInit) => Promise<string> }
|
||||
| undefined
|
||||
if (!fetchTools) {
|
||||
// 可选依赖未就绪:降级路径(必需依赖不会走到这里——激活前置检已拦)
|
||||
}
|
||||
api.pi.registerTool({
|
||||
name: 'wiki_lookup',
|
||||
// ...
|
||||
execute: async (args) => {
|
||||
// 服务方法约定 async;调用方自行限时(与宿主性能护栏同构)
|
||||
const body = await withTimeout(fetchTools!.fetch(args.url), 8_000, 'wiki_lookup')
|
||||
// ...
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `api.service(name, impl)`:按名键控;重载/停用时随注册表清空,重激活即重新注册。
|
||||
- `api.requirePlugin(id)`:返回 `{ serviceName: impl }`;未声明 → 抛错;必需依赖未就绪 → 抛错;可选未就绪 → `undefined`。
|
||||
- **服务方法约定全 async**,调用方用 `withTimeout` 限时、必要时 `withRetry`(见 L7)——被调插件慢不能变成调用方卡死。
|
||||
- 依赖可以指向内置插件(如 `browser`),内置与外置同一注册表。
|
||||
|
||||
## 4. UI 展示
|
||||
|
||||
- 管理页插件行名称旁出现**链接图标**,悬停 Tooltip 列出每个依赖:id / 范围 / 必需或可选 / 实时状态(可用·绿 / 未安装·橙 / 已停用·橙 / 版本不符·橙 / 装载失败·红)。
|
||||
- 依赖问题的详细错误在该插件行红字与二级页展示;「去启用」到对应插件的行上操作。
|
||||
|
||||
## 5. 检验器
|
||||
|
||||
`pnpm plugin:check` 会校验:依赖 id 合法、范围语法、不能依赖自身、不能同时出现在必需与可选、声明了依赖但 `minContract < 0.3.0`(旧宿主会静默忽略声明)、调用了 `requirePlugin` 却未声明依赖。
|
||||
Reference in New Issue
Block a user