jarvis-ai-assistant 0.3.30__py3-none-any.whl → 0.7.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +458 -152
  3. jarvis/jarvis_agent/agent_manager.py +17 -13
  4. jarvis/jarvis_agent/builtin_input_handler.py +2 -6
  5. jarvis/jarvis_agent/config_editor.py +2 -7
  6. jarvis/jarvis_agent/event_bus.py +82 -12
  7. jarvis/jarvis_agent/file_context_handler.py +329 -0
  8. jarvis/jarvis_agent/file_methodology_manager.py +3 -4
  9. jarvis/jarvis_agent/jarvis.py +628 -55
  10. jarvis/jarvis_agent/language_extractors/__init__.py +57 -0
  11. jarvis/jarvis_agent/language_extractors/c_extractor.py +21 -0
  12. jarvis/jarvis_agent/language_extractors/cpp_extractor.py +21 -0
  13. jarvis/jarvis_agent/language_extractors/go_extractor.py +21 -0
  14. jarvis/jarvis_agent/language_extractors/java_extractor.py +84 -0
  15. jarvis/jarvis_agent/language_extractors/javascript_extractor.py +79 -0
  16. jarvis/jarvis_agent/language_extractors/python_extractor.py +21 -0
  17. jarvis/jarvis_agent/language_extractors/rust_extractor.py +21 -0
  18. jarvis/jarvis_agent/language_extractors/typescript_extractor.py +84 -0
  19. jarvis/jarvis_agent/language_support_info.py +486 -0
  20. jarvis/jarvis_agent/main.py +34 -10
  21. jarvis/jarvis_agent/memory_manager.py +7 -16
  22. jarvis/jarvis_agent/methodology_share_manager.py +10 -16
  23. jarvis/jarvis_agent/prompt_manager.py +1 -1
  24. jarvis/jarvis_agent/prompts.py +193 -171
  25. jarvis/jarvis_agent/protocols.py +8 -12
  26. jarvis/jarvis_agent/run_loop.py +105 -9
  27. jarvis/jarvis_agent/session_manager.py +2 -3
  28. jarvis/jarvis_agent/share_manager.py +20 -22
  29. jarvis/jarvis_agent/shell_input_handler.py +1 -2
  30. jarvis/jarvis_agent/stdio_redirect.py +295 -0
  31. jarvis/jarvis_agent/task_analyzer.py +31 -6
  32. jarvis/jarvis_agent/task_manager.py +11 -27
  33. jarvis/jarvis_agent/tool_executor.py +2 -3
  34. jarvis/jarvis_agent/tool_share_manager.py +12 -24
  35. jarvis/jarvis_agent/utils.py +5 -1
  36. jarvis/jarvis_agent/web_bridge.py +189 -0
  37. jarvis/jarvis_agent/web_output_sink.py +53 -0
  38. jarvis/jarvis_agent/web_server.py +786 -0
  39. jarvis/jarvis_c2rust/__init__.py +26 -0
  40. jarvis/jarvis_c2rust/cli.py +575 -0
  41. jarvis/jarvis_c2rust/collector.py +250 -0
  42. jarvis/jarvis_c2rust/constants.py +26 -0
  43. jarvis/jarvis_c2rust/library_replacer.py +1254 -0
  44. jarvis/jarvis_c2rust/llm_module_agent.py +1272 -0
  45. jarvis/jarvis_c2rust/loaders.py +207 -0
  46. jarvis/jarvis_c2rust/models.py +28 -0
  47. jarvis/jarvis_c2rust/optimizer.py +2157 -0
  48. jarvis/jarvis_c2rust/scanner.py +1681 -0
  49. jarvis/jarvis_c2rust/transpiler.py +2983 -0
  50. jarvis/jarvis_c2rust/utils.py +385 -0
  51. jarvis/jarvis_code_agent/build_validation_config.py +132 -0
  52. jarvis/jarvis_code_agent/code_agent.py +1371 -220
  53. jarvis/jarvis_code_agent/code_analyzer/__init__.py +65 -0
  54. jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
  55. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
  56. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +106 -0
  57. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +74 -0
  58. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
  59. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +72 -0
  60. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +70 -0
  61. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +53 -0
  62. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +47 -0
  63. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +61 -0
  64. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +110 -0
  65. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +154 -0
  66. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +110 -0
  67. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +153 -0
  68. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
  69. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +648 -0
  70. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
  71. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
  72. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
  73. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
  74. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
  75. jarvis/jarvis_code_agent/code_analyzer/language_support.py +110 -0
  76. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +49 -0
  77. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +299 -0
  78. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +215 -0
  79. jarvis/jarvis_code_agent/code_analyzer/languages/java_language.py +212 -0
  80. jarvis/jarvis_code_agent/code_analyzer/languages/javascript_language.py +254 -0
  81. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +269 -0
  82. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +281 -0
  83. jarvis/jarvis_code_agent/code_analyzer/languages/typescript_language.py +280 -0
  84. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +605 -0
  85. jarvis/jarvis_code_agent/code_analyzer/structured_code.py +556 -0
  86. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +252 -0
  87. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +58 -0
  88. jarvis/jarvis_code_agent/lint.py +501 -8
  89. jarvis/jarvis_code_agent/utils.py +141 -0
  90. jarvis/jarvis_code_analysis/code_review.py +493 -584
  91. jarvis/jarvis_data/config_schema.json +128 -12
  92. jarvis/jarvis_git_squash/main.py +4 -5
  93. jarvis/jarvis_git_utils/git_commiter.py +82 -75
  94. jarvis/jarvis_mcp/sse_mcp_client.py +22 -29
  95. jarvis/jarvis_mcp/stdio_mcp_client.py +12 -13
  96. jarvis/jarvis_mcp/streamable_mcp_client.py +15 -14
  97. jarvis/jarvis_memory_organizer/memory_organizer.py +55 -74
  98. jarvis/jarvis_methodology/main.py +32 -48
  99. jarvis/jarvis_multi_agent/__init__.py +287 -55
  100. jarvis/jarvis_multi_agent/main.py +36 -4
  101. jarvis/jarvis_platform/base.py +524 -202
  102. jarvis/jarvis_platform/human.py +7 -8
  103. jarvis/jarvis_platform/kimi.py +30 -36
  104. jarvis/jarvis_platform/openai.py +88 -25
  105. jarvis/jarvis_platform/registry.py +26 -10
  106. jarvis/jarvis_platform/tongyi.py +24 -25
  107. jarvis/jarvis_platform/yuanbao.py +32 -43
  108. jarvis/jarvis_platform_manager/main.py +66 -77
  109. jarvis/jarvis_platform_manager/service.py +8 -13
  110. jarvis/jarvis_rag/cli.py +53 -55
  111. jarvis/jarvis_rag/embedding_manager.py +13 -18
  112. jarvis/jarvis_rag/llm_interface.py +8 -9
  113. jarvis/jarvis_rag/query_rewriter.py +10 -21
  114. jarvis/jarvis_rag/rag_pipeline.py +24 -27
  115. jarvis/jarvis_rag/reranker.py +4 -5
  116. jarvis/jarvis_rag/retriever.py +28 -30
  117. jarvis/jarvis_sec/__init__.py +305 -0
  118. jarvis/jarvis_sec/agents.py +143 -0
  119. jarvis/jarvis_sec/analysis.py +276 -0
  120. jarvis/jarvis_sec/checkers/__init__.py +32 -0
  121. jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
  122. jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
  123. jarvis/jarvis_sec/cli.py +139 -0
  124. jarvis/jarvis_sec/clustering.py +1439 -0
  125. jarvis/jarvis_sec/file_manager.py +427 -0
  126. jarvis/jarvis_sec/parsers.py +73 -0
  127. jarvis/jarvis_sec/prompts.py +268 -0
  128. jarvis/jarvis_sec/report.py +336 -0
  129. jarvis/jarvis_sec/review.py +453 -0
  130. jarvis/jarvis_sec/status.py +264 -0
  131. jarvis/jarvis_sec/types.py +20 -0
  132. jarvis/jarvis_sec/utils.py +499 -0
  133. jarvis/jarvis_sec/verification.py +848 -0
  134. jarvis/jarvis_sec/workflow.py +226 -0
  135. jarvis/jarvis_smart_shell/main.py +38 -87
  136. jarvis/jarvis_stats/cli.py +2 -2
  137. jarvis/jarvis_stats/stats.py +8 -8
  138. jarvis/jarvis_stats/storage.py +15 -21
  139. jarvis/jarvis_stats/visualizer.py +1 -1
  140. jarvis/jarvis_tools/clear_memory.py +3 -20
  141. jarvis/jarvis_tools/cli/main.py +21 -23
  142. jarvis/jarvis_tools/edit_file.py +1019 -132
  143. jarvis/jarvis_tools/execute_script.py +83 -25
  144. jarvis/jarvis_tools/file_analyzer.py +6 -9
  145. jarvis/jarvis_tools/generate_new_tool.py +14 -21
  146. jarvis/jarvis_tools/lsp_client.py +1552 -0
  147. jarvis/jarvis_tools/methodology.py +2 -3
  148. jarvis/jarvis_tools/read_code.py +1736 -35
  149. jarvis/jarvis_tools/read_symbols.py +140 -0
  150. jarvis/jarvis_tools/read_webpage.py +12 -13
  151. jarvis/jarvis_tools/registry.py +427 -200
  152. jarvis/jarvis_tools/retrieve_memory.py +20 -19
  153. jarvis/jarvis_tools/rewrite_file.py +72 -158
  154. jarvis/jarvis_tools/save_memory.py +3 -15
  155. jarvis/jarvis_tools/search_web.py +18 -18
  156. jarvis/jarvis_tools/sub_agent.py +36 -43
  157. jarvis/jarvis_tools/sub_code_agent.py +25 -26
  158. jarvis/jarvis_tools/virtual_tty.py +55 -33
  159. jarvis/jarvis_utils/clipboard.py +7 -10
  160. jarvis/jarvis_utils/config.py +232 -45
  161. jarvis/jarvis_utils/embedding.py +8 -5
  162. jarvis/jarvis_utils/fzf.py +8 -8
  163. jarvis/jarvis_utils/git_utils.py +225 -36
  164. jarvis/jarvis_utils/globals.py +3 -3
  165. jarvis/jarvis_utils/http.py +1 -1
  166. jarvis/jarvis_utils/input.py +99 -48
  167. jarvis/jarvis_utils/jsonnet_compat.py +465 -0
  168. jarvis/jarvis_utils/methodology.py +52 -48
  169. jarvis/jarvis_utils/utils.py +819 -491
  170. jarvis_ai_assistant-0.7.6.dist-info/METADATA +600 -0
  171. jarvis_ai_assistant-0.7.6.dist-info/RECORD +218 -0
  172. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/entry_points.txt +4 -0
  173. jarvis/jarvis_agent/config.py +0 -92
  174. jarvis/jarvis_agent/edit_file_handler.py +0 -296
  175. jarvis/jarvis_platform/ai8.py +0 -332
  176. jarvis/jarvis_tools/ask_user.py +0 -54
  177. jarvis_ai_assistant-0.3.30.dist-info/METADATA +0 -381
  178. jarvis_ai_assistant-0.3.30.dist-info/RECORD +0 -137
  179. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/WHEEL +0 -0
  180. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/licenses/LICENSE +0 -0
  181. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/top_level.txt +0 -0
