feat(edit-file): add replace_all for precise or global replacement
This commit is contained in:
parent
13689e80c2
commit
6aba89ae9b
@ -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"]
|
||||
|
||||
@ -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"])
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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` 抽取,避免全文读取。
|
||||
|
||||
@ -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` 抽取,避免全文读取。
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
@ -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": [],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user