# Patch/Clarify 工具 + 工具目录 + 提示词优化 实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. > **Post-task review:** After each task commit, run the `simplify` skill to review changed code for reuse, quality, and efficiency. **Goal:** 为 Neta Agent 引擎新增 patch(模糊补丁)和 clarify(阻塞式提问)工具,引入轻量工具目录,重写提示词提升工具触发率。 **Architecture:** 新建 `tools/catalog.ts` 管理工具 schema 元数据(不管理实例)。patch 工具依赖 `tools/fuzzy_match.ts` 九级匹配引擎。clarify 工具通过 `attempt.ts` 的 `onClarifyRequest` 回调实现阻塞,gateway 层用 Promise+Map 协调 WebSocket 消息,REST 入口降级为文本输出。 **Tech Stack:** TypeScript, @sinclair/typebox, Midway.js Socket.IO, Node.js fs/promises **Base path:** `packages/backend/src/modules/netaclaw` --- ### Task 1: 工具目录 catalog.ts **Files:** - Create: `tools/catalog.ts` - Modify: `runtime/prompt_builder.ts` - [ ] **Step 1: 创建 tools/catalog.ts** ```typescript // tools/catalog.ts /** * 轻量工具目录 — 只注册 schema 元数据,不管理工具实例。 * 用于 prompt_builder 查询可用工具名列表,替代硬编码 BASE_TOOLS。 */ export interface ToolSchema { name: string; toolset: string; description: string; } const catalog = new Map(); export function registerSchema(schema: ToolSchema): void { catalog.set(schema.name, schema); } export function getToolNamesByToolsets(toolsets: string[]): string[] { const names: string[] = []; for (const entry of catalog.values()) { if (toolsets.includes(entry.toolset)) names.push(entry.name); } return names; } /** 默认启用的工具集(所有 Agent 都有) */ export const TOOLSET_DEFAULTS = ['base', 'planning', 'interaction'] as const; ``` - [ ] **Step 2: 修改 prompt_builder.ts — 从 catalog 查询工具名** 将 `runtime/prompt_builder.ts` 中硬编码的 `BASE_TOOLS` 和 `collectAvailableToolNames` 改为从 catalog 查询: ```typescript // runtime/prompt_builder.ts — 替换原有的 BASE_TOOLS 和 collectAvailableToolNames import { getToolNamesByToolsets, TOOLSET_DEFAULTS } from '../tools/catalog.js'; // 删除: const BASE_TOOLS = ['bash', 'read_file', 'write_file', 'list_dir', 'todo']; export function collectAvailableToolNames(opts: CollectToolNamesOpts): string[] { const toolsets = [...TOOLSET_DEFAULTS]; if (opts.memoryEnabled) toolsets.push('memory'); if (opts.hasSkills) toolsets.push('skill'); if (opts.crewRole === 'master') toolsets.push('crew'); return getToolNamesByToolsets(toolsets); } ``` - [ ] **Step 3: 现有工具注册到 catalog — bash.ts** 在 `tools/builtin/bash.ts` 文件末尾追加: ```typescript import { registerSchema } from '../catalog.js'; registerSchema({ name: 'bash', toolset: 'base', description: bashTool.description }); ``` - [ ] **Step 4: 现有工具注册到 catalog — file.ts** 在 `tools/builtin/file.ts` 文件末尾追加: ```typescript import { registerSchema } from '../catalog.js'; registerSchema({ name: 'read_file', toolset: 'base', description: readFileTool.description }); registerSchema({ name: 'write_file', toolset: 'base', description: writeFileTool.description }); registerSchema({ name: 'list_dir', toolset: 'base', description: listDirTool.description }); ``` - [ ] **Step 5: 现有工具注册到 catalog — memory.ts** 在 `tools/builtin/memory.ts` 文件末尾追加: ```typescript import { registerSchema } from '../catalog.js'; registerSchema({ name: 'memory_save', toolset: 'memory', description: '存储、更新或删除长期记忆。' }); registerSchema({ name: 'memory_recall', toolset: 'memory', description: '搜索长期记忆中的相关信息。' }); ``` - [ ] **Step 6: 现有工具注册到 catalog — todo_tool.ts** 在 `tools/todo_tool.ts` 文件末尾追加: ```typescript import { registerSchema } from './catalog.js'; registerSchema({ name: 'todo', toolset: 'planning', description: todoToolSchema.description }); ``` - [ ] **Step 7: 工厂工具注册到 catalog — skill 工具** 在以下文件末尾各追加 `registerSchema` 调用(schema 是静态的,只有 execute 需要运行时依赖): `tools/builtin/read_skill.ts` 末尾: ```typescript import { registerSchema } from '../catalog.js'; registerSchema({ name: 'read_skill', toolset: 'skill', description: '读取指定技能的 SKILL.md 内容' }); ``` `tools/builtin/read_skill_file.ts` 末尾: ```typescript import { registerSchema } from '../catalog.js'; registerSchema({ name: 'read_skill_file', toolset: 'skill', description: '读取技能的附属文件内容' }); ``` `tools/builtin/skill_manage.ts` 末尾: ```typescript import { registerSchema } from '../catalog.js'; registerSchema({ name: 'skill_manage', toolset: 'skill', description: '创建、更新或删除技能' }); ``` - [ ] **Step 8: 工厂工具注册到 catalog — crew 工具** `tools/builtin/delegate_task.ts` 末尾: ```typescript import { registerSchema } from '../catalog.js'; registerSchema({ name: 'delegate_task', toolset: 'crew', description: '委派任务给指定成员 Agent' }); ``` `tools/builtin/delegate_parallel.ts` 末尾: ```typescript import { registerSchema } from '../catalog.js'; registerSchema({ name: 'delegate_parallel', toolset: 'crew', description: '并行委派多个任务给不同成员' }); ``` `tools/builtin/escalate.ts` 末尾: ```typescript import { registerSchema } from '../catalog.js'; registerSchema({ name: 'escalate', toolset: 'crew', description: '将问题升级给用户或上级 Agent' }); ``` - [ ] **Step 9: catalog.ts 自注册所有工具文件** > 消除 side-effect import 维护负担:catalog.ts 自身 import 所有工具文件触发注册。 在 `tools/catalog.ts` 底部追加: ```typescript // --- 集中注册入口:import 触发各工具文件的 registerSchema --- import './builtin/bash.js'; import './builtin/file.js'; import './builtin/patch.js'; import './builtin/clarify.js'; import './builtin/memory.js'; import './builtin/read_skill.js'; import './builtin/read_skill_file.js'; import './builtin/skill_manage.js'; import './builtin/delegate_task.js'; import './builtin/delegate_parallel.js'; import './builtin/escalate.js'; import './todo_tool.js'; ``` > 注意:这些 import 放在 catalog.ts 底部(registerSchema 函数定义之后),确保函数已可用。消费方只需 `import { getToolNamesByToolsets } from '../tools/catalog.js'`。 - [ ] **Step 10: 验证构建** Run: `cd packages/backend && npm run build` Expected: 编译成功,无类型错误 - [ ] **Step 11: Commit** ```bash git add packages/backend/src/modules/netaclaw/tools/catalog.ts \ packages/backend/src/modules/netaclaw/tools/builtin/bash.ts \ packages/backend/src/modules/netaclaw/tools/builtin/file.ts \ packages/backend/src/modules/netaclaw/tools/builtin/memory.ts \ packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts \ packages/backend/src/modules/netaclaw/tools/builtin/read_skill_file.ts \ packages/backend/src/modules/netaclaw/tools/builtin/skill_manage.ts \ packages/backend/src/modules/netaclaw/tools/builtin/delegate_task.ts \ packages/backend/src/modules/netaclaw/tools/builtin/delegate_parallel.ts \ packages/backend/src/modules/netaclaw/tools/builtin/escalate.ts \ packages/backend/src/modules/netaclaw/tools/todo_tool.ts \ packages/backend/src/modules/netaclaw/runtime/prompt_builder.ts \ packages/backend/src/modules/netaclaw/gateway/server.ts \ packages/backend/src/modules/netaclaw/service/agent_executor.ts git commit -m "feat(netaclaw): add tool catalog for schema-based tool name resolution" ``` - [ ] **Step 12: 运行 simplify skill 审查本次变更** --- ### Task 2: 九级模糊匹配引擎 fuzzy_match.ts **Files:** - Create: `tools/fuzzy_match.ts` - [ ] **Step 1: 创建 tools/fuzzy_match.ts — 类型定义和策略框架** ```typescript // tools/fuzzy_match.ts /** * 九级模糊匹配引擎 * 按优先级依次尝试 9 种策略,首个成功即返回。 */ export interface FuzzyMatchResult { strategy: string; startIndex: number; endIndex: number; matchedText: string; } type Strategy = { name: string; find: (content: string, search: string) => FuzzyMatchResult[]; }; /** 在 content 中查找所有匹配 search 的位置,按策略优先级 */ export function fuzzyFindAll(content: string, search: string): FuzzyMatchResult[] { for (const strategy of strategies) { const results = strategy.find(content, search); if (results.length > 0) return results; } return []; } // --- 辅助函数 --- /** Levenshtein 距离 */ function levenshtein(a: string, b: string): number { const m = a.length, n = b.length; const dp: number[][] = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)) ); for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]); } } return dp[m][n]; } /** 两字符串相似度 0-1 */ function similarity(a: string, b: string): number { if (a === b) return 1; const maxLen = Math.max(a.length, b.length); if (maxLen === 0) return 1; return 1 - levenshtein(a, b) / maxLen; } /** 将 content 按 normalizer 转换后查找 search,映射回原始索引 */ function findByNormalization( content: string, search: string, normalizer: (s: string) => string, strategyName: string, ): FuzzyMatchResult[] { const normContent = normalizer(content); const normSearch = normalizer(search); if (normSearch.length === 0) return []; // 构建字符映射:normIndex → origIndex // 逐字符对比原始和标准化后的内容,建立位置映射 const charMap = buildCharMap(content, normContent); const results: FuzzyMatchResult[] = []; let pos = 0; while (true) { const idx = normContent.indexOf(normSearch, pos); if (idx === -1) break; const endIdx = idx + normSearch.length; const origStart = charMap[idx] ?? 0; const origEnd = (endIdx < charMap.length ? charMap[endIdx] : content.length); results.push({ strategy: strategyName, startIndex: origStart, endIndex: origEnd, matchedText: content.slice(origStart, origEnd), }); pos = idx + 1; } return results; } /** * 构建标准化索引 → 原始索引的映射表。 * 对于行级 normalizer(不改变行数),按行对齐映射。 * 对于可能改变行数的 normalizer,逐字符扫描。 */ function buildCharMap(original: string, normalized: string): number[] { // 简单实现:如果行数相同,按行内偏移映射 const origLines = original.split('\n'); const normLines = normalized.split('\n'); if (origLines.length === normLines.length) { // 行级映射:每行内按比例映射 const map: number[] = []; let origOffset = 0; let normOffset = 0; for (let i = 0; i < normLines.length; i++) { const origLen = origLines[i].length; const normLen = normLines[i].length; for (let j = 0; j < normLen; j++) { map[normOffset + j] = origOffset + Math.round((j / Math.max(normLen, 1)) * origLen); } // 换行符映射 if (i < normLines.length - 1) { map[normOffset + normLen] = origOffset + origLen; } origOffset += origLen + 1; normOffset += normLen + 1; } return map; } // 行数不同(如 whitespace_normalized):按字符比例映射 const map: number[] = []; const ratio = original.length / Math.max(normalized.length, 1); for (let i = 0; i < normalized.length; i++) { map[i] = Math.round(i * ratio); } return map; } ``` - [ ] **Step 2: 实现 9 个策略** 在 `tools/fuzzy_match.ts` 底部追加策略数组: ```typescript // --- 9 级策略 --- const strategies: Strategy[] = [ // 1. exact { name: 'exact', find(content, search) { const results: FuzzyMatchResult[] = []; let pos = 0; while (true) { const idx = content.indexOf(search, pos); if (idx === -1) break; results.push({ strategy: 'exact', startIndex: idx, endIndex: idx + search.length, matchedText: search }); pos = idx + 1; } return results; }, }, // 2. line_trimmed — 每行 trim 后匹配 { name: 'line_trimmed', find(content, search) { return findByNormalization(content, search, s => s.split('\n').map(l => l.trim()).join('\n'), 'line_trimmed'); }, }, // 3. whitespace_normalized — 连续空白合并为单空格 { name: 'whitespace_normalized', find(content, search) { return findByNormalization(content, search, s => s.replace(/\s+/g, ' '), 'whitespace_normalized'); }, }, // 4. indent_flexible — 去除行首所有空白 { name: 'indent_flexible', find(content, search) { return findByNormalization(content, search, s => s.split('\n').map(l => l.trimStart()).join('\n'), 'indent_flexible'); }, }, // 5. escape_normalized — \\n → \n, \\t → \t { name: 'escape_normalized', find(content, search) { const norm = (s: string) => s.replace(/\\n/g, '\n').replace(/\\t/g, '\t'); return findByNormalization(content, search, norm, 'escape_normalized'); }, }, // 6. trimmed_boundary — 仅首尾行 trim { name: 'trimmed_boundary', find(content, search) { const norm = (s: string) => { const lines = s.split('\n'); if (lines.length > 0) lines[0] = lines[0].trim(); if (lines.length > 1) lines[lines.length - 1] = lines[lines.length - 1].trim(); return lines.join('\n'); }; return findByNormalization(content, search, norm, 'trimmed_boundary'); }, }, // 7. unicode_normalized — 智能引号/em-dash/省略号 → ASCII { name: 'unicode_normalized', find(content, search) { const norm = (s: string) => s .replace(/[\u2018\u2019\u201A]/g, "'") .replace(/[\u201C\u201D\u201E]/g, '"') .replace(/[\u2013\u2014]/g, '--') .replace(/\u2026/g, '...'); return findByNormalization(content, search, norm, 'unicode_normalized'); }, }, // 8. block_anchor — 首尾行锚定 + 中间 Levenshtein ≥60% { name: 'block_anchor', find(content, search) { const searchLines = search.split('\n'); if (searchLines.length < 3) return []; const firstLine = searchLines[0].trim(); const lastLine = searchLines[searchLines.length - 1].trim(); const contentLines = content.split('\n'); const results: FuzzyMatchResult[] = []; for (let i = 0; i < contentLines.length; i++) { if (contentLines[i].trim() !== firstLine) continue; for (let j = i + searchLines.length - 1; j < contentLines.length && j < i + searchLines.length + 5; j++) { if (contentLines[j].trim() !== lastLine) continue; const candidateMiddle = contentLines.slice(i + 1, j).join('\n'); const searchMiddle = searchLines.slice(1, -1).join('\n'); if (similarity(candidateMiddle, searchMiddle) >= 0.6) { const startIndex = content.split('\n').slice(0, i).join('\n').length + (i > 0 ? 1 : 0); const matchedLines = contentLines.slice(i, j + 1); const matchedText = matchedLines.join('\n'); results.push({ strategy: 'block_anchor', startIndex, endIndex: startIndex + matchedText.length, matchedText }); } } } return results; }, }, // 9. context_aware — 逐行相似度 ≥80%,整体 ≥50% { name: 'context_aware', find(content, search) { const searchLines = search.split('\n'); const contentLines = content.split('\n'); if (searchLines.length === 0) return []; const results: FuzzyMatchResult[] = []; for (let i = 0; i <= contentLines.length - searchLines.length; i++) { let matchCount = 0; for (let j = 0; j < searchLines.length; j++) { if (similarity(contentLines[i + j].trim(), searchLines[j].trim()) >= 0.8) matchCount++; } if (matchCount / searchLines.length >= 0.5) { const startIndex = contentLines.slice(0, i).join('\n').length + (i > 0 ? 1 : 0); const matchedText = contentLines.slice(i, i + searchLines.length).join('\n'); results.push({ strategy: 'context_aware', startIndex, endIndex: startIndex + matchedText.length, matchedText }); } } return results; }, }, ]; ``` - [ ] **Step 3: 验证构建** Run: `cd packages/backend && npm run build` Expected: 编译成功 - [ ] **Step 4: Commit** ```bash git add packages/backend/src/modules/netaclaw/tools/fuzzy_match.ts git commit -m "feat(netaclaw): add 9-level fuzzy match engine for patch tool" ``` - [ ] **Step 5: 运行 simplify skill 审查本次变更** --- ### Task 3: Patch 工具 **Files:** - Create: `tools/builtin/patch.ts` - Modify: `gateway/server.ts` - Modify: `service/agent_executor.ts` - [ ] **Step 1: 创建 tools/builtin/patch.ts** ```typescript // tools/builtin/patch.ts import { Type } from '@sinclair/typebox'; import * as fs from 'fs/promises'; import { AgentToolWithMeta, textResult } from '../common.js'; import { fuzzyFindAll } from '../fuzzy_match.js'; import { registerSchema } from '../catalog.js'; const PatchParams = Type.Object({ path: Type.String({ description: '文件绝对路径' }), old_string: Type.String({ description: '要查找的文本片段' }), new_string: Type.String({ description: '替换为的文本' }), replace_all: Type.Optional(Type.Boolean({ description: '替换所有匹配,默认 false' })), }); export const patchTool: AgentToolWithMeta = { name: 'patch', label: '模糊补丁', description: '对文件进行局部查找替换,支持模糊匹配。比 write_file 更安全、更省 token。', parameters: PatchParams, async execute(_id, params) { try { const content = await fs.readFile(params.path, 'utf-8'); const matches = fuzzyFindAll(content, params.old_string); if (matches.length === 0) { return textResult(`未找到匹配。请检查 old_string 是否正确,或提供更多上下文。`); } if (matches.length > 1 && !params.replace_all) { return textResult(`找到 ${matches.length} 处匹配(策略: ${matches[0].strategy}),请提供更多上下文确保唯一匹配,或设置 replace_all=true。`); } // 从后往前替换,避免索引偏移 let result = content; const sorted = [...matches].sort((a, b) => b.startIndex - a.startIndex); for (const m of sorted) { result = result.slice(0, m.startIndex) + params.new_string + result.slice(m.endIndex); } await fs.writeFile(params.path, result, 'utf-8'); const count = params.replace_all ? matches.length : 1; return textResult(`已替换 ${count} 处(策略: ${matches[0].strategy}): ${params.path}`); } catch (err: any) { return textResult(`patch 失败: ${err.message}`); } }, }; registerSchema({ name: 'patch', toolset: 'base', description: patchTool.description }); ``` - [ ] **Step 2: gateway/server.ts — defaultTools 加入 patchTool** ```typescript // gateway/server.ts 顶部 import 追加 import { patchTool } from '../tools/builtin/patch.js'; // 修改 defaultTools(第 50 行) private defaultTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool]; ``` - [ ] **Step 3: agent_executor.ts — defaultTools 加入 patchTool** ```typescript // service/agent_executor.ts 顶部 import 追加 import { patchTool } from '../tools/builtin/patch.js'; // 修改 defaultTools(第 40 行) private readonly defaultTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool]; ``` - [ ] **Step 3.5: crew_orchestrator.ts + crew_delegate.ts — builtinTools 加入 patchTool** > Crew 子 Agent 也需要 patch 进行文件编辑(但不加 clarify,Crew 无用户交互通道)。 ```typescript // service/crew_orchestrator.ts 顶部 import 追加 import { patchTool } from '../tools/builtin/patch.js'; // 修改第 24 行 BUILTIN_TOOL_NAMES 追加 'patch' const BUILTIN_TOOL_NAMES = ['bash', 'read_file', 'write_file', 'list_dir', 'patch', 'delegate_task', 'delegate_parallel', 'escalate']; // 修改第 175 行 builtinTools 追加 patchTool const builtinTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool]; ``` ```typescript // service/crew_delegate.ts 顶部 import 追加 import { patchTool } from '../tools/builtin/patch.js'; // 修改第 17 行 BUILTIN_TOOL_NAMES 追加 'patch' const BUILTIN_TOOL_NAMES = ['bash', 'read_file', 'write_file', 'list_dir', 'patch']; // 修改第 40 行 builtinTools 追加 patchTool const builtinTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool]; ``` - [ ] **Step 4: 验证构建** Run: `cd packages/backend && npm run build` Expected: 编译成功 - [ ] **Step 5: Commit** ```bash git add packages/backend/src/modules/netaclaw/tools/builtin/patch.ts \ packages/backend/src/modules/netaclaw/gateway/server.ts \ packages/backend/src/modules/netaclaw/service/agent_executor.ts \ packages/backend/src/modules/netaclaw/service/crew_orchestrator.ts \ packages/backend/src/modules/netaclaw/service/crew_delegate.ts git commit -m "feat(netaclaw): add patch tool with fuzzy matching" ``` - [ ] **Step 6: 运行 simplify skill 审查本次变更** --- ### Task 4: Clarify 工具 + WebSocket 协议 + 阻塞机制 **Files:** - Create: `tools/builtin/clarify.ts` - Modify: `gateway/protocol.ts` - Modify: `runtime/attempt.ts` - Modify: `runtime/agent.ts` - Modify: `gateway/server.ts` - [ ] **Step 1: 创建 tools/builtin/clarify.ts** ```typescript // tools/builtin/clarify.ts import { Type } from '@sinclair/typebox'; import { AgentToolWithMeta, textResult } from '../common.js'; import { registerSchema } from '../catalog.js'; const ClarifyParams = Type.Object({ question: Type.String({ description: '要问用户的问题' }), choices: Type.Optional(Type.Array(Type.String(), { maxItems: 4, description: '预设选项(最多4个)' })), }); /** 降级 execute:当无 WebSocket 回调时,返回问题文本让 Agent 作为回复输出 */ export const clarifyTool: AgentToolWithMeta = { name: 'clarify', label: '向用户提问', description: '当任务需求不明确时,向用户提出澄清问题。支持选择题和开放式问题。', parameters: ClarifyParams, async execute(_id, params) { const choicesText = params.choices?.length ? `\n选项: ${params.choices.map((c, i) => `${i + 1}. ${c}`).join(', ')}` : ''; return textResult(`[需要用户确认] ${params.question}${choicesText}`); }, }; registerSchema({ name: 'clarify', toolset: 'interaction', description: clarifyTool.description }); ``` - [ ] **Step 2: 扩展 gateway/protocol.ts** 在 `protocol.ts` 的 `ClientMessage` 和 `ServerEvent` 联合类型中追加: ```typescript // --- 客户端 → 服务端 追加 --- export interface ClientClarifyResponseMessage { type: 'clarify_response'; sessionId: string; requestId: string; answer: string; } export type ClientMessage = ClientChatMessage | ClientPingMessage | ClientSetThinkingLevelMessage | ClientClarifyResponseMessage; // --- 服务端 → 客户端 追加 --- export interface ServerClarifyRequestEvent { type: 'clarify_request'; sessionId: string; data: { requestId: string; question: string; choices?: string[] }; } // ServerEvent 联合类型追加 ServerClarifyRequestEvent ``` - [ ] **Step 3: 修改 runtime/attempt.ts — 新增 onClarifyRequest 回调** ```typescript // attempt.ts — AttemptParams 接口追加字段 export interface AttemptParams { // ... 现有字段不变 ... onClarifyRequest?: (question: string, choices?: string[]) => Promise; } // attempt.ts — 工具执行循环中(第 69-79 行区域),在 tool.execute 之前插入 clarify 分支 if (tc.name === 'clarify' && params.onClarifyRequest) { const args = JSON.parse(tc.arguments); try { resultText = await params.onClarifyRequest(args.question, args.choices); } catch { resultText = '用户未回答,请自行判断并继续。'; } } else if (!tool) { resultText = `错误: 工具 "${tc.name}" 不存在`; } else { try { const result = await tool.execute(tc.id, JSON.parse(tc.arguments)); resultText = typeof result === 'string' ? result : JSON.stringify(result); } catch (err: any) { resultText = `工具执行错误: ${err.message}`; } } ``` - [ ] **Step 4: 修改 runtime/agent.ts — 透传 onClarifyRequest** ```typescript // agent.ts — AgentRunParams 接口追加 export interface AgentRunParams { // ... 现有字段不变 ... onClarifyRequest?: (question: string, choices?: string[]) => Promise; } // agent.ts — runAttempt 调用处(第 86-96 行)追加透传 return runAttempt({ // ... 现有参数不变 ... onClarifyRequest: params.onClarifyRequest, }); ``` - [ ] **Step 5: 修改 gateway/server.ts — 注入 clarify 阻塞回调** > **Critical**: Midway.js `@WSController` 是 request-scope(每连接一个实例),`clarifyResolvers` 必须是**模块级** Map,否则 clarify_response 可能路由到不同实例导致 resolve 永远不被调用。超时设为 20s(前端适配前快速降级)。 ```typescript // gateway/server.ts — 顶部 import 追加 import { randomUUID } from 'crypto'; import { clarifyTool } from '../tools/builtin/clarify.js'; // 模块级 Map(不是实例属性!@WSController 每连接一个实例) const clarifyResolvers = new Map void; timer: NodeJS.Timeout }>(); // defaultTools 追加 clarifyTool private defaultTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool, clarifyTool]; // runAgent 调用处(第 219 行区域)追加 onClarifyRequest onClarifyRequest: async (question, choices) => { const requestId = randomUUID(); this.send({ type: 'clarify_request', sessionId: sid, data: { requestId, question, choices } }); return new Promise((resolve) => { const timer = setTimeout(() => { clarifyResolvers.delete(requestId); resolve('用户未在规定时间内回答。请根据已有信息自行判断并继续执行。'); }, 20_000); // 前端适配前用短超时快速降级 clarifyResolvers.set(requestId, { resolve, timer }); }); }, // onMessage 方法中(第 80 行区域)追加 clarify_response 处理 if (msg.type === 'clarify_response') { const entry = clarifyResolvers.get(msg.requestId); if (entry) { clearTimeout(entry.timer); clarifyResolvers.delete(msg.requestId); entry.resolve(msg.answer); } } ``` - [ ] **Step 6: 验证构建** Run: `cd packages/backend && npm run build` Expected: 编译成功 - [ ] **Step 7: Commit** ```bash git add packages/backend/src/modules/netaclaw/tools/builtin/clarify.ts \ packages/backend/src/modules/netaclaw/gateway/protocol.ts \ packages/backend/src/modules/netaclaw/runtime/attempt.ts \ packages/backend/src/modules/netaclaw/runtime/agent.ts \ packages/backend/src/modules/netaclaw/gateway/server.ts git commit -m "feat(netaclaw): add clarify tool with blocking WebSocket interaction" ``` - [ ] **Step 8: 运行 simplify skill 审查本次变更** --- ### Task 5: 提示词重写 + todo TypeBox 迁移 **Files:** - Modify: `runtime/prompt_guidance.ts` - Modify: `tools/todo_tool.ts` - [ ] **Step 1: 重写 prompt_guidance.ts — TOOL_USE_ENFORCEMENT 改为动态函数** > **Critical**: 原设计硬编码了 clarify/patch 工具名,Crew 子 Agent 没有这些工具会被误导调用不存在的工具。改为根据实际可用工具列表动态生成。 删除 `prompt_guidance.ts` 第 29-34 行的 `export const TOOL_USE_ENFORCEMENT = ...`,替换为: ```typescript /** * 根据实际可用工具列表动态生成工具使用规范。 * Crew 子 Agent 没有 clarify/patch,不应在提示词中出现这些工具。 */ export function getToolUseEnforcement(toolNames: string[]): string { const rules = `# 工具使用规范 你必须通过工具采取行动 - 不要只描述你打算做什么。 ## 强制规则 - 当你说"我来检查"、"让我执行"时,必须在同一回复中立即发起工具调用 - 不要以"下一步我会..."结束回复 - 现在就执行 - 持续工作直到任务真正完成,不要停在计划阶段 - 每条回复要么包含推进任务的工具调用,要么向用户交付最终结果`; const scenarios: string[] = []; if (toolNames.includes('read_file')) scenarios.push('需要读取文件内容时 → read_file(不要凭记忆猜测)'); if (toolNames.includes('patch')) scenarios.push('需要修改已有文件时 → patch(局部修改)或 write_file(新建/全量重写)'); else if (toolNames.includes('write_file')) scenarios.push('需要修改文件时 → write_file'); if (toolNames.includes('list_dir')) scenarios.push('需要了解目录结构时 → list_dir'); if (toolNames.includes('bash')) scenarios.push('需要执行命令时 → bash'); if (toolNames.includes('clarify')) scenarios.push('任务需求不明确时 → clarify(主动提问,不要猜测)'); if (toolNames.includes('todo')) scenarios.push('复杂任务开始前 → todo(创建任务列表)'); return scenarios.length > 0 ? `${rules}\n\n## 必须使用工具的场景\n${scenarios.map(s => `- ${s}`).join('\n')}\n\n## 操作前先确认\n- 修改文件前先 read_file 确认当前内容\n- 执行命令前确认工作目录` : rules; } ``` 同时修改 `runtime/prompt_builder.ts` 中 Layer 2 的调用: ```typescript // prompt_builder.ts — 将 TOOL_USE_ENFORCEMENT 常量引用改为函数调用 // 旧: import { TOOL_USE_ENFORCEMENT, ... } from './prompt_guidance.js'; // 新: import { getToolUseEnforcement, ... } from './prompt_guidance.js'; // Layer 2 中: // 旧: parts.push(TOOL_USE_ENFORCEMENT); // 新: parts.push(getToolUseEnforcement(params.availableToolNames)); ``` - [ ] **Step 2: 重写 prompt_guidance.ts — TOOL_BEHAVIOR** 替换 `prompt_guidance.ts` 第 104-117 行的 `TOOL_BEHAVIOR` 对象: ```typescript const TOOL_BEHAVIOR: Record = { memory_save: `## 记忆使用策略 使用 memory_save 存储对未来对话有价值的持久信息:用户偏好、环境细节、项目约束。 保持记忆紧凑,聚焦于减少用户未来重复纠正的事实。 不要存储任务进度、会话结果、临时 TODO 状态。更新已有记忆而非创建重复条目。`, todo: `## 任务规划策略 收到用户请求后,先评估任务复杂度: **必须使用 todo:** 涉及 2 个以上步骤、修改多个文件、需要先调研再实施、用户一次提出多个需求。 **无需使用 todo:** 单步操作(查看文件、回答问题、一条命令)、简单单文件小修改。 列表顺序=优先级。同一时间只标记一个 in_progress。完成立即标记 completed。 任务列表是你的工作契约 - 它让用户看到你的计划并跟踪进度。`, skill_manage: `## 技能管理策略 完成复杂任务(5步以上工具调用)后,考虑用 skill_manage 将方法保存为技能以便复用。 使用技能时如发现过时或错误,立即修补,不要等用户要求。`, patch: `## 文件编辑策略 修改已有文件时,优先使用 patch 进行局部替换,而非 write_file 全量重写。 **用 patch:** 修改函数、修复 bug、调整配置、添加/删除代码片段。 **用 write_file:** 创建新文件、文件需要完全重写。 old_string 提供足够上下文确保唯一匹配。不需要精确匹配缩进和空白。`, clarify: `## 主动提问策略 任务需求不明确、存在多种合理解读、或缺少关键信息时,使用 clarify 向用户提问。 **应该提问:** 指令模糊有多种解读、缺少关键参数、操作有风险需确认。 **不应提问:** 任务明确可直接执行、可从上下文推断、琐碎实现细节。 提供 choices 选项让用户快速选择。一次只问一个问题。`, }; ``` - [ ] **Step 3: todo_tool.ts — TypeBox 迁移** 替换 `tools/todo_tool.ts` 的 schema 定义(第 3-41 行): ```typescript import { Type, Static } from '@sinclair/typebox'; import { TodoStore } from '../runtime/todo_store.js'; import { registerSchema } from './catalog.js'; const TodoItemSchema = Type.Object({ id: Type.String({ description: '唯一标识' }), content: Type.String({ description: '任务描述' }), status: Type.Union([ Type.Literal('pending'), Type.Literal('in_progress'), Type.Literal('completed'), Type.Literal('cancelled'), ], { description: '当前状态' }), }); const TodoParams = Type.Object({ todos: Type.Optional(Type.Array(TodoItemSchema, { description: '任务项数组。省略则读取当前列表。' })), merge: Type.Optional(Type.Boolean({ description: 'true=按 id 增量更新。false(默认)=全量替换。', default: false })), }); export const todoToolSchema = { name: 'todo' as const, description: '管理当前会话的任务列表。用于复杂任务或用户提供多个任务时。\n' + '不传参数=读取当前列表。传 todos 数组=写入。\n' + 'merge=false(默认)全量替换,merge=true 按 id 增量更新。\n' + '列表顺序=优先级。同一时间只能有一个 in_progress。', parameters: TodoParams, }; type TodoExecArgs = Static; /** 执行 todo 工具 */ export function executeTodo( store: TodoStore, args: TodoExecArgs, ): { todos: any[]; summary: any } { if (args.todos && args.todos.length > 0) { store.write(args.todos, args.merge ?? false); } return { todos: store.read(), summary: store.getSummary(), }; } registerSchema({ name: 'todo', toolset: 'planning', description: todoToolSchema.description }); ``` - [ ] **Step 4: 验证构建** Run: `cd packages/backend && npm run build` Expected: 编译成功 - [ ] **Step 5: Commit** ```bash git add packages/backend/src/modules/netaclaw/runtime/prompt_guidance.ts \ packages/backend/src/modules/netaclaw/tools/todo_tool.ts git commit -m "feat(netaclaw): rewrite tool prompts and migrate todo schema to TypeBox" ``` - [ ] **Step 6: 运行 simplify skill 审查本次变更** --- ### Task 6: 集成验证 **Files:** 无新文件,验证现有改动的集成正确性 - [ ] **Step 1: 完整构建验证** Run: `cd packages/backend && npm run build` Expected: 零错误,零警告 - [ ] **Step 2: 验证 catalog 工具名解析** 在构建产物中检查 `collectAvailableToolNames` 是否正确返回所有工具名。手动验证方式: Run: `cd packages/backend && node -e "import('./dist/modules/netaclaw/tools/builtin/bash.js').then(() => import('./dist/modules/netaclaw/tools/builtin/file.js')).then(() => import('./dist/modules/netaclaw/tools/builtin/patch.js')).then(() => import('./dist/modules/netaclaw/tools/builtin/clarify.js')).then(() => import('./dist/modules/netaclaw/tools/builtin/memory.js')).then(() => import('./dist/modules/netaclaw/tools/todo_tool.js')).then(() => import('./dist/modules/netaclaw/runtime/prompt_builder.js')).then(m => console.log(m.collectAvailableToolNames({ memoryEnabled: true, hasSkills: true, crewRole: 'master' })))"` Expected: `['bash', 'read_file', 'write_file', 'list_dir', 'patch', 'todo', 'clarify', 'memory_save', 'memory_recall', 'read_skill', 'read_skill_file', 'skill_manage', 'delegate_task', 'delegate_parallel', 'escalate']` - [ ] **Step 3: 验证 prompt_guidance 输出** Run: `cd packages/backend && node -e "import('./dist/modules/netaclaw/runtime/prompt_guidance.js').then(m => { console.log('=== ENFORCEMENT ==='); console.log(m.getToolUseEnforcement(['bash','read_file','write_file','list_dir','patch','todo','clarify']).slice(0, 300)); console.log('=== BEHAVIOR ==='); console.log(m.getToolBehaviorGuidance(['todo', 'patch', 'clarify']).slice(0, 300)); })"` Expected: ENFORCEMENT 输出包含所有 7 个工具的场景描述;BEHAVIOR 输出包含 todo/patch/clarify 策略 - [ ] **Step 4: 最终 Commit(如有修复)** ```bash git add -A git commit -m "fix(netaclaw): integration fixes for tool catalog and prompts" ```