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;
|
let content = originalContent;
|
||||||
|
|
||||||
for (const block of edit.blocks) {
|
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) {
|
if (positions.length === 0) {
|
||||||
const normalizedSearch = normalizeSearchString(block.search, {
|
const normalizedSearch = normalizeSearchString(search, {
|
||||||
trimTrailingWhitespace: true,
|
trimTrailingWhitespace: true,
|
||||||
normalizeLineEndings: true,
|
normalizeLineEndings: true,
|
||||||
});
|
});
|
||||||
@@ -204,32 +206,41 @@ function computeSearchReplaceContent(
|
|||||||
|
|
||||||
const normalizedPositions = findSearchPositions(normalizedContent, normalizedSearch);
|
const normalizedPositions = findSearchPositions(normalizedContent, normalizedSearch);
|
||||||
|
|
||||||
if (normalizedPositions.length === 1) {
|
if (normalizedPositions.length >= 1) {
|
||||||
// 找到规范化匹配,需要在原始内容中找到对应位置
|
// 找到规范化匹配
|
||||||
// 使用逐行匹配的方式
|
if (replaceAll) {
|
||||||
content = replaceWithNormalization(content, block.search, block.replace);
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedPositions.length === 0) {
|
if (normalizedPositions.length === 0) {
|
||||||
throw new Error(`未找到要替换的内容`);
|
throw new Error(`未找到要替换的内容`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedPositions.length > 1) {
|
|
||||||
throw new Error(`找到 ${normalizedPositions.length} 处匹配,请提供更多上下文`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (positions.length === 0) {
|
if (positions.length === 0) {
|
||||||
throw new Error(`未找到要替换的内容`);
|
throw new Error(`未找到要替换的内容`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (positions.length > 1) {
|
// replaceAll 模式:替换所有匹配
|
||||||
throw new Error(`找到 ${positions.length} 处匹配,请提供更多上下文`);
|
if (replaceAll) {
|
||||||
|
content = content.split(search).join(replace);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行替换
|
if (positions.length > 1) {
|
||||||
content = content.replace(block.search, block.replace);
|
throw new Error(`找到 ${positions.length} 处匹配,请提供更多上下文或使用 replace_all`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行单个替换
|
||||||
|
content = content.replace(search, replace);
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
@@ -273,6 +284,52 @@ function replaceWithNormalization(
|
|||||||
throw new Error('规范化替换失败');
|
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 应用后的新内容
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -95,12 +95,13 @@ export function createSearchReplaceEdit(
|
|||||||
export function createSingleSearchReplaceEdit(
|
export function createSingleSearchReplaceEdit(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
search: string,
|
search: string,
|
||||||
replace: string
|
replace: string,
|
||||||
|
replaceAll = false
|
||||||
): SearchReplaceEdit {
|
): SearchReplaceEdit {
|
||||||
return {
|
return {
|
||||||
mode: 'search-replace',
|
mode: 'search-replace',
|
||||||
filePath,
|
filePath,
|
||||||
blocks: [{ search, replace }],
|
blocks: [{ search, replace, replaceAll }],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export interface SearchReplaceBlock {
|
|||||||
search: string;
|
search: string;
|
||||||
/** 替换后的新字符串 */
|
/** 替换后的新字符串 */
|
||||||
replace: string;
|
replace: string;
|
||||||
|
/** 是否替换所有匹配项(默认 false,只替换唯一匹配) */
|
||||||
|
replaceAll?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ function validateSearchReplaceBlock(
|
|||||||
errors: EditValidationError[],
|
errors: EditValidationError[],
|
||||||
warnings: EditValidationWarning[]
|
warnings: EditValidationWarning[]
|
||||||
): void {
|
): void {
|
||||||
const { search, replace } = block;
|
const { search, replace, replaceAll } = block;
|
||||||
|
|
||||||
// 检查搜索字符串是否为空
|
// 检查搜索字符串是否为空
|
||||||
if (!search || search.length === 0) {
|
if (!search || search.length === 0) {
|
||||||
@@ -186,7 +186,8 @@ function validateSearchReplaceBlock(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (positions.length > 1) {
|
// 如果不是 replaceAll 模式,检查是否唯一
|
||||||
|
if (!replaceAll && positions.length > 1) {
|
||||||
// 找到多个匹配
|
// 找到多个匹配
|
||||||
const lineNumbers = positions.map(pos =>
|
const lineNumbers = positions.map(pos =>
|
||||||
fileContent.slice(0, pos).split('\n').length
|
fileContent.slice(0, pos).split('\n').length
|
||||||
@@ -194,7 +195,7 @@ function validateSearchReplaceBlock(
|
|||||||
|
|
||||||
errors.push({
|
errors.push({
|
||||||
type: 'ambiguous',
|
type: 'ambiguous',
|
||||||
message: `找到 ${positions.length} 处匹配(行 ${lineNumbers.join(', ')})。请提供更多上下文使搜索字符串唯一`,
|
message: `找到 ${positions.length} 处匹配(行 ${lineNumbers.join(', ')})。请提供更多上下文使搜索字符串唯一,或使用 replace_all 替换所有匹配`,
|
||||||
search: search.length > 100 ? search.slice(0, 100) + '...' : search,
|
search: search.length > 100 ? search.slice(0, 100) + '...' : search,
|
||||||
occurrences: positions.length,
|
occurrences: positions.length,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,33 +26,39 @@ export const editFileTool: ToolWithMetadata = {
|
|||||||
deferLoading: false, // 核心工具,始终可用
|
deferLoading: false, // 核心工具,始终可用
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
file_path: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '要编辑的文件路径',
|
description: 'The absolute path to the file to modify',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
old_string: {
|
old_string: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '要被替换的原始字符串(必须精确匹配)',
|
description: 'The text to replace',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
new_string: {
|
new_string: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '替换后的新字符串',
|
description: 'The text to replace it with (must be different from old_string)',
|
||||||
required: true,
|
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> => {
|
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 oldString = params.old_string as string;
|
||||||
const newString = params.new_string as string;
|
const newString = params.new_string as string;
|
||||||
|
const replaceAll = (params.replace_all as boolean) ?? false;
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const absolutePath = path.isAbsolute(filePath)
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
? filePath
|
? filePath
|
||||||
: path.join(cwd, filePath);
|
: path.join(cwd, filePath);
|
||||||
|
|
||||||
// 创建编辑对象
|
// 创建编辑对象
|
||||||
const edit = createSingleSearchReplaceEdit(absolutePath, oldString, newString);
|
const edit = createSingleSearchReplaceEdit(absolutePath, oldString, newString, replaceAll);
|
||||||
|
|
||||||
// 先验证
|
// 先验证
|
||||||
const validation = await validateEdit(edit);
|
const validation = await validateEdit(edit);
|
||||||
|
|||||||
Reference in New Issue
Block a user