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

1"""文件编辑工具 — EditFile (SEARCH/REPLACE)""" 

2 

3import difflib 

4import logging 

5from pathlib import Path 

6from .base import BaseTool 

7from .registry import tool_registry 

8 

9logger = logging.getLogger("merco.tools.edit") 

10 

11 

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) 

21 

22 

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 

31 

32 

33# ── EditFile ──────────────────────────────────────────────────────── 

34 

35class EditFile(BaseTool): 

36 """编辑文件内容 — SEARCH/REPLACE 模式,含 diff 预览和确认""" 

37 

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 } 

64 

65 async def execute(self, path: str, search: str, replace: str) -> dict: 

66 """edit_file 

67 

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}' 不存在"} 

76 

77 old_content = file_path.read_text(encoding="utf-8") 

78 

79 # 校验 SEARCH 唯一性 

80 err = _validate_search(old_content, search, path) 

81 if err: 

82 return {"error": err} 

83 

84 # 执行替换 

85 new_content = old_content.replace(search, replace, 1) 

86 diff_text = _generate_diff(path, old_content, new_content) 

87 

88 if not diff_text.strip(): 

89 return {"success": True, "path": path, 

90 "message": "文件内容无变化", "diff": ""} 

91 

92 return { 

93 "planned_edit": True, 

94 "path": path, 

95 "old_content": old_content, 

96 "new_content": new_content, 

97 "diff": diff_text, 

98 } 

99 

100 

101# ── 自注册 ── 

102tool_registry.register(EditFile())