GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-16-tool-registry-patch-clarify.md

988 lines
36 KiB
Markdown
Raw Normal View History

2026-05-20 21:39:12 +08:00
# 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<string, ToolSchema>();
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<typeof PatchParams, unknown> = {
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 进行文件编辑(但不加 clarifyCrew 无用户交互通道)。
```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<typeof ClarifyParams, unknown> = {
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<string>;
}
// 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<string>;
}
// 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<string, { resolve: (answer: string) => 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<string>((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<string, string> = {
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<typeof TodoParams>;
/** 执行 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"
```