@@ -6,6 +6,9 @@ import signal
6
6
  import subprocess
7
7
  import sys
8
8
  import time
9
+ import atexit
10
+ import errno
11
+ import threading
9
12
  from pathlib import Path
10
13
  from typing import Any, Callable, Dict, List, Optional, Tuple
11
14
  from datetime import datetime, date
@@ -23,7 +26,6 @@ from jarvis.jarvis_utils.config import (
23
26
  from jarvis.jarvis_utils.embedding import get_context_token_count
24
27
  from jarvis.jarvis_utils.globals import get_in_chat, get_interrupt, set_interrupt
25
28
  from jarvis.jarvis_utils.input import user_confirm
26
- from jarvis.jarvis_utils.output import OutputType, PrettyOutput
27
29
 
28
30
  # 向后兼容:导出 get_yes_no 供外部模块引用
29
31
  get_yes_no = user_confirm
@@ -59,6 +61,10 @@ COMMAND_MAPPING = {
59
61
  "jst": "jarvis-stats",
60
62
  # 记忆整理
61
63
  "jmo": "jarvis-memory-organizer",
64
+ # 安全分析
65
+ "jsec": "jarvis-sec",
66
+ # C2Rust迁移
67
+ "jc2r": "jarvis-c2rust",
62
68
  }
63
69
 
64
70
  # RAG 依赖检测工具函数(更精确)
@@ -205,6 +211,117 @@ def _setup_signal_handler() -> None:
205
211
  signal.signal(signal.SIGINT, sigint_handler)
206
212
 
207
213
 
214
+ # ----------------------------
215
+ # 单实例文件锁(放置于初始化早期使用)
216
+ # ----------------------------
217
+ _INSTANCE_LOCK_PATH: Optional[Path] = None
218
+
219
+
220
+ def _get_instance_lock_path(lock_name: str = "instance.lock") -> Path:
221
+ try:
222
+ data_dir = Path(str(get_data_dir()))
223
+ except Exception:
224
+ data_dir = Path(os.path.expanduser("~/.jarvis"))
225
+ data_dir.mkdir(parents=True, exist_ok=True)
226
+ return data_dir / lock_name
227
+
228
+
229
+ def _read_lock_owner_pid(lock_path: Path) -> Optional[int]:
230
+ try:
231
+ txt = lock_path.read_text(encoding="utf-8", errors="ignore").strip()
232
+ if not txt:
233
+ return None
234
+ try:
235
+ info = json.loads(txt)
236
+ pid = info.get("pid")
237
+ return int(pid) if pid is not None else None
238
+ except Exception:
239
+ # 兼容纯数字PID
240
+ return int(txt)
241
+ except Exception:
242
+ return None
243
+
244
+
245
+ def _is_process_alive(pid: int) -> bool:
246
+ if pid is None or pid <= 0:
247
+ return False
248
+ try:
249
+ os.kill(pid, 0)
250
+ except ProcessLookupError:
251
+ return False
252
+ except PermissionError:
253
+ # 无权限但进程存在
254
+ return True
255
+ except OSError as e:
256
+ # 某些平台上,EPERM 表示进程存在但无权限
257
+ if getattr(e, "errno", None) == errno.EPERM:
258
+ return True
259
+ return False
260
+ else:
261
+ return True
262
+
263
+
264
+ def _release_instance_lock() -> None:
265
+ global _INSTANCE_LOCK_PATH
266
+ try:
267
+ if _INSTANCE_LOCK_PATH and _INSTANCE_LOCK_PATH.exists():
268
+ _INSTANCE_LOCK_PATH.unlink()
269
+ except Exception:
270
+ # 清理失败不影响退出
271
+ pass
272
+ _INSTANCE_LOCK_PATH = None
273
+
274
+
275
+ def _acquire_single_instance_lock(lock_name: str = "instance.lock") -> None:
276
+ """
277
+ 在数据目录(~/.jarvis 或配置的数据目录)下创建实例锁,防止重复启动。
278
+ 如果检测到已有存活实例,提示后退出。
279
+ """
280
+ global _INSTANCE_LOCK_PATH
281
+ lock_path = _get_instance_lock_path(lock_name)
282
+
283
+ # 已存在锁:检查是否为有效存活实例
284
+ if lock_path.exists():
285
+ pid = _read_lock_owner_pid(lock_path)
286
+ if pid and _is_process_alive(pid):
287
+ print(f"⚠️ 检测到已有一个 Jarvis 实例正在运行 (PID: {pid})。\n如果确认不存在正在运行的实例,请删除锁文件后重试:{lock_path}")
288
+ sys.exit(0)
289
+ # 尝试移除陈旧锁
290
+ try:
291
+ lock_path.unlink()
292
+ except Exception:
293
+ print(f"❌ 无法删除旧锁文件:{lock_path},请手动清理后重试。")
294
+ sys.exit(1)
295
+
296
+ # 原子创建锁文件,避免并发竞争
297
+ flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY
298
+ try:
299
+ fd = os.open(str(lock_path), flags)
300
+ with os.fdopen(fd, "w", encoding="utf-8") as fp:
301
+ payload = {
302
+ "pid": os.getpid(),
303
+ "time": int(time.time()),
304
+ "argv": sys.argv[:10],
305
+ }
306
+ try:
307
+ fp.write(json.dumps(payload, ensure_ascii=False))
308
+ except Exception:
309
+ fp.write(str(os.getpid()))
310
+ _INSTANCE_LOCK_PATH = lock_path
311
+ atexit.register(_release_instance_lock)
312
+ except FileExistsError:
313
+ # 极端并发下再次校验
314
+ pid = _read_lock_owner_pid(lock_path)
315
+ if pid and _is_process_alive(pid):
316
+ print(f"⚠️ 检测到已有一个 Jarvis 实例正在运行 (PID: {pid})。")
317
+ sys.exit(0)
318
+ print(f"❌ 锁文件已存在但可能为陈旧状态:{lock_path},请手动删除后重试。")
319
+ sys.exit(1)
320
+ except Exception as e:
321
+ print(f"❌ 创建实例锁失败: {e}")
322
+ sys.exit(1)
323
+
324
+
208
325
  def _check_pip_updates() -> bool:
209
326
  """检查pip安装的Jarvis是否有更新
210
327
 
@@ -234,7 +351,7 @@ def _check_pip_updates() -> bool:
234
351
  with urllib.request.urlopen(url, timeout=5) as response:
235
352
  data = json.loads(response.read().decode())
236
353
  latest_version = data["info"]["version"]
237
- except (urllib.error.URLError, KeyError, json.JSONDecodeError):
354
+ except (urllib.error.URLError, KeyError, ValueError):
238
355
  return False
239
356
 
240
357
  # 比较版本
@@ -242,13 +359,10 @@ def _check_pip_updates() -> bool:
242
359
  latest_ver = version.parse(latest_version)
243
360
 
244
361
  if latest_ver > current_ver:
245
- PrettyOutput.print(
246
- f"检测到新版本 v{latest_version} (当前版本: v{__version__})",
247
- OutputType.INFO,
248
- )
362
+ print(f"ℹ️ 检测到新版本 v{latest_version} (当前版本: v{__version__})")
249
363
 
250
364
  # 检测是否在虚拟环境中
251
- in_venv = hasattr(sys, "real_prefix") or (
365
+ hasattr(sys, "real_prefix") or (
252
366
  hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
253
367
  )
254
368
 
@@ -292,7 +406,7 @@ def _check_pip_updates() -> bool:
292
406
 
293
407
  # 自动尝试升级(失败时提供手动命令)
294
408
  try:
295
- PrettyOutput.print("正在自动更新 Jarvis,请稍候...", OutputType.INFO)
409
+ print("ℹ️ 正在自动更新 Jarvis,请稍候...")
296
410
  result = subprocess.run(
297
411
  cmd_list,
298
412
  capture_output=True,
@@ -302,25 +416,18 @@ def _check_pip_updates() -> bool:
302
416
  timeout=600,
303
417
  )
304
418
  if result.returncode == 0:
305
- PrettyOutput.print("更新成功,正在重启以应用新版本...", OutputType.SUCCESS)
419
+ print("更新成功,正在重启以应用新版本...")
306
420
  # 更新检查日期,避免重复触发
307
421
  last_check_file.write_text(today_str)
308
422
  return True
309
423
  else:
310
424
  err = (result.stderr or result.stdout or "").strip()
311
425
  if err:
312
- PrettyOutput.print(
313
- f"自动更新失败,错误信息(已截断): {err[:500]}",
314
- OutputType.WARNING,
315
- )
316
- PrettyOutput.print(
317
- f"请手动执行以下命令更新: {update_cmd}", OutputType.INFO
318
- )
426
+ print(f"⚠️ 自动更新失败,错误信息(已截断): {err[:500]}")
427
+ print(f"ℹ️ 请手动执行以下命令更新: {update_cmd}")
319
428
  except Exception:
320
- PrettyOutput.print("自动更新出现异常,已切换为手动更新方式。", OutputType.WARNING)
321
- PrettyOutput.print(
322
- f"请手动执行以下命令更新: {update_cmd}", OutputType.INFO
323
- )
429
+ print("⚠️ 自动更新出现异常,已切换为手动更新方式。")
430
+ print(f"ℹ️ 请手动执行以下命令更新: {update_cmd}")
324
431
 
325
432
  # 更新检查日期
326
433
  last_check_file.write_text(today_str)
@@ -338,14 +445,22 @@ def _check_jarvis_updates() -> bool:
338
445
  返回:
339
446
  bool: 是否需要重启进程
340
447
  """
341
- script_dir = Path(os.path.dirname(os.path.dirname(__file__)))
448
+ # 从当前文件目录向上查找包含 .git 的仓库根目录,修复原先只检查 src/jarvis 的问题
449
+ try:
450
+ script_path = Path(__file__).resolve()
451
+ repo_root: Optional[Path] = None
452
+ for d in [script_path.parent] + list(script_path.parents):
453
+ if (d / ".git").exists():
454
+ repo_root = d
455
+ break
456
+ except Exception:
457
+ repo_root = None
342
458
 
343
- # 先检查是否是git源码安装
344
- git_dir = script_dir / ".git"
345
- if git_dir.exists():
459
+ # 先检查是否是git源码安装(找到仓库根目录即认为是源码安装)
460
+ if repo_root and (repo_root / ".git").exists():
346
461
  from jarvis.jarvis_utils.git_utils import check_and_update_git_repo
347
462
 
348
- return check_and_update_git_repo(str(script_dir))
463
+ return check_and_update_git_repo(str(repo_root))
349
464
 
350
465
  # 检查是否是pip/uv pip安装的版本
351
466
  return _check_pip_updates()
@@ -353,8 +468,6 @@ def _check_jarvis_updates() -> bool:
353
468
 
354
469
  def _show_usage_stats(welcome_str: str) -> None:
355
470
  """显示Jarvis使用统计信息"""
356
- from jarvis.jarvis_utils.output import OutputType, PrettyOutput
357
-
358
471
  try:
359
472
 
360
473
  from rich.console import Console, Group
@@ -365,6 +478,7 @@ def _show_usage_stats(welcome_str: str) -> None:
365
478
  console = Console()
366
479
 
367
480
  from jarvis.jarvis_stats.stats import StatsManager
481
+ from jarvis.jarvis_stats.storage import StatsStorage
368
482
 
369
483
  # 获取所有可用的指标
370
484
  all_metrics = StatsManager.list_metrics()
@@ -380,19 +494,47 @@ def _show_usage_stats(welcome_str: str) -> None:
380
494
  "other": {"title": "📦 其他指标", "metrics": {}, "suffix": ""},
381
495
  }
