jarvis-ai-assistant 0.7.16__py3-none-any.whl → 1.0.2__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 (279) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +567 -222
  3. jarvis/jarvis_agent/agent_manager.py +19 -12
  4. jarvis/jarvis_agent/builtin_input_handler.py +79 -11
  5. jarvis/jarvis_agent/config_editor.py +7 -2
  6. jarvis/jarvis_agent/event_bus.py +24 -13
  7. jarvis/jarvis_agent/events.py +19 -1
  8. jarvis/jarvis_agent/file_context_handler.py +67 -64
  9. jarvis/jarvis_agent/file_methodology_manager.py +38 -24
  10. jarvis/jarvis_agent/jarvis.py +186 -114
  11. jarvis/jarvis_agent/language_extractors/__init__.py +8 -1
  12. jarvis/jarvis_agent/language_extractors/c_extractor.py +7 -4
  13. jarvis/jarvis_agent/language_extractors/cpp_extractor.py +9 -4
  14. jarvis/jarvis_agent/language_extractors/go_extractor.py +7 -4
  15. jarvis/jarvis_agent/language_extractors/java_extractor.py +27 -20
  16. jarvis/jarvis_agent/language_extractors/javascript_extractor.py +22 -17
  17. jarvis/jarvis_agent/language_extractors/python_extractor.py +7 -4
  18. jarvis/jarvis_agent/language_extractors/rust_extractor.py +7 -4
  19. jarvis/jarvis_agent/language_extractors/typescript_extractor.py +22 -17
  20. jarvis/jarvis_agent/language_support_info.py +250 -219
  21. jarvis/jarvis_agent/main.py +19 -23
  22. jarvis/jarvis_agent/memory_manager.py +9 -6
  23. jarvis/jarvis_agent/methodology_share_manager.py +21 -15
  24. jarvis/jarvis_agent/output_handler.py +4 -2
  25. jarvis/jarvis_agent/prompt_builder.py +7 -6
  26. jarvis/jarvis_agent/prompt_manager.py +113 -8
  27. jarvis/jarvis_agent/prompts.py +317 -85
  28. jarvis/jarvis_agent/protocols.py +5 -2
  29. jarvis/jarvis_agent/run_loop.py +192 -32
  30. jarvis/jarvis_agent/session_manager.py +7 -3
  31. jarvis/jarvis_agent/share_manager.py +23 -13
  32. jarvis/jarvis_agent/shell_input_handler.py +12 -8
  33. jarvis/jarvis_agent/stdio_redirect.py +25 -26
  34. jarvis/jarvis_agent/task_analyzer.py +29 -23
  35. jarvis/jarvis_agent/task_list.py +869 -0
  36. jarvis/jarvis_agent/task_manager.py +26 -23
  37. jarvis/jarvis_agent/tool_executor.py +6 -5
  38. jarvis/jarvis_agent/tool_share_manager.py +24 -14
  39. jarvis/jarvis_agent/user_interaction.py +3 -3
  40. jarvis/jarvis_agent/utils.py +9 -1
  41. jarvis/jarvis_agent/web_bridge.py +37 -17
  42. jarvis/jarvis_agent/web_output_sink.py +5 -2
  43. jarvis/jarvis_agent/web_server.py +165 -36
  44. jarvis/jarvis_c2rust/__init__.py +1 -1
  45. jarvis/jarvis_c2rust/cli.py +260 -141
  46. jarvis/jarvis_c2rust/collector.py +37 -18
  47. jarvis/jarvis_c2rust/constants.py +60 -0
  48. jarvis/jarvis_c2rust/library_replacer.py +242 -1010
  49. jarvis/jarvis_c2rust/library_replacer_checkpoint.py +133 -0
  50. jarvis/jarvis_c2rust/library_replacer_llm.py +287 -0
  51. jarvis/jarvis_c2rust/library_replacer_loader.py +191 -0
  52. jarvis/jarvis_c2rust/library_replacer_output.py +134 -0
  53. jarvis/jarvis_c2rust/library_replacer_prompts.py +124 -0
  54. jarvis/jarvis_c2rust/library_replacer_utils.py +188 -0
  55. jarvis/jarvis_c2rust/llm_module_agent.py +98 -1044
  56. jarvis/jarvis_c2rust/llm_module_agent_apply.py +170 -0
  57. jarvis/jarvis_c2rust/llm_module_agent_executor.py +288 -0
  58. jarvis/jarvis_c2rust/llm_module_agent_loader.py +170 -0
  59. jarvis/jarvis_c2rust/llm_module_agent_prompts.py +268 -0
  60. jarvis/jarvis_c2rust/llm_module_agent_types.py +57 -0
  61. jarvis/jarvis_c2rust/llm_module_agent_utils.py +150 -0
  62. jarvis/jarvis_c2rust/llm_module_agent_validator.py +119 -0
  63. jarvis/jarvis_c2rust/loaders.py +28 -10
  64. jarvis/jarvis_c2rust/models.py +5 -2
  65. jarvis/jarvis_c2rust/optimizer.py +192 -1974
  66. jarvis/jarvis_c2rust/optimizer_build_fix.py +286 -0
  67. jarvis/jarvis_c2rust/optimizer_clippy.py +766 -0
  68. jarvis/jarvis_c2rust/optimizer_config.py +49 -0
  69. jarvis/jarvis_c2rust/optimizer_docs.py +183 -0
  70. jarvis/jarvis_c2rust/optimizer_options.py +48 -0
  71. jarvis/jarvis_c2rust/optimizer_progress.py +469 -0
  72. jarvis/jarvis_c2rust/optimizer_report.py +52 -0
  73. jarvis/jarvis_c2rust/optimizer_unsafe.py +309 -0
  74. jarvis/jarvis_c2rust/optimizer_utils.py +469 -0
  75. jarvis/jarvis_c2rust/optimizer_visibility.py +185 -0
  76. jarvis/jarvis_c2rust/scanner.py +229 -166
  77. jarvis/jarvis_c2rust/transpiler.py +531 -2732
  78. jarvis/jarvis_c2rust/transpiler_agents.py +503 -0
  79. jarvis/jarvis_c2rust/transpiler_build.py +1294 -0
  80. jarvis/jarvis_c2rust/transpiler_codegen.py +204 -0
  81. jarvis/jarvis_c2rust/transpiler_compile.py +146 -0
  82. jarvis/jarvis_c2rust/transpiler_config.py +178 -0
  83. jarvis/jarvis_c2rust/transpiler_context.py +122 -0
  84. jarvis/jarvis_c2rust/transpiler_executor.py +516 -0
  85. jarvis/jarvis_c2rust/transpiler_generation.py +278 -0
  86. jarvis/jarvis_c2rust/transpiler_git.py +163 -0
  87. jarvis/jarvis_c2rust/transpiler_mod_utils.py +225 -0
  88. jarvis/jarvis_c2rust/transpiler_modules.py +336 -0
  89. jarvis/jarvis_c2rust/transpiler_planning.py +394 -0
  90. jarvis/jarvis_c2rust/transpiler_review.py +1196 -0
  91. jarvis/jarvis_c2rust/transpiler_symbols.py +176 -0
  92. jarvis/jarvis_c2rust/utils.py +269 -79
  93. jarvis/jarvis_code_agent/after_change.py +233 -0
  94. jarvis/jarvis_code_agent/build_validation_config.py +37 -30
  95. jarvis/jarvis_code_agent/builtin_rules.py +68 -0
  96. jarvis/jarvis_code_agent/code_agent.py +976 -1517
  97. jarvis/jarvis_code_agent/code_agent_build.py +227 -0
  98. jarvis/jarvis_code_agent/code_agent_diff.py +246 -0
  99. jarvis/jarvis_code_agent/code_agent_git.py +525 -0
  100. jarvis/jarvis_code_agent/code_agent_impact.py +177 -0
  101. jarvis/jarvis_code_agent/code_agent_lint.py +283 -0
  102. jarvis/jarvis_code_agent/code_agent_llm.py +159 -0
  103. jarvis/jarvis_code_agent/code_agent_postprocess.py +105 -0
  104. jarvis/jarvis_code_agent/code_agent_prompts.py +46 -0
  105. jarvis/jarvis_code_agent/code_agent_rules.py +305 -0
  106. jarvis/jarvis_code_agent/code_analyzer/__init__.py +52 -48
  107. jarvis/jarvis_code_agent/code_analyzer/base_language.py +12 -10
  108. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +12 -11
  109. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +16 -12
  110. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +26 -17
  111. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +558 -104
  112. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +27 -16
  113. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +22 -18
  114. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +21 -16
  115. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +20 -16
  116. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +27 -16
  117. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +47 -23
  118. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +71 -37
  119. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +162 -35
  120. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +111 -57
  121. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +18 -12
  122. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +185 -183
  123. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +2 -1
  124. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +24 -15
  125. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +227 -141
  126. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +321 -247
  127. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +37 -29
  128. jarvis/jarvis_code_agent/code_analyzer/language_support.py +21 -13
  129. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +15 -9
  130. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +75 -45
  131. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +87 -52
  132. jarvis/jarvis_code_agent/code_analyzer/languages/java_language.py +84 -51
  133. jarvis/jarvis_code_agent/code_analyzer/languages/javascript_language.py +94 -64
  134. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +109 -71
  135. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +97 -63
  136. jarvis/jarvis_code_agent/code_analyzer/languages/typescript_language.py +103 -69
  137. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +271 -268
  138. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +76 -64
  139. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +92 -19
  140. jarvis/jarvis_code_agent/diff_visualizer.py +998 -0
  141. jarvis/jarvis_code_agent/lint.py +223 -524
  142. jarvis/jarvis_code_agent/rule_share_manager.py +158 -0
  143. jarvis/jarvis_code_agent/rules/clean_code.md +144 -0
  144. jarvis/jarvis_code_agent/rules/code_review.md +115 -0
  145. jarvis/jarvis_code_agent/rules/documentation.md +165 -0
  146. jarvis/jarvis_code_agent/rules/generate_rules.md +52 -0
  147. jarvis/jarvis_code_agent/rules/performance.md +158 -0
  148. jarvis/jarvis_code_agent/rules/refactoring.md +139 -0
  149. jarvis/jarvis_code_agent/rules/security.md +160 -0
  150. jarvis/jarvis_code_agent/rules/tdd.md +78 -0
  151. jarvis/jarvis_code_agent/test_rules/cpp_test.md +118 -0
  152. jarvis/jarvis_code_agent/test_rules/go_test.md +98 -0
  153. jarvis/jarvis_code_agent/test_rules/java_test.md +99 -0
  154. jarvis/jarvis_code_agent/test_rules/javascript_test.md +113 -0
  155. jarvis/jarvis_code_agent/test_rules/php_test.md +117 -0
  156. jarvis/jarvis_code_agent/test_rules/python_test.md +91 -0
  157. jarvis/jarvis_code_agent/test_rules/ruby_test.md +102 -0
  158. jarvis/jarvis_code_agent/test_rules/rust_test.md +86 -0
  159. jarvis/jarvis_code_agent/utils.py +36 -26
  160. jarvis/jarvis_code_analysis/checklists/loader.py +21 -21
  161. jarvis/jarvis_code_analysis/code_review.py +64 -33
  162. jarvis/jarvis_data/config_schema.json +285 -192
  163. jarvis/jarvis_git_squash/main.py +8 -6
  164. jarvis/jarvis_git_utils/git_commiter.py +53 -76
  165. jarvis/jarvis_mcp/__init__.py +5 -2
  166. jarvis/jarvis_mcp/sse_mcp_client.py +40 -30
  167. jarvis/jarvis_mcp/stdio_mcp_client.py +27 -19
  168. jarvis/jarvis_mcp/streamable_mcp_client.py +35 -26
  169. jarvis/jarvis_memory_organizer/memory_organizer.py +78 -55
  170. jarvis/jarvis_methodology/main.py +48 -39
  171. jarvis/jarvis_multi_agent/__init__.py +56 -23
  172. jarvis/jarvis_multi_agent/main.py +15 -18
  173. jarvis/jarvis_platform/base.py +179 -111
  174. jarvis/jarvis_platform/human.py +27 -16
  175. jarvis/jarvis_platform/kimi.py +52 -45
  176. jarvis/jarvis_platform/openai.py +101 -40
  177. jarvis/jarvis_platform/registry.py +51 -33
  178. jarvis/jarvis_platform/tongyi.py +68 -38
  179. jarvis/jarvis_platform/yuanbao.py +59 -43
  180. jarvis/jarvis_platform_manager/main.py +68 -76
  181. jarvis/jarvis_platform_manager/service.py +24 -14
  182. jarvis/jarvis_rag/README_CONFIG.md +314 -0
  183. jarvis/jarvis_rag/README_DYNAMIC_LOADING.md +311 -0
  184. jarvis/jarvis_rag/README_ONLINE_MODELS.md +230 -0
  185. jarvis/jarvis_rag/__init__.py +57 -4
  186. jarvis/jarvis_rag/cache.py +3 -1
  187. jarvis/jarvis_rag/cli.py +48 -68
  188. jarvis/jarvis_rag/embedding_interface.py +39 -0
  189. jarvis/jarvis_rag/embedding_manager.py +7 -230
  190. jarvis/jarvis_rag/embeddings/__init__.py +41 -0
  191. jarvis/jarvis_rag/embeddings/base.py +114 -0
  192. jarvis/jarvis_rag/embeddings/cohere.py +66 -0
  193. jarvis/jarvis_rag/embeddings/edgefn.py +117 -0
  194. jarvis/jarvis_rag/embeddings/local.py +260 -0
  195. jarvis/jarvis_rag/embeddings/openai.py +62 -0
  196. jarvis/jarvis_rag/embeddings/registry.py +293 -0
  197. jarvis/jarvis_rag/llm_interface.py +8 -6
  198. jarvis/jarvis_rag/query_rewriter.py +8 -9
  199. jarvis/jarvis_rag/rag_pipeline.py +61 -52
  200. jarvis/jarvis_rag/reranker.py +7 -75
  201. jarvis/jarvis_rag/reranker_interface.py +32 -0
  202. jarvis/jarvis_rag/rerankers/__init__.py +41 -0
  203. jarvis/jarvis_rag/rerankers/base.py +109 -0
  204. jarvis/jarvis_rag/rerankers/cohere.py +67 -0
  205. jarvis/jarvis_rag/rerankers/edgefn.py +140 -0
  206. jarvis/jarvis_rag/rerankers/jina.py +79 -0
  207. jarvis/jarvis_rag/rerankers/local.py +89 -0
  208. jarvis/jarvis_rag/rerankers/registry.py +293 -0
  209. jarvis/jarvis_rag/retriever.py +58 -43
  210. jarvis/jarvis_sec/__init__.py +66 -141
  211. jarvis/jarvis_sec/agents.py +21 -17
  212. jarvis/jarvis_sec/analysis.py +80 -33
  213. jarvis/jarvis_sec/checkers/__init__.py +7 -13
  214. jarvis/jarvis_sec/checkers/c_checker.py +356 -164
  215. jarvis/jarvis_sec/checkers/rust_checker.py +47 -29
  216. jarvis/jarvis_sec/cli.py +43 -21
  217. jarvis/jarvis_sec/clustering.py +430 -272
  218. jarvis/jarvis_sec/file_manager.py +99 -55
  219. jarvis/jarvis_sec/parsers.py +9 -6
  220. jarvis/jarvis_sec/prompts.py +4 -3
  221. jarvis/jarvis_sec/report.py +44 -22
  222. jarvis/jarvis_sec/review.py +180 -107
  223. jarvis/jarvis_sec/status.py +50 -41
  224. jarvis/jarvis_sec/types.py +3 -0
  225. jarvis/jarvis_sec/utils.py +160 -83
  226. jarvis/jarvis_sec/verification.py +411 -181
  227. jarvis/jarvis_sec/workflow.py +132 -21
  228. jarvis/jarvis_smart_shell/main.py +28 -41
  229. jarvis/jarvis_stats/cli.py +14 -12
  230. jarvis/jarvis_stats/stats.py +28 -19
  231. jarvis/jarvis_stats/storage.py +14 -8
  232. jarvis/jarvis_stats/visualizer.py +12 -7
  233. jarvis/jarvis_tools/base.py +5 -2
  234. jarvis/jarvis_tools/clear_memory.py +13 -9
  235. jarvis/jarvis_tools/cli/main.py +23 -18
  236. jarvis/jarvis_tools/edit_file.py +572 -873
  237. jarvis/jarvis_tools/execute_script.py +10 -7
  238. jarvis/jarvis_tools/file_analyzer.py +7 -8
  239. jarvis/jarvis_tools/meta_agent.py +287 -0
  240. jarvis/jarvis_tools/methodology.py +5 -3
  241. jarvis/jarvis_tools/read_code.py +305 -1438
  242. jarvis/jarvis_tools/read_symbols.py +50 -17
  243. jarvis/jarvis_tools/read_webpage.py +19 -18
  244. jarvis/jarvis_tools/registry.py +435 -156
  245. jarvis/jarvis_tools/retrieve_memory.py +16 -11
  246. jarvis/jarvis_tools/save_memory.py +8 -6
  247. jarvis/jarvis_tools/search_web.py +31 -31
  248. jarvis/jarvis_tools/sub_agent.py +32 -28
  249. jarvis/jarvis_tools/sub_code_agent.py +44 -60
  250. jarvis/jarvis_tools/task_list_manager.py +1811 -0
  251. jarvis/jarvis_tools/virtual_tty.py +29 -19
  252. jarvis/jarvis_utils/__init__.py +4 -0
  253. jarvis/jarvis_utils/builtin_replace_map.py +2 -1
  254. jarvis/jarvis_utils/clipboard.py +9 -8
  255. jarvis/jarvis_utils/collections.py +331 -0
  256. jarvis/jarvis_utils/config.py +699 -194
  257. jarvis/jarvis_utils/dialogue_recorder.py +294 -0
  258. jarvis/jarvis_utils/embedding.py +6 -3
  259. jarvis/jarvis_utils/file_processors.py +7 -1
  260. jarvis/jarvis_utils/fzf.py +9 -3
  261. jarvis/jarvis_utils/git_utils.py +71 -42
  262. jarvis/jarvis_utils/globals.py +116 -32
  263. jarvis/jarvis_utils/http.py +6 -2
  264. jarvis/jarvis_utils/input.py +318 -83
  265. jarvis/jarvis_utils/jsonnet_compat.py +119 -104
  266. jarvis/jarvis_utils/methodology.py +37 -28
  267. jarvis/jarvis_utils/output.py +201 -44
  268. jarvis/jarvis_utils/utils.py +986 -628
  269. {jarvis_ai_assistant-0.7.16.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/METADATA +49 -33
  270. jarvis_ai_assistant-1.0.2.dist-info/RECORD +304 -0
  271. jarvis/jarvis_code_agent/code_analyzer/structured_code.py +0 -556
  272. jarvis/jarvis_tools/generate_new_tool.py +0 -205
  273. jarvis/jarvis_tools/lsp_client.py +0 -1552
  274. jarvis/jarvis_tools/rewrite_file.py +0 -105
  275. jarvis_ai_assistant-0.7.16.dist-info/RECORD +0 -218
  276. {jarvis_ai_assistant-0.7.16.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/WHEEL +0 -0
  277. {jarvis_ai_assistant-0.7.16.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/entry_points.txt +0 -0
  278. {jarvis_ai_assistant-0.7.16.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/licenses/LICENSE +0 -0
  279. {jarvis_ai_assistant-0.7.16.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/top_level.txt +0 -0
@@ -1,28 +1,40 @@
1
- # -*- coding: utf-8 -*-
1
+ """普通文件编辑工具(基于 search/replace 的非结构化编辑)"""
2
+
2
3
  import os
3
4
  import shutil
4
- import time
5
- from enum import Enum
6
- from typing import Any, Dict, List, Optional, Tuple
7
-
8
5
 
6
+ from jarvis.jarvis_utils.output import PrettyOutput
9
7
 
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" # 其他错误
8
+ # -*- coding: utf-8 -*-
9
+ from typing import Any
10
+ from typing import Dict
11
+ from typing import List
12
+ from typing import Optional
13
+ from typing import Tuple
19
14
 
20
15
 
21
- class EditFileTool:
22
- """文件编辑工具,用于对文件进行结构化编辑"""
16
+ class EditFileNormalTool:
17
+ """普通文件编辑工具,完全基于 search/replace 进行文件编辑"""
23
18
 
24
19
  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字符),建议拆分为多个较小的编辑操作"
20
+ description = (
21
+ "使用 search/replace 对文件进行普通文本编辑(不依赖块id),支持同时修改多个文件。\n\n"
22
+ "💡 使用方式:\n"
23
+ "1. 直接指定要编辑的文件路径\n"
24
+ "2. 为每个文件提供一组 search/replace 操作\n"
25
+ "3. 使用精确匹配查找 search 文本,找到匹配后替换为新文本\n\n"
26
+ "🚀 特殊功能:\n"
27
+ '- 当 search 为空字符串 "" 时,表示直接重写整个文件,replace 的内容将作为文件的完整新内容\n'
28
+ "- 如果存在多个diffs且第一个diff的search为空字符串,将只应用第一个diff(重写整个文件),跳过后续所有diffs\n\n"
29
+ "⚠️ 提示:\n"
30
+ "- search 使用精确字符串匹配,不支持正则表达式\n"
31
+ "- **重要:search 必须提供足够的上下文来唯一定位目标位置**,避免匹配到错误的位置。建议包含:\n"
32
+ " * 目标代码的前后几行上下文(至少包含目标代码所在函数的签名或关键标识)\n"
33
+ " * 目标代码附近的唯一标识符(如函数名、变量名、注释等)\n"
34
+ " * 避免使用过短的 search 文本(如单个单词、短字符串),除非能确保唯一性\n"
35
+ "- 如果某个 search 在文件中找不到精确匹配(search非空时),将导致该文件的编辑失败,文件内容会回滚到原始状态\n"
36
+ "- 建议在 search 中包含足够的上下文,确保能唯一匹配到目标位置,避免误匹配"
37
+ )
26
38
 
27
39
  parameters = {
28
40
  "type": "object",
@@ -41,50 +53,36 @@ class EditFileTool:
41
53
  "items": {
42
54
  "type": "object",
43
55
  "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
56
  "search": {
58
57
  "type": "string",
59
- "description": "要搜索的文本(对于edit操作必需)",
58
+ "description": '要搜索的原始文本(不支持正则表达式)。当为空字符串""时,表示直接重写整个文件,replace的内容将作为文件的完整新内容。非空时,**重要:必须提供足够的上下文来唯一定位目标位置**,建议包含目标代码的前后几行上下文、函数签名或唯一标识符,避免匹配到错误的位置。',
60
59
  },
61
60
  "replace": {
62
61
  "type": "string",
63
- "description": "替换后的文本(对于edit操作必需)",
62
+ "description": "替换后的文本(可以为空字符串)",
64
63
  },
65
64
  },
66
- "required": ["block_id", "action"],
65
+ "required": ["search", "replace"],
67
66
  },
68
- "description": "修改操作列表,每个操作包含一个结构化编辑块",
67
+ "description": "普通文本替换操作列表,按顺序依次应用到文件内容",
69
68
  },
70
69
  },
71
70
  "required": ["file_path", "diffs"],
72
71
  },
73
- "description": "要修改的文件列表,每个文件包含文件路径和对应的编辑操作列表",
72
+ "description": "要修改的文件列表,每个文件包含文件路径和对应的 search/replace 操作列表",
74
73
  },
