Skip to content

v5.0.2.9 六项优化 · 设计说明

状态:已实现 · 版本:v5.0.2.9 · 日期:2026-04-15

本文档记录 v5.0.2.9 polish round 的六项联合优化的设计意图、契约边界和测试覆盖,便于后续维护和回归。

概览

#优化项关键模块测试
1AssistantResponse 钩子 + UserPromptSubmit rewrite/abortpackages/cli/src/lib/session-hooks.js, repl/agent-repl.js21 unit + 3 integration + 3 e2e
2Skill-Embedded MCP 上下文过滤desktop-app-vue/src/main/llm/context-engineering.js45 unit + 3 integration
3UnifiedToolRegistry 冷启动优化 (deferSkills)desktop-app-vue/src/main/ai-engine/unified-tool-registry.js5 integration
4Vitest maxForks=2 OOM 防御6 个 vitest.config.js
5MCPClient 单服务器 disconnect 别名desktop-app-vue/src/main/mcp/mcp-client-manager.js31 unit
6Category Routing 扩展 embedding/audiodesktop-app-vue/src/main/llm/llm-manager.js34 unit

1. 会话级钩子三件套 · 第二阶段

设计目标

v5.0.2.9 第一阶段(参见 CLAUDE-patterns.md Hooks 三件套段)补齐了 SessionStart / UserPromptSubmit / SessionEnd 三个会话级事件的触发点。本轮补齐:

  1. AssistantResponse:与 agentLoop() 返回值对齐的第四个事件
  2. UserPromptSubmit 的 rewrite / abort 控制能力:钩子 stdout 返回 JSON {rewrittenPrompt}{abort, reason} 即可改写或拦截当轮 prompt

契约

SESSION_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "AssistantResponse", "SessionEnd"](冻结数组,typo 直接抛错)。

js
// 新 helper:fireUserPromptSubmit(hookDb, originalPrompt, context)
//   returns { prompt: string, abort: boolean, reason?: string, results: HookResult[] }
const { prompt, abort, reason } = await fireUserPromptSubmit(_hookDb, trimmed, {
  sessionId, messageCount: messages.length,
});
if (abort) { console.log(`[hook abort] ${reason}`); continue; }
const effectivePrompt = prompt; // 用 hook 改写后的 prompt

钩子 stdout 解析规则:

  • result.stdout / result.output / result.result 任一字段中找 JSON
  • {"rewrittenPrompt": "..."} → 改写后续传给 LLM 的 prompt
  • {"abort": true, "reason": "..."} → 直接跳过本轮,不调用 LLM
  • 多个钩子按优先级链式生效,第一个 abort 立即短路

AssistantResponseagentLoop() 返回之后 fire-and-forget:

js
await fireSessionHook(_hookDb, HookEvents.AssistantResponse, {
  sessionId, response, messageCount, provider, model,
});

关键设计取舍

  • fire-and-forget for AssistantResponse:与 PreToolUse 一致,钩子失败不能影响 REPL
  • Helper 吞错返回 {prompt: original, abort: false}:DB 异常时静默回退,永远不阻断对话
  • 不引入 IPC:保持 helper 纯函数语义,外部 worker 通过钩子注册而非 helper 直连

文件位置

文件角色
packages/cli/src/lib/session-hooks.js唯一触发点;SESSION_HOOK_EVENTS + fireSessionHook + fireUserPromptSubmit
packages/cli/src/repl/agent-repl.js4 个 fire 站点
packages/cli/__tests__/unit/session-hooks.test.js21 单测
packages/cli/__tests__/integration/session-hooks-lifecycle.test.js3 集成(4 事件 + 多轮累计 + broken db 容错)
packages/cli/__tests__/e2e/session-hooks-smoke.test.js3 e2e(子进程导入、null hookDb no-op、rewrite 路径)

2. Skill-Embedded MCP 上下文过滤

设计目标

S3 阶段(参见 CLAUDE-patterns.md Skill-Embedded MCP)让技能在 SKILL.md 内联声明 MCP 服务器。本轮补齐 LLM prompt 端的过滤:当某个技能激活时,只把它 mount 的 MCP 服务器对应的工具暴露给模型,避免 130+ 技能场景下的工具爆炸。

契约

buildOptimizedPrompt({ activeMcpServers }) 新增可选参数:

js
const result = ctx.buildOptimizedPrompt({
  systemPrompt, tools,
  activeMcpServers: new Set(["weather"]), // null/undefined → 不过滤(向后兼容)
});

过滤规则(私有 _filterMcpToolsByServer):

  • 非 MCP 工具(无 source==="mcp" / kind==="mcp" / tags includes "mcp")始终透传
  • MCP 工具的 serverName 解析顺序:tool.serverNametool.mcpServertags 中匹配 server:<name>
  • 解析不到 serverName 的 MCP 工具按"全局"处理保留(避免误伤旧工具)

关键设计取舍

  • Set 或数组皆可:内部统一转 Set 做包含判断,对调用方友好
  • 白名单语义:传入空集合 = 屏蔽全部 MCP 工具;不传 = 旧行为
  • 不污染原数组:返回新数组,避免上层缓存被修改

文件位置

文件角色
desktop-app-vue/src/main/llm/context-engineering.js_filterMcpToolsByServer
desktop-app-vue/src/main/llm/__tests__/context-engineering.test.js45 单测(含本轮新增过滤覆盖)
desktop-app-vue/tests/integration/skill-mcp-context-filter.integration.test.js3 集成 e2e

