jarvis-ai-assistant 0.1.222__py3-none-any.whl → 0.7.0__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 (162) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +1143 -245
  3. jarvis/jarvis_agent/agent_manager.py +97 -0
  4. jarvis/jarvis_agent/builtin_input_handler.py +12 -10
  5. jarvis/jarvis_agent/config_editor.py +57 -0
  6. jarvis/jarvis_agent/edit_file_handler.py +392 -99
  7. jarvis/jarvis_agent/event_bus.py +48 -0
  8. jarvis/jarvis_agent/events.py +157 -0
  9. jarvis/jarvis_agent/file_context_handler.py +79 -0
  10. jarvis/jarvis_agent/file_methodology_manager.py +117 -0
  11. jarvis/jarvis_agent/jarvis.py +1117 -147
  12. jarvis/jarvis_agent/main.py +78 -34
  13. jarvis/jarvis_agent/memory_manager.py +195 -0
  14. jarvis/jarvis_agent/methodology_share_manager.py +174 -0
  15. jarvis/jarvis_agent/prompt_manager.py +82 -0
  16. jarvis/jarvis_agent/prompts.py +46 -9
  17. jarvis/jarvis_agent/protocols.py +4 -1
  18. jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
  19. jarvis/jarvis_agent/run_loop.py +146 -0
  20. jarvis/jarvis_agent/session_manager.py +9 -9
  21. jarvis/jarvis_agent/share_manager.py +228 -0
  22. jarvis/jarvis_agent/shell_input_handler.py +23 -3
  23. jarvis/jarvis_agent/stdio_redirect.py +295 -0
  24. jarvis/jarvis_agent/task_analyzer.py +212 -0
  25. jarvis/jarvis_agent/task_manager.py +154 -0
  26. jarvis/jarvis_agent/task_planner.py +496 -0
  27. jarvis/jarvis_agent/tool_executor.py +8 -4
  28. jarvis/jarvis_agent/tool_share_manager.py +139 -0
  29. jarvis/jarvis_agent/user_interaction.py +42 -0
  30. jarvis/jarvis_agent/utils.py +54 -0
  31. jarvis/jarvis_agent/web_bridge.py +189 -0
  32. jarvis/jarvis_agent/web_output_sink.py +53 -0
  33. jarvis/jarvis_agent/web_server.py +751 -0
  34. jarvis/jarvis_c2rust/__init__.py +26 -0
  35. jarvis/jarvis_c2rust/cli.py +613 -0
  36. jarvis/jarvis_c2rust/collector.py +258 -0
  37. jarvis/jarvis_c2rust/library_replacer.py +1122 -0
  38. jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
  39. jarvis/jarvis_c2rust/optimizer.py +960 -0
  40. jarvis/jarvis_c2rust/scanner.py +1681 -0
  41. jarvis/jarvis_c2rust/transpiler.py +2325 -0
  42. jarvis/jarvis_code_agent/build_validation_config.py +133 -0
  43. jarvis/jarvis_code_agent/code_agent.py +1605 -178
  44. jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
  45. jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
  46. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
  47. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
  48. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
  49. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
  50. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
  51. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
  52. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
  53. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
  54. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
  55. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
  56. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
  57. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
  58. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
  59. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
  60. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
  61. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
  62. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
  63. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
  64. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
  65. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
  66. jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
  67. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
  68. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
  69. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
  70. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
  71. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
  72. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
  73. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
  74. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
  75. jarvis/jarvis_code_agent/lint.py +275 -13
  76. jarvis/jarvis_code_agent/utils.py +142 -0
  77. jarvis/jarvis_code_analysis/checklists/loader.py +20 -6
  78. jarvis/jarvis_code_analysis/code_review.py +583 -548
  79. jarvis/jarvis_data/config_schema.json +339 -28
  80. jarvis/jarvis_git_squash/main.py +22 -13
  81. jarvis/jarvis_git_utils/git_commiter.py +171 -55
  82. jarvis/jarvis_mcp/sse_mcp_client.py +22 -15
  83. jarvis/jarvis_mcp/stdio_mcp_client.py +4 -4
  84. jarvis/jarvis_mcp/streamable_mcp_client.py +36 -16
  85. jarvis/jarvis_memory_organizer/memory_organizer.py +753 -0
  86. jarvis/jarvis_methodology/main.py +48 -63
  87. jarvis/jarvis_multi_agent/__init__.py +302 -43
  88. jarvis/jarvis_multi_agent/main.py +70 -24
  89. jarvis/jarvis_platform/ai8.py +40 -23
  90. jarvis/jarvis_platform/base.py +210 -49
  91. jarvis/jarvis_platform/human.py +11 -1
  92. jarvis/jarvis_platform/kimi.py +82 -76
  93. jarvis/jarvis_platform/openai.py +73 -1
  94. jarvis/jarvis_platform/registry.py +8 -15
  95. jarvis/jarvis_platform/tongyi.py +115 -101
  96. jarvis/jarvis_platform/yuanbao.py +89 -63
  97. jarvis/jarvis_platform_manager/main.py +194 -132
  98. jarvis/jarvis_platform_manager/service.py +122 -86
  99. jarvis/jarvis_rag/cli.py +156 -53
  100. jarvis/jarvis_rag/embedding_manager.py +155 -12
  101. jarvis/jarvis_rag/llm_interface.py +10 -13
  102. jarvis/jarvis_rag/query_rewriter.py +63 -12
  103. jarvis/jarvis_rag/rag_pipeline.py +222 -40
  104. jarvis/jarvis_rag/reranker.py +26 -3
  105. jarvis/jarvis_rag/retriever.py +270 -14
  106. jarvis/jarvis_sec/__init__.py +3605 -0
  107. jarvis/jarvis_sec/checkers/__init__.py +32 -0
  108. jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
  109. jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
  110. jarvis/jarvis_sec/cli.py +116 -0
  111. jarvis/jarvis_sec/report.py +257 -0
  112. jarvis/jarvis_sec/status.py +264 -0
  113. jarvis/jarvis_sec/types.py +20 -0
  114. jarvis/jarvis_sec/workflow.py +219 -0
  115. jarvis/jarvis_smart_shell/main.py +405 -137
  116. jarvis/jarvis_stats/__init__.py +13 -0
  117. jarvis/jarvis_stats/cli.py +387 -0
  118. jarvis/jarvis_stats/stats.py +711 -0
  119. jarvis/jarvis_stats/storage.py +612 -0
  120. jarvis/jarvis_stats/visualizer.py +282 -0
  121. jarvis/jarvis_tools/ask_user.py +1 -0
  122. jarvis/jarvis_tools/base.py +18 -2
  123. jarvis/jarvis_tools/clear_memory.py +239 -0
  124. jarvis/jarvis_tools/cli/main.py +220 -144
  125. jarvis/jarvis_tools/execute_script.py +52 -12
  126. jarvis/jarvis_tools/file_analyzer.py +17 -12
  127. jarvis/jarvis_tools/generate_new_tool.py +46 -24
  128. jarvis/jarvis_tools/read_code.py +277 -18
  129. jarvis/jarvis_tools/read_symbols.py +141 -0
  130. jarvis/jarvis_tools/read_webpage.py +86 -13
  131. jarvis/jarvis_tools/registry.py +294 -90
  132. jarvis/jarvis_tools/retrieve_memory.py +227 -0
  133. jarvis/jarvis_tools/save_memory.py +194 -0
  134. jarvis/jarvis_tools/search_web.py +62 -28
  135. jarvis/jarvis_tools/sub_agent.py +205 -0
  136. jarvis/jarvis_tools/sub_code_agent.py +217 -0
  137. jarvis/jarvis_tools/virtual_tty.py +330 -62
  138. jarvis/jarvis_utils/builtin_replace_map.py +4 -5
  139. jarvis/jarvis_utils/clipboard.py +90 -0
  140. jarvis/jarvis_utils/config.py +607 -50
  141. jarvis/jarvis_utils/embedding.py +3 -0
  142. jarvis/jarvis_utils/fzf.py +57 -0
  143. jarvis/jarvis_utils/git_utils.py +251 -29
  144. jarvis/jarvis_utils/globals.py +174 -17
  145. jarvis/jarvis_utils/http.py +58 -79
  146. jarvis/jarvis_utils/input.py +899 -153
  147. jarvis/jarvis_utils/methodology.py +210 -83
  148. jarvis/jarvis_utils/output.py +220 -137
  149. jarvis/jarvis_utils/utils.py +1906 -135
  150. jarvis_ai_assistant-0.7.0.dist-info/METADATA +465 -0
  151. jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
  152. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +8 -2
  153. jarvis/jarvis_git_details/main.py +0 -265
  154. jarvis/jarvis_platform/oyi.py +0 -357
  155. jarvis/jarvis_tools/edit_file.py +0 -255
  156. jarvis/jarvis_tools/rewrite_file.py +0 -195
  157. jarvis_ai_assistant-0.1.222.dist-info/METADATA +0 -767
  158. jarvis_ai_assistant-0.1.222.dist-info/RECORD +0 -110
  159. /jarvis/{jarvis_git_details → jarvis_memory_organizer}/__init__.py +0 -0
  160. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
  161. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
  162. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