75
74
  },
76
75
  "required": ["files"],
77
76
  }
78
77
 
79
78
  def __init__(self):
80
- """初始化文件编辑工具"""
79
+ """初始化普通文件编辑工具"""
81
80
  pass
82
81
 
83
-
84
82
  @staticmethod
85
83
  def _validate_basic_args(args: Dict[str, Any]) -> Optional[Dict[str, Any]]:
86
84
  """验证基本参数
87
-
85
+
88
86
  Returns:
89
87
  如果验证失败,返回错误响应;否则返回None
90
88
  """
@@ -103,305 +101,449 @@ class EditFileTool:
103
101
  "stdout": "",
104
102
  "stderr": "files参数必须是数组类型",
105
103
  }
106
-
104
+
107
105
  if len(files) == 0:
108
106
  return {
109
107
  "success": False,
110
108
  "stdout": "",
111
109
  "stderr": "files数组不能为空",
112
110
  }
113
-
111
+
114
112
  # 验证每个文件项
115
113
  for idx, file_item in enumerate(files):
116
114
  if not isinstance(file_item, dict):
117
115
  return {
118
116
  "success": False,
119
117
  "stdout": "",
120
- "stderr": f"files数组第 {idx+1} 项必须是字典类型",
118
+ "stderr": f"files数组第 {idx + 1} 项必须是字典类型",
121
119
  }
