feat(core): 优化 edit_file 工具参数结构

- 将参数 path 重命名为 file_path
- 添加 replace_all 参数支持替换所有匹配项
- 更新 SearchReplaceBlock 类型支持 replaceAll 选项
- 修改验证器和应用器支持全局替换模式
This commit is contained in:
2025-12-18 00:41:34 +08:00
parent 014dd2e6fc
commit 22a19426ef
5 changed files with 92 additions and 25 deletions
+71 -14
View File
@@ -186,12 +186,14 @@ function computeSearchReplaceContent(
let content = originalContent;
for (const block of edit.blocks) {
const { search, replace, replaceAll } = block;
// 首先尝试直接匹配
let positions = findSearchPositions(content, block.search);
let positions = findSearchPositions(content, search);
// 如果没有找到,尝试规范化后匹配
if (positions.length === 0) {
const normalizedSearch = normalizeSearchString(block.search, {
const normalizedSearch = normalizeSearchString(search, {
trimTrailingWhitespace: true,
normalizeLineEndings: true,
});
@@ -204,32 +206,41 @@ function computeSearchReplaceContent(
const normalizedPositions = findSearchPositions(normalizedContent, normalizedSearch);
if (normalizedPositions.length === 1) {
// 找到规范化匹配,需要在原始内容中找到对应位置
// 使用逐行匹配的方式
content = replaceWithNormalization(content, block.search, block.replace);
if (normalizedPositions.length >= 1) {
// 找到规范化匹配
if (replaceAll) {
// replaceAll 模式:替换所有匹配
content = replaceAllWithNormalization(content, search, replace);
} else if (normalizedPositions.length === 1) {
// 单一匹配:使用逐行匹配的方式
content = replaceWithNormalization(content, search, replace);
} else {
throw new Error(`找到 ${normalizedPositions.length} 处匹配,请提供更多上下文或使用 replace_all`);
}
continue;
}
if (normalizedPositions.length === 0) {
throw new Error(`未找到要替换的内容`);
}
if (normalizedPositions.length > 1) {
throw new Error(`找到 ${normalizedPositions.length} 处匹配,请提供更多上下文`);
}
}
if (positions.length === 0) {
throw new Error(`未找到要替换的内容`);
}
if (positions.length > 1) {
throw new Error(`找到 ${positions.length} 处匹配,请提供更多上下文`);
// replaceAll 模式:替换所有匹配
if (replaceAll) {
content = content.split(search).join(replace);
continue;
}
// 执行替换
content = content.replace(block.search, block.replace);
if (positions.length > 1) {
throw new Error(`找到 ${positions.length} 处匹配,请提供更多上下文或使用 replace_all`);
}
// 执行单个替换
content = content.replace(search, replace);
}
return content;
@@ -273,6 +284,52 @@ function replaceWithNormalization(
throw new Error('规范化替换失败');
}
/**
* 使用规范化方式替换所有匹配的内容
*/
function replaceAllWithNormalization(
content: string,
search: string,
replace: string
): string {
const searchLines = search.split('\n');
const replaceLines = replace.split('\n');
const contentLines = content.split('\n');
const resultLines: string[] = [];
// 找到第一行的匹配位置
const firstSearchLine = searchLines[0].trimEnd();
let i = 0;
while (i < contentLines.length) {
if (
i <= contentLines.length - searchLines.length &&
contentLines[i].trimEnd() === firstSearchLine
) {
// 检查后续行是否都匹配
let allMatch = true;
for (let j = 1; j < searchLines.length; j++) {
if (contentLines[i + j].trimEnd() !== searchLines[j].trimEnd()) {
allMatch = false;
break;
}
}
if (allMatch) {
// 找到匹配,执行替换
resultLines.push(...replaceLines);
i += searchLines.length;
continue;
}
}
resultLines.push(contentLines[i]);
i++;
}
return resultLines.join('\n');
}
/**
* 计算 diff 应用后的新内容
*/
+3 -2
View File
@@ -95,12 +95,13 @@ export function createSearchReplaceEdit(
export function createSingleSearchReplaceEdit(
filePath: string,
search: string,
replace: string
replace: string,
replaceAll = false
): SearchReplaceEdit {
return {
mode: 'search-replace',
filePath,
blocks: [{ search, replace }],
blocks: [{ search, replace, replaceAll }],
};
}
+2
View File
@@ -20,6 +20,8 @@ export interface SearchReplaceBlock {
search: string;
/** 替换后的新字符串 */
replace: string;
/** 是否替换所有匹配项(默认 false,只替换唯一匹配) */
replaceAll?: boolean;
}
/**
+4 -3
View File
@@ -135,7 +135,7 @@ function validateSearchReplaceBlock(
errors: EditValidationError[],
warnings: EditValidationWarning[]
): void {
const { search, replace } = block;
const { search, replace, replaceAll } = block;
// 检查搜索字符串是否为空
if (!search || search.length === 0) {
@@ -186,7 +186,8 @@ function validateSearchReplaceBlock(
return;
}
if (positions.length > 1) {
// 如果不是 replaceAll 模式,检查是否唯一
if (!replaceAll && positions.length > 1) {
// 找到多个匹配
const lineNumbers = positions.map(pos =>
fileContent.slice(0, pos).split('\n').length
@@ -194,7 +195,7 @@ function validateSearchReplaceBlock(
errors.push({
type: 'ambiguous',
message: `找到 ${positions.length} 处匹配(行 ${lineNumbers.join(', ')})。请提供更多上下文使搜索字符串唯一`,
message: `找到 ${positions.length} 处匹配(行 ${lineNumbers.join(', ')})。请提供更多上下文使搜索字符串唯一,或使用 replace_all 替换所有匹配`,
search: search.length > 100 ? search.slice(0, 100) + '...' : search,
occurrences: positions.length,
});
@@ -26,33 +26,39 @@ export const editFileTool: ToolWithMetadata = {
deferLoading: false, // 核心工具,始终可用
},
parameters: {
path: {
file_path: {
type: 'string',
description: '要编辑的文件路径',
description: 'The absolute path to the file to modify',
required: true,
},
old_string: {
type: 'string',
description: '要被替换的原始字符串(必须精确匹配)',
description: 'The text to replace',
required: true,
},
new_string: {
type: 'string',
description: '替换后的新字符串',
description: 'The text to replace it with (must be different from old_string)',
required: true,
},
replace_all: {
type: 'boolean',
description: 'Replace all occurences of old_string (default false)',
required: false,
},
},
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
const filePath = params.path as string;
const filePath = params.file_path as string;
const oldString = params.old_string as string;
const newString = params.new_string as string;
const replaceAll = (params.replace_all as boolean) ?? false;
const cwd = process.cwd();
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.join(cwd, filePath);
// 创建编辑对象
const edit = createSingleSearchReplaceEdit(absolutePath, oldString, newString);
const edit = createSingleSearchReplaceEdit(absolutePath, oldString, newString, replaceAll);
// 先验证
const validation = await validateEdit(edit);