From 6aba89ae9b05730d781f657c6143e7cfa94a613e Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Sun, 3 May 2026 15:46:16 +0800 Subject: [PATCH] feat(edit-file): add replace_all for precise or global replacement --- core/main_terminal_parts/tools_definition.py | 6 ++++++ core/main_terminal_parts/tools_execution.py | 10 +++++++++- easyagent/src/tools/edit_file.js | 16 ++++++++++++---- modules/file_manager.py | 10 +++++++--- prompts/main_system.txt | 2 +- prompts/main_system_qwenvl.txt | 2 +- scripts/api_tool_role_experiment.py | 3 ++- server/chat_flow_tool_loop.py | 5 ++++- 8 files changed, 42 insertions(+), 12 deletions(-) diff --git a/core/main_terminal_parts/tools_definition.py b/core/main_terminal_parts/tools_definition.py index ffff9d9..82b7e9d 100644 --- a/core/main_terminal_parts/tools_definition.py +++ b/core/main_terminal_parts/tools_definition.py @@ -545,6 +545,12 @@ class MainTerminalToolsDefinitionMixin: "new_string": { "type": "string", "description": "用于替换的新文本(必须不同于 old_string)" + }, + "replace_all": { + "type": "boolean", + "enum": [True, False], + "description": "是否替换所有匹配内容。false=仅替换首个匹配,true=替换全部匹配。", + "default": False } }), "required": ["file_path", "old_string", "new_string"] diff --git a/core/main_terminal_parts/tools_execution.py b/core/main_terminal_parts/tools_execution.py index e252cd5..b927a11 100644 --- a/core/main_terminal_parts/tools_execution.py +++ b/core/main_terminal_parts/tools_execution.py @@ -1065,16 +1065,24 @@ class MainTerminalToolsExecutionMixin: path = arguments.get("file_path") old_text = arguments.get("old_string") new_text = arguments.get("new_string") + replace_all = arguments.get("replace_all", False) if not path: result = {"success": False, "error": "缺少必要参数: file_path"} elif old_text is None or new_text is None: result = {"success": False, "error": "缺少必要参数: old_string/new_string"} + elif not isinstance(replace_all, bool): + result = {"success": False, "error": "replace_all 必须是 true 或 false"} elif old_text == new_text: result = {"success": False, "error": "old_string 与 new_string 相同,无法执行替换"} elif not old_text: result = {"success": False, "error": "old_string 不能为空,请从 read_file 内容中精确复制"} else: - result = self.file_manager.replace_in_file(path, old_text, new_text) + result = self.file_manager.replace_in_file( + path, + old_text, + new_text, + replace_all=replace_all + ) elif tool_name == "create_folder": result = self.file_manager.create_folder(arguments["path"]) diff --git a/easyagent/src/tools/edit_file.js b/easyagent/src/tools/edit_file.js index d947320..4d3cd20 100644 --- a/easyagent/src/tools/edit_file.js +++ b/easyagent/src/tools/edit_file.js @@ -45,6 +45,10 @@ function editFileTool(workspace, args) { const target = resolvePath(workspace, args.file_path); const oldString = args.old_string ?? ''; const newString = args.new_string ?? ''; + const replaceAll = args.replace_all ?? false; + if (typeof replaceAll !== 'boolean') { + return { success: false, error: 'replace_all 必须是 true 或 false' }; + } const ctx = getContainerContext(); if (ctx) { const resolved = resolveContainerPath(workspace, args.file_path || '.', ctx); @@ -70,8 +74,10 @@ function editFileTool(workspace, args) { if (!original.includes(oldString)) { return { success: false, error: 'old_string 未匹配到内容' }; } - const updated = original.split(oldString).join(newString); - const replacements = original.split(oldString).length - 1; + const replacements = replaceAll ? (original.split(oldString).length - 1) : 1; + const updated = replaceAll + ? original.split(oldString).join(newString) + : original.replace(oldString, newString); const writeResp = execContainerAction(ctx, 'write_file', { path: rel, content: updated, @@ -104,8 +110,10 @@ function editFileTool(workspace, args) { if (!creating && !original.includes(oldString)) { return { success: false, error: 'old_string 未匹配到内容' }; } - const updated = creating ? newString : original.split(oldString).join(newString); - let replacements = creating ? 0 : original.split(oldString).length - 1; + const updated = creating + ? newString + : (replaceAll ? original.split(oldString).join(newString) : original.replace(oldString, newString)); + let replacements = creating ? 0 : (replaceAll ? (original.split(oldString).length - 1) : 1); if (creating && newString) replacements = 1; fs.writeFileSync(target, updated, 'utf8'); const diff = diffSummary(original, updated); diff --git a/modules/file_manager.py b/modules/file_manager.py index f4e0a1c..97233f5 100644 --- a/modules/file_manager.py +++ b/modules/file_manager.py @@ -1166,7 +1166,7 @@ class FileManager: "error": write_error } - def replace_in_file(self, path: str, old_text: str, new_text: str) -> Dict: + def replace_in_file(self, path: str, old_text: str, new_text: str, replace_all: bool = False) -> Dict: """替换文件中的内容""" # 先读取文件 result = self.read_file(path) @@ -1196,8 +1196,12 @@ class FileManager: # 替换内容 if old_text: - new_content = content.replace(old_text, new_text) - count = content.count(old_text) + if replace_all: + count = content.count(old_text) + new_content = content.replace(old_text, new_text) + else: + count = 1 + new_content = content.replace(old_text, new_text, 1) else: # 空文件直接写入新内容 new_content = new_text diff --git a/prompts/main_system.txt b/prompts/main_system.txt index 81ccfb3..84ccc62 100644 --- a/prompts/main_system.txt +++ b/prompts/main_system.txt @@ -87,7 +87,7 @@ - `create_file`:创建空文件(禁止在根目录创建,需先建子目录) - `write_file`:写入文件(`append` 控制覆盖/追加) - `read_file`:读取文件(支持 `read`/`search`/`extract` 三种模式) -- `edit_file`:精确字符串替换 +- `edit_file`:精确字符串替换(`replace_all` 可选,默认 `false`,仅替换首个匹配;为 `true` 时替换全部匹配) - `delete_file` / `rename_file` / `create_folder`:文件管理 **注意**:超大文件用 `search` 定位 + `extract` 抽取,避免全文读取。 diff --git a/prompts/main_system_qwenvl.txt b/prompts/main_system_qwenvl.txt index d46e047..e41502b 100644 --- a/prompts/main_system_qwenvl.txt +++ b/prompts/main_system_qwenvl.txt @@ -87,7 +87,7 @@ - `create_file`:创建空文件(禁止在根目录创建,需先建子目录) - `write_file`:写入文件(`append` 控制覆盖/追加) - `read_file`:读取文件(支持 `read`/`search`/`extract` 三种模式) -- `edit_file`:精确字符串替换 +- `edit_file`:精确字符串替换(`replace_all` 可选,默认 `false`,仅替换首个匹配;为 `true` 时替换全部匹配) - `delete_file` / `rename_file` / `create_folder`:文件管理 **注意**:超大文件用 `search` 定位 + `extract` 抽取,避免全文读取。 diff --git a/scripts/api_tool_role_experiment.py b/scripts/api_tool_role_experiment.py index ca018fc..463d00a 100644 --- a/scripts/api_tool_role_experiment.py +++ b/scripts/api_tool_role_experiment.py @@ -80,7 +80,8 @@ def minimal_tool_definitions() -> List[Dict[str, Any]]: "properties": { "file_path": {"type": "string", "description": "目标文件的相对路径"}, "old_string": {"type": "string", "description": "需要替换的原文"}, - "new_string": {"type": "string", "description": "替换后的新内容"} + "new_string": {"type": "string", "description": "替换后的新内容"}, + "replace_all": {"type": "boolean", "description": "是否替换全部匹配", "default": False} }, "required": ["file_path", "old_string", "new_string"] } diff --git a/server/chat_flow_tool_loop.py b/server/chat_flow_tool_loop.py index 2a04a4a..cf13f07 100644 --- a/server/chat_flow_tool_loop.py +++ b/server/chat_flow_tool_loop.py @@ -45,7 +45,9 @@ def _build_tool_approval_preview(web_terminal, function_name: str, arguments: Di file_path = args.get("file_path") old_string = args.get("old_string") new_string = args.get("new_string") + replace_all = args.get("replace_all", False) preview["file_path"] = file_path + preview["replace_all"] = replace_all if not file_path: preview["summary"] = "缺少 file_path" return preview @@ -82,7 +84,8 @@ def _build_tool_approval_preview(web_terminal, function_name: str, arguments: Di "old_start_line": start_line_no, "old_end_line": end_line_no, } - preview["summary"] = f"编辑 {file_path} 第 {start_line_no}-{end_line_no} 行" + mode_text = "全部匹配" if replace_all is True else "首个匹配" + preview["summary"] = f"编辑 {file_path} 第 {start_line_no}-{end_line_no} 行({mode_text})" else: preview["edit_context"] = { "before": [],