122
-
120
+
123
121
  file_path = file_item.get("file_path")
124
122
  diffs = file_item.get("diffs", [])
125
-
123
+
126
124
  if not file_path:
127
125
  return {
128
126
  "success": False,
129
127
  "stdout": "",
130
- "stderr": f"files数组第 {idx+1} 项缺少必需参数:file_path",
128
+ "stderr": f"files数组第 {idx + 1} 项缺少必需参数:file_path",
131
129
  }
132
-
130
+
133
131
  if not diffs:
134
132
  return {
135
133
  "success": False,
136
134
  "stdout": "",
137
- "stderr": f"files数组第 {idx+1} 项缺少必需参数:diffs",
135
+ "stderr": f"files数组第 {idx + 1} 项缺少必需参数:diffs",
138
136
  }
139
-
137
+
140
138
  if not isinstance(diffs, list):
141
139
  return {
142
140
  "success": False,
143
141
  "stdout": "",
144
- "stderr": f"files数组第 {idx+1} 项的diffs参数必须是数组类型",
142
+ "stderr": f"files数组第 {idx + 1} 项的diffs参数必须是数组类型",
145
143
  }
146
-
144
+
147
145
  return None
148
146
 
149
147
  @staticmethod
150
- def _get_file_cache(agent: Any, filepath: str) -> Optional[Dict[str, Any]]:
151
- """获取文件的缓存信息
152
-
148
+ def _read_file_with_backup(file_path: str) -> Tuple[str, Optional[str]]:
149
+ """读取文件并创建备份
150
+
153
151
  Args:
