jarvis-ai-assistant 0.7.8__py3-none-any.whl → 1.0.2__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 (279) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +567 -222
  3. jarvis/jarvis_agent/agent_manager.py +19 -12
  4. jarvis/jarvis_agent/builtin_input_handler.py +79 -11
  5. jarvis/jarvis_agent/config_editor.py +7 -2
  6. jarvis/jarvis_agent/event_bus.py +24 -13
  7. jarvis/jarvis_agent/events.py +19 -1
  8. jarvis/jarvis_agent/file_context_handler.py +67 -64
  9. jarvis/jarvis_agent/file_methodology_manager.py +38 -24
  10. jarvis/jarvis_agent/jarvis.py +186 -114
  11. jarvis/jarvis_agent/language_extractors/__init__.py +8 -1
  12. jarvis/jarvis_agent/language_extractors/c_extractor.py +7 -4
  13. jarvis/jarvis_agent/language_extractors/cpp_extractor.py +9 -4
  14. jarvis/jarvis_agent/language_extractors/go_extractor.py +7 -4
  15. jarvis/jarvis_agent/language_extractors/java_extractor.py +27 -20
  16. jarvis/jarvis_agent/language_extractors/javascript_extractor.py +22 -17
  17. jarvis/jarvis_agent/language_extractors/python_extractor.py +7 -4
  18. jarvis/jarvis_agent/language_extractors/rust_extractor.py +7 -4
  19. jarvis/jarvis_agent/language_extractors/typescript_extractor.py +22 -17
  20. jarvis/jarvis_agent/language_support_info.py +250 -219
  21. jarvis/jarvis_agent/main.py +19 -23
  22. jarvis/jarvis_agent/memory_manager.py +9 -6
  23. jarvis/jarvis_agent/methodology_share_manager.py +21 -15
  24. jarvis/jarvis_agent/output_handler.py +4 -2
  25. jarvis/jarvis_agent/prompt_builder.py +7 -6
  26. jarvis/jarvis_agent/prompt_manager.py +113 -8
  27. jarvis/jarvis_agent/prompts.py +317 -85
  28. jarvis/jarvis_agent/protocols.py +5 -2
  29. jarvis/jarvis_agent/run_loop.py +192 -32
  30. jarvis/jarvis_agent/session_manager.py +7 -3
  31. jarvis/jarvis_agent/share_manager.py +23 -13
  32. jarvis/jarvis_agent/shell_input_handler.py +12 -8
  33. jarvis/jarvis_agent/stdio_redirect.py +25 -26
  34. jarvis/jarvis_agent/task_analyzer.py +29 -23
  35. jarvis/jarvis_agent/task_list.py +869 -0
  36. jarvis/jarvis_agent/task_manager.py +26 -23
  37. jarvis/jarvis_agent/tool_executor.py +6 -5
  38. jarvis/jarvis_agent/tool_share_manager.py +24 -14
  39. jarvis/jarvis_agent/user_interaction.py +3 -3
  40. jarvis/jarvis_agent/utils.py +9 -1
  41. jarvis/jarvis_agent/web_bridge.py +37 -17
  42. jarvis/jarvis_agent/web_output_sink.py +5 -2
  43. jarvis/jarvis_agent/web_server.py +165 -36
  44. jarvis/jarvis_c2rust/__init__.py +1 -1
  45. jarvis/jarvis_c2rust/cli.py +260 -141
  46. jarvis/jarvis_c2rust/collector.py +37 -18
  47. jarvis/jarvis_c2rust/constants.py +60 -0
  48. jarvis/jarvis_c2rust/library_replacer.py +242 -1010
  49. jarvis/jarvis_c2rust/library_replacer_checkpoint.py +133 -0
  50. jarvis/jarvis_c2rust/library_replacer_llm.py +287 -0
  51. jarvis/jarvis_c2rust/library_replacer_loader.py +191 -0
  52. jarvis/jarvis_c2rust/library_replacer_output.py +134 -0
  53. jarvis/jarvis_c2rust/library_replacer_prompts.py +124 -0
  54. jarvis/jarvis_c2rust/library_replacer_utils.py +188 -0
  55. jarvis/jarvis_c2rust/llm_module_agent.py +98 -1044
  56. jarvis/jarvis_c2rust/llm_module_agent_apply.py +170 -0
  57. jarvis/jarvis_c2rust/llm_module_agent_executor.py +288 -0
  58. jarvis/jarvis_c2rust/llm_module_agent_loader.py +170 -0
  59. jarvis/jarvis_c2rust/llm_module_agent_prompts.py +268 -0
  60. jarvis/jarvis_c2rust/llm_module_agent_types.py +57 -0
  61. jarvis/jarvis_c2rust/llm_module_agent_utils.py +150 -0
  62. jarvis/jarvis_c2rust/llm_module_agent_validator.py +119 -0
  63. jarvis/jarvis_c2rust/loaders.py +28 -10
  64. jarvis/jarvis_c2rust/models.py +5 -2
  65. jarvis/jarvis_c2rust/optimizer.py +192 -1974
  66. jarvis/jarvis_c2rust/optimizer_build_fix.py +286 -0
  67. jarvis/jarvis_c2rust/optimizer_clippy.py +766 -0
  68. jarvis/jarvis_c2rust/optimizer_config.py +49 -0
  69. jarvis/jarvis_c2rust/optimizer_docs.py +183 -0
  70. jarvis/jarvis_c2rust/optimizer_options.py +48 -0
  71. jarvis/jarvis_c2rust/optimizer_progress.py +469 -0
  72. jarvis/jarvis_c2rust/optimizer_report.py +52 -0
  73. jarvis/jarvis_c2rust/optimizer_unsafe.py +309 -0
  74. jarvis/jarvis_c2rust/optimizer_utils.py +469 -0
  75. jarvis/jarvis_c2rust/optimizer_visibility.py +185 -0
  76. jarvis/jarvis_c2rust/scanner.py +229 -166
  77. jarvis/jarvis_c2rust/transpiler.py +531 -2732
  78. jarvis/jarvis_c2rust/transpiler_agents.py +503 -0
  79. jarvis/jarvis_c2rust/transpiler_build.py +1294 -0
  80. jarvis/jarvis_c2rust/transpiler_codegen.py +204 -0
  81. jarvis/jarvis_c2rust/transpiler_compile.py +146 -0
  82. jarvis/jarvis_c2rust/transpiler_config.py +178 -0
  83. jarvis/jarvis_c2rust/transpiler_context.py +122 -0
  84. jarvis/jarvis_c2rust/transpiler_executor.py +516 -0
  85. jarvis/jarvis_c2rust/transpiler_generation.py +278 -0
  86. jarvis/jarvis_c2rust/transpiler_git.py +163 -0
  87. jarvis/jarvis_c2rust/transpiler_mod_utils.py +225 -0
  88. jarvis/jarvis_c2rust/transpiler_modules.py +336 -0
  89. jarvis/jarvis_c2rust/transpiler_planning.py +394 -0
  90. jarvis/jarvis_c2rust/transpiler_review.py +1196 -0
  91. jarvis/jarvis_c2rust/transpiler_symbols.py +176 -0
  92. jarvis/jarvis_c2rust/utils.py +269 -79
  93. jarvis/jarvis_code_agent/after_change.py +233 -0
  94. jarvis/jarvis_code_agent/build_validation_config.py +37 -30
  95. jarvis/jarvis_code_agent/builtin_rules.py +68 -0
  96. jarvis/jarvis_code_agent/code_agent.py +976 -1517
  97. jarvis/jarvis_code_agent/code_agent_build.py +227 -0
  98. jarvis/jarvis_code_agent/code_agent_diff.py +246 -0
  99. jarvis/jarvis_code_agent/code_agent_git.py +525 -0
  100. jarvis/jarvis_code_agent/code_agent_impact.py +177 -0
  101. jarvis/jarvis_code_agent/code_agent_lint.py +283 -0
  102. jarvis/jarvis_code_agent/code_agent_llm.py +159 -0
  103. jarvis/jarvis_code_agent/code_agent_postprocess.py +105 -0
  104. jarvis/jarvis_code_agent/code_agent_prompts.py +46 -0
  105. jarvis/jarvis_code_agent/code_agent_rules.py +305 -0
  106. jarvis/jarvis_code_agent/code_analyzer/__init__.py +52 -48
  107. jarvis/jarvis_code_agent/code_analyzer/base_language.py +12 -10
  108. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +12 -11
  109. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +16 -12
  110. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +26 -17
  111. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +558 -104
  112. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +27 -16
  113. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +22 -18
  114. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +21 -16
  115. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +20 -16
  116. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +27 -16
  117. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +47 -23
  118. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +71 -37
  119. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +162 -35
  120. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +111 -57
  121. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +18 -12
  122. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +185 -183
  123. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +2 -1
  124. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +24 -15
  125. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +227 -141
  126. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +321 -247
  127. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +37 -29
  128. jarvis/jarvis_code_agent/code_analyzer/language_support.py +21 -13
  129. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +15 -9
  130. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +75 -45
  131. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +87 -52
  132. jarvis/jarvis_code_agent/code_analyzer/languages/java_language.py +84 -51
  133. jarvis/jarvis_code_agent/code_analyzer/languages/javascript_language.py +94 -64
  134. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +109 -71
  135. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +97 -63
  136. jarvis/jarvis_code_agent/code_analyzer/languages/typescript_language.py +103 -69
  137. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +271 -268
  138. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +76 -64
  139. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +92 -19
  140. jarvis/jarvis_code_agent/diff_visualizer.py +998 -0
  141. jarvis/jarvis_code_agent/lint.py +223 -524
  142. jarvis/jarvis_code_agent/rule_share_manager.py +158 -0
  143. jarvis/jarvis_code_agent/rules/clean_code.md +144 -0
  144. jarvis/jarvis_code_agent/rules/code_review.md +115 -0
  145. jarvis/jarvis_code_agent/rules/documentation.md +165 -0
  146. jarvis/jarvis_code_agent/rules/generate_rules.md +52 -0
  147. jarvis/jarvis_code_agent/rules/performance.md +158 -0
  148. jarvis/jarvis_code_agent/rules/refactoring.md +139 -0
  149. jarvis/jarvis_code_agent/rules/security.md +160 -0
  150. jarvis/jarvis_code_agent/rules/tdd.md +78 -0
  151. jarvis/jarvis_code_agent/test_rules/cpp_test.md +118 -0
  152. jarvis/jarvis_code_agent/test_rules/go_test.md +98 -0
  153. jarvis/jarvis_code_agent/test_rules/java_test.md +99 -0
  154. jarvis/jarvis_code_agent/test_rules/javascript_test.md +113 -0
  155. jarvis/jarvis_code_agent/test_rules/php_test.md +117 -0
  156. jarvis/jarvis_code_agent/test_rules/python_test.md +91 -0
  157. jarvis/jarvis_code_agent/test_rules/ruby_test.md +102 -0
  158. jarvis/jarvis_code_agent/test_rules/rust_test.md +86 -0
  159. jarvis/jarvis_code_agent/utils.py +36 -26
  160. jarvis/jarvis_code_analysis/checklists/loader.py +21 -21
  161. jarvis/jarvis_code_analysis/code_review.py +64 -33
  162. jarvis/jarvis_data/config_schema.json +285 -192
  163. jarvis/jarvis_git_squash/main.py +8 -6
  164. jarvis/jarvis_git_utils/git_commiter.py +53 -76
  165. jarvis/jarvis_mcp/__init__.py +5 -2
  166. jarvis/jarvis_mcp/sse_mcp_client.py +40 -30
  167. jarvis/jarvis_mcp/stdio_mcp_client.py +27 -19
  168. jarvis/jarvis_mcp/streamable_mcp_client.py +35 -26
  169. jarvis/jarvis_memory_organizer/memory_organizer.py +78 -55
  170. jarvis/jarvis_methodology/main.py +48 -39
  171. jarvis/jarvis_multi_agent/__init__.py +56 -23
  172. jarvis/jarvis_multi_agent/main.py +15 -18
  173. jarvis/jarvis_platform/base.py +179 -111
  174. jarvis/jarvis_platform/human.py +27 -16
  175. jarvis/jarvis_platform/kimi.py +52 -45
  176. jarvis/jarvis_platform/openai.py +101 -40
  177. jarvis/jarvis_platform/registry.py +51 -33
  178. jarvis/jarvis_platform/tongyi.py +68 -38
  179. jarvis/jarvis_platform/yuanbao.py +59 -43
  180. jarvis/jarvis_platform_manager/main.py +68 -76
  181. jarvis/jarvis_platform_manager/service.py +24 -14
  182. jarvis/jarvis_rag/README_CONFIG.md +314 -0
  183. jarvis/jarvis_rag/README_DYNAMIC_LOADING.md +311 -0
  184. jarvis/jarvis_rag/README_ONLINE_MODELS.md +230 -0
  185. jarvis/jarvis_rag/__init__.py +57 -4
  186. jarvis/jarvis_rag/cache.py +3 -1
  187. jarvis/jarvis_rag/cli.py +48 -68
  188. jarvis/jarvis_rag/embedding_interface.py +39 -0
  189. jarvis/jarvis_rag/embedding_manager.py +7 -230
  190. jarvis/jarvis_rag/embeddings/__init__.py +41 -0
  191. jarvis/jarvis_rag/embeddings/base.py +114 -0
  192. jarvis/jarvis_rag/embeddings/cohere.py +66 -0
  193. jarvis/jarvis_rag/embeddings/edgefn.py +117 -0
  194. jarvis/jarvis_rag/embeddings/local.py +260 -0
  195. jarvis/jarvis_rag/embeddings/openai.py +62 -0
  196. jarvis/jarvis_rag/embeddings/registry.py +293 -0
  197. jarvis/jarvis_rag/llm_interface.py +8 -6
  198. jarvis/jarvis_rag/query_rewriter.py +8 -9
  199. jarvis/jarvis_rag/rag_pipeline.py +61 -52
  200. jarvis/jarvis_rag/reranker.py +7 -75
  201. jarvis/jarvis_rag/reranker_interface.py +32 -0
  202. jarvis/jarvis_rag/rerankers/__init__.py +41 -0
  203. jarvis/jarvis_rag/rerankers/base.py +109 -0
  204. jarvis/jarvis_rag/rerankers/cohere.py +67 -0
  205. jarvis/jarvis_rag/rerankers/edgefn.py +140 -0
  206. jarvis/jarvis_rag/rerankers/jina.py +79 -0
  207. jarvis/jarvis_rag/rerankers/local.py +89 -0
  208. jarvis/jarvis_rag/rerankers/registry.py +293 -0
  209. jarvis/jarvis_rag/retriever.py +58 -43
  210. jarvis/jarvis_sec/__init__.py +66 -141
  211. jarvis/jarvis_sec/agents.py +21 -17
  212. jarvis/jarvis_sec/analysis.py +80 -33
  213. jarvis/jarvis_sec/checkers/__init__.py +7 -13
  214. jarvis/jarvis_sec/checkers/c_checker.py +356 -164
  215. jarvis/jarvis_sec/checkers/rust_checker.py +47 -29
  216. jarvis/jarvis_sec/cli.py +43 -21
  217. jarvis/jarvis_sec/clustering.py +430 -272
  218. jarvis/jarvis_sec/file_manager.py +99 -55
  219. jarvis/jarvis_sec/parsers.py +9 -6
  220. jarvis/jarvis_sec/prompts.py +4 -3
  221. jarvis/jarvis_sec/report.py +44 -22
  222. jarvis/jarvis_sec/review.py +180 -107
  223. jarvis/jarvis_sec/status.py +50 -41
  224. jarvis/jarvis_sec/types.py +3 -0
  225. jarvis/jarvis_sec/utils.py +160 -83
  226. jarvis/jarvis_sec/verification.py +411 -181
  227. jarvis/jarvis_sec/workflow.py +132 -21
  228. jarvis/jarvis_smart_shell/main.py +28 -41
  229. jarvis/jarvis_stats/cli.py +14 -12
  230. jarvis/jarvis_stats/stats.py +28 -19
  231. jarvis/jarvis_stats/storage.py +14 -8
  232. jarvis/jarvis_stats/visualizer.py +12 -7
  233. jarvis/jarvis_tools/base.py +5 -2
  234. jarvis/jarvis_tools/clear_memory.py +13 -9
  235. jarvis/jarvis_tools/cli/main.py +23 -18
  236. jarvis/jarvis_tools/edit_file.py +572 -873
  237. jarvis/jarvis_tools/execute_script.py +10 -7
  238. jarvis/jarvis_tools/file_analyzer.py +7 -8
  239. jarvis/jarvis_tools/meta_agent.py +287 -0
  240. jarvis/jarvis_tools/methodology.py +5 -3
  241. jarvis/jarvis_tools/read_code.py +305 -1438
  242. jarvis/jarvis_tools/read_symbols.py +50 -17
  243. jarvis/jarvis_tools/read_webpage.py +19 -18
  244. jarvis/jarvis_tools/registry.py +435 -156
  245. jarvis/jarvis_tools/retrieve_memory.py +16 -11
  246. jarvis/jarvis_tools/save_memory.py +8 -6
  247. jarvis/jarvis_tools/search_web.py +31 -31
  248. jarvis/jarvis_tools/sub_agent.py +32 -28
  249. jarvis/jarvis_tools/sub_code_agent.py +44 -60
  250. jarvis/jarvis_tools/task_list_manager.py +1811 -0
  251. jarvis/jarvis_tools/virtual_tty.py +29 -19
  252. jarvis/jarvis_utils/__init__.py +4 -0
  253. jarvis/jarvis_utils/builtin_replace_map.py +2 -1
  254. jarvis/jarvis_utils/clipboard.py +9 -8
  255. jarvis/jarvis_utils/collections.py +331 -0
  256. jarvis/jarvis_utils/config.py +699 -194
  257. jarvis/jarvis_utils/dialogue_recorder.py +294 -0
  258. jarvis/jarvis_utils/embedding.py +6 -3
  259. jarvis/jarvis_utils/file_processors.py +7 -1
  260. jarvis/jarvis_utils/fzf.py +9 -3
  261. jarvis/jarvis_utils/git_utils.py +71 -42
  262. jarvis/jarvis_utils/globals.py +116 -32
  263. jarvis/jarvis_utils/http.py +6 -2
  264. jarvis/jarvis_utils/input.py +318 -83
  265. jarvis/jarvis_utils/jsonnet_compat.py +119 -104
  266. jarvis/jarvis_utils/methodology.py +37 -28
  267. jarvis/jarvis_utils/output.py +201 -44
  268. jarvis/jarvis_utils/utils.py +986 -628
  269. {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/METADATA +49 -33
  270. jarvis_ai_assistant-1.0.2.dist-info/RECORD +304 -0
  271. jarvis/jarvis_code_agent/code_analyzer/structured_code.py +0 -556
  272. jarvis/jarvis_tools/generate_new_tool.py +0 -205
  273. jarvis/jarvis_tools/lsp_client.py +0 -1552
  274. jarvis/jarvis_tools/rewrite_file.py +0 -105
  275. jarvis_ai_assistant-0.7.8.dist-info/RECORD +0 -218
  276. {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/WHEEL +0 -0
  277. {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/entry_points.txt +0 -0
  278. {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/licenses/LICENSE +0 -0
  279. {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/top_level.txt +0 -0
@@ -1,78 +1,64 @@
1
- # -*- coding: utf-8 -*-
2
1
  """Jarvis代码代理模块。
3
2
 
4
3
  该模块提供CodeAgent类,用于处理代码修改任务。
5
4
  """
6
5
 
6
+ import hashlib
7
7
  import os
8
+
9
+ from jarvis.jarvis_utils.output import PrettyOutput
10
+
11
+ # -*- coding: utf-8 -*-
8
12
  import subprocess
9
13
  import sys
10
- import hashlib
11
- from typing import Any, Dict, List, Optional, Tuple
14
+ from typing import Any, Optional
12
15
 
13
16
  import typer
14
- import yaml
15
17
 
16
18
  from jarvis.jarvis_agent import Agent
17
19
  from jarvis.jarvis_agent.events import AFTER_TOOL_CALL
18
- from jarvis.jarvis_code_agent.lint import (
19
- get_lint_tools,
20
- get_lint_commands_for_files,
21
- group_commands_by_tool,
22
- get_format_commands_for_files,
23
- )
24
- from jarvis.jarvis_code_agent.code_analyzer.build_validator import BuildValidator, BuildResult, FallbackBuildValidator
25
20
  from jarvis.jarvis_code_agent.build_validation_config import BuildValidationConfig
26
- from jarvis.jarvis_git_utils.git_commiter import GitCommitTool
21
+ from jarvis.jarvis_code_agent.code_agent_build import BuildValidationManager
22
+ from jarvis.jarvis_code_agent.code_agent_diff import DiffManager
23
+ from jarvis.jarvis_code_agent.code_agent_git import GitManager
24
+ from jarvis.jarvis_code_agent.code_agent_impact import ImpactManager
25
+ from jarvis.jarvis_code_agent.code_agent_lint import LintManager
26
+ from jarvis.jarvis_code_agent.code_agent_llm import LLMManager
27
+ from jarvis.jarvis_code_agent.code_agent_postprocess import PostProcessManager
28
+ from jarvis.jarvis_code_agent.code_agent_prompts import get_system_prompt
29
+ from jarvis.jarvis_code_agent.code_agent_rules import RulesManager
27
30
  from jarvis.jarvis_code_agent.code_analyzer import ContextManager
28
- from jarvis.jarvis_code_agent.code_analyzer.llm_context_recommender import ContextRecommender
29
- from jarvis.jarvis_code_agent.code_analyzer import ImpactAnalyzer, parse_git_diff_to_edits
30
- from jarvis.jarvis_utils.config import (
31
- is_confirm_before_apply_patch,
32
- is_enable_static_analysis,
33
- is_enable_build_validation,
34
- get_build_validation_timeout,
35
- get_git_check_mode,
36
- set_config,
37
- get_data_dir,
38
- is_enable_intent_recognition,
39
- is_enable_impact_analysis,
40
- get_smart_platform_name,
41
- get_smart_model_name,
31
+ from jarvis.jarvis_code_agent.code_analyzer.llm_context_recommender import (
32
+ ContextRecommender,
42
33
  )
43
- from jarvis.jarvis_platform.registry import PlatformRegistry
44
34
  from jarvis.jarvis_code_agent.utils import get_project_overview
45
- from jarvis.jarvis_utils.git_utils import (
46
- confirm_add_new_files,
47
- detect_large_code_deletion,
48
- find_git_root_and_cd,
49
- get_commits_between,
50
- get_diff,
51
- get_diff_file_list,
52
- get_latest_commit_hash,
53
- handle_commit_workflow,
54
- has_uncommitted_changes,
55
- revert_change,
56
- )
57
- from jarvis.jarvis_utils.input import get_multiline_input, user_confirm
58
- from jarvis.jarvis_utils.output import OutputType, PrettyOutput # 保留用于语法高亮
59
- from jarvis.jarvis_utils.utils import init_env, _acquire_single_instance_lock
35
+ from jarvis.jarvis_platform.registry import PlatformRegistry
36
+ from jarvis.jarvis_utils.config import get_smart_model_name
37
+ from jarvis.jarvis_utils.config import get_smart_platform_name
38
+ from jarvis.jarvis_utils.config import is_confirm_before_apply_patch
39
+ from jarvis.jarvis_utils.config import is_enable_intent_recognition
40
+ from jarvis.jarvis_utils.config import set_config
41
+ from jarvis.jarvis_utils.git_utils import detect_large_code_deletion
42
+ from jarvis.jarvis_utils.git_utils import find_git_root_and_cd
43
+ from jarvis.jarvis_utils.git_utils import get_commits_between
44
+ from jarvis.jarvis_utils.git_utils import get_diff
45
+ from jarvis.jarvis_utils.git_utils import get_diff_between_commits
46
+ from jarvis.jarvis_utils.git_utils import get_diff_file_list
47
+ from jarvis.jarvis_utils.git_utils import get_latest_commit_hash
48
+ from jarvis.jarvis_utils.git_utils import handle_commit_workflow
49
+ from jarvis.jarvis_utils.git_utils import revert_change
50
+ from jarvis.jarvis_utils.input import get_multiline_input
51
+ from jarvis.jarvis_utils.input import user_confirm
52
+ from jarvis.jarvis_utils.output import OutputType # 保留用于语法高亮
53
+ from jarvis.jarvis_utils.utils import _acquire_single_instance_lock
54
+ from jarvis.jarvis_utils.utils import init_env
55
+ from jarvis.jarvis_utils.tag import ot
56
+ from jarvis.jarvis_utils.globals import set_current_agent
57
+ from jarvis.jarvis_utils.globals import clear_current_agent
60
58
 
61
59
  app = typer.Typer(help="Jarvis 代码助手")
62
60
 
63
61
 
64
- def _format_build_error(result: BuildResult, max_len: int = 2000) -> str:
65
- """格式化构建错误信息,限制输出长度"""
66
- error_msg = result.error_message or ""
67
- output = result.output or ""
68
-
69
- full_error = f"{error_msg}\n{output}".strip()
70
-
71
- if len(full_error) > max_len:
72
- return full_error[:max_len] + "\n... (输出已截断)"
73
- return full_error
74
-
75
-
76
62
  class CodeAgent(Agent):
77
63
  """Jarvis系统的代码修改代理。
78
64
 
@@ -85,27 +71,48 @@ class CodeAgent(Agent):
85
71
  need_summary: bool = True,
86
72
  append_tools: Optional[str] = None,
87
73
  tool_group: Optional[str] = None,
88
- non_interactive: Optional[bool] = None,
74
+ non_interactive: Optional[bool] = True,
89
75
  rule_names: Optional[str] = None,
90
- **kwargs,
91
- ):
76
+ disable_review: bool = False,
77
+ review_max_iterations: int = 0,
78
+ enable_task_list_manager: bool = True,
79
+ **kwargs: Any,
80
+ ) -> None:
92
81
  self.root_dir = os.getcwd()
93
82
  self.tool_group = tool_group
83
+ # 记录当前是否为非交互模式,便于在提示词/输入中动态调整行为说明
84
+ self.non_interactive: bool = bool(non_interactive)
85
+ # Review 相关配置
86
+ self.disable_review = disable_review
87
+ self.review_max_iterations = review_max_iterations
88
+
89
+ # 存储开始时的commit hash,用于后续git diff获取
90
+ self.start_commit: Optional[str] = None
94
91
 
95
92
  # 初始化上下文管理器
96
93
  self.context_manager = ContextManager(self.root_dir)
97
94
  # 上下文推荐器将在Agent创建后初始化(需要LLM模型)
98
95
  self.context_recommender: Optional[ContextRecommender] = None
99
96
 
97
+ # 初始化各个管理器
98
+ self.rules_manager = RulesManager(self.root_dir)
99
+ self.git_manager = GitManager(self.root_dir)
100
+ self.diff_manager = DiffManager(self.root_dir)
101
+ self.impact_manager = ImpactManager(self.root_dir, self.context_manager)
102
+ self.build_validation_manager = BuildValidationManager(self.root_dir)
103
+ self.lint_manager = LintManager(self.root_dir)
104
+ self.post_process_manager = PostProcessManager(self.root_dir)
105
+ # LLM管理器将在模型初始化后创建
106
+
100
107
  # 检测 git username 和 email 是否已设置
101
- self._check_git_config()
108
+ self.git_manager.check_git_config()
102
109
  base_tools = [
103
110
  "execute_script",
104
111
  "read_code",
105
- "edit_file",
106
- "rewrite_file",
107
- "lsp_client", # LSP客户端工具,用于获取代码补全、悬停等信息
112
+ "edit_file", # 普通 search/replace 编辑
108
113
  ]
114
+ if enable_task_list_manager:
115
+ base_tools.append("task_list_manager") # 任务列表管理工具
109
116
 
110
117
  if append_tools:
111
118
  additional_tools = [
@@ -115,48 +122,15 @@ class CodeAgent(Agent):
115
122
  # 去重
116
123
  base_tools = list(dict.fromkeys(base_tools))
117
124
 
118
- code_system_prompt = self._get_system_prompt()
119
- # 先加载全局规则(数据目录 rules),再加载项目规则(.jarvis/rules),并拼接为单一规则块注入
120
- global_rules = self._read_global_rules()
121
- project_rules = self._read_project_rules()
122
-
123
- combined_parts: List[str] = []
124
- loaded_rule_names: List[str] = [] # 记录加载的规则名称
125
-
126
- if global_rules:
127
- combined_parts.append(global_rules)
128
- loaded_rule_names.append("global_rule")
129
- if project_rules:
130
- combined_parts.append(project_rules)
131
- loaded_rule_names.append("project_rule")
132
-
133
- # 如果指定了 rule_names,从 rules.yaml 文件中读取并添加多个规则
134
- if rule_names:
135
- rule_list = [name.strip() for name in rule_names.split(',') if name.strip()]
136
- for rule_name in rule_list:
137
- named_rule = self._get_named_rule(rule_name)
138
- if named_rule:
139
- combined_parts.append(named_rule)
140
- loaded_rule_names.append(rule_name)
141
-
142
- if combined_parts:
143
- merged_rules = "\n\n".join(combined_parts)
144
- code_system_prompt = (
145
- f"{code_system_prompt}\n\n"
146
- f"<rules>\n{merged_rules}\n</rules>"
147
- )
148
- # 显示加载的规则名称
149
- if loaded_rule_names:
150
- rules_display = ", ".join(loaded_rule_names)
151
- print(f"ℹ️ 已加载规则: {rules_display}")
152
-
125
+ code_system_prompt = get_system_prompt()
126
+
153
127
  # 调用父类 Agent 的初始化
154
128
  # 默认禁用方法论和分析,但允许通过 kwargs 覆盖
155
129
  use_methodology = kwargs.pop("use_methodology", False)
156
130
  use_analysis = kwargs.pop("use_analysis", False)
157
131
  # name 使用传入的值,如果没有传入则使用默认值 "CodeAgent"
158
132
  name = kwargs.pop("name", "CodeAgent")
159
-
133
+
160
134
  # 准备显式传递给 super().__init__ 的参数
161
135
  # 注意:这些参数如果也在 kwargs 中,需要先移除,避免重复传递错误
162
136
  explicit_params = {
@@ -170,12 +144,12 @@ class CodeAgent(Agent):
170
144
  "non_interactive": non_interactive,
171
145
  "use_tools": base_tools,
172
146
  }
173
-
147
+
174
148
  # 自动移除所有显式传递的参数,避免重复传递错误
175
149
  # 这样以后添加新参数时,只要在 explicit_params 中声明,就会自动处理
176
150
  for key in explicit_params:
177
151
  kwargs.pop(key, None)
178
-
152
+
179
153
  super().__init__(
180
154
  **explicit_params,
181
155
  **kwargs,
@@ -192,32 +166,36 @@ class CodeAgent(Agent):
192
166
  parent_model = None
193
167
  if self.model:
194
168
  parent_model = self.model
195
-
169
+
196
170
  self.context_recommender = ContextRecommender(
197
- self.context_manager,
198
- parent_model=parent_model
171
+ self.context_manager, parent_model=parent_model
199
172
  )
200
173
  except Exception as e:
201
174
  # LLM推荐器初始化失败
202
- print(f"⚠️ 上下文推荐器初始化失败: {e},将跳过上下文推荐功能")
175
+ PrettyOutput.auto_print(
176
+ f"⚠️ 上下文推荐器初始化失败: {e},将跳过上下文推荐功能"
177
+ )
203
178
 
204
179
  self.event_bus.subscribe(AFTER_TOOL_CALL, self._on_after_tool_call)
205
-
180
+
206
181
  # 打印语言功能支持表格
207
182
  try:
208
- from jarvis.jarvis_agent.language_support_info import print_language_support_table
183
+ from jarvis.jarvis_agent.language_support_info import (
184
+ print_language_support_table,
185
+ )
186
+
209
187
  print_language_support_table()
210
188
  except Exception:
211
189
  pass
212
190
 
213
- def _init_model(self, model_group: Optional[str]):
191
+ def _init_model(self, model_group: Optional[str]) -> None:
214
192
  """初始化模型平台(CodeAgent使用smart平台,适用于代码生成等复杂场景)"""
215
193
  platform_name = get_smart_platform_name(model_group)
216
194
  model_name = get_smart_model_name(model_group)
217
195
 
218
196
  maybe_model = PlatformRegistry().create_platform(platform_name)
219
197
  if maybe_model is None:
220
- print(f"⚠️ 平台 {platform_name} 不存在,将使用smart模型")
198
+ PrettyOutput.auto_print(f"⚠️ 平台 {platform_name} 不存在,将使用smart模型")
221
199
  maybe_model = PlatformRegistry().get_smart_platform()
222
200
 
223
201
  # 在此处收敛为非可选类型,确保后续赋值满足类型检查
@@ -229,690 +207,19 @@ class CodeAgent(Agent):
229
207
  self.model.set_model_group(model_group)
230
208
  self.model.set_suppress_output(False)
231
209
 
232
- def _get_system_prompt(self) -> str:
233
- """获取代码工程师的系统提示词"""
234
- return """
235
- 你是Jarvis代码工程师,专注于**项目级代码分析、精准修改与问题排查**,核心原则:自主决策不犹豫、高效精准不冗余、修改审慎可回退、工具精通不臆测。
236
-
237
- 工作流程(闭环执行,每步必落地):
238
- 1. 需求拆解与项目对齐:
239
- - 先明确用户需求的核心目标(如“修复XX报错”“新增XX功能”“优化XX性能”),标注关键约束(如“兼容Python 3.8+”“不修改核心依赖”);
240
- - 快速定位项目核心目录(如src/、main/)、技术栈(语言/框架/版本)、代码风格规范(如PEP8、ESLint规则),避免无的放矢。
241
-
242
- 2. 目标文件精准定位(工具优先,拒绝盲搜):
243
- - 优先通过 lsp_client 的 search_symbol(符号搜索)定位关联文件(如函数、类、变量所属文件);
244
- - 若符号不明确,用全文搜索工具按“关键词+文件类型过滤”(如“关键词:user_login + 后缀:.py”)缩小范围;
245
- - 仅当工具无法定位时,才用 read_code 读取疑似目录下的核心文件(如入口文件、配置文件),避免无效读取。
246
-
247
- 3. 代码深度分析(基于工具,禁止虚构):
248
- - 符号分析:用 lsp_client 的 document_symbols(文档符号)、get_symbol_info(符号详情)、definition(定义跳转)、references(引用查询),确认符号的作用域、依赖关系、调用链路;
249
- - 内容分析:用 read_code 读取目标文件完整内容,重点关注“逻辑分支、异常处理、依赖引入、配置参数”,记录关键代码片段(如报错位置、待修改逻辑);
250
- - 影响范围评估:用 lsp_client 的 references 查询待修改符号的所有引用场景,预判修改可能波及的模块,避免“改一处崩一片”。
251
-
252
- 4. 最小变更方案设计(可回退、易维护):
253
- - 优先选择“局部修改”(如修改函数内逻辑、补充条件判断),而非“重构”或“全文件重写”;
254
- - 方案需满足:① 覆盖需求核心;② 不破坏现有功能;③ 符合项目代码风格;④ 便于后续回退(如仅修改必要行,不删无关代码);
255
- - 若需修改核心逻辑(如公共函数、配置文件),先记录原始代码片段(如用临时文件保存到 /tmp/backup_xxx.txt),再执行修改。
256
-
257
- 5. 先读后写,精准执行(工具规范使用):
258
- - 必须先通过 read_code 读取目标文件完整内容,确认待修改位置的上下文(如前后代码逻辑、缩进格式),再调用编辑工具;
259
- - 编辑工具选择:
260
- - 局部修改(改少数行、补代码块):用 edit_file,明确标注“修改范围(行号/代码片段)+ 修改内容”(如“替换第15-20行的循环逻辑为:xxx”);
261
- - 全文件重写(如格式统一、逻辑重构):仅当局部修改无法满足需求时使用 rewrite_file,重写前必须备份原始文件到 /tmp/rewrite_backup_xxx.txt。
262
-
263
- 6. 验证与兜底(避免无效交付):
264
- - 修改后优先通过 lsp_client 的语法检查功能(若支持)验证代码无语法错误;
265
- - 若涉及功能变更,建议补充1-2行核心测试用例(或提示用户验证场景),确保修改生效;
266
- - 记录修改日志(保存到 /tmp/modify_log_xxx.txt),内容包括:修改时间、目标文件、修改原因、原始代码片段、修改后代码片段,便于问题追溯。
267
-
268
- 工具使用规范(精准调用,不浪费资源):
269
- - lsp_client:仅传递有效参数(如符号名精准、文件路径明确),避免模糊查询(如无关键词的全局搜索);
270
- - 全文搜索:必须添加“文件类型过滤”“目录过滤”,减少无效结果(如仅搜索 src/ 目录下的 .java 文件);
271
- - read_code:仅读取目标文件和关联依赖文件,不读取日志、测试数据、第三方依赖包等无关文件;
272
- - edit_file/rewrite_file:修改后必须保持代码缩进、命名规范与原文件一致(如原文件用4空格缩进,不改为2空格),不引入多余空行、注释。
273
-
274
- 代码质量约束(底线要求,不可突破):
275
- 1. 语法正确性:修改后代码无语法错误、无未定义变量/函数、无依赖缺失;
276
- 2. 功能兼容性:不破坏现有正常功能,修改后的代码能适配项目已有的调用场景;
277
- 3. 风格一致性:严格遵循项目既有风格(如命名规范、缩进、注释格式),不引入个人风格;
278
- 4. 可维护性:修改逻辑清晰,关键改动可加简洁注释(如“// 修复XX报错:XX场景下变量未初始化”),不写“魔法值”“冗余代码”。
279
-
280
- 调试指引(问题闭环,高效排查):
281
- - 定位报错:优先用 lsp_client 定位报错位置,结合 read_code 查看上下文,确认报错类型(语法错/逻辑错/运行时错);
282
- - 日志补充:若报错模糊,在关键位置(如函数入口、循环内、异常捕获前)增加打印日志,内容包括“变量值、执行步骤、时间戳”(如 print(f"[DEBUG] user_login: username={username}, status={status}")),日志输出到 /tmp/ 目录,不污染项目日志;
283
- - 中间结果保存:复杂逻辑修改时,用临时文件(/tmp/temp_result_xxx.txt)保存中间数据(如计算结果、接口返回值),便于验证逻辑正确性;
284
- - 回退机制:若修改后出现新问题,立即用备份文件回退,重新分析,不盲目叠加修改。
285
-
286
- 禁止行为(红线不可碰):
287
- 1. 禁止虚构代码、依赖、文件路径,所有结论必须基于工具返回结果或实际读取的代码;
288
- 2. 禁止无差别读取项目所有文件,避免浪费资源;
289
- 3. 禁止大篇幅删除、重构未明确要求修改的代码;
290
- 4. 禁止引入项目未依赖的第三方库(除非用户明确允许);
291
- 5. 禁止修改 /tmp/ 以外的非项目目录文件,避免污染环境。
292
-
293
- """
294
-
295
- def _read_project_rules(self) -> Optional[str]:
296
- """读取 .jarvis/rules 内容,如果存在则返回字符串,否则返回 None"""
210
+ # 初始化LLM管理器(使用普通模型,不使用smart模型)
211
+ self.llm_manager = LLMManager(parent_model=self.model, model_group=model_group)
212
+ # 同步模型组到全局,便于后续工具(如提交信息生成)获取一致的模型配置
297
213
  try:
298
- rules_path = os.path.join(self.root_dir, ".jarvis", "rule")
299
- if os.path.exists(rules_path) and os.path.isfile(rules_path):
300
- with open(rules_path, "r", encoding="utf-8", errors="replace") as f:
301
- content = f.read().strip()
302
- return content if content else None
303
- except Exception:
304
- # 读取规则失败时忽略,不影响主流程
305
- pass
306
- return None
214
+ from jarvis.jarvis_utils.globals import set_global_model_group
307
215
 
308
- def _read_global_rules(self) -> Optional[str]:
309
- """读取数据目录 rules 内容,如果存在则返回字符串,否则返回 None"""
310
- try:
311
- rules_path = os.path.join(get_data_dir(), "rule")
312
- if os.path.exists(rules_path) and os.path.isfile(rules_path):
313
- with open(rules_path, "r", encoding="utf-8", errors="replace") as f:
314
- content = f.read().strip()
315
- return content if content else None
216
+ set_global_model_group(model_group)
316
217
  except Exception:
317
- # 读取规则失败时忽略,不影响主流程
218
+ # 若全局同步失败,不影响主流程
318
219
  pass
319
- return None
320
-
321
- def _get_named_rule(self, rule_name: str) -> Optional[str]:
322
- """从 rules.yaml 文件中获取指定名称的规则
323
-
324
- 参数:
325
- rule_name: 规则名称
326
-
327
- 返回:
328
- str: 规则内容,如果未找到则返回 None
329
- """
330
- try:
331
- # 读取全局数据目录下的 rules.yaml
332
- global_rules_yaml_path = os.path.join(get_data_dir(), "rules.yaml")
333
- global_rules = {}
334
- if os.path.exists(global_rules_yaml_path) and os.path.isfile(global_rules_yaml_path):
335
- with open(global_rules_yaml_path, "r", encoding="utf-8", errors="replace") as f:
336
- global_rules = yaml.safe_load(f) or {}
337
-
338
- # 读取 git 根目录下的 rules.yaml
339
- project_rules_yaml_path = os.path.join(self.root_dir, "rules.yaml")
340
- project_rules = {}
341
- if os.path.exists(project_rules_yaml_path) and os.path.isfile(project_rules_yaml_path):
342
- with open(project_rules_yaml_path, "r", encoding="utf-8", errors="replace") as f:
343
- project_rules = yaml.safe_load(f) or {}
344
-
345
- # 合并配置:项目配置覆盖全局配置
346
- merged_rules = {**global_rules, **project_rules}
347
-
348
- # 查找指定的规则
349
- if rule_name in merged_rules:
350
- rule_value = merged_rules[rule_name]
351
- # 如果值是字符串,直接返回
352
- if isinstance(rule_value, str):
353
- return rule_value.strip() if rule_value.strip() else None
354
- # 如果值是其他类型,转换为字符串
355
- return str(rule_value).strip() if str(rule_value).strip() else None
356
-
357
- return None
358
- except Exception as e:
359
- # 读取规则失败时忽略,不影响主流程
360
- print(f"⚠️ 读取 rules.yaml 失败: {e}")
361
- return None
362
-
363
- def _check_git_config(self) -> None:
364
- """检查 git username 和 email 是否已设置,如果没有则提示并退出"""
365
- try:
366
- # 检查 git user.name
367
- result = subprocess.run(
368
- ["git", "config", "--get", "user.name"],
369
- capture_output=True,
370
- text=True,
371
- check=False,
372
- )
373
- username = result.stdout.strip()
374
-
375
- # 检查 git user.email
376
- result = subprocess.run(
377
- ["git", "config", "--get", "user.email"],
378
- capture_output=True,
379
- text=True,
380
- check=False,
381
- )
382
- email = result.stdout.strip()
383
-
384
- # 如果任一配置未设置,提示并退出
385
- if not username or not email:
386
- missing_configs = []
387
- if not username:
388
- missing_configs.append(
389
- ' git config --global user.name "Your Name"'
390
- )
391
- if not email:
392
- missing_configs.append(
393
- ' git config --global user.email "your.email@example.com"'
394
- )
395
-
396
- message = "❌ Git 配置不完整\n\n请运行以下命令配置 Git:\n" + "\n".join(
397
- missing_configs
398
- )
399
- print(f"⚠️ {message}")
400
- # 通过配置控制严格校验模式(JARVIS_GIT_CHECK_MODE):
401
- # - warn: 仅告警并继续,后续提交可能失败
402
- # - strict: 严格模式(默认),直接退出
403
- mode = get_git_check_mode().lower()
404
- if mode == "warn":
405
- print("ℹ️ 已启用 Git 校验警告模式(JARVIS_GIT_CHECK_MODE=warn),将继续运行。"
406
- "注意:后续提交可能失败,请尽快配置 git user.name 与 user.email。")
407
- return
408
- sys.exit(1)
409
-
410
- except FileNotFoundError:
411
- print("❌ 未找到 git 命令,请先安装 Git")
412
- sys.exit(1)
413
- except Exception as e:
414
- print(f"❌ 检查 Git 配置时出错: {str(e)}")
415
- sys.exit(1)
416
-
417
- def _find_git_root(self) -> str:
418
- """查找并切换到git根目录
419
-
420
- 返回:
421
- str: git根目录路径
422
- """
423
-
424
- curr_dir = os.getcwd()
425
- git_dir = find_git_root_and_cd(curr_dir)
426
- self.root_dir = git_dir
427
-
428
- return git_dir
429
-
430
- def _update_gitignore(self, git_dir: str) -> None:
431
- """检查并更新.gitignore文件,确保忽略.jarvis目录,并追加常用语言的忽略规则(若缺失)
432
-
433
- 参数:
434
- git_dir: git根目录路径
435
- """
436
- gitignore_path = os.path.join(git_dir, ".gitignore")
437
-
438
- # 常用忽略规则(按语言/场景分组)
439
- sections = {
440
- "General": [
441
- ".jarvis",
442
- ".DS_Store",
443
- "Thumbs.db",
444
- "*.log",
445
- "*.tmp",
446
- "*.swp",
447
- "*.swo",
448
- ".idea/",
449
- ".vscode/",
450
- ],
451
- "Python": [
452
- "__pycache__/",
453
- "*.py[cod]",
454
- "*$py.class",
455
- ".Python",
456
- "env/",
457
- "venv/",
458
- ".venv/",
459
- "build/",
460
- "dist/",
461
- "develop-eggs/",
462
- "downloads/",
463
- "eggs/",
464
- ".eggs/",
465
- "lib/",
466
- "lib64/",
467
- "parts/",
468
- "sdist/",
469
- "var/",
470
- "wheels/",
471
- "pip-wheel-metadata/",
472
- "share/python-wheels/",
473
- "*.egg-info/",
474
- ".installed.cfg",
475
- "*.egg",
476
- "MANIFEST",
477
- ".mypy_cache/",
478
- ".pytest_cache/",
479
- ".ruff_cache/",
480
- ".tox/",
481
- ".coverage",
482
- ".coverage.*",
483
- "htmlcov/",
484
- ".hypothesis/",
485
- ".ipynb_checkpoints",
486
- ".pyre/",
487
- ".pytype/",
488
- ],
489
- "Rust": [
490
- "target/",
491
- ],
492
- "Node": [
493
- "node_modules/",
494
- "npm-debug.log*",
495
- "yarn-debug.log*",
496
- "yarn-error.log*",
497
- "pnpm-debug.log*",
498
- "lerna-debug.log*",
499
- "dist/",
500
- "coverage/",
501
- ".turbo/",
502
- ".next/",
503
- ".nuxt/",
504
- "out/",
505
- ],
506
- "Go": [
507
- "bin/",
508
- "vendor/",
509
- "coverage.out",
510
- ],
511
- "Java": [
512
- "target/",
513
- "*.class",
514
- ".gradle/",
515
- "build/",
516
- "out/",
517
- ],
518
- "C/C++": [
519
- "build/",
520
- "cmake-build-*/",
521
- "*.o",
522
- "*.a",
523
- "*.so",
524
- "*.obj",
525
- "*.dll",
526
- "*.dylib",
527
- "*.exe",
528
- "*.pdb",
529
- ],
530
- ".NET": [
531
- "bin/",
532
- "obj/",
533
- ],
534
- }
535
-
536
- existing_content = ""
537
- if os.path.exists(gitignore_path):
538
- with open(gitignore_path, "r", encoding="utf-8", errors="replace") as f:
539
- existing_content = f.read()
540
-
541
- # 已存在的忽略项(去除注释与空行)
542
- existing_set = set(
543
- ln.strip()
544
- for ln in existing_content.splitlines()
545
- if ln.strip() and not ln.strip().startswith("#")
546
- )
547
-
548
- # 计算缺失项并准备追加内容
549
- new_lines: List[str] = []
550
- for name, patterns in sections.items():
551
- missing = [p for p in patterns if p not in existing_set]
552
- if missing:
553
- new_lines.append(f"# {name}")
554
- new_lines.extend(missing)
555
- new_lines.append("") # 分组空行
556
-
557
- if not os.path.exists(gitignore_path):
558
- # 新建 .gitignore(仅包含缺失项;此处即为全部常用规则)
559
- with open(gitignore_path, "w", encoding="utf-8", newline="\n") as f:
560
- content_to_write = "\n".join(new_lines).rstrip()
561
- if content_to_write:
562
- f.write(content_to_write + "\n")
563
- print("✅ 已创建 .gitignore 并添加常用忽略规则")
564
- else:
565
- if new_lines:
566
- # 追加缺失的规则
567
- with open(gitignore_path, "a", encoding="utf-8", newline="\n") as f:
568
- # 若原文件不以换行结尾,先补一行
569
- if existing_content and not existing_content.endswith("\n"):
570
- f.write("\n")
571
- f.write("\n".join(new_lines).rstrip() + "\n")
572
- print("✅ 已更新 .gitignore,追加常用忽略规则")
573
-
574
- def _handle_git_changes(self, prefix: str, suffix: str) -> None:
575
- """处理git仓库中的未提交修改"""
576
-
577
- if has_uncommitted_changes():
578
-
579
- git_commiter = GitCommitTool()
580
- git_commiter.execute({"prefix": prefix, "suffix": suffix, "agent": self, "model_group": getattr(self.model, "model_group", None)})
581
-
582
- def _init_env(self, prefix: str, suffix: str) -> None:
583
- """初始化环境,组合以下功能:
584
- 1. 查找git根目录
585
- 2. 检查并更新.gitignore文件
586
- 3. 处理未提交的修改
587
- 4. 配置git对换行符变化不敏感
588
- """
589
-
590
- git_dir = self._find_git_root()
591
- self._update_gitignore(git_dir)
592
- self._handle_git_changes(prefix, suffix)
593
- # 配置git对换行符变化不敏感
594
- self._configure_line_ending_settings()
595
-
596
- def _configure_line_ending_settings(self) -> None:
597
- """配置git对换行符变化不敏感,只在当前设置与目标设置不一致时修改"""
598
- target_settings = {
599
- "core.autocrlf": "false",
600
- "core.safecrlf": "false",
601
- "core.whitespace": "cr-at-eol", # 忽略行尾的CR
602
- }
603
-
604
- # 获取当前设置并检查是否需要修改
605
- need_change = False
606
- current_settings = {}
607
- for key, target_value in target_settings.items():
608
- result = subprocess.run(
609
- ["git", "config", "--get", key],
610
- capture_output=True,
611
- text=True,
612
- check=False,
613
- )
614
- current_value = result.stdout.strip()
615
- current_settings[key] = current_value
616
- if current_value != target_value:
617
- need_change = True
618
-
619
- if not need_change:
620
-
621
- return
622
-
623
- print("⚠️ 正在修改git换行符敏感设置,这会影响所有文件的换行符处理方式")
624
- # 避免在循环中逐条打印,先拼接后统一打印
625
- lines = ["将进行以下设置:"]
626
- for key, value in target_settings.items():
627
- current = current_settings.get(key, "未设置")
628
- lines.append(f"{key}: {current} -> {value}")
629
- joined_lines = '\n'.join(lines)
630
- print(f"ℹ️ {joined_lines}")
631
-
632
- # 直接执行设置,不需要用户确认
633
- for key, value in target_settings.items():
634
- subprocess.run(["git", "config", key, value], check=True)
635
-
636
- # 对于Windows系统,提示用户可以创建.gitattributes文件
637
- if sys.platform.startswith("win"):
638
- self._handle_windows_line_endings()
639
-
640
- print("✅ git换行符敏感设置已更新")
641
-
642
- def _handle_windows_line_endings(self) -> None:
643
- """在Windows系统上处理换行符问题,提供建议而非强制修改"""
644
- gitattributes_path = os.path.join(self.root_dir, ".gitattributes")
645
-
646
- # 检查是否已存在.gitattributes文件
647
- if os.path.exists(gitattributes_path):
648
- with open(gitattributes_path, "r", encoding="utf-8") as f:
649
- content = f.read()
650
- # 如果已经有换行符相关配置,就不再提示
651
- if any(keyword in content for keyword in ["text=", "eol=", "binary"]):
652
- return
653
-
654
- print("ℹ️ 提示:在Windows系统上,建议配置 .gitattributes 文件来避免换行符问题。")
655
- print("ℹ️ 这可以防止仅因换行符不同而导致整个文件被标记为修改。")
656
-
657
- if user_confirm("是否要创建一个最小化的.gitattributes文件?", False):
658
- # 最小化的内容,只影响特定类型的文件
659
- minimal_content = """# Jarvis建议的最小化换行符配置
660
- # 默认所有文本文件使用LF,只有Windows特定文件使用CRLF
661
-
662
- # 默认所有文本文件使用LF
663
- * text=auto eol=lf
664
-
665
- # Windows批处理文件需要CRLF
666
- *.bat text eol=crlf
667
- *.cmd text eol=crlf
668
- *.ps1 text eol=crlf
669
- """
670
-
671
- if not os.path.exists(gitattributes_path):
672
- with open(gitattributes_path, "w", encoding="utf-8", newline="\n") as f:
673
- f.write(minimal_content)
674
- print("✅ 已创建最小化的 .gitattributes 文件")
675
- else:
676
- print("ℹ️ 将以下内容追加到现有 .gitattributes 文件:")
677
- PrettyOutput.print(minimal_content, OutputType.CODE, lang="text") # 保留语法高亮
678
- if user_confirm("是否追加到现有文件?", True):
679
- with open(
680
- gitattributes_path, "a", encoding="utf-8", newline="\n"
681
- ) as f:
682
- f.write("\n" + minimal_content)
683
- print("✅ 已更新 .gitattributes 文件")
684
- else:
685
- print("ℹ️ 跳过 .gitattributes 文件创建。如遇换行符问题,可手动创建此文件。")
686
-
687
- def _record_code_changes_stats(self, diff_text: str) -> None:
688
- """记录代码变更的统计信息。
689
-
690
- Args:
691
- diff_text: git diff的文本输出
692
- """
693
- from jarvis.jarvis_stats.stats import StatsManager
694
- import re
695
-
696
- # 匹配插入行数
697
- insertions_match = re.search(r"(\d+)\s+insertions?\(\+\)", diff_text)
698
- if insertions_match:
699
- insertions = int(insertions_match.group(1))
700
- StatsManager.increment(
701
- "code_lines_inserted", amount=insertions, group="code_agent"
702
- )
703
-
704
- # 匹配删除行数
705
- deletions_match = re.search(r"(\d+)\s+deletions?\(\-\)", diff_text)
706
- if deletions_match:
707
- deletions = int(deletions_match.group(1))
708
- StatsManager.increment(
709
- "code_lines_deleted", amount=deletions, group="code_agent"
710
- )
711
-
712
- def _handle_uncommitted_changes(self) -> None:
713
- """处理未提交的修改,包括:
714
- 1. 提示用户确认是否提交
715
- 2. 如果确认,则检查新增文件数量
716
- 3. 如果新增文件超过20个,让用户确认是否添加
717
- 4. 如果用户拒绝添加大量文件,提示修改.gitignore并重新检测
718
- 5. 暂存并提交所有修改
719
- """
720
- if has_uncommitted_changes():
721
- # 获取代码变更统计
722
- try:
723
- diff_result = subprocess.run(
724
- ["git", "diff", "HEAD", "--shortstat"],
725
- capture_output=True,
726
- text=True,
727
- encoding="utf-8",
728
- errors="replace",
729
- check=True,
730
- )
731
- if diff_result.returncode == 0 and diff_result.stdout:
732
- self._record_code_changes_stats(diff_result.stdout)
733
- except subprocess.CalledProcessError:
734
- pass
735
-
736
- print("⚠️ 检测到未提交的修改,是否要提交?")
737
- if not user_confirm("是否要提交?", True):
738
- return
739
-
740
- try:
741
- confirm_add_new_files()
742
-
743
- if not has_uncommitted_changes():
744
- return
745
-
746
- # 获取当前分支的提交总数
747
- # 兼容空仓库或无 HEAD 的场景:失败时将提交计数视为 0,继续执行提交流程
748
- commit_count = 0
749
- try:
750
- commit_result = subprocess.run(
751
- ["git", "rev-list", "--count", "HEAD"],
752
- capture_output=True,
753
- text=True,
754
- encoding="utf-8",
755
- errors="replace",
756
- check=False,
757
- )
758
- if commit_result.returncode == 0:
759
- out = commit_result.stdout.strip()
760
- if out.isdigit():
761
- commit_count = int(out)
762
- except Exception:
763
- commit_count = 0
764
-
765
- # 暂存所有修改
766
- subprocess.run(["git", "add", "."], check=True)
767
-
768
- # 提交变更
769
- subprocess.run(
770
- ["git", "commit", "-m", f"CheckPoint #{commit_count + 1}"],
771
- check=True,
772
- )
773
- except subprocess.CalledProcessError as e:
774
- print(f"❌ 提交失败: {str(e)}")
775
-
776
- def _show_commit_history(
777
- self, start_commit: Optional[str], end_commit: Optional[str]
778
- ) -> List[Tuple[str, str]]:
779
- """显示两个提交之间的提交历史
780
-
781
- 参数:
782
- start_commit: 起始提交hash
783
- end_commit: 结束提交hash
784
-
785
- 返回:
786
- 包含(commit_hash, commit_message)的元组列表
787
- """
788
- if start_commit and end_commit:
789
- commits = get_commits_between(start_commit, end_commit)
790
- else:
791
- commits = []
792
-
793
- if commits:
794
- # 统计生成的commit数量
795
- from jarvis.jarvis_stats.stats import StatsManager
796
-
797
- StatsManager.increment("commits_generated", group="code_agent")
798
-
799
- commit_messages = "检测到以下提交记录:\n" + "\n".join(
800
- f"- {commit_hash[:7]}: {message}" for commit_hash, message in commits
801
- )
802
- print(f"ℹ️ {commit_messages}")
803
- return commits
804
-
805
- def _format_modified_files(self, modified_files: List[str]) -> None:
806
- """格式化修改的文件
807
-
808
- Args:
809
- modified_files: 修改的文件列表
810
- """
811
- if not modified_files:
812
- return
813
-
814
- # 获取格式化命令
815
- format_commands = get_format_commands_for_files(modified_files, self.root_dir)
816
- if not format_commands:
817
- return
818
-
819
- print("🔧 正在格式化代码...")
820
-
821
- # 执行格式化命令
822
- formatted_files = set()
823
- for tool_name, file_path, command in format_commands:
824
- try:
825
- # 检查文件是否存在
826
- abs_file_path = os.path.join(self.root_dir, file_path) if not os.path.isabs(file_path) else file_path
827
- if not os.path.exists(abs_file_path):
828
- continue
829
-
830
- # 执行格式化命令
831
- result = subprocess.run(
832
- command,
833
- shell=True,
834
- cwd=self.root_dir,
835
- capture_output=True,
836
- text=True,
837
- encoding="utf-8",
838
- errors="replace",
839
- timeout=300, # 300秒超时
840
- )
841
-
842
- if result.returncode == 0:
843
- formatted_files.add(file_path)
844
- print(f"✅ 已格式化: {os.path.basename(file_path)} ({tool_name})")
845
- else:
846
- # 格式化失败,记录但不中断流程
847
- error_msg = (result.stderr or result.stdout or "").strip()
848
- if error_msg:
849
- print(f"⚠️ 格式化失败 ({os.path.basename(file_path)}, {tool_name}): {error_msg[:200]}")
850
- except subprocess.TimeoutExpired:
851
- print(f"⚠️ 格式化超时: {os.path.basename(file_path)} ({tool_name})")
852
- except FileNotFoundError:
853
- # 工具未安装,跳过
854
- continue
855
- except Exception as e:
856
- # 其他错误,记录但继续
857
- print(f"⚠️ 格式化失败 ({os.path.basename(file_path)}, {tool_name}): {str(e)[:100]}")
858
- continue
859
-
860
- if formatted_files:
861
- print(f"✅ 已格式化 {len(formatted_files)} 个文件")
862
- # 暂存格式化后的文件
863
- try:
864
- for file_path in formatted_files:
865
- abs_file_path = os.path.join(self.root_dir, file_path) if not os.path.isabs(file_path) else file_path
866
- if os.path.exists(abs_file_path):
867
- subprocess.run(
868
- ["git", "add", file_path],
869
- cwd=self.root_dir,
870
- check=False,
871
- stdout=subprocess.DEVNULL,
872
- stderr=subprocess.DEVNULL,
873
- )
874
- except Exception:
875
- pass
876
-
877
- def _handle_commit_confirmation(
878
- self,
879
- commits: List[Tuple[str, str]],
880
- start_commit: Optional[str],
881
- prefix: str,
882
- suffix: str,
883
- ) -> None:
884
- """处理提交确认和可能的重置"""
885
- if commits and user_confirm("是否接受以上提交记录?", True):
886
- # 统计接受的commit数量
887
- from jarvis.jarvis_stats.stats import StatsManager
888
-
889
- StatsManager.increment("commits_accepted", group="code_agent")
890
-
891
- subprocess.run(
892
- ["git", "reset", "--mixed", str(start_commit)],
893
- stdout=subprocess.DEVNULL,
894
- stderr=subprocess.DEVNULL,
895
- check=True,
896
- )
897
-
898
- # 检测变更文件并格式化
899
- modified_files = get_diff_file_list()
900
- if modified_files:
901
- self._format_modified_files(modified_files)
902
-
903
- git_commiter = GitCommitTool()
904
- git_commiter.execute({"prefix": prefix, "suffix": suffix, "agent": self, "model_group": getattr(self.model, "model_group", None)})
905
-
906
- # 在用户接受commit后,根据配置决定是否保存记忆
907
- if self.force_save_memory:
908
- self.memory_manager.prompt_memory_save()
909
- elif start_commit:
910
- if user_confirm("是否要重置到初始提交?", True):
911
- os.system(f"git reset --hard {str(start_commit)}") # 确保转换为字符串
912
- print("ℹ️ 已重置到初始提交")
913
220
 
914
221
  def run(self, user_input: str, prefix: str = "", suffix: str = "") -> Optional[str]:
915
- """使用给定的用户输入运行代码代理。
222
+ """使用给定的用户输入运行代码代理.
916
223
 
917
224
  参数:
918
225
  user_input: 用户的需求/请求
@@ -920,10 +227,42 @@ class CodeAgent(Agent):
920
227
  返回:
921
228
  str: 描述执行结果的输出,成功时返回None
922
229
  """
923
- prev_dir = os.getcwd()
924
230
  try:
925
- self._init_env(prefix, suffix)
231
+ set_current_agent(self.name, self)
232
+
233
+ # 根据当前模式生成额外说明,供 LLM 感知执行策略
234
+ prev_dir = os.getcwd()
235
+ non_interactive_note = ""
236
+ if getattr(self, "non_interactive", False):
237
+ non_interactive_note = (
238
+ "\n\n[系统说明]\n"
239
+ "本次会话处于**非交互模式**:\n"
240
+ "- 在 PLAN 模式中给出清晰、可执行的详细计划后,应**自动进入 EXECUTE 模式执行计划**,不要等待用户额外确认;\n"
241
+ "- 在 EXECUTE 模式中,保持一步一步的小步提交和可回退策略,但不需要向用户反复询问“是否继续”;\n"
242
+ "- 如遇信息严重不足,可以在 RESEARCH 模式中自行补充必要分析,而不是卡在等待用户输入。\n"
243
+ )
244
+
245
+ self.git_manager.init_env(prefix, suffix, self)
926
246
  start_commit = get_latest_commit_hash()
247
+ self.start_commit = start_commit
248
+
249
+ # 将初始 commit 信息添加到 addon_prompt(安全回退点)
250
+ if start_commit:
251
+ initial_commit_prompt = f"""
252
+ **🔖 初始 Git Commit(安全回退点)**:
253
+ 本次任务开始时的初始 commit 是:`{start_commit}`
254
+
255
+ **⚠️ 重要提示**:如果文件被破坏得很严重无法恢复,可以使用以下命令重置到这个初始 commit:
256
+ ```bash
257
+ git reset --hard {start_commit}
258
+ ```
259
+ 这将丢弃所有未提交的更改,将工作区恢复到任务开始时的状态。请谨慎使用此命令,确保这是你真正想要的操作。
260
+ """
261
+ # 将初始 commit 信息追加到现有的 addon_prompt
262
+ current_addon = self.session.addon_prompt or ""
263
+ self.set_addon_prompt(
264
+ f"{current_addon}\n{initial_commit_prompt}".strip()
265
+ )
927
266
 
928
267
  # 获取项目概况信息
929
268
  project_overview = get_project_overview(self.root_dir)
@@ -932,9 +271,11 @@ class CodeAgent(Agent):
932
271
  1. 每次响应仅执行一步操作,先分析再修改,避免一步多改。
933
272
  2. 充分利用工具理解用户需求和现有代码,禁止凭空假设。
934
273
  3. 如果不清楚要修改的文件,必须先分析并找出需要修改的文件,明确目标后再进行编辑。
935
- 4. 代码编辑任务优先使用 PATCH 操作,确保搜索文本在目标文件中有且仅有一次精确匹配,保证修改的准确性和安全性。
936
- 5. 如需大范围重写,才可使用 REWRITE 操作。
937
- 6. 如遇信息不明,优先调用工具补充分析,不要主观臆断。
274
+ 4. 对于简单的文本替换,推荐使用 edit_file 工具进行精确修改。避免使用 sed 命令,因为sed极易出错且可能产生不可预期的结果。对于复杂代码(超过50行或涉及多文件协调),禁止直接使用sed或python脚本编辑,必须使用task_list_manager创建任务列表进行安全拆分。
275
+ 5. 代码编辑任务优先使用 PATCH 操作,确保搜索文本在目标文件中有且仅有一次精确匹配,保证修改的准确性和安全性。
276
+ 6. 如需大范围重写(超过200行或涉及重构),请使用 edit_file 工具配合空search参数 "",并提前备份原始文件。
277
+ 7. 如遇信息不明,优先调用工具补充分析,不要主观臆断。
278
+ 8. **重要:清理临时文件**:开发过程中产生的临时文件(如测试文件、调试脚本、备份文件、临时日志等)必须在提交前清理删除,否则会被自动提交到git仓库。如果创建了临时文件用于调试或测试,完成后必须立即删除。
938
279
  """
939
280
 
940
281
  # 智能上下文推荐:根据用户输入推荐相关上下文
@@ -943,25 +284,21 @@ class CodeAgent(Agent):
943
284
  # 在意图识别和上下文推荐期间抑制模型输出
944
285
  was_suppressed = False
945
286
  if self.model:
946
- was_suppressed = getattr(self.model, '_suppress_output', False)
287
+ was_suppressed = getattr(self.model, "_suppress_output", False)
947
288
  self.model.set_suppress_output(True)
948
289
  try:
949
- print("🔍 正在进行智能上下文推荐....")
950
-
951
290
  # 生成上下文推荐(基于关键词和项目上下文)
952
291
  recommendation = self.context_recommender.recommend_context(
953
292
  user_input=user_input,
954
293
  )
955
-
294
+
956
295
  # 格式化推荐结果
957
- context_recommendation_text = self.context_recommender.format_recommendation(recommendation)
958
-
959
- # 打印推荐的上下文
960
- if context_recommendation_text:
961
- print(f"ℹ️ {context_recommendation_text}")
962
- except Exception as e:
296
+ context_recommendation_text = (
297
+ self.context_recommender.format_recommendation(recommendation)
298
+ )
299
+ except Exception:
963
300
  # 上下文推荐失败不应该影响主流程
964
- print(f"⚠️ 上下文推荐失败: {e}")
301
+ pass
965
302
  finally:
966
303
  # 恢复模型输出设置
967
304
  if self.model:
@@ -972,553 +309,147 @@ class CodeAgent(Agent):
972
309
  project_overview
973
310
  + "\n\n"
974
311
  + first_tip
312
+ + non_interactive_note
975
313
  + context_recommendation_text
976
314
  + "\n\n任务描述:\n"
977
315
  + user_input
978
316
  )
979
317
  else:
980
- enhanced_input = first_tip + context_recommendation_text + "\n\n任务描述:\n" + user_input
318
+ enhanced_input = (
319
+ first_tip
320
+ + non_interactive_note
321
+ + context_recommendation_text
322
+ + "\n\n任务描述:\n"
323
+ + user_input
324
+ )
981
325
 
982
326
  try:
983
327
  if self.model:
984
328
  self.model.set_suppress_output(False)
985
- super().run(enhanced_input)
329
+ result = super().run(enhanced_input)
330
+ # 确保返回值是 str 或 None
331
+ if result is None:
332
+ result_str = None
333
+ else:
334
+ result_str = str(result)
986
335
  except RuntimeError as e:
987
- print(f"⚠️ 执行失败: {str(e)}")
336
+ PrettyOutput.auto_print(f"⚠️ 执行失败: {str(e)}")
988
337
  return str(e)
989
338
 
339
+ # 处理未提交的更改(在 review 之前先提交)
340
+ self.git_manager.handle_uncommitted_changes()
341
+
342
+ # 如果启用了 review,执行 review 和修复循环
343
+ if not self.disable_review:
344
+ self._review_and_fix(
345
+ user_input=user_input,
346
+ enhanced_input=enhanced_input,
347
+ prefix=prefix,
348
+ suffix=suffix,
349
+ code_generation_summary=result_str,
350
+ )
990
351
 
991
-
992
- self._handle_uncommitted_changes()
993
352
  end_commit = get_latest_commit_hash()
994
- commits = self._show_commit_history(start_commit, end_commit)
995
- self._handle_commit_confirmation(commits, start_commit, prefix, suffix)
996
- return None
353
+ commits = self.git_manager.show_commit_history(
354
+ self.start_commit, end_commit
355
+ )
356
+ self.git_manager.handle_commit_confirmation(
357
+ commits,
358
+ self.start_commit,
359
+ prefix,
360
+ suffix,
361
+ self,
362
+ self.post_process_manager.post_process_modified_files,
363
+ )
364
+ return result_str
997
365
 
998
366
  except RuntimeError as e:
999
367
  return f"Error during execution: {str(e)}"
1000
368
  finally:
369
+ # 在run方法结束时反注册agent
370
+ clear_current_agent()
371
+
1001
372
  # Ensure switching back to the original working directory after CodeAgent completes
1002
373
  try:
1003
374
  os.chdir(prev_dir)
1004
375
  except Exception:
1005
376
  pass
1006
377
 
1007
- def _build_name_status_map(self) -> dict:
1008
- """构造按文件的状态映射与差异文本,删除文件不展示diff,仅提示删除"""
1009
- status_map = {}
1010
- try:
1011
- head_exists = bool(get_latest_commit_hash())
1012
- # 临时 -N 以包含未跟踪文件的差异检测
1013
- subprocess.run(["git", "add", "-N", "."], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
1014
- cmd = ["git", "diff", "--name-status"] + (["HEAD"] if head_exists else [])
1015
- res = subprocess.run(
1016
- cmd,
1017
- capture_output=True,
1018
- text=True,
1019
- encoding="utf-8",
1020
- errors="replace",
1021
- check=False,
1022
- )
1023
- finally:
1024
- subprocess.run(["git", "reset"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
1025
-
1026
- if res.returncode == 0 and res.stdout:
1027
- for line in res.stdout.splitlines():
1028
- if not line.strip():
1029
- continue
1030
- parts = line.split("\t")
1031
- if not parts:
1032
- continue
1033
- status = parts[0]
1034
- if status.startswith("R") or status.startswith("C"):
1035
- # 重命名/复制:使用新路径作为键
1036
- if len(parts) >= 3:
1037
- old_path, new_path = parts[1], parts[2]
1038
- status_map[new_path] = status
1039
- # 也记录旧路径,便于匹配 name-only 的结果
1040
- status_map[old_path] = status
1041
- elif len(parts) >= 2:
1042
- status_map[parts[-1]] = status
1043
- else:
1044
- if len(parts) >= 2:
1045
- status_map[parts[1]] = status
1046
- return status_map
378
+ def _on_after_tool_call(
379
+ self,
380
+ agent: Agent,
381
+ current_response: Optional[str] = None,
382
+ need_return: Optional[bool] = None,
383
+ tool_prompt: Optional[str] = None,
384
+ **kwargs: Any,
385
+ ) -> None:
386
+ """工具调用后回调函数。"""
1047
387
 
1048
- def _get_file_diff(self, file_path: str) -> str:
1049
- """获取单文件的diff,包含新增文件内容;失败时返回空字符串"""
1050
- head_exists = bool(get_latest_commit_hash())
1051
- try:
1052
- # 为了让未跟踪文件也能展示diff,临时 -N 该文件
1053
- subprocess.run(["git", "add", "-N", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
1054
- cmd = ["git", "diff"] + (["HEAD"] if head_exists else []) + ["--", file_path]
1055
- res = subprocess.run(
1056
- cmd,
1057
- capture_output=True,
1058
- text=True,
1059
- encoding="utf-8",
1060
- errors="replace",
1061
- check=False,
1062
- )
1063
- if res.returncode == 0:
1064
- return res.stdout or ""
1065
- return ""
1066
- finally:
1067
- subprocess.run(["git", "reset", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
388
+ final_ret = ""
389
+ diff = get_diff()
1068
390
 
1069
- def _build_per_file_patch_preview(self, modified_files: List[str]) -> str:
1070
- """构建按文件的补丁预览"""
1071
- status_map = self._build_name_status_map()
1072
- lines: List[str] = []
391
+ if diff:
392
+ start_hash = get_latest_commit_hash()
393
+ modified_files = get_diff_file_list()
1073
394
 
1074
- def _get_file_numstat(file_path: str) -> Tuple[int, int]:
1075
- """获取单文件的新增/删除行数,失败时返回(0,0)"""
1076
- head_exists = bool(get_latest_commit_hash())
395
+ # 使用增强的 diff 可视化(如果可用)
1077
396
  try:
1078
- # 让未跟踪文件也能统计到新增行数
1079
- subprocess.run(["git", "add", "-N", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
1080
- cmd = ["git", "diff", "--numstat"] + (["HEAD"] if head_exists else []) + ["--", file_path]
1081
- res = subprocess.run(
1082
- cmd,
1083
- capture_output=True,
1084
- text=True,
1085
- encoding="utf-8",
1086
- errors="replace",
1087
- check=False,
397
+ from jarvis.jarvis_code_agent.diff_visualizer import (
398
+ visualize_diff_enhanced,
1088
399
  )
1089
- if res.returncode == 0 and res.stdout:
1090
- for line in res.stdout.splitlines():
1091
- parts = line.strip().split("\t")
1092
- if len(parts) >= 3:
1093
- add_s, del_s = parts[0], parts[1]
1094
-
1095
- def to_int(x: str) -> int:
1096
- try:
1097
- return int(x)
1098
- except Exception:
1099
- # 二进制或无法解析时显示为0
1100
- return 0
1101
-
1102
- return to_int(add_s), to_int(del_s)
1103
- finally:
1104
- subprocess.run(["git", "reset", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
1105
- return (0, 0)
1106
-
1107
- for f in modified_files:
1108
- status = status_map.get(f, "")
1109
- adds, dels = _get_file_numstat(f)
1110
- total_changes = adds + dels
1111
-
1112
- # 删除文件:不展示diff,仅提示(附带删除行数信息如果可用)
1113
- if (status.startswith("D")) or (not os.path.exists(f)):
1114
- if dels > 0:
1115
- lines.append(f"- {f} 文件被删除(删除{dels}行)")
1116
- else:
1117
- lines.append(f"- {f} 文件被删除")
1118
- continue
1119
-
1120
- # 变更过大:仅提示新增/删除行数,避免输出超长diff
1121
- if total_changes > 300:
1122
- lines.append(f"- {f} 新增{adds}行/删除{dels}行(变更过大,预览已省略)")
1123
- continue
400
+ from jarvis.jarvis_utils.config import get_diff_show_line_numbers
401
+ from jarvis.jarvis_utils.config import get_diff_visualization_mode
402
+
403
+ # 显示整体 diff(使用增强可视化)
404
+ visualization_mode = get_diff_visualization_mode()
405
+ show_line_numbers = get_diff_show_line_numbers()
406
+ # 构建文件路径显示(多文件时显示所有文件名)
407
+ file_path_display = ", ".join(modified_files) if modified_files else ""
408
+ visualize_diff_enhanced(
409
+ diff,
410
+ file_path=file_path_display,
411
+ mode=visualization_mode,
412
+ show_line_numbers=show_line_numbers,
413
+ )
414
+ except ImportError:
415
+ # 如果导入失败,回退到原有方式
416
+ PrettyOutput.print(diff, OutputType.CODE, lang="diff")
417
+ except Exception as e:
418
+ # 如果可视化失败,回退到原有方式
419
+ PrettyOutput.auto_print(f"⚠️ Diff 可视化失败,使用默认方式: {e}")
420
+ PrettyOutput.print(diff, OutputType.CODE, lang="diff")
1124
421
 
1125
- # 其它情况:展示该文件的diff
1126
- file_diff = self._get_file_diff(f)
1127
- if file_diff.strip():
1128
- lines.append(f"文件: {f}\n```diff\n{file_diff}\n```")
1129
- else:
1130
- # 当无法获取到diff(例如重命名或特殊状态),避免空输出
1131
- lines.append(f"- {f} 变更已记录(无可展示的文本差异)")
1132
- return "\n".join(lines)
422
+ # 更新上下文管理器
423
+ self.impact_manager.update_context_for_modified_files(modified_files)
1133
424
 
1134
- def _update_context_for_modified_files(self, modified_files: List[str]) -> None:
1135
- """更新上下文管理器:当文件被修改后,更新符号表和依赖图"""
1136
- if not modified_files:
1137
- return
1138
- print("🔄 正在更新代码上下文...")
1139
- for file_path in modified_files:
1140
- if os.path.exists(file_path):
1141
- try:
1142
- with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
1143
- content = f.read()
1144
- self.context_manager.update_context_for_file(file_path, content)
1145
- except Exception:
1146
- # 如果读取文件失败,跳过更新
1147
- pass
425
+ # 进行影响范围分析
426
+ impact_report = self.impact_manager.analyze_edit_impact(modified_files)
1148
427
 
1149
- def _analyze_edit_impact(self, modified_files: List[str]) -> Optional[Any]:
1150
- """进行影响范围分析(如果启用)
1151
-
1152
- Returns:
1153
- ImpactReport: 影响分析报告,如果未启用或失败则返回None
1154
- """
1155
- if not is_enable_impact_analysis():
1156
- return None
1157
-
1158
- print("🔍 正在进行变更影响分析...")
1159
- try:
1160
- impact_analyzer = ImpactAnalyzer(self.context_manager)
1161
- all_edits = []
1162
- for file_path in modified_files:
1163
- if os.path.exists(file_path):
1164
- edits = parse_git_diff_to_edits(file_path, self.root_dir)
1165
- all_edits.extend(edits)
1166
-
1167
- if not all_edits:
1168
- return None
1169
-
1170
- # 按文件分组编辑
1171
- edits_by_file = {}
1172
- for edit in all_edits:
1173
- if edit.file_path not in edits_by_file:
1174
- edits_by_file[edit.file_path] = []
1175
- edits_by_file[edit.file_path].append(edit)
1176
-
1177
- # 对每个文件进行影响分析
1178
- impact_report = None
1179
- for file_path, edits in edits_by_file.items():
1180
- report = impact_analyzer.analyze_edit_impact(file_path, edits)
1181
- if report:
1182
- # 合并报告
1183
- if impact_report is None:
1184
- impact_report = report
1185
- else:
1186
- # 合并多个报告,去重
1187
- impact_report.affected_files = list(set(impact_report.affected_files + report.affected_files))
1188
-
1189
- # 合并符号(基于文件路径和名称去重)
1190
- symbol_map = {}
1191
- for symbol in impact_report.affected_symbols + report.affected_symbols:
1192
- key = (symbol.file_path, symbol.name, symbol.line_start)
1193
- if key not in symbol_map:
1194
- symbol_map[key] = symbol
1195
- impact_report.affected_symbols = list(symbol_map.values())
1196
-
1197
- impact_report.affected_tests = list(set(impact_report.affected_tests + report.affected_tests))
1198
-
1199
- # 合并接口变更(基于符号名和文件路径去重)
1200
- interface_map = {}
1201
- for change in impact_report.interface_changes + report.interface_changes:
1202
- key = (change.file_path, change.symbol_name, change.change_type)
1203
- if key not in interface_map:
1204
- interface_map[key] = change
1205
- impact_report.interface_changes = list(interface_map.values())
1206
-
1207
- impact_report.impacts.extend(report.impacts)
1208
-
1209
- # 合并建议
1210
- impact_report.recommendations = list(set(impact_report.recommendations + report.recommendations))
1211
-
1212
- # 使用更高的风险等级
1213
- if report.risk_level.value == 'high' or impact_report.risk_level.value == 'high':
1214
- impact_report.risk_level = report.risk_level if report.risk_level.value == 'high' else impact_report.risk_level
1215
- elif report.risk_level.value == 'medium':
1216
- impact_report.risk_level = report.risk_level
1217
-
1218
- return impact_report
1219
- except Exception as e:
1220
- # 影响分析失败不应该影响主流程,仅记录日志
1221
- print(f"⚠️ 影响范围分析失败: {e}")
1222
- return None
1223
-
1224
- def _handle_impact_report(self, impact_report: Optional[Any], agent: Agent, final_ret: str) -> str:
1225
- """处理影响范围分析报告
1226
-
1227
- Args:
1228
- impact_report: 影响分析报告
1229
- agent: Agent实例
1230
- final_ret: 当前的结果字符串
1231
-
1232
- Returns:
1233
- 更新后的结果字符串
1234
- """
1235
- if not impact_report:
1236
- return final_ret
1237
-
1238
- impact_summary = impact_report.to_string(self.root_dir)
1239
- final_ret += f"\n\n{impact_summary}\n"
1240
-
1241
- # 如果是高风险,在提示词中提醒
1242
- if impact_report.risk_level.value == 'high':
1243
- agent.set_addon_prompt(
1244
- f"{agent.get_addon_prompt() or ''}\n\n"
1245
- f"⚠️ 高风险编辑警告:\n"
1246
- f"检测到此编辑为高风险操作,请仔细检查以下内容:\n"
1247
- f"- 受影响文件: {len(impact_report.affected_files)} 个\n"
1248
- f"- 接口变更: {len(impact_report.interface_changes)} 个\n"
1249
- f"- 相关测试: {len(impact_report.affected_tests)} 个\n"
1250
- f"建议运行相关测试并检查所有受影响文件。"
1251
- )
1252
-
1253
- return final_ret
1254
-
1255
- def _handle_build_validation_disabled(self, modified_files: List[str], config: Any, agent: Agent, final_ret: str) -> str:
1256
- """处理构建验证已禁用的情况
1257
-
1258
- Returns:
1259
- 更新后的结果字符串
1260
- """
1261
- reason = config.get_disable_reason()
1262
- reason_text = f"(原因: {reason})" if reason else ""
1263
- final_ret += f"\n\nℹ️ 构建验证已禁用{reason_text},仅进行基础静态检查\n"
1264
-
1265
- # 输出基础静态检查日志
1266
- file_count = len(modified_files)
1267
- files_str = ", ".join(os.path.basename(f) for f in modified_files[:3])
1268
- if file_count > 3:
1269
- files_str += f" 等{file_count}个文件"
1270
-
1271
- # 使用兜底验证器进行基础静态检查
1272
- fallback_validator = FallbackBuildValidator(self.root_dir, timeout=get_build_validation_timeout())
1273
- static_check_result = fallback_validator.validate(modified_files)
1274
- if not static_check_result.success:
1275
- final_ret += f"\n⚠️ 基础静态检查失败:\n{static_check_result.error_message or static_check_result.output}\n"
1276
- agent.set_addon_prompt(
1277
- f"基础静态检查失败,请根据以下错误信息修复代码:\n{static_check_result.error_message or static_check_result.output}\n"
1278
- )
1279
- else:
1280
- final_ret += f"\n✅ 基础静态检查通过(耗时 {static_check_result.duration:.2f}秒)\n"
1281
-
1282
- return final_ret
1283
-
1284
- def _handle_build_validation_failure(self, build_validation_result: Any, config: Any, modified_files: List[str], agent: Agent, final_ret: str) -> str:
1285
- """处理构建验证失败的情况
1286
-
1287
- Returns:
1288
- 更新后的结果字符串
1289
- """
1290
- if not config.has_been_asked():
1291
- # 首次失败,询问用户
1292
- error_preview = _format_build_error(build_validation_result)
1293
- print(f"\n⚠️ 构建验证失败:\n{error_preview}\n")
1294
- print("ℹ️ 提示:如果此项目需要在特殊环境(如容器)中构建,或使用独立构建脚本,"
1295
- "可以选择禁用构建验证,后续将仅进行基础静态检查。")
1296
-
1297
- if user_confirm(
1298
- "是否要禁用构建验证,后续仅进行基础静态检查?",
1299
- default=False,
1300
- ):
1301
- # 用户选择禁用
1302
- config.disable_build_validation(
1303
- reason="用户选择禁用(项目可能需要在特殊环境中构建)"
1304
- )
1305
- config.mark_as_asked()
1306
- final_ret += "\n\nℹ️ 已禁用构建验证,后续将仅进行基础静态检查\n"
1307
-
1308
- # 输出基础静态检查日志
1309
- file_count = len(modified_files)
1310
- files_str = ", ".join(os.path.basename(f) for f in modified_files[:3])
1311
- if file_count > 3:
1312
- files_str += f" 等{file_count}个文件"
1313
-
1314
- # 立即进行基础静态检查
1315
- fallback_validator = FallbackBuildValidator(self.root_dir, timeout=get_build_validation_timeout())
1316
- static_check_result = fallback_validator.validate(modified_files)
1317
- if not static_check_result.success:
1318
- final_ret += f"\n⚠️ 基础静态检查失败:\n{static_check_result.error_message or static_check_result.output}\n"
1319
- agent.set_addon_prompt(
1320
- f"基础静态检查失败,请根据以下错误信息修复代码:\n{static_check_result.error_message or static_check_result.output}\n"
1321
- )
1322
- else:
1323
- final_ret += f"\n✅ 基础静态检查通过(耗时 {static_check_result.duration:.2f}秒)\n"
1324
- else:
1325
- # 用户选择继续验证,标记为已询问
1326
- config.mark_as_asked()
1327
- final_ret += f"\n\n⚠️ 构建验证失败:\n{_format_build_error(build_validation_result)}\n"
1328
- # 如果构建失败,添加修复提示
1329
- agent.set_addon_prompt(
1330
- f"构建验证失败,请根据以下错误信息修复代码:\n{_format_build_error(build_validation_result)}\n"
1331
- "请仔细检查错误信息,修复编译/构建错误后重新提交。"
1332
- )
1333
- else:
1334
- # 已经询问过,直接显示错误
1335
- final_ret += f"\n\n⚠️ 构建验证失败:\n{_format_build_error(build_validation_result)}\n"
1336
- # 如果构建失败,添加修复提示
1337
- agent.set_addon_prompt(
1338
- f"构建验证失败,请根据以下错误信息修复代码:\n{_format_build_error(build_validation_result)}\n"
1339
- "请仔细检查错误信息,修复编译/构建错误后重新提交。"
428
+ per_file_preview = self.diff_manager.build_per_file_patch_preview(
429
+ modified_files, use_enhanced_visualization=False
1340
430
  )
1341
-
1342
- return final_ret
1343
-
1344
- def _handle_build_validation(self, modified_files: List[str], agent: Agent, final_ret: str) -> Tuple[Optional[Any], str]:
1345
- """处理构建验证
1346
-
1347
- Returns:
1348
- (build_validation_result, updated_final_ret)
1349
- """
1350
- if not is_enable_build_validation():
1351
- return None, final_ret
1352
-
1353
- config = BuildValidationConfig(self.root_dir)
1354
-
1355
- # 检查是否已禁用构建验证
1356
- if config.is_build_validation_disabled():
1357
- final_ret = self._handle_build_validation_disabled(modified_files, config, agent, final_ret)
1358
- return None, final_ret
1359
-
1360
- # 未禁用,进行构建验证
1361
- build_validation_result = self._validate_build_after_edit(modified_files)
1362
- if build_validation_result:
1363
- if not build_validation_result.success:
1364
- final_ret = self._handle_build_validation_failure(
1365
- build_validation_result, config, modified_files, agent, final_ret
1366
- )
1367
- else:
1368
- build_system_info = f" ({build_validation_result.build_system.value})" if build_validation_result.build_system else ""
1369
- final_ret += f"\n\n✅ 构建验证通过{build_system_info}(耗时 {build_validation_result.duration:.2f}秒)\n"
1370
-
1371
- return build_validation_result, final_ret
1372
-
1373
- def _handle_static_analysis(self, modified_files: List[str], build_validation_result: Optional[Any], config: Any, agent: Agent, final_ret: str) -> str:
1374
- """处理静态分析
1375
-
1376
- Returns:
1377
- 更新后的结果字符串
1378
- """
1379
- # 检查是否启用静态分析
1380
- if not is_enable_static_analysis():
1381
- print("ℹ️ 静态分析已禁用,跳过静态检查")
1382
- return final_ret
1383
-
1384
- # 检查是否有可用的lint工具
1385
- lint_tools_info = "\n".join(
1386
- f" - {file}: 使用 {'、'.join(get_lint_tools(file))}"
1387
- for file in modified_files
1388
- if get_lint_tools(file)
1389
- )
1390
-
1391
- if not lint_tools_info:
1392
- print("ℹ️ 未找到可用的静态检查工具,跳过静态检查")
1393
- return final_ret
1394
-
1395
- # 如果构建验证失败且未禁用,不进行静态分析(避免重复错误)
1396
- # 如果构建验证已禁用,则进行静态分析(因为只做了基础静态检查)
1397
- should_skip_static = (
1398
- build_validation_result
1399
- and not build_validation_result.success
1400
- and not config.is_build_validation_disabled()
1401
- )
1402
-
1403
- if should_skip_static:
1404
- print("ℹ️ 构建验证失败,跳过静态分析(避免重复错误)")
1405
- return final_ret
1406
-
1407
- # 直接执行静态扫描
1408
- lint_results = self._run_static_analysis(modified_files)
1409
- if lint_results:
1410
- # 有错误或警告,让大模型修复
1411
- errors_summary = self._format_lint_results(lint_results)
1412
- # 打印完整的检查结果
1413
- print(f"⚠️ 静态扫描发现问题:\n{errors_summary}")
1414
- addon_prompt = f"""
1415
- 静态扫描发现以下问题,请根据错误信息修复代码:
1416
-
1417
- {errors_summary}
1418
-
1419
- 请仔细检查并修复所有问题。
1420
- """
1421
- agent.set_addon_prompt(addon_prompt)
1422
- final_ret += "\n\n⚠️ 静态扫描发现问题,已提示修复\n"
1423
- else:
1424
- final_ret += "\n\n✅ 静态扫描通过\n"
1425
-
1426
- return final_ret
1427
431
 
1428
- def _ask_llm_about_large_deletion(self, detection_result: Dict[str, int], preview: str) -> bool:
1429
- """询问大模型大量代码删除是否合理
1430
-
1431
- 参数:
1432
- detection_result: 检测结果字典,包含 'insertions', 'deletions', 'net_deletions'
1433
- preview: 补丁预览内容
1434
-
1435
- 返回:
1436
- bool: 如果大模型认为合理返回True,否则返回False
1437
- """
1438
- if not self.model:
1439
- # 如果没有模型,默认认为合理
1440
- return True
1441
-
1442
- insertions = detection_result['insertions']
1443
- deletions = detection_result['deletions']
1444
- net_deletions = detection_result['net_deletions']
1445
-
1446
- prompt = f"""检测到大量代码删除,请判断是否合理:
1447
-
1448
- 统计信息:
1449
- - 新增行数: {insertions}
1450
- - 删除行数: {deletions}
1451
- - 净删除行数: {net_deletions}
1452
-
1453
- 补丁预览:
1454
- {preview}
1455
-
1456
- 请仔细分析以上代码变更,判断这些大量代码删除是否合理。可能的情况包括:
1457
- 1. 重构代码,删除冗余或过时的代码
1458
- 2. 简化实现,用更简洁的代码替换复杂的实现
1459
- 3. 删除未使用的代码或功能
1460
- 4. 错误地删除了重要代码
1461
-
1462
- 请使用以下协议回答(必须包含且仅包含以下标记之一):
1463
- - 如果认为这些删除是合理的,回答: <!!!YES!!!>
1464
- - 如果认为这些删除不合理或存在风险,回答: <!!!NO!!!>
1465
-
1466
- 请严格按照协议格式回答,不要添加其他内容。
1467
- """
1468
-
1469
- try:
1470
- print("🤖 正在询问大模型判断大量代码删除是否合理...")
1471
- response = self.model.chat_until_success(prompt) # type: ignore
1472
-
1473
- # 使用确定的协议标记解析回答
1474
- if "<!!!YES!!!>" in response:
1475
- print("✅ 大模型确认:代码删除合理")
1476
- return True
1477
- elif "<!!!NO!!!>" in response:
1478
- print("⚠️ 大模型确认:代码删除不合理")
1479
- return False
1480
- else:
1481
- # 如果无法找到协议标记,默认认为不合理(保守策略)
1482
- print(f"⚠️ 无法找到协议标记,默认认为不合理。回答内容: {response[:200]}")
1483
- return False
1484
- except Exception as e:
1485
- # 如果询问失败,默认认为不合理(保守策略)
1486
- print(f"⚠️ 询问大模型失败: {str(e)},默认认为不合理")
1487
- return False
1488
-
1489
- def _on_after_tool_call(self, agent: Agent, current_response=None, need_return=None, tool_prompt=None, **kwargs) -> None:
1490
- """工具调用后回调函数。"""
1491
- final_ret = ""
1492
- diff = get_diff()
1493
-
1494
- if diff:
1495
- start_hash = get_latest_commit_hash()
1496
- PrettyOutput.print(diff, OutputType.CODE, lang="diff") # 保留语法高亮
1497
- modified_files = get_diff_file_list()
1498
-
1499
- # 更新上下文管理器
1500
- self._update_context_for_modified_files(modified_files)
1501
-
1502
- # 进行影响范围分析
1503
- impact_report = self._analyze_edit_impact(modified_files)
1504
-
1505
- per_file_preview = self._build_per_file_patch_preview(modified_files)
1506
-
1507
432
  # 所有模式下,在提交前检测大量代码删除并询问大模型
1508
433
  detection_result = detect_large_code_deletion()
1509
434
  if detection_result is not None:
1510
435
  # 检测到大量代码删除,询问大模型是否合理
1511
- is_reasonable = self._ask_llm_about_large_deletion(detection_result, per_file_preview)
436
+ is_reasonable = self.llm_manager.ask_llm_about_large_deletion(
437
+ detection_result, per_file_preview
438
+ )
1512
439
  if not is_reasonable:
1513
440
  # 大模型认为不合理,撤销修改
1514
- print("ℹ️ 已撤销修改(大模型认为代码删除不合理)")
441
+ PrettyOutput.auto_print("ℹ️ 已撤销修改(大模型认为代码删除不合理)")
1515
442
  revert_change()
1516
- final_ret += "\n\n修改被撤销(检测到大量代码删除且大模型判断不合理)\n"
443
+ final_ret += (
444
+ "\n\n修改被撤销(检测到大量代码删除且大模型判断不合理)\n"
445
+ )
1517
446
  final_ret += f"# 补丁预览(按文件):\n{per_file_preview}"
1518
- PrettyOutput.print(final_ret, OutputType.USER, lang="markdown") # 保留语法高亮
447
+ PrettyOutput.print(
448
+ final_ret, OutputType.USER, lang="markdown"
449
+ ) # 保留语法高亮
1519
450
  self.session.prompt += final_ret
1520
451
  return
1521
-
452
+
1522
453
  commited = handle_commit_workflow()
1523
454
  if commited:
1524
455
  # 统计代码行数变化
@@ -1533,7 +464,7 @@ class CodeAgent(Agent):
1533
464
  check=True,
1534
465
  )
1535
466
  if diff_result.returncode == 0 and diff_result.stdout:
1536
- self._record_code_changes_stats(diff_result.stdout)
467
+ self.git_manager.record_code_changes_stats(diff_result.stdout)
1537
468
  except subprocess.CalledProcessError:
1538
469
  pass
1539
470
 
@@ -1550,8 +481,12 @@ class CodeAgent(Agent):
1550
481
  if commits:
1551
482
  # 获取最新的提交信息(commits列表按时间倒序,第一个是最新的)
1552
483
  latest_commit_hash, latest_commit_message = commits[0]
1553
- commit_short_hash = latest_commit_hash[:7] if len(latest_commit_hash) >= 7 else latest_commit_hash
1554
-
484
+ commit_short_hash = (
485
+ latest_commit_hash[:7]
486
+ if len(latest_commit_hash) >= 7
487
+ else latest_commit_hash
488
+ )
489
+
1555
490
  final_ret += (
1556
491
  f"\n\n代码已修改完成\n"
1557
492
  f"✅ 已自动提交\n"
@@ -1559,16 +494,25 @@ class CodeAgent(Agent):
1559
494
  f" 提交信息: {latest_commit_message}\n"
1560
495
  f"\n补丁内容(按文件):\n{per_file_preview}\n"
1561
496
  )
1562
-
497
+
1563
498
  # 添加影响范围分析报告
1564
- final_ret = self._handle_impact_report(impact_report, self, final_ret)
1565
-
499
+ final_ret = self.impact_manager.handle_impact_report(
500
+ impact_report, self, final_ret
501
+ )
502
+
1566
503
  # 构建验证
1567
504
  config = BuildValidationConfig(self.root_dir)
1568
- build_validation_result, final_ret = self._handle_build_validation(modified_files, self, final_ret)
1569
-
505
+ (
506
+ build_validation_result,
507
+ final_ret,
508
+ ) = self.build_validation_manager.handle_build_validation(
509
+ modified_files, self, final_ret
510
+ )
511
+
1570
512
  # 静态分析
1571
- final_ret = self._handle_static_analysis(modified_files, build_validation_result, config, self, final_ret)
513
+ final_ret = self.lint_manager.handle_static_analysis(
514
+ modified_files, build_validation_result, config, self, final_ret
515
+ )
1572
516
  else:
1573
517
  # 如果没有获取到commits,尝试直接从end_hash获取commit信息
1574
518
  commit_info = ""
@@ -1582,9 +526,20 @@ class CodeAgent(Agent):
1582
526
  errors="replace",
1583
527
  check=False,
1584
528
  )
1585
- if result.returncode == 0 and result.stdout and "|" in result.stdout:
1586
- commit_hash, commit_message = result.stdout.strip().split("|", 1)
1587
- commit_short_hash = commit_hash[:7] if len(commit_hash) >= 7 else commit_hash
529
+ if (
530
+ result.returncode == 0
531
+ and result.stdout
532
+ and "|" in result.stdout
533
+ ):
534
+ (
535
+ commit_hash,
536
+ commit_message,
537
+ ) = result.stdout.strip().split("|", 1)
538
+ commit_short_hash = (
539
+ commit_hash[:7]
540
+ if len(commit_hash) >= 7
541
+ else commit_hash
542
+ )
1588
543
  commit_info = (
1589
544
  f"\n✅ 已自动提交\n"
1590
545
  f" Commit ID: {commit_short_hash} ({commit_hash})\n"
@@ -1592,7 +547,7 @@ class CodeAgent(Agent):
1592
547
  )
1593
548
  except Exception:
1594
549
  pass
1595
-
550
+
1596
551
  if commit_info:
1597
552
  final_ret += f"\n\n代码已修改完成{commit_info}\n"
1598
553
  else:
@@ -1619,186 +574,487 @@ class CodeAgent(Agent):
1619
574
  self.session.prompt += final_ret
1620
575
  return
1621
576
 
1622
- def _run_static_analysis(self, modified_files: List[str]) -> List[Tuple[str, str, str, int, str]]:
1623
- """执行静态分析
1624
-
1625
- Args:
1626
- modified_files: 修改的文件列表
1627
-
1628
- Returns:
1629
- [(tool_name, file_path, command, returncode, output), ...] 格式的结果列表
1630
- 只返回有错误或警告的结果(returncode != 0
577
+ def _truncate_diff_for_review(self, git_diff: str, token_ratio: float = 0.4) -> str:
578
+ """截断 git diff 以适应 token 限制(用于 review)
579
+
580
+ 参数:
581
+ git_diff: 原始的 git diff 内容
582
+ token_ratio: token 使用比例(默认 0.4,即 40%,review 需要更多上下文)
583
+
584
+ 返回:
585
+ str: 截断后的 git diff(如果超出限制则截断并添加提示、文件列表和起始 commit
1631
586
  """
1632
- if not modified_files:
1633
- return []
1634
-
1635
- # 获取所有lint命令
1636
- commands = get_lint_commands_for_files(modified_files, self.root_dir)
1637
- if not commands:
1638
- return []
1639
-
1640
- # 输出静态检查日志
1641
- file_count = len(modified_files)
1642
- files_str = ", ".join(os.path.basename(f) for f in modified_files[:3])
1643
- if file_count > 3:
1644
- files_str += f" 等{file_count}个文件"
1645
- tool_names = list(set(cmd[0] for cmd in commands))
1646
- tools_str = ", ".join(tool_names[:3])
1647
- if len(tool_names) > 3:
1648
- tools_str += f" 等{len(tool_names)}个工具"
1649
- print("🔍 静态检查中...")
1650
-
1651
- results = []
1652
- # 记录每个文件的检查结果
1653
- file_results = [] # [(file_path, tool_name, status, message), ...]
1654
-
1655
- # 按工具分组,相同工具可以批量执行
1656
- grouped = group_commands_by_tool(commands)
1657
-
1658
- for tool_name, file_commands in grouped.items():
1659
- for file_path, command in file_commands:
1660
- file_name = os.path.basename(file_path)
1661
- try:
1662
- # 检查文件是否存在
1663
- abs_file_path = os.path.join(self.root_dir, file_path) if not os.path.isabs(file_path) else file_path
1664
- if not os.path.exists(abs_file_path):
1665
- file_results.append((file_name, tool_name, "跳过", "文件不存在"))
1666
- continue
1667
-
1668
- # 打印执行的命令
1669
- print(f"ℹ️ 执行: {command}")
1670
-
1671
- # 执行命令
1672
- result = subprocess.run(
1673
- command,
1674
- shell=True,
1675
- cwd=self.root_dir,
1676
- capture_output=True,
1677
- text=True,
1678
- encoding="utf-8",
1679
- errors="replace",
1680
- timeout=600, # 600秒超时
587
+ if not git_diff or not git_diff.strip():
588
+ return git_diff
589
+
590
+ from jarvis.jarvis_utils.embedding import get_context_token_count
591
+ from jarvis.jarvis_utils.config import get_max_input_token_count
592
+
593
+ # 获取最大输入 token 数量
594
+ model_group = self.model.model_group if self.model else None
595
+ try:
596
+ max_input_tokens = get_max_input_token_count(model_group)
597
+ except Exception:
598
+ # 如果获取失败,使用默认值(约 100000 tokens)
599
+ max_input_tokens = 100000
600
+
601
+ # 使用指定比例作为 diff 的 token 限制
602
+ max_diff_tokens = int(max_input_tokens * token_ratio)
603
+
604
+ # 计算 diff 的 token 数量
605
+ diff_token_count = get_context_token_count(git_diff)
606
+
607
+ if diff_token_count <= max_diff_tokens:
608
+ return git_diff
609
+
610
+ # 如果 diff 内容太大,进行截断
611
+ # 先提取修改的文件列表和起始 commit
612
+ import re
613
+
614
+ files = set()
615
+ # 匹配 "diff --git a/path b/path" 格式
616
+ pattern = r"^diff --git a/([^\s]+) b/([^\s]+)$"
617
+ for line in git_diff.split("\n"):
618
+ match = re.match(pattern, line)
619
+ if match:
620
+ file_a = match.group(1)
621
+ file_b = match.group(2)
622
+ files.add(file_b)
623
+ if file_a != file_b:
624
+ files.add(file_a)
625
+ modified_files = sorted(list(files))
626
+
627
+ # 获取起始 commit id
628
+ start_commit = self.start_commit if hasattr(self, "start_commit") else None
629
+
630
+ lines = git_diff.split("\n")
631
+ truncated_lines = []
632
+ current_tokens = 0
633
+
634
+ for line in lines:
635
+ line_tokens = get_context_token_count(line)
636
+ if current_tokens + line_tokens > max_diff_tokens:
637
+ # 添加截断提示
638
+ truncated_lines.append("")
639
+ truncated_lines.append(
640
+ "# ⚠️ diff内容过大,已截断显示(review 需要更多上下文)"
641
+ )
642
+ truncated_lines.append(
643
+ f"# 原始diff共 {len(lines)} 行,{diff_token_count} tokens"
644
+ )
645
+ truncated_lines.append(
646
+ f"# 显示前 {len(truncated_lines) - 3} 行,约 {current_tokens} tokens"
647
+ )
648
+ truncated_lines.append(
649
+ f"# 限制: {max_diff_tokens} tokens (输入窗口的 {token_ratio * 100:.0f}%)"
650
+ )
651
+
652
+ # 添加起始 commit id
653
+ if start_commit:
654
+ truncated_lines.append("")
655
+ truncated_lines.append(f"# 起始 Commit ID: {start_commit}")
656
+
657
+ # 添加完整修改文件列表
658
+ if modified_files:
659
+ truncated_lines.append("")
660
+ truncated_lines.append(
661
+ f"# 完整修改文件列表(共 {len(modified_files)} 个文件):"
1681
662
  )
1682
-
1683
- # 只记录有错误或警告的结果
1684
- if result.returncode != 0:
1685
- output = result.stdout + result.stderr
1686
- if output.strip(): # 有输出才记录
1687
- results.append((tool_name, file_path, command, result.returncode, output))
1688
- file_results.append((file_name, tool_name, "失败", "发现问题"))
1689
- # 失败时打印检查结果
1690
- output_preview = output[:2000] if len(output) > 2000 else output
1691
- print(f"⚠️ 检查失败 ({file_name}):\n{output_preview}")
1692
- if len(output) > 2000:
1693
- print(f"⚠️ ... (输出已截断,共 {len(output)} 字符)")
1694
- else:
1695
- file_results.append((file_name, tool_name, "通过", ""))
1696
- else:
1697
- file_results.append((file_name, tool_name, "通过", ""))
1698
-
1699
- except subprocess.TimeoutExpired:
1700
- results.append((tool_name, file_path, command, -1, "执行超时(600秒)"))
1701
- file_results.append((file_name, tool_name, "超时", "执行超时(600秒)"))
1702
- print(f"⚠️ 检查超时 ({file_name}): 执行超时(600秒)")
1703
- except FileNotFoundError:
1704
- # 工具未安装,跳过
1705
- file_results.append((file_name, tool_name, "跳过", "工具未安装"))
1706
- continue
1707
- except Exception as e:
1708
- # 其他错误,记录但继续
1709
- print(f"⚠️ 执行lint命令失败: {command}, 错误: {e}")
1710
- file_results.append((file_name, tool_name, "失败", f"执行失败: {str(e)[:50]}"))
1711
- continue
1712
-
1713
- # 一次性打印所有检查结果
1714
- if file_results:
1715
- total_files = len(file_results)
1716
- passed_count = sum(1 for _, _, status, _ in file_results if status == "通过")
1717
- failed_count = sum(1 for _, _, status, _ in file_results if status == "失败")
1718
- timeout_count = sum(1 for _, _, status, _ in file_results if status == "超时")
1719
- sum(1 for _, _, status, _ in file_results if status == "跳过")
1720
-
1721
- # 收缩为一行的结果摘要
1722
- summary = f"🔍 静态检查: {total_files}个文件"
1723
- if failed_count > 0:
1724
- summary += f", {failed_count}失败"
1725
- if timeout_count > 0:
1726
- summary += f", {timeout_count}超时"
1727
- if passed_count == total_files:
1728
- summary += " ✅全部通过"
1729
-
1730
- if failed_count > 0 or timeout_count > 0:
1731
- print(f"⚠️ {summary}")
663
+ for file_path in modified_files:
664
+ truncated_lines.append(f"# - {file_path}")
665
+
666
+ break
667
+
668
+ truncated_lines.append(line)
669
+ current_tokens += line_tokens
670
+
671
+ return "\n".join(truncated_lines)
672
+
673
+ def _build_review_prompts(
674
+ self,
675
+ user_input: str,
676
+ git_diff: str,
677
+ code_generation_summary: Optional[str] = None,
678
+ ) -> tuple:
679
+ """构建 review Agent 的 prompts
680
+
681
+ 参数:
682
+ user_input: 用户原始需求
683
+ git_diff: 代码修改的 git diff(会自动进行 token 限制处理)
684
+
685
+ 返回:
686
+ tuple: (system_prompt, user_prompt, summary_prompt)
687
+ """
688
+ system_prompt = """你是代码审查专家。你的任务是审查代码修改是否正确完成了用户需求。
689
+
690
+ 审查标准:
691
+ 1. 功能完整性:代码修改是否完整实现了用户需求的所有功能点?
692
+ 2. 代码正确性:修改的代码逻辑是否正确,有无明显的 bug 或错误?
693
+ 3. 代码质量:代码是否符合最佳实践,有无明显的代码异味?
694
+ 4. 潜在风险:修改是否可能引入新的问题或破坏现有功能?
695
+
696
+ 审查要求:
697
+ - 仔细阅读用户需求、代码生成总结(summary)和代码修改(git diff)
698
+ - **对代码生成总结中的关键信息进行充分验证**:不能盲目信任总结,必须结合 git diff 和实际代码逐条核对
699
+ - 如需了解更多上下文,必须使用 read_code 工具读取相关文件以验证总结中提到的行为/位置/文件是否真实存在并符合描述
700
+ - 基于实际代码进行审查,不要凭空假设
701
+ - 如果代码生成总结与实际代码不一致,应以实际代码为准,并将不一致情况作为问题记录
702
+ - 只关注本次修改相关的问题,不要审查无关代码"""
703
+
704
+ user_prompt = f"""请审查以下代码修改是否正确完成了用户需求。
705
+
706
+ 【用户需求】
707
+ {user_input}
708
+
709
+ 【代码生成总结】
710
+ {code_generation_summary if code_generation_summary else "无代码生成总结信息(如为空,说明主 Agent 未生成总结,请完全依赖 git diff 和实际代码进行审查)"}
711
+
712
+ 【代码修改(Git Diff)】
713
+ ```diff
714
+ {git_diff}
715
+
716
+ ```
717
+
718
+ 请仔细审查代码修改,并特别注意:
719
+ - 不要直接相信代码生成总结中的描述,而是将其视为“待核实的说明”
720
+ - 对总结中提到的每一个关键修改点(如函数/文件/行为变化),都应在 git diff 或实际代码中找到对应依据
721
+ - 如发现总结与实际代码不一致,必须在审查结果中指出
722
+
723
+ 如需要可使用 read_code 工具查看更多上下文。
724
+
725
+ 如果审查完毕,直接输出 {ot("!!!COMPLETE!!!")},不要输出其他任何内容。
726
+ """
727
+
728
+ summary_prompt = """请输出 JSON 格式的审查结果,格式如下:
729
+
730
+ ```json
731
+ {
732
+ "ok": true/false, // 审查是否通过
733
+ "issues": [ // 发现的问题列表(如果 ok 为 true,可以为空数组)
734
+ {
735
+ "type": "问题类型", // 如:功能缺失、逻辑错误、代码质量、潜在风险
736
+ "description": "问题描述",
737
+ "location": "问题位置(文件:行号)",
738
+ "suggestion": "修复建议"
739
+ }
740
+ ],
741
+ "summary": "审查总结" // 简要说明审查结论
742
+ }
743
+ ```
744
+
745
+ 注意:
746
+ - 如果代码修改完全满足用户需求且无明显问题,设置 ok 为 true
747
+ - 如果存在需要修复的问题,设置 ok 为 false,并在 issues 中列出所有问题
748
+ - 每个问题都要提供具体的修复建议"""
749
+
750
+ return system_prompt, user_prompt, summary_prompt
751
+
752
+ def _parse_review_result(
753
+ self, summary: str, review_agent: Optional[Any] = None, max_retries: int = 3
754
+ ) -> dict:
755
+ """解析 review 结果
756
+
757
+ 参数:
758
+ summary: review Agent 的输出
759
+ review_agent: review Agent 实例,用于格式修复
760
+ max_retries: 最大重试次数
761
+
762
+ 返回:
763
+ dict: 解析后的审查结果,包含 ok 和 issues 字段
764
+ """
765
+ import json
766
+ import re
767
+
768
+ def _try_parse_json(content: str) -> tuple[bool, dict | None, str | None]:
769
+ """尝试解析JSON,返回(成功, 结果, json字符串)"""
770
+ # 尝试从输出中提取 JSON
771
+ # 首先尝试匹配 ```json ... ``` 代码块
772
+ json_match = re.search(r"```json\s*([\s\S]*?)\s*```", content)
773
+ if json_match:
774
+ json_str = json_match.group(1).strip()
1732
775
  else:
1733
- print(f"✅ {summary}")
1734
- else:
1735
- print("✅ 静态检查完成")
1736
-
1737
- return results
1738
-
1739
- def _format_lint_results(self, results: List[Tuple[str, str, str, int, str]]) -> str:
1740
- """格式化lint结果
1741
-
1742
- Args:
1743
- results: [(tool_name, file_path, command, returncode, output), ...]
1744
-
1745
- Returns:
1746
- 格式化的错误信息字符串
776
+ # 尝试匹配裸 JSON 对象
777
+ json_match = re.search(r'\{[\s\S]*"ok"[\s\S]*\}', content)
778
+ if json_match:
779
+ json_str = json_match.group(0)
780
+ else:
781
+ return False, None, None
782
+
783
+ try:
784
+ result = json.loads(json_str)
785
+ if isinstance(result, dict):
786
+ return True, result, json_str
787
+ else:
788
+ return False, None, json_str
789
+ except json.JSONDecodeError:
790
+ return False, None, json_str
791
+
792
+ # 第一次尝试解析
793
+ success, result, json_str = _try_parse_json(summary)
794
+ if success and result is not None:
795
+ return {
796
+ "ok": result.get("ok", True),
797
+ "issues": result.get("issues", []),
798
+ "summary": result.get("summary", ""),
799
+ }
800
+
801
+ # 如果没有提供review_agent,无法修复,返回默认值
802
+ if review_agent is None:
803
+ PrettyOutput.auto_print("⚠️ 无法解析 review 结果,且无法修复格式")
804
+ return {"ok": True, "issues": [], "summary": "无法解析审查结果"}
805
+
806
+ # 尝试修复格式
807
+ for retry in range(max_retries):
808
+ PrettyOutput.auto_print(
809
+ f"🔧 第 {retry + 1}/{max_retries} 次尝试修复 JSON 格式..."
810
+ )
811
+
812
+ fix_prompt = f"""
813
+ 之前的review回复格式不正确,无法解析为有效的JSON格式。
814
+
815
+ 原始回复内容:
816
+ ```
817
+ {summary}
818
+ ```
819
+
820
+ 请严格按照以下JSON格式重新组织你的回复:
821
+
822
+ ```json
823
+ {{
824
+ "ok": true/false, // 表示代码是否通过审查
825
+ "summary": "总体评价和建议", // 简短总结
826
+ "issues": [ // 问题列表,如果没有问题则为空数组
827
+ {{
828
+ "type": "问题类型", // 如: bug, style, performance, security等
829
+ "description": "问题描述",
830
+ "location": "问题位置", // 文件名和行号
831
+ "suggestion": "修复建议"
832
+ }}
833
+ ]
834
+ }}
835
+ ```
836
+
837
+ 确保回复只包含上述JSON格式,不要包含其他解释或文本。"""
838
+
839
+ try:
840
+ # 使用review_agent的底层model进行修复,保持review_agent的专用配置和系统prompt
841
+ fixed_summary = review_agent.model.chat_until_success(fix_prompt)
842
+ if fixed_summary:
843
+ success, result, _ = _try_parse_json(str(fixed_summary))
844
+ if success and result is not None:
845
+ PrettyOutput.auto_print(
846
+ f"✅ JSON格式修复成功(第 {retry + 1} 次)"
847
+ )
848
+ return {
849
+ "ok": result.get("ok", True),
850
+ "issues": result.get("issues", []),
851
+ "summary": result.get("summary", ""),
852
+ }
853
+ else:
854
+ PrettyOutput.auto_print("⚠️ 修复后的格式仍不正确,继续尝试...")
855
+ summary = str(fixed_summary) # 使用修复后的内容继续尝试
856
+ else:
857
+ PrettyOutput.auto_print("⚠️ 修复请求无响应")
858
+
859
+ except Exception as e:
860
+ PrettyOutput.auto_print(f"⚠️ 修复过程中出错: {e}")
861
+
862
+ # 3次修复都失败,标记需要重新review
863
+ PrettyOutput.auto_print("❌ JSON格式修复失败,需要重新进行review")
864
+ return {
865
+ "ok": False,
866
+ "issues": [],
867
+ "summary": "JSON_FORMAT_ERROR",
868
+ "need_re_review": True,
869
+ }
870
+
871
+ def _review_and_fix(
872
+ self,
873
+ user_input: str,
874
+ enhanced_input: str,
875
+ prefix: str = "",
876
+ suffix: str = "",
877
+ code_generation_summary: Optional[str] = None,
878
+ ) -> None:
879
+ """执行 review 和修复循环
880
+
881
+ 参数:
882
+ user_input: 用户原始需求
883
+ enhanced_input: 增强后的用户输入(用于修复)
884
+ prefix: 前缀
885
+ suffix: 后缀
1747
886
  """
1748
- if not results:
1749
- return ""
1750
-
1751
- lines = []
1752
- for tool_name, file_path, command, returncode, output in results:
1753
- lines.append(f"工具: {tool_name}")
1754
- lines.append(f"文件: {file_path}")
1755
- lines.append(f"命令: {command}")
1756
- if returncode == -1:
1757
- lines.append(f"错误: {output}")
887
+ from jarvis.jarvis_agent import Agent
888
+
889
+ iteration = 0
890
+ max_iterations = self.review_max_iterations
891
+ # 如果 max_iterations 0,表示无限 review
892
+ is_infinite = max_iterations == 0
893
+
894
+ while is_infinite or iteration < max_iterations:
895
+ iteration += 1
896
+
897
+ # 获取从开始到当前的 git diff(提前检测是否有代码修改)
898
+ current_commit = get_latest_commit_hash()
899
+ if self.start_commit is None or current_commit == self.start_commit:
900
+ git_diff = get_diff() # 获取未提交的更改
1758
901
  else:
1759
- # 限制输出长度,避免过长
1760
- output_preview = output[:1000] if len(output) > 1000 else output
1761
- lines.append(f"输出:\n{output_preview}")
1762
- if len(output) > 1000:
1763
- lines.append(f"... (输出已截断,共 {len(output)} 字符)")
1764
- lines.append("") # 空行分隔
1765
-
1766
- return "\n".join(lines)
1767
-
1768
- def _validate_build_after_edit(self, modified_files: List[str]) -> Optional[BuildResult]:
1769
- """编辑后验证构建
1770
-
1771
- Args:
1772
- modified_files: 修改的文件列表
1773
-
1774
- Returns:
1775
- BuildResult: 验证结果,如果验证被禁用或出错则返回None
902
+ git_diff = get_diff_between_commits(self.start_commit, current_commit)
903
+
904
+ if not git_diff or not git_diff.strip():
905
+ PrettyOutput.auto_print("ℹ️ 没有代码修改,跳过审查")
906
+ return
907
+
908
+ # 每轮审查开始前显示清晰的提示信息
909
+ if not self.non_interactive:
910
+ if is_infinite:
911
+ PrettyOutput.auto_print(
912
+ f"\n🔄 代码审查循环 - 第 {iteration} 轮(无限模式)"
913
+ )
914
+ else:
915
+ PrettyOutput.auto_print(
916
+ f"\n🔄 代码审查循环 - 第 {iteration}/{max_iterations} 轮"
917
+ )
918
+ if not user_confirm("是否开始本轮代码审查?", default=True):
919
+ PrettyOutput.auto_print("ℹ️ 用户终止了代码审查")
920
+ return
921
+ else:
922
+ if is_infinite:
923
+ PrettyOutput.auto_print(
924
+ f"\n🔍 开始第 {iteration} 轮代码审查...(无限模式)"
925
+ )
926
+ else:
927
+ PrettyOutput.auto_print(
928
+ f"\n🔍 开始第 {iteration}/{max_iterations} 轮代码审查..."
929
+ )
930
+
931
+ # 对 git diff 进行 token 限制处理(review 需要更多上下文,使用 40% 的 token 比例)
932
+ truncated_git_diff = self._truncate_diff_for_review(
933
+ git_diff, token_ratio=0.4
934
+ )
935
+ if truncated_git_diff != git_diff:
936
+ PrettyOutput.auto_print("⚠️ Git diff 内容过大,已截断以适应 token 限制")
937
+
938
+ # 构建 review prompts
939
+ sys_prompt, usr_prompt, sum_prompt = self._build_review_prompts(
940
+ user_input, truncated_git_diff, code_generation_summary
941
+ )
942
+
943
+ rules_prompt = self.rules_manager.load_all_rules(
944
+ ",".join(self.loaded_rule_names)
945
+ )
946
+
947
+ review_agent = Agent(
948
+ system_prompt=sys_prompt + f"\n\n<rules>\n{rules_prompt}\n</rules>",
949
+ name=f"CodeReview-Agent-{iteration}",
950
+ model_group=self.model.model_group if self.model else None,
951
+ summary_prompt=sum_prompt,
952
+ need_summary=True,
953
+ auto_complete=True,
954
+ use_tools=["execute_script", "read_code"],
955
+ non_interactive=self.non_interactive,
956
+ use_methodology=False,
957
+ use_analysis=False,
958
+ )
959
+
960
+ # 运行 review
961
+ summary = review_agent.run(usr_prompt)
962
+
963
+ # 解析审查结果,支持格式修复和重新review
964
+ result = self._parse_review_result(
965
+ str(summary) if summary else "", review_agent=review_agent
966
+ )
967
+
968
+ # 检查是否需要重新review(JSON格式错误3次修复失败)
969
+ if result.get("need_re_review", False):
970
+ PrettyOutput.auto_print(
971
+ f"\n🔄 JSON格式修复失败,重新进行代码审查(第 {iteration} 轮)"
972
+ )
973
+ # 跳过当前迭代,重新开始review流程
974
+ continue
975
+
976
+ if result["ok"]:
977
+ PrettyOutput.auto_print(f"\n✅ 代码审查通过(第 {iteration} 轮)")
978
+ if result.get("summary"):
979
+ PrettyOutput.auto_print(f" {result['summary']}")
980
+ return
981
+
982
+ # 审查未通过,需要修复
983
+ PrettyOutput.auto_print(f"\n⚠️ 代码审查发现问题(第 {iteration} 轮):")
984
+ for i, issue in enumerate(result.get("issues", []), 1):
985
+ issue_type = issue.get("type", "未知")
986
+ description = issue.get("description", "无描述")
987
+ location = issue.get("location", "未知位置")
988
+ suggestion = issue.get("suggestion", "无建议")
989
+ PrettyOutput.auto_print(f" {i}. [{issue_type}] {description}")
990
+ PrettyOutput.auto_print(f" 位置: {location}")
991
+ PrettyOutput.auto_print(f" 建议: {suggestion}")
992
+
993
+ # 在每轮审查后给用户一个终止选择
994
+ if not self.non_interactive:
995
+ if not user_confirm("是否继续修复这些问题?", default=True):
996
+ PrettyOutput.auto_print("ℹ️ 用户选择终止审查,保持当前代码状态")
997
+ return
998
+
999
+ # 只有在非无限模式下才检查是否达到最大迭代次数
1000
+ if not is_infinite and iteration >= max_iterations:
1001
+ PrettyOutput.auto_print(
1002
+ f"\n⚠️ 已达到最大审查次数 ({max_iterations}),停止审查"
1003
+ )
1004
+ # 在非交互模式下直接返回,交互模式下询问用户
1005
+ if not self.non_interactive:
1006
+ if not user_confirm("是否继续修复?", default=False):
1007
+ return
1008
+ # 用户选择继续,重置迭代次数
1009
+ iteration = 0
1010
+ max_iterations = self.review_max_iterations
1011
+ is_infinite = max_iterations == 0
1012
+ else:
1013
+ return
1014
+
1015
+ # 构建修复 prompt
1016
+ fix_prompt = f"""代码审查发现以下问题,请修复:
1017
+
1018
+ 【审查结果】
1019
+ {result.get("summary", "")}
1020
+
1021
+ 【问题列表】
1022
+ """
1023
+ for i, issue in enumerate(result.get("issues", []), 1):
1024
+ fix_prompt += f"{i}. [{issue.get('type', '未知')}] {issue.get('description', '')}\n"
1025
+ fix_prompt += f" 位置: {issue.get('location', '')}\n"
1026
+ fix_prompt += f" 建议: {issue.get('suggestion', '')}\n\n"
1027
+
1028
+ fix_prompt += "\n请根据上述问题进行修复,确保代码正确实现用户需求。"
1029
+
1030
+ PrettyOutput.auto_print("\n🔧 开始修复问题...")
1031
+
1032
+ # 调用 super().run() 进行修复
1033
+ try:
1034
+ if self.model:
1035
+ self.model.set_suppress_output(False)
1036
+ super().run(fix_prompt)
1037
+ except RuntimeError as e:
1038
+ PrettyOutput.auto_print(f"⚠️ 修复失败: {str(e)}")
1039
+ return
1040
+
1041
+ # 处理未提交的更改
1042
+ self.git_manager.handle_uncommitted_changes()
1043
+
1044
+ def add_runtime_rule(self, rule_name: str) -> None:
1045
+ """添加运行时加载的规则到跟踪列表
1046
+
1047
+ 用于记录通过builtin_input_handler等方式动态加载的规则,
1048
+ 确保这些规则能够被后续的子代理继承。
1049
+
1050
+ 参数:
1051
+ rule_name: 规则名称
1776
1052
  """
1777
- if not is_enable_build_validation():
1778
- return None
1779
-
1780
- # 检查项目配置,看是否已禁用构建验证
1781
- config = BuildValidationConfig(self.root_dir)
1782
- if config.is_build_validation_disabled():
1783
- # 已禁用,返回None,由调用方处理基础静态检查
1784
- return None
1785
-
1786
- # 输出编译检查日志
1787
- file_count = len(modified_files)
1788
- files_str = ", ".join(os.path.basename(f) for f in modified_files[:3])
1789
- if file_count > 3:
1790
- files_str += f" 等{file_count}个文件"
1791
- print(f"🔨 正在进行编译检查 ({files_str})...")
1792
-
1793
- try:
1794
- timeout = get_build_validation_timeout()
1795
- validator = BuildValidator(self.root_dir, timeout=timeout)
1796
- result = validator.validate(modified_files)
1797
- return result
1798
- except Exception as e:
1799
- # 构建验证失败不应该影响主流程,仅记录日志
1800
- print(f"⚠️ 构建验证执行失败: {e}")
1801
- return None
1053
+ if not rule_name or not isinstance(rule_name, str):
1054
+ return
1055
+
1056
+ # 同时更新完整规则集合(自动去重)
1057
+ self.loaded_rule_names.add(rule_name)
1802
1058
 
1803
1059
 
1804
1060
  @app.command()
@@ -1834,23 +1090,33 @@ def cli(
1834
1090
  help="提交信息后缀(用换行分隔)",
1835
1091
  ),
1836
1092
  non_interactive: bool = typer.Option(
1837
- False, "-n", "--non-interactive", help="启用非交互模式:用户无法与命令交互,脚本执行超时限制为5分钟"
1093
+ False,
1094
+ "-n",
1095
+ "--non-interactive",
1096
+ help="启用非交互模式:用户无法与命令交互,脚本执行超时限制为5分钟",
1838
1097
  ),
1839
1098
  rule_names: Optional[str] = typer.Option(
1840
- None, "--rule-names", help="指定规则名称列表,用逗号分隔,从 rules.yaml 文件中读取对应的规则内容"
1099
+ None,
1100
+ "--rule-names",
1101
+ help="指定规则名称列表,用逗号分隔,从 rules.yaml 文件中读取对应的规则内容",
1102
+ ),
1103
+ disable_review: bool = typer.Option(
1104
+ False,
1105
+ "--disable-review",
1106
+ help="启用代码审查:在代码修改完成后自动进行代码审查,发现问题则自动修复",
1107
+ ),
1108
+ review_max_iterations: int = typer.Option(
1109
+ 0,
1110
+ "--review-max-iterations",
1111
+ help="代码审查最大迭代次数,达到上限后停止审查(默认3次)",
1841
1112
  ),
1842
1113
  ) -> None:
1843
1114
  """Jarvis主入口点。"""
1844
- # CLI 标志:非交互模式(不依赖配置文件)
1845
- if non_interactive:
1846
- try:
1847
- os.environ["JARVIS_NON_INTERACTIVE"] = "true"
1848
- except Exception:
1849
- pass
1850
- # 注意:全局配置同步放在 init_env 之后执行,避免被 init_env 覆盖
1851
1115
  # 非交互模式要求从命令行传入任务
1852
1116
  if non_interactive and not (requirement and str(requirement).strip()):
1853
- print("❌ 非交互模式已启用:必须使用 --requirement 传入任务内容,因多行输入不可用。")
1117
+ PrettyOutput.auto_print(
1118
+ "❌ 非交互模式已启用:必须使用 --requirement 传入任务内容,因多行输入不可用。"
1119
+ )
1854
1120
  raise typer.Exit(code=2)
1855
1121
  init_env(
1856
1122
  "欢迎使用 Jarvis-CodeAgent,您的代码工程助手已准备就绪!",
@@ -1862,13 +1128,11 @@ def cli(
1862
1128
  # 在初始化环境后同步 CLI 选项到全局配置,避免被 init_env 覆盖
1863
1129
  try:
1864
1130
  if model_group:
1865
- set_config("JARVIS_LLM_GROUP", str(model_group))
1131
+ set_config("llm_group", str(model_group))
1866
1132
  if tool_group:
1867
- set_config("JARVIS_TOOL_GROUP", str(tool_group))
1133
+ set_config("tool_group", str(tool_group))
1868
1134
  if restore_session:
1869
- set_config("JARVIS_RESTORE_SESSION", True)
1870
- if non_interactive:
1871
- set_config("JARVIS_NON_INTERACTIVE", True)
1135
+ set_config("restore_session", True)
1872
1136
  except Exception:
1873
1137
  # 静默忽略同步异常,不影响主流程
1874
1138
  pass
@@ -1882,9 +1146,13 @@ def cli(
1882
1146
  )
1883
1147
  except (subprocess.CalledProcessError, FileNotFoundError):
1884
1148
  curr_dir_path = os.getcwd()
1885
- print(f"⚠️ 警告:当前目录 '{curr_dir_path}' 不是一个git仓库。")
1886
- init_git = True if non_interactive else user_confirm(
1887
- f"是否要在 '{curr_dir_path}' 中初始化一个新的git仓库?", default=True
1149
+ PrettyOutput.auto_print(f"⚠️ 警告:当前目录 '{curr_dir_path}' 不是一个git仓库。")
1150
+ init_git = (
1151
+ True
1152
+ if non_interactive
1153
+ else user_confirm(
1154
+ f"是否要在 '{curr_dir_path}' 中初始化一个新的git仓库?", default=True
1155
+ )
1888
1156
  )
1889
1157
  if init_git:
1890
1158
  try:
@@ -1896,12 +1164,12 @@ def cli(
1896
1164
  encoding="utf-8",
1897
1165
  errors="replace",
1898
1166
  )
1899
- print("✅ 已成功初始化git仓库。")
1167
+ PrettyOutput.auto_print("✅ 已成功初始化git仓库。")
1900
1168
  except (subprocess.CalledProcessError, FileNotFoundError) as e:
1901
- print(f"❌ 初始化git仓库失败: {e}")
1169
+ PrettyOutput.auto_print(f"❌ 初始化git仓库失败: {e}")
1902
1170
  sys.exit(1)
1903
1171
  else:
1904
- print("ℹ️ 操作已取消。Jarvis需要在git仓库中运行。")
1172
+ PrettyOutput.auto_print("ℹ️ 操作已取消。Jarvis需要在git仓库中运行。")
1905
1173
  sys.exit(0)
1906
1174
 
1907
1175
  curr_dir = os.getcwd()
@@ -1909,13 +1177,14 @@ def cli(
1909
1177
  # 在定位到 git 根目录后,按仓库维度加锁,避免跨仓库互斥
1910
1178
  try:
1911
1179
  repo_root = os.getcwd()
1912
- lock_name = f"code_agent_{hashlib.md5(repo_root.encode('utf-8')).hexdigest()}.lock"
1180
+ lock_name = (
1181
+ f"code_agent_{hashlib.md5(repo_root.encode('utf-8')).hexdigest()}.lock"
1182
+ )
1913
1183
  _acquire_single_instance_lock(lock_name=lock_name)
1914
1184
  except Exception:
1915
1185
  # 回退到全局锁,确保至少有互斥保护
1916
1186
  _acquire_single_instance_lock(lock_name="code_agent.lock")
1917
1187
  try:
1918
-
1919
1188
  agent = CodeAgent(
1920
1189
  model_group=model_group,
1921
1190
  need_summary=False,
@@ -1923,31 +1192,221 @@ def cli(
1923
1192
  tool_group=tool_group,
1924
1193
  non_interactive=non_interactive,
1925
1194
  rule_names=rule_names,
1195
+ disable_review=disable_review,
1196
+ review_max_iterations=review_max_iterations,
1926
1197
  )
1927
1198
 
1199
+ # 显示可用的规则信息
1200
+ _print_available_rules(agent.rules_manager, rule_names)
1201
+
1928
1202
  # 尝试恢复会话
1929
1203
  if restore_session:
1930
1204
  if agent.restore_session():
1931
- print("✅ 已从 .jarvis/saved_session.json 恢复会话。")
1205
+ PrettyOutput.auto_print("✅ 已从 .jarvis/saved_session.json 恢复会话。")
1932
1206
  else:
1933
- print("⚠️ 无法从 .jarvis/saved_session.json 恢复会话。")
1207
+ PrettyOutput.auto_print(
1208
+ "⚠️ 无法从 .jarvis/saved_session.json 恢复会话。"
1209
+ )
1934
1210
 
1935
1211
  if requirement:
1936
1212
  agent.run(requirement, prefix=prefix, suffix=suffix)
1213
+ if agent.non_interactive:
1214
+ raise typer.Exit(code=0)
1937
1215
  else:
1938
1216
  while True:
1939
1217
  user_input = get_multiline_input("请输入你的需求(输入空行退出):")
1940
1218
  if not user_input:
1941
1219
  raise typer.Exit(code=0)
1942
1220
  agent.run(user_input, prefix=prefix, suffix=suffix)
1221
+ if agent.non_interactive:
1222
+ raise typer.Exit(code=0)
1943
1223
 
1944
1224
  except typer.Exit:
1945
1225
  raise
1946
1226
  except RuntimeError as e:
1947
- print(f"❌ 错误: {str(e)}")
1227
+ PrettyOutput.auto_print(f"❌ 错误: {str(e)}")
1948
1228
  sys.exit(1)
1949
1229
 
1950
1230
 
1231
+ def _print_available_rules(
1232
+ rules_manager: RulesManager, rule_names: Optional[str] = None
1233
+ ) -> None:
1234
+ """打印可用的规则信息
1235
+
1236
+ 参数:
1237
+ rules_manager: 规则管理器实例
1238
+ rule_names: 用户指定的规则名称列表(逗号分隔)
1239
+ """
1240
+ try:
1241
+ from rich.console import Console
1242
+ from rich.panel import Panel
1243
+ from rich.text import Text
1244
+
1245
+ console = Console()
1246
+
1247
+ # 获取所有可用规则
1248
+ all_rules = rules_manager.get_all_available_rule_names()
1249
+ builtin_rules = all_rules.get("builtin", [])
1250
+ file_rules = all_rules.get("files", [])
1251
+ yaml_rules = all_rules.get("yaml", [])
1252
+
1253
+ # 获取已加载的规则
1254
+ loaded_rules = []
1255
+ if rule_names:
1256
+ rule_list = [name.strip() for name in rule_names.split(",") if name.strip()]
1257
+ for rule_name in rule_list:
1258
+ if rules_manager.get_named_rule(rule_name):
1259
+ loaded_rules.append(rule_name)
1260
+
1261
+ # 检查项目规则和全局规则
1262
+ has_project_rule = rules_manager.read_project_rule() is not None
1263
+ has_global_rule = rules_manager.read_global_rules() is not None
1264
+
1265
+ # 构建规则信息内容
1266
+ content_parts = []
1267
+
1268
+ # 显示所有规则(按来源分类)
1269
+ has_any_rules = False
1270
+
1271
+ # 内置规则
1272
+ if builtin_rules:
1273
+ has_any_rules = True
1274
+ builtin_text = Text()
1275
+ builtin_text.append("📚 内置规则 ", style="bold cyan")
1276
+ builtin_text.append(f"({len(builtin_rules)} 个): ", style="dim")
1277
+ for i, rule in enumerate(builtin_rules):
1278
+ if i > 0:
1279
+ builtin_text.append(", ", style="dim")
1280
+ builtin_text.append(rule, style="yellow")
1281
+ content_parts.append(builtin_text)
1282
+
1283
+ # 用户自定义规则
1284
+ user_custom_rules = file_rules + yaml_rules
1285
+ if user_custom_rules:
1286
+ has_any_rules = True
1287
+ user_text = Text()
1288
+ user_text.append("👤 用户自定义规则 ", style="bold green")
1289
+ user_text.append(f"({len(user_custom_rules)} 个): ", style="dim")
1290
+
1291
+ # 分别显示文件规则和YAML规则
1292
+ custom_rules_parts = []
1293
+ if file_rules:
1294
+ file_part = Text()
1295
+ file_part.append("文件规则: ", style="blue")
1296
+ for i, rule in enumerate(file_rules):
1297
+ if i > 0:
1298
+ file_part.append(", ", style="dim")
1299
+ file_part.append(rule, style="cyan")
1300
+ custom_rules_parts.append(file_part)
1301
+
1302
+ if yaml_rules:
1303
+ yaml_part = Text()
1304
+ yaml_part.append("YAML规则: ", style="magenta")
1305
+ for i, rule in enumerate(yaml_rules):
1306
+ if i > 0:
1307
+ yaml_part.append(", ", style="dim")
1308
+ yaml_part.append(rule, style="magenta")
1309
+ custom_rules_parts.append(yaml_part)
1310
+
1311
+ # 合并显示自定义规则
1312
+ for i, part in enumerate(custom_rules_parts):
1313
+ if i > 0:
1314
+ user_text.append(" | ", style="dim")
1315
+ user_text.append(part)
1316
+
1317
+ content_parts.append(user_text)
1318
+
1319
+ # 分别显示详细的文件规则和YAML规则(保留原有详细信息)
1320
+ if file_rules:
1321
+ has_any_rules = True
1322
+ file_text = Text()
1323
+ file_text.append("📄 详细文件规则 ", style="bold blue")
1324
+ file_text.append(f"({len(file_rules)} 个): ", style="dim")
1325
+ for i, rule in enumerate(file_rules):
1326
+ if i > 0:
1327
+ file_text.append(", ", style="dim")
1328
+ file_text.append(rule, style="cyan")
1329
+ content_parts.append(file_text)
1330
+
1331
+ if yaml_rules:
1332
+ has_any_rules = True
1333
+ yaml_text = Text()
1334
+ yaml_text.append("📝 详细YAML规则 ", style="bold magenta")
1335
+ yaml_text.append(f"({len(yaml_rules)} 个): ", style="dim")
1336
+ for i, rule in enumerate(yaml_rules):
1337
+ if i > 0:
1338
+ yaml_text.append(", ", style="dim")
1339
+ yaml_text.append(rule, style="magenta")
1340
+ content_parts.append(yaml_text)
1341
+
1342
+ # 如果没有规则,显示提示
1343
+ if not has_any_rules:
1344
+ no_rules_text = Text()
1345
+ no_rules_text.append("ℹ️ 当前没有可用的规则", style="dim")
1346
+ content_parts.append(no_rules_text)
1347
+
1348
+ # 提示信息
1349
+ if has_any_rules:
1350
+ tip_text = Text()
1351
+ tip_text.append("💡 提示: ", style="bold green")
1352
+ tip_text.append("使用 ", style="dim")
1353
+ tip_text.append("--rule-names", style="bold yellow")
1354
+ tip_text.append(" 参数加载规则,例如: ", style="dim")
1355
+ tip_text.append("--rule-names tdd,clean_code", style="bold yellow")
1356
+ tip_text.append("\n 或使用 ", style="dim")
1357
+ tip_text.append("@", style="bold yellow")
1358
+ tip_text.append(" 触发规则加载,例如: ", style="dim")
1359
+ tip_text.append("@tdd @clean_code", style="bold yellow")
1360
+ content_parts.append(tip_text)
1361
+
1362
+ # 显示已加载的规则
1363
+ if loaded_rules:
1364
+ loaded_text = Text()
1365
+ loaded_text.append("✅ 已加载规则: ", style="bold green")
1366
+ for i, rule in enumerate(loaded_rules):
1367
+ if i > 0:
1368
+ loaded_text.append(", ", style="dim")
1369
+ loaded_text.append(rule, style="bold yellow")
1370
+ content_parts.append(loaded_text)
1371
+
1372
+ # 显示项目规则和全局规则
1373
+ if has_project_rule or has_global_rule:
1374
+ rule_files_text = Text()
1375
+ if has_project_rule:
1376
+ rule_files_text.append("📁 项目规则: ", style="bold blue")
1377
+ rule_files_text.append(".jarvis/rule", style="dim")
1378
+ if has_global_rule:
1379
+ rule_files_text.append(" | ", style="dim")
1380
+ if has_global_rule:
1381
+ rule_files_text.append("🌐 全局规则: ", style="bold magenta")
1382
+ rule_files_text.append("~/.jarvis/rule", style="dim")
1383
+ content_parts.append(rule_files_text)
1384
+
1385
+ # 如果有规则信息,使用 Panel 打印
1386
+ if content_parts:
1387
+ from rich.console import Group
1388
+
1389
+ # 创建内容组
1390
+ content_group = Group(*content_parts)
1391
+
1392
+ # 创建 Panel
1393
+ panel = Panel(
1394
+ content_group,
1395
+ title="📋 规则信息",
1396
+ title_align="center",
1397
+ border_style="cyan",
1398
+ padding=(0, 1),
1399
+ )
1400
+
1401
+ console.print(panel)
1402
+ except Exception as e:
1403
+ # 显示错误信息而不是静默失败
1404
+ PrettyOutput.auto_print(f"⚠️ 规则信息显示失败: {e}")
1405
+ import traceback
1406
+
1407
+ traceback.print_exc()
1408
+
1409
+
1951
1410
  def main() -> None:
1952
1411
  """Application entry point."""
1953
1412
  app()