Skip to content

15 多插件冲突治理与优先级规范实战(让插件生态可控)

这篇讲“插件多了之后怎么不乱”。

核心源码入口

  • src/plugins/manifest-registry.ts
  • src/plugins/loader.ts
  • src/plugins/config-state.ts
  • src/plugins/registry.ts
  • src/plugins/hooks.ts
  • src/plugins/commands.ts
  • src/config/validation.ts
  • src/cli/plugins-cli.ts

总体裁决思路

来源优先级 -> 启用决策 -> 槽位排他 -> 注册冲突拦截 -> 诊断输出

模块一 来源优先级与重复 ID 裁决

manifest-registry.ts 里明确了来源排名:

  1. config
  2. workspace
  3. global
  4. bundled

即:config > workspace > global > bundled

同 ID 冲突时做两步判断:

  1. realpath 相同:视为同一插件的路径别名,允许静默去重
  2. realpath 不同:按来源优先级选赢家,并写 duplicate warning

这避免了“软链路径误报冲突”。

模块二 启用状态决策

normalizePluginsConfig(...) 统一解析配置:

  1. enabled
  2. allow/deny
  3. load.paths
  4. slots.memory
  5. entries.<id>.enabled/config

resolveEnableState(...) 返回 {enabled, reason},典型 reason:

  1. plugins disabled
  2. blocked by denylist
  3. not in allowlist
  4. bundled (disabled by default)

模块三 memory slot 排他治理

resolveMemorySlotDecision(...) 保证 memory 插件单选:

  1. slot = null:memory 能力整体禁用
  2. slot = <id>:只允许指定插件
  3. 自动选择模式:第一个占位,后续 memory 插件被拒绝

loader.ts 还会做 memorySlotMatched 检查,slot 指向不存在插件时输出 warn。

模块四 注册阶段冲突拦截

registry.ts 在注册期硬拦截:

  1. registerGatewayMethod(...)
  • 与 core method 或已注册 method 冲突即拒绝
  1. registerHttpRoute(...)
  • path 重复即拒绝
  1. registerCommand(...)
  • 委托 registerPluginCommand(...) 校验重名和非法命令

冲突不会“后者覆盖前者”,而是明确失败并留诊断。

模块五 Hook 冲突治理

plugins/hooks.ts 规则:

  1. getHooksForName(...) 按 priority 降序
  2. runModifyingHook(...) 顺序执行 + merge
  3. runVoidHook(...) 并行执行

这样既保证安全插件优先,又不牺牲观察型 hook 吞吐。

模块六 诊断闭环

所有问题汇总到 registry.diagnostics

  1. loader 错误
  2. manifest 冲突
  3. 注册冲突
  4. 配置 schema 错误

然后:

  1. config/validation.ts 映射为配置 issue
  2. plugins list/doctor 输出可读结论和修复提示

最小复刻骨架

ts
type Origin = "config" | "workspace" | "global" | "bundled";
const RANK: Record<Origin, number> = { config: 0, workspace: 1, global: 2, bundled: 3 };

function pickByOrigin(a: Candidate, b: Candidate) {
  return RANK[a.origin] <= RANK[b.origin] ? a : b;
}

function resolveEnabled(id: string, cfg: NormalizedCfg): { enabled: boolean; reason?: string } {
  if (!cfg.enabled) return { enabled: false, reason: "plugins disabled" };
  if (cfg.deny.includes(id)) return { enabled: false, reason: "blocked by denylist" };
  if (cfg.allow.length && !cfg.allow.includes(id)) return { enabled: false, reason: "not in allowlist" };
  return { enabled: true };
}

function assertUniqueMethod(name: string, core: Set<string>, exists: Set<string>) {
  if (!name.trim()) throw new Error("missing method");
  if (core.has(name) || exists.has(name)) throw new Error(`gateway method already registered: ${name}`);
}

自检清单

  1. 同 ID 不同来源时是否按固定优先级裁决。
  2. 同一插件不同路径别名是否不会误报冲突。
  3. memory slot 是否严格单选。
  4. method/route/command 冲突是否明确报错。
  5. diagnostics 是否能在 CLI 一眼看出原因。
  6. Hook priority 是否能稳定压过低优先级插件。

用工程视角拆解 AI 智能体框架