154
- agent: Agent实例
155
- filepath: 文件路径
156
-
152
+ file_path: 文件路径
153
+
157
154
  Returns:
158
- 缓存信息字典,如果不存在则返回None
155
+ (文件内容, 备份文件路径或None)
159
156
  """
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)
157
+ abs_path = os.path.abspath(file_path)
158
+ os.makedirs(os.path.dirname(abs_path), exist_ok=True)
159
+
160
+ file_content = ""
161
+ backup_path = None
162
+ if os.path.exists(abs_path):
163
+ with open(abs_path, "r", encoding="utf-8") as f:
164
+ file_content = f.read()
165
+ # 创建备份文件
166
+ backup_path = abs_path + ".bak"
167
+ try:
168
+ shutil.copy2(abs_path, backup_path)
169
+ except Exception:
170
+ # 备份失败不影响主流程
171
+ backup_path = None
172
+
173
+ return file_content, backup_path
169
174
 
170
175
  @staticmethod
171
- def _is_cache_valid(cache_info: Optional[Dict[str, Any]], filepath: str) -> bool:
172
- """检查缓存是否有效
173
-
176
+ def _write_file_with_rollback(
177
+ abs_path: str, content: str, backup_path: Optional[str]
178
+ ) -> Tuple[bool, Optional[str]]:
179
+ """写入文件,失败时回滚
180
+
174
181
  Args:
175
- cache_info: 缓存信息字典
176
- filepath: 文件路径
177
-
182
+ abs_path: 文件绝对路径
183
+ content: 要写入的内容
184
+ backup_path: 备份文件路径或None
185
+
178
186
  Returns:
179
- True表示缓存有效,False表示缓存无效
187
+ (是否成功, 错误信息或None)
180
188
  """