@@ -13,7 +13,11 @@ class EditFileHandler(OutputHandler):
13
13
  ot("PATCH file=(?:'([^']+)'|\"([^\"]+)\"|([^>]+))") + r"\s*"
14
14
  r"(?:"
15
15
  + ot("DIFF")
16
- + r"\s*"
16
+ + r"\s*(?:"
17
+ # 可选的RANGE标签,限制替换行号范围
18
+ + r"(?:" + ot("RANGE") + r"(.*?)" + ct("RANGE") + r"\s*)?"
19
+ + r"(?:"
20
+ # 单点替换(SEARCH/REPLACE)
17
21
  + ot("SEARCH")
18
22
  + r"(.*?)"
19
23
  + ct("SEARCH")
@@ -21,15 +25,29 @@ class EditFileHandler(OutputHandler):
21
25
  + ot("REPLACE")
22
26
  + r"(.*?)"
23
27
  + ct("REPLACE")
28
+ + r"|"
29
+ # 区间替换(SEARCH_START/SEARCH_END/REPLACE)
30
+ + ot("SEARCH_START")
31
+ + r"(.*?)"
32
+ + ct("SEARCH_START")
33
+ + r"\s*"
34
+ + ot("SEARCH_END")
35
+ + r"(.*?)"
36
+ + ct("SEARCH_END")
24
37
  + r"\s*"
