jarvis-ai-assistant 0.3.29__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.
@@ -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",
@@ -247,6 +259,14 @@
247
259
  },
248
260
  "default": []
249
261
  },
262
+ "JARVIS_AFTER_TOOL_CALL_CB_DIRS": {
263
+ "type": "array",
264
+ "description": "工具调用后回调函数实现目录",
265
+ "items": {
266
+ "type": "string"
267
+ },
268
+ "default": []
269
+ },
250
270
  "JARVIS_CENTRAL_METHODOLOGY_REPO": {
251
271
  "type": "string",
252
272
  "description": "中心方法论Git仓库地址,该仓库会自动添加到方法论加载路径中",
@@ -262,6 +282,11 @@
262
282
  "description": "是否打印提示",
263
283
  "default": false
264
284
  },
285
+ "JARVIS_PRINT_ERROR_TRACEBACK": {
286
+ "type": "boolean",
287
+ "description": "是否在错误输出时打印回溯调用链",
288
+ "default": false
289
+ },
265
290
  "JARVIS_ENABLE_STATIC_ANALYSIS": {
266
291
  "type": "boolean",
267
292
  "description": "是否启用静态代码分析",
@@ -270,7 +295,7 @@
270
295
  "JARVIS_FORCE_SAVE_MEMORY": {
271
296
  "type": "boolean",
272
297
  "description": "是否强制保存记忆",
273
- "default": true
298
+ "default": false
274
299
  },
275
300
  "JARVIS_ENABLE_GIT_JCA_SWITCH": {
276
301
  "type": "boolean",
@@ -333,7 +358,23 @@
333
358
  "JARVIS_RAG_GROUPS": {
334
359
  "type": "array",
335
360
  "description": "预定义的RAG配置组",
336
- "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
+ ],
337
378
  "items": {
338
379
  "type": "object",
339
380
  "additionalProperties": {
@@ -413,7 +454,8 @@
413
454
  "required": [
414
455
  "template"
415
456
  ]
416
- }
457
+ },
458
+ "default": {}
417
459
  },
418
460
  "OPENAI_API_KEY": {
419
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] 包含:
@@ -173,9 +173,20 @@ class generate_new_tool:
173
173
  __import__(pkg)
174
174
  except ImportError:
175
175
 
176
- import subprocess
176
+ import subprocess, sys, os
177
+ from shutil import which as _which
178
+ # 优先使用 uv 安装(先查 venv 内 uv,再查 PATH 中 uv),否则回退到 python -m pip
179
+ if sys.platform == "win32":
180
+ venv_uv = os.path.join(sys.prefix, "Scripts", "uv.exe")
181
+ else:
182
+ venv_uv = os.path.join(sys.prefix, "bin", "uv")
183
+ uv_executable = venv_uv if os.path.exists(venv_uv) else (_which("uv") or None)
184
+ if uv_executable:
185
+ install_cmd = [uv_executable, "pip", "install", pkg]
186
+ else:
187
+ install_cmd = [sys.executable, "-m", "pip", "install", pkg]
188
+ subprocess.run(install_cmd, check=True)
177
189
 
178
- subprocess.run(["pip", "install", pkg], check=True)
179
190
 
180
191
  except Exception as e:
181
192
  PrettyOutput.print(f"依赖检查/安装失败: {str(e)}", OutputType.WARNING)
@@ -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数据存储目录路径。
@@ -378,6 +395,20 @@ def get_roles_dirs() -> List[str]:
378
395
  ]
379
396
 
380
397
 
398
+ def get_after_tool_call_cb_dirs() -> List[str]:
399
+ """
400
+ 获取工具调用后回调函数实现目录。
401
+
402
+ 返回:
403
+ List[str]: 工具调用后回调函数实现目录列表
404
+ """
405
+ return [
406
+ os.path.expanduser(os.path.expandvars(str(p)))
407
+ for p in GLOBAL_CONFIG_DATA.get("JARVIS_AFTER_TOOL_CALL_CB_DIRS", [])
408
+ if p
409
+ ]
410
+
411
+
381
412
  def get_central_methodology_repo() -> str:
