diff --git a/packages/core/src/editors/applier.ts b/packages/core/src/editors/applier.ts index 5e618e9..c51260f 100644 --- a/packages/core/src/editors/applier.ts +++ b/packages/core/src/editors/applier.ts @@ -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 应用后的新内容 */ diff --git a/packages/core/src/editors/parsers.ts b/packages/core/src/editors/parsers.ts index 983ee37..2b01af5 100644 --- a/packages/core/src/editors/parsers.ts +++ b/packages/core/src/editors/parsers.ts @@ -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 }], }; } diff --git a/packages/core/src/editors/types.ts b/packages/core/src/editors/types.ts index f69132a..f4b0608 100644 --- a/packages/core/src/editors/types.ts +++ b/packages/core/src/editors/types.ts @@ -20,6 +20,8 @@ export interface SearchReplaceBlock { search: string; /** 替换后的新字符串 */ replace: string; + /** 是否替换所有匹配项(默认 false,只替换唯一匹配) */ + replaceAll?: boolean; } /** diff --git a/packages/core/src/editors/validator.ts b/packages/core/src/editors/validator.ts index 7d9ac74..9950def 100644 --- a/packages/core/src/editors/validator.ts +++ b/packages/core/src/editors/validator.ts @@ -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, }); diff --git a/packages/core/src/tools/filesystem/edit_file.ts b/packages/core/src/tools/filesystem/edit_file.ts index c71584b..d876d3b 100644 --- a/packages/core/src/tools/filesystem/edit_file.ts +++ b/packages/core/src/tools/filesystem/edit_file.ts @@ -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): Promise => { - 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);