38
+ + ot("REPLACE")
39
+ + r"(.*?)"
40
+ + ct("REPLACE")
41
+ + r")"
42
+ + r")\s*"
25
43
  + ct("DIFF")
26
44
  + r"\s*)+"
27
- + ct("PATCH"),
28
- re.DOTALL,
45
+ + r"^" + ct("PATCH"),
46
+ re.DOTALL | re.MULTILINE,
29
47
  )
30
48
  self.diff_pattern = re.compile(
31
49
  ot("DIFF")
32
- + r"\s*"
50
+ + r"\s*(?:" + ot("RANGE") + r"(.*?)" + ct("RANGE") + r"\s*)?"
33
51
  + ot("SEARCH")
34
52
  + r"(.*?)"
35
53
  + ct("SEARCH")
@@ -41,6 +59,24 @@ class EditFileHandler(OutputHandler):
41
59
  + ct("DIFF"),
42
60
  re.DOTALL,
43
61
  )
62
+ self.diff_range_pattern = re.compile(
63
+ ot("DIFF")
64
+ + r"\s*(?:" + ot("RANGE") + r"(.*?)" + ct("RANGE") + r"\s*)?"
65
+ + ot("SEARCH_START")
66
+ + r"(.*?)"
67
+ + ct("SEARCH_START")
68
+ + r"\s*"
69
+ + ot("SEARCH_END")
70
+ + r"(.*?)"
71
+ + ct("SEARCH_END")
72
+ + r"\s*"
73
+ + ot("REPLACE")
74
+ + r"(.*?)"
75
+ + ct("REPLACE")
76
+ + r"\s*"
77
+ + ct("DIFF"),
78
+ re.DOTALL,
79
+ )
44
80
 
45
81
  def handle(self, response: str, agent: Any) -> Tuple[bool, str]:
46
82
  """处理文件编辑响应
@@ -54,17 +90,24 @@ class EditFileHandler(OutputHandler):
54
90
  """
55
91
  patches = self._parse_patches(response)
56
92
  if not patches:
93
+ # 当响应中存在 PATCH 标签但未能解析出合法的补丁内容时,提示格式错误并给出合法格式
94
+ has_patch_open = bool(re.search("<PATCH", response))
95
+ has_patch_close = bool(re.search(ct("PATCH"), response))
96
+ if has_patch_open and has_patch_close:
97
+ return False, f"PATCH格式错误。合法的格式如下:\n{self.prompt()}"
57
98
  return False, "未找到有效的文件编辑指令"
58
99
 
100
+ # 记录 PATCH 操作调用统计
101
+ from jarvis.jarvis_stats.stats import StatsManager
102
+
103
+ StatsManager.increment("patch", group="tool")
104
+
59
105
  results = []
60
106
 
61
107
  for file_path, diffs in patches.items():
62
108
  file_path = os.path.abspath(file_path)
63
- file_patches = [
64
- {"SEARCH": diff["SEARCH"], "REPLACE": diff["REPLACE"]} for diff in diffs
65
- ]
109
+ file_patches = diffs
66
110
 
67
- print(f"📝 正在处理文件 {file_path}...")
68
111
  success, result = self._fast_edit(file_path, file_patches)
69
112
 
70
113
  if success:
@@ -84,7 +127,11 @@ class EditFileHandler(OutputHandler):
84
127
  Returns:
85
128
  bool: 返回是否能处理该响应
86
129
  """
87
- return bool(self.patch_pattern.search(response))
130
+ # 只要检测到 PATCH 标签(包含起止标签),即认为可处理,
131
+ # 具体合法性由 handle() 内的解析与错误提示负责
132
+ has_patch_open = bool(re.search("<PATCH", response))
133
+ has_patch_close = bool(re.search(ct("PATCH"), response))
134
+ return has_patch_open and has_patch_close
88
135
 
89
136
  def prompt(self) -> str:
90
137
  """获取处理器的提示信息