382
413
  """
383
414
  获取中心方法论Git仓库地址。
@@ -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]]] = {}
@@ -417,25 +429,32 @@ def check_and_update_git_repo(repo_path: str) -> bool:
417
429
  hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
418
430
  )
419
431
 
420
- is_uv_env = False
421
- if in_venv:
422
- # 检查是否在uv创建的虚拟环境内
423
- if sys.platform == "win32":
424
- uv_path = os.path.join(sys.prefix, "Scripts", "uv.exe")
425
- else:
426
- uv_path = os.path.join(sys.prefix, "bin", "uv")
427
- if os.path.exists(uv_path):
428
- is_uv_env = True
432
+ # 检测 uv 可用性:优先虚拟环境内的 uv,其次 PATH 中的 uv
433
+ from shutil import which as _which
434
+ uv_executable = None
435
+ if sys.platform == "win32":
436
+ venv_uv = os.path.join(sys.prefix, "Scripts", "uv.exe")
437
+ else:
438
+ venv_uv = os.path.join(sys.prefix, "bin", "uv")
439
+ if os.path.exists(venv_uv):
440
+ uv_executable = venv_uv
441
+ else:
442
+ path_uv = _which("uv")
443
+ if path_uv:
444
+ uv_executable = path_uv
429
445
 
430
446
  # 根据环境选择安装命令
431
447
  # 检测是否安装了 RAG 特性(更精确)
432
448
  rag_installed = is_rag_installed()
433
449
 
434
- # 根据环境和 RAG 特性选择安装命令
435
- if rag_installed:
436
- if is_uv_env:
437
- install_cmd = ["uv", "pip", "install", "-e", ".[rag]"]
450
+ # 根据 uv 可用性与 RAG 特性选择安装命令(优先使用 uv)
451
+ if uv_executable:
452
+ if rag_installed:
453
+ install_cmd = [uv_executable, "pip", "install", "-e", ".[rag]"]
438
454
  else:
455
+ install_cmd = [uv_executable, "pip", "install", "-e", "."]
456
+ else:
457
+ if rag_installed:
439
458
  install_cmd = [
440
459
  sys.executable,
441
460
  "-m",
@@ -444,9 +463,6 @@ def check_and_update_git_repo(repo_path: str) -> bool:
444
463
  "-e",
445
464
  ".[rag]",
446
465
  ]
447
- else:
448
- if is_uv_env:
449
- install_cmd = ["uv", "pip", "install", "-e", "."]
450
466
  else:
451
467
  install_cmd = [
452
468
  sys.executable,
@@ -252,17 +252,19 @@ def _check_pip_updates() -> bool:
252
252
  hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
253
253
  )
254
254
 
255
- # 检测是否使用uv
256
- is_uv_env = False
255
+ # 检测是否可用 uv(优先使用虚拟环境内的uv,其次PATH中的uv)
256
+ from shutil import which as _which
257
257
  uv_executable: Optional[str] = None
258
- if in_venv:
259
- if sys.platform == "win32":
260
- uv_path = Path(sys.prefix) / "Scripts" / "uv.exe"
261
- else:
262
- uv_path = Path(sys.prefix) / "bin" / "uv"
263
- if uv_path.exists():
264
- is_uv_env = True
265
- uv_executable = str(uv_path)
258
+ if sys.platform == "win32":
259
+ venv_uv = Path(sys.prefix) / "Scripts" / "uv.exe"
260
+ else:
261
+ venv_uv = Path(sys.prefix) / "bin" / "uv"
262
+ if venv_uv.exists():
263
+ uv_executable = str(venv_uv)
264
+ else:
265
+ path_uv = _which("uv")
266
+ if path_uv:
267
+ uv_executable = path_uv
266
268
 
267
269
  # 检测是否安装了 RAG 特性(更精确)
268
270
  from jarvis.jarvis_utils.utils import (
@@ -274,7 +276,7 @@ def _check_pip_updates() -> bool:
274
276
  package_spec = (
275
277
  "jarvis-ai-assistant[rag]" if rag_installed else "jarvis-ai-assistant"
276
278
  )
277
- if is_uv_env and uv_executable:
279
+ if uv_executable:
278
280
  cmd_list = [uv_executable, "pip", "install", "--upgrade", package_spec]
279
281
  update_cmd = f"uv pip install --upgrade {package_spec}"
280
282
  else:
@@ -677,7 +679,7 @@ def _show_usage_stats(welcome_str: str) -> None:
677
679
 
678
680
  # 愿景 Panel
679
681
  vision_text = Text(
680
- "重新定义开发者体验,打破人与工具的界限,构建开发者与AI之间真正的共生伙伴关系。",
682
+ "让开发者与AI成为共生伙伴",
681
683
  justify="center",
682
684
  style="italic",
683
685
  )
@@ -692,7 +694,7 @@ def _show_usage_stats(welcome_str: str) -> None:
692
694
 
693
695
  # 使命 Panel
694
696
  mission_text = Text(
695
- "通过深度人机协作,将开发者的灵感(Vibe)高效落地为代码与行动,释放创造之力。",
697
+ "让灵感高效落地为代码与行动",
696
698
  justify="center",
697
699
  style="italic",
698
700
  )
@@ -1140,11 +1142,17 @@ def _collect_optional_config_interactively(
1140
1142
  )
1141
1143
 
1142
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
1143
1151
  changed = (
1144
1152
  _ask_and_set(
1145
1153
  "JARVIS_PRETTY_OUTPUT",
1146
1154
  "是否启用更美观的终端输出(Pretty Output)?",
1147
- False,
1155
+ _default_pretty,
1148
1156
  "bool",
1149
1157
  )
1150
1158
  or changed
@@ -1198,7 +1206,7 @@ def _collect_optional_config_interactively(
1198
1206
  _ask_and_set(
1199
1207
  "JARVIS_FORCE_SAVE_MEMORY",
1200
1208
  "是否强制保存会话记忆?",
1201
- True,
1209
+ False,
1202
1210
  "bool",
1203
1211
  )
1204
1212
  or changed
@@ -1328,6 +1336,38 @@ def _collect_optional_config_interactively(
1328
1336
 
1329
1337
 
1330
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
+
1331
1371
  new_mode = get_choice(
1332
1372
  tip,
1333
1373
  choices,
@@ -1342,6 +1382,7 @@ def _collect_optional_config_interactively(
1342
1382
  return False
1343
1383
 
1344
1384
  changed = _ask_git_check_mode() or changed
1385
+ changed = _ask_patch_format_mode() or changed
1345
1386
 
1346
1387
  # Git 提交提示词(可选)
1347
1388
  changed = (
@@ -1728,9 +1769,22 @@ def get_file_md5(filepath: str) -> str:
1728
1769
  filepath: 要计算哈希的文件路径
1729
1770
 
1730
1771
  返回:
1731
- str: 文件内容的MD5哈希值
1772
+ str: 文件内容的MD5哈希值(为降低内存占用,仅读取前100MB进行计算)
1732
1773
  """
1733
- 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()
1734
1788
 
1735
1789
 
1736
1790
  def get_file_line_count(filename: str) -> int:
@@ -1743,7 +1797,9 @@ def get_file_line_count(filename: str) -> int:
1743
1797
  int: 文件中的行数,如果文件无法读取则返回0
1744
1798
  """
1745
1799
  try:
1746
- 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)
1747
1803
  except Exception:
1748
1804
  return 0
1749
1805
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jarvis-ai-assistant
3
- Version: 0.3.29
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