# 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 } | 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` 却未声明依赖。