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": {
|
"new_string": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "用于替换的新文本(必须不同于 old_string)"
|
"description": "用于替换的新文本(必须不同于 old_string)"
|
||||||
|
},
|
||||||
|
"replace_all": {
|
||||||
|
"type": "boolean",
|
||||||
|
"enum": [True, False],
|
||||||
|
"description": "是否替换所有匹配内容。false=仅替换首个匹配,true=替换全部匹配。",
|
||||||
|
"default": False
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
"required": ["file_path", "old_string", "new_string"]
|
"required": ["file_path", "old_string", "new_string"]
|
||||||
|
|||||||
@ -1065,16 +1065,24 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
path = arguments.get("file_path")
|
path = arguments.get("file_path")
|
||||||
old_text = arguments.get("old_string")
|
old_text = arguments.get("old_string")
|
||||||
new_text = arguments.get("new_string")
|
new_text = arguments.get("new_string")
|
||||||
|
replace_all = arguments.get("replace_all", False)
|
||||||
if not path:
|
if not path:
|
||||||
result = {"success": False, "error": "缺少必要参数: file_path"}
|
result = {"success": False, "error": "缺少必要参数: file_path"}
|
||||||
elif old_text is None or new_text is None:
|
elif old_text is None or new_text is None:
|
||||||
result = {"success": False, "error": "缺少必要参数: old_string/new_string"}
|
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:
|
elif old_text == new_text:
|
||||||
result = {"success": False, "error": "old_string 与 new_string 相同,无法执行替换"}
|
result = {"success": False, "error": "old_string 与 new_string 相同,无法执行替换"}
|
||||||
elif not old_text:
|
elif not old_text:
|
||||||
result = {"success": False, "error": "old_string 不能为空,请从 read_file 内容中精确复制"}
|
result = {"success": False, "error": "old_string 不能为空,请从 read_file 内容中精确复制"}
|
||||||
else:
|
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":
|
elif tool_name == "create_folder":
|
||||||
result = self.file_manager.create_folder(arguments["path"])
|
result = self.file_manager.create_folder(arguments["path"])
|
||||||
|
|
||||||
|
|||||||
@ -45,6 +45,10 @@ function editFileTool(workspace, args) {
|
|||||||
const target = resolvePath(workspace, args.file_path);
|
const target = resolvePath(workspace, args.file_path);
|
||||||
const oldString = args.old_string ?? '';
|
const oldString = args.old_string ?? '';
|
||||||
const newString = args.new_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();
|
const ctx = getContainerContext();
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
const resolved = resolveContainerPath(workspace, args.file_path || '.', ctx);
|
const resolved = resolveContainerPath(workspace, args.file_path || '.', ctx);
|
||||||
@ -70,8 +74,10 @@ function editFileTool(workspace, args) {
|
|||||||
if (!original.includes(oldString)) {
|
if (!original.includes(oldString)) {
|
||||||
return { success: false, error: 'old_string 未匹配到内容' };
|
return { success: false, error: 'old_string 未匹配到内容' };
|
||||||
}
|
}
|
||||||
const updated = original.split(oldString).join(newString);
|
const replacements = replaceAll ? (original.split(oldString).length - 1) : 1;
|
||||||
const replacements = original.split(oldString).length - 1;
|
const updated = replaceAll
|
||||||
|
? original.split(oldString).join(newString)
|
||||||
|
: original.replace(oldString, newString);
|
||||||
const writeResp = execContainerAction(ctx, 'write_file', {
|
const writeResp = execContainerAction(ctx, 'write_file', {
|
||||||
path: rel,
|
path: rel,
|
||||||
content: updated,
|
content: updated,
|
||||||
@ -104,8 +110,10 @@ function editFileTool(workspace, args) {
|
|||||||
if (!creating && !original.includes(oldString)) {
|
if (!creating && !original.includes(oldString)) {
|
||||||
return { success: false, error: 'old_string 未匹配到内容' };
|
return { success: false, error: 'old_string 未匹配到内容' };
|
||||||
}
|
}
|
||||||
const updated = creating ? newString : original.split(oldString).join(newString);
|
const updated = creating
|
||||||
let replacements = creating ? 0 : original.split(oldString).length - 1;
|
? 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;
|
if (creating && newString) replacements = 1;
|
||||||
fs.writeFileSync(target, updated, 'utf8');
|
fs.writeFileSync(target, updated, 'utf8');
|
||||||
const diff = diffSummary(original, updated);
|
const diff = diffSummary(original, updated);
|
||||||
|
|||||||
@ -1166,7 +1166,7 @@ class FileManager:
|
|||||||
"error": write_error
|
"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)
|
result = self.read_file(path)
|
||||||
@ -1196,8 +1196,12 @@ class FileManager:
|
|||||||
|
|
||||||
# 替换内容
|
# 替换内容
|
||||||
if old_text:
|
if old_text:
|
||||||
new_content = content.replace(old_text, new_text)
|
if replace_all:
|
||||||
count = content.count(old_text)
|
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:
|
else:
|
||||||
# 空文件直接写入新内容
|
# 空文件直接写入新内容
|
||||||
new_content = new_text
|
new_content = new_text
|
||||||
|
|||||||
@ -87,7 +87,7 @@
|
|||||||
- `create_file`:创建空文件(禁止在根目录创建,需先建子目录)
|
- `create_file`:创建空文件(禁止在根目录创建,需先建子目录)
|
||||||
- `write_file`:写入文件(`append` 控制覆盖/追加)
|
- `write_file`:写入文件(`append` 控制覆盖/追加)
|
||||||
- `read_file`:读取文件(支持 `read`/`search`/`extract` 三种模式)
|
- `read_file`:读取文件(支持 `read`/`search`/`extract` 三种模式)
|
||||||
- `edit_file`:精确字符串替换
|
- `edit_file`:精确字符串替换(`replace_all` 可选,默认 `false`,仅替换首个匹配;为 `true` 时替换全部匹配)
|
||||||
- `delete_file` / `rename_file` / `create_folder`:文件管理
|
- `delete_file` / `rename_file` / `create_folder`:文件管理
|
||||||
|
|
||||||
**注意**:超大文件用 `search` 定位 + `extract` 抽取,避免全文读取。
|
**注意**:超大文件用 `search` 定位 + `extract` 抽取,避免全文读取。
|
||||||
|
|||||||
@ -87,7 +87,7 @@
|
|||||||
- `create_file`:创建空文件(禁止在根目录创建,需先建子目录)
|
- `create_file`:创建空文件(禁止在根目录创建,需先建子目录)
|
||||||
- `write_file`:写入文件(`append` 控制覆盖/追加)
|
- `write_file`:写入文件(`append` 控制覆盖/追加)
|
||||||
- `read_file`:读取文件(支持 `read`/`search`/`extract` 三种模式)
|
- `read_file`:读取文件(支持 `read`/`search`/`extract` 三种模式)
|
||||||
- `edit_file`:精确字符串替换
|
- `edit_file`:精确字符串替换(`replace_all` 可选,默认 `false`,仅替换首个匹配;为 `true` 时替换全部匹配)
|
||||||
- `delete_file` / `rename_file` / `create_folder`:文件管理
|
- `delete_file` / `rename_file` / `create_folder`:文件管理
|
||||||
|
|
||||||
**注意**:超大文件用 `search` 定位 + `extract` 抽取,避免全文读取。
|
**注意**:超大文件用 `search` 定位 + `extract` 抽取,避免全文读取。
|
||||||
|
|||||||
@ -80,7 +80,8 @@ def minimal_tool_definitions() -> List[Dict[str, Any]]:
|
|||||||
"properties": {
|
"properties": {
|
||||||
"file_path": {"type": "string", "description": "目标文件的相对路径"},
|
"file_path": {"type": "string", "description": "目标文件的相对路径"},
|
||||||
"old_string": {"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"]
|
"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")
|
file_path = args.get("file_path")
|
||||||
old_string = args.get("old_string")
|
old_string = args.get("old_string")
|
||||||
new_string = args.get("new_string")
|
new_string = args.get("new_string")
|
||||||
|
replace_all = args.get("replace_all", False)
|
||||||
preview["file_path"] = file_path
|
preview["file_path"] = file_path
|
||||||
|
preview["replace_all"] = replace_all
|
||||||
if not file_path:
|
if not file_path:
|
||||||
preview["summary"] = "缺少 file_path"
|
preview["summary"] = "缺少 file_path"
|
||||||
return preview
|
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_start_line": start_line_no,
|
||||||
"old_end_line": end_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:
|
else:
|
||||||
preview["edit_context"] = {
|
preview["edit_context"] = {
|
||||||
"before": [],
|
"before": [],
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user