181
- if not cache_info:
182
- return False
183
-
184
189
  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
190
+ with open(abs_path, "w", encoding="utf-8") as f:
191
+ f.write(content)
192
+ return (True, None)
193
+ except Exception as write_error:
194
+ # 写入失败,尝试回滚
195
+ if backup_path and os.path.exists(backup_path):
196
+ try:
197
+ shutil.copy2(backup_path, abs_path)
198
+ os.remove(backup_path)
199
+ except Exception:
200
+ pass
201
+ error_msg = f"文件写入失败: {str(write_error)}"
202
+ PrettyOutput.auto_print(f" {error_msg}")
203
+ return (False, error_msg)
204
+
205
+ @staticmethod
206
+ def _validate_normal_diff(
207
+ diff: Dict[str, Any], idx: int
208
+ ) -> Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]:
209
+ """验证并转换 normal 类型的 diff
210
+
211
+ Returns:
212
+ (错误响应或None, 规范化后的diff或None)
213
+ """
214
+ search = diff.get("search")
215
+ replace = diff.get("replace")
216
+
217
+ if search is None:
218
+ return (
219
+ {
220
+ "success": False,
221
+ "stdout": "",
222
+ "stderr": f"第 {idx} 个diff缺少search参数",
223
+ },
224
+ None,
225
+ )
226
+ if not isinstance(search, str):
227
+ return (
228
+ {
229
+ "success": False,
230
+ "stdout": "",
231
+ "stderr": f"第 {idx} 个diff的search参数必须是字符串",
232
+ },
233
+ None,
234
+ )
235
+ # 允许空字符串作为search参数,表示直接重写整个文件
236
+
237
+ if replace is None:
238
+ return (
239
+ {
240
+ "success": False,
241
+ "stdout": "",
242
+ "stderr": f"第 {idx} 个diff缺少replace参数",
243
+ },
244
+ None,
245
+ )
246
+ if not isinstance(replace, str):
247
+ return (
248
+ {
249
+ "success": False,
250
+ "stdout": "",
251
+ "stderr": f"第 {idx} 个diff的replace参数必须是字符串",
252
+ },
253
+ None,
254
+ )
255
+
256
+ return (
257
+ None,
258
+ {
259
+ "search": search,
260
+ "replace": replace,
261
+ },
262
+ )
203
263
 
204
264
  @staticmethod
205
- def _find_block_by_id_in_cache(cache_info: Dict[str, Any], block_id: str) -> Optional[Dict[str, Any]]:
206
- """从缓存中根据块id定位代码块
207
-
265
+ def _count_matches(content: str, search_text: str) -> int:
266
+ """统计文本在内容中的匹配次数
267
+
208
268
  Args:
209
- cache_info: 缓存信息字典
210
- block_id: 块id
211
-
269
+ content: 文件内容
270
+ search_text: 要搜索的文本
271
+
212
272
  Returns:
213
- 如果找到,返回包含 content 的字典;否则返回 None
273
+ 匹配次数
214
274
  """
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
275
+ if not search_text:
276
+ return 0
277
+ return content.count(search_text)
227
278
 
228
279
  @staticmethod
229
- def _update_cache_timestamp(agent: Any, filepath: str) -> None:
230
- """更新缓存的时间戳
231
-
280
+ def _generate_diff_preview(
281
+ original_content: str,
282
+ modified_content: str,
283
+ file_path: str,
284
+ match_count: int,
285
+ search_text: str,
286
+ replace_text: str,
287
+ agent: Optional[Any] = None,
288
+ token_ratio: float = 0.3,
289
+ ) -> str:
290
+ """生成修改后的预览diff
291
+
232
292
  Args:
233
- agent: Agent实例
234
- filepath: 文件路径
293
+ original_content: 原始文件内容
294
+ modified_content: 修改后的文件内容
295
+ file_path: 文件路径
296
+ match_count: 匹配次数
297
+ search_text: 搜索文本
298
+ replace_text: 替换文本
299
+ agent: 可选的 agent 实例,用于获取剩余 token 数量
300
+ token_ratio: token 使用比例(默认 0.3,即 30%)
301
+
302
+ Returns:
303
+ 预览diff字符串
235
304
  """
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
- # 更新文件修改时间
305
+ import difflib
306
+
307
+ # 生成统一的diff格式
308
+ original_lines = original_content.splitlines(keepends=True)
309
+ modified_lines = modified_content.splitlines(keepends=True)
310
+
311
+ # 使用difflib生成统一的diff
312
+ diff = list(
313
+ difflib.unified_diff(
314
+ original_lines,
315
+ modified_lines,
316
+ fromfile=f"a/{file_path}",
317
+ tofile=f"b/{file_path}",
318
+ lineterm="",
319
+ )
320
+ )
321
+
322
+ diff_preview = "".join(diff)
323
+
324
+ # 根据剩余token计算最大字符数
325
+ max_diff_chars = None
326
+
327
+ # 优先尝试使用 agent 获取剩余 token(更准确,包含对话历史)
328
+ if agent:
247
329
  try:
