v5.0.2.9 六项优化 · 设计说明
状态:已实现 · 版本:v5.0.2.9 · 日期:2026-04-15
本文档记录 v5.0.2.9 polish round 的六项联合优化的设计意图、契约边界和测试覆盖,便于后续维护和回归。
概览
| # | 优化项 | 关键模块 | 测试 |
|---|---|---|---|
| 1 | AssistantResponse 钩子 + UserPromptSubmit rewrite/abort | packages/cli/src/lib/session-hooks.js, repl/agent-repl.js | 21 unit + 3 integration + 3 e2e |
| 2 | Skill-Embedded MCP 上下文过滤 | desktop-app-vue/src/main/llm/context-engineering.js | 45 unit + 3 integration |
| 3 | UnifiedToolRegistry 冷启动优化 (deferSkills) | desktop-app-vue/src/main/ai-engine/unified-tool-registry.js | 5 integration |
| 4 | Vitest maxForks=2 OOM 防御 | 6 个 vitest.config.js | — |
| 5 | MCPClient 单服务器 disconnect 别名 | desktop-app-vue/src/main/mcp/mcp-client-manager.js | 31 unit |
| 6 | Category Routing 扩展 embedding/audio | desktop-app-vue/src/main/llm/llm-manager.js | 34 unit |
1. 会话级钩子三件套 · 第二阶段
设计目标
v5.0.2.9 第一阶段(参见 CLAUDE-patterns.md Hooks 三件套段)补齐了 SessionStart / UserPromptSubmit / SessionEnd 三个会话级事件的触发点。本轮补齐:
- AssistantResponse:与
agentLoop()返回值对齐的第四个事件 - UserPromptSubmit 的 rewrite / abort 控制能力:钩子 stdout 返回 JSON
{rewrittenPrompt}或{abort, reason}即可改写或拦截当轮 prompt
契约
SESSION_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "AssistantResponse", "SessionEnd"](冻结数组,typo 直接抛错)。
// 新 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 立即短路
AssistantResponse 在 agentLoop() 返回之后 fire-and-forget:
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.js | 4 个 fire 站点 |
packages/cli/__tests__/unit/session-hooks.test.js | 21 单测 |
packages/cli/__tests__/integration/session-hooks-lifecycle.test.js | 3 集成(4 事件 + 多轮累计 + broken db 容错) |
packages/cli/__tests__/e2e/session-hooks-smoke.test.js | 3 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 }) 新增可选参数:
const result = ctx.buildOptimizedPrompt({
systemPrompt, tools,
activeMcpServers: new Set(["weather"]), // null/undefined → 不过滤(向后兼容)
});过滤规则(私有 _filterMcpToolsByServer):
- 非 MCP 工具(无
source==="mcp"/kind==="mcp"/tags includes "mcp")始终透传 - MCP 工具的 serverName 解析顺序:
tool.serverName→tool.mcpServer→tags中匹配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.js | 45 单测(含本轮新增过滤覆盖) |
desktop-app-vue/tests/integration/skill-mcp-context-filter.integration.test.js | 3 集成 e2e |
3. UnifiedToolRegistry 冷启动优化
设计目标
启动期 138 技能的 SKILL.md 解析 + 分组阻塞了 initialize() 数百毫秒。本轮拆分为:
- Fast init:
initialize({ deferSkills: true })立刻返回(仅导入 FunctionCaller / MCP 工具) - Lazy import:第一个公共读 API 调用(
getSkillManifest/getToolsForLLM/getAllTools/getToolContext/getToolsBySkill)触发技能导入 - Background warm-up:
setImmediate调度,无任何读 API 时也最终导入完成
契约
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.js | 5 集成(fast init / lazy / eager / background) |
4. Vitest maxForks=2 OOM 防御
为避免 forks pool 并行度失控导致 Node 堆耗尽,统一在 6 个 vitest.config.js 设置:
test: { pool: "forks", poolOptions: { forks: { maxForks: 2, minForks: 1 } } }涉及包:packages/core-config、packages/core-db、packages/core-env、packages/core-infra、packages/shared-logger、packages/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。
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 两类。
契约
const LLM_CATEGORIES = {
QUICK: "quick", DEEP: "deep", REASONING: "reasoning",
VISION: "vision", CREATIVE: "creative",
EMBEDDING: "embedding", // 新
AUDIO: "audio", // 新
};| 类别 | 优先级链 | 默认 options |
|---|---|---|
| EMBEDDING | ollama → openai → volcengine → gemini → custom | { requireEmbedding: true } |
| AUDIO | openai → gemini → volcengine → custom → ollama | { requireAudio: true } |
inferCategoryFromModelHints 新增检测:
capability: "embedding"→ EMBEDDINGcapability: "audio" / "speech" / "transcription"→ AUDIOmodelHints.embedding === true→ EMBEDDINGmodelHints.audio === true→ AUDIO
关键设计取舍
- EMBEDDING 默认 ollama 优先:本地 nomic-embed-text 又快又免费,符合"数据不出境"
- AUDIO 默认 openai 优先:whisper / tts 系列质量最高,本地 ollama 暂无成熟语音模型
requireEmbedding/requireAudio标记:未来可用于 provider 能力探测,目前透传
文件位置
| 文件 | 角色 |
|---|---|
desktop-app-vue/src/main/llm/llm-manager.js | LLM_CATEGORIES / CATEGORY_PROVIDER_PRIORITY / CATEGORY_OPTIONS / inferCategoryFromModelHints |
desktop-app-vue/src/main/llm/__tests__/llm-manager-category-routing.test.js | 34 单测(含 EMBEDDING / AUDIO 覆盖) |
测试矩阵汇总
| 层 | 覆盖 |
|---|---|
| Unit | session-hooks 21、context-engineering 45、llm-manager 34、mcp-client-manager 31 |
| Integration | session-hooks-lifecycle 3、skill-mcp-context-filter 3、unified-tool-registry-deferred 5 |
| E2E | session-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 演进
