jarvis-ai-assistant 0.7.0__py3-none-any.whl → 0.7.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +243 -139
  3. jarvis/jarvis_agent/agent_manager.py +5 -10
  4. jarvis/jarvis_agent/builtin_input_handler.py +2 -6
  5. jarvis/jarvis_agent/config_editor.py +2 -7
  6. jarvis/jarvis_agent/event_bus.py +82 -12
  7. jarvis/jarvis_agent/file_context_handler.py +265 -15
  8. jarvis/jarvis_agent/file_methodology_manager.py +3 -4
  9. jarvis/jarvis_agent/jarvis.py +113 -98
  10. jarvis/jarvis_agent/language_extractors/__init__.py +57 -0
  11. jarvis/jarvis_agent/language_extractors/c_extractor.py +21 -0
  12. jarvis/jarvis_agent/language_extractors/cpp_extractor.py +21 -0
  13. jarvis/jarvis_agent/language_extractors/go_extractor.py +21 -0
  14. jarvis/jarvis_agent/language_extractors/java_extractor.py +84 -0
  15. jarvis/jarvis_agent/language_extractors/javascript_extractor.py +79 -0
  16. jarvis/jarvis_agent/language_extractors/python_extractor.py +21 -0
  17. jarvis/jarvis_agent/language_extractors/rust_extractor.py +21 -0
  18. jarvis/jarvis_agent/language_extractors/typescript_extractor.py +84 -0
  19. jarvis/jarvis_agent/language_support_info.py +486 -0
  20. jarvis/jarvis_agent/main.py +6 -12
  21. jarvis/jarvis_agent/memory_manager.py +7 -16
  22. jarvis/jarvis_agent/methodology_share_manager.py +10 -16
  23. jarvis/jarvis_agent/prompt_manager.py +1 -1
  24. jarvis/jarvis_agent/prompts.py +193 -171
  25. jarvis/jarvis_agent/protocols.py +8 -12
  26. jarvis/jarvis_agent/run_loop.py +77 -14
  27. jarvis/jarvis_agent/session_manager.py +2 -3
  28. jarvis/jarvis_agent/share_manager.py +12 -21
  29. jarvis/jarvis_agent/shell_input_handler.py +1 -2
  30. jarvis/jarvis_agent/task_analyzer.py +26 -4
  31. jarvis/jarvis_agent/task_manager.py +11 -27
  32. jarvis/jarvis_agent/tool_executor.py +2 -3
  33. jarvis/jarvis_agent/tool_share_manager.py +12 -24
  34. jarvis/jarvis_agent/web_server.py +55 -20
  35. jarvis/jarvis_c2rust/__init__.py +5 -5
  36. jarvis/jarvis_c2rust/cli.py +461 -499
  37. jarvis/jarvis_c2rust/collector.py +45 -53
  38. jarvis/jarvis_c2rust/constants.py +26 -0
  39. jarvis/jarvis_c2rust/library_replacer.py +264 -132
  40. jarvis/jarvis_c2rust/llm_module_agent.py +162 -190
  41. jarvis/jarvis_c2rust/loaders.py +207 -0
  42. jarvis/jarvis_c2rust/models.py +28 -0
  43. jarvis/jarvis_c2rust/optimizer.py +1592 -395
  44. jarvis/jarvis_c2rust/transpiler.py +1722 -1064
  45. jarvis/jarvis_c2rust/utils.py +385 -0
  46. jarvis/jarvis_code_agent/build_validation_config.py +2 -3
  47. jarvis/jarvis_code_agent/code_agent.py +394 -320
  48. jarvis/jarvis_code_agent/code_analyzer/__init__.py +3 -0
  49. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +4 -0
  50. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +17 -2
  51. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +3 -0
  52. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +36 -4
  53. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +9 -0
  54. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +9 -0
  55. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +12 -1
  56. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +22 -5
  57. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +57 -32
  58. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +62 -6
  59. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +8 -9
  60. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +290 -5
  61. jarvis/jarvis_code_agent/code_analyzer/language_support.py +21 -0
  62. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +21 -3
  63. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +72 -4
  64. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +35 -3
  65. jarvis/jarvis_code_agent/code_analyzer/languages/java_language.py +212 -0
  66. jarvis/jarvis_code_agent/code_analyzer/languages/javascript_language.py +254 -0
  67. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +52 -2
  68. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +73 -1
  69. jarvis/jarvis_code_agent/code_analyzer/languages/typescript_language.py +280 -0
  70. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +306 -152
  71. jarvis/jarvis_code_agent/code_analyzer/structured_code.py +556 -0
  72. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +193 -18
  73. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +18 -8
  74. jarvis/jarvis_code_agent/lint.py +258 -27
  75. jarvis/jarvis_code_agent/utils.py +0 -1
  76. jarvis/jarvis_code_analysis/code_review.py +19 -24
  77. jarvis/jarvis_data/config_schema.json +53 -26
  78. jarvis/jarvis_git_squash/main.py +4 -5
  79. jarvis/jarvis_git_utils/git_commiter.py +44 -49
  80. jarvis/jarvis_mcp/sse_mcp_client.py +20 -27
  81. jarvis/jarvis_mcp/stdio_mcp_client.py +11 -12
  82. jarvis/jarvis_mcp/streamable_mcp_client.py +15 -14
  83. jarvis/jarvis_memory_organizer/memory_organizer.py +55 -74
  84. jarvis/jarvis_methodology/main.py +32 -48
  85. jarvis/jarvis_multi_agent/__init__.py +79 -61
  86. jarvis/jarvis_multi_agent/main.py +3 -7
  87. jarvis/jarvis_platform/base.py +469 -199
  88. jarvis/jarvis_platform/human.py +7 -8
  89. jarvis/jarvis_platform/kimi.py +30 -36
  90. jarvis/jarvis_platform/openai.py +65 -27
  91. jarvis/jarvis_platform/registry.py +26 -10
  92. jarvis/jarvis_platform/tongyi.py +24 -25
  93. jarvis/jarvis_platform/yuanbao.py +31 -42
  94. jarvis/jarvis_platform_manager/main.py +66 -77
  95. jarvis/jarvis_platform_manager/service.py +8 -13
  96. jarvis/jarvis_rag/cli.py +49 -51
  97. jarvis/jarvis_rag/embedding_manager.py +13 -18
  98. jarvis/jarvis_rag/llm_interface.py +8 -9
  99. jarvis/jarvis_rag/query_rewriter.py +10 -21
  100. jarvis/jarvis_rag/rag_pipeline.py +24 -27
  101. jarvis/jarvis_rag/reranker.py +4 -5
  102. jarvis/jarvis_rag/retriever.py +28 -30
  103. jarvis/jarvis_sec/__init__.py +220 -3520
  104. jarvis/jarvis_sec/agents.py +143 -0
  105. jarvis/jarvis_sec/analysis.py +276 -0
  106. jarvis/jarvis_sec/cli.py +29 -6
  107. jarvis/jarvis_sec/clustering.py +1439 -0
  108. jarvis/jarvis_sec/file_manager.py +427 -0
  109. jarvis/jarvis_sec/parsers.py +73 -0
  110. jarvis/jarvis_sec/prompts.py +268 -0
  111. jarvis/jarvis_sec/report.py +83 -4
  112. jarvis/jarvis_sec/review.py +453 -0
  113. jarvis/jarvis_sec/utils.py +499 -0
  114. jarvis/jarvis_sec/verification.py +848 -0
  115. jarvis/jarvis_sec/workflow.py +7 -0
  116. jarvis/jarvis_smart_shell/main.py +38 -87
  117. jarvis/jarvis_stats/cli.py +1 -1
  118. jarvis/jarvis_stats/stats.py +7 -7
  119. jarvis/jarvis_stats/storage.py +15 -21
  120. jarvis/jarvis_tools/clear_memory.py +3 -20
  121. jarvis/jarvis_tools/cli/main.py +20 -23
  122. jarvis/jarvis_tools/edit_file.py +1066 -0
  123. jarvis/jarvis_tools/execute_script.py +42 -21
  124. jarvis/jarvis_tools/file_analyzer.py +6 -9
  125. jarvis/jarvis_tools/generate_new_tool.py +11 -20
  126. jarvis/jarvis_tools/lsp_client.py +1552 -0
  127. jarvis/jarvis_tools/methodology.py +2 -3
  128. jarvis/jarvis_tools/read_code.py +1525 -87
  129. jarvis/jarvis_tools/read_symbols.py +2 -3
  130. jarvis/jarvis_tools/read_webpage.py +7 -10
  131. jarvis/jarvis_tools/registry.py +370 -181
  132. jarvis/jarvis_tools/retrieve_memory.py +20 -19
  133. jarvis/jarvis_tools/rewrite_file.py +105 -0
  134. jarvis/jarvis_tools/save_memory.py +3 -15
  135. jarvis/jarvis_tools/search_web.py +3 -7
  136. jarvis/jarvis_tools/sub_agent.py +17 -6
  137. jarvis/jarvis_tools/sub_code_agent.py +14 -16
  138. jarvis/jarvis_tools/virtual_tty.py +54 -32
  139. jarvis/jarvis_utils/clipboard.py +7 -10
  140. jarvis/jarvis_utils/config.py +98 -63
  141. jarvis/jarvis_utils/embedding.py +5 -5
  142. jarvis/jarvis_utils/fzf.py +8 -8
  143. jarvis/jarvis_utils/git_utils.py +81 -67
  144. jarvis/jarvis_utils/input.py +24 -49
  145. jarvis/jarvis_utils/jsonnet_compat.py +465 -0
  146. jarvis/jarvis_utils/methodology.py +33 -35
  147. jarvis/jarvis_utils/utils.py +245 -202
  148. {jarvis_ai_assistant-0.7.0.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/METADATA +205 -70
  149. jarvis_ai_assistant-0.7.6.dist-info/RECORD +218 -0
  150. jarvis/jarvis_agent/edit_file_handler.py +0 -584
  151. jarvis/jarvis_agent/rewrite_file_handler.py +0 -141
  152. jarvis/jarvis_agent/task_planner.py +0 -496
  153. jarvis/jarvis_platform/ai8.py +0 -332
  154. jarvis/jarvis_tools/ask_user.py +0 -54
  155. jarvis_ai_assistant-0.7.0.dist-info/RECORD +0 -192
  156. {jarvis_ai_assistant-0.7.0.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/WHEEL +0 -0
  157. {jarvis_ai_assistant-0.7.0.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/entry_points.txt +0 -0
  158. {jarvis_ai_assistant-0.7.0.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/licenses/LICENSE +0 -0
  159. {jarvis_ai_assistant-0.7.0.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1066 @@
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ import shutil
4
+ import time
5
+ from enum import Enum
6
+ from typing import Any, Dict, List, Optional, Tuple
7
+
8
+
9
+
10
+ class EditErrorType(Enum):
11
+ """编辑错误类型枚举"""
12
+ BLOCK_ID_NOT_FOUND = "block_id_not_found" # 块id不存在
13
+ CACHE_INVALID = "cache_invalid" # 缓存无效
14
+ MULTIPLE_MATCHES = "multiple_matches" # 多处匹配
15
+ SEARCH_NOT_FOUND = "search_not_found" # 搜索文本未找到
16
+ PARAMETER_MISSING = "parameter_missing" # 参数缺失
17
+ UNSUPPORTED_ACTION = "unsupported_action" # 不支持的操作
18
+ OTHER = "other" # 其他错误
19
+
20
+
21
+ class EditFileTool:
22
+ """文件编辑工具,用于对文件进行结构化编辑"""
23
+
24
+ name = "edit_file"
25
+ description = "对文件进行结构化编辑(通过块id),支持同时修改多个文件。\n\n 💡 使用步骤:\n 1. 先使用read_code工具获取文件的结构化块id\n 2. 通过块id进行精确的代码块操作(删除、插入、替换、编辑)\n 3. 避免手动计算行号,减少错误风险\n 4. 可以在一次调用中同时修改多个文件\n\n 📝 支持的操作类型:\n - delete: 删除块\n - insert_before: 在块前插入内容\n - insert_after: 在块后插入内容\n - replace: 替换整个块\n - edit: 在块内进行search/replace(需要提供search和replace参数)\n\n ⚠️ 重要提示:\n - 不要一次修改太多内容,建议分多次进行,避免超过LLM的上下文窗口大小\n - 如果修改内容较长(超过2048字符),建议拆分为多个较小的编辑操作"
26
+
27
+ parameters = {
28
+ "type": "object",
29
+ "properties": {
30
+ "files": {
31
+ "type": "array",
32
+ "items": {
33
+ "type": "object",
34
+ "properties": {
35
+ "file_path": {
36
+ "type": "string",
37
+ "description": "要修改的文件路径(支持绝对路径和相对路径)",
38
+ },
39
+ "diffs": {
40
+ "type": "array",
41
+ "items": {
42
+ "type": "object",
43
+ "properties": {
44
+ "block_id": {
45
+ "type": "string",
46
+ "description": "要操作的块id(从read_code工具获取的结构化块id)",
47
+ },
48
+ "action": {
49
+ "type": "string",
50
+ "enum": ["delete", "insert_before", "insert_after", "replace", "edit"],
51
+ "description": "操作类型:delete(删除块)、insert_before(在块前插入)、insert_after(在块后插入)、replace(替换块)、edit(在块内进行search/replace)",
52
+ },
53
+ "content": {
54
+ "type": "string",
55
+ "description": "新内容(对于insert_before、insert_after、replace操作必需,delete和edit操作不需要)",
56
+ },
57
+ "search": {
58
+ "type": "string",
59
+ "description": "要搜索的文本(对于edit操作必需)",
60
+ },
61
+ "replace": {
62
+ "type": "string",
63
+ "description": "替换后的文本(对于edit操作必需)",
64
+ },
65
+ },
66
+ "required": ["block_id", "action"],
67
+ },
68
+ "description": "修改操作列表,每个操作包含一个结构化编辑块",
69
+ },
70
+ },
71
+ "required": ["file_path", "diffs"],
72
+ },
73
+ "description": "要修改的文件列表,每个文件包含文件路径和对应的编辑操作列表",
74
+ },
75
+ },
76
+ "required": ["files"],
77
+ }
78
+
79
+ def __init__(self):
80
+ """初始化文件编辑工具"""
81
+ pass
82
+
83
+
84
+ @staticmethod
85
+ def _validate_basic_args(args: Dict[str, Any]) -> Optional[Dict[str, Any]]:
86
+ """验证基本参数
87
+
88
+ Returns:
89
+ 如果验证失败,返回错误响应;否则返回None
90
+ """
91
+ files = args.get("files")
92
+
93
+ if not files:
94
+ return {
95
+ "success": False,
96
+ "stdout": "",
97
+ "stderr": "缺少必需参数:files",
98
+ }
99
+
100
+ if not isinstance(files, list):
101
+ return {
102
+ "success": False,
103
+ "stdout": "",
104
+ "stderr": "files参数必须是数组类型",
105
+ }
106
+
107
+ if len(files) == 0:
108
+ return {
109
+ "success": False,
110
+ "stdout": "",
111
+ "stderr": "files数组不能为空",
112
+ }
113
+
114
+ # 验证每个文件项
115
+ for idx, file_item in enumerate(files):
116
+ if not isinstance(file_item, dict):
117
+ return {
118
+ "success": False,
119
+ "stdout": "",
120
+ "stderr": f"files数组第 {idx+1} 项必须是字典类型",
121
+ }
122
+
123
+ file_path = file_item.get("file_path")
124
+ diffs = file_item.get("diffs", [])
125
+
126
+ if not file_path:
127
+ return {
128
+ "success": False,
129
+ "stdout": "",
130
+ "stderr": f"files数组第 {idx+1} 项缺少必需参数:file_path",
131
+ }
132
+
133
+ if not diffs:
134
+ return {
135
+ "success": False,
136
+ "stdout": "",
137
+ "stderr": f"files数组第 {idx+1} 项缺少必需参数:diffs",
138
+ }
139
+
140
+ if not isinstance(diffs, list):
141
+ return {
142
+ "success": False,
143
+ "stdout": "",
144
+ "stderr": f"files数组第 {idx+1} 项的diffs参数必须是数组类型",
145
+ }
146
+
147
+ return None
148
+
149
+ @staticmethod
150
+ def _get_file_cache(agent: Any, filepath: str) -> Optional[Dict[str, Any]]:
151
+ """获取文件的缓存信息
152
+
153
+ Args:
154
+ agent: Agent实例
155
+ filepath: 文件路径
156
+
157
+ Returns:
158
+ 缓存信息字典,如果不存在则返回None
159
+ """
160
+ if not agent:
161
+ return None
162
+
163
+ cache = agent.get_user_data("read_code_cache")
164
+ if not cache:
165
+ return None
166
+
167
+ abs_path = os.path.abspath(filepath)
168
+ return cache.get(abs_path)
169
+
170
+ @staticmethod
171
+ def _is_cache_valid(cache_info: Optional[Dict[str, Any]], filepath: str) -> bool:
172
+ """检查缓存是否有效
173
+
174
+ Args:
175
+ cache_info: 缓存信息字典
176
+ filepath: 文件路径
177
+
178
+ Returns:
179
+ True表示缓存有效,False表示缓存无效
180
+ """
181
+ if not cache_info:
182
+ return False
183
+
184
+ try:
185
+ # 检查文件是否存在
186
+ if not os.path.exists(filepath):
187
+ return False
188
+
189
+ # 检查文件修改时间是否变化
190
+ current_mtime = os.path.getmtime(filepath)
191
+ cached_mtime = cache_info.get("file_mtime")
192
+
193
+ if cached_mtime is None or abs(current_mtime - cached_mtime) > 0.1: # 允许0.1秒的误差
194
+ return False
195
+
196
+ # 检查缓存数据结构是否完整
197
+ if "id_list" not in cache_info or "blocks" not in cache_info or "total_lines" not in cache_info:
198
+ return False
199
+
200
+ return True
201
+ except Exception:
202
+ return False
203
+
204
+ @staticmethod
205
+ def _find_block_by_id_in_cache(cache_info: Dict[str, Any], block_id: str) -> Optional[Dict[str, Any]]:
206
+ """从缓存中根据块id定位代码块
207
+
208
+ Args:
209
+ cache_info: 缓存信息字典
210
+ block_id: 块id
211
+
212
+ Returns:
213
+ 如果找到,返回包含 content 的字典;否则返回 None
214
+ """
215
+ if not cache_info:
216
+ return None
217
+
218
+ # 直接从 blocks 字典中查找
219
+ blocks = cache_info.get("blocks", {})
220
+ block = blocks.get(block_id)
221
+ if block:
222
+ return {
223
+ "content": block.get("content", ""),
224
+ }
225
+
226
+ return None
227
+
228
+ @staticmethod
229
+ def _update_cache_timestamp(agent: Any, filepath: str) -> None:
230
+ """更新缓存的时间戳
231
+
232
+ Args:
233
+ agent: Agent实例
234
+ filepath: 文件路径
235
+ """
236
+ if not agent:
237
+ return
238
+
239
+ cache = agent.get_user_data("read_code_cache")
240
+ if not cache:
241
+ return
242
+
243
+ abs_path = os.path.abspath(filepath)
244
+ if abs_path in cache:
245
+ cache[abs_path]["read_time"] = time.time()
246
+ # 更新文件修改时间
247
+ try:
248
+ if os.path.exists(abs_path):
249
+ cache[abs_path]["file_mtime"] = os.path.getmtime(abs_path)
250
+ except Exception:
251
+ pass
252
+ agent.set_user_data("read_code_cache", cache)
253
+
254
+ @staticmethod
255
+ def _validate_structured(diff: Dict[str, Any], idx: int) -> Tuple[Optional[Dict[str, Any]], Optional[Dict[str, str]]]:
256
+ """验证并转换structured类型的diff
257
+
258
+ Returns:
259
+ (错误响应或None, patch字典或None)
260
+ """
261
+ block_id = diff.get("block_id")
262
+ action = diff.get("action")
263
+ content = diff.get("content")
264
+
265
+ if block_id is None:
266
+ return ({
267
+ "success": False,
268
+ "stdout": "",
269
+ "stderr": f"第 {idx+1} 个diff缺少block_id参数",
270
+ }, None)
271
+ if not isinstance(block_id, str):
272
+ return ({
273
+ "success": False,
274
+ "stdout": "",
275
+ "stderr": f"第 {idx+1} 个diff的block_id参数必须是字符串",
276
+ }, None)
277
+ if not block_id.strip():
278
+ return ({
279
+ "success": False,
280
+ "stdout": "",
281
+ "stderr": f"第 {idx+1} 个diff的block_id参数不能为空",
282
+ }, None)
283
+
284
+ if action is None:
285
+ return ({
286
+ "success": False,
287
+ "stdout": "",
288
+ "stderr": f"第 {idx+1} 个diff缺少action参数",
289
+ }, None)
290
+ if not isinstance(action, str):
291
+ return ({
292
+ "success": False,
293
+ "stdout": "",
294
+ "stderr": f"第 {idx+1} 个diff的action参数必须是字符串",
295
+ }, None)
296
+ if action not in ["delete", "insert_before", "insert_after", "replace", "edit"]:
297
+ return ({
298
+ "success": False,
299
+ "stdout": "",
300
+ "stderr": f"第 {idx+1} 个diff的action参数必须是 delete、insert_before、insert_after、replace 或 edit 之一",
301
+ }, None)
302
+
303
+ # 对于edit操作,需要search和replace参数
304
+ if action == "edit":
305
+ search = diff.get("search")
306
+ replace = diff.get("replace")
307
+ if search is None:
308
+ return ({
309
+ "success": False,
310
+ "stdout": "",
311
+ "stderr": f"第 {idx+1} 个diff的action为 edit,需要提供search参数",
312
+ }, None)
313
+ if not isinstance(search, str):
314
+ return ({
315
+ "success": False,
316
+ "stdout": "",
317
+ "stderr": f"第 {idx+1} 个diff的search参数必须是字符串",
318
+ }, None)
319
+ if replace is None:
320
+ return ({
321
+ "success": False,
322
+ "stdout": "",
323
+ "stderr": f"第 {idx+1} 个diff的action为 edit,需要提供replace参数",
324
+ }, None)
325
+ if not isinstance(replace, str):
326
+ return ({
327
+ "success": False,
328
+ "stdout": "",
329
+ "stderr": f"第 {idx+1} 个diff的replace参数必须是字符串",
330
+ }, None)
331
+ # 对于非delete和非edit操作,content是必需的
332
+ elif action != "delete":
333
+ if content is None:
334
+ return ({
335
+ "success": False,
336
+ "stdout": "",
337
+ "stderr": f"第 {idx+1} 个diff的action为 {action},需要提供content参数",
338
+ }, None)
339
+ if not isinstance(content, str):
340
+ return ({
341
+ "success": False,
342
+ "stdout": "",
343
+ "stderr": f"第 {idx+1} 个diff的content参数必须是字符串",
344
+ }, None)
345
+
346
+ patch = {
347
+ "STRUCTURED_BLOCK_ID": block_id,
348
+ "STRUCTURED_ACTION": action,
349
+ }
350
+ if content is not None:
351
+ patch["STRUCTURED_CONTENT"] = content
352
+ if action == "edit":
353
+ patch["STRUCTURED_SEARCH"] = diff.get("search")
354
+ patch["STRUCTURED_REPLACE"] = diff.get("replace")
355
+ return (None, patch)
356
+
357
+
358
+ @staticmethod
359
+ def _convert_diffs_to_patches(diffs: List[Dict[str, Any]]) -> Tuple[Optional[Dict[str, Any]], List[Dict[str, str]]]:
360
+ """验证并转换diffs为内部patches格式
361
+
362
+ Returns:
363
+ (错误响应或None, patches列表)
364
+ """
365
+ patches = []
366
+ for idx, diff in enumerate(diffs):
367
+ if not isinstance(diff, dict):
368
+ return ({
369
+ "success": False,
370
+ "stdout": "",
371
+ "stderr": f"第 {idx+1} 个diff必须是字典类型",
372
+ }, [])
373
+
374
+ # 所有diff都是structured类型
375
+ error_response, patch = EditFileTool._validate_structured(diff, idx + 1)
376
+
377
+ if error_response:
378
+ return (error_response, [])
379
+
380
+ if patch:
381
+ patches.append(patch)
382
+
383
+ return (None, patches)
384
+
385
+ def execute(self, args: Dict[str, Any]) -> Dict[str, Any]:
386
+ """执行文件编辑操作(支持同时修改多个文件)"""
387
+ try:
388
+ # 验证基本参数
389
+ error_response = EditFileTool._validate_basic_args(args)
390
+ if error_response:
391
+ return error_response
392
+
393
+ files = args.get("files", [])
394
+ agent = args.get("agent", None)
395
+
396
+ # 记录 PATCH 操作调用统计
397
+ try:
398
+ from jarvis.jarvis_stats.stats import StatsManager
399
+
400
+ StatsManager.increment("patch", group="tool")
401
+ except Exception:
402
+ pass
403
+
404
+ # 处理每个文件
405
+ all_results = []
406
+ overall_success = True
407
+ successful_files = []
408
+ failed_files = []
409
+
410
+ for file_item in files:
411
+ file_path = file_item.get("file_path")
412
+ diffs = file_item.get("diffs", [])
413
+
414
+ # 转换diffs为patches
415
+ error_response, patches = EditFileTool._convert_diffs_to_patches(diffs)
416
+ if error_response:
417
+ all_results.append(f"❌ {file_path}: {error_response.get('stderr', '参数验证失败')}")
418
+ failed_files.append(file_path)
419
+ overall_success = False
420
+ continue
421
+
422
+ # 执行编辑
423
+ success, result = self._fast_edit(file_path, patches, agent)
424
+
425
+ if success:
426
+ all_results.append(f"✅ {file_path}: 修改成功")
427
+ successful_files.append(file_path)
428
+ else:
429
+ all_results.append(f"❌ {file_path}: {result}")
430
+ failed_files.append(file_path)
431
+ overall_success = False
432
+
433
+ # 构建输出信息
434
+ output_lines = []
435
+ if successful_files:
436
+ output_lines.append(f"✅ 成功修改 {len(successful_files)} 个文件:")
437
+ for file_path in successful_files:
438
+ output_lines.append(f" - {file_path}")
439
+
440
+ if failed_files:
441
+ output_lines.append(f"\n❌ 失败 {len(failed_files)} 个文件:")
442
+ for file_path in failed_files:
443
+ output_lines.append(f" - {file_path}")
444
+
445
+ stdout_text = "\n".join(all_results)
446
+ summary = "\n".join(output_lines) if output_lines else ""
447
+
448
+ if overall_success:
449
+ return {
450
+ "success": True,
451
+ "stdout": stdout_text + ("\n\n" + summary if summary else ""),
452
+ "stderr": "",
453
+ }
454
+ else:
455
+ return {
456
+ "success": False,
457
+ "stdout": stdout_text + ("\n\n" + summary if summary else ""),
458
+ "stderr": summary if summary else "部分文件修改失败",
459
+ }
460
+
461
+ except Exception as e:
462
+ error_msg = f"文件编辑失败: {str(e)}"
463
+ print(f"❌ {error_msg}")
464
+ return {"success": False, "stdout": "", "stderr": error_msg}
465
+
466
+ @staticmethod
467
+ def _read_file_with_backup(file_path: str) -> Tuple[str, Optional[str]]:
468
+ """读取文件并创建备份
469
+
470
+ Args:
471
+ file_path: 文件路径
472
+
473
+ Returns:
474
+ (文件内容, 备份文件路径或None)
475
+ """
476
+ abs_path = os.path.abspath(file_path)
477
+ os.makedirs(os.path.dirname(abs_path), exist_ok=True)
478
+
479
+ file_content = ""
480
+ backup_path = None
481
+ if os.path.exists(abs_path):
482
+ with open(abs_path, "r", encoding="utf-8") as f:
483
+ file_content = f.read()
484
+ # 创建备份文件
485
+ backup_path = abs_path + ".bak"
486
+ try:
487
+ shutil.copy2(abs_path, backup_path)
488
+ except Exception:
489
+ # 备份失败不影响主流程
490
+ backup_path = None
491
+
492
+ return file_content, backup_path
493
+
494
+ @staticmethod
495
+ def _order_patches_by_range(patches: List[Dict[str, str]]) -> List[Dict[str, str]]:
496
+ """按顺序返回补丁列表
497
+
498
+ 注意:对于结构化编辑,由于需要在实际应用时才能获取块的行号范围,
499
+ 这里暂时按原始顺序返回。如果需要优化,可以在应用时动态排序。
500
+
501
+ Args:
502
+ patches: 补丁列表
503
+
504
+ Returns:
505
+ 补丁列表(当前按原始顺序返回)
506
+ """
507
+ # 对于结构化编辑,暂时按原始顺序处理
508
+ # 如果需要按行号排序,需要在应用时动态获取块的行号范围
509
+ return patches
510
+
511
+
512
+ @staticmethod
513
+ def _restore_file_from_cache(cache_info: Dict[str, Any]) -> str:
514
+ """从缓存恢复文件内容
515
+
516
+ Args:
517
+ cache_info: 缓存信息字典
518
+
519
+ Returns:
520
+ 恢复的文件内容字符串(与原始文件内容完全一致)
521
+ """
522
+ if not cache_info:
523
+ return ""
524
+
525
+ # 按照 id_list 的顺序恢复
526
+ id_list = cache_info.get("id_list", [])
527
+ blocks = cache_info.get("blocks", {})
528
+ file_ends_with_newline = cache_info.get("file_ends_with_newline", False)
529
+
530
+ result = []
531
+ for idx, block_id in enumerate(id_list):
532
+ block = blocks.get(block_id)
533
+ if block:
534
+ content = block.get('content', '')
535
+ if content:
536
+ result.append(content)
537
+ # 在块之间添加换行符(最后一个块后面根据文件是否以换行符结尾决定)
538
+ is_last_block = (idx == len(id_list) - 1)
539
+ if is_last_block:
540
+ # 最后一个块:如果文件以换行符结尾,添加换行符
541
+ if file_ends_with_newline:
542
+ result.append('\n')
543
+ else:
544
+ # 非最后一个块:在块之间添加换行符
545
+ result.append('\n')
546
+
547
+ return ''.join(result) if result else ""
548
+
549
+ @staticmethod
550
+ def _apply_structured_edit_to_cache(
551
+ cache_info: Dict[str, Any],
552
+ block_id: str,
553
+ action: str,
554
+ new_content: Optional[str] = None,
555
+ search: Optional[str] = None,
556
+ replace: Optional[str] = None
557
+ ) -> Tuple[bool, Optional[str], Optional[EditErrorType]]:
558
+ """在缓存中应用结构化编辑
559
+
560
+ Args:
561
+ cache_info: 缓存信息字典(会被修改)
562
+ block_id: 块id(字符串,从read_code工具获取)
563
+ action: 操作类型(delete, insert_before, insert_after, replace, edit)
564
+ new_content: 新内容(对于非delete和非edit操作)
565
+ search: 要搜索的文本(对于edit操作)
566
+ replace: 替换后的文本(对于edit操作)
567
+
568
+ Returns:
569
+ (是否成功, 错误信息, 错误类型)
570
+ """
571
+ if not cache_info:
572
+ return (False, "缓存信息不完整", EditErrorType.CACHE_INVALID)
573
+
574
+ # 从 blocks 字典中查找
575
+ blocks = cache_info.get("blocks", {})
576
+ block = blocks.get(block_id)
577
+
578
+ if block is None:
579
+ return (False, f"未找到块id: {block_id}。请使用read_code工具查看文件的结构化块id。", EditErrorType.BLOCK_ID_NOT_FOUND)
580
+
581
+ # 根据操作类型执行编辑
582
+ if action == "delete":
583
+ # 删除块:将当前块的内容清空
584
+ block['content'] = ""
585
+ return (True, None, None)
586
+
587
+ elif action == "insert_before":
588
+ # 在块前插入:在当前块的内容前面插入文本
589
+ if new_content is None:
590
+ return (False, "insert_before操作需要提供content参数", EditErrorType.PARAMETER_MISSING)
591
+
592
+ current_content = block.get('content', '')
593
+ # 自动添加换行符:在插入内容后添加换行符(如果插入内容不以换行符结尾)
594
+ if new_content and not new_content.endswith('\n'):
595
+ new_content = new_content + '\n'
596
+ block['content'] = new_content + current_content
597
+ return (True, None, None)
598
+
599
+ elif action == "insert_after":
600
+ # 在块后插入:在当前块的内容后面插入文本
601
+ if new_content is None:
602
+ return (False, "insert_after操作需要提供content参数", EditErrorType.PARAMETER_MISSING)
603
+
604
+ current_content = block.get('content', '')
605
+ # 自动添加换行符:在插入内容前添加换行符(如果插入内容不以换行符开头)
606
+ # 避免重复换行符:如果当前内容以换行符结尾,则不需要添加
607
+ if new_content and not new_content.startswith('\n'):
608
+ # 如果当前内容不以换行符结尾,则在插入内容前添加换行符
609
+ if not current_content or not current_content.endswith('\n'):
610
+ new_content = '\n' + new_content
611
+ block['content'] = current_content + new_content
612
+ return (True, None, None)
613
+
614
+ elif action == "replace":
615
+ # 替换块
616
+ if new_content is None:
617
+ return (False, "replace操作需要提供content参数", EditErrorType.PARAMETER_MISSING)
618
+
619
+ block['content'] = new_content
620
+ return (True, None, None)
621
+
622
+ elif action == "edit":
623
+ # 在块内进行search/replace
624
+ if search is None:
625
+ return (False, "edit操作需要提供search参数", EditErrorType.PARAMETER_MISSING)
626
+ if replace is None:
627
+ return (False, "edit操作需要提供replace参数", EditErrorType.PARAMETER_MISSING)
628
+
629
+ current_content = block.get('content', '')
630
+
631
+ # 检查匹配次数:必须刚好只有一处匹配
632
+ match_count = current_content.count(search)
633
+ if match_count == 0:
634
+ return (False, f"在块 {block_id} 中未找到要搜索的文本: {search[:100]}...", EditErrorType.SEARCH_NOT_FOUND)
635
+ elif match_count > 1:
636
+ # 找到所有匹配位置,并显示上下文
637
+ lines = current_content.split('\n')
638
+ matches_info = []
639
+ search_lines = search.split('\n')
640
+ search_line_count = len(search_lines)
641
+
642
+ # 使用更精确的方法查找所有匹配位置(处理多行搜索)
643
+ start_pos = 0
644
+ match_idx = 0
645
+ while match_idx < match_count and start_pos < len(current_content):
646
+ pos = current_content.find(search, start_pos)
647
+ if pos == -1:
648
+ break
649
+
650
+ # 计算匹配位置所在的行号
651
+ content_before_match = current_content[:pos]
652
+ line_idx = content_before_match.count('\n')
653
+
654
+ # 显示上下文(前后各2行)
655
+ start_line = max(0, line_idx - 2)
656
+ end_line = min(len(lines), line_idx + search_line_count + 2)
657
+ context_lines = lines[start_line:end_line]
658
+ context = '\n'.join([
659
+ f" {start_line + i + 1:4d}: {context_lines[i]}"
660
+ for i in range(len(context_lines))
661
+ ])
662
+
663
+ # 标记匹配的行
664
+ match_start_in_context = line_idx - start_line
665
+ match_end_in_context = match_start_in_context + search_line_count
666
+ matches_info.append(f"匹配位置 {len(matches_info) + 1} (行 {line_idx + 1}):\n{context}")
667
+
668
+ start_pos = pos + 1 # 继续查找下一个匹配
669
+ match_idx += 1
670
+
671
+ if len(matches_info) >= 5: # 最多显示5个匹配位置
672
+ break
673
+
674
+ matches_preview = "\n\n".join(matches_info)
675
+ if match_count > len(matches_info):
676
+ matches_preview += f"\n\n... 还有 {match_count - len(matches_info)} 处匹配未显示"
677
+
678
+ search_preview = search[:100] + "..." if len(search) > 100 else search
679
+ error_msg = (
680
+ f"在块 {block_id} 中找到 {match_count} 处匹配,但 edit 操作要求刚好只有一处匹配。\n"
681
+ f"搜索文本: {search_preview}\n\n"
682
+ f"匹配位置详情:\n{matches_preview}\n\n"
683
+ f"💡 提示:请提供更多的上下文(如包含前后几行代码)来唯一标识要替换的位置。"
684
+ )
685
+ return (False, error_msg, EditErrorType.MULTIPLE_MATCHES)
686
+
687
+ # 在块内进行替换(只替换第一次出现,此时已经确认只有一处)
688
+ block['content'] = current_content.replace(search, replace, 1)
689
+ return (True, None, None)
690
+
691
+ else:
692
+ return (False, f"不支持的操作类型: {action}", EditErrorType.UNSUPPORTED_ACTION)
693
+
694
+ @staticmethod
695
+ def _format_patch_description(patch: Dict[str, str]) -> str:
696
+ """格式化补丁描述用于错误信息
697
+
698
+ Args:
699
+ patch: 补丁字典
700
+
701
+ Returns:
702
+ 补丁描述字符串
703
+ """
704
+ if "STRUCTURED_BLOCK_ID" in patch:
705
+ block_id = patch.get('STRUCTURED_BLOCK_ID', '')
706
+ action = patch.get('STRUCTURED_ACTION', '')
707
+ if action == "edit":
708
+ search = patch.get('STRUCTURED_SEARCH', '')
709
+ replace = patch.get('STRUCTURED_REPLACE', '')
710
+ search_preview = search[:50] + "..." if len(search) > 50 else search
711
+ replace_preview = replace[:50] + "..." if len(replace) > 50 else replace
712
+ return f"结构化编辑: block_id={block_id}, action={action}, search={search_preview}, replace={replace_preview}"
713
+ else:
714
+ content = patch.get('STRUCTURED_CONTENT', '')
715
+ if content:
716
+ content_preview = content[:100] + "..." if len(content) > 100 else content
717
+ return f"结构化编辑: block_id={block_id}, action={action}, content={content_preview}"
718
+ else:
719
+ return f"结构化编辑: block_id={block_id}, action={action}"
720
+ else:
721
+ return "未知的补丁格式"
722
+
723
+ @staticmethod
724
+ def _generate_error_summary(
725
+ abs_path: str,
726
+ failed_patches: List[Dict[str, Any]],
727
+ patch_count: int,
728
+ successful_patches: int
729
+ ) -> str:
730
+ """生成错误摘要
731
+
732
+ Args:
733
+ abs_path: 文件绝对路径
734
+ failed_patches: 失败的补丁列表
735
+ patch_count: 总补丁数
736
+ successful_patches: 成功的补丁数
737
+
738
+ Returns:
739
+ 错误摘要字符串
740
+ """
741
+ error_details = []
742
+ has_block_id_error = False # 是否有块id相关错误
743
+ has_cache_error = False # 是否有缓存相关错误
744
+ has_multiple_matches_error = False # 是否有多处匹配错误
745
+ has_other_error = False # 是否有其他错误
746
+
747
+ for p in failed_patches:
748
+ patch = p["patch"]
749
+ patch_desc = EditFileTool._format_patch_description(patch)
750
+ error_msg = p['error']
751
+ error_type = p.get('error_type') # 获取错误类型(如果存在)
752
+ error_details.append(f" - 失败的补丁: {patch_desc}\n 错误: {error_msg}")
753
+
754
+ # 优先使用错误类型进行判断(如果存在),否则回退到字符串匹配
755
+ if error_type:
756
+ if error_type == EditErrorType.BLOCK_ID_NOT_FOUND:
757
+ has_block_id_error = True
758
+ elif error_type == EditErrorType.CACHE_INVALID:
759
+ has_cache_error = True
760
+ elif error_type == EditErrorType.MULTIPLE_MATCHES:
761
+ has_multiple_matches_error = True
762
+ else:
763
+ has_other_error = True
764
+ else:
765
+ # 回退到字符串匹配(兼容旧代码或异常情况)
766
+ error_msg_lower = error_msg.lower()
767
+
768
+ # 块id相关错误:检查是否包含"块id"和"未找到"/"不存在"/"找不到"等关键词
769
+ if ("块id" in error_msg or "block_id" in error_msg_lower or "block id" in error_msg_lower) and (
770
+ "未找到" in error_msg or "不存在" in error_msg or "找不到" in error_msg or
771
+ "not found" in error_msg_lower
772
+ ):
773
+ has_block_id_error = True
774
+ # 缓存相关错误:检查是否包含"缓存"或"cache"关键词
775
+ elif ("缓存" in error_msg or "cache" in error_msg_lower) and (
776
+ "信息不完整" in error_msg or "无效" in error_msg or "过期" in error_msg or
777
+ "invalid" in error_msg_lower or "expired" in error_msg_lower
778
+ ):
779
+ has_cache_error = True
780
+ # 多处匹配错误:检查是否包含"匹配"和数量相关的关键词
781
+ elif ("匹配" in error_msg or "match" in error_msg_lower) and (
782
+ "处" in error_msg or "个" in error_msg or "multiple" in error_msg_lower or
783
+ ("找到" in error_msg and ("处" in error_msg or "个" in error_msg))
784
+ ):
785
+ # 识别多处匹配错误(错误消息中已经包含了详细提示)
786
+ has_multiple_matches_error = True
787
+ else:
788
+ has_other_error = True
789
+
790
+ if successful_patches == 0:
791
+ summary = (
792
+ f"文件 {abs_path} 修改失败(全部失败,文件未修改)。\n"
793
+ f"失败: {len(failed_patches)}/{patch_count}.\n"
794
+ f"失败详情:\n" + "\n".join(error_details)
795
+ )
796
+ else:
797
+ summary = (
798
+ f"文件 {abs_path} 修改部分成功。\n"
799
+ f"成功: {successful_patches}/{patch_count}, "
800
+ f"失败: {len(failed_patches)}/{patch_count}.\n"
801
+ f"失败详情:\n" + "\n".join(error_details)
802
+ )
803
+
804
+ # 根据错误类型添加不同的提示
805
+ # 注意:多处匹配错误的错误消息中已经包含了详细提示,不需要额外添加
806
+ hints = []
807
+ if has_block_id_error:
808
+ hints.append("💡 块id不存在:请检查块id是否正确,或使用 read_code 工具重新读取文件以获取最新的块id列表。")
809
+ if has_cache_error:
810
+ hints.append("💡 缓存问题:文件可能已被外部修改,请使用 read_code 工具重新读取文件。")
811
+ if has_other_error and not (has_block_id_error or has_cache_error or has_multiple_matches_error):
812
+ hints.append("💡 提示:请检查块id、操作类型和参数是否正确。")
813
+
814
+ if hints:
815
+ summary += "\n\n" + "\n".join(hints)
816
+
817
+ return summary
818
+
819
+ @staticmethod
820
+ def _write_file_with_rollback(
821
+ abs_path: str,
822
+ content: str,
823
+ backup_path: Optional[str]
824
+ ) -> Tuple[bool, Optional[str]]:
825
+ """写入文件,失败时回滚
826
+
827
+ Args:
828
+ abs_path: 文件绝对路径
829
+ content: 要写入的内容
830
+ backup_path: 备份文件路径或None
831
+
832
+ Returns:
833
+ (是否成功, 错误信息或None)
834
+ """
835
+ try:
836
+ with open(abs_path, "w", encoding="utf-8") as f:
837
+ f.write(content)
838
+ return (True, None)
839
+ except Exception as write_error:
840
+ # 写入失败,尝试回滚
841
+ if backup_path and os.path.exists(backup_path):
842
+ try:
843
+ shutil.copy2(backup_path, abs_path)
844
+ os.remove(backup_path)
845
+ except Exception:
846
+ pass
847
+ error_msg = f"文件写入失败: {str(write_error)}"
848
+ print(f"❌ {error_msg}")
849
+ return (False, error_msg)
850
+
851
+ @staticmethod
852
+ def _fast_edit(file_path: str, patches: List[Dict[str, str]], agent: Any = None) -> Tuple[bool, str]:
853
+ """快速应用补丁到文件
854
+
855
+ 该方法基于缓存进行编辑:
856
+ 1. 先检查缓存有效性,无效则提示重新读取
857
+ 2. 在缓存中应用所有补丁
858
+ 3. 从缓存恢复文件内容并写入
859
+ 4. 更新缓存的时间戳
860
+
861
+ Args:
862
+ file_path: 要修改的文件路径,支持绝对路径和相对路径
863
+ patches: 补丁列表,每个补丁包含 STRUCTURED_BLOCK_ID
864
+ agent: Agent实例,用于访问缓存
865
+
866
+ Returns:
867
+ Tuple[bool, str]:
868
+ 返回处理结果元组,第一个元素表示是否所有补丁都成功应用,
869
+ 第二个元素为结果信息,全部成功时为修改后的文件内容,部分或全部失败时为错误信息
870
+ """
871
+ abs_path = os.path.abspath(file_path)
872
+ backup_path = None
873
+
874
+ try:
875
+ # 检查缓存有效性
876
+ cache_info = EditFileTool._get_file_cache(agent, abs_path)
877
+ if not EditFileTool._is_cache_valid(cache_info, abs_path):
878
+ error_msg = (
879
+ f"⚠️ 缓存无效或文件已被外部修改。\n"
880
+ f"📋 文件: {abs_path}\n"
881
+ f"💡 请先使用 read_code 工具重新读取文件,然后再进行编辑。"
882
+ )
883
+ return False, error_msg
884
+
885
+ # 创建缓存副本,避免直接修改原缓存
886
+ cache_copy = {
887
+ "id_list": list(cache_info["id_list"]), # 浅拷贝列表
888
+ "blocks": {k: v.copy() for k, v in cache_info["blocks"].items()}, # 深拷贝字典
889
+ "total_lines": cache_info["total_lines"],
890
+ "read_time": cache_info.get("read_time", time.time()),
891
+ "file_mtime": cache_info.get("file_mtime", 0),
892
+ "file_ends_with_newline": cache_info.get("file_ends_with_newline", False),
893
+ }
894
+
895
+ # 创建备份
896
+ if os.path.exists(abs_path):
897
+ backup_path = abs_path + ".bak"
898
+ try:
899
+ shutil.copy2(abs_path, backup_path)
900
+ except Exception:
901
+ backup_path = None
902
+
903
+ # 对补丁进行排序
904
+ ordered_patches = EditFileTool._order_patches_by_range(patches)
905
+ patch_count = len(ordered_patches)
906
+ failed_patches: List[Dict[str, Any]] = []
907
+ successful_patches = 0
908
+
909
+ # 在缓存中应用所有补丁
910
+ for patch in ordered_patches:
911
+ # 结构化编辑模式
912
+ if "STRUCTURED_BLOCK_ID" in patch:
913
+ block_id = patch.get("STRUCTURED_BLOCK_ID", "")
914
+ action = patch.get("STRUCTURED_ACTION", "")
915
+ new_content = patch.get("STRUCTURED_CONTENT")
916
+ search = patch.get("STRUCTURED_SEARCH")
917
+ replace = patch.get("STRUCTURED_REPLACE")
918
+ try:
919
+ success, error_msg, error_type = EditFileTool._apply_structured_edit_to_cache(
920
+ cache_copy, block_id, action, new_content, search, replace
921
+ )
922
+ if success:
923
+ successful_patches += 1
924
+ else:
925
+ failed_patches.append({"patch": patch, "error": error_msg, "error_type": error_type})
926
+ except Exception as e:
927
+ error_msg = (
928
+ f"结构化编辑执行出错: {str(e)}\n"
929
+ f"block_id: {block_id}, action: {action}"
930
+ )
931
+ failed_patches.append({"patch": patch, "error": error_msg, "error_type": EditErrorType.OTHER})
932
+ else:
933
+ # 如果不支持的模式,记录错误
934
+ error_msg = "不支持的补丁格式。支持的格式: STRUCTURED_BLOCK_ID"
935
+ failed_patches.append({"patch": patch, "error": error_msg, "error_type": EditErrorType.OTHER})
936
+
937
+ # 如果有失败的补丁,且没有成功的补丁,则不写入文件
938
+ if failed_patches and successful_patches == 0:
939
+ if backup_path and os.path.exists(backup_path):
940
+ try:
941
+ os.remove(backup_path)
942
+ except Exception:
943
+ pass
944
+ summary = EditFileTool._generate_error_summary(
945
+ abs_path, failed_patches, patch_count, successful_patches
946
+ )
947
+ print(f"❌ {summary}")
948
+ return False, summary
949
+
950
+ # 从缓存恢复文件内容
951
+ modified_content = EditFileTool._restore_file_from_cache(cache_copy)
952
+ if not modified_content:
953
+ error_msg = (
954
+ "从缓存恢复文件内容失败。\n"
955
+ "可能原因:缓存数据结构损坏或文件结构异常。\n\n"
956
+ "💡 提示:请使用 read_code 工具重新读取文件,然后再进行编辑。"
957
+ )
958
+ if backup_path and os.path.exists(backup_path):
959
+ try:
960
+ os.remove(backup_path)
961
+ except Exception:
962
+ pass
963
+ return False, error_msg
964
+
965
+ # 写入文件
966
+ success, error_msg = EditFileTool._write_file_with_rollback(abs_path, modified_content, backup_path)
967
+ if not success:
968
+ # 写入失败通常是权限、磁盘空间等问题,不需要重新读取文件
969
+ error_msg += (
970
+ "\n\n💡 提示:文件写入失败,可能是权限不足、磁盘空间不足或文件被锁定。"
971
+ "请检查文件权限和磁盘空间,或稍后重试。"
972
+ )
973
+ return False, error_msg
974
+
975
+ # 写入成功,更新缓存
976
+ if agent:
977
+ cache = agent.get_user_data("read_code_cache")
978
+ if cache and abs_path in cache:
979
+ # 更新缓存内容
980
+ cache[abs_path] = cache_copy
981
+ # 更新缓存时间戳
982
+ EditFileTool._update_cache_timestamp(agent, abs_path)
983
+ agent.set_user_data("read_code_cache", cache)
984
+
985
+ # 写入成功,删除备份文件
986
+ if backup_path and os.path.exists(backup_path):
987
+ try:
988
+ os.remove(backup_path)
989
+ except Exception:
990
+ pass
991
+
992
+ # 如果有失败的补丁,返回部分成功信息
993
+ if failed_patches:
994
+ summary = EditFileTool._generate_error_summary(
995
+ abs_path, failed_patches, patch_count, successful_patches
996
+ )
997
+ print(f"❌ {summary}")
998
+ return False, summary
999
+
1000
+ return True, modified_content
1001
+
1002
+ except Exception as e:
1003
+ # 发生异常时,尝试回滚
1004
+ if backup_path and os.path.exists(backup_path):
1005
+ try:
1006
+ shutil.copy2(backup_path, abs_path)
1007
+ os.remove(backup_path)
1008
+ except Exception:
1009
+ pass
1010
+
1011
+ # 根据异常类型给出不同的提示
1012
+ error_type = type(e).__name__
1013
+ error_str = str(e)
1014
+
1015
+ # 检查是否是权限错误
1016
+ is_permission_error = (
1017
+ error_type == "PermissionError" or
1018
+ (error_type == "OSError" and hasattr(e, 'errno') and e.errno == 13) or
1019
+ "Permission denied" in error_str or
1020
+ "权限" in error_str or
1021
+ "permission" in error_str.lower()
1022
+ )
1023
+
1024
+ # 检查是否是磁盘空间错误
1025
+ is_space_error = (
1026
+ (error_type == "OSError" and hasattr(e, 'errno') and e.errno == 28) or
1027
+ "No space left" in error_str or
1028
+ "No space" in error_str or
1029
+ "ENOSPC" in error_str or
1030
+ "磁盘" in error_str or
1031
+ "空间" in error_str
1032
+ )
1033
+
1034
+ # 检查是否是文件不存在错误
1035
+ is_not_found_error = (
1036
+ error_type == "FileNotFoundError" or
1037
+ (error_type == "OSError" and hasattr(e, 'errno') and e.errno == 2) or
1038
+ "No such file" in error_str or
1039
+ "文件不存在" in error_str
1040
+ )
1041
+
1042
+ # 检查是否是缓存或块相关错误(这些通常是我们自己的错误消息)
1043
+ is_cache_error = (
1044
+ "cache" in error_str.lower() or
1045
+ "缓存" in error_str or
1046
+ "未找到块id" in error_str or
1047
+ "块id" in error_str
1048
+ )
1049
+
1050
+ if is_permission_error:
1051
+ hint = "💡 提示:文件权限不足,请检查文件权限或使用管理员权限运行。"
1052
+ elif is_space_error:
1053
+ hint = "💡 提示:磁盘空间不足,请清理磁盘空间后重试。"
1054
+ elif is_not_found_error:
1055
+ hint = "💡 提示:文件不存在,请检查文件路径是否正确。"
1056
+ elif is_cache_error:
1057
+ hint = "💡 提示:缓存或块id相关错误,请使用 read_code 工具重新读取文件,然后再进行编辑。"
1058
+ elif "block" in error_str.lower() or "块" in error_str:
1059
+ hint = "💡 提示:块操作错误,请使用 read_code 工具重新读取文件,然后再进行编辑。"
1060
+ else:
1061
+ hint = f"💡 提示:发生未知错误({error_type}),请检查错误信息或重试。如问题持续,请使用 read_code 工具重新读取文件。"
1062
+
1063
+ error_msg = f"文件修改失败: {error_str}\n\n{hint}"
1064
+ print(f"❌ {error_msg}")
1065
+ return False, error_msg
1066
+