GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-16-tool-registry-patch-clarify.md
2026-05-20 21:39:12 +08:00

988 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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"
```