@@ -92,17 +139,48 @@ class EditFileHandler(OutputHandler):
92
139
  Returns:
93
140
  str: 返回处理器的提示字符串
94
141
  """
95
- return f"""文件编辑指令格式:
96
- {ot("PATCH file=文件路径")}
97
- {ot("DIFF")}
142
+ from jarvis.jarvis_utils.config import get_patch_format
143
+
144
+ patch_format = get_patch_format()
145
+
146
+ search_prompt = f"""{ot("DIFF")}
147
+ {ot("RANGE")}起止行号(如: 10-50),可选{ct("RANGE")}
98
148
  {ot("SEARCH")}原始代码{ct("SEARCH")}
99
149
  {ot("REPLACE")}新代码{ct("REPLACE")}
100
- {ct("DIFF")}
150
+ {ct("DIFF")}"""
151
+
152
+ search_range_prompt = f"""{ot("DIFF")}
153
+ {ot("RANGE")}起止行号(如: 10-50),可选{ct("RANGE")}
154
+ {ot("SEARCH_START")}起始标记{ct("SEARCH_START")}
155
+ {ot("SEARCH_END")}结束标记{ct("SEARCH_END")}
156
+ {ot("REPLACE")}替换内容{ct("REPLACE")}
157
+ {ct("DIFF")}"""
158
+
159
+ if patch_format == "search":
160
+ formats = search_prompt
161
+ supported_formats = "仅支持单点替换(SEARCH/REPLACE)"
162
+ usage_recommendation = ""
163
+ elif patch_format == "search_range":
164
+ formats = search_range_prompt
165
+ supported_formats = "仅支持区间替换(SEARCH_START/SEARCH_END/REPLACE),可选RANGE限定行号范围"
166
+ usage_recommendation = ""
167
+ else: # all
168
+ formats = f"{search_prompt}\n或\n{search_range_prompt}"
169
+ supported_formats = "支持两种DIFF块:单点替换(SEARCH/REPLACE)与区间替换(SEARCH_START/SEARCH_END/REPLACE)"
170
+ usage_recommendation = "\n推荐:优先使用单点替换(SEARCH/REPLACE)模式,除非需要大段修改,否则不要使用区间替换(SEARCH_START/SEARCH_END/REPLACE)模式"
171
+
172
+ return f"""文件编辑指令格式:
173
+ {ot("PATCH file=文件路径")}
174
+ {formats}
101
175
  {ct("PATCH")}
102
176
 
103
- 可以返回多个PATCH块用于同时修改多个文件
104
- 每个PATCH块可以包含多个DIFF块,每个DIFF块包含一组搜索和替换内容。
105
- 搜索文本必须能在文件中唯一匹配,否则编辑将失败。"""
177
+ 注意:
178
+ - {ot("PATCH")} 和 {ct("PATCH")} 必须出现在行首,否则不生效(会被忽略)
179
+ - {supported_formats}{usage_recommendation}
180
+ - {ot("RANGE")}start-end{ct("RANGE")} 可用于单点替换(SEARCH/REPLACE)和区间替换(SEARCH_START/SEARCH_END)模式,表示只在指定行号范围内进行匹配与替换(1-based,闭区间);省略则在整个文件范围内处理
181
+ - 单点替换要求 SEARCH 在有效范围内唯一匹配(仅替换第一个匹配)
182
+ - 区间替换会从包含 {ot("SEARCH_START")} 的行首开始,到包含 {ot("SEARCH_END")} 的行尾结束,替换整个区域
183
+ 否则编辑将失败。"""
106
184
 
107
185
  def name(self) -> str:
108
186
  """获取处理器的名称
@@ -135,18 +213,77 @@ class EditFileHandler(OutputHandler):
135
213
  }
