Coverage for merco/tools/edit.py: 94%
35 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 14:04 +0800
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 14:04 +0800
1"""文件编辑工具 — EditFile (SEARCH/REPLACE)"""
3import difflib
4import logging
5from pathlib import Path
6from .base import BaseTool
7from .registry import tool_registry
9logger = logging.getLogger("merco.tools.edit")
12def _generate_diff(filepath: str, old: str, new: str) -> str:
13 """生成 unified diff 文本"""
14 diff_lines = list(difflib.unified_diff(
15 old.splitlines(keepends=True),
16 new.splitlines(keepends=True),
17 fromfile=filepath,
18 tofile=filepath,
19 ))
20 return "".join(diff_lines)
23def _validate_search(content: str, search: str, path: str) -> str | None:
24 """校验 SEARCH 内容是否唯一存在于文件中。返回错误信息或 None。"""
25 count = content.count(search)
26 if count == 0:
27 return f"在 `{path}` 中**未找到**匹配内容:\n```\n{search}\n```"
28 if count > 1:
29 return f"在 `{path}` 中找到 **{count} 处**匹配,`search` 内容必须唯一"
30 return None
33# ── EditFile ────────────────────────────────────────────────────────
35class EditFile(BaseTool):
36 """编辑文件内容 — SEARCH/REPLACE 模式,含 diff 预览和确认"""
38 name = "edit_file"
39 description = (
40 "🔧 修改已有文件的首选工具。用 SEARCH/REPLACE 块精准替换:\n"
41 "- search: 文件中的原内容(必须唯一)\n"
42 "- replace: 替换后的内容\n\n"
43 "会展示 diff 预览并等待用户确认。如需创建新文件请用 write_file。"
44 )
45 toolset = "file"
46 parameters = {
47 "type": "object",
48 "properties": {
49 "path": {
50 "type": "string",
51 "description": "文件路径",
52 },
53 "search": {
54 "type": "string",
55 "description": "文件中的原内容,必须唯一",
56 },
57 "replace": {
58 "type": "string",
59 "description": "替换后的内容",
60 },
61 },
62 "required": ["path", "search", "replace"],
63 }
65 async def execute(self, path: str, search: str, replace: str) -> dict:
66 """edit_file
68 Args:
69 path: 文件路径
70 search: 原内容(必须唯一)
71 replace: 新内容
72 """
73 file_path = Path(path)
74 if not file_path.exists():
75 return {"error": f"文件 '{path}' 不存在"}
77 old_content = file_path.read_text(encoding="utf-8")
79 # 校验 SEARCH 唯一性
80 err = _validate_search(old_content, search, path)
81 if err:
82 return {"error": err}
84 # 执行替换
85 new_content = old_content.replace(search, replace, 1)
86 diff_text = _generate_diff(path, old_content, new_content)
88 if not diff_text.strip():
89 return {"success": True, "path": path,
90 "message": "文件内容无变化", "diff": ""}
92 return {
93 "planned_edit": True,
94 "path": path,
95 "old_content": old_content,
96 "new_content": new_content,
97 "diff": diff_text,
98 }
101# ── 自注册 ──
102tool_registry.register(EditFile())