382
496
 
383
- # 遍历所有指标,使用快速总量读取以避免全量扫描
384
- for metric in all_metrics:
497
+ # 复用存储实例,避免重复创建
498
+ storage = StatsStorage()
499
+
500
+ # 一次性读取元数据,避免重复读取
501
+ try:
502
+ meta = storage._load_json(storage.meta_file)
503
+ metrics_info = meta.get("metrics", {})
504
+ except Exception:
505
+ metrics_info = {}
506
+
507
+ # 批量读取所有总量文件,避免逐个文件操作
508
+ metric_totals: Dict[str, float] = {}
509
+ totals_dir = storage.totals_dir
510
+ if totals_dir.exists():
385
511
  try:
386
- total = StatsManager.get_metric_total(metric)
512
+ for total_file in totals_dir.glob("*"):
513
+ if total_file.is_file():
514
+ try:
515
+ with open(total_file, "r", encoding="utf-8") as f:
516
+ total = float((f.read() or "0").strip() or "0")
517
+ if total > 0:
518
+ metric_totals[total_file.name] = total
519
+ except Exception:
520
+ pass
387
521
  except Exception:
388
- total = 0.0
522
+ pass
389
523
 
524
+ # 遍历所有指标,使用批量读取的数据
525
+ for metric in all_metrics:
526
+ # 从批量读取的数据中获取总量
527
+ total = metric_totals.get(metric, 0.0)
528
+
390
529
  if not total or total <= 0:
391
530
  continue
392
531
 
393
- # 优先使用元信息中的分组(在写入指标时已记录)
394
- info = StatsManager.get_metric_info(metric) or {}
395
- group = info.get("group", "other")
532
+ # 从已加载的元数据中获取分组信息,避免重复读取
533
+ try:
534
+ info = metrics_info.get(metric, {})
535
+ group = info.get("group", "other")
536
+ except Exception:
537
+ group = "other"
396
538
 
397
539
  if group == "tool":
398
540
  categorized_stats["tool"]["metrics"][metric] = int(total)
@@ -679,7 +821,7 @@ def _show_usage_stats(welcome_str: str) -> None:
679
821
 
680
822
  # 愿景 Panel
681
823
  vision_text = Text(
682
- "重新定义开发者体验,打破人与工具的界限,构建开发者与AI之间真正的共生伙伴关系。",
824
+ "让开发者与AI成为共生伙伴",
683
825
  justify="center",
684
826
  style="italic",
685
827
  )
@@ -694,7 +836,7 @@ def _show_usage_stats(welcome_str: str) -> None:
694
836
 
695
837
  # 使命 Panel
696
838
  mission_text = Text(
697
- "通过深度人机协作,将开发者的灵感(Vibe)高效落地为代码与行动,释放创造之力。",
839
+ "让灵感高效落地为代码与行动",
698
840
  justify="center",
699
841
  style="italic",
700
842
  )
@@ -755,11 +897,11 @@ def _show_usage_stats(welcome_str: str) -> None:
755
897
  # 输出错误信息以便调试
756
898
  import traceback
757
899
 
758
- PrettyOutput.print(f"统计显示出错: {str(e)}", OutputType.ERROR)
759
- PrettyOutput.print(traceback.format_exc(), OutputType.ERROR)
900
+ print(f"统计显示出错: {str(e)}")
901
+ print(f"❌ {traceback.format_exc()}")
760
902
 
761
903
 
