jarvis-ai-assistant 0.3.30__py3-none-any.whl → 0.3.32__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 +348 -83
- jarvis/jarvis_agent/jarvis.py +83 -0
- jarvis/jarvis_code_agent/code_agent.py +88 -6
- jarvis/jarvis_data/config_schema.json +38 -4
- jarvis/jarvis_platform/openai.py +26 -1
- 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/input.py +1 -1
- jarvis/jarvis_utils/utils.py +63 -9
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.3.32.dist-info}/METADATA +1 -1
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.3.32.dist-info}/RECORD +21 -21
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.3.32.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.3.32.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.3.32.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.3.32.dist-info}/top_level.txt +0 -0
jarvis/jarvis_agent/jarvis.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
"""Jarvis AI 助手主入口模块"""
|
3
3
|
from typing import Optional, List
|
4
|
+
import shutil
|
5
|
+
from datetime import datetime
|
4
6
|
|
5
7
|
import typer
|
6
8
|
|
@@ -16,6 +18,7 @@ from jarvis.jarvis_utils.config import (
|
|
16
18
|
get_agent_definition_dirs,
|
17
19
|
get_multi_agent_dirs,
|
18
20
|
get_roles_dirs,
|
21
|
+
get_data_dir,
|
19
22
|
)
|
20
23
|
import jarvis.jarvis_utils.utils as jutils
|
21
24
|
from jarvis.jarvis_utils.input import user_confirm, get_single_line_input
|
@@ -184,6 +187,72 @@ def handle_interactive_config_option(
|
|
184
187
|
return True
|
185
188
|
|
186
189
|
|
190
|
+
def handle_backup_option(backup: bool) -> bool:
|
191
|
+
"""处理数据备份选项,返回是否已处理并需提前结束。"""
|
192
|
+
if not backup:
|
193
|
+
return False
|
194
|
+
|
195
|
+
init_env("", config_file=None)
|
196
|
+
data_dir = Path(get_data_dir())
|
197
|
+
if not data_dir.is_dir():
|
198
|
+
PrettyOutput.print(f"数据目录不存在: {data_dir}", OutputType.ERROR)
|
199
|
+
return True
|
200
|
+
|
201
|
+
backup_dir = Path(os.path.expanduser("~/jarvis_backups"))
|
202
|
+
backup_dir.mkdir(exist_ok=True)
|
203
|
+
|
204
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
205
|
+
backup_file_base = backup_dir / f"jarvis_data_{timestamp}"
|
206
|
+
|
207
|
+
try:
|
208
|
+
archive_path = shutil.make_archive(
|
209
|
+
str(backup_file_base), "zip", root_dir=str(data_dir)
|
210
|
+
)
|
211
|
+
PrettyOutput.print(f"数据已成功备份到: {archive_path}", OutputType.SUCCESS)
|
212
|
+
except Exception as e:
|
213
|
+
PrettyOutput.print(f"数据备份失败: {e}", OutputType.ERROR)
|
214
|
+
|
215
|
+
return True
|
216
|
+
|
217
|
+
|
218
|
+
def handle_restore_option(restore_path: Optional[str]) -> bool:
|
219
|
+
"""处理数据恢复选项,返回是否已处理并需提前结束。"""
|
220
|
+
if not restore_path:
|
221
|
+
return False
|
222
|
+
|
223
|
+
restore_file = Path(restore_path)
|
224
|
+
if not restore_file.is_file():
|
225
|
+
PrettyOutput.print(f"指定的恢复文件不存在: {restore_path}", OutputType.ERROR)
|
226
|
+
return True
|
227
|
+
|
228
|
+
init_env("", config_file=None)
|
229
|
+
data_dir = Path(get_data_dir())
|
230
|
+
|
231
|
+
if data_dir.exists():
|
232
|
+
if not user_confirm(
|
233
|
+
f"数据目录 '{data_dir}' 已存在,恢复操作将覆盖它。是否继续?", default=False
|
234
|
+
):
|
235
|
+
PrettyOutput.print("恢复操作已取消。", OutputType.INFO)
|
236
|
+
return True
|
237
|
+
try:
|
238
|
+
shutil.rmtree(data_dir)
|
239
|
+
except Exception as e:
|
240
|
+
PrettyOutput.print(f"无法移除现有数据目录: {e}", OutputType.ERROR)
|
241
|
+
return True
|
242
|
+
|
243
|
+
try:
|
244
|
+
data_dir.mkdir(parents=True)
|
245
|
+
shutil.unpack_archive(str(restore_file), str(data_dir), "zip")
|
246
|
+
PrettyOutput.print(
|
247
|
+
f"数据已从 '{restore_path}' 成功恢复到 '{data_dir}'", OutputType.SUCCESS
|
248
|
+
)
|
249
|
+
|
250
|
+
except Exception as e:
|
251
|
+
PrettyOutput.print(f"数据恢复失败: {e}", OutputType.ERROR)
|
252
|
+
|
253
|
+
return True
|
254
|
+
|
255
|
+
|
187
256
|
def preload_config_for_flags(config_file: Optional[str]) -> None:
|
188
257
|
"""预加载配置(仅用于读取功能开关),不会显示欢迎信息或影响后续 init_env。"""
|
189
258
|
try:
|
@@ -521,6 +590,12 @@ def run_cli(
|
|
521
590
|
"--disable-methodology-analysis",
|
522
591
|
help="禁用方法论和任务分析(覆盖配置文件设置)",
|
523
592
|
),
|
593
|
+
backup_data: bool = typer.Option(
|
594
|
+
False, "--backup-data", help="备份 Jarvis 数据目录 (~/.jarvis)"
|
595
|
+
),
|
596
|
+
restore_data: Optional[str] = typer.Option(
|
597
|
+
None, "--restore-data", help="从指定的压缩包恢复 Jarvis 数据"
|
598
|
+
),
|
524
599
|
) -> None:
|
525
600
|
"""Jarvis AI assistant command-line interface."""
|
526
601
|
if ctx.invoked_subcommand is not None:
|
@@ -529,6 +604,14 @@ def run_cli(
|
|
529
604
|
# 使用 rich 输出命令与快捷方式总览
|
530
605
|
print_commands_overview()
|
531
606
|
|
607
|
+
# 处理数据备份
|
608
|
+
if handle_backup_option(backup_data):
|
609
|
+
return
|
610
|
+
|
611
|
+
# 处理数据恢复
|
612
|
+
if handle_restore_option(restore_data):
|
613
|
+
return
|
614
|
+
|
532
615
|
# 处理配置文件编辑
|
533
616
|
if handle_edit_option(edit, config_file):
|
534
617
|
return
|
@@ -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_platform/openai.py
CHANGED
@@ -24,7 +24,32 @@ class OpenAIModel(BasePlatform):
|
|
24
24
|
self.base_url = os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1")
|
25
25
|
self.model_name = os.getenv("JARVIS_MODEL") or "gpt-4o"
|
26
26
|
|
27
|
-
|
27
|
+
# Optional: Inject extra HTTP headers via environment variable
|
28
|
+
# Expected format: OPENAI_EXTRA_HEADERS='{"Header-Name": "value", "X-Trace": "abc"}'
|
29
|
+
headers_str = os.getenv("OPENAI_EXTRA_HEADERS")
|
30
|
+
self.extra_headers: Dict[str, str] = {}
|
31
|
+
if headers_str:
|
32
|
+
try:
|
33
|
+
parsed = json.loads(headers_str)
|
34
|
+
if isinstance(parsed, dict):
|
35
|
+
# Ensure all header keys/values are strings
|
36
|
+
self.extra_headers = {str(k): str(v) for k, v in parsed.items()}
|
37
|
+
else:
|
38
|
+
PrettyOutput.print("OPENAI_EXTRA_HEADERS 应为 JSON 对象,如 {'X-Source':'jarvis'}", OutputType.WARNING)
|
39
|
+
except Exception as e:
|
40
|
+
PrettyOutput.print(f"解析 OPENAI_EXTRA_HEADERS 失败: {e}", OutputType.WARNING)
|
41
|
+
|
42
|
+
# Initialize OpenAI client, try to pass default headers if SDK supports it
|
43
|
+
try:
|
44
|
+
if self.extra_headers:
|
45
|
+
self.client = OpenAI(api_key=self.api_key, base_url=self.base_url, default_headers=self.extra_headers)
|
46
|
+
else:
|
47
|
+
self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)
|
48
|
+
except TypeError:
|
49
|
+
# Fallback: SDK version may not support default_headers
|
50
|
+
self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)
|
51
|
+
if self.extra_headers:
|
52
|
+
PrettyOutput.print("当前 OpenAI SDK 不支持 default_headers,未能注入额外 HTTP 头", OutputType.WARNING)
|
28
53
|
self.messages: List[Dict[str, str]] = []
|
29
54
|
self.system_message = ""
|
30
55
|
|
jarvis/jarvis_tools/edit_file.py
CHANGED
@@ -27,15 +27,25 @@ 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, 闭区间), 用于限定匹配范围
|
41
|
+
- **说明**: 区间替换会从包含 SEARCH_START 的行首开始,到包含 SEARCH_END 的行尾结束,替换整个区域
|
34
42
|
|
35
43
|
## 核心原则
|
36
44
|
1. **精准修改**: 只修改必要的代码部分,保持其他部分不变
|
37
45
|
2. **最小补丁原则**: 生成最小范围的补丁,包含必要的上下文
|
38
|
-
3. **唯一匹配**:
|
46
|
+
3. **唯一匹配**:
|
47
|
+
- 单点替换:确保 SEARCH 在文件中唯一匹配
|
48
|
+
- 区间替换:确保在指定范围内,第一个 SEARCH_START 之后能找到 SEARCH_END
|
39
49
|
4. **格式保持**: 严格保持原始代码的格式风格
|
40
50
|
5. **部分成功**: 支持多个文件编辑,允许部分文件编辑成功
|
41
51
|
|
@@ -62,12 +72,24 @@ class FileSearchReplaceTool:
|
|
62
72
|
},
|
63
73
|
"SEARCH": {
|
64
74
|
"type": "string",
|
65
|
-
"description": "
|
75
|
+
"description": "需要查找的原始代码(单点替换模式)",
|
76
|
+
},
|
77
|
+
"SEARCH_START": {
|
78
|
+
"type": "string",
|
79
|
+
"description": "区间替换的起始标记(包含在替换范围内)",
|
80
|
+
},
|
81
|
+
"SEARCH_END": {
|
82
|
+
"type": "string",
|
83
|
+
"description": "区间替换的结束标记(包含在替换范围内)",
|
66
84
|
},
|
67
85
|
"REPLACE": {
|
68
86
|
"type": "string",
|
69
87
|
"description": "替换后的新代码",
|
70
88
|
},
|
89
|
+
"RANGE": {
|
90
|
+
"type": "string",
|
91
|
+
"description": "行号范围 'start-end'(1-based,闭区间),可选,仅用于区间替换模式,用于限定匹配与替换的行号范围",
|
92
|
+
},
|
71
93
|
},
|
72
94
|
},
|
73
95
|
},
|
@@ -93,10 +115,17 @@ class FileSearchReplaceTool:
|
|
93
115
|
args: 包含以下键的字典:
|
94
116
|
- files: 文件列表,每个文件包含(必填):
|
95
117
|
- path: 要修改的文件路径
|
96
|
-
- changes:
|
97
|
-
|
98
|
-
|
99
|
-
|
118
|
+
- changes: 修改列表,每个修改支持两种格式:
|
119
|
+
1) 单点替换:
|
120
|
+
- reason: 修改原因描述
|
121
|
+
- SEARCH: 需要查找的原始代码(必须包含足够上下文)
|
122
|
+
- REPLACE: 替换后的新代码
|
123
|
+
2) 区间替换:
|
124
|
+
- reason: 修改原因描述
|
125
|
+
- SEARCH_START: 起始标记
|
126
|
+
- SEARCH_END: 结束标记
|
127
|
+
- REPLACE: 替换后的新代码
|
128
|
+
- **说明**: 区间替换会从包含 SEARCH_START 的行首开始,到包含 SEARCH_END 的行尾结束,替换整个区域
|
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", "--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/input.py
CHANGED
@@ -482,7 +482,7 @@ def _get_multiline_input_internal(
|
|
482
482
|
"""Handle Ctrl+O by exiting the prompt and returning the sentinel value."""
|
483
483
|
event.app.exit(result=CTRL_O_SENTINEL)
|
484
484
|
|
485
|
-
@bindings.add("c-t", filter=has_focus(DEFAULT_BUFFER))
|
485
|
+
@bindings.add("c-t", filter=has_focus(DEFAULT_BUFFER), eager=True)
|
486
486
|
def _(event):
|
487
487
|
"""Return a shell command like '!bash' for upper input_handler to execute."""
|
488
488
|
|