jarvis-ai-assistant 0.1.222__py3-none-any.whl → 0.7.0__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 (162) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +1143 -245
  3. jarvis/jarvis_agent/agent_manager.py +97 -0
  4. jarvis/jarvis_agent/builtin_input_handler.py +12 -10
  5. jarvis/jarvis_agent/config_editor.py +57 -0
  6. jarvis/jarvis_agent/edit_file_handler.py +392 -99
  7. jarvis/jarvis_agent/event_bus.py +48 -0
  8. jarvis/jarvis_agent/events.py +157 -0
  9. jarvis/jarvis_agent/file_context_handler.py +79 -0
  10. jarvis/jarvis_agent/file_methodology_manager.py +117 -0
  11. jarvis/jarvis_agent/jarvis.py +1117 -147
  12. jarvis/jarvis_agent/main.py +78 -34
  13. jarvis/jarvis_agent/memory_manager.py +195 -0
  14. jarvis/jarvis_agent/methodology_share_manager.py +174 -0
  15. jarvis/jarvis_agent/prompt_manager.py +82 -0
  16. jarvis/jarvis_agent/prompts.py +46 -9
  17. jarvis/jarvis_agent/protocols.py +4 -1
  18. jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
  19. jarvis/jarvis_agent/run_loop.py +146 -0
  20. jarvis/jarvis_agent/session_manager.py +9 -9
  21. jarvis/jarvis_agent/share_manager.py +228 -0
  22. jarvis/jarvis_agent/shell_input_handler.py +23 -3
  23. jarvis/jarvis_agent/stdio_redirect.py +295 -0
  24. jarvis/jarvis_agent/task_analyzer.py +212 -0
  25. jarvis/jarvis_agent/task_manager.py +154 -0
  26. jarvis/jarvis_agent/task_planner.py +496 -0
  27. jarvis/jarvis_agent/tool_executor.py +8 -4
  28. jarvis/jarvis_agent/tool_share_manager.py +139 -0
  29. jarvis/jarvis_agent/user_interaction.py +42 -0
  30. jarvis/jarvis_agent/utils.py +54 -0
  31. jarvis/jarvis_agent/web_bridge.py +189 -0
  32. jarvis/jarvis_agent/web_output_sink.py +53 -0
  33. jarvis/jarvis_agent/web_server.py +751 -0
  34. jarvis/jarvis_c2rust/__init__.py +26 -0
  35. jarvis/jarvis_c2rust/cli.py +613 -0
  36. jarvis/jarvis_c2rust/collector.py +258 -0
  37. jarvis/jarvis_c2rust/library_replacer.py +1122 -0
  38. jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
  39. jarvis/jarvis_c2rust/optimizer.py +960 -0
  40. jarvis/jarvis_c2rust/scanner.py +1681 -0
  41. jarvis/jarvis_c2rust/transpiler.py +2325 -0
  42. jarvis/jarvis_code_agent/build_validation_config.py +133 -0
  43. jarvis/jarvis_code_agent/code_agent.py +1605 -178
  44. jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
  45. jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
  46. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
  47. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
  48. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
  49. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
  50. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
  51. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
  52. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
  53. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
  54. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
  55. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
  56. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
  57. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
  58. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
  59. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
  60. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
  61. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
  62. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
  63. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
  64. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
  65. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
  66. jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
  67. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
  68. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
  69. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
  70. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
  71. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
  72. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
  73. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
  74. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
  75. jarvis/jarvis_code_agent/lint.py +275 -13
  76. jarvis/jarvis_code_agent/utils.py +142 -0
  77. jarvis/jarvis_code_analysis/checklists/loader.py +20 -6
  78. jarvis/jarvis_code_analysis/code_review.py +583 -548
  79. jarvis/jarvis_data/config_schema.json +339 -28
  80. jarvis/jarvis_git_squash/main.py +22 -13
  81. jarvis/jarvis_git_utils/git_commiter.py +171 -55
  82. jarvis/jarvis_mcp/sse_mcp_client.py +22 -15
  83. jarvis/jarvis_mcp/stdio_mcp_client.py +4 -4
  84. jarvis/jarvis_mcp/streamable_mcp_client.py +36 -16
  85. jarvis/jarvis_memory_organizer/memory_organizer.py +753 -0
  86. jarvis/jarvis_methodology/main.py +48 -63
  87. jarvis/jarvis_multi_agent/__init__.py +302 -43
  88. jarvis/jarvis_multi_agent/main.py +70 -24
  89. jarvis/jarvis_platform/ai8.py +40 -23
  90. jarvis/jarvis_platform/base.py +210 -49
  91. jarvis/jarvis_platform/human.py +11 -1
  92. jarvis/jarvis_platform/kimi.py +82 -76
  93. jarvis/jarvis_platform/openai.py +73 -1
  94. jarvis/jarvis_platform/registry.py +8 -15
  95. jarvis/jarvis_platform/tongyi.py +115 -101
  96. jarvis/jarvis_platform/yuanbao.py +89 -63
  97. jarvis/jarvis_platform_manager/main.py +194 -132
  98. jarvis/jarvis_platform_manager/service.py +122 -86
  99. jarvis/jarvis_rag/cli.py +156 -53
  100. jarvis/jarvis_rag/embedding_manager.py +155 -12
  101. jarvis/jarvis_rag/llm_interface.py +10 -13
  102. jarvis/jarvis_rag/query_rewriter.py +63 -12
  103. jarvis/jarvis_rag/rag_pipeline.py +222 -40
  104. jarvis/jarvis_rag/reranker.py +26 -3
  105. jarvis/jarvis_rag/retriever.py +270 -14
  106. jarvis/jarvis_sec/__init__.py +3605 -0
  107. jarvis/jarvis_sec/checkers/__init__.py +32 -0
  108. jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
  109. jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
  110. jarvis/jarvis_sec/cli.py +116 -0
  111. jarvis/jarvis_sec/report.py +257 -0
  112. jarvis/jarvis_sec/status.py +264 -0
  113. jarvis/jarvis_sec/types.py +20 -0
  114. jarvis/jarvis_sec/workflow.py +219 -0
  115. jarvis/jarvis_smart_shell/main.py +405 -137
  116. jarvis/jarvis_stats/__init__.py +13 -0
  117. jarvis/jarvis_stats/cli.py +387 -0
  118. jarvis/jarvis_stats/stats.py +711 -0
  119. jarvis/jarvis_stats/storage.py +612 -0
  120. jarvis/jarvis_stats/visualizer.py +282 -0
  121. jarvis/jarvis_tools/ask_user.py +1 -0
  122. jarvis/jarvis_tools/base.py +18 -2
  123. jarvis/jarvis_tools/clear_memory.py +239 -0
  124. jarvis/jarvis_tools/cli/main.py +220 -144
  125. jarvis/jarvis_tools/execute_script.py +52 -12
  126. jarvis/jarvis_tools/file_analyzer.py +17 -12
  127. jarvis/jarvis_tools/generate_new_tool.py +46 -24
  128. jarvis/jarvis_tools/read_code.py +277 -18
  129. jarvis/jarvis_tools/read_symbols.py +141 -0
  130. jarvis/jarvis_tools/read_webpage.py +86 -13
  131. jarvis/jarvis_tools/registry.py +294 -90
  132. jarvis/jarvis_tools/retrieve_memory.py +227 -0
  133. jarvis/jarvis_tools/save_memory.py +194 -0
  134. jarvis/jarvis_tools/search_web.py +62 -28
  135. jarvis/jarvis_tools/sub_agent.py +205 -0
  136. jarvis/jarvis_tools/sub_code_agent.py +217 -0
  137. jarvis/jarvis_tools/virtual_tty.py +330 -62
  138. jarvis/jarvis_utils/builtin_replace_map.py +4 -5
  139. jarvis/jarvis_utils/clipboard.py +90 -0
  140. jarvis/jarvis_utils/config.py +607 -50
  141. jarvis/jarvis_utils/embedding.py +3 -0
  142. jarvis/jarvis_utils/fzf.py +57 -0
  143. jarvis/jarvis_utils/git_utils.py +251 -29
  144. jarvis/jarvis_utils/globals.py +174 -17
  145. jarvis/jarvis_utils/http.py +58 -79
  146. jarvis/jarvis_utils/input.py +899 -153
  147. jarvis/jarvis_utils/methodology.py +210 -83
  148. jarvis/jarvis_utils/output.py +220 -137
  149. jarvis/jarvis_utils/utils.py +1906 -135
  150. jarvis_ai_assistant-0.7.0.dist-info/METADATA +465 -0
  151. jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
  152. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +8 -2
  153. jarvis/jarvis_git_details/main.py +0 -265
  154. jarvis/jarvis_platform/oyi.py +0 -357
  155. jarvis/jarvis_tools/edit_file.py +0 -255
  156. jarvis/jarvis_tools/rewrite_file.py +0 -195
  157. jarvis_ai_assistant-0.1.222.dist-info/METADATA +0 -767
  158. jarvis_ai_assistant-0.1.222.dist-info/RECORD +0 -110
  159. /jarvis/{jarvis_git_details → jarvis_memory_organizer}/__init__.py +0 -0
  160. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
  161. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
  162. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