762
- def init_env(welcome_str: str, config_file: Optional[str] = None) -> None:
904
+ def init_env(welcome_str: str = "", config_file: Optional[str] = None) -> None:
763
905
  """初始化Jarvis环境
764
906
 
765
907
  参数:
@@ -769,35 +911,60 @@ def init_env(welcome_str: str, config_file: Optional[str] = None) -> None:
769
911
  # 0. 检查是否处于Jarvis打开的终端环境,避免嵌套
770
912
  try:
771
913
  if os.environ.get("JARVIS_TERMINAL") == "1":
772
- PrettyOutput.print(
773
- "检测到当前终端由 Jarvis 打开。再次启动可能导致嵌套。",
774
- OutputType.WARNING,
775
- )
914
+ print("⚠️ 检测到当前终端由 Jarvis 打开。再次启动可能导致嵌套。")
776
915
  if not user_confirm("是否仍要继续启动 Jarvis?", default=False):
777
- PrettyOutput.print("已取消启动以避免终端嵌套。", OutputType.INFO)
916
+ print("ℹ️ 已取消启动以避免终端嵌套。")
778
917
  sys.exit(0)
779
918
  except Exception:
780
919
  pass
781
920
 
782
921
  # 1. 设置信号处理
783
- _setup_signal_handler()
922
+ try:
923
+ _setup_signal_handler()
924
+ except Exception:
925
+ pass
784
926
 
785
- # 2. 统计命令使用
786
- count_cmd_usage()
927
+ # 2. 统计命令使用(异步执行,避免阻塞初始化)
928
+ try:
929
+ count_cmd_usage()
930
+ except Exception:
931
+ # 静默失败,不影响正常使用
932
+ pass
787
933
 
788
934
  # 3. 设置配置文件
789
935
  global g_config_file
790
936
  g_config_file = config_file
791
- load_config()
937
+ try:
938
+ load_config()
939
+ except Exception:
940
+ # 静默失败,不影响正常使用
941
+ pass
792
942
 
793
943
  # 4. 显示历史统计数据(仅在显示欢迎信息时显示)
944
+ # 使用延迟加载,避免阻塞初始化
794
945
  if welcome_str:
795
- _show_usage_stats(welcome_str)
946
+ try:
947
+ # 在后台线程中显示统计,避免阻塞主流程
948
+ import threading
949
+ def show_stats_async():
950
+ try:
951
+ _show_usage_stats(welcome_str)
952
+ except Exception:
953
+ pass
954
+ stats_thread = threading.Thread(target=show_stats_async, daemon=True)
955
+ stats_thread.start()
956
+ except Exception:
957
+ # 静默失败,不影响正常使用
958
+ pass
796
959
 
797
- # 5. 检查Jarvis更新
798
- if _check_jarvis_updates():
799
- os.execv(sys.executable, [sys.executable] + sys.argv)
800
- sys.exit(0)
960
+ # 5. 检查Jarvis更新(异步执行,避免阻塞)
961
+ try:
962
+ if _check_jarvis_updates():
963
+ os.execv(sys.executable, [sys.executable] + sys.argv)
964
+ sys.exit(0)
965
+ except Exception:
966
+ # 静默失败,不影响正常使用
967
+ pass
801
968
 
802
969
 
803
970
  def _interactive_config_setup(config_file_path: Path):
@@ -809,9 +976,7 @@ def _interactive_config_setup(config_file_path: Path):
809
976
  user_confirm as get_yes_no,
810
977
  )
811
978
 
812
- PrettyOutput.print(
813
- "欢迎使用 Jarvis!未找到配置文件,现在开始引导配置。", OutputType.INFO
814
- )
979
+ print("ℹ️ 欢迎使用 Jarvis!未找到配置文件,现在开始引导配置。")
815
980
 
816
981
  # 1. 选择平台
817
982
  registry = PlatformRegistry.get_global_platform_registry()
@@ -821,7 +986,7 @@ def _interactive_config_setup(config_file_path: Path):
821
986
  # 2. 配置环境变量
822
987
  platform_class = registry.platforms.get(platform_name)
823
988
  if not platform_class:
824
- PrettyOutput.print(f"平台 '{platform_name}' 加载失败。", OutputType.ERROR)
989
+ print(f"平台 '{platform_name}' 加载失败。")
825
990
  sys.exit(1)
826
991
 
827
992
  env_vars = {}
@@ -829,9 +994,7 @@ def _interactive_config_setup(config_file_path: Path):
829
994
  defaults = platform_class.get_env_defaults()
830
995
  config_guide = platform_class.get_env_config_guide()
831
996
  if required_keys:
832
- PrettyOutput.print(
833
- f"请输入 {platform_name} 平台所需的配置信息:", OutputType.INFO
834
- )
997
+ print(f"ℹ️ 请输入 {platform_name} 平台所需的配置信息:")
835
998
 
836
999
  # 如果有配置指导,先显示总体说明
837
1000
  if config_guide:
@@ -842,7 +1005,7 @@ def _interactive_config_setup(config_file_path: Path):
842
1005
  guide_lines.append("")
843
1006
  guide_lines.append(f"{key} 获取方法:")
844
1007
  guide_lines.append(str(config_guide[key]))
845
- PrettyOutput.print("\n".join(guide_lines), OutputType.INFO)
1008
+ print("ℹ️ " + "\n".join(guide_lines))
846
1009
  else:
847
1010
  # 若无指导,仍需遍历以保持后续逻辑一致
848
1011
  pass
@@ -864,7 +1027,7 @@ def _interactive_config_setup(config_file_path: Path):
864
1027
  try:
865
1028
  platform_instance = registry.create_platform(platform_name)
866
1029
  if not platform_instance:
867
- PrettyOutput.print(f"无法创建平台 '{platform_name}'。", OutputType.ERROR)
1030
+ print(f"无法创建平台 '{platform_name}'。")
868
1031
  sys.exit(1)
869
1032
 
870
1033
  model_list_tuples = platform_instance.get_model_list()
@@ -876,13 +1039,13 @@ def _interactive_config_setup(config_file_path: Path):
876
1039
  model_name, _ = model_list_tuples[selected_index]
877
1040
 
878
1041
  except Exception:
879
- PrettyOutput.print("获取模型列表失败", OutputType.ERROR)
1042
+ print("获取模型列表失败")
880
1043
  if not get_yes_no("无法获取模型列表,是否继续配置?"):
881
1044
  sys.exit(1)
882
1045
  model_name = get_input("请输入模型名称:")
883
1046
 
884
1047
  # 4. 测试配置
885
- PrettyOutput.print("正在测试配置...", OutputType.INFO)
1048
+ print("ℹ️ 正在测试配置...")
886
1049
  test_passed = False
887
1050
  try:
888
1051
  platform_instance = registry.create_platform(platform_name)
@@ -891,16 +1054,14 @@ def _interactive_config_setup(config_file_path: Path):
891
1054
  response_generator = platform_instance.chat("hello")
892
1055
  response = "".join(response_generator)
893
1056
  if response:
894
- PrettyOutput.print(
895
- f"测试成功,模型响应: {response}", OutputType.SUCCESS
896
- )
1057
+ print(f"✅ 测试成功,模型响应: {response}")
897
1058
  test_passed = True
898
1059
  else:
899
- PrettyOutput.print("测试失败,模型没有响应。", OutputType.ERROR)
1060
+ print("测试失败,模型没有响应。")
900
1061
  else:
901
- PrettyOutput.print("测试失败,无法创建平台实例。", OutputType.ERROR)
1062
+ print("测试失败,无法创建平台实例。")
902
1063
  except Exception:
903
- PrettyOutput.print("测试失败", OutputType.ERROR)
1064
+ print("测试失败")
904
1065
 
905
1066
  # 5. 交互式确认并应用配置(不直接生成配置文件)
906
1067
  config_data = {
@@ -911,7 +1072,7 @@ def _interactive_config_setup(config_file_path: Path):
911
1072
 
912
1073
  if not test_passed:
913
1074
  if not get_yes_no("配置测试失败,是否仍要应用该配置并继续?", default=False):
914
- PrettyOutput.print("已取消配置。", OutputType.INFO)
1075
+ print("ℹ️ 已取消配置。")
915
1076
  sys.exit(0)
916
1077
 
917
1078
  # 6. 选择其他功能开关与可选项(复用统一逻辑)
@@ -934,11 +1095,11 @@ def _interactive_config_setup(config_file_path: Path):
934
1095
  if header:
935
1096
  f.write(header)
936
1097
  f.write(yaml_str)
937
- PrettyOutput.print(f"配置文件已生成: {config_file_path}", OutputType.SUCCESS)
938
- PrettyOutput.print("配置完成,请重新启动Jarvis。", OutputType.INFO)
1098
+ print(f"配置文件已生成: {config_file_path}")
1099
+ print("ℹ️ 配置完成,请重新启动Jarvis。")
939
1100
  sys.exit(0)
940
1101
  except Exception:
941
- PrettyOutput.print("写入配置文件失败", OutputType.ERROR)
1102
+ print("写入配置文件失败")
942
1103
  sys.exit(1)
943
1104
 
944
1105
 
@@ -1016,359 +1177,420 @@ def _process_env_variables(config_data: dict) -> None:
1016
1177
  )
1017
1178
 
1018
1179
 
1019
- def _collect_optional_config_interactively(
1020
- config_data: dict, ask_all: bool = False
1021
- ) -> bool:
1022
- """
1023
- 复用的交互式配置收集逻辑:
1024
- - ask_all=False(默认):仅对缺省的新功能开关/可选项逐项询问,已存在项跳过
1025
- - ask_all=True:对所有项进行询问,默认值取自当前配置文件,可覆盖现有设置
1026
- - 修改传入的 config_data
1027
- - 包含更多来自 config.py 的可选项
1028
- 返回:
1029
- bool: 是否有变更
1030
- """
1031
- from jarvis.jarvis_utils.input import user_confirm as get_yes_no
1032
- from jarvis.jarvis_utils.input import get_single_line_input
1180
+ def _ask_config_bool(config_data: dict, ask_all: bool, _key: str, _tip: str, _default: bool) -> bool:
1181
+ """询问并设置布尔类型配置项"""
1182
+ try:
1183
+ if not ask_all and _key in config_data:
1184
+ return False
1185
+ from jarvis.jarvis_utils.input import user_confirm as get_yes_no
1186
+ cur = bool(config_data.get(_key, _default))
1187
+ val = get_yes_no(_tip, default=cur)
1188
+ if bool(val) == cur:
1189
+ return False
1190
+ config_data[_key] = bool(val)
1191
+ return True
1192
+ except Exception:
1193
+ return False
1033
1194
 
1034
- def _ask_and_set(_key, _tip, _default, _type="bool"):
1035
- try:
1036
- if not ask_all and _key in config_data:
1037
- return False
1038
- if _type == "bool":
1039
- cur = bool(config_data.get(_key, _default))
1040
- val = get_yes_no(_tip, default=cur)
1041
- # 与当前值相同则不写入,避免冗余
1042
- if bool(val) == cur:
1043
- return False
1044
- config_data[_key] = bool(val)
1045
- else:
1046
- cur = str(config_data.get(_key, _default or ""))
1047
- val = get_single_line_input(f"{_tip}", default=cur)
1048
- v = ("" if val is None else str(val)).strip()
1049
- # 输入与当前值相同则不写入
1050
- if v == cur:
1051
- return False
1052
- config_data[_key] = v
1053
- return True
1054
- except Exception:
1055
- # 异常时不写入,保持精简
1195
+
1196
+ def _ask_config_str(config_data: dict, ask_all: bool, _key: str, _tip: str, _default: str = "") -> bool:
1197
+ """询问并设置字符串类型配置项"""
1198
+ try:
1199
+ if not ask_all and _key in config_data:
1056
1200
  return False
1201
+ from jarvis.jarvis_utils.input import get_single_line_input
1202
+ cur = str(config_data.get(_key, _default or ""))
1203
+ val = get_single_line_input(f"{_tip}", default=cur)
1204
+ v = ("" if val is None else str(val)).strip()
1205
+ if v == cur:
1206
+ return False
1207
+ config_data[_key] = v
1208
+ return True
1209
+ except Exception:
1210
+ return False
1057
1211
 
1058
- def _ask_and_set_optional_str(_key, _tip, _default: str = "") -> bool:
1059
- try:
1060
- if not ask_all and _key in config_data:
1061
- return False
1062
- cur = str(config_data.get(_key, _default or ""))
1063
- val = get_single_line_input(f"{_tip}", default=cur)
1064
- if val is None:
1065
- return False
1066
- s = str(val).strip()
1067
- # 空输入表示不改变
1068
- if s == "":
1069
- return False
1070
- if s == cur:
1071
- return False
1072
- config_data[_key] = s
1073
- return True
1074
- except Exception:
1212
+
1213
+ def _ask_config_optional_str(config_data: dict, ask_all: bool, _key: str, _tip: str, _default: str = "") -> bool:
1214
+ """询问并设置可选字符串类型配置项(空输入表示不改变)"""
1215
+ try:
1216
+ if not ask_all and _key in config_data:
1217
+ return False
1218
+ from jarvis.jarvis_utils.input import get_single_line_input
1219
+ cur = str(config_data.get(_key, _default or ""))
1220
+ val = get_single_line_input(f"{_tip}", default=cur)
1221
+ if val is None:
1222
+ return False
1223
+ s = str(val).strip()
1224
+ if s == "" or s == cur:
1075
1225
  return False
1226
+ config_data[_key] = s
1227
+ return True
1228
+ except Exception:
1229
+ return False
1230
+
1076
1231
 
1077
- def _ask_and_set_int(_key, _tip, _default: int) -> bool:
1232
+ def _ask_config_int(config_data: dict, ask_all: bool, _key: str, _tip: str, _default: int) -> bool:
1233
+ """询问并设置整数类型配置项"""
1234
+ try:
1235
+ if not ask_all and _key in config_data:
1236
+ return False
1237
+ from jarvis.jarvis_utils.input import get_single_line_input
1238
+ cur = str(config_data.get(_key, _default))
1239
+ val_str = get_single_line_input(f"{_tip}", default=cur)
1240
+ s = "" if val_str is None else str(val_str).strip()
1241
+ if s == "" or s == cur:
1242
+ return False
1078
1243
  try:
1079
- if not ask_all and _key in config_data:
1080
- return False
1081
- cur = str(config_data.get(_key, _default))
1082
- val_str = get_single_line_input(f"{_tip}", default=cur)
1083
- s = "" if val_str is None else str(val_str).strip()
1084
- if s == "" or s == cur:
1085
- return False
1086
- try:
1087
- v = int(s)
1088
- except Exception:
1089
- return False
1090
- if str(v) == cur:
1091
- return False
1092
- config_data[_key] = v
1093
- return True
1244
+ v = int(s)
1094
1245
  except Exception:
1095
1246
  return False
1247
+ if str(v) == cur:
1248
+ return False
1249
+ config_data[_key] = v
1250
+ return True
1251
+ except Exception:
1252
+ return False
1096
1253
 
1097
- def _ask_and_set_list(_key, _tip) -> bool:
1098
- try:
1099
- if not ask_all and _key in config_data:
1100
- return False
1101
- cur_val = config_data.get(_key, [])
1102
- if isinstance(cur_val, list):
1103
- cur_display = ", ".join([str(x) for x in cur_val])
1104
- else:
1105
- cur_display = str(cur_val or "")
1106
- val = get_single_line_input(f"{_tip}", default=cur_display)
1107
- if val is None:
1108
- return False
1109
- s = str(val).strip()
1110
- if s == cur_display.strip():
1111
- return False
1112
- if not s:
1113
- # 输入为空表示不改变
1114
- return False
1115
- items = [x.strip() for x in s.split(",") if x.strip()]
1116
- if isinstance(cur_val, list) and items == cur_val:
1117
- return False
1118
- config_data[_key] = items
1119
- return True
1120
- except Exception:
1254
+
1255
+ def _ask_config_list(config_data: dict, ask_all: bool, _key: str, _tip: str) -> bool:
1256
+ """询问并设置列表类型配置项(逗号分隔)"""
1257
+ try:
1258
+ if not ask_all and _key in config_data:
1259
+ return False
1260
+ from jarvis.jarvis_utils.input import get_single_line_input
1261
+ cur_val = config_data.get(_key, [])
1262
+ if isinstance(cur_val, list):
1263
+ cur_display = ", ".join([str(x) for x in cur_val])
1264
+ else:
1265
+ cur_display = str(cur_val or "")
1266
+ val = get_single_line_input(f"{_tip}", default=cur_display)
1267
+ if val is None:
1268
+ return False
1269
+ s = str(val).strip()
1270
+ if s == cur_display.strip():
1121
1271
  return False
1272
+ if not s:
1273
+ return False
1274
+ items = [x.strip() for x in s.split(",") if x.strip()]
1275
+ if isinstance(cur_val, list) and items == cur_val:
1276
+ return False
1277
+ config_data[_key] = items
1278
+ return True
1279
+ except Exception:
1280
+ return False
1281
+
1122
1282
 
1283
+ def _collect_basic_switches(config_data: dict, ask_all: bool) -> bool:
1284
+ """收集基础开关配置"""
1123
1285
  changed = False
1124
- # 现有两个开关
1125
- changed = (
1126
- _ask_and_set(
1127
- "JARVIS_ENABLE_GIT_JCA_SWITCH",
1128
- "是否在检测到Git仓库时,提示并可自动切换到代码开发模式(jca)?",
1129
- False,
1130
- "bool",
1131
- )
1132
- or changed
1133
- )
1134
- changed = (
1135
- _ask_and_set(
1136
- "JARVIS_ENABLE_STARTUP_CONFIG_SELECTOR",
1137
- "在进入默认通用代理前,是否先列出可用配置(agent/multi_agent/roles)供选择?",
1138
- False,
1139
- "bool",
1140
- )
1141
- or changed
1142
- )
1286
+ changed = _ask_config_bool(
1287
+ config_data, ask_all,
1288
+ "JARVIS_ENABLE_GIT_JCA_SWITCH",
1289
+ "是否在检测到Git仓库时,提示并可自动切换到代码开发模式(jca)?",
1290
+ False,
1291
+ ) or changed
1292
+ changed = _ask_config_bool(
1293
+ config_data, ask_all,
1294
+ "JARVIS_ENABLE_STARTUP_CONFIG_SELECTOR",
1295
+ "在进入默认通用代理前,是否先列出可用配置(agent/multi_agent/roles)供选择?",
1296
+ False,
1297
+ ) or changed
1298
+ return changed
1143
1299
 
1144
- # 新增的配置项交互(通用体验相关)
1145
- changed = (
1146
- _ask_and_set(
1147
- "JARVIS_PRETTY_OUTPUT",
1148
- "是否启用更美观的终端输出(Pretty Output)?",
1149
- False,
1150
- "bool",
1151
- )
1152
- or changed
1153
- )
1154
- changed = (
1155
- _ask_and_set(
1156
- "JARVIS_PRINT_PROMPT",
1157
- "是否打印发送给模型的提示词(Prompt)?",
1158
- False,
1159
- "bool",
1160
- )
1161
- or changed
1162
- )
1163
- changed = (
1164
- _ask_and_set(
1165
- "JARVIS_IMMEDIATE_ABORT",
1166
- "是否启用立即中断?\n- 选择 是/true:在对话输出流的每次迭代中检测到用户中断(例如 Ctrl+C)时,立即返回当前已生成的内容并停止继续输出。\n- 选择 否/false:不会在输出过程中立刻返回,而是按既有流程处理(不中途打断输出)。",
1167
- False,
1168
- "bool",
1169
- )
1170
- or changed
1171
- )
1172
- changed = (
1173
- _ask_and_set(
1174
- "JARVIS_ENABLE_STATIC_ANALYSIS",
1175
- "是否启用静态代码分析(Static Analysis)?",
1176
- True,
1177
- "bool",
1178
- )
1179
- or changed
1180
- )
1181
- changed = (
1182
- _ask_and_set(
1183
- "JARVIS_USE_METHODOLOGY",
1184
- "是否启用方法论系统(Methodology)?",
1185
- True,
1186
- "bool",
1187
- )
1188
- or changed
1189
- )
1190
- changed = (
1191
- _ask_and_set(
1192
- "JARVIS_USE_ANALYSIS",
1193
- "是否启用分析流程(Analysis)?",
1194
- True,
1195
- "bool",
1196
- )
1197
- or changed
1198
- )
1199
- changed = (
1200
- _ask_and_set(
1201
- "JARVIS_FORCE_SAVE_MEMORY",
1202
- "是否强制保存会话记忆?",
1203
- True,
1204
- "bool",
1205
- )
1206
- or changed
1207
- )
1208
1300
 
1209
- # 代码与工具操作安全提示
1210
- changed = (
1211
- _ask_and_set(
1212
- "JARVIS_EXECUTE_TOOL_CONFIRM",
1213
- "执行工具前是否需要确认?",
1214
- False,
1215
- "bool",
1216
- )
1217
- or changed
1218
- )
1219
- changed = (
1220
- _ask_and_set(
1221
- "JARVIS_CONFIRM_BEFORE_APPLY_PATCH",
1222
- "应用补丁前是否需要确认?",
1223
- False,
1224
- "bool",
1225
- )
1226
- or changed
1227
- )
1301
+ def _collect_ui_experience_config(config_data: dict, ask_all: bool) -> bool:
1302
+ """收集UI体验相关配置"""
1303
+ changed = False
1304
+ try:
1305
+ import platform as _platform_mod
1306
+ _default_pretty = False if _platform_mod.system() == "Windows" else True
1307
+ except Exception:
1308
+ _default_pretty = True
1309
+
1310
+ changed = _ask_config_bool(
1311
+ config_data, ask_all,
1312
+ "JARVIS_PRETTY_OUTPUT",
1313
+ "是否启用更美观的终端输出(Pretty Output)?",
1314
+ _default_pretty,
1315
+ ) or changed
1316
+ changed = _ask_config_bool(
1317
+ config_data, ask_all,
1318
+ "JARVIS_PRINT_PROMPT",
1319
+ "是否打印发送给模型的提示词(Prompt)?",
1320
+ False,
1321
+ ) or changed
1322
+ changed = _ask_config_bool(
1323
+ config_data, ask_all,
1324
+ "JARVIS_IMMEDIATE_ABORT",
1325
+ "是否启用立即中断?\n- 选择 是/true:在对话输出流的每次迭代中检测到用户中断(例如 Ctrl+C)时,立即返回当前已生成的内容并停止继续输出。\n- 选择 否/false:不会在输出过程中立刻返回,而是按既有流程处理(不中途打断输出)。",
1326
+ False,
1327
+ ) or changed
1328
+ return changed
1228
1329
 
1229
- # 数据目录与最大输入Token
1230
- from jarvis.jarvis_utils.config import get_data_dir as _get_data_dir # lazy import
1231
1330
 
1232
- changed = (
1233
- _ask_and_set_optional_str(
1234
- "JARVIS_DATA_PATH",
1235
- f"是否自定义数据目录路径(JARVIS_DATA_PATH)?留空使用默认: {_get_data_dir()}",
1236
- )
1237
- or changed
1238
- )
1239
- changed = (
1240
- _ask_and_set_int(
1241
- "JARVIS_MAX_INPUT_TOKEN_COUNT",
1242
- "自定义最大输入Token数量(留空使用默认: 32000)",
1243
- 32000,
1244
- )
1245
- or changed
1246
- )
1247
- changed = (
1248
- _ask_and_set_int(
1249
- "JARVIS_TOOL_FILTER_THRESHOLD",
1250
- "设置AI工具筛选阈值 (当可用工具数超过此值时触发AI筛选, 默认30)",
1251
- 30,
1252
- )
1253
- or changed
1254
- )
1331
+ def _collect_analysis_config(config_data: dict, ask_all: bool) -> bool:
1332
+ """收集代码分析相关配置"""
1333
+ changed = False
1334
+ changed = _ask_config_bool(
1335
+ config_data, ask_all,
1336
+ "JARVIS_ENABLE_STATIC_ANALYSIS",
1337
+ "是否启用静态代码分析(Static Analysis)?",
1338
+ True,
1339
+ ) or changed
1340
+ changed = _ask_config_bool(
1341
+ config_data, ask_all,
1342
+ "JARVIS_ENABLE_BUILD_VALIDATION",
1343
+ "是否启用构建验证(Build Validation)?在代码编辑后自动验证代码能否成功编译/构建。",
1344
+ True,
1345
+ ) or changed
1346
+ changed = _ask_config_int(
1347
+ config_data, ask_all,
1348
+ "JARVIS_BUILD_VALIDATION_TIMEOUT",
1349
+ "构建验证的超时时间(秒,默认30秒)",
1350
+ 30,
1351
+ ) or changed
1352
+ changed = _ask_config_bool(
1353
+ config_data, ask_all,
1354
+ "JARVIS_ENABLE_IMPACT_ANALYSIS",
1355
+ "是否启用编辑影响范围分析(Impact Analysis)?分析代码编辑的影响范围,识别可能受影响的文件、函数、测试等。",
1356
+ True,
1357
+ ) or changed
1358
+ return changed
1255
1359
 
1256
- # 目录类配置(逗号分隔)
1257
- changed = (
1258
- _ask_and_set_list(
1259
- "JARVIS_TOOL_LOAD_DIRS",
1260
- "指定工具加载目录(逗号分隔,留空跳过):",
1261
- )
1262
- or changed
1263
- )
1264
- changed = (
1265
- _ask_and_set_list(
1266
- "JARVIS_METHODOLOGY_DIRS",
1267
- "指定方法论加载目录(逗号分隔,留空跳过):",
1268
- )
1269
- or changed
1270
- )
1271
- changed = (
1272
- _ask_and_set_list(
1273
- "JARVIS_AGENT_DEFINITION_DIRS",
1274
- "指定 agent 定义加载目录(逗号分隔,留空跳过):",
1275
- )
1276
- or changed
1277
- )
1278
- changed = (
1279
- _ask_and_set_list(
1280
- "JARVIS_MULTI_AGENT_DIRS",
1281
- "指定 multi_agent 加载目录(逗号分隔,留空跳过):",
1282
- )
1283
- or changed
1284
- )
1285
- changed = (
1286
- _ask_and_set_list(
1287
- "JARVIS_ROLES_DIRS",
1288
- "指定 roles 加载目录(逗号分隔,留空跳过):",
1289
- )
1290
- or changed
1291
- )
1292
1360
 
1293
- # Web 搜索配置(可选)
1294
- changed = (
1295
- _ask_and_set_optional_str(
1296
- "JARVIS_WEB_SEARCH_PLATFORM",
1297
- "配置 Web 搜索平台名称(留空跳过):",
1298
- )
1299
- or changed
1300
- )
1301
- changed = (
1302
- _ask_and_set_optional_str(
1303
- "JARVIS_WEB_SEARCH_MODEL",
1304
- "配置 Web 搜索模型名称(留空跳过):",
1305
- )
1306
- or changed
1307
- )
1361
+ def _collect_agent_features_config(config_data: dict, ask_all: bool) -> bool:
1362
+ """收集Agent功能相关配置"""
1363
+ changed = False
1364
+ changed = _ask_config_bool(
1365
+ config_data, ask_all,
1366
+ "JARVIS_USE_METHODOLOGY",
1367
+ "是否启用方法论系统(Methodology)?",
1368
+ True,
1369
+ ) or changed
1370
+ changed = _ask_config_bool(
1371
+ config_data, ask_all,
1372
+ "JARVIS_USE_ANALYSIS",
1373
+ "是否启用分析流程(Analysis)?",
1374
+ True,
1375
+ ) or changed
1376
+ changed = _ask_config_bool(
1377
+ config_data, ask_all,
1378
+ "JARVIS_FORCE_SAVE_MEMORY",
1379
+ "是否强制保存会话记忆?",
1380
+ False,
1381
+ ) or changed
1382
+ return changed
1308
1383
 
1309
- # Git 校验模式
1310
- def _ask_git_check_mode() -> bool:
1311
- try:
1312
- _key = "JARVIS_GIT_CHECK_MODE"
1313
- if not ask_all and _key in config_data:
1314
- return False
1315
1384
 
1316
- from jarvis.jarvis_utils.input import get_choice
1317
- from jarvis.jarvis_utils.config import get_git_check_mode
1318
-
1319
- current_mode = config_data.get(_key, get_git_check_mode())
1320
- choices = ["strict", "warn"]
1321
- tip = (
1322
- "请选择 Git 仓库检查模式 (JARVIS_GIT_CHECK_MODE):\n"
1323
- "此设置决定了当在 Git 仓库中检测到未提交的更改时,Jarvis应如何处理。\n"
1324
- "这对于确保代码修改和提交操作在干净的工作区上进行至关重要。\n"
1325
- " - strict: (推荐) 如果存在未提交的更改,则中断相关操作(如代码修改、自动提交)。\n"
1326
- " 这可以防止意外覆盖或丢失本地工作。\n"
1327
- " - warn: 如果存在未提交的更改,仅显示警告信息,然后继续执行操作。\n"
1328
- " 适用于您希望绕过检查并自行管理仓库状态的场景。"
1329
- )
1385
+ def _collect_session_config(config_data: dict, ask_all: bool) -> bool:
1386
+ """收集会话与调试相关配置"""
1387
+ changed = False
1388
+ changed = _ask_config_bool(
1389
+ config_data, ask_all,
1390
+ "JARVIS_SAVE_SESSION_HISTORY",
1391
+ "是否保存会话记录?",
1392
+ False,
1393
+ ) or changed
1394
+ changed = _ask_config_bool(
1395
+ config_data, ask_all,
1396
+ "JARVIS_PRINT_ERROR_TRACEBACK",
1397
+ "是否在错误输出时打印回溯调用链?",
1398
+ False,
1399
+ ) or changed
1400
+ changed = _ask_config_bool(
1401
+ config_data, ask_all,
1402
+ "JARVIS_SKIP_PREDEFINED_TASKS",
1403
+ "是否跳过预定义任务加载(不读取 pre-command 列表)?",
1404
+ False,
1405
+ ) or changed
1406
+ changed = _ask_config_int(
1407
+ config_data, ask_all,
1408
+ "JARVIS_CONVERSATION_TURN_THRESHOLD",
1409
+ "对话轮次阈值(达到此轮次时触发总结,建议50-100):",
1410
+ 50,
1411
+ ) or changed
1412
+ return changed
1330
1413
 
1331
1414
 
1415
+ def _collect_safety_config(config_data: dict, ask_all: bool) -> bool:
1416
+ """收集代码与工具操作安全提示配置"""
1417
+ changed = False
1418
+ changed = _ask_config_bool(
1419
+ config_data, ask_all,
1420
+ "JARVIS_EXECUTE_TOOL_CONFIRM",
1421
+ "执行工具前是否需要确认?",
1422
+ False,
1423
+ ) or changed
1424
+ changed = _ask_config_bool(
1425
+ config_data, ask_all,
1426
+ "JARVIS_CONFIRM_BEFORE_APPLY_PATCH",
1427
+ "应用补丁前是否需要确认?",
1428
+ False,
1429
+ ) or changed
1430
+ return changed
1332
1431
 
1333
- new_mode = get_choice(
1334
- tip,
1335
- choices,
1336
- )
1337
1432
 
1338
- if new_mode == current_mode:
1339
- return False
1433
+ def _collect_data_and_token_config(config_data: dict, ask_all: bool) -> bool:
1434
+ """收集数据目录与最大输入Token配置"""
1435
+ changed = False
1436
+ from jarvis.jarvis_utils.config import get_data_dir as _get_data_dir
1437
+ changed = _ask_config_optional_str(
1438
+ config_data, ask_all,
1439
+ "JARVIS_DATA_PATH",
1440
+ f"是否自定义数据目录路径(JARVIS_DATA_PATH)?留空使用默认: {_get_data_dir()}",
1441
+ ) or changed
1442
+ changed = _ask_config_int(
1443
+ config_data, ask_all,
1444
+ "JARVIS_MAX_INPUT_TOKEN_COUNT",
1445
+ "自定义最大输入Token数量(留空使用默认: 32000)",
1446
+ 32000,
1447
+ ) or changed
1448
+ changed = _ask_config_int(
1449
+ config_data, ask_all,
1450
+ "JARVIS_TOOL_FILTER_THRESHOLD",
1451
+ "设置AI工具筛选阈值 (当可用工具数超过此值时触发AI筛选, 默认30)",
1452
+ 30,
1453
+ ) or changed
1454
+ return changed
1340
1455
 
1341
- config_data[_key] = new_mode
1342
- return True
1343
- except Exception:
1344
- return False
1345
1456
 
1346
- changed = _ask_git_check_mode() or changed
1457
+ def _collect_advanced_config(config_data: dict, ask_all: bool) -> bool:
1458
+ """收集高级配置(自动总结、脚本超时等)"""
1459
+ changed = False
1460
+ changed = _ask_config_int(
1461
+ config_data, ask_all,
1462
+ "JARVIS_SCRIPT_EXECUTION_TIMEOUT",
1463
+ "脚本执行超时时间(秒,默认300,仅非交互模式生效)",
1464
+ 300,
1465
+ ) or changed
1466
+ changed = _ask_config_int(
1467
+ config_data, ask_all,
1468
+ "JARVIS_ADDON_PROMPT_THRESHOLD",
1469
+ "附加提示的触发阈值(字符数,默认1024)。当消息长度超过此值时,会自动添加默认的附加提示",
1470
+ 1024,
1471
+ ) or changed
1472
+ changed = _ask_config_bool(
1473
+ config_data, ask_all,
1474
+ "JARVIS_ENABLE_INTENT_RECOGNITION",
1475
+ "是否启用意图识别功能?用于智能上下文推荐中的LLM意图提取和语义分析",
1476
+ True,
1477
+ ) or changed
1478
+ return changed
1479
+
1347
1480
 
1348
- # Git 提交提示词(可选)
1349
- changed = (
1350
- _ask_and_set_optional_str(
1351
- "JARVIS_GIT_COMMIT_PROMPT",
1352
- "自定义 Git 提交提示模板(留空跳过):",
1481
+ def _collect_directory_config(config_data: dict, ask_all: bool) -> bool:
1482
+ """收集目录类配置(逗号分隔)"""
1483
+ changed = False
1484
+ changed = _ask_config_list(
1485
+ config_data, ask_all,
1486
+ "JARVIS_TOOL_LOAD_DIRS",
1487
+ "指定工具加载目录(逗号分隔,留空跳过):",
1488
+ ) or changed
1489
+ changed = _ask_config_list(
1490
+ config_data, ask_all,
1491
+ "JARVIS_METHODOLOGY_DIRS",
1492
+ "指定方法论加载目录(逗号分隔,留空跳过):",
1493
+ ) or changed
1494
+ changed = _ask_config_list(
1495
+ config_data, ask_all,
1496
+ "JARVIS_AGENT_DEFINITION_DIRS",
1497
+ "指定 agent 定义加载目录(逗号分隔,留空跳过):",
1498
+ ) or changed
1499
+ changed = _ask_config_list(
1500
+ config_data, ask_all,
1501
+ "JARVIS_MULTI_AGENT_DIRS",
1502
+ "指定 multi_agent 加载目录(逗号分隔,留空跳过):",
1503
+ ) or changed
1504
+ changed = _ask_config_list(
1505
+ config_data, ask_all,
1506
+ "JARVIS_ROLES_DIRS",
1507
+ "指定 roles 加载目录(逗号分隔,留空跳过):",
1508
+ ) or changed
1509
+ changed = _ask_config_list(
1510
+ config_data, ask_all,
1511
+ "JARVIS_AFTER_TOOL_CALL_CB_DIRS",
1512
+ "指定工具调用后回调实现目录(逗号分隔,留空跳过):",
1513
+ ) or changed
1514
+ return changed
1515
+
1516
+
1517
+ def _collect_web_search_config(config_data: dict, ask_all: bool) -> bool:
1518
+ """收集Web搜索配置"""
1519
+ changed = False
1520
+ changed = _ask_config_optional_str(
1521
+ config_data, ask_all,
1522
+ "JARVIS_WEB_SEARCH_PLATFORM",
1523
+ "配置 Web 搜索平台名称(留空跳过):",
1524
+ ) or changed
1525
+ changed = _ask_config_optional_str(
1526
+ config_data, ask_all,
1527
+ "JARVIS_WEB_SEARCH_MODEL",
1528
+ "配置 Web 搜索模型名称(留空跳过):",
1529
+ ) or changed
1530
+ return changed
1531
+
1532
+
1533
+ def _ask_git_check_mode(config_data: dict, ask_all: bool) -> bool:
1534
+ """询问Git校验模式"""
1535
+ try:
1536
+ _key = "JARVIS_GIT_CHECK_MODE"
1537
+ if not ask_all and _key in config_data:
1538
+ return False
1539
+ from jarvis.jarvis_utils.input import get_choice
1540
+ from jarvis.jarvis_utils.config import get_git_check_mode
1541
+ current_mode = config_data.get(_key, get_git_check_mode())
1542
+ choices = ["strict", "warn"]
1543
+ tip = (
1544
+ "请选择 Git 仓库检查模式 (JARVIS_GIT_CHECK_MODE):\n"
1545
+ "此设置决定了当在 Git 仓库中检测到未提交的更改时,Jarvis应如何处理。\n"
1546
+ "这对于确保代码修改和提交操作在干净的工作区上进行至关重要。\n"
1547
+ " - strict: (推荐) 如果存在未提交的更改,则中断相关操作(如代码修改、自动提交)。\n"
1548
+ " 这可以防止意外覆盖或丢失本地工作。\n"
1549
+ " - warn: 如果存在未提交的更改,仅显示警告信息,然后继续执行操作。\n"
1550
+ " 适用于您希望绕过检查并自行管理仓库状态的场景。"
1353
1551
  )
1354
- or changed
1355
- )
1552
+ new_mode = get_choice(tip, choices)
1553
+ if new_mode == current_mode:
1554
+ return False
1555
+ config_data[_key] = new_mode
1556
+ return True
1557
+ except Exception:
1558
+ return False
1559
+
1560
+
1561
+ def _collect_git_config(config_data: dict, ask_all: bool) -> bool:
1562
+ """收集Git相关配置"""
1563
+ changed = False
1564
+ changed = _ask_git_check_mode(config_data, ask_all) or changed
1565
+ changed = _ask_config_optional_str(
1566
+ config_data, ask_all,
1567
+ "JARVIS_GIT_COMMIT_PROMPT",
1568
+ "自定义 Git 提交提示模板(留空跳过):",
1569
+ ) or changed
1570
+ return changed
1356
1571
 
1357
- # RAG 配置(可选)
1572
+
1573
+ def _collect_rag_config(config_data: dict, ask_all: bool) -> bool:
1574
+ """收集RAG配置"""
1575
+ changed = False
1358
1576
  try:
1359
1577
  from jarvis.jarvis_utils.config import (
1360
1578
  get_rag_embedding_model as _get_rag_embedding_model,
1361
1579
  get_rag_rerank_model as _get_rag_rerank_model,
1362
1580
  )
1363
-
1581
+ from jarvis.jarvis_utils.input import user_confirm as get_yes_no
1582
+ from jarvis.jarvis_utils.input import get_single_line_input
1583
+
1364
1584
  rag_default_embed = _get_rag_embedding_model()
1365
1585
  rag_default_rerank = _get_rag_rerank_model()
1366
1586
  except Exception:
1367
1587
  rag_default_embed = "BAAI/bge-m3"
1368
1588
  rag_default_rerank = "BAAI/bge-reranker-v2-m3"
1369
-
1589
+ get_yes_no = None
1590
+ get_single_line_input = None
1591
+
1370
1592
  try:
1371
- if "JARVIS_RAG" not in config_data:
1593
+ if "JARVIS_RAG" not in config_data and get_yes_no:
1372
1594
  if get_yes_no("是否配置 RAG 检索增强参数?", default=False):
1373
1595
  rag_conf: Dict[str, Any] = {}
1374
1596
  emb = get_single_line_input(
@@ -1395,49 +1617,74 @@ def _collect_optional_config_interactively(
1395
1617
  changed = True
1396
1618
  except Exception:
1397
1619
  pass
1620
+ return changed
1398
1621
 
1399
- # 中心仓库配置
1400
- changed = (
1401
- _ask_and_set(
1402
- "JARVIS_CENTRAL_METHODOLOGY_REPO",
1403
- "请输入中心方法论仓库地址(可留空跳过):",
1404
- "",
1405
- "str",
1406
- )
1407
- or changed
1408
- )
1409
- changed = (
1410
- _ask_and_set(
1411
- "JARVIS_CENTRAL_TOOL_REPO",
1412
- "请输入中心工具仓库地址(可留空跳过):",
1413
- "",
1414
- "str",
1415
- )
1416
- or changed
1417
- )
1418
-
1419
- # 已移除 LLM 组配置交互
1420
1622
 
1421
- # 已移除 RAG 组配置交互
1623
+ def _collect_central_repo_config(config_data: dict, ask_all: bool) -> bool:
1624
+ """收集中心仓库配置"""
1625
+ changed = False
1626
+ changed = _ask_config_str(
1627
+ config_data, ask_all,
1628
+ "JARVIS_CENTRAL_METHODOLOGY_REPO",
1629
+ "请输入中心方法论仓库路径或Git地址(可留空跳过):",
1630
+ "",
1631
+ ) or changed
1632
+ changed = _ask_config_str(
1633
+ config_data, ask_all,
1634
+ "JARVIS_CENTRAL_TOOL_REPO",
1635
+ "请输入中心工具仓库路径或Git地址(可留空跳过):",
1636
+ "",
1637
+ ) or changed
1638
+ return changed
1422
1639
 
1423
- # 已移除 工具组配置交互
1424
1640
 
1425
- # 已移除:替换映射(JARVIS_REPLACE_MAP)的交互式配置,保持最简交互
1426
- # SHELL 覆盖(可选)
1641
+ def _collect_shell_config(config_data: dict, ask_all: bool) -> bool:
1642
+ """收集SHELL覆盖配置"""
1643
+ changed = False
1427
1644
  try:
1645
+ import os
1428
1646
  default_shell = os.getenv("SHELL", "/bin/bash")
1429
- changed = (
1430
- _ask_and_set_optional_str(
1431
- "SHELL",
1432
- f"覆盖 SHELL 路径(留空使用系统默认: {default_shell}):",
1433
- default_shell,
1434
- )
1435
- or changed
1436
- )
1647
+ changed = _ask_config_optional_str(
1648
+ config_data, ask_all,
1649
+ "SHELL",
1650
+ f"覆盖 SHELL 路径(留空使用系统默认: {default_shell}):",
1651
+ default_shell,
1652
+ ) or changed
1437
1653
  except Exception:
1438
1654
  pass
1655
+ return changed
1656
+
1439
1657
 
1440
- # 已移除:MCP(JARVIS_MCP)的交互式配置,保持最简交互
1658
+ def _collect_optional_config_interactively(
1659
+ config_data: dict, ask_all: bool = False
1660
+ ) -> bool:
1661
+ """
1662
+ 复用的交互式配置收集逻辑:
1663
+ - ask_all=False(默认):仅对缺省的新功能开关/可选项逐项询问,已存在项跳过
1664
+ - ask_all=True:对所有项进行询问,默认值取自当前配置文件,可覆盖现有设置
1665
+ - 修改传入的 config_data
1666
+ - 包含更多来自 config.py 的可选项
1667
+ 返回:
1668
+ bool: 是否有变更
1669
+ """
1670
+ changed = False
1671
+
1672
+ # 收集各类配置
1673
+ changed = _collect_basic_switches(config_data, ask_all) or changed
1674
+ changed = _collect_ui_experience_config(config_data, ask_all) or changed
1675
+ changed = _collect_analysis_config(config_data, ask_all) or changed
1676
+ changed = _collect_agent_features_config(config_data, ask_all) or changed
1677
+ changed = _collect_session_config(config_data, ask_all) or changed
1678
+ changed = _collect_safety_config(config_data, ask_all) or changed
1679
+ changed = _collect_data_and_token_config(config_data, ask_all) or changed
1680
+ changed = _collect_advanced_config(config_data, ask_all) or changed
1681
+ changed = _collect_directory_config(config_data, ask_all) or changed
1682
+ changed = _collect_web_search_config(config_data, ask_all) or changed
1683
+ changed = _collect_git_config(config_data, ask_all) or changed
1684
+ changed = _collect_rag_config(config_data, ask_all) or changed
1685
+ changed = _collect_central_repo_config(config_data, ask_all) or changed
1686
+ changed = _collect_shell_config(config_data, ask_all) or changed
1687
+
1441
1688
  return changed
1442
1689
 
1443
1690
 
@@ -1492,15 +1739,13 @@ def _load_and_process_config(jarvis_dir: str, config_file: str) -> None:
1492
1739
  # 更新全局配置
1493
1740
  set_global_env_data(config_data)
1494
1741
  except Exception:
1495
- PrettyOutput.print("加载配置文件失败", OutputType.ERROR)
1742
+ print("加载配置文件失败")
1496
1743
  if get_yes_no("配置文件格式错误,是否删除并重新配置?"):
1497
1744
  try:
1498
1745
  os.remove(config_file)
1499
- PrettyOutput.print(
1500
- "已删除损坏的配置文件,请重启Jarvis以重新配置。", OutputType.SUCCESS
1501
- )
1746
+ print("✅ 已删除损坏的配置文件,请重启Jarvis以重新配置。")
1502
1747
  except Exception:
1503
- PrettyOutput.print("删除配置文件失败", OutputType.ERROR)
1748
+ print("删除配置文件失败")
1504
1749
  sys.exit(1)
1505
1750
 
1506
1751
 
@@ -1657,69 +1902,113 @@ def _read_old_config_file(config_file):
1657
1902
  {str(k): str(v) for k, v in config_data.items() if v is not None}
1658
1903
  )
1659
1904
  set_global_env_data(config_data)
1660
- PrettyOutput.print(
1661
- "检测到旧格式配置文件,旧格式以后将不再支持,请尽快迁移到新格式",
1662
- OutputType.WARNING,
1663
- )
1905
+ print("⚠️ 检测到旧格式配置文件,旧格式以后将不再支持,请尽快迁移到新格式")
1906
+
1907
+
1908
+ # 线程本地存储,用于共享重试计数器
1909
+ _retry_context = threading.local()
1910
+
1911
+
1912
+ def _get_retry_count() -> int:
1913
+ """获取当前线程的重试计数"""
1914
+ if not hasattr(_retry_context, 'count'):
1915
+ _retry_context.count = 0
1916
+ return _retry_context.count
1917
+
1918
+
1919
+ def _increment_retry_count() -> int:
1920
+ """增加重试计数并返回新的计数值"""
1921
+ if not hasattr(_retry_context, 'count'):
1922
+ _retry_context.count = 0
1923
+ _retry_context.count += 1
1924
+ return _retry_context.count
1925
+
1926
+
1927
+ def _reset_retry_count():
1928
+ """重置重试计数"""
1929
+ _retry_context.count = 0
1664
1930
 
1665
1931
 
1666
- def while_success(func: Callable[[], Any], sleep_time: float = 0.1, max_retries: int = 5) -> Any:
1932
+ def while_success(func: Callable[[], Any]) -> Any:
1667
1933
  """循环执行函数直到成功(累计日志后统一打印,避免逐次加框)