136
214
  """
137
215
  patches: Dict[str, List[Dict[str, str]]] = {}
216
+
138
217
  for match in self.patch_pattern.finditer(response):
139
218
  # Get the file path from the appropriate capture group
140
219
  file_path = match.group(1) or match.group(2) or match.group(3)
141
- diffs = []
142
- for diff_match in self.diff_pattern.finditer(match.group(0)):
143
- # 完全保留原始格式(包括所有空白和换行)
144
- diffs.append(
145
- {
146
- "SEARCH": diff_match.group(1), # 原始SEARCH内容
147
- "REPLACE": diff_match.group(2), # 原始REPLACE内容
220
+ diffs: List[Dict[str, str]] = []
221
+
222
+ # 逐块解析,保持 DIFF 顺序
223
+ diff_block_pattern = re.compile(ot("DIFF") + r"(.*?)" + ct("DIFF"), re.DOTALL)
224
+ for block_match in diff_block_pattern.finditer(match.group(0)):
225
+ block_text = block_match.group(1)
226
+
227
+ # 提取可选的行号范围
228
+ range_scope = None
229
+ range_scope_match = re.match(
230
+ r"^\s*" + ot("RANGE") + r"(.*?)" + ct("RANGE") + r"\s*",
231
+ block_text,
232
+ re.DOTALL,
233
+ )
234
+ if range_scope_match:
235
+ range_scope = range_scope_match.group(1).strip()
236
+ # 仅移除块首部的RANGE标签,避免误删内容中的同名标记
237
+ block_text = block_text[range_scope_match.end():]
238
+ # 统一按 all 解析:无视配置,始终尝试区间替换
239
+ range_match = re.search(
240
+ ot("SEARCH_START")
241
+ + r"(.*?)"
242
+ + ct("SEARCH_START")
243
+ + r"\s*"
244
+ + ot("SEARCH_END")
245
+ + r"(.*?)"
246
+ + ct("SEARCH_END")
247
+ + r"\s*"
248
+ + ot("REPLACE")
249
+ + r"(.*?)"
250
+ + ct("REPLACE"),
251
+ block_text,
252
+ re.DOTALL,
253
+ )
254
+ if range_match:
255
+ diff_item: Dict[str, str] = {
256
+ "SEARCH_START": range_match.group(1), # 原始SEARCH_START内容
257
+ "SEARCH_END": range_match.group(2), # 原始SEARCH_END内容
258
+ "REPLACE": range_match.group(3), # 原始REPLACE内容
148
259
  }
260
+ if range_scope:
261
+ diff_item["RANGE"] = range_scope
262
+ diffs.append(diff_item)
263
+ continue
264
+
265
+ # 解析单点替换(统一按 all 解析:无视配置,始终尝试单点替换)
266
+ single_match = re.search(
267
+ ot("SEARCH")
268
+ + r"(.*?)"
269
+ + ct("SEARCH")
270
+ + r"\s*"
271
+ + ot("REPLACE")
272
+ + r"(.*?)"
273
+ + ct("REPLACE"),
274
+ block_text,
275
+ re.DOTALL,
149
276
  )
277
+ if single_match:
278
+ diff_item = {
279
+ "SEARCH": single_match.group(1), # 原始SEARCH内容
280
+ "REPLACE": single_match.group(2), # 原始REPLACE内容
281
+ }
282
+ # SEARCH 模式支持 RANGE,如果存在则添加
283
+ if range_scope:
284
+ diff_item["RANGE"] = range_scope
285
+ diffs.append(diff_item)
286
+
150
287
  if diffs:
151
288
  if file_path in patches:
152
289
  patches[file_path].extend(diffs)
@@ -190,102 +327,258 @@ class EditFileHandler(OutputHandler):
190
327
  failed_patches: List[Dict[str, Any]] = []
191
328
  successful_patches = 0
192
329
 
193
- for patch in patches:
194
- patch_count += 1
195
- search_text = patch["SEARCH"]
196
- replace_text = patch["REPLACE"]
197
-
198
- # 精确匹配搜索文本(保留原始换行和空格)
199
- exact_search = search_text
330
+ # 当存在RANGE时,确保按行号从后往前应用补丁,避免前面补丁影响后续RANGE的行号
331
+ ordered_patches: List[Dict[str, str]] = []
332
+ range_items: List[Tuple[int, int, int, Dict[str, str]]] = []
333
+ non_range_items: List[Tuple[int, Dict[str, str]]] = []
334
+ for idx, p in enumerate(patches):
335
+ r = p.get("RANGE")
336
+ if r and str(r).strip():
337
+ m = re.match(r"\s*(\d+)\s*-\s*(\d+)\s*$", str(r))
338
+ if m:
339
+ start_line = int(m.group(1))
340
+ end_line = int(m.group(2))
341
+ range_items.append((start_line, end_line, idx, p))
342
+ else:
343
+ # RANGE格式无效的补丁保持原有顺序
344
+ non_range_items.append((idx, p))
345
+ else:
346
+ # 无RANGE的补丁保持原有顺序
347
+ non_range_items.append((idx, p))
348
+ # 先应用RANGE补丁:按start_line、end_line、原始索引逆序
349
+ range_items.sort(key=lambda x: (x[0], x[1], x[2]), reverse=True)
350
+ ordered_patches = [item[3] for item in range_items] + [item[1] for item in non_range_items]
351
+
352
+ patch_count = len(ordered_patches)
353
+ for patch in ordered_patches:
200
354
  found = False
201
355
 
202
- if exact_search in modified_content:
203
- # 直接执行替换(保留所有原始格式)
204
- modified_content = modified_content.replace(
205
- exact_search, replace_text
206
- )
207
- print(f" 补丁 #{patch_count} 应用成功")
208
- found = True
209
- else:
210
- # 如果匹配不到,并且search与replace块的首尾都是换行,尝试去掉第一个和最后一个换行
356
+ # 处理可选的RANGE范围:格式 "start-end"(1-based, 闭区间)
357
+ scoped = False
358
+ prefix = suffix = ""
359
+ base_content = modified_content
360
+ if "RANGE" in patch and str(patch["RANGE"]).strip():
361
+ m = re.match(r"\s*(\d+)\s*-\s*(\d+)\s*$", str(patch["RANGE"]))
362
+ if not m:
363
+ error_msg = "RANGE格式无效,应为 'start-end' 的行号范围(1-based, 闭区间)"
364
+ failed_patches.append({"patch": patch, "error": error_msg})
365
+ # 不进行本补丁其它处理
366
+ continue
367
+ start_line = int(m.group(1))
368
+ end_line = int(m.group(2))
369
+
370
+ # 拆分为三段
371
+ lines = modified_content.splitlines(keepends=True)
372
+ total_lines = len(lines)
211
373
  if (
212
- search_text.startswith("\n")
213
- and search_text.endswith("\n")
214
- and replace_text.startswith("\n")
215
- and replace_text.endswith("\n")
374
+ start_line < 1
375
+ or end_line < 1
376
+ or start_line > end_line
377
+ or start_line > total_lines
216
378
  ):
217
- stripped_search = search_text[1:-1]
218
- stripped_replace = replace_text[1:-1]
219
- if stripped_search in modified_content:
220
- modified_content = modified_content.replace(
221
- stripped_search, stripped_replace
222
- )
223
- print(f"✅ 补丁 #{patch_count} 应用成功 (自动去除首尾换行)")
224
- found = True
225
-
226
- if not found:
227
- # 尝试增加缩进重试
228
- current_search = search_text
229
- current_replace = replace_text
379
+ error_msg = f"RANGE行号无效(文件共有{total_lines}行)"
380
+ failed_patches.append({"patch": patch, "error": error_msg})
381
+ continue
382
+ # 截断end_line不超过总行数
383
+ end_line = min(end_line, total_lines)
384
+
385
+ prefix = "".join(lines[: start_line - 1])
386
+ base_content = "".join(lines[start_line - 1 : end_line])
387
+ suffix = "".join(lines[end_line:])
388
+ scoped = True
389
+
390
+ # 单点替换
391
+ if "SEARCH" in patch:
392
+ search_text = patch["SEARCH"]
393
+ replace_text = patch["REPLACE"]
394
+
395
+ # 精确匹配搜索文本(保留原始换行和空格)
396
+ exact_search = search_text
397
+
398
+ def _count_occurrences(haystack: str, needle: str) -> int:
399
+ if not needle:
400
+ return 0
401
+ return haystack.count(needle)
402
+
403
+ # 1) 精确匹配,要求唯一
404
+ cnt = _count_occurrences(base_content, exact_search)
405
+ if cnt == 1:
406
+ base_content = base_content.replace(exact_search, replace_text, 1)
407
+ found = True
408
+ elif cnt > 1:
409
+ # 提供更详细的错误信息,帮助调试
410
+ range_info = f"(RANGE: {patch.get('RANGE', '无')})" if "RANGE" in patch else ""
411
+ base_preview = base_content[:200] + "..." if len(base_content) > 200 else base_content
412
+ error_msg = (
413
+ f"SEARCH 在指定范围内出现多次,要求唯一匹配{range_info}。"
414
+ f"匹配次数: {cnt}。"
415
+ f"搜索内容: {repr(exact_search[:50])}。"
416
+ f"范围内容预览: {repr(base_preview)}"
417
+ )
418
+ failed_patches.append({"patch": patch, "error": error_msg})
419
+ # 不继续尝试其它变体
420
+ continue
421
+ else:
422
+ # 2) 若首尾均为换行,尝试去掉首尾换行后匹配,要求唯一
230
423
  if (
231
- current_search.startswith("\n")
232
- and current_search.endswith("\n")
233
- and current_replace.startswith("\n")
234
- and current_replace.endswith("\n")
424
+ search_text.startswith("\n")
425
+ and search_text.endswith("\n")
426
+ and replace_text.startswith("\n")
427
+ and replace_text.endswith("\n")
235
428
  ):
236
- current_search = current_search[1:-1]
237
- current_replace = current_replace[1:-1]
238
-
239
- for space_count in range(1, 17):
240
- indented_search = "\n".join(
241
- " " * space_count + line if line.strip() else line
242
- for line in current_search.split("\n")
243
- )
244
- indented_replace = "\n".join(
245
- " " * space_count + line if line.strip() else line
246
- for line in current_replace.split("\n")
247
- )
248
- if indented_search in modified_content:
249
- modified_content = modified_content.replace(
250
- indented_search, indented_replace
251
- )
252
- print(
253
- f"✅ 补丁 #{patch_count} 应用成功 (自动增加 {space_count} 个空格缩进)"
429
+ stripped_search = search_text[1:-1]
430
+ stripped_replace = replace_text[1:-1]
431
+ cnt2 = _count_occurrences(base_content, stripped_search)
432
+ if cnt2 == 1:
433
+ base_content = base_content.replace(
434
+ stripped_search, stripped_replace, 1
254
435
  )
255
436
  found = True
256
- break
437
+ elif cnt2 > 1:
438
+ error_msg = "SEARCH 在指定范围内出现多次(去掉首尾换行后),要求唯一匹配"
439
+ failed_patches.append({"patch": patch, "error": error_msg})
440
+ continue
441
+
442
+ # 3) 尝试缩进适配(1..16个空格),要求唯一
443
+ if not found:
444
+ current_search = search_text
445
+ current_replace = replace_text
446
+ if (
447
+ current_search.startswith("\n")
448
+ and current_search.endswith("\n")
449
+ and current_replace.startswith("\n")
450
+ and current_replace.endswith("\n")
451
+ ):
452
+ current_search = current_search[1:-1]
453
+ current_replace = current_replace[1:-1]
454
+
455
+ for space_count in range(1, 17):
456
+ indented_search = "\n".join(
457
+ " " * space_count + line if line.strip() else line
458
+ for line in current_search.split("\n")
459
+ )
460
+ indented_replace = "\n".join(
461
+ " " * space_count + line if line.strip() else line
462
+ for line in current_replace.split("\n")
463
+ )
464
+ cnt3 = _count_occurrences(base_content, indented_search)
465
+ if cnt3 == 1:
466
+ base_content = base_content.replace(
467
+ indented_search, indented_replace, 1
468
+ )
469
+ found = True
470
+ break
471
+ elif cnt3 > 1:
472
+ error_msg = "SEARCH 在指定范围内出现多次(缩进适配后),要求唯一匹配"
473
+ failed_patches.append({"patch": patch, "error": error_msg})
474
+ # 多匹配直接失败,不再继续尝试其它缩进
475
+ found = False
476
+ break
477
+
478
+ if not found:
479
+ # 未找到任何可用的唯一匹配
480
+ failed_patches.append({"patch": patch, "error": "未找到唯一匹配的SEARCH"})
481
+
482
+ # 区间替换
483
+ elif "SEARCH_START" in patch and "SEARCH_END" in patch:
484
+ search_start = patch["SEARCH_START"]
485
+ search_end = patch["SEARCH_END"]
486
+ replace_text = patch["REPLACE"]
487
+
488
+ # 范围替换(包含边界),命中第一个起始标记及其后的第一个结束标记
489
+ start_idx = base_content.find(search_start)
490
+ if start_idx == -1:
491
+ error_msg = "未找到SEARCH_START"
492
+ failed_patches.append({"patch": patch, "error": error_msg})
493
+ else:
494
+ # 从 search_start 之后开始查找 search_end
495
+ end_idx = base_content.find(search_end, start_idx + len(search_start))
496
+ if end_idx == -1:
497
+ error_msg = "在SEARCH_START之后未找到SEARCH_END"
498
+ failed_patches.append({"patch": patch, "error": error_msg})
499
+ else:
500
+ # 将替换范围扩展到整行
501
+ # 找到 start_idx 所在行的行首
502
+ line_start_idx = base_content.rfind("\n", 0, start_idx) + 1
503
+
504
+ # 找到 end_idx 所在行的行尾
505
+ match_end_pos = end_idx + len(search_end)
506
+ line_end_idx = base_content.find("\n", match_end_pos)
507
+
508
+ if line_end_idx == -1:
509
+ # 如果没有找到换行符,说明是最后一行
510
+ end_of_range = len(base_content)
511
+ else:
512
+ # 包含换行符
513
+ end_of_range = line_end_idx + 1
514
+
515
+ final_replace_text = replace_text
516
+ original_slice = base_content[line_start_idx:end_of_range]
517
+
518
+ # 如果原始片段以换行符结尾,且替换内容不为空且不以换行符结尾,
519
+ # 则为替换内容添加换行符以保持格式
520
+ if (
521
+ final_replace_text
522
+ and original_slice.endswith("\n")
523
+ and not final_replace_text.endswith("\n")
524
+ ):
525
+ final_replace_text += "\n"
526
+
527
+ base_content = (
528
+ base_content[:line_start_idx]
529
+ + final_replace_text
530
+ + base_content[end_of_range:]
531
+ )
532
+ found = True
257
533
 
258
- if found:
259
- successful_patches += 1
260
534
  else:
261
- error_msg = "搜索文本在文件中不存在"
262
- PrettyOutput.print(
263
- f"{error_msg}:\n{search_text}",
264
- output_type=OutputType.WARNING,
265
- )
535
+ error_msg = "不支持的补丁格式"
266
536
  failed_patches.append({"patch": patch, "error": error_msg})
267
537
 
538
+ # 若使用了RANGE,则将局部修改写回整体内容
539
+ if found:
540
+ if scoped:
541
+ modified_content = prefix + base_content + suffix
542
+ else:
543
+ modified_content = base_content
544
+ successful_patches += 1
545
+
268
546
  # 写入修改后的内容
269
547
  with open(file_path, "w", encoding="utf-8") as f:
270
548
  f.write(modified_content)
271
549
 
272
550
  if failed_patches:
273
- error_details = [
274
- f" - 失败的补丁: \n{p['patch']['SEARCH']}\n 错误: {p['error']}"
275
- for p in failed_patches
276
- ]
277
- summary = (
278
- f"文件 {file_path} 修改部分成功。\n"
279
- f"成功: {successful_patches}/{patch_count}, "
280
- f"失败: {len(failed_patches)}/{patch_count}.\n"
281
- f"失败详情:\n" + "\n".join(error_details)
282
- )
283
- print(f" {summary}")
551
+ error_details = []
552
+ for p in failed_patches:
553
+ patch = p["patch"]
554
+ if "SEARCH" in patch:
555
+ patch_desc = patch["SEARCH"]
556
+ else:
557
+ patch_desc = (
558
+ "SEARCH_START:\n"
559
+ + (patch.get("SEARCH_START", ""))
560
+ + "\nSEARCH_END:\n"
561
+ + (patch.get("SEARCH_END", ""))
562
+ )
563
+ error_details.append(f" - 失败的补丁: \n{patch_desc}\n 错误: {p['error']}")
564
+ if successful_patches == 0:
565
+ summary = (
566
+ f"文件 {file_path} 修改失败(全部失败)。\n"
567
+ f"失败: {len(failed_patches)}/{patch_count}.\n"
568
+ f"失败详情:\n" + "\n".join(error_details)
569
+ )
570
+ else:
571
+ summary = (
572
+ f"文件 {file_path} 修改部分成功。\n"
573
+ f"成功: {successful_patches}/{patch_count}, "
574
+ f"失败: {len(failed_patches)}/{patch_count}.\n"
575
+ f"失败详情:\n" + "\n".join(error_details)
576
+ )
577
+ PrettyOutput.print(summary, OutputType.ERROR)
284
578
  return False, summary
285
579
 
286
- print(f"✅ 文件 {file_path} 修改完成,应用了 {patch_count} 个补丁")
287
580
  return True, modified_content
288
581
 
289
582
  except Exception as e:
290
- print(f"文件修改失败: {str(e)}")
583
+ PrettyOutput.print(f"文件修改失败: {str(e)}", OutputType.ERROR)
291
584
  return False, f"文件修改失败: {str(e)}"
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 事件总线(EventBus)
4
+
5
+ 目标(阶段一,最小变更):
6
+ - 提供简单可靠的发布/订阅机制
7
+ - 回调异常隔离,避免影响主流程
8
+ - 不引入额外依赖,便于在 Agent 中渐进集成
9
+ """
10
+ from collections import defaultdict
11
+ from typing import Callable, DefaultDict, List, Any
12
+
13
+
14
+
15
+ class EventBus:
16
+ """
17
+ 简单的同步事件总线。
18
+ - subscribe(event, callback): 订阅事件
19
+ - emit(event, **kwargs): 广播事件
20
+ - unsubscribe(event, callback): 取消订阅
21
+ """
22
+
23
+ def __init__(self) -> None:
24
+ self._listeners: DefaultDict[str, List[Callable[..., None]]] = defaultdict(list)
25
+
26
+ def subscribe(self, event: str, callback: Callable[..., None]) -> None:
27
+ if not callable(callback):
28
+ raise TypeError("callback must be callable")
29
+ self._listeners[event].append(callback)
30
+
31
+ def unsubscribe(self, event: str, callback: Callable[..., None]) -> None:
32
+ if event not in self._listeners:
33
+ return
34
+ try:
35
+ self._listeners[event].remove(callback)
36
+ except ValueError:
37
+ pass
38
+
39
+ def emit(self, event: str, **payload: Any) -> None:
40
+ """
41
+ 广播事件。回调中的异常将被捕获并忽略,以保证主流程稳定。
42
+ """
43
+ for cb in list(self._listeners.get(event, [])):
44
+ try:
45
+ cb(**payload)
46
+ except Exception:
47
+ # 避免回调异常中断主流程
48
+ continue