3. UnifiedToolRegistry 冷启动优化

设计目标

启动期 138 技能的 SKILL.md 解析 + 分组阻塞了 initialize() 数百毫秒。本轮拆分为:

  1. Fast initinitialize({ deferSkills: true }) 立刻返回(仅导入 FunctionCaller / MCP 工具)
  2. Lazy import:第一个公共读 API 调用(getSkillManifest / getToolsForLLM / getAllTools / getToolContext / getToolsBySkill)触发技能导入
  3. Background warm-upsetImmediate 调度,无任何读 API 时也最终导入完成

契约

js
await registry.initialize({ deferSkills: true }); // 不阻塞
// 业务读 API → 自动 _ensureSkillsImported()
const manifest = registry.getSkillManifest();

向后兼容:默认 initialize() 仍走 eager 路径(直接 _importSkills + _autoGroupRemainingTools),原有测试 / 直接访问 registry.tools registry.skills 的代码完全无需修改。

SkillRegistry.update 事件下沉为 _skillsImported = false(懒重新导入),不再 eager re-import。

文件位置

文件角色
desktop-app-vue/src/main/ai-engine/unified-tool-registry.js_doInitialize / _ensureSkillsImported
desktop-app-vue/src/main/ipc/phases/phase-9-15-core.js生产 wiring:{ deferSkills: true }
desktop-app-vue/tests/integration/unified-tool-registry-deferred.integration.test.js5 集成(fast init / lazy / eager / background)

4. Vitest maxForks=2 OOM 防御

为避免 forks pool 并行度失控导致 Node 堆耗尽,统一在 6 个 vitest.config.js 设置:

js
test: { pool: "forks", poolOptions: { forks: { maxForks: 2, minForks: 1 } } }

涉及包:packages/core-configpackages/core-dbpackages/core-envpackages/core-infrapackages/shared-loggerpackages/web-panel。其他包(cli / desktop-app-vue)原本就有 OOM 友好配置。

参见 CLAUDE-troubleshooting.md "Vitest 并行执行 OOM" 段。


5. MCPClient 单服务器 disconnect 别名

MCPClientManager.disconnect(name)disconnectServer(name) 的别名,配合 mountSkillMcpServers / unmountSkillMcpServers 的"按需 mount"流程。unmountSkillMcpServers 优先调 disconnect,回退到 disconnectAll

js
async disconnect(name) { return this.disconnectServer(name); }

测试:desktop-app-vue/tests/unit/mcp/mcp-client-manager.test.js (31 tests,含 alias 用例)。


6. Category Routing 扩展 embedding / audio

设计目标

Category Routing S2(参见 CLAUDE-patterns.md 类别路由)原有 5 类别(quick/deep/reasoning/vision/creative)覆盖文本生成场景,但 RAG embedding 调用与语音转写 / TTS 调用没有归类,依旧硬编码 provider。本轮补齐 embedding / audio 两类。

契约

js
const LLM_CATEGORIES = {
  QUICK: "quick", DEEP: "deep", REASONING: "reasoning",
  VISION: "vision", CREATIVE: "creative",
  EMBEDDING: "embedding", // 新
  AUDIO: "audio",         // 新
};
类别优先级链默认 options
EMBEDDINGollama → openai → volcengine → gemini → custom{ requireEmbedding: true }
AUDIOopenai → gemini → volcengine → custom → ollama{ requireAudio: true }

inferCategoryFromModelHints 新增检测:

  • capability: "embedding" → EMBEDDING
  • capability: "audio" / "speech" / "transcription" → AUDIO
  • modelHints.embedding === true → EMBEDDING
  • modelHints.audio === true → AUDIO

关键设计取舍

  • EMBEDDING 默认 ollama 优先:本地 nomic-embed-text 又快又免费,符合"数据不出境"
  • AUDIO 默认 openai 优先:whisper / tts 系列质量最高,本地 ollama 暂无成熟语音模型
  • requireEmbedding / requireAudio 标记:未来可用于 provider 能力探测,目前透传

文件位置

文件角色
desktop-app-vue/src/main/llm/llm-manager.jsLLM_CATEGORIES / CATEGORY_PROVIDER_PRIORITY / CATEGORY_OPTIONS / inferCategoryFromModelHints
desktop-app-vue/src/main/llm/__tests__/llm-manager-category-routing.test.js34 单测(含 EMBEDDING / AUDIO 覆盖)

测试矩阵汇总

覆盖
Unitsession-hooks 21、context-engineering 45、llm-manager 34、mcp-client-manager 31
Integrationsession-hooks-lifecycle 3、skill-mcp-context-filter 3、unified-tool-registry-deferred 5
E2Esession-hooks-smoke 3
总计145 测试,全绿

兼容性

  • 所有改动均向后兼容:未传 activeMcpServers / 未启用 deferSkills / 不订阅 AssistantResponse 钩子时,行为与 v5.0.2.9 之前完全一致。
  • 旧 SKILL.md 不需要迁移;旧 hookDb registry 不需要迁移;旧 MCP 调用不需要修改。

相关文档

  • CLAUDE-patterns.md — Hooks 三件套 / Skill-Embedded MCP / Category Routing 模式
  • CLAUDE-troubleshooting.md — Vitest OOM 防御
  • docs/design/HOOKS_SYSTEM_DESIGN.md — 钩子系统总体设计
  • docs/design/modules/16_AI技能系统.md — 技能系统
  • docs/design/modules/82_CLI_Runtime收口路线图.md — CLI runtime 演进

基于 MIT 许可发布