@@ -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
 
@@ -0,0 +1,57 @@
1
+ # -*- coding: utf-8 -*-
2
+ """FZF selector utility."""
3
+ import shutil
4
+ import subprocess
5
+ from typing import List, Optional, Union, Dict, Any, cast
6
+
7
+ def fzf_select(
8
+ options: Union[List[str], List[Dict[str, Any]]],
9
+ prompt: str = "SELECT> ",
10
+ key: Optional[str] = None,
11
+ ) -> Optional[str]:
12
+ """
13
+ Uses fzf to select an item from a list.
14
+
15
+ Args:
16
+ options: A list of strings or dicts to choose from.
17
+ prompt: The prompt to display in fzf.
18
+ key: If options is a list of dicts, this is the key to display.
19
+
20
+ Returns:
21
+ The selected item, or None if fzf is not available or the selection is cancelled.
22
+ """
23
+ if shutil.which("fzf") is None:
24
+ return None
25
+
26
+ if not options:
27
+ return None
28
+
29
+ if isinstance(options[0], dict):
30
+ if key is None:
31
+ raise ValueError("A key must be provided for a list of dicts.")
32
+ options_dict = cast(List[Dict[str, Any]], options)
33
+ input_lines = [str(item.get(key, "")) for item in options_dict]
34
+ else:
35
+ input_lines = [str(item) for item in options]
36
+
37
+ try:
38
+ process = subprocess.run(
39
+ [
40
+ "fzf",
41
+ "--prompt",
42
+ prompt,
43
+ "--height",
44
+ "40%",
45
+ "--border",
46
+ "--layout=reverse",
47
+ ],
48
+ input="\n".join(input_lines),
49
+ stdout=subprocess.PIPE,
50
+ stderr=subprocess.PIPE,
51
+ text=True,
52
+ check=True,
53
+ )
54
+ selected = process.stdout.strip()
55
+ return selected if selected else None
56
+ except (subprocess.CalledProcessError, FileNotFoundError):
57
+ return None
@@ -14,11 +14,12 @@ import os
14
14
  import re