1668
1934
 
1669
1935
  参数:
1670
1936
  func -- 要执行的函数
1671
- sleep_time -- 每次失败后的等待时间(秒)
1672
- max_retries -- 最大重试次数,默认5次
1673
1937
 
1674
1938
  返回:
1675
1939
  函数执行结果
1940
+
1941
+ 注意:
1942
+ 与while_true共享重试计数器,累计重试6次,使用指数退避(第一次等待1s)
1676
1943
  """
1944
+ MAX_RETRIES = 6
1677
1945
  result: Any = None
1678
- retry_count = 0
1679
- while retry_count < max_retries:
1946
+
1947
+ while True:
1680
1948
  try:
1681
1949
  result = func()
1950
+ _reset_retry_count() # 成功后重置计数器
1682
1951
  break
1683
- except Exception:
1684
- retry_count += 1
1685
- if retry_count < max_retries:
1686
- PrettyOutput.print(
1687
- f"发生异常,重试中 ({retry_count}/{max_retries}),等待 {sleep_time}s...",
1688
- OutputType.WARNING,
1689
- )
1690
- time.sleep(sleep_time)
1691
- continue
1952
+ except Exception as e:
1953
+ retry_count = _increment_retry_count()
1954
+ if retry_count <= MAX_RETRIES:
1955
+ # 指数退避:第1次等待1s (2^0),第2次等待2s (2^1),第3次等待4s (2^2),第4次等待8s (2^3),第6次等待32s (2^5)
1956
+ sleep_time = 2 ** (retry_count - 1)
1957
+ if retry_count < MAX_RETRIES:
1958
+ print(f"⚠️ 发生异常:\n{e}\n重试中 ({retry_count}/{MAX_RETRIES}),等待 {sleep_time}s...")
1959
+ time.sleep(sleep_time)
1960
+ else:
1961
+ print(f"⚠️ 发生异常:\n{e}\n已达到最大重试次数 ({retry_count}/{MAX_RETRIES})")
1962
+ _reset_retry_count()
1963
+ raise
1964
+ else:
1965
+ _reset_retry_count()
1966
+ raise
1692
1967
  return result
1693
1968
 
1694
1969
 
1695
- def while_true(func: Callable[[], bool], sleep_time: float = 0.1, max_retries: int = 5) -> Any:
1970
+ def while_true(func: Callable[[], bool]) -> Any:
1696
1971
  """循环执行函数直到返回True(累计日志后统一打印,避免逐次加框)
