jarvis-ai-assistant 0.3.30__py3-none-any.whl → 0.3.31__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.
jarvis/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """Jarvis AI Assistant"""
3
3
 
4
- __version__ = "0.3.30"
4
+ __version__ = "0.3.31"
@@ -729,7 +729,14 @@ class Agent:
729
729
  pass
730
730
 
731
731
  response = self.model.chat_until_success(message) # type: ignore
732
-
732
+ # 防御: 模型可能返回空响应(None或空字符串),统一为空字符串并告警
733
+ if not response:
734
+ try:
735
+ PrettyOutput.print("模型返回空响应,已使用空字符串回退。", OutputType.WARNING)
736
+ except Exception:
737
+ pass
738
+ response = ""
739
+
733
740
  # 事件:模型调用后
734
741
  try:
735
742
  self.event_bus.emit(
@@ -761,7 +768,13 @@ class Agent:
761
768
  summary = self.model.chat_until_success(
762
769
  self.session.prompt + "\n" + SUMMARY_REQUEST_PROMPT
763
770
  ) # type: ignore
764
-
771
+ # 防御: 可能返回空响应(None或空字符串),统一为空字符串并告警
772
+ if not summary:
773
+ try:
774
+ PrettyOutput.print("总结模型返回空响应,已使用空字符串回退。", OutputType.WARNING)
775
+ except Exception:
776
+ pass
777
+ summary = ""
765
778
  return summary
766
779
  except Exception:
767
780
  PrettyOutput.print("总结对话历史失败", OutputType.ERROR)
@@ -898,6 +911,13 @@ class Agent:
898
911
  if not self.model:
899
912
  raise RuntimeError("Model not initialized")
900
913
  ret = self.model.chat_until_success(self.session.prompt) # type: ignore
914
+ # 防御: 总结阶段模型可能返回空响应(None或空字符串),统一为空字符串并告警
915
+ if not ret:
916
+ try:
917
+ PrettyOutput.print("总结阶段模型返回空响应,已使用空字符串回退。", OutputType.WARNING)
918
+ except Exception:
919
+ pass
920
+ ret = ""
901
921
  result = ret
902
922
 
903
923
  # 广播完成总结事件
@@ -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
  """处理文件编辑响应
@@ -65,9 +101,7 @@ class EditFileHandler(OutputHandler):
65
101
 
66
102
  for file_path, diffs in patches.items():
67
103
  file_path = os.path.abspath(file_path)
68
- file_patches = [
69
- {"SEARCH": diff["SEARCH"], "REPLACE": diff["REPLACE"]} for diff in diffs
70
- ]
104
+ file_patches = diffs
71
105
 
72
106
  success, result = self._fast_edit(file_path, file_patches)
73
107
 
@@ -96,17 +130,44 @@ class EditFileHandler(OutputHandler):
96
130
  Returns:
97
131
  str: 返回处理器的提示字符串
98
132
  """
99
- return f"""文件编辑指令格式:
100
- {ot("PATCH file=文件路径")}
101
- {ot("DIFF")}
133
+ from jarvis.jarvis_utils.config import get_patch_format
134
+
135
+ patch_format = get_patch_format()
136
+
137
+ search_prompt = f"""{ot("DIFF")}
102
138
  {ot("SEARCH")}原始代码{ct("SEARCH")}
103
139
  {ot("REPLACE")}新代码{ct("REPLACE")}
104
- {ct("DIFF")}
140
+ {ct("DIFF")}"""
141
+
142
+ search_range_prompt = f"""{ot("DIFF")}
143
+ {ot("RANGE")}起止行号(如: 10-50),可选{ct("RANGE")}
144
+ {ot("SEARCH_START")}起始标记{ct("SEARCH_START")}
145
+ {ot("SEARCH_END")}结束标记{ct("SEARCH_END")}
146
+ {ot("REPLACE")}替换内容{ct("REPLACE")}
147
+ {ct("DIFF")}"""
148
+
149
+ if patch_format == "search":
150
+ formats = search_prompt
151
+ supported_formats = "仅支持单点替换(SEARCH/REPLACE)"
152
+ elif patch_format == "search_range":
153
+ formats = search_range_prompt
154
+ supported_formats = "仅支持区间替换(SEARCH_START/SEARCH_END/REPLACE),可选RANGE限定行号范围"
155
+ else: # all
156
+ formats = f"{search_prompt}\n或\n{search_range_prompt}"
157
+ supported_formats = "支持两种DIFF块:单点替换(SEARCH/REPLACE)与区间替换(SEARCH_START/SEARCH_END/REPLACE)"
158
+
159
+ return f"""文件编辑指令格式:
160
+ {ot("PATCH file=文件路径")}
161
+ {formats}
105
162
  {ct("PATCH")}
106
163
 
107
- 可以返回多个PATCH块用于同时修改多个文件
108
- 每个PATCH块可以包含多个DIFF块,每个DIFF块包含一组搜索和替换内容。
109
- 搜索文本必须能在文件中唯一匹配,否则编辑将失败。"""
164
+ 注意:
165
+ - {ot("PATCH")} 和 {ct("PATCH")} 必须出现在行首,否则不生效(会被忽略)
166
+ - {supported_formats}
167
+ - {ot("RANGE")}start-end{ct("RANGE")} 仅用于区间替换模式(SEARCH_START/SEARCH_END),表示只在指定行号范围内进行匹配与替换(1-based,闭区间);省略则在整个文件范围内处理
168
+ - 单点替换要求 SEARCH 在有效范围内唯一匹配(仅替换第一个匹配)
169
+ - 区间替换命中有效范围内的第一个 {ot("SEARCH_START")} 及其后的第一个 {ot("SEARCH_END")}
170
+ 否则编辑将失败。"""
110
171
 
111
172
  def name(self) -> str:
112
173
  """获取处理器的名称
@@ -139,18 +200,75 @@ class EditFileHandler(OutputHandler):
139
200
  }
140
201
  """
141
202
  patches: Dict[str, List[Dict[str, str]]] = {}
203
+
142
204
  for match in self.patch_pattern.finditer(response):
143
205
  # Get the file path from the appropriate capture group
144
206
  file_path = match.group(1) or match.group(2) or match.group(3)
145
- diffs = []
146
- for diff_match in self.diff_pattern.finditer(match.group(0)):
147
- # 完全保留原始格式(包括所有空白和换行)
148
- diffs.append(
149
- {
150
- "SEARCH": diff_match.group(1), # 原始SEARCH内容
151
- "REPLACE": diff_match.group(2), # 原始REPLACE内容
207
+ diffs: List[Dict[str, str]] = []
208
+
209
+ # 逐块解析,保持 DIFF 顺序
210
+ diff_block_pattern = re.compile(ot("DIFF") + r"(.*?)" + ct("DIFF"), re.DOTALL)
211
+ for block_match in diff_block_pattern.finditer(match.group(0)):
212
+ block_text = block_match.group(1)
213
+
214
+ # 提取可选的行号范围
215
+ range_scope = None
216
+ range_scope_match = re.match(
217
+ r"^\s*" + ot("RANGE") + r"(.*?)" + ct("RANGE") + r"\s*",
218
+ block_text,
219
+ re.DOTALL,
220
+ )
221
+ if range_scope_match:
222
+ range_scope = range_scope_match.group(1).strip()
223
+ # 仅移除块首部的RANGE标签,避免误删内容中的同名标记
224
+ block_text = block_text[range_scope_match.end():]
225
+ # 统一按 all 解析:无视配置,始终尝试区间替换
226
+ range_match = re.search(
227
+ ot("SEARCH_START")
228
+ + r"(.*?)"
229
+ + ct("SEARCH_START")
230
+ + r"\s*"
231
+ + ot("SEARCH_END")
232
+ + r"(.*?)"
233
+ + ct("SEARCH_END")
234
+ + r"\s*"
235
+ + ot("REPLACE")
236
+ + r"(.*?)"
237
+ + ct("REPLACE"),
238
+ block_text,
239
+ re.DOTALL,
240
+ )
241
+ if range_match:
242
+ diff_item: Dict[str, str] = {
243
+ "SEARCH_START": range_match.group(1), # 原始SEARCH_START内容
244
+ "SEARCH_END": range_match.group(2), # 原始SEARCH_END内容
245
+ "REPLACE": range_match.group(3), # 原始REPLACE内容
152
246
  }
247
+ if range_scope:
248
+ diff_item["RANGE"] = range_scope
249
+ diffs.append(diff_item)
250
+ continue
251
+
252
+ # 解析单点替换(统一按 all 解析:无视配置,始终尝试单点替换)
253
+ single_match = re.search(
254
+ ot("SEARCH")
255
+ + r"(.*?)"
256
+ + ct("SEARCH")
257
+ + r"\s*"
258
+ + ot("REPLACE")
259
+ + r"(.*?)"
260
+ + ct("REPLACE"),
261
+ block_text,
262
+ re.DOTALL,
153
263
  )
264
+ if single_match:
265
+ diff_item = {
266
+ "SEARCH": single_match.group(1), # 原始SEARCH内容
267
+ "REPLACE": single_match.group(2), # 原始REPLACE内容
268
+ }
269
+ # SEARCH 模式不支持 RANGE,直接忽略
270
+ diffs.append(diff_item)
271
+
154
272
  if diffs:
155
273
  if file_path in patches:
156
274
  patches[file_path].extend(diffs)
@@ -194,85 +312,214 @@ class EditFileHandler(OutputHandler):
194
312
  failed_patches: List[Dict[str, Any]] = []
195
313
  successful_patches = 0
196
314
 
197
- for patch in patches:
198
- patch_count += 1
199
- search_text = patch["SEARCH"]
200
- replace_text = patch["REPLACE"]
201
-
202
- # 精确匹配搜索文本(保留原始换行和空格)
203
- exact_search = search_text
315
+ # 当存在RANGE时,确保按行号从后往前应用补丁,避免前面补丁影响后续RANGE的行号
316
+ ordered_patches: List[Dict[str, str]] = []
317
+ range_items: List[Tuple[int, int, int, Dict[str, str]]] = []
318
+ non_range_items: List[Tuple[int, Dict[str, str]]] = []
319
+ for idx, p in enumerate(patches):
320
+ r = p.get("RANGE")
321
+ if r and str(r).strip():
322
+ m = re.match(r"\s*(\d+)\s*-\s*(\d+)\s*$", str(r))
323
+ if m:
324
+ start_line = int(m.group(1))
325
+ end_line = int(m.group(2))
326
+ range_items.append((start_line, end_line, idx, p))
327
+ else:
328
+ # RANGE格式无效的补丁保持原有顺序
329
+ non_range_items.append((idx, p))
330
+ else:
331
+ # 无RANGE的补丁保持原有顺序
332
+ non_range_items.append((idx, p))
333
+ # 先应用RANGE补丁:按start_line、end_line、原始索引逆序
334
+ range_items.sort(key=lambda x: (x[0], x[1], x[2]), reverse=True)
335
+ ordered_patches = [item[3] for item in range_items] + [item[1] for item in non_range_items]
336
+
337
+ patch_count = len(ordered_patches)
338
+ for patch in ordered_patches:
204
339
  found = False
205
340
 
206
- if exact_search in modified_content:
207
- # 直接执行替换(保留所有原始格式),只替换第一个匹配
208
- modified_content = modified_content.replace(
209
- exact_search, replace_text, 1
210
- )
211
-
212
- found = True
213
- else:
214
- # 如果匹配不到,并且search与replace块的首尾都是换行,尝试去掉第一个和最后一个换行
341
+ # 处理可选的RANGE范围:格式 "start-end"(1-based, 闭区间)
342
+ scoped = False
343
+ prefix = suffix = ""
344
+ base_content = modified_content
345
+ if "RANGE" in patch and str(patch["RANGE"]).strip():
346
+ m = re.match(r"\s*(\d+)\s*-\s*(\d+)\s*$", str(patch["RANGE"]))
347
+ if not m:
348
+ error_msg = "RANGE格式无效,应为 'start-end' 的行号范围(1-based, 闭区间)"
349
+ failed_patches.append({"patch": patch, "error": error_msg})
350
+ # 不进行本补丁其它处理
351
+ continue
352
+ start_line = int(m.group(1))
353
+ end_line = int(m.group(2))
354
+
355
+ # 拆分为三段
356
+ lines = modified_content.splitlines(keepends=True)
357
+ total_lines = len(lines)
215
358
  if (
216
- search_text.startswith("\n")
217
- and search_text.endswith("\n")
218
- and replace_text.startswith("\n")
219
- and replace_text.endswith("\n")
359
+ start_line < 1
360
+ or end_line < 1
361
+ or start_line > end_line
362
+ or start_line > total_lines
220
363
  ):
221
- stripped_search = search_text[1:-1]
222
- stripped_replace = replace_text[1:-1]
223
- if stripped_search in modified_content:
224
- modified_content = modified_content.replace(
225
- stripped_search, stripped_replace, 1
226
- )
227
-
228
- found = True
229
-
230
- if not found:
231
- # 尝试增加缩进重试
232
- current_search = search_text
233
- current_replace = replace_text
364
+ error_msg = f"RANGE行号无效(文件共有{total_lines}行)"
365
+ failed_patches.append({"patch": patch, "error": error_msg})
366
+ continue
367
+ # 截断end_line不超过总行数
368
+ end_line = min(end_line, total_lines)
369
+
370
+ prefix = "".join(lines[: start_line - 1])
371
+ base_content = "".join(lines[start_line - 1 : end_line])
372
+ suffix = "".join(lines[end_line:])
373
+ scoped = True
374
+
375
+ # 单点替换
376
+ if "SEARCH" in patch:
377
+ search_text = patch["SEARCH"]
378
+ replace_text = patch["REPLACE"]
379
+
380
+ # 精确匹配搜索文本(保留原始换行和空格)
381
+ exact_search = search_text
382
+
383
+ def _count_occurrences(haystack: str, needle: str) -> int:
384
+ if not needle:
385
+ return 0
386
+ return haystack.count(needle)
387
+
388
+ # 1) 精确匹配,要求唯一
389
+ cnt = _count_occurrences(base_content, exact_search)
390
+ if cnt == 1:
391
+ base_content = base_content.replace(exact_search, replace_text, 1)
392
+ found = True
393
+ elif cnt > 1:
394
+ error_msg = "SEARCH 在指定范围内出现多次,要求唯一匹配"
395
+ failed_patches.append({"patch": patch, "error": error_msg})
396
+ # 不继续尝试其它变体
397
+ continue
398
+ else:
399
+ # 2) 若首尾均为换行,尝试去掉首尾换行后匹配,要求唯一
234
400
  if (
235
- current_search.startswith("\n")
236
- and current_search.endswith("\n")
237
- and current_replace.startswith("\n")
238
- and current_replace.endswith("\n")
401
+ search_text.startswith("\n")
402
+ and search_text.endswith("\n")
403
+ and replace_text.startswith("\n")
404
+ and replace_text.endswith("\n")
239
405
  ):
240
- current_search = current_search[1:-1]
241
- current_replace = current_replace[1:-1]
242
-
243
- for space_count in range(1, 17):
244
- indented_search = "\n".join(
245
- " " * space_count + line if line.strip() else line
246
- for line in current_search.split("\n")
247
- )
248
- indented_replace = "\n".join(
249
- " " * space_count + line if line.strip() else line
250
- for line in current_replace.split("\n")
251
- )
252
- if indented_search in modified_content:
253
- modified_content = modified_content.replace(
254
- indented_search, indented_replace, 1
406
+ stripped_search = search_text[1:-1]
407
+ stripped_replace = replace_text[1:-1]
408
+ cnt2 = _count_occurrences(base_content, stripped_search)
409
+ if cnt2 == 1:
410
+ base_content = base_content.replace(
411
+ stripped_search, stripped_replace, 1
255
412
  )
256
-
257
413
  found = True
258
- break
414
+ elif cnt2 > 1:
415
+ error_msg = "SEARCH 在指定范围内出现多次(去掉首尾换行后),要求唯一匹配"
416
+ failed_patches.append({"patch": patch, "error": error_msg})
417
+ continue
418
+
419
+ # 3) 尝试缩进适配(1..16个空格),要求唯一
420
+ if not found:
421
+ current_search = search_text
422
+ current_replace = replace_text
423
+ if (
424
+ current_search.startswith("\n")
425
+ and current_search.endswith("\n")
426
+ and current_replace.startswith("\n")
427
+ and current_replace.endswith("\n")
428
+ ):
429
+ current_search = current_search[1:-1]
430
+ current_replace = current_replace[1:-1]
431
+
432
+ for space_count in range(1, 17):
433
+ indented_search = "\n".join(
434
+ " " * space_count + line if line.strip() else line
435
+ for line in current_search.split("\n")
436
+ )
437
+ indented_replace = "\n".join(
438
+ " " * space_count + line if line.strip() else line
439
+ for line in current_replace.split("\n")
440
+ )
441
+ cnt3 = _count_occurrences(base_content, indented_search)
442
+ if cnt3 == 1:
443
+ base_content = base_content.replace(
444
+ indented_search, indented_replace, 1
445
+ )
446
+ found = True
447
+ break
448
+ elif cnt3 > 1:
449
+ error_msg = "SEARCH 在指定范围内出现多次(缩进适配后),要求唯一匹配"
450
+ failed_patches.append({"patch": patch, "error": error_msg})
451
+ # 多匹配直接失败,不再继续尝试其它缩进
452
+ found = False
453
+ break
454
+
455
+ if not found:
456
+ # 未找到任何可用的唯一匹配
457
+ failed_patches.append({"patch": patch, "error": "未找到唯一匹配的SEARCH"})
458
+
459
+ # 区间替换
460
+ elif "SEARCH_START" in patch and "SEARCH_END" in patch:
461
+ search_start = patch["SEARCH_START"]
462
+ search_end = patch["SEARCH_END"]
463
+ replace_text = patch["REPLACE"]
464
+
465
+ # 范围替换(包含边界),命中第一个起始标记及其后的第一个结束标记
466
+ start_idx = base_content.find(search_start)
467
+ if start_idx == -1:
468
+ error_msg = "未找到SEARCH_START"
469
+ failed_patches.append({"patch": patch, "error": error_msg})
470
+ else:
471
+ end_idx = base_content.find(search_end, start_idx)
472
+ if end_idx == -1:
473
+ error_msg = "在SEARCH_START之后未找到SEARCH_END"
474
+ failed_patches.append({"patch": patch, "error": error_msg})
475
+ else:
476
+ # 避免额外空行:
477
+ # 若 REPLACE 以换行结尾且 SEARCH_END 后紧跟换行符,
478
+ # 则将该换行并入替换范围,防止出现双重换行导致“多一行”
479
+ end_of_range = end_idx + len(search_end)
480
+ if (
481
+ end_of_range < len(base_content)
482
+ and base_content[end_of_range] == "\n"
483
+ and replace_text.endswith("\n")
484
+ ):
485
+ end_of_range += 1
486
+ base_content = (
487
+ base_content[:start_idx]
488
+ + replace_text
489
+ + base_content[end_of_range:]
490
+ )
491
+ found = True
259
492
 
260
- if found:
261
- successful_patches += 1
262
493
  else:
263
- error_msg = "搜索文本在文件中不存在"
264
-
494
+ error_msg = "不支持的补丁格式"
265
495
  failed_patches.append({"patch": patch, "error": error_msg})
266
496
 
497
+ # 若使用了RANGE,则将局部修改写回整体内容
498
+ if found:
499
+ if scoped:
500
+ modified_content = prefix + base_content + suffix
501
+ else:
502
+ modified_content = base_content
503
+ successful_patches += 1
504
+
267
505
  # 写入修改后的内容
268
506
  with open(file_path, "w", encoding="utf-8") as f:
269
507
  f.write(modified_content)
270
508
 
271
509
  if failed_patches:
272
- error_details = [
273
- f" - 失败的补丁: \n{p['patch']['SEARCH']}\n 错误: {p['error']}"
274
- for p in failed_patches
275
- ]
510
+ error_details = []
511
+ for p in failed_patches:
512
+ patch = p["patch"]
513
+ if "SEARCH" in patch:
514
+ patch_desc = patch["SEARCH"]
515
+ else:
516
+ patch_desc = (
517
+ "SEARCH_START:\n"
518
+ + (patch.get("SEARCH_START", ""))
519
+ + "\nSEARCH_END:\n"
520
+ + (patch.get("SEARCH_END", ""))
521
+ )
522
+ error_details.append(f" - 失败的补丁: \n{patch_desc}\n 错误: {p['error']}")
276
523
  if successful_patches == 0:
277
524
  summary = (
278
525
  f"文件 {file_path} 修改失败(全部失败)。\n"
@@ -529,8 +529,9 @@ class CodeAgent:
529
529
  if self.agent.force_save_memory:
530
530
  self.agent.memory_manager.prompt_memory_save()
531
531
  elif start_commit:
532
- os.system(f"git reset --hard {str(start_commit)}") # 确保转换为字符串
533
- PrettyOutput.print("已重置到初始提交", OutputType.INFO)
532
+ if user_confirm("是否要重置到初始提交?", True):
533
+ os.system(f"git reset --hard {str(start_commit)}") # 确保转换为字符串
534
+ PrettyOutput.print("已重置到初始提交", OutputType.INFO)
534
535
 
535
536
  def run(self, user_input: str, prefix: str = "", suffix: str = "") -> Optional[str]:
536
537
  """使用给定的用户输入运行代码代理。
@@ -603,10 +604,92 @@ class CodeAgent:
603
604
  """工具调用后回调函数。"""
604
605
  final_ret = ""
605
606
  diff = get_diff()
607
+
608
+ # 构造按文件的状态映射与差异文本,删除文件不展示diff,仅提示删除
609
+ def _build_name_status_map() -> dict:
610
+ status_map = {}
611
+ try:
612
+ head_exists = bool(get_latest_commit_hash())
613
+ # 临时 -N 以包含未跟踪文件的差异检测
614
+ subprocess.run(["git", "add", "-N", "."], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
615
+ cmd = ["git", "diff", "--name-status"] + (["HEAD"] if head_exists else [])
616
+ res = subprocess.run(
617
+ cmd,
618
+ capture_output=True,
619
+ text=True,
620
+ encoding="utf-8",
621
+ errors="replace",
622
+ check=False,
623
+ )
624
+ finally:
625
+ subprocess.run(["git", "reset"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
626
+
627
+ if res.returncode == 0 and res.stdout:
628
+ for line in res.stdout.splitlines():
629
+ if not line.strip():
630
+ continue
631
+ parts = line.split("\t")
632
+ if not parts:
633
+ continue
634
+ status = parts[0]
635
+ if status.startswith("R") or status.startswith("C"):
636
+ # 重命名/复制:使用新路径作为键
637
+ if len(parts) >= 3:
638
+ old_path, new_path = parts[1], parts[2]
639
+ status_map[new_path] = status
640
+ # 也记录旧路径,便于匹配 name-only 的结果
641
+ status_map[old_path] = status
642
+ elif len(parts) >= 2:
643
+ status_map[parts[-1]] = status
644
+ else:
645
+ if len(parts) >= 2:
646
+ status_map[parts[1]] = status
647
+ return status_map
648
+
649
+ def _get_file_diff(file_path: str) -> str:
650
+ """获取单文件的diff,包含新增文件内容;失败时返回空字符串"""
651
+ head_exists = bool(get_latest_commit_hash())
652
+ try:
653
+ # 为了让未跟踪文件也能展示diff,临时 -N 该文件
654
+ subprocess.run(["git", "add", "-N", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
655
+ cmd = ["git", "diff"] + (["HEAD"] if head_exists else []) + ["--", file_path]
656
+ res = subprocess.run(
657
+ cmd,
658
+ capture_output=True,
659
+ text=True,
660
+ encoding="utf-8",
661
+ errors="replace",
662
+ check=False,
663
+ )
664
+ if res.returncode == 0:
665
+ return res.stdout or ""
666
+ return ""
667
+ finally:
668
+ subprocess.run(["git", "reset", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
669
+
670
+ def _build_per_file_patch_preview(modified_files: List[str]) -> str:
671
+ status_map = _build_name_status_map()
672
+ lines: List[str] = []
673
+ for f in modified_files:
674
+ status = status_map.get(f, "")
675
+ # 删除文件:不展示diff,仅提示
676
+ if (status.startswith("D")) or (not os.path.exists(f)):
677
+ lines.append(f"- {f} 文件被删除")
678
+ continue
679
+ # 其它情况:展示该文件的diff
680
+ file_diff = _get_file_diff(f)
681
+ if file_diff.strip():
682
+ lines.append(f"文件: {f}\n```diff\n{file_diff}\n```")
683
+ else:
684
+ # 当无法获取到diff(例如重命名或特殊状态),避免空输出
685
+ lines.append(f"- {f} 变更已记录(无可展示的文本差异)")
686
+ return "\n".join(lines)
687
+
606
688
  if diff:
607
689
  start_hash = get_latest_commit_hash()
608
690
  PrettyOutput.print(diff, OutputType.CODE, lang="diff")
609
691
  modified_files = get_diff_file_list()
692
+ per_file_preview = _build_per_file_patch_preview(modified_files)
610
693
  commited = handle_commit_workflow()
611
694
  if commited:
612
695
  # 统计代码行数变化
@@ -630,15 +713,14 @@ class CodeAgent:
630
713
 
631
714
  StatsManager.increment("code_modifications", group="code_agent")
632
715
 
633
-
634
716
  # 获取提交信息
635
717
  end_hash = get_latest_commit_hash()
636
718
  commits = get_commits_between(start_hash, end_hash)
637
719
 
638
- # 添加提交信息到final_ret
720
+ # 添加提交信息到final_ret(按文件展示diff;删除文件仅提示)
639
721
  if commits:
640
722
  final_ret += (
641
- f"\n\n代码已修改完成\n补丁内容:\n```diff\n{diff}\n```\n"
723
+ f"\n\n代码已修改完成\n补丁内容(按文件):\n{per_file_preview}\n"
642
724
  )
643
725
  # 修改后的提示逻辑
644
726
  lint_tools_info = "\n".join(
@@ -665,7 +747,7 @@ class CodeAgent:
665
747
  final_ret += "\n\n修改没有生效\n"
666
748
  else:
667
749
  final_ret += "\n修改被拒绝\n"
668
- final_ret += f"# 补丁预览:\n```diff\n{diff}\n```"
750
+ final_ret += f"# 补丁预览(按文件):\n{per_file_preview}"
669
751
  else:
670
752
  return
671
753
  # 用户确认最终结果
@@ -7,6 +7,7 @@
7
7
  "JARVIS_MCP": {
8
8
  "type": "array",
9
9
  "description": "MCP工具配置列表",
10
+ "default": [],
10
11
  "items": {
11
12
  "type": "object",
12
13
  "oneOf": [
@@ -182,11 +183,22 @@
182
183
  "description": "执行工具前是否需要确认",
183
184
  "default": false
184
185
  },
186
+ "JARVIS_TOOL_FILTER_THRESHOLD": {
187
+ "type": "number",
188
+ "description": "AI工具筛选阈值:当可用工具数量超过此值时触发AI筛选",
189
+ "default": 30
190
+ },
185
191
  "JARVIS_CONFIRM_BEFORE_APPLY_PATCH": {
186
192
  "type": "boolean",
187
193
  "description": "应用补丁前是否需要确认",
188
194
  "default": false
189
195
  },
196
+ "JARVIS_PATCH_FORMAT": {
197
+ "type": "string",
198
+ "enum": ["all", "search", "search_range"],
199
+ "description": "补丁格式处理模式:all 同时支持 SEARCH 与 SEARCH_START/SEARCH_END;search 仅允许精确片段匹配;search_range 仅允许范围匹配。",
200
+ "default": "all"
201
+ },
190
202
  "JARVIS_DATA_PATH": {
191
203
  "type": "string",
192
204
  "description": "Jarvis数据存储目录路径",
@@ -195,7 +207,7 @@
195
207
  "JARVIS_PRETTY_OUTPUT": {
196
208
  "type": "boolean",
197
209
  "description": "是否启用美化输出",
198
- "default": false
210
+ "default": true
199
211
  },
200
212
  "JARVIS_USE_METHODOLOGY": {
201
213
  "type": "boolean",
@@ -270,6 +282,11 @@
270
282
  "description": "是否打印提示",
271
283
  "default": false
272
284
  },
285
+ "JARVIS_PRINT_ERROR_TRACEBACK": {
286
+ "type": "boolean",
287
+ "description": "是否在错误输出时打印回溯调用链",
288
+ "default": false
289
+ },
273
290
  "JARVIS_ENABLE_STATIC_ANALYSIS": {
274
291
  "type": "boolean",
275
292
  "description": "是否启用静态代码分析",
@@ -278,7 +295,7 @@
278
295
  "JARVIS_FORCE_SAVE_MEMORY": {
279
296
  "type": "boolean",
280
297
  "description": "是否强制保存记忆",
281
- "default": true
298
+ "default": false
282
299
  },
283
300
  "JARVIS_ENABLE_GIT_JCA_SWITCH": {
284
301
  "type": "boolean",
@@ -341,7 +358,23 @@
341
358
  "JARVIS_RAG_GROUPS": {
342
359
  "type": "array",
343
360
  "description": "预定义的RAG配置组",
344
- "default": [],
361
+ "default": [
362
+ {
363
+ "text": {
364
+ "embedding_model": "BAAI/bge-m3",
365
+ "rerank_model": "BAAI/bge-reranker-v2-m3",
366
+ "use_bm25": true,
367
+ "use_rerank": true
368
+ }
369
+ },
370
+ {
371
+ "code": {
372
+ "embedding_model": "Qodo/Qodo-Embed-1-1.5B",
373
+ "use_bm25": false,
374
+ "use_rerank": false
375
+ }
376
+ }
377
+ ],
345
378
  "items": {
346
379
  "type": "object",
347
380
  "additionalProperties": {
@@ -421,7 +454,8 @@
421
454
  "required": [
422
455
  "template"
423
456
  ]
424
- }
457
+ },
458
+ "default": {}
425
459
  },
426
460
  "OPENAI_API_KEY": {
427
461
  "type": "string",
@@ -27,15 +27,24 @@ class FileSearchReplaceTool:
27
27
 
28
28
  ## 基本使用
29
29
  1. 指定需要修改的文件路径(单个或多个)
30
- 2. 提供一组或多组修改,每个修改包含:
31
- - reason: 修改原因描述
32
- - SEARCH: 需要查找的原始代码(必须包含足够上下文)
33
- - REPLACE: 替换后的新代码
30
+ 2. 提供一组或多组修改,每个修改支持两种格式:
31
+ - 单点替换:
32
+ - reason: 修改原因描述
33
+ - SEARCH: 需要查找的原始代码(必须包含足够上下文)
34
+ - REPLACE: 替换后的新代码
35
+ - 区间替换:
36
+ - reason: 修改原因描述
37
+ - SEARCH_START: 起始标记(包含在替换范围内)
38
+ - SEARCH_END: 结束标记(包含在替换范围内)
39
+ - REPLACE: 替换后的新代码
40
+ - RANGE: 可选的行号范围 'start-end' (1-based, 闭区间), 用于限定匹配范围
34
41
 
35
42
  ## 核心原则
36
43
  1. **精准修改**: 只修改必要的代码部分,保持其他部分不变
37
44
  2. **最小补丁原则**: 生成最小范围的补丁,包含必要的上下文
38
- 3. **唯一匹配**: 确保搜索文本在文件中唯一匹配
45
+ 3. **唯一匹配**:
46
+ - 单点替换:确保 SEARCH 在文件中唯一匹配
47
+ - 区间替换:确保 SEARCH_START 在文件中唯一匹配,且在其后 SEARCH_END 也唯一匹配
39
48
  4. **格式保持**: 严格保持原始代码的格式风格
40
49
  5. **部分成功**: 支持多个文件编辑,允许部分文件编辑成功
41
50
 
@@ -62,12 +71,24 @@ class FileSearchReplaceTool:
62
71
  },
63
72
  "SEARCH": {
64
73
  "type": "string",
65
- "description": "需要查找的原始代码",
74
+ "description": "需要查找的原始代码(单点替换模式)",
75
+ },
76
+ "SEARCH_START": {
77
+ "type": "string",
78
+ "description": "区间替换的起始标记(包含在替换范围内)",
79
+ },
80
+ "SEARCH_END": {
81
+ "type": "string",
82
+ "description": "区间替换的结束标记(包含在替换范围内)",
66
83
  },
67
84
  "REPLACE": {
68
85
  "type": "string",
69
86
  "description": "替换后的新代码",
70
87
  },
88
+ "RANGE": {
89
+ "type": "string",
90
+ "description": "行号范围 'start-end'(1-based,闭区间),可选,仅用于区间替换模式,用于限定匹配与替换的行号范围",
91
+ },
71
92
  },
72
93
  },
73
94
  },
@@ -93,10 +114,18 @@ class FileSearchReplaceTool:
93
114
  args: 包含以下键的字典:
94
115
  - files: 文件列表,每个文件包含(必填):
95
116
  - path: 要修改的文件路径
96
- - changes: 修改列表,每个修改包含:
97
- - reason: 修改原因描述
98
- - SEARCH: 需要查找的原始代码(必须包含足够上下文)
99
- - REPLACE: 替换后的新代码
117
+ - changes: 修改列表,每个修改支持两种格式:
118
+ 1) 单点替换:
119
+ - reason: 修改原因描述
120
+ - SEARCH: 需要查找的原始代码(必须包含足够上下文)
121
+ - REPLACE: 替换后的新代码
122
+ 2) 区间替换:
123
+ - reason: 修改原因描述
124
+ - SEARCH_START: 起始标记(包含在替换范围内)
125
+ - SEARCH_END: 结束标记(包含在替换范围内)
126
+ - REPLACE: 替换后的新代码
127
+ 通用可选项:
128
+ - RANGE: 形如 'start-end'(1-based,闭区间),仅用于区间替换模式。当提供时仅在该行号范围内执行匹配与替换;省略则在整个文件范围内处理
100
129
 
101
130
  返回:
102
131
  Dict[str, Any] 包含:
@@ -62,10 +62,9 @@ class ReadCodeTool:
62
62
  }
63
63
 
64
64
  # 读取文件内容
65
+ # 第一遍流式读取,仅统计总行数,避免一次性读入内存
65
66
  with open(abs_path, "r", encoding="utf-8", errors="ignore") as f:
66
- lines = f.readlines()
67
-
68
- total_lines = len(lines)
67
+ total_lines = sum(1 for _ in f)
69
68
 
70
69
  # 处理空文件情况
71
70
  if total_lines == 0:
@@ -99,14 +98,16 @@ class ReadCodeTool:
99
98
  "stderr": f"无效的行范围 [{start_line}-{end_line}] (总行数: {total_lines})",
100
99
  }
101
100
 
102
- # 添加行号并构建输出内容
103
- selected_lines = lines[start_line - 1 : end_line]
104
- numbered_content = "".join(
105
- [
106
- f"{i:4d}:{line}"
107
- for i, line in enumerate(selected_lines, start=start_line)
108
- ]
109
- )
101
+ # 添加行号并构建输出内容(第二遍流式读取,仅提取范围行)
102
+ selected_items = []
103
+ with open(abs_path, "r", encoding="utf-8", errors="ignore") as f:
104
+ for i, line in enumerate(f, start=1):
105
+ if i < start_line:
106
+ continue
107
+ if i > end_line:
108
+ break
109
+ selected_items.append((i, line))
110
+ numbered_content = "".join(f"{i:4d}:{line}" for i, line in selected_items)
110
111
 
111
112
  # 构建输出格式
112
113
  output = (
@@ -51,6 +51,7 @@ arguments:
51
51
  - 完全按照上述格式
52
52
  - 使用正确的YAML格式,2个空格作为缩进
53
53
  - 包含所有必需参数
54
+ - {ot("TOOL_CALL")} 和 {ct("TOOL_CALL")} 必须出现在行首
54
55
  </rule>
55
56
 
56
57
  <rule>
@@ -101,6 +102,7 @@ arguments:
101
102
  - 创建虚构对话
102
103
  - 在没有所需信息的情况下继续
103
104
  - yaml 格式错误
105
+ - {ot("TOOL_CALL")} 和 {ct("TOOL_CALL")} 没有出现在行首
104
106
  </common_errors>
105
107
  </tool_system_guide>
106
108
  """
@@ -121,7 +123,8 @@ class ToolRegistry(OutputHandlerProtocol):
121
123
  return "TOOL_CALL"
122
124
 
123
125
  def can_handle(self, response: str) -> bool:
124
- return ot("TOOL_CALL") in response
126
+ # 仅当 {ot("TOOL_CALL")} 出现在行首时才认为可以处理
127
+ return re.search(rf'(?m){re.escape(ot("TOOL_CALL"))}', response) is not None
125
128
 
126
129
  def prompt(self) -> str:
127
130
  """加载工具"""
@@ -608,11 +611,9 @@ class ToolRegistry(OutputHandlerProtocol):
608
611
 
609
612
  @staticmethod
610
613
  def _has_tool_calls_block(content: str) -> bool:
611
- """从内容中提取工具调用块"""
612
- return (
613
- re.search(ot("TOOL_CALL") + r"(.*?)" + ct("TOOL_CALL"), content, re.DOTALL)
614
- is not None
615
- )
614
+ """从内容中提取工具调用块(仅匹配行首标签)"""
615
+ pattern = rf'(?ms){re.escape(ot("TOOL_CALL"))}(.*?)^{re.escape(ct("TOOL_CALL"))}'
616
+ return re.search(pattern, content) is not None
616
617
 
617
618
  @staticmethod
618
619
  def _extract_tool_calls(
@@ -633,22 +634,22 @@ class ToolRegistry(OutputHandlerProtocol):
633
634
  Exception: 如果工具调用缺少必要字段
634
635
  """
635
636
  # 将内容拆分为行
636
- data = re.findall(
637
- ot("TOOL_CALL") + r"(.*?)" + ct("TOOL_CALL"), content, re.DOTALL
638
- )
637
+ pattern = rf'(?ms){re.escape(ot("TOOL_CALL"))}(.*?)^{re.escape(ct("TOOL_CALL"))}'
638
+ data = re.findall(pattern, content)
639
639
  auto_completed = False
640
640
  if not data:
641
- # can_handle 确保 ot("TOOL_CALL") 在内容中。
642
- # 如果数据为空,则表示 ct("TOOL_CALL") 可能丢失。
643
- if ot("TOOL_CALL") in content and ct("TOOL_CALL") not in content:
644
- # 尝试通过附加结束标签来修复它
641
+ # can_handle 确保 ot("TOOL_CALL") 在内容中(行首)。
642
+ # 如果数据为空,则表示行首的 ct("TOOL_CALL") 可能丢失。
643
+ has_open_at_bol = re.search(rf'(?m){re.escape(ot("TOOL_CALL"))}', content) is not None
644
+ has_close_at_bol = re.search(rf'(?m)^{re.escape(ct("TOOL_CALL"))}', content) is not None
645
+ if has_open_at_bol and not has_close_at_bol:
646
+ # 尝试通过附加结束标签来修复它(确保结束标签位于行首)
645
647
  fixed_content = content.strip() + f"\n{ct('TOOL_CALL')}"
646
648
 
647
649
  # 再次提取,并检查YAML是否有效
648
650
  temp_data = re.findall(
649
- ot("TOOL_CALL") + r"(.*?)" + ct("TOOL_CALL"),
651
+ pattern,
650
652
  fixed_content,
651
- re.DOTALL,
652
653
  )
653
654
 
654
655
  if temp_data:
@@ -248,6 +248,23 @@ def is_confirm_before_apply_patch() -> bool:
248
248
  return GLOBAL_CONFIG_DATA.get("JARVIS_CONFIRM_BEFORE_APPLY_PATCH", False)
249
249
 
250
250
 
251
+ def get_patch_format() -> str:
252
+ """
253
+ 获取补丁格式。
254
+
255
+ - "search": 仅使用精确匹配的 `SEARCH` 模式。此模式对能力较弱的模型更稳定,因为它要求代码片段完全匹配。
256
+ - "search_range": 仅使用 `SEARCH_START` 和 `SEARCH_END` 的范围匹配模式。此模式对能力较强的模型更灵活,因为它允许在代码块内部进行修改,而不要求整个块完全匹配。
257
+ - "all": 同时支持以上两种模式(默认)。
258
+
259
+ 返回:
260
+ str: "all", "search", or "search_range"
261
+ """
262
+ mode = GLOBAL_CONFIG_DATA.get("JARVIS_PATCH_FORMAT", "all")
263
+ if mode in ["all", "search", "search_range"]:
264
+ return mode
265
+ return "all"
266
+
267
+
251
268
  def get_data_dir() -> str:
252
269
  """
253
270
  获取Jarvis数据存储目录路径。
@@ -21,6 +21,9 @@ def get_context_token_count(text: str) -> int:
21
21
  返回:
22
22
  int: 文本中的token数量
23
23
  """
24
+ # 防御性检查:入参为 None 或空字符串时直接返回 0
25
+ if text is None or text == "":
26
+ return 0
24
27
  try:
25
28
  import tiktoken
26
29
 
@@ -34,7 +34,13 @@ def find_git_root_and_cd(start_dir: str = ".") -> str:
34
34
  """
35
35
  os.chdir(start_dir)
36
36
  try:
37
- git_root = os.popen("git rev-parse --show-toplevel").read().strip()
37
+ result = subprocess.run(
38
+ ["git", "rev-parse", "--show-toplevel"],
39
+ capture_output=True,
40
+ text=True,
41
+ check=True,
42
+ )
43
+ git_root = result.stdout.strip()
38
44
  if not git_root:
39
45
  subprocess.run(["git", "init"], check=True)
40
46
  git_root = os.path.abspath(".")
@@ -291,7 +297,13 @@ def get_modified_line_ranges() -> Dict[str, List[Tuple[int, int]]]:
291
297
  行号从1开始。
292
298
  """
293
299
  # 获取所有文件的Git差异
294
- diff_output = os.popen("git show").read()
300
+ # 仅用于解析修改行范围,减少上下文以降低输出体积和解析成本
301
+ result = subprocess.run(
302
+ ["git", "show", "-U0", "--no-color"],
303
+ capture_output=True,
304
+ text=True,
305
+ )
306
+ diff_output = result.stdout
295
307
 
296
308
  # 解析差异以获取修改的文件及其行范围
297
309
  result: Dict[str, List[Tuple[int, int]]] = {}
@@ -679,7 +679,7 @@ def _show_usage_stats(welcome_str: str) -> None:
679
679
 
680
680
  # 愿景 Panel
681
681
  vision_text = Text(
682
- "重新定义开发者体验,打破人与工具的界限,构建开发者与AI之间真正的共生伙伴关系。",
682
+ "让开发者与AI成为共生伙伴",
683
683
  justify="center",
684
684
  style="italic",
685
685
  )
@@ -694,7 +694,7 @@ def _show_usage_stats(welcome_str: str) -> None:
694
694
 
695
695
  # 使命 Panel
696
696
  mission_text = Text(
697
- "通过深度人机协作,将开发者的灵感(Vibe)高效落地为代码与行动,释放创造之力。",
697
+ "让灵感高效落地为代码与行动",
698
698
  justify="center",
699
699
  style="italic",
700
700
  )
@@ -1142,11 +1142,17 @@ def _collect_optional_config_interactively(
1142
1142
  )
1143
1143
 
1144
1144
  # 新增的配置项交互(通用体验相关)
1145
+ # 根据平台统一默认值:Windows下为False,其它平台为True(与config.get_pretty_output一致)
1146
+ try:
1147
+ import platform as _platform_mod
1148
+ _default_pretty = False if _platform_mod.system() == "Windows" else True
1149
+ except Exception:
1150
+ _default_pretty = True
1145
1151
  changed = (
1146
1152
  _ask_and_set(
1147
1153
  "JARVIS_PRETTY_OUTPUT",
1148
1154
  "是否启用更美观的终端输出(Pretty Output)?",
1149
- False,
1155
+ _default_pretty,
1150
1156
  "bool",
1151
1157
  )
1152
1158
  or changed
@@ -1200,7 +1206,7 @@ def _collect_optional_config_interactively(
1200
1206
  _ask_and_set(
1201
1207
  "JARVIS_FORCE_SAVE_MEMORY",
1202
1208
  "是否强制保存会话记忆?",
1203
- True,
1209
+ False,
1204
1210
  "bool",
1205
1211
  )
1206
1212
  or changed
@@ -1330,6 +1336,38 @@ def _collect_optional_config_interactively(
1330
1336
 
1331
1337
 
1332
1338
 
1339
+ new_mode = get_choice(
1340
+ tip,
1341
+ choices,
1342
+ )
1343
+
1344
+ if new_mode == current_mode:
1345
+ return False
1346
+
1347
+ config_data[_key] = new_mode
1348
+ return True
1349
+ except Exception:
1350
+ return False
1351
+
1352
+ def _ask_patch_format_mode() -> bool:
1353
+ try:
1354
+ _key = "JARVIS_PATCH_FORMAT"
1355
+ if not ask_all and _key in config_data:
1356
+ return False
1357
+
1358
+ from jarvis.jarvis_utils.input import get_choice
1359
+ from jarvis.jarvis_utils.config import get_patch_format
1360
+
1361
+ current_mode = config_data.get(_key, get_patch_format())
1362
+ choices = ["all", "search", "search_range"]
1363
+ tip = (
1364
+ "请选择补丁格式处理模式 (JARVIS_PATCH_FORMAT):\n"
1365
+ "该设置影响 edit_file_handler 在处理补丁时允许的匹配方式。\n"
1366
+ " - all: 同时支持 SEARCH 与 SEARCH_START/SEARCH_END 两种模式(默认)。\n"
1367
+ " - search: 仅允许精确片段匹配(SEARCH)。更稳定,适合较弱模型或严格控制改动。\n"
1368
+ " - search_range: 仅允许范围匹配(SEARCH_START/SEARCH_END)。更灵活,适合较强模型和块内细粒度修改。"
1369
+ )
1370
+
1333
1371
  new_mode = get_choice(
1334
1372
  tip,
1335
1373
  choices,
@@ -1344,6 +1382,7 @@ def _collect_optional_config_interactively(
1344
1382
  return False
1345
1383
 
1346
1384
  changed = _ask_git_check_mode() or changed
1385
+ changed = _ask_patch_format_mode() or changed
1347
1386
 
1348
1387
  # Git 提交提示词(可选)
1349
1388
  changed = (
@@ -1730,9 +1769,22 @@ def get_file_md5(filepath: str) -> str:
1730
1769
  filepath: 要计算哈希的文件路径
1731
1770
 
1732
1771
  返回:
1733
- str: 文件内容的MD5哈希值
1772
+ str: 文件内容的MD5哈希值(为降低内存占用,仅读取前100MB进行计算)
1734
1773
  """
1735
- return hashlib.md5(open(filepath, "rb").read(100 * 1024 * 1024)).hexdigest()
1774
+ # 采用流式读取,避免一次性加载100MB到内存
1775
+ h = hashlib.md5()
1776
+ max_bytes = 100 * 1024 * 1024 # 与原实现保持一致:仅读取前100MB
1777
+ buf_size = 8 * 1024 * 1024 # 8MB缓冲
1778
+ read_bytes = 0
1779
+ with open(filepath, "rb") as f:
1780
+ while read_bytes < max_bytes:
1781
+ to_read = min(buf_size, max_bytes - read_bytes)
1782
+ chunk = f.read(to_read)
1783
+ if not chunk:
1784
+ break
1785
+ h.update(chunk)
1786
+ read_bytes += len(chunk)
1787
+ return h.hexdigest()
1736
1788
 
1737
1789
 
1738
1790
  def get_file_line_count(filename: str) -> int:
@@ -1745,7 +1797,9 @@ def get_file_line_count(filename: str) -> int:
1745
1797
  int: 文件中的行数,如果文件无法读取则返回0
1746
1798
  """
1747
1799
  try:
1748
- return len(open(filename, "r", encoding="utf-8", errors="ignore").readlines())
1800
+ # 使用流式逐行计数,避免将整个文件读入内存
1801
+ with open(filename, "r", encoding="utf-8", errors="ignore") as f:
1802
+ return sum(1 for _ in f)
1749
1803
  except Exception:
1750
1804
  return 0
1751
1805
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jarvis-ai-assistant
3
- Version: 0.3.30
3
+ Version: 0.3.31
4
4
  Summary: Jarvis: An AI assistant that uses tools to interact with the system
5
5
  Home-page: https://github.com/skyfireitdiy/Jarvis
6
6
  Author: skyfire
@@ -1,10 +1,10 @@
1
- jarvis/__init__.py,sha256=UpbmB-TeVTIG-j9qC27w2Pdl9nDtcZc6sO3wyLbGRJ8,74
2
- jarvis/jarvis_agent/__init__.py,sha256=BmIyS7P47Z9Ora2YfywMhvp1G2CYyU2WAzuOM1YgnuU,47962
1
+ jarvis/__init__.py,sha256=ty3_Sg4Xh7MccnSdYA1axmu51agflxzcbgAXl0e21-s,74
2
+ jarvis/jarvis_agent/__init__.py,sha256=Mp0lcivL99cL2eIjHz-D1C86XMOhJkFYtnQcJoflfH4,49033
3
3
  jarvis/jarvis_agent/agent_manager.py,sha256=qNcMy5Xc5ZT26JfczBg4b4D5udKVHSFsCFjlpbIdmPo,3076
4
4
  jarvis/jarvis_agent/builtin_input_handler.py,sha256=wS-FqpT3pIXwHn1dfL3SpXonUKWgVThbQueUIeyRc2U,2917
5
5
  jarvis/jarvis_agent/config.py,sha256=Ni1aTVzmdERJ89A1jsC21Tsys_9MM-TTx1w5XwxyEwA,3130
6
6
  jarvis/jarvis_agent/config_editor.py,sha256=hlb9EYxKWcR_qdW2O89CgNDdciR9Isi743JU_1gD8j4,1927
7
- jarvis/jarvis_agent/edit_file_handler.py,sha256=5sFz84jqy2gpc0aLOre2bvz8_DitlBoWZs_cQwftWLw,11570
7
+ jarvis/jarvis_agent/edit_file_handler.py,sha256=liPsL133PvgiTywesqVulfJdhaw6PIq4HXVCFstjFJg,23786
8
8
  jarvis/jarvis_agent/event_bus.py,sha256=pRdfk7d0OG18K6yNfWlCvAh_dW5p9sBtT2Yc3jGmzgo,1519
9
9
  jarvis/jarvis_agent/events.py,sha256=rmFQ37PasImCh7OCdCzNBvubk-kHwcUiYLgzmL0t0_4,3689
10
10
  jarvis/jarvis_agent/file_methodology_manager.py,sha256=LnhgTx5xQXCBK8esjCkFbgFm9iEyFX7TryUlC40Kzpw,4428
@@ -28,7 +28,7 @@ jarvis/jarvis_agent/tool_share_manager.py,sha256=Do08FRxis0ynwR2a6iRoa6Yq0qCP8Nk
28
28
  jarvis/jarvis_agent/user_interaction.py,sha256=tifFN49GkO_Q80sqOTVmhxwbNWTazF3K0cr8AnnvzdU,1453
29
29
  jarvis/jarvis_agent/utils.py,sha256=ldgfuNTNu4JU7Y1LtystBl85OC6H3A4OMycg0XBt_Cs,1615
30
30
  jarvis/jarvis_code_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
- jarvis/jarvis_code_agent/code_agent.py,sha256=0DjuaKSCz3vnS82EsjU9XB2Dc4si7Ryhbne9kL7IS14,32367
31
+ jarvis/jarvis_code_agent/code_agent.py,sha256=nCe0GP6mIzAjJBn2aFqVrDRSjMHnenkDrSoaM0VY87U,36576
32
32
  jarvis/jarvis_code_agent/lint.py,sha256=_qLJB_bC3PuoHG-j4EGOnYzNGO26jHlKLbkysfyQW1c,3954
33
33
  jarvis/jarvis_code_analysis/code_review.py,sha256=Z0JsvyVPPHPm6rfo4fqaQr7CdZKIllo9jqStzV0i_-o,34470
34
34
  jarvis/jarvis_code_analysis/checklists/__init__.py,sha256=LIXAYa1sW3l7foP6kohLWnE98I_EQ0T7z5bYKHq6rJA,78
@@ -51,7 +51,7 @@ jarvis/jarvis_code_analysis/checklists/shell.py,sha256=aRFYhQQvTgbYd-uY5pc8UHIUA
51
51
  jarvis/jarvis_code_analysis/checklists/sql.py,sha256=vR0T6qC7b4dURjJVAd7kSVxyvZEQXPG1Jqc2sNTGp5c,2355
52
52
  jarvis/jarvis_code_analysis/checklists/swift.py,sha256=TPx4I6Gupvs6tSerRKmTSKEPQpOLEbH2Y7LXg1uBgxc,2566
53
53
  jarvis/jarvis_code_analysis/checklists/web.py,sha256=25gGD7pDadZQybNFvALYxWvK0VRjGQb1NVJQElwjyk0,3943
54
- jarvis/jarvis_data/config_schema.json,sha256=GIzimcbNNedR3Fy_WwVdWAOgi-ODVerpWX83kYHF0NM,13068
54
+ jarvis/jarvis_data/config_schema.json,sha256=q5kXr067yPYcebEWyhqFBmfX1KgyjTAr_hCku__zSZ4,14166
55
55
  jarvis/jarvis_data/tiktoken/9b5ad71b2ce5302211f9c61530b329a4922fc6a4,sha256=Ijkht27pm96ZW3_3OFE-7xAPtR0YyTWXoRO8_-hlsqc,1681126
56
56
  jarvis/jarvis_git_squash/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
57
  jarvis/jarvis_git_squash/main.py,sha256=BRbsEQVXwseVFKliVqV8_JPh1om6QT6dLTHw0jQ7OE0,2474
@@ -97,14 +97,14 @@ jarvis/jarvis_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
97
97
  jarvis/jarvis_tools/ask_user.py,sha256=M6DdLNryCE8y1JcdZHEifUgZkPUEPNKc-zDW5p0Mb1k,2029
98
98
  jarvis/jarvis_tools/base.py,sha256=tFZkRlbV_a-pbjM-ci9AYmXVJm__FXuzVWKbQEyz4Ao,1639
99
99
  jarvis/jarvis_tools/clear_memory.py,sha256=8DOq6dHLemfKTJqu227PWBIp8Iu5K7EXwINzL8DYk8M,8205
100
- jarvis/jarvis_tools/edit_file.py,sha256=a6iyeH2MroejGCoaNDkXwGbSfqFikKbh6tG6Woljvi8,7105
100
+ jarvis/jarvis_tools/edit_file.py,sha256=HYUk31ehf2pKB7nY3nm_ahcqUv9bigaHSUc-f5e_0LQ,9129
101
101
  jarvis/jarvis_tools/execute_script.py,sha256=oDOMn8GcV6qKP4d0RFT6xbHGTazRmaOlp-h_e_Wj80c,6227
102
102
  jarvis/jarvis_tools/file_analyzer.py,sha256=jzVb8fAJn3dWwpCiYH-Wuxva4kpHqBB2_V3x3mzY0Gs,4158
103
103
  jarvis/jarvis_tools/generate_new_tool.py,sha256=tJz0YtfDwyH9y00VEWw3Btqr9JCNhvtI8BN9i5hYk_M,8560
104
104
  jarvis/jarvis_tools/methodology.py,sha256=_K4GIDUodGEma3SvNRo7Qs5rliijgNespVLyAPN35JU,5233
105
- jarvis/jarvis_tools/read_code.py,sha256=qeQZ_emyPI5RTFx4HSgLBtWSwh8V5chqMjxu2uKzmfY,6100
105
+ jarvis/jarvis_tools/read_code.py,sha256=F1RuO0c69t0h7CvrUGqrTyNcOCcUrFQPACc61O_YSso,6382
106
106
  jarvis/jarvis_tools/read_webpage.py,sha256=YTmoalY8y-jdQuoj9IL6ZjXPOevUj2P_9arJngPhbUY,5317
107
- jarvis/jarvis_tools/registry.py,sha256=iUzHEia3ufWqfLqlBRJl5KwKCU_0EvqEEiO136NHvRs,32846
107
+ jarvis/jarvis_tools/registry.py,sha256=yVXBrJ7plyn7Dr3dD6mPmgd6eiBftmd19Cc84-PwVN8,33312
108
108
  jarvis/jarvis_tools/retrieve_memory.py,sha256=hhhGSr7jebPHICY9oEKICyI8mfqsRtKjh58qZNZApKc,8624
109
109
  jarvis/jarvis_tools/rewrite_file.py,sha256=CuvjWPTbUaPbex9FKSmw_Ru4r6R-CX_3vqTqCTp8nHA,6959
110
110
  jarvis/jarvis_tools/save_memory.py,sha256=RQtNxcpU53FFv_EBjH0i0oyQ7jWubm-trD1BHuqaGjI,6985
@@ -117,21 +117,21 @@ jarvis/jarvis_tools/cli/main.py,sha256=WL2GNV7WqYl7G1-btRGvCkzDCMk4fPfNvzCrnUFVP
117
117
  jarvis/jarvis_utils/__init__.py,sha256=67h0ldisGlh3oK4DAeNEL2Bl_VsI3tSmfclasyVlueM,850
118
118
  jarvis/jarvis_utils/builtin_replace_map.py,sha256=z8iAqsbZUiGFaozxG1xSu128op8udqHOeEw-GxNt4bU,1708
119
119
  jarvis/jarvis_utils/clipboard.py,sha256=D3wzQeqg_yiH7Axs4d6MRxyNa9XxdnenH-ND2uj2WVQ,2967
120
- jarvis/jarvis_utils/config.py,sha256=YpIwuRxLhChe0XYoh7nVK0peEILo1NCBvAnxzouHRag,20407
121
- jarvis/jarvis_utils/embedding.py,sha256=oEOEM2qf16DMYwPsQe6srET9BknyjOdY2ef0jsp3Or8,2714
120
+ jarvis/jarvis_utils/config.py,sha256=AbDfL6hBpD6G_cRlr_bOAXECsj_vIq9tHXgkbsBIv5E,21119
121
+ jarvis/jarvis_utils/embedding.py,sha256=x6mrkL7Bc3qgfuBDsjc4fg4nKG8ofGxVLVVydbsb8PY,2838
122
122
  jarvis/jarvis_utils/file_processors.py,sha256=XiM248SHS7lLgQDCbORVFWqinbVDUawYxWDOsLXDxP8,3043
123
123
  jarvis/jarvis_utils/fzf.py,sha256=vCs0Uh5dUqGbWzXn2JCtLLCOYE2B39ZNdNveR9PK4DA,1681
124
- jarvis/jarvis_utils/git_utils.py,sha256=vclHYC2LzyyyHQCMaYENGUwfr7NoajpG8aJ_qr3LgJo,23731
124
+ jarvis/jarvis_utils/git_utils.py,sha256=Wz0oTOsqdQ0JWl69pj02PTI0jrdC6VOizd1uw0QXw_U,24078
125
125
  jarvis/jarvis_utils/globals.py,sha256=7Xvf9HY6jYJL4vSD1F1WCoxHkHCAyltJUYt4V9gGVU4,8865
126
126
  jarvis/jarvis_utils/http.py,sha256=eRhV3-GYuWmQ0ogq9di9WMlQkFcVb1zGCrySnOgT1x0,4392
127
127
  jarvis/jarvis_utils/input.py,sha256=EBdjPopkxVpG4JsnP9gtTSJ10u_scagujyKyOMhatLQ,36524
128
128
  jarvis/jarvis_utils/methodology.py,sha256=z_renvRGgHiC-XTQPuN6rvrJ_ffHlwxK_b1BU_jmNAQ,12800
129
129
  jarvis/jarvis_utils/output.py,sha256=y2fVcao_2ZowFl0IxUrJZCi8T6ZM0z-iPzpk8T8eLxc,13623
130
130
  jarvis/jarvis_utils/tag.py,sha256=f211opbbbTcSyzCDwuIK_oCnKhXPNK-RknYyGzY1yD0,431
131
- jarvis/jarvis_utils/utils.py,sha256=U3B_uz0-iZa4412krx37AsB6PXBJAvVyxOSlsLjPbRM,70590
132
- jarvis_ai_assistant-0.3.30.dist-info/licenses/LICENSE,sha256=AGgVgQmTqFvaztRtCAXsAMryUymB18gZif7_l2e1XOg,1063
133
- jarvis_ai_assistant-0.3.30.dist-info/METADATA,sha256=VnXgGix74c52WCbc-y1dLDDxayj4RK2zCZBVokpw4PQ,18752
134
- jarvis_ai_assistant-0.3.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
135
- jarvis_ai_assistant-0.3.30.dist-info/entry_points.txt,sha256=4GcWKFxRJD-QU14gw_3ZaW4KuEVxOcZK9i270rwPdjA,1395
136
- jarvis_ai_assistant-0.3.30.dist-info/top_level.txt,sha256=1BOxyWfzOP_ZXj8rVTDnNCJ92bBGB0rwq8N1PCpoMIs,7
137
- jarvis_ai_assistant-0.3.30.dist-info/RECORD,,
131
+ jarvis/jarvis_utils/utils.py,sha256=1rfnpFXeCp-6dL9_4eNOXnFvxZduTCoMWBdfDx7AMrE,72718
132
+ jarvis_ai_assistant-0.3.31.dist-info/licenses/LICENSE,sha256=AGgVgQmTqFvaztRtCAXsAMryUymB18gZif7_l2e1XOg,1063
133
+ jarvis_ai_assistant-0.3.31.dist-info/METADATA,sha256=JdHbYQDNFNAhhuk5lxOvFXNRvVRDx8y7nA6_pzMSVbM,18752
134
+ jarvis_ai_assistant-0.3.31.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
135
+ jarvis_ai_assistant-0.3.31.dist-info/entry_points.txt,sha256=4GcWKFxRJD-QU14gw_3ZaW4KuEVxOcZK9i270rwPdjA,1395
136
+ jarvis_ai_assistant-0.3.31.dist-info/top_level.txt,sha256=1BOxyWfzOP_ZXj8rVTDnNCJ92bBGB0rwq8N1PCpoMIs,7
137
+ jarvis_ai_assistant-0.3.31.dist-info/RECORD,,