15
15
  import subprocess
16
16
  import sys
17
- from typing import Any, Dict, List, Set, Tuple
17
+ from typing import Any, Dict, List, Optional, Set, Tuple
18
18
 
19
- from jarvis.jarvis_utils.config import is_confirm_before_apply_patch
19
+ from jarvis.jarvis_utils.config import get_data_dir, is_confirm_before_apply_patch
20
20
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
21
21
  from jarvis.jarvis_utils.input import user_confirm
22
+ from jarvis.jarvis_utils.utils import is_rag_installed
22
23
 
23
24
 
24
25
  def find_git_root_and_cd(start_dir: str = ".") -> str:
@@ -33,7 +34,13 @@ def find_git_root_and_cd(start_dir: str = ".") -> str:
33
34
  """
34
35
  os.chdir(start_dir)
35
36
  try:
36
- 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()
37
44
  if not git_root:
38
45
  subprocess.run(["git", "init"], check=True)
39
46
  git_root = os.path.abspath(".")
@@ -208,6 +215,126 @@ def revert_change() -> None:
208
215
  PrettyOutput.print(f"恢复更改失败: {str(e)}", OutputType.ERROR)
209
216
 
210
217
 
218
+ def detect_large_code_deletion(threshold: int = 200) -> Optional[Dict[str, int]]:
219
+ """检测是否有大量代码删除
220
+
221
+ 参数:
222
+ threshold: 净删除行数阈值,默认200行
223
+
224
+ 返回:
225
+ Optional[Dict[str, int]]: 如果检测到大量删除,返回包含统计信息的字典:
226
+ {
227
+ 'insertions': int, # 新增行数
228
+ 'deletions': int, # 删除行数
229
+ 'net_deletions': int # 净删除行数
230
+ }
231
+ 如果没有大量删除或发生错误,返回None
232
+ """
233
+ try:
234
+ # 临时暂存所有文件以便获取完整的diff统计
235
+ subprocess.run(["git", "add", "-N", "."], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
236
+
237
+ # 检查是否有HEAD
238
+ head_check = subprocess.run(
239
+ ["git", "rev-parse", "--verify", "HEAD"],
240
+ stderr=subprocess.DEVNULL,
241
+ stdout=subprocess.DEVNULL,
242
+ )
243
+
244
+ if head_check.returncode == 0:
245
+ # 有HEAD,获取相对于HEAD的diff统计
246
+ diff_result = subprocess.run(
247
+ ["git", "diff", "HEAD", "--shortstat"],
248
+ capture_output=True,
249
+ text=True,
250
+ encoding="utf-8",
251
+ errors="replace",
252
+ check=False,
253
+ )
254
+ else:
255
+ # 空仓库,获取工作区diff统计
256
+ diff_result = subprocess.run(
257
+ ["git", "diff", "--shortstat"],
258
+ capture_output=True,
259
+ text=True,
260
+ encoding="utf-8",
261
+ errors="replace",
262
+ check=False,
263
+ )
264
+
265
+ # 重置暂存区
266
+ subprocess.run(["git", "reset"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
267
+
268
+ # 解析插入和删除行数
269
+ if diff_result.returncode == 0 and diff_result.stdout:
270
+ insertions = 0
271
+ deletions = 0
272
+ insertions_match = re.search(r"(\d+)\s+insertions?\(\+\)", diff_result.stdout)
273
+ deletions_match = re.search(r"(\d+)\s+deletions?\(\-\)", diff_result.stdout)
274
+ if insertions_match:
275
+ insertions = int(insertions_match.group(1))
276
+ if deletions_match:
277
+ deletions = int(deletions_match.group(1))
278
+
279
+ # 检查是否有大量代码删除(净删除超过阈值)
280
+ net_deletions = deletions - insertions
281
+ if net_deletions > threshold:
282
+ return {
283
+ 'insertions': insertions,
284
+ 'deletions': deletions,
285
+ 'net_deletions': net_deletions
286
+ }
287
+ return None
288
+ except Exception:
289
+ # 如果检查过程中出错,返回None
290
+ return None
291
+
292
+
293
+ def confirm_large_code_deletion(detection_result: Dict[str, int]) -> bool:
294
+ """询问用户是否确认大量代码删除
295
+
296
+ 参数:
297
+ detection_result: 检测结果字典,包含 'insertions', 'deletions', 'net_deletions'
298
+
299
+ 返回:
300
+ bool: 如果用户确认,返回True;如果用户拒绝,返回False
301
+ """
302
+ insertions = detection_result['insertions']
303
+ deletions = detection_result['deletions']
304
+ net_deletions = detection_result['net_deletions']
305
+
306
+ PrettyOutput.print(
307
+ f"⚠️ 检测到大量代码删除:净删除 {net_deletions} 行(删除 {deletions} 行,新增 {insertions} 行)",
308
+ OutputType.WARNING,
309
+ )
310
+ if not user_confirm(
311
+ "此补丁包含大量代码删除,是否合理?", default=True
312
+ ):
313
+ # 用户认为不合理,拒绝提交
314
+ revert_change()
315
+ PrettyOutput.print(
316
+ "已拒绝本次提交(用户认为补丁不合理)", OutputType.INFO
317
+ )
318
+ return False
319
+ return True
320
+
321
+
322
+ def check_large_code_deletion(threshold: int = 200) -> bool:
323
+ """检查是否有大量代码删除并询问用户确认
324
+
325
+ 参数:
326
+ threshold: 净删除行数阈值,默认200行
327
+
328
+ 返回:
329
+ bool: 如果检测到大量删除且用户拒绝提交,返回False;否则返回True
330
+ """
331
+ detection_result = detect_large_code_deletion(threshold)
332
+ if detection_result is None:
333
+ return True
334
+
335
+ return confirm_large_code_deletion(detection_result)
336
+
337
+
211
338
  def handle_commit_workflow() -> bool:
212
339
  """Handle the git commit workflow and return the commit details.
213
340
 
@@ -228,6 +355,10 @@ def handle_commit_workflow() -> bool:
228
355
  if not has_uncommitted_changes():
229
356
  return False
230
357
 
358
+ # 在提交前检查是否有大量代码删除
359
+ if not check_large_code_deletion():
360
+ return False
361
+
231
362
  # 获取当前分支的提交总数
232
363
  commit_result = subprocess.run(
233
364
  ["git", "rev-list", "--count", "HEAD"], capture_output=True, text=True
@@ -245,7 +376,7 @@ def handle_commit_workflow() -> bool:
245
376
  ["git", "commit", "-m", f"CheckPoint #{commit_count + 1}"], check=True
246
377
  )
247
378
  return True
248
- except subprocess.CalledProcessError as e:
379
+ except subprocess.CalledProcessError:
249
380
  return False
250
381
 
251
382
 
@@ -282,7 +413,7 @@ def get_latest_commit_hash() -> str:
282
413
  return ""
283
414
 
284
415
 
285
- def get_modified_line_ranges() -> Dict[str, Tuple[int, int]]:
416
+ def get_modified_line_ranges() -> Dict[str, List[Tuple[int, int]]]:
286
417
  """从Git差异中获取所有更改文件的修改行范围
287
418
 
288
419
  返回:
@@ -290,10 +421,16 @@ def get_modified_line_ranges() -> Dict[str, Tuple[int, int]]:
290
421
  行号从1开始。
291
422
  """
292
423
  # 获取所有文件的Git差异
293
- diff_output = os.popen("git show").read()
424
+ # 仅用于解析修改行范围,减少上下文以降低输出体积和解析成本
425
+ proc = subprocess.run(
426
+ ["git", "show", "--no-color"],
427
+ capture_output=True,
428
+ text=True,
429
+ )
430
+ diff_output = proc.stdout
294
431
 
295
432
  # 解析差异以获取修改的文件及其行范围
296
- result = {}
433
+ result: Dict[str, List[Tuple[int, int]]] = {}
297
434
  current_file = None
298
435
 
299
436
  for line in diff_output.splitlines():
@@ -309,7 +446,9 @@ def get_modified_line_ranges() -> Dict[str, Tuple[int, int]]:
309
446
  start_line = int(range_match.group(1)) # 保持从1开始
310
447
  line_count = int(range_match.group(2)) if range_match.group(2) else 1
311
448
  end_line = start_line + line_count - 1
312
- result[current_file] = (start_line, end_line)
449
+ if current_file not in result:
450
+ result[current_file] = []
451
+ result[current_file].append((start_line, end_line))
313
452
 
314
453
  return result
315
454
 
@@ -326,7 +465,7 @@ def is_file_in_git_repo(filepath: str) -> bool:
326
465
 
327
466
  # 检查文件路径是否在仓库根目录下
328
467
  return os.path.abspath(filepath).startswith(os.path.abspath(repo_root))
329
- except:
468
+ except Exception:
330
469
  return False
331
470
 
332
471
 
@@ -339,25 +478,21 @@ def check_and_update_git_repo(repo_path: str) -> bool:
339
478
  返回:
340
479
  bool: 是否执行了更新
341
480
  """
481
+ # 检查上次检查日期
482
+ last_check_file = os.path.join(get_data_dir(), "last_git_check")
483
+ today_str = datetime.date.today().strftime("%Y-%m-%d")
484
+ if os.path.exists(last_check_file):
485
+ with open(last_check_file, "r") as f:
486
+ last_check_date = f.read().strip()
487
+ if last_check_date == today_str:
488
+ return False
489
+
342
490
  curr_dir = os.path.abspath(os.getcwd())
343
491
  git_root = find_git_root_and_cd(repo_path)
344
492
  if git_root is None:
345
493
  return False
346
494
 
347
495
  try:
348
- # 检查最新提交时间是否为今天
349
- commit_date_result = subprocess.run(
350
- ["git", "log", "-1", "--format=%cd", "--date=short"],
351
- cwd=git_root,
352
- capture_output=True,
353
- text=True,
354
- )
355
- if commit_date_result.returncode == 0:
356
- commit_date = commit_date_result.stdout.strip()
357
- today = datetime.date.today().strftime("%Y-%m-%d")
358
- if commit_date == today:
359
- return False
360
-
361
496
  # 检查是否有未提交的修改
362
497
  if has_uncommitted_changes():
363
498
  return False
@@ -418,8 +553,51 @@ def check_and_update_git_repo(repo_path: str) -> bool:
418
553
  hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
419
554
  )
420
555
 
421
- # 尝试普通安装
422
- install_cmd = [sys.executable, "-m", "pip", "install", "-e", "."]
556
+ # 检测 uv 可用性:优先虚拟环境内的 uv,其次 PATH 中的 uv
557
+ from shutil import which as _which
558
+ uv_executable = None
559
+ if sys.platform == "win32":
560
+ venv_uv = os.path.join(sys.prefix, "Scripts", "uv.exe")
561
+ else:
562
+ venv_uv = os.path.join(sys.prefix, "bin", "uv")
563
+ if os.path.exists(venv_uv):
564
+ uv_executable = venv_uv
565
+ else:
566
+ path_uv = _which("uv")
567
+ if path_uv:
568
+ uv_executable = path_uv
569
+
570
+ # 根据环境选择安装命令
571
+ # 检测是否安装了 RAG 特性(更精确)
572
+ rag_installed = is_rag_installed()
573
+
574
+ # 根据 uv 可用性与 RAG 特性选择安装命令(优先使用 uv)
575
+ if uv_executable:
576
+ if rag_installed:
577
+ install_cmd = [uv_executable, "pip", "install", "-e", ".[rag]"]
578
+ else:
579
+ install_cmd = [uv_executable, "pip", "install", "-e", "."]
580
+ else:
581
+ if rag_installed:
582
+ install_cmd = [
583
+ sys.executable,
584
+ "-m",
585
+ "pip",
586
+ "install",
587
+ "-e",
588
+ ".[rag]",
589
+ ]
590
+ else:
591
+ install_cmd = [
592
+ sys.executable,
593
+ "-m",
594
+ "pip",
595
+ "install",
596
+ "-e",
597
+ ".",
598
+ ]
599
+
600
+ # 尝试安装
423
601
  result = subprocess.run(
424
602
  install_cmd, cwd=git_root, capture_output=True, text=True
425
603
  )
@@ -454,6 +632,9 @@ def check_and_update_git_repo(repo_path: str) -> bool:
454
632
  f"安装过程中发生意外错误: {str(e)}", OutputType.ERROR
455
633
  )
456
634
  return False
635
+ # 更新检查日期文件
636
+ with open(last_check_file, "w") as f:
637
+ f.write(today_str)
457
638
  return False
458
639
  except Exception as e:
459
640
  PrettyOutput.print(f"Git仓库更新检查失败: {e}", OutputType.WARNING)
@@ -534,17 +715,19 @@ def get_recent_commits_with_files() -> List[Dict[str, Any]]:
534
715
  ],
535
716
  capture_output=True,
536
717
  text=True,
718
+ encoding="utf-8",
719
+ errors="replace",
537
720
  )
538
- if result.returncode != 0:
721
+ if result.returncode != 0 or result.stdout is None:
539
722
  return []
540
723
 
541
724
  # 解析提交信息
542
- commits = []
725
+ commits: List[Dict[str, Any]] = []
543
726
  lines = result.stdout.splitlines()
544
727
  for i in range(0, len(lines), 4):
545
728
  if i + 3 >= len(lines):
546
729
  break
547
- commit = {
730
+ commit: Dict[str, Any] = {
548
731
  "hash": lines[i],
549
732
  "message": lines[i + 1],
550
733
  "author": lines[i + 2],
@@ -563,7 +746,7 @@ def get_recent_commits_with_files() -> List[Dict[str, Any]]:
563
746
  if files_result.returncode == 0:
564
747
  file_lines = files_result.stdout.splitlines()
565
748
  unique_files: Set[str] = set(filter(None, file_lines))
566
- commit["files"] = list(unique_files)[:20] # type: ignore[list-item] # 限制最多20个文件
749
+ commit["files"] = list(unique_files)[:20] # 限制最多20个文件
567
750
 
568
751
  return commits
569
752
 
@@ -658,8 +841,47 @@ def confirm_add_new_files() -> None:
658
841
 
659
842
  if not user_confirm(
660
843
  "是否要添加这些变更(如果不需要请修改.gitignore文件以忽略不需要的文件)?",
661
- False,
844
+ True,
662
845
  ):
846
+ # 用户选择 N:自动将未跟踪文件列表添加到仓库根目录的 .gitignore
847
+ try:
848
+ repo_root_result = subprocess.run(
849
+ ["git", "rev-parse", "--show-toplevel"],
850
+ capture_output=True,
851
+ text=True,
852
+ check=True,
853
+ )
854
+ repo_root = repo_root_result.stdout.strip() or "."
855
+ except Exception:
856
+ repo_root = "."
857
+ gitignore_path = os.path.join(repo_root, ".gitignore")
858
+
859
+ # 仅对未跟踪的新文件进行忽略(已跟踪文件无法通过 .gitignore 忽略)
860
+ files_to_ignore = sorted(set(new_files))
861
+
862
+ # 读取已存在的 .gitignore 以避免重复添加
863
+ existing_lines: Set[str] = set()
864
+ try:
865
+ if os.path.exists(gitignore_path):
866
+ with open(gitignore_path, "r", encoding="utf-8") as f:
867
+ existing_lines = set(line.strip() for line in f if line.strip())
868
+ except Exception:
869
+ existing_lines = set()
870
+
871
+ # 追加未存在的文件路径到 .gitignore(使用相对于仓库根目录的路径)
872
+ try:
873
+ with open(gitignore_path, "a", encoding="utf-8") as f:
874
+ for file in files_to_ignore:
875
+ abs_path = os.path.abspath(file)
876
+ rel_path = os.path.relpath(abs_path, repo_root)
877
+ # 避免无效的相对路径(不应出现 .. 前缀),有则回退用原始值
878
+ entry = rel_path if not rel_path.startswith("..") else file
879
+ if entry not in existing_lines:
880
+ f.write(entry + "\n")
881
+ PrettyOutput.print("已将未跟踪文件添加到 .gitignore,正在重新检测...", OutputType.INFO)
882
+ except Exception as e:
883
+ PrettyOutput.print(f"更新 .gitignore 失败: {str(e)}", OutputType.WARNING)
884
+
663
885
  continue
664
886
 
665
887
  break