248
- if os.path.exists(abs_path):
249
- cache[abs_path]["file_mtime"] = os.path.getmtime(abs_path)
330
+ remaining_tokens = agent.get_remaining_token_count()
331
+ if remaining_tokens > 0:
332
+ # 使用剩余 token 的指定比例作为字符限制(1 token ≈ 4字符)
333
+ max_diff_chars = int(remaining_tokens * token_ratio * 4)
334
+ if max_diff_chars <= 0:
335
+ max_diff_chars = None
250
336
  except Exception:
251
337
  pass
252
- agent.set_user_data("read_code_cache", cache)
338
+
339
+ # 回退方案:使用输入窗口的指定比例转换为字符数
340
+ if max_diff_chars is None:
341
+ try:
342
+ from jarvis.jarvis_utils.config import get_max_input_token_count
343
+
344
+ max_input_tokens = get_max_input_token_count()
345
+ max_diff_chars = int(max_input_tokens * token_ratio * 4)
346
+ except Exception:
347
+ # 如果获取失败,使用默认值(约 10000 字符)
348
+ max_diff_chars = 10000
349
+
350
+ # 限制diff长度
351
+ if len(diff_preview) > max_diff_chars:
352
+ diff_preview = (
353
+ diff_preview[:max_diff_chars] + "\n... (diff 内容过长,已截断)"
354
+ )
355
+
356
+ return diff_preview
253
357
 
254
358
  @staticmethod
255
- def _validate_structured(diff: Dict[str, Any], idx: int) -> Tuple[Optional[Dict[str, Any]], Optional[Dict[str, str]]]:
256
- """验证并转换structured类型的diff
257
-
359
+ def _confirm_multiple_matches(
360
+ agent: Any,
361
+ file_path: str,
362
+ original_content: str,
363
+ modified_content: str,
364
+ match_count: int,
365
+ search_text: str,
366
+ replace_text: str,
367
+ ) -> bool:
368
+ """使用 agent 确认多个匹配是否应该继续
369
+
370
+ Args:
371
+ agent: Agent 实例
372
+ file_path: 文件路径
373
+ original_content: 原始文件内容
374
+ modified_content: 修改后的文件内容
375
+ match_count: 匹配次数
376
+ search_text: 搜索文本
377
+ replace_text: 替换文本
378
+
258
379
  Returns:
259
- (错误响应或None, patch字典或None)
380
+ True 表示确认继续,False 表示取消
260
381
  """
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)
382
+ try:
383
+ from jarvis.jarvis_agent import Agent
356
384
 
385
+ agent_instance: Agent = agent
386
+ if not agent_instance or not agent_instance.model:
387
+ # 如果没有 agent 或 model,默认不继续
388
+ return False
389
+
390
+ # 生成预览diff
391
+ diff_preview = EditFileNormalTool._generate_diff_preview(
392
+ original_content,
393
+ modified_content,
394
+ file_path,
395
+ match_count,
396
+ search_text,
397
+ replace_text,
398
+ agent=agent_instance,
399
+ token_ratio=0.3, # 使用30%的剩余token用于diff预览
400
+ )
401
+
402
+ prompt = f"""检测到文件编辑操作中,search 文本在文件中存在多处匹配,需要您确认是否继续修改:
403
+
404
+ 文件路径:{file_path}
405
+
406
+ 匹配统计:
407
+ - 匹配数量: {match_count}
408
+ - 搜索文本长度: {len(search_text)} 字符
409
+ - 替换文本长度: {len(replace_text)} 字符
410
+
411
+ 修改预览(diff):
412
+ {diff_preview}
413
+
414
+ 请仔细分析以上代码变更,判断这些修改是否合理。可能的情况包括:
415
+ 1. 这些匹配位置都是您想要修改的,修改是正确的
416
+ 2. 这些匹配位置不是您想要的,或者需要更精确的定位
417
+ 3. 修改可能影响其他不相关的代码
418
+
419
+ 请使用以下协议回答(必须包含且仅包含以下标记之一):
420
+ - 如果认为这些修改是合理的,回答: <!!!YES!!!>
421
+ - 如果认为这些修改不合理或存在风险,回答: <!!!NO!!!>
422
+
423
+ 请严格按照协议格式回答,不要添加其他内容。"""
424
+
425
+ PrettyOutput.auto_print("🤖 正在询问大模型确认多处匹配的修改是否合理...")
426
+ response = agent_instance.model.chat_until_success(prompt)
427
+ response_str = str(response or "")
428
+
429
+ # 使用确定的协议标记解析回答
430
+ if "<!!!YES!!!>" in response_str:
431
+ PrettyOutput.auto_print("✅ 大模型确认:修改合理,继续执行")
432
+ return True
433
+ elif "<!!!NO!!!>" in response_str:
434
+ PrettyOutput.auto_print("⚠️ 大模型确认:修改不合理,取消操作")
435
+ return False
436
+ else:
437
+ # 如果无法找到协议标记,默认认为不合理(保守策略)
438
+ PrettyOutput.auto_print(
439
+ f"⚠️ 无法找到协议标记,默认认为不合理。回答内容: {response_str[:200]}"
440
+ )
441
+ return False
442
+ except Exception as e:
443
+ # 确认过程出错,默认不继续
444
+ PrettyOutput.auto_print(f"⚠️ 确认过程出错: {e},默认取消操作")
445
+ return False
357
446
 
358
447
  @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列表)
448
+ def _apply_normal_edits_to_content(
449
+ original_content: str,
450
+ diffs: List[Dict[str, Any]],
451
+ agent: Optional[Any] = None,
452
+ file_path: Optional[str] = None,
453
+ start_idx: int = 0,
454
+ ) -> Tuple[bool, str, Optional[Dict[str, Any]], Optional[int]]:
455
+ """对文件内容按顺序应用普通 search/replace 编辑(使用字符串替换)
456
+
457
+ Args:
458
+ original_content: 原始文件内容(或已部分修改的内容)
459
+ diffs: diff 列表
460
+ agent: 可选的 agent 实例
461
+ file_path: 可选的文件路径
462
+ start_idx: 从哪个 diff 索引开始处理(0-based,用于继续处理剩余 diffs)
463
+
464
+ 返回:
465
+ (是否成功, 新内容或错误信息, 确认信息字典或None, 需要确认的diff索引或None)
466
+ 确认信息字典包含: match_count, search_text, replace_text, modified_content, current_content
364
467
  """
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)
468
+ content = original_content
469
+
470
+ for idx, diff in enumerate(diffs[start_idx:], start=start_idx + 1):
471
+ search = diff["search"]
472
+ replace = diff["replace"]
473
+
474
+ # 处理空字符串search的特殊情况
475
+ if search == "":
476
+ # 空字符串表示直接重写整个文件
477
+ content = replace
478
+ # 空search只处理第一个diff,跳过后续所有diffs
479
+ break
480
+
481
+ # 验证 search 文本
482
+ if not isinstance(search, str):
483
+ error_info = f"第 {idx} 个diff失败:search 文本必须是字符串"
484
+ return False, error_info, None, None
485
+
486
+ # 统计匹配次数
487
+ match_count = EditFileNormalTool._count_matches(content, search)
488
+
489
+ if match_count == 0:
490
+ # 找不到匹配
491
+ error_info = f"第 {idx} 个diff失败:未找到精确匹配的文本"
492
+ if search:
493
+ error_info += f"\n搜索文本: {search[:200]}..."
494
+ error_info += (
495
+ "\n💡 提示:如果搜索文本在文件中存在但未找到匹配,可能是因为:"
496
+ )
497
+ error_info += "\n 1. 搜索文本包含不可见字符或格式不匹配(建议检查空格、换行等)"
498
+ error_info += "\n 2. **文件可能已被更新**:如果文件在其他地方被修改了,搜索文本可能已经不存在或已改变"
499
+ if file_path:
500
+ error_info += f"\n 💡 建议:使用 `read_code` 工具重新读取文件 `{file_path}` 查看当前内容,"
501
+ error_info += "\n 确认文件是否已被更新,然后根据实际内容调整 search 文本"
502
+ return False, error_info, None, None
503
+
504
+ if match_count == 1:
505
+ # 唯一匹配,直接替换
506
+ content = content.replace(search, replace, 1)
507
+ else:
508
+ # 多个匹配,需要确认
509
+ # 生成修改后的内容(替换所有匹配)
510
+ modified_content = content.replace(search, replace)
511
+ # 返回确认信息,包含当前内容以便继续处理后续 diffs
512
+ confirm_info = {
513
+ "match_count": match_count,
514
+ "search_text": search,
515
+ "replace_text": replace,
516
+ "modified_content": modified_content,
517
+ "current_content": content, # 保存当前内容,用于继续处理
518
+ "diff_idx": idx, # 保存当前 diff 索引
519
+ }
520
+ error_info = (
521
+ f"第 {idx} 个diff失败:发现 {match_count} 处匹配,需要确认后再修改"
522
+ )
523
+ return False, error_info, confirm_info, idx
524
+
525
+ return True, content, None, None
384
526
 
385
527
  def execute(self, args: Dict[str, Any]) -> Dict[str, Any]:
386
- """执行文件编辑操作(支持同时修改多个文件)"""
528
+ """执行普通 search/replace 文件编辑操作(支持同时修改多个文件)"""
387
529
  try:
388
- # 验证基本参数
389
- error_response = EditFileTool._validate_basic_args(args)
530
+ # 验证基本参数(files 结构)
531
+ error_response = EditFileNormalTool._validate_basic_args(args)
390
532
  if error_response:
391
533
  return error_response
392
-
534
+
393
535
  files = args.get("files", [])
394
- agent = args.get("agent", None)
536
+ # 获取 agent 实例(v1.0 协议中 agent 在 args 中)
537
+ agent = args.get("agent")
395
538
 
396
539
  # 记录 PATCH 操作调用统计
397
540
  try:
398
541
  from jarvis.jarvis_stats.stats import StatsManager
399
542
 
400
- StatsManager.increment("patch", group="tool")
543
+ StatsManager.increment("patch_normal", group="tool")
401
544
  except Exception:
402
545
  pass
403
546
 
404
- # 处理每个文件
405
547
  all_results = []
406
548
  overall_success = True
407
549
  successful_files = []
@@ -411,22 +553,164 @@ class EditFileTool:
411
553
  file_path = file_item.get("file_path")
412
554
  diffs = file_item.get("diffs", [])
413
555
 
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', '参数验证失败')}")
556
+ # 校验并规范化 diffs
557
+ normalized_diffs: List[Dict[str, Any]] = []
558
+ for idx, diff in enumerate(diffs, start=1):
559
+ if not isinstance(diff, dict):
560
+ all_results.append(
561
+ f"❌ {file_path}: 第 {idx} 个diff必须是字典类型"
562
+ )
563
+ failed_files.append(file_path)
564
+ overall_success = False
565
+ normalized_diffs = []
566
+ break
567
+
568
+ error, normalized = EditFileNormalTool._validate_normal_diff(
569
+ diff, idx
570
+ )
571
+ if error:
572
+ all_results.append(
573
+ f"❌ {file_path}: {error.get('stderr', '参数验证失败')}"
574
+ )
575
+ failed_files.append(file_path)
576
+ overall_success = False
577
+ normalized_diffs = []
578
+ break
579
+
580
+ if normalized is not None:
581
+ normalized_diffs.append(normalized)
582
+
583
+ if not normalized_diffs:
584
+ # 该文件的diffs有问题,已记录错误,跳过
585
+ continue
586
+
587
+ # 读取原始内容并创建备份
588
+ (
589
+ original_content,
590
+ backup_path,
591
+ ) = EditFileNormalTool._read_file_with_backup(file_path)
592
+
593
+ # 应用所有普通编辑,使用循环处理所有可能的确认情况
594
+ current_content = original_content
595
+ current_start_idx = 0
596
+ success = False
597
+ result_or_error = ""
598
+ max_confirm_iterations = len(normalized_diffs) * 2 # 防止无限循环
599
+ confirm_iteration = 0
600
+
601
+ while confirm_iteration < max_confirm_iterations:
602
+ (
603
+ iter_success,
604
+ iter_result_or_error,
605
+ iter_confirm_info,
606
+ iter_confirm_diff_idx,
607
+ ) = EditFileNormalTool._apply_normal_edits_to_content(
608
+ current_content,
609
+ normalized_diffs,
610
+ agent=agent,
611
+ file_path=file_path,
612
+ start_idx=current_start_idx,
613
+ )
614
+
615
+ if iter_success:
616
+ # 所有 diffs 处理成功
617
+ success = True
618
+ result_or_error = iter_result_or_error
619
+ break
620
+
621
+ # 处理失败,检查是否需要确认
622
+ if (
623
+ iter_confirm_info
624
+ and agent
625
+ and iter_confirm_diff_idx is not None
626
+ ):
627
+ # 需要确认
628
+ confirmed = EditFileNormalTool._confirm_multiple_matches(
629
+ agent,
630
+ file_path,
631
+ original_content,
632
+ iter_confirm_info["modified_content"],
633
+ iter_confirm_info["match_count"],
634
+ iter_confirm_info["search_text"],
635
+ iter_confirm_info["replace_text"],
636
+ )
637
+
638
+ if confirmed:
639
+ # 确认继续,应用当前 diff 的所有匹配替换
640
+ current_content = iter_confirm_info["modified_content"]
641
+ current_diff_idx = iter_confirm_info.get(
642
+ "diff_idx", iter_confirm_diff_idx
643
+ )
644
+ # 从下一个 diff 继续处理
645
+ # current_diff_idx 是 1-based(第几个 diff),转换为 0-based 列表索引
646
+ # 例如:diff_idx=2 表示第 2 个 diff(diffs[1]),下一个是 diffs[2],所以 start_idx=2
647
+ # 注意:current_diff_idx 是 1-based,下一个 diff 的 0-based 索引正好等于 current_diff_idx
648
+ current_start_idx = current_diff_idx
649
+ confirm_iteration += 1
650
+ # 继续循环处理剩余 diffs
651
+ continue
652
+ else:
653
+ # 确认取消
654
+ if backup_path and os.path.exists(backup_path):
655
+ try:
656
+ os.remove(backup_path)
657
+ except Exception:
658
+ pass
659
+ result_or_error = "操作已取消(发现多处匹配,已确认不继续)"
660
+ all_results.append(f"❌ {file_path}: {result_or_error}")
661
+ failed_files.append(file_path)
662
+ overall_success = False
663
+ success = False # 标记为失败,跳出循环
664
+ break
665
+ else:
666
+ # 没有确认信息或没有 agent,直接失败
667
+ success = False
668
+ result_or_error = iter_result_or_error
669
+ break
670
+
671
+ if not success:
672
+ # 处理失败,确保有错误信息
673
+ if confirm_iteration >= max_confirm_iterations:
674
+ # 达到最大确认次数,可能陷入循环
675
+ if not result_or_error:
676
+ result_or_error = f"处理失败:达到最大确认次数限制({max_confirm_iterations}),可能存在循环确认问题"
677
+ elif not result_or_error:
678
+ # 如果没有设置错误信息,使用默认错误信息
679
+ result_or_error = "处理失败:未知错误"
680
+
681
+ # 处理失败
682
+ if backup_path and os.path.exists(backup_path):
683
+ try:
684
+ os.remove(backup_path)
685
+ except Exception:
686
+ pass
687
+ all_results.append(f"❌ {file_path}: {result_or_error}")
418
688
  failed_files.append(file_path)
419
689
  overall_success = False
420
690
  continue
421
691
 
422
- # 执行编辑
423
- success, result = self._fast_edit(file_path, patches, agent)
692
+ # 编辑成功,继续写入文件
693
+ result_or_error = result_or_error # 此时 result_or_error 是新内容
424
694
 
425
- if success:
695
+ # 写入文件(失败时回滚)
696
+ abs_path = os.path.abspath(file_path)
697
+ (
698
+ write_success,
699
+ write_error,
700
+ ) = EditFileNormalTool._write_file_with_rollback(
701
+ abs_path, result_or_error, backup_path
702
+ )
703
+ if write_success:
704
+ # 写入成功,删除备份文件
705
+ if backup_path and os.path.exists(backup_path):
706
+ try:
707
+ os.remove(backup_path)
708
+ except Exception:
709
+ pass
426
710
  all_results.append(f"✅ {file_path}: 修改成功")
427
711
  successful_files.append(file_path)
428
712
  else:
429
- all_results.append(f"❌ {file_path}: {result}")
713
+ all_results.append(f"❌ {file_path}: {write_error}")
430
714
  failed_files.append(file_path)
431
715
  overall_success = False
432
716
 
@@ -436,12 +720,12 @@ class EditFileTool:
436
720
  output_lines.append(f"✅ 成功修改 {len(successful_files)} 个文件:")
437
721
  for file_path in successful_files:
438
722
  output_lines.append(f" - {file_path}")
439
-
723
+
440
724
  if failed_files:
441
725
  output_lines.append(f"\n❌ 失败 {len(failed_files)} 个文件:")
442
726
  for file_path in failed_files:
443
727
  output_lines.append(f" - {file_path}")
444
-
728
+
445
729
  stdout_text = "\n".join(all_results)
446
730
  summary = "\n".join(output_lines) if output_lines else ""
447
731
 
@@ -452,615 +736,30 @@ class EditFileTool:
452
736
  "stderr": "",
453
737
  }
454
738
  else:
739
+ # 失败时,stderr 应该包含详细的错误信息,而不仅仅是文件列表
740
+ # 从 all_results 中提取失败文件的详细错误信息
741
+ failed_details = []
742
+ for result in all_results:
743
+ if result.startswith("❌"):
744
+ failed_details.append(result)
745
+
746
+ # 如果有详细的错误信息,使用它们;否则使用 summary
747
+ stderr_content = (
748
+ "\n".join(failed_details)
749
+ if failed_details
750
+ else (summary if summary else "部分文件修改失败")
751
+ )
752
+
455
753
  return {
456
754
  "success": False,
457
755
  "stdout": stdout_text + ("\n\n" + summary if summary else ""),
458
- "stderr": summary if summary else "部分文件修改失败",
756
+ "stderr": stderr_content,
459
757
  }
460
758
 
461
759
  except Exception as e:
462
760
  error_msg = f"文件编辑失败: {str(e)}"
463
- print(f"❌ {error_msg}")
761
+ PrettyOutput.auto_print(f"❌ {error_msg}")
464
762
  return {"success": False, "stdout": "", "stderr": error_msg}
465
763
 
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
764
 
765
+ __all__ = ["EditFileNormalTool"]