1697
1972
 
1698
1973
  参数:
1699
1974
  func: 要执行的函数,必须返回布尔值
1700
- sleep_time: 每次失败后的等待时间(秒)
1701
- max_retries: 最大重试次数,默认5次
1702
1975
 
1703
1976
  返回:
1704
1977
  函数最终返回的True值
1705
1978
 
1706
1979
  注意:
1707
1980
  与while_success不同,此函数只检查返回是否为True,
1708
- 不捕获异常,异常会直接抛出
1981
+ 不捕获异常,异常会直接抛出。
1982
+ 与while_success共享重试计数器,累计重试6次,使用指数退避(第一次等待1s)
1709
1983
  """
1984
+ MAX_RETRIES = 6
1710
1985
  ret: bool = False
1711
- retry_count = 0
1712
- while retry_count < max_retries:
1713
- ret = func()
1714
- if ret:
1986
+
1987
+ while True:
1988
+ try:
1989
+ ret = func()
1990
+ if ret:
1991
+ _reset_retry_count() # 成功后重置计数器
1992
+ break
1993
+ except Exception:
1994
+ # 异常直接抛出,不捕获
1995
+ _reset_retry_count()
1996
+ raise
1997
+
1998
+ retry_count = _increment_retry_count()
1999
+ if retry_count <= MAX_RETRIES:
2000
+ # 指数退避:第1次等待1s (2^0),第2次等待2s (2^1),第3次等待4s (2^2),第4次等待8s (2^3),第6次等待32s (2^5)
2001
+ sleep_time = 2 ** (retry_count - 1)
2002
+ if retry_count < MAX_RETRIES:
2003
+ print(f"⚠️ 返回空值,重试中 ({retry_count}/{MAX_RETRIES}),等待 {sleep_time}s...")
2004
+ time.sleep(sleep_time)
2005
+ else:
2006
+ print(f"⚠️ 返回空值,已达到最大重试次数 ({retry_count}/{MAX_RETRIES})")
2007
+ _reset_retry_count()
2008
+ break
2009
+ else:
2010
+ _reset_retry_count()
1715
2011
  break
1716
- retry_count += 1
1717
- if retry_count < max_retries:
1718
- PrettyOutput.print(
1719
- f"返回空值,重试中 ({retry_count}/{max_retries}),等待 {sleep_time}s...",
1720
- OutputType.WARNING,
1721
- )
1722
- time.sleep(sleep_time)
1723
2012
  return ret
1724
2013
 
1725
2014
 
@@ -1730,9 +2019,22 @@ def get_file_md5(filepath: str) -> str:
1730
2019
  filepath: 要计算哈希的文件路径
1731
2020
 
1732
2021
  返回:
1733
- str: 文件内容的MD5哈希值
2022
+ str: 文件内容的MD5哈希值(为降低内存占用,仅读取前100MB进行计算)
1734
2023
  """
