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 +1 -1
- jarvis/jarvis_agent/__init__.py +22 -2
- jarvis/jarvis_agent/edit_file_handler.py +330 -83
- jarvis/jarvis_code_agent/code_agent.py +88 -6
- jarvis/jarvis_data/config_schema.json +38 -4
- jarvis/jarvis_tools/edit_file.py +39 -10
- jarvis/jarvis_tools/read_code.py +12 -11
- jarvis/jarvis_tools/registry.py +16 -15
- jarvis/jarvis_utils/config.py +17 -0
- jarvis/jarvis_utils/embedding.py +3 -0
- jarvis/jarvis_utils/git_utils.py +14 -2
- jarvis/jarvis_utils/utils.py +61 -7
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.3.31.dist-info}/METADATA +1 -1
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.3.31.dist-info}/RECORD +18 -18
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.3.31.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.3.31.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.3.31.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.3.31.dist-info}/top_level.txt +0 -0
jarvis/__init__.py
CHANGED
jarvis/jarvis_agent/__init__.py
CHANGED
@@ -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
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
108
|
-
|
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
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
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
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
359
|
+
start_line < 1
|
360
|
+
or end_line < 1
|
361
|
+
or start_line > end_line
|
362
|
+
or start_line > total_lines
|
220
363
|
):
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
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
|
-
|
236
|
-
and
|
237
|
-
and
|
238
|
-
and
|
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
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
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
|
-
|
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
|
-
|
274
|
-
|
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
|
-
|
533
|
-
|
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
|
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"#
|
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":
|
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":
|
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",
|
jarvis/jarvis_tools/edit_file.py
CHANGED
@@ -27,15 +27,24 @@ class FileSearchReplaceTool:
|
|
27
27
|
|
28
28
|
## 基本使用
|
29
29
|
1. 指定需要修改的文件路径(单个或多个)
|
30
|
-
2.
|
31
|
-
-
|
32
|
-
|
33
|
-
|
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
|
-
|
98
|
-
|
99
|
-
|
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] 包含:
|
jarvis/jarvis_tools/read_code.py
CHANGED
@@ -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
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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 = (
|
jarvis/jarvis_tools/registry.py
CHANGED
@@ -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
|
-
|
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
|
-
|
613
|
-
|
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
|
-
|
637
|
-
|
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
|
-
#
|
643
|
-
|
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
|
-
|
651
|
+
pattern,
|
650
652
|
fixed_content,
|
651
|
-
re.DOTALL,
|
652
653
|
)
|
653
654
|
|
654
655
|
if temp_data:
|
jarvis/jarvis_utils/config.py
CHANGED
@@ -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数据存储目录路径。
|
jarvis/jarvis_utils/embedding.py
CHANGED
jarvis/jarvis_utils/git_utils.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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]]] = {}
|
jarvis/jarvis_utils/utils.py
CHANGED
@@ -679,7 +679,7 @@ def _show_usage_stats(welcome_str: str) -> None:
|
|
679
679
|
|
680
680
|
# 愿景 Panel
|
681
681
|
vision_text = Text(
|
682
|
-
"
|
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
|
-
"
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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,10 +1,10 @@
|
|
1
|
-
jarvis/__init__.py,sha256=
|
2
|
-
jarvis/jarvis_agent/__init__.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
121
|
-
jarvis/jarvis_utils/embedding.py,sha256=
|
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=
|
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=
|
132
|
-
jarvis_ai_assistant-0.3.
|
133
|
-
jarvis_ai_assistant-0.3.
|
134
|
-
jarvis_ai_assistant-0.3.
|
135
|
-
jarvis_ai_assistant-0.3.
|
136
|
-
jarvis_ai_assistant-0.3.
|
137
|
-
jarvis_ai_assistant-0.3.
|
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,,
|
File without changes
|
{jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.3.31.dist-info}/entry_points.txt
RENAMED
File without changes
|
{jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.3.31.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
File without changes
|