feat(core): 优化 edit_file 工具参数结构
- 将参数 path 重命名为 file_path - 添加 replace_all 参数支持替换所有匹配项 - 更新 SearchReplaceBlock 类型支持 replaceAll 选项 - 修改验证器和应用器支持全局替换模式
This commit is contained in:
@@ -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 应用后的新内容
|
||||
*/
|
||||
|
||||
@@ -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 }],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface SearchReplaceBlock {
|
||||
search: string;
|
||||
/** 替换后的新字符串 */
|
||||
replace: string;
|
||||
/** 是否替换所有匹配项(默认 false,只替换唯一匹配) */
|
||||
replaceAll?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user