1735
- return hashlib.md5(open(filepath, "rb").read(100 * 1024 * 1024)).hexdigest()
2024
+ # 采用流式读取,避免一次性加载100MB到内存
2025
+ h = hashlib.md5()
2026
+ max_bytes = 100 * 1024 * 1024 # 与原实现保持一致:仅读取前100MB
2027
+ buf_size = 8 * 1024 * 1024 # 8MB缓冲
2028
+ read_bytes = 0
2029
+ with open(filepath, "rb") as f:
2030
+ while read_bytes < max_bytes:
2031
+ to_read = min(buf_size, max_bytes - read_bytes)
2032
+ chunk = f.read(to_read)
2033
+ if not chunk:
2034
+ break
2035
+ h.update(chunk)
2036
+ read_bytes += len(chunk)
2037
+ return h.hexdigest()
1736
2038
 
1737
2039
 
1738
2040
  def get_file_line_count(filename: str) -> int:
@@ -1745,7 +2047,9 @@ def get_file_line_count(filename: str) -> int:
1745
2047
  int: 文件中的行数,如果文件无法读取则返回0
1746
2048
  """
1747
2049
  try:
1748
- return len(open(filename, "r", encoding="utf-8", errors="ignore").readlines())
2050
+ # 使用流式逐行计数,避免将整个文件读入内存
2051
+ with open(filename, "r", encoding="utf-8", errors="ignore") as f:
2052
+ return sum(1 for _ in f)
1749
2053
  except Exception:
1750
2054
  return 0
1751
2055
 
@@ -1771,14 +2075,52 @@ def count_cmd_usage() -> None:
1771
2075
 
1772
2076
 
1773
2077
  def is_context_overflow(
1774
- content: str, model_group_override: Optional[str] = None
2078
+ content: str,
2079
+ model_group_override: Optional[str] = None,
2080
+ platform: Optional[Any] = None
1775
2081
  ) -> bool:
1776
- """判断文件内容是否超出上下文限制"""
1777
- return get_context_token_count(content) > get_max_big_content_size(
1778
- model_group_override
1779
- )
1780
-
1781
-
2082
+ """判断文件内容是否超出上下文限制
2083
+
2084
+ 参数:
2085
+ content: 要检查的内容
2086
+ model_group_override: 模型组覆盖(可选)
2087
+ platform: 平台实例(可选),如果提供则使用剩余token数量判断
2088
+
2089
+ 返回:
2090
+ bool: 如果内容超出上下文限制返回True
2091
+ """
2092
+ # 快速长度预估:如果内容长度明显超过限制,直接返回True,无需精确计算token
2093
+ if content:
2094
+ # 粗略估算:假设平均每个token约4个字符,保守估计使用3.5个字符/token
2095
+ estimated_tokens = len(content) // 3.5
2096
+
2097
+ # 获取最大token限制
2098
+ max_tokens = get_max_big_content_size(model_group_override)
2099
+
2100
+ # 如果预估token数超过限制的150%,直接认为超出(避免精确计算)
2101
+ if estimated_tokens > max_tokens * 1.5:
2102
+ return True
2103
+
2104
+ # 如果预估token数小于限制的50%,直接认为安全
2105
+ if estimated_tokens < max_tokens * 0.5:
2106
+ return False
2107
+
2108
+ # 只有在预估结果不明确时,才进行精确的token计算
2109
+ content_tokens = get_context_token_count(content)
2110
+
2111
+ # 优先使用剩余token数量
2112
+ if platform is not None:
2113
+ try:
2114
+ remaining_tokens = platform.get_remaining_token_count()
2115
+ # 如果内容token数超过剩余token的80%,认为超出限制
2116
+ threshold = int(remaining_tokens * 0.8)
2117
+ if threshold > 0:
2118
+ return content_tokens > threshold
2119
+ except Exception:
2120
+ pass
2121
+
2122
+ # 回退方案:使用输入窗口限制
2123
+ return content_tokens > get_max_big_content_size(model_group_override)
1782
2124
  def get_loc_stats() -> str:
1783
2125
  """使用loc命令获取当前目录的代码统计信息
