feat(core): 实现先读后写验证机制
确保 write_file 和 edit_file 修改已存在的文件前必须先调用 read_file - 在 AgentToolExecutor 中添加 readFiles 状态跟踪已读文件 - 创建 read-before-write.ts hook 拦截写操作并验证 - 在 Agent 初始化时注册验证 hook - 提供 AI 友好的错误消息引导正确操作
This commit is contained in:
@@ -66,6 +66,7 @@ export interface ToolExecutionContext {
|
||||
export class AgentToolExecutor {
|
||||
private registry: ToolRegistry;
|
||||
private discoveredTools: Set<string> = new Set();
|
||||
private readFiles: Set<string> = new Set();
|
||||
private toolDescriptionContext: PromptContext | null = null;
|
||||
private currentAgentMode: AgentInfo | null = null;
|
||||
|
||||
@@ -393,6 +394,45 @@ export class AgentToolExecutor {
|
||||
this.discoveredTools.clear();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 文件读取跟踪(用于"先读后写"验证)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 记录文件已被读取
|
||||
*/
|
||||
recordFileRead(absolutePath: string): void {
|
||||
this.readFiles.add(absolutePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否已被读取
|
||||
*/
|
||||
hasFileBeenRead(absolutePath: string): boolean {
|
||||
return this.readFiles.has(absolutePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已读取的文件列表
|
||||
*/
|
||||
getReadFiles(): string[] {
|
||||
return [...this.readFiles];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置已读取的文件(用于会话恢复)
|
||||
*/
|
||||
setReadFiles(files: string[]): void {
|
||||
this.readFiles = new Set(files);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除已读取的文件记录
|
||||
*/
|
||||
clearReadFiles(): void {
|
||||
this.readFiles.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具数量统计
|
||||
*/
|
||||
|
||||
@@ -23,6 +23,8 @@ import { AgentToolExecutor, type ToolStartInfo, type ToolEndInfo, type WaitingFo
|
||||
import { AgentMessageHandler, type DoomLoopInfo } from './agent-message-handler.js';
|
||||
import { AgentModeManager } from './agent-mode-manager.js';
|
||||
import { AgentVisionHandler } from './agent-vision-handler.js';
|
||||
import { getHookManager } from '../hooks/index.js';
|
||||
import { createReadBeforeWriteHook } from '../hooks/read-before-write.js';
|
||||
|
||||
// 重新导出类型
|
||||
export type { ToolStartInfo, ToolEndInfo, DoomLoopInfo, WaitingForInputInfo };
|
||||
@@ -122,6 +124,25 @@ export class Agent {
|
||||
setRegistry(registry: ToolRegistry): void {
|
||||
this.toolExecutor = new AgentToolExecutor(registry);
|
||||
this.visionHandler.setRegistry(registry);
|
||||
|
||||
// 注册 "先读后写" 验证 Hook
|
||||
this.registerReadBeforeWriteHook();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 "先读后写" 验证 Hook
|
||||
* 确保 write_file/edit_file 修改已存在的文件前必须先调用 read_file
|
||||
*/
|
||||
private registerReadBeforeWriteHook(): void {
|
||||
const hookManager = getHookManager();
|
||||
if (!hookManager || !this.toolExecutor) return;
|
||||
|
||||
const hook = createReadBeforeWriteHook(
|
||||
() => this.toolExecutor,
|
||||
() => process.cwd()
|
||||
);
|
||||
|
||||
hookManager.registerHooks(hook);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,6 +23,9 @@ export {
|
||||
type ProjectConfig,
|
||||
} from './config-loader.js';
|
||||
|
||||
// Read-Before-Write Hook
|
||||
export { createReadBeforeWriteHook } from './read-before-write.js';
|
||||
|
||||
// 类型导出
|
||||
export type {
|
||||
HookType,
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Read-Before-Write 验证 Hook
|
||||
*
|
||||
* 确保 write_file 和 edit_file 操作修改已存在的文件前,
|
||||
* 必须先调用 read_file 读取该文件。
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import type {
|
||||
Hooks,
|
||||
ToolExecuteBeforeInput,
|
||||
ToolExecuteBeforeOutput,
|
||||
ToolExecuteAfterInput,
|
||||
ToolExecuteAfterOutput,
|
||||
} from './types.js';
|
||||
import type { AgentToolExecutor } from '../core/agent-tool-executor.js';
|
||||
|
||||
/**
|
||||
* 规范化文件路径为绝对路径
|
||||
*/
|
||||
function normalizeFilePath(filePath: string, cwd: string): string {
|
||||
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
||||
// 解析 '..' 和 '.' 确保路径一致
|
||||
return path.resolve(absolutePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
*/
|
||||
async function fileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 AI 友好的错误消息
|
||||
*/
|
||||
function buildErrorMessage(tool: string, filePath: string, absolutePath: string): string {
|
||||
const action = tool === 'write_file' ? '写入' : '编辑';
|
||||
const actionEn = tool === 'write_file' ? 'write to' : 'edit';
|
||||
const toolName = tool === 'write_file' ? 'write_file' : 'edit_file';
|
||||
|
||||
return `[Read-Before-Write Error]
|
||||
|
||||
Cannot ${actionEn} existing file without reading it first.
|
||||
|
||||
File: ${filePath}
|
||||
Absolute path: ${absolutePath}
|
||||
|
||||
Before using ${toolName} on an existing file, you MUST first call read_file to understand its current content.
|
||||
|
||||
Required action:
|
||||
1. Call read_file with path: "${filePath}"
|
||||
2. Review the file content
|
||||
3. Then retry ${toolName} with appropriate changes
|
||||
|
||||
This ensures you don't accidentally overwrite important content.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Read-Before-Write 验证 Hook
|
||||
*
|
||||
* @param getToolExecutor - 获取 AgentToolExecutor 实例的函数
|
||||
* @param getCwd - 获取当前工作目录的函数
|
||||
*/
|
||||
export function createReadBeforeWriteHook(
|
||||
getToolExecutor: () => AgentToolExecutor | null,
|
||||
getCwd: () => string
|
||||
): Hooks {
|
||||
return {
|
||||
'tool.execute.before': async (
|
||||
input: ToolExecuteBeforeInput,
|
||||
output: ToolExecuteBeforeOutput
|
||||
): Promise<void> => {
|
||||
const { tool, args } = input;
|
||||
|
||||
// 只验证 write_file 和 edit_file
|
||||
if (tool !== 'write_file' && tool !== 'edit_file') {
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = args.path as string;
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cwd = getCwd();
|
||||
const absolutePath = normalizeFilePath(filePath, cwd);
|
||||
|
||||
// 检查文件是否存在
|
||||
const exists = await fileExists(absolutePath);
|
||||
|
||||
// 如果文件不存在,write_file 是创建新文件 - 允许
|
||||
if (!exists && tool === 'write_file') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果文件不存在但调用 edit_file,让工具自己处理这个错误
|
||||
if (!exists && tool === 'edit_file') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 文件存在 - 检查是否已读取
|
||||
const toolExecutor = getToolExecutor();
|
||||
if (!toolExecutor) {
|
||||
return; // 没有 executor 无法验证
|
||||
}
|
||||
|
||||
if (!toolExecutor.hasFileBeenRead(absolutePath)) {
|
||||
// 阻止操作
|
||||
output.skip = true;
|
||||
output.skipResult = {
|
||||
success: false,
|
||||
output: '',
|
||||
error: buildErrorMessage(tool, filePath, absolutePath),
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
'tool.execute.after': async (
|
||||
input: ToolExecuteAfterInput,
|
||||
output: ToolExecuteAfterOutput
|
||||
): Promise<void> => {
|
||||
const { tool, args } = input;
|
||||
|
||||
// 跟踪成功的 read_file 调用
|
||||
if (tool === 'read_file' && output.result.success) {
|
||||
const filePath = args.path as string;
|
||||
if (!filePath) return;
|
||||
|
||||
const cwd = getCwd();
|
||||
const absolutePath = normalizeFilePath(filePath, cwd);
|
||||
|
||||
const toolExecutor = getToolExecutor();
|
||||
if (toolExecutor) {
|
||||
toolExecutor.recordFileRead(absolutePath);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user