98 lines
4.6 KiB
Markdown
98 lines
4.6 KiB
Markdown
# 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` 却未声明依赖。
|