1784
2126
 
@@ -1846,16 +2188,10 @@ def _pull_git_repo(repo_path: Path, repo_type: str):
1846
2188
  subprocess.TimeoutExpired,
1847
2189
  FileNotFoundError,
1848
2190
  ) as e:
1849
- PrettyOutput.print(
1850
- f"放弃 '{repo_path.name}' 的更改失败: {str(e)}",
1851
- OutputType.ERROR,
1852
- )
2191
+ print(f"❌ 放弃 '{repo_path.name}' 的更改失败: {str(e)}")
1853
2192
  return
1854
2193
  else:
1855
- PrettyOutput.print(
1856
- f"跳过更新 '{repo_path.name}' 以保留未提交的更改。",
1857
- OutputType.INFO,
1858
- )
2194
+ print(f"ℹ️ 跳过更新 '{repo_path.name}' 以保留未提交的更改。")
1859
2195
  return
1860
2196
 
1861
2197
  # 获取更新前的commit hash
@@ -1908,25 +2244,17 @@ def _pull_git_repo(repo_path: Path, repo_type: str):
1908
2244
  after_hash = after_hash_result.stdout.strip()
1909
2245
 
1910
2246
  if before_hash != after_hash:
1911
- PrettyOutput.print(
1912
- f"{repo_type}库 '{repo_path.name}' 已更新。", OutputType.SUCCESS
1913
- )
2247
+ print(f"✅ {repo_type}库 '{repo_path.name}' 已更新。")
1914
2248
 
1915
2249
  except FileNotFoundError:
1916
- PrettyOutput.print(
1917
- f"git 命令未找到,跳过更新 '{repo_path.name}'。", OutputType.WARNING
1918
- )
2250
+ print(f"⚠️ git 命令未找到,跳过更新 '{repo_path.name}'。")
1919
2251
  except subprocess.TimeoutExpired:
1920
- PrettyOutput.print(f"更新 '{repo_path.name}' 超时。", OutputType.ERROR)
2252
+ print(f"更新 '{repo_path.name}' 超时。")
1921
2253
  except subprocess.CalledProcessError as e:
1922
2254
  error_message = e.stderr.strip() if e.stderr else str(e)
1923
- PrettyOutput.print(
1924
- f"更新 '{repo_path.name}' 失败: {error_message}", OutputType.ERROR
1925
- )
2255
+ print(f"❌ 更新 '{repo_path.name}' 失败: {error_message}")
1926
2256
  except Exception as e:
1927
- PrettyOutput.print(
1928
- f"更新 '{repo_path.name}' 时发生未知错误: {str(e)}", OutputType.ERROR
1929
- )
2257
+ print(f"❌ 更新 '{repo_path.name}' 时发生未知错误: {str(e)}")
1930
2258
 
1931
2259
 
1932
2260
  def daily_check_git_updates(repo_dirs: List[str], repo_type: str):
@@ -1959,4 +2287,4 @@ def daily_check_git_updates(repo_dirs: List[str], repo_type: str):
1959
2287
  try:
1960
2288
  last_check_file.write_text(str(time.time()))
1961
2289
  except IOError as e:
1962
- PrettyOutput.print(f"无法写入git更新检查时间戳: {e}", OutputType.WARNING)
2290
+ print(f"⚠️ 无法写入git更新检查时间戳: {e}")