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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +458 -152
  3. jarvis/jarvis_agent/agent_manager.py +17 -13
  4. jarvis/jarvis_agent/builtin_input_handler.py +2 -6
  5. jarvis/jarvis_agent/config_editor.py +2 -7
  6. jarvis/jarvis_agent/event_bus.py +82 -12
  7. jarvis/jarvis_agent/file_context_handler.py +329 -0
  8. jarvis/jarvis_agent/file_methodology_manager.py +3 -4
  9. jarvis/jarvis_agent/jarvis.py +628 -55
  10. jarvis/jarvis_agent/language_extractors/__init__.py +57 -0
  11. jarvis/jarvis_agent/language_extractors/c_extractor.py +21 -0
  12. jarvis/jarvis_agent/language_extractors/cpp_extractor.py +21 -0
  13. jarvis/jarvis_agent/language_extractors/go_extractor.py +21 -0
  14. jarvis/jarvis_agent/language_extractors/java_extractor.py +84 -0
  15. jarvis/jarvis_agent/language_extractors/javascript_extractor.py +79 -0
  16. jarvis/jarvis_agent/language_extractors/python_extractor.py +21 -0
  17. jarvis/jarvis_agent/language_extractors/rust_extractor.py +21 -0
  18. jarvis/jarvis_agent/language_extractors/typescript_extractor.py +84 -0
  19. jarvis/jarvis_agent/language_support_info.py +486 -0
  20. jarvis/jarvis_agent/main.py +34 -10
  21. jarvis/jarvis_agent/memory_manager.py +7 -16
  22. jarvis/jarvis_agent/methodology_share_manager.py +10 -16
  23. jarvis/jarvis_agent/prompt_manager.py +1 -1
  24. jarvis/jarvis_agent/prompts.py +193 -171
  25. jarvis/jarvis_agent/protocols.py +8 -12
  26. jarvis/jarvis_agent/run_loop.py +105 -9
  27. jarvis/jarvis_agent/session_manager.py +2 -3
  28. jarvis/jarvis_agent/share_manager.py +20 -22
  29. jarvis/jarvis_agent/shell_input_handler.py +1 -2
  30. jarvis/jarvis_agent/stdio_redirect.py +295 -0
  31. jarvis/jarvis_agent/task_analyzer.py +31 -6
  32. jarvis/jarvis_agent/task_manager.py +11 -27
  33. jarvis/jarvis_agent/tool_executor.py +2 -3
  34. jarvis/jarvis_agent/tool_share_manager.py +12 -24
  35. jarvis/jarvis_agent/utils.py +5 -1
  36. jarvis/jarvis_agent/web_bridge.py +189 -0
  37. jarvis/jarvis_agent/web_output_sink.py +53 -0
  38. jarvis/jarvis_agent/web_server.py +786 -0
  39. jarvis/jarvis_c2rust/__init__.py +26 -0
  40. jarvis/jarvis_c2rust/cli.py +575 -0
  41. jarvis/jarvis_c2rust/collector.py +250 -0
  42. jarvis/jarvis_c2rust/constants.py +26 -0
  43. jarvis/jarvis_c2rust/library_replacer.py +1254 -0
  44. jarvis/jarvis_c2rust/llm_module_agent.py +1272 -0
  45. jarvis/jarvis_c2rust/loaders.py +207 -0
  46. jarvis/jarvis_c2rust/models.py +28 -0
  47. jarvis/jarvis_c2rust/optimizer.py +2157 -0
  48. jarvis/jarvis_c2rust/scanner.py +1681 -0
  49. jarvis/jarvis_c2rust/transpiler.py +2983 -0
  50. jarvis/jarvis_c2rust/utils.py +385 -0
  51. jarvis/jarvis_code_agent/build_validation_config.py +132 -0
  52. jarvis/jarvis_code_agent/code_agent.py +1371 -220
  53. jarvis/jarvis_code_agent/code_analyzer/__init__.py +65 -0
  54. jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
  55. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
  56. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +106 -0
  57. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +74 -0
  58. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
  59. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +72 -0
  60. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +70 -0
  61. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +53 -0
  62. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +47 -0
  63. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +61 -0
  64. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +110 -0
  65. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +154 -0
  66. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +110 -0
  67. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +153 -0
  68. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
  69. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +648 -0
  70. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
  71. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
  72. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
  73. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
  74. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
  75. jarvis/jarvis_code_agent/code_analyzer/language_support.py +110 -0
  76. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +49 -0
  77. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +299 -0
  78. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +215 -0
  79. jarvis/jarvis_code_agent/code_analyzer/languages/java_language.py +212 -0
  80. jarvis/jarvis_code_agent/code_analyzer/languages/javascript_language.py +254 -0
  81. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +269 -0
  82. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +281 -0
  83. jarvis/jarvis_code_agent/code_analyzer/languages/typescript_language.py +280 -0
  84. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +605 -0
  85. jarvis/jarvis_code_agent/code_analyzer/structured_code.py +556 -0
  86. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +252 -0
  87. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +58 -0
  88. jarvis/jarvis_code_agent/lint.py +501 -8
  89. jarvis/jarvis_code_agent/utils.py +141 -0
  90. jarvis/jarvis_code_analysis/code_review.py +493 -584
  91. jarvis/jarvis_data/config_schema.json +128 -12
  92. jarvis/jarvis_git_squash/main.py +4 -5
  93. jarvis/jarvis_git_utils/git_commiter.py +82 -75
  94. jarvis/jarvis_mcp/sse_mcp_client.py +22 -29
  95. jarvis/jarvis_mcp/stdio_mcp_client.py +12 -13
  96. jarvis/jarvis_mcp/streamable_mcp_client.py +15 -14
  97. jarvis/jarvis_memory_organizer/memory_organizer.py +55 -74
  98. jarvis/jarvis_methodology/main.py +32 -48
  99. jarvis/jarvis_multi_agent/__init__.py +287 -55
  100. jarvis/jarvis_multi_agent/main.py +36 -4
  101. jarvis/jarvis_platform/base.py +524 -202
  102. jarvis/jarvis_platform/human.py +7 -8
  103. jarvis/jarvis_platform/kimi.py +30 -36
  104. jarvis/jarvis_platform/openai.py +88 -25
  105. jarvis/jarvis_platform/registry.py +26 -10
  106. jarvis/jarvis_platform/tongyi.py +24 -25
  107. jarvis/jarvis_platform/yuanbao.py +32 -43
  108. jarvis/jarvis_platform_manager/main.py +66 -77
  109. jarvis/jarvis_platform_manager/service.py +8 -13
  110. jarvis/jarvis_rag/cli.py +53 -55
  111. jarvis/jarvis_rag/embedding_manager.py +13 -18
  112. jarvis/jarvis_rag/llm_interface.py +8 -9
  113. jarvis/jarvis_rag/query_rewriter.py +10 -21
  114. jarvis/jarvis_rag/rag_pipeline.py +24 -27
  115. jarvis/jarvis_rag/reranker.py +4 -5
  116. jarvis/jarvis_rag/retriever.py +28 -30
  117. jarvis/jarvis_sec/__init__.py +305 -0
  118. jarvis/jarvis_sec/agents.py +143 -0
  119. jarvis/jarvis_sec/analysis.py +276 -0
  120. jarvis/jarvis_sec/checkers/__init__.py +32 -0
  121. jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
  122. jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
  123. jarvis/jarvis_sec/cli.py +139 -0
  124. jarvis/jarvis_sec/clustering.py +1439 -0
  125. jarvis/jarvis_sec/file_manager.py +427 -0
  126. jarvis/jarvis_sec/parsers.py +73 -0
  127. jarvis/jarvis_sec/prompts.py +268 -0
  128. jarvis/jarvis_sec/report.py +336 -0
  129. jarvis/jarvis_sec/review.py +453 -0
  130. jarvis/jarvis_sec/status.py +264 -0
  131. jarvis/jarvis_sec/types.py +20 -0
  132. jarvis/jarvis_sec/utils.py +499 -0
  133. jarvis/jarvis_sec/verification.py +848 -0
  134. jarvis/jarvis_sec/workflow.py +226 -0
  135. jarvis/jarvis_smart_shell/main.py +38 -87
  136. jarvis/jarvis_stats/cli.py +2 -2
  137. jarvis/jarvis_stats/stats.py +8 -8
  138. jarvis/jarvis_stats/storage.py +15 -21
  139. jarvis/jarvis_stats/visualizer.py +1 -1
  140. jarvis/jarvis_tools/clear_memory.py +3 -20
  141. jarvis/jarvis_tools/cli/main.py +21 -23
  142. jarvis/jarvis_tools/edit_file.py +1019 -132
  143. jarvis/jarvis_tools/execute_script.py +83 -25
  144. jarvis/jarvis_tools/file_analyzer.py +6 -9
  145. jarvis/jarvis_tools/generate_new_tool.py +14 -21
  146. jarvis/jarvis_tools/lsp_client.py +1552 -0
  147. jarvis/jarvis_tools/methodology.py +2 -3
  148. jarvis/jarvis_tools/read_code.py +1736 -35
  149. jarvis/jarvis_tools/read_symbols.py +140 -0
  150. jarvis/jarvis_tools/read_webpage.py +12 -13
  151. jarvis/jarvis_tools/registry.py +427 -200
  152. jarvis/jarvis_tools/retrieve_memory.py +20 -19
  153. jarvis/jarvis_tools/rewrite_file.py +72 -158
  154. jarvis/jarvis_tools/save_memory.py +3 -15
  155. jarvis/jarvis_tools/search_web.py +18 -18
  156. jarvis/jarvis_tools/sub_agent.py +36 -43
  157. jarvis/jarvis_tools/sub_code_agent.py +25 -26
  158. jarvis/jarvis_tools/virtual_tty.py +55 -33
  159. jarvis/jarvis_utils/clipboard.py +7 -10
  160. jarvis/jarvis_utils/config.py +232 -45
  161. jarvis/jarvis_utils/embedding.py +8 -5
  162. jarvis/jarvis_utils/fzf.py +8 -8
  163. jarvis/jarvis_utils/git_utils.py +225 -36
  164. jarvis/jarvis_utils/globals.py +3 -3
  165. jarvis/jarvis_utils/http.py +1 -1
  166. jarvis/jarvis_utils/input.py +99 -48
  167. jarvis/jarvis_utils/jsonnet_compat.py +465 -0
  168. jarvis/jarvis_utils/methodology.py +52 -48
  169. jarvis/jarvis_utils/utils.py +819 -491
  170. jarvis_ai_assistant-0.7.6.dist-info/METADATA +600 -0
  171. jarvis_ai_assistant-0.7.6.dist-info/RECORD +218 -0
  172. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/entry_points.txt +4 -0
  173. jarvis/jarvis_agent/config.py +0 -92
  174. jarvis/jarvis_agent/edit_file_handler.py +0 -296
  175. jarvis/jarvis_platform/ai8.py +0 -332
  176. jarvis/jarvis_tools/ask_user.py +0 -54
  177. jarvis_ai_assistant-0.3.30.dist-info/METADATA +0 -381
  178. jarvis_ai_assistant-0.3.30.dist-info/RECORD +0 -137
  179. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/WHEEL +0 -0
  180. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/licenses/LICENSE +0 -0
  181. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/top_level.txt +0 -0
@@ -7,42 +7,73 @@
7
7
  import os
8
8
  import subprocess
9
9
  import sys
10
- from typing import List, Optional, Tuple
10
+ import hashlib
11
+ from typing import Any, Dict, List, Optional, Tuple
11
12
 
12
13
  import typer
14
+ import yaml
13
15
 
14
16
  from jarvis.jarvis_agent import Agent
15
17
  from jarvis.jarvis_agent.events import AFTER_TOOL_CALL
16
- from jarvis.jarvis_agent.builtin_input_handler import builtin_input_handler
17
- from jarvis.jarvis_agent.edit_file_handler import EditFileHandler
18
- from jarvis.jarvis_agent.shell_input_handler import shell_input_handler
19
- from jarvis.jarvis_code_agent.lint import get_lint_tools
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
+ from jarvis.jarvis_code_agent.build_validation_config import BuildValidationConfig
20
26
  from jarvis.jarvis_git_utils.git_commiter import GitCommitTool
21
- from jarvis.jarvis_tools.registry import ToolRegistry
27
+ 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
22
30
  from jarvis.jarvis_utils.config import (
23
31
  is_confirm_before_apply_patch,
24
32
  is_enable_static_analysis,
33
+ is_enable_build_validation,
34
+ get_build_validation_timeout,
25
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,
26
42
  )
43
+ from jarvis.jarvis_platform.registry import PlatformRegistry
44
+ from jarvis.jarvis_code_agent.utils import get_project_overview
27
45
  from jarvis.jarvis_utils.git_utils import (
28
46
  confirm_add_new_files,
47
+ detect_large_code_deletion,
29
48
  find_git_root_and_cd,
30
49
  get_commits_between,
31
50
  get_diff,
32
51
  get_diff_file_list,
33
52
  get_latest_commit_hash,
34
- get_recent_commits_with_files,
35
53
  handle_commit_workflow,
36
54
  has_uncommitted_changes,
55
+ revert_change,
37
56
  )
38
57
  from jarvis.jarvis_utils.input import get_multiline_input, user_confirm
39
- from jarvis.jarvis_utils.output import OutputType, PrettyOutput
40
- from jarvis.jarvis_utils.utils import get_loc_stats, init_env
58
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput # 保留用于语法高亮
59
+ from jarvis.jarvis_utils.utils import init_env, _acquire_single_instance_lock
41
60
 
42
61
  app = typer.Typer(help="Jarvis 代码助手")
43
62
 
44
63
 
45
- class CodeAgent:
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
+ class CodeAgent(Agent):
46
77
  """Jarvis系统的代码修改代理。
47
78
 
48
79
  负责处理代码分析、修改和git操作。
@@ -54,23 +85,26 @@ class CodeAgent:
54
85
  need_summary: bool = True,
55
86
  append_tools: Optional[str] = None,
56
87
  tool_group: Optional[str] = None,
88
+ non_interactive: Optional[bool] = None,
89
+ rule_names: Optional[str] = None,
90
+ **kwargs,
57
91
  ):
58
92
  self.root_dir = os.getcwd()
59
93
  self.tool_group = tool_group
60
94
 
95
+ # 初始化上下文管理器
96
+ self.context_manager = ContextManager(self.root_dir)
97
+ # 上下文推荐器将在Agent创建后初始化(需要LLM模型)
98
+ self.context_recommender: Optional[ContextRecommender] = None
99
+
61
100
  # 检测 git username 和 email 是否已设置
62
101
  self._check_git_config()
63
- tool_registry = ToolRegistry() # type: ignore
64
102
  base_tools = [
65
103
  "execute_script",
66
- "search_web",
67
- "ask_user",
68
104
  "read_code",
105
+ "edit_file",
69
106
  "rewrite_file",
70
- "save_memory",
71
- "retrieve_memory",
72
- "clear_memory",
73
- "sub_code_agent",
107
+ "lsp_client", # LSP客户端工具,用于获取代码补全、悬停等信息
74
108
  ]
75
109
 
76
110
  if append_tools:
@@ -81,87 +115,251 @@ class CodeAgent:
81
115
  # 去重
82
116
  base_tools = list(dict.fromkeys(base_tools))
83
117
 
84
- tool_registry.use_tools(base_tools)
85
118
  code_system_prompt = self._get_system_prompt()
86
- self.agent = Agent(
87
- system_prompt=code_system_prompt,
88
- name="CodeAgent",
89
- auto_complete=False,
90
- output_handler=[tool_registry, EditFileHandler()], # type: ignore
91
- model_group=model_group,
92
- input_handler=[shell_input_handler, builtin_input_handler],
93
- need_summary=need_summary,
94
- use_methodology=False, # 禁用方法论
95
- use_analysis=False, # 禁用分析
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
+
153
+ # 调用父类 Agent 的初始化
154
+ # 默认禁用方法论和分析,但允许通过 kwargs 覆盖
155
+ use_methodology = kwargs.pop("use_methodology", False)
156
+ use_analysis = kwargs.pop("use_analysis", False)
157
+ # name 使用传入的值,如果没有传入则使用默认值 "CodeAgent"
158
+ name = kwargs.pop("name", "CodeAgent")
159
+
160
+ # 准备显式传递给 super().__init__ 的参数
161
+ # 注意:这些参数如果也在 kwargs 中,需要先移除,避免重复传递错误
162
+ explicit_params = {
163
+ "system_prompt": code_system_prompt,
164
+ "name": name,
165
+ "auto_complete": False,
166
+ "model_group": model_group,
167
+ "need_summary": need_summary,
168
+ "use_methodology": use_methodology,
169
+ "use_analysis": use_analysis,
170
+ "non_interactive": non_interactive,
171
+ "use_tools": base_tools,
172
+ }
173
+
174
+ # 自动移除所有显式传递的参数,避免重复传递错误
175
+ # 这样以后添加新参数时,只要在 explicit_params 中声明,就会自动处理
176
+ for key in explicit_params:
177
+ kwargs.pop(key, None)
178
+
179
+ super().__init__(
180
+ **explicit_params,
181
+ **kwargs,
96
182
  )
97
183
 
98
- self.agent.event_bus.subscribe(AFTER_TOOL_CALL, self._on_after_tool_call)
184
+ self._agent_type = "code_agent"
185
+
186
+ # 建立CodeAgent与Agent的关联,便于工具获取上下文管理器
187
+ self._code_agent = self
188
+
189
+ # 初始化上下文推荐器(自己创建LLM模型,使用父Agent的配置)
190
+ try:
191
+ # 获取当前Agent的model实例
192
+ parent_model = None
193
+ if self.model:
194
+ parent_model = self.model
195
+
196
+ self.context_recommender = ContextRecommender(
197
+ self.context_manager,
198
+ parent_model=parent_model
199
+ )
200
+ except Exception as e:
201
+ # LLM推荐器初始化失败
202
+ print(f"⚠️ 上下文推荐器初始化失败: {e},将跳过上下文推荐功能")
203
+
204
+ self.event_bus.subscribe(AFTER_TOOL_CALL, self._on_after_tool_call)
205
+
206
+ # 打印语言功能支持表格
207
+ try:
208
+ from jarvis.jarvis_agent.language_support_info import print_language_support_table
209
+ print_language_support_table()
210
+ except Exception:
211
+ pass
212
+
213
+ def _init_model(self, model_group: Optional[str]):
214
+ """初始化模型平台(CodeAgent使用smart平台,适用于代码生成等复杂场景)"""
215
+ platform_name = get_smart_platform_name(model_group)
216
+ model_name = get_smart_model_name(model_group)
217
+
218
+ maybe_model = PlatformRegistry().create_platform(platform_name)
219
+ if maybe_model is None:
220
+ print(f"⚠️ 平台 {platform_name} 不存在,将使用smart模型")
221
+ maybe_model = PlatformRegistry().get_smart_platform()
222
+
223
+ # 在此处收敛为非可选类型,确保后续赋值满足类型检查
224
+ self.model = maybe_model
225
+
226
+ if model_name:
227
+ self.model.set_model_name(model_name)
228
+
229
+ self.model.set_model_group(model_group)
230
+ self.model.set_suppress_output(False)
99
231
 
100
232
  def _get_system_prompt(self) -> str:
101
233
  """获取代码工程师的系统提示词"""
102
234
  return """
103
- <code_engineer_guide>
104
- ## 角色定位
105
- 你是Jarvis系统的代码工程师,一个专业的代码分析和修改助手。你的职责是:
106
- - 理解用户的代码需求,并提供高质量的实现方案
107
- - 精确分析项目结构和代码,准确定位需要修改的位置
108
- - 编写符合项目风格和标准的代码
109
- - 在修改代码时保持谨慎,确保不破坏现有功能
110
- - 做出专业的技术决策,减少用户决策负担
111
-
112
- ## 核心原则
113
- - 自主决策:基于专业判断做出决策,减少用户询问
114
- - 高效精准:提供完整解决方案,避免反复修改
115
- - 修改审慎:修改前充分分析影响范围,做到一次把事情做好
116
- - 工具精通:选择最高效工具路径解决问题
117
-
118
- ## 工作流程
119
- 1. **项目分析**:分析项目结构,确定需修改的文件
120
- 2. **需求分析**:理解需求意图,选择影响最小的实现方案
121
- 3. **代码分析**:详细分析目标文件,禁止虚构现有代码
122
- - 结构分析:优先使用文件搜索工具快速定位文件和目录结构
123
- - 内容搜索:优先使用全文搜索工具进行函数、类、变量等内容的搜索,避免遗漏
124
- - 依赖关系:如需分析依赖、调用关系,可结合代码分析工具辅助
125
- - 代码阅读:使用 read_code 工具获取目标文件的完整内容或指定范围内容,禁止凭空假设代码
126
- - 变更影响:如需分析变更影响范围,可结合版本控制工具辅助判断
127
- - 工具优先级:优先使用自动化工具,减少人工推断,确保分析结果准确
128
- 4. **方案设计**:确定最小变更方案,保持代码结构
129
- 5. **实施修改**:遵循"先读后写"原则,保持代码风格一致性
130
-
131
- ## 工具使用
132
- - 项目结构:优先使用文件搜索命令查找文件
133
- - 代码搜索:优先使用内容搜索工具
134
- - 代码阅读:优先使用read_code工具
135
- - 仅在命令行工具不足时使用专用工具
136
-
137
- ## 文件编辑工具使用规范
138
- - 对于部分文件内容修改,使用edit_file工具
139
- - 对于需要重写整个文件内容,使用rewrite_file工具
140
- - 对于简单的修改,可以使用execute_script工具执行shell命令完成
141
-
142
- ## 子任务与子CodeAgent
143
- - 当出现以下情况时,优先使用 sub_code_agent 工具将子任务托管给子 CodeAgent(自动完成并生成总结):
144
- - 需要在当前任务下并行推进较大且相对独立的代码改造
145
- - 涉及多文件/多模块的大范围变更,或需要较长的工具调用链
146
- - 需要隔离上下文以避免污染当前对话(如探索性改动、PoC)
147
- - 需要专注于单一子问题,阶段性产出可独立复用的结果
148
- - 其余常规、小粒度改动直接在当前 Agent 中完成即可
149
- </code_engineer_guide>
150
-
151
- <say_to_llm>
152
- 1. 保持专注与耐心,先分析再行动;将复杂问题拆解为可执行的小步骤
153
- 2. 以结果为导向,同时简明呈现关键推理依据,避免无关噪音
154
- 3. 信息不足时,主动提出最少且关键的问题以澄清需求
155
- 4. 输出前自检:一致性、边界条件、依赖关系、回滚与风险提示
156
- 5. 选择对现有系统影响最小且可回退的方案,确保稳定性与可维护性
157
- 6. 保持项目风格:结构、命名、工具使用与现有规范一致
158
- 7. 工具优先:使用搜索、read_code、版本控制与静态分析验证结论,拒绝臆测
159
- 8. 面对错误与不确定,给出修复计划与备选路径,持续迭代优于停滞
160
- 9. 沟通清晰:用要点列出结论、变更范围、影响评估与下一步行动
161
- 10. 持续改进:沉淀经验为可复用清单,下一次做得更快更稳
162
- </say_to_llm>
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
+
163
293
  """
164
294
 
295
+ def _read_project_rules(self) -> Optional[str]:
296
+ """读取 .jarvis/rules 内容,如果存在则返回字符串,否则返回 None"""
297
+ 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
307
+
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
316
+ except Exception:
317
+ # 读取规则失败时忽略,不影响主流程
318
+ 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
+
165
363
  def _check_git_config(self) -> None:
166
364
  """检查 git username 和 email 是否已设置,如果没有则提示并退出"""
167
365
  try:
@@ -198,25 +396,22 @@ class CodeAgent:
198
396
  message = "❌ Git 配置不完整\n\n请运行以下命令配置 Git:\n" + "\n".join(
199
397
  missing_configs
200
398
  )
201
- PrettyOutput.print(message, OutputType.WARNING)
399
+ print(f"⚠️ {message}")
202
400
  # 通过配置控制严格校验模式(JARVIS_GIT_CHECK_MODE):
203
401
  # - warn: 仅告警并继续,后续提交可能失败
204
402
  # - strict: 严格模式(默认),直接退出
205
403
  mode = get_git_check_mode().lower()
206
404
  if mode == "warn":
207
- PrettyOutput.print(
208
- "已启用 Git 校验警告模式(JARVIS_GIT_CHECK_MODE=warn),将继续运行。"
209
- "注意:后续提交可能失败,请尽快配置 git user.name 与 user.email。",
210
- OutputType.INFO,
211
- )
405
+ print("ℹ️ 已启用 Git 校验警告模式(JARVIS_GIT_CHECK_MODE=warn),将继续运行。"
406
+ "注意:后续提交可能失败,请尽快配置 git user.name 与 user.email。")
212
407
  return
213
408
  sys.exit(1)
214
409
 
215
410
  except FileNotFoundError:
216
- PrettyOutput.print("❌ 未找到 git 命令,请先安装 Git", OutputType.ERROR)
411
+ print("❌ 未找到 git 命令,请先安装 Git")
217
412
  sys.exit(1)
218
413
  except Exception as e:
219
- PrettyOutput.print(f"❌ 检查 Git 配置时出错: {str(e)}", OutputType.ERROR)
414
+ print(f"❌ 检查 Git 配置时出错: {str(e)}")
220
415
  sys.exit(1)
221
416
 
222
417
  def _find_git_root(self) -> str:
@@ -233,29 +428,148 @@ class CodeAgent:
233
428
  return git_dir
234
429
 
235
430
  def _update_gitignore(self, git_dir: str) -> None:
236
- """检查并更新.gitignore文件,确保忽略.jarvis目录
431
+ """检查并更新.gitignore文件,确保忽略.jarvis目录,并追加常用语言的忽略规则(若缺失)
237
432
 
238
433
  参数:
239
434
  git_dir: git根目录路径
240
435
  """
241
-
242
436
  gitignore_path = os.path.join(git_dir, ".gitignore")
243
- jarvis_ignore = ".jarvis"
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("") # 分组空行
244
556
 
245
557
  if not os.path.exists(gitignore_path):
246
- with open(gitignore_path, "w", encoding="utf-8") as f:
247
- f.write(f"{jarvis_ignore}\n")
248
- PrettyOutput.print(
249
- f"已创建 .gitignore 并添加 '{jarvis_ignore}'", OutputType.SUCCESS
250
- )
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 并添加常用忽略规则")
251
564
  else:
252
- with open(gitignore_path, "r+", encoding="utf-8") as f:
253
- content = f.read()
254
- if jarvis_ignore not in content.splitlines():
255
- f.write(f"\n{jarvis_ignore}\n")
256
- PrettyOutput.print(
257
- f"已更新 .gitignore,添加 '{jarvis_ignore}'", OutputType.SUCCESS
258
- )
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,追加常用忽略规则")
259
573
 
260
574
  def _handle_git_changes(self, prefix: str, suffix: str) -> None:
261
575
  """处理git仓库中的未提交修改"""
@@ -263,7 +577,7 @@ class CodeAgent:
263
577
  if has_uncommitted_changes():
264
578
 
265
579
  git_commiter = GitCommitTool()
266
- git_commiter.execute({"prefix": prefix, "suffix": suffix, "agent": self.agent, "model_group": getattr(self.agent.model, "model_group", None)})
580
+ git_commiter.execute({"prefix": prefix, "suffix": suffix, "agent": self, "model_group": getattr(self.model, "model_group", None)})
267
581
 
268
582
  def _init_env(self, prefix: str, suffix: str) -> None:
269
583
  """初始化环境,组合以下功能:
@@ -306,16 +620,14 @@ class CodeAgent:
306
620
 
307
621
  return
308
622
 
309
- PrettyOutput.print(
310
- "⚠️ 正在修改git换行符敏感设置,这会影响所有文件的换行符处理方式",
311
- OutputType.WARNING,
312
- )
623
+ print("⚠️ 正在修改git换行符敏感设置,这会影响所有文件的换行符处理方式")
313
624
  # 避免在循环中逐条打印,先拼接后统一打印
314
625
  lines = ["将进行以下设置:"]
315
626
  for key, value in target_settings.items():
316
627
  current = current_settings.get(key, "未设置")
317
628
  lines.append(f"{key}: {current} -> {value}")
318
- PrettyOutput.print("\n".join(lines), OutputType.INFO)
629
+ joined_lines = '\n'.join(lines)
630
+ print(f"ℹ️ {joined_lines}")
319
631
 
320
632
  # 直接执行设置,不需要用户确认
321
633
  for key, value in target_settings.items():
@@ -325,7 +637,7 @@ class CodeAgent:
325
637
  if sys.platform.startswith("win"):
326
638
  self._handle_windows_line_endings()
327
639
 
328
- PrettyOutput.print("git换行符敏感设置已更新", OutputType.SUCCESS)
640
+ print("git换行符敏感设置已更新")
329
641
 
330
642
  def _handle_windows_line_endings(self) -> None:
331
643
  """在Windows系统上处理换行符问题,提供建议而非强制修改"""
@@ -339,13 +651,8 @@ class CodeAgent:
339
651
  if any(keyword in content for keyword in ["text=", "eol=", "binary"]):
340
652
  return
341
653
 
342
- PrettyOutput.print(
343
- "提示:在Windows系统上,建议配置 .gitattributes 文件来避免换行符问题。",
344
- OutputType.INFO,
345
- )
346
- PrettyOutput.print(
347
- "这可以防止仅因换行符不同而导致整个文件被标记为修改。", OutputType.INFO
348
- )
654
+ print("ℹ️ 提示:在Windows系统上,建议配置 .gitattributes 文件来避免换行符问题。")
655
+ print("ℹ️ 这可以防止仅因换行符不同而导致整个文件被标记为修改。")
349
656
 
350
657
  if user_confirm("是否要创建一个最小化的.gitattributes文件?", False):
351
658
  # 最小化的内容,只影响特定类型的文件
@@ -364,25 +671,18 @@ class CodeAgent:
364
671
  if not os.path.exists(gitattributes_path):
365
672
  with open(gitattributes_path, "w", encoding="utf-8", newline="\n") as f:
366
673
  f.write(minimal_content)
367
- PrettyOutput.print(
368
- "已创建最小化的 .gitattributes 文件", OutputType.SUCCESS
369
- )
674
+ print("✅ 已创建最小化的 .gitattributes 文件")
370
675
  else:
371
- PrettyOutput.print(
372
- "将以下内容追加到现有 .gitattributes 文件:", OutputType.INFO
373
- )
374
- PrettyOutput.print(minimal_content, OutputType.CODE, lang="text")
676
+ print("ℹ️ 将以下内容追加到现有 .gitattributes 文件:")
677
+ PrettyOutput.print(minimal_content, OutputType.CODE, lang="text") # 保留语法高亮
375
678
  if user_confirm("是否追加到现有文件?", True):
376
679
  with open(
377
680
  gitattributes_path, "a", encoding="utf-8", newline="\n"
378
681
  ) as f:
379
682
  f.write("\n" + minimal_content)
380
- PrettyOutput.print("已更新 .gitattributes 文件", OutputType.SUCCESS)
683
+ print("已更新 .gitattributes 文件")
381
684
  else:
382
- PrettyOutput.print(
383
- "跳过 .gitattributes 文件创建。如遇换行符问题,可手动创建此文件。",
384
- OutputType.INFO,
385
- )
685
+ print("ℹ️ 跳过 .gitattributes 文件创建。如遇换行符问题,可手动创建此文件。")
386
686
 
387
687
  def _record_code_changes_stats(self, diff_text: str) -> None:
388
688
  """记录代码变更的统计信息。
@@ -433,7 +733,7 @@ class CodeAgent:
433
733
  except subprocess.CalledProcessError:
434
734
  pass
435
735
 
436
- PrettyOutput.print("检测到未提交的修改,是否要提交?", OutputType.WARNING)
736
+ print("⚠️ 检测到未提交的修改,是否要提交?")
437
737
  if not user_confirm("是否要提交?", True):
438
738
  return
439
739
 
@@ -471,7 +771,7 @@ class CodeAgent:
471
771
  check=True,
472
772
  )
473
773
  except subprocess.CalledProcessError as e:
474
- PrettyOutput.print(f"提交失败: {str(e)}", OutputType.ERROR)
774
+ print(f"提交失败: {str(e)}")
475
775
 
476
776
  def _show_commit_history(
477
777
  self, start_commit: Optional[str], end_commit: Optional[str]
@@ -499,9 +799,81 @@ class CodeAgent:
499
799
  commit_messages = "检测到以下提交记录:\n" + "\n".join(
500
800
  f"- {commit_hash[:7]}: {message}" for commit_hash, message in commits
501
801
  )
502
- PrettyOutput.print(commit_messages, OutputType.INFO)
802
+ print(f"ℹ️ {commit_messages}")
503
803
  return commits
504
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
+
505
877
  def _handle_commit_confirmation(
506
878
  self,
507
879
  commits: List[Tuple[str, str]],
@@ -522,15 +894,22 @@ class CodeAgent:
522
894
  stderr=subprocess.DEVNULL,
523
895
  check=True,
524
896
  )
897
+
898
+ # 检测变更文件并格式化
899
+ modified_files = get_diff_file_list()
900
+ if modified_files:
901
+ self._format_modified_files(modified_files)
902
+
525
903
  git_commiter = GitCommitTool()
526
- git_commiter.execute({"prefix": prefix, "suffix": suffix, "agent": self.agent, "model_group": getattr(self.agent.model, "model_group", None)})
904
+ git_commiter.execute({"prefix": prefix, "suffix": suffix, "agent": self, "model_group": getattr(self.model, "model_group", None)})
527
905
 
528
906
  # 在用户接受commit后,根据配置决定是否保存记忆
529
- if self.agent.force_save_memory:
530
- self.agent.memory_manager.prompt_memory_save()
907
+ if self.force_save_memory:
908
+ self.memory_manager.prompt_memory_save()
531
909
  elif start_commit:
532
- os.system(f"git reset --hard {str(start_commit)}") # 确保转换为字符串
533
- PrettyOutput.print("已重置到初始提交", OutputType.INFO)
910
+ if user_confirm("是否要重置到初始提交?", True):
911
+ os.system(f"git reset --hard {str(start_commit)}") # 确保转换为字符串
912
+ print("ℹ️ 已重置到初始提交")
534
913
 
535
914
  def run(self, user_input: str, prefix: str = "", suffix: str = "") -> Optional[str]:
536
915
  """使用给定的用户输入运行代码代理。
@@ -541,51 +920,71 @@ class CodeAgent:
541
920
  返回:
542
921
  str: 描述执行结果的输出,成功时返回None
543
922
  """
923
+ prev_dir = os.getcwd()
544
924
  try:
545
925
  self._init_env(prefix, suffix)
546
926
  start_commit = get_latest_commit_hash()
547
927
 
548
- # 获取项目统计信息并附加到用户输入
549
- loc_stats = get_loc_stats()
550
- commits_info = get_recent_commits_with_files()
551
-
552
- project_info = []
553
- if loc_stats:
554
- project_info.append(f"代码统计:\n{loc_stats}")
555
- if commits_info:
556
- commits_str = "\n".join(
557
- f"提交 {i+1}: {commit['hash'][:7]} - {commit['message']} ({len(commit['files'])}个文件)\n"
558
- + "\n".join(f" - {file}" for file in commit["files"][:5])
559
- + ("\n ..." if len(commit["files"]) > 5 else "")
560
- for i, commit in enumerate(commits_info[:5])
561
- )
562
- project_info.append(f"最近提交:\n{commits_str}")
928
+ # 获取项目概况信息
929
+ project_overview = get_project_overview(self.root_dir)
563
930
 
564
931
  first_tip = """请严格遵循以下规范进行代码修改任务:
565
932
  1. 每次响应仅执行一步操作,先分析再修改,避免一步多改。
566
933
  2. 充分利用工具理解用户需求和现有代码,禁止凭空假设。
567
934
  3. 如果不清楚要修改的文件,必须先分析并找出需要修改的文件,明确目标后再进行编辑。
568
- 4. 代码编辑任务优先使用 edit_file 工具,确保搜索文本在目标文件中有且仅有一次精确匹配,保证修改的准确性和安全性。
569
- 5. 如需大范围重写,才可使用 rewrite_file 工具。
935
+ 4. 代码编辑任务优先使用 PATCH 操作,确保搜索文本在目标文件中有且仅有一次精确匹配,保证修改的准确性和安全性。
936
+ 5. 如需大范围重写,才可使用 REWRITE 操作。
570
937
  6. 如遇信息不明,优先调用工具补充分析,不要主观臆断。
571
938
  """
572
939
 
573
- if project_info:
940
+ # 智能上下文推荐:根据用户输入推荐相关上下文
941
+ context_recommendation_text = ""
942
+ if self.context_recommender and is_enable_intent_recognition():
943
+ # 在意图识别和上下文推荐期间抑制模型输出
944
+ was_suppressed = False
945
+ if self.model:
946
+ was_suppressed = getattr(self.model, '_suppress_output', False)
947
+ self.model.set_suppress_output(True)
948
+ try:
949
+ print("🔍 正在进行智能上下文推荐....")
950
+
951
+ # 生成上下文推荐(基于关键词和项目上下文)
952
+ recommendation = self.context_recommender.recommend_context(
953
+ user_input=user_input,
954
+ )
955
+
956
+ # 格式化推荐结果
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:
963
+ # 上下文推荐失败不应该影响主流程
964
+ print(f"⚠️ 上下文推荐失败: {e}")
965
+ finally:
966
+ # 恢复模型输出设置
967
+ if self.model:
968
+ self.model.set_suppress_output(was_suppressed)
969
+
970
+ if project_overview:
574
971
  enhanced_input = (
575
- "项目概况:\n"
576
- + "\n\n".join(project_info)
972
+ project_overview
577
973
  + "\n\n"
578
974
  + first_tip
975
+ + context_recommendation_text
579
976
  + "\n\n任务描述:\n"
580
977
  + user_input
581
978
  )
582
979
  else:
583
- enhanced_input = first_tip + "\n\n任务描述:\n" + user_input
980
+ enhanced_input = first_tip + context_recommendation_text + "\n\n任务描述:\n" + user_input
584
981
 
585
982
  try:
586
- self.agent.run(enhanced_input)
983
+ if self.model:
984
+ self.model.set_suppress_output(False)
985
+ super().run(enhanced_input)
587
986
  except RuntimeError as e:
588
- PrettyOutput.print(f"执行失败: {str(e)}", OutputType.WARNING)
987
+ print(f"⚠️ 执行失败: {str(e)}")
589
988
  return str(e)
590
989
 
591
990
 
@@ -598,15 +997,528 @@ class CodeAgent:
598
997
 
599
998
  except RuntimeError as e:
600
999
  return f"Error during execution: {str(e)}"
1000
+ finally:
1001
+ # Ensure switching back to the original working directory after CodeAgent completes
1002
+ try:
1003
+ os.chdir(prev_dir)
1004
+ except Exception:
1005
+ pass
1006
+
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
1047
+
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)
1068
+
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] = []
1073
+
1074
+ def _get_file_numstat(file_path: str) -> Tuple[int, int]:
1075
+ """获取单文件的新增/删除行数,失败时返回(0,0)"""
1076
+ head_exists = bool(get_latest_commit_hash())
1077
+ 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,
1088
+ )
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
1124
+
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)
1133
+
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
1148
+
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
+ "请仔细检查错误信息,修复编译/构建错误后重新提交。"
1340
+ )
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
+
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
601
1488
 
602
1489
  def _on_after_tool_call(self, agent: Agent, current_response=None, need_return=None, tool_prompt=None, **kwargs) -> None:
603
1490
  """工具调用后回调函数。"""
604
1491
  final_ret = ""
605
1492
  diff = get_diff()
1493
+
606
1494
  if diff:
607
1495
  start_hash = get_latest_commit_hash()
608
- PrettyOutput.print(diff, OutputType.CODE, lang="diff")
1496
+ PrettyOutput.print(diff, OutputType.CODE, lang="diff") # 保留语法高亮
609
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
+ # 所有模式下,在提交前检测大量代码删除并询问大模型
1508
+ detection_result = detect_large_code_deletion()
1509
+ if detection_result is not None:
1510
+ # 检测到大量代码删除,询问大模型是否合理
1511
+ is_reasonable = self._ask_llm_about_large_deletion(detection_result, per_file_preview)
1512
+ if not is_reasonable:
1513
+ # 大模型认为不合理,撤销修改
1514
+ print("ℹ️ 已撤销修改(大模型认为代码删除不合理)")
1515
+ revert_change()
1516
+ final_ret += "\n\n修改被撤销(检测到大量代码删除且大模型判断不合理)\n"
1517
+ final_ret += f"# 补丁预览(按文件):\n{per_file_preview}"
1518
+ PrettyOutput.print(final_ret, OutputType.USER, lang="markdown") # 保留语法高亮
1519
+ self.session.prompt += final_ret
1520
+ return
1521
+
610
1522
  commited = handle_commit_workflow()
611
1523
  if commited:
612
1524
  # 统计代码行数变化
@@ -630,65 +1542,267 @@ class CodeAgent:
630
1542
 
631
1543
  StatsManager.increment("code_modifications", group="code_agent")
632
1544
 
633
-
634
1545
  # 获取提交信息
635
1546
  end_hash = get_latest_commit_hash()
636
1547
  commits = get_commits_between(start_hash, end_hash)
637
1548
 
638
- # 添加提交信息到final_ret
1549
+ # 添加提交信息到final_ret(按文件展示diff;删除文件仅提示)
639
1550
  if commits:
1551
+ # 获取最新的提交信息(commits列表按时间倒序,第一个是最新的)
1552
+ 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
+
640
1555
  final_ret += (
641
- f"\n\n代码已修改完成\n补丁内容:\n```diff\n{diff}\n```\n"
1556
+ f"\n\n代码已修改完成\n"
1557
+ f"✅ 已自动提交\n"
1558
+ f" Commit ID: {commit_short_hash} ({latest_commit_hash})\n"
1559
+ f" 提交信息: {latest_commit_message}\n"
1560
+ f"\n补丁内容(按文件):\n{per_file_preview}\n"
642
1561
  )
643
- # 修改后的提示逻辑
644
- lint_tools_info = "\n".join(
645
- f" - {file}: 使用 {'、'.join(get_lint_tools(file))}"
646
- for file in modified_files
647
- if get_lint_tools(file)
648
- )
649
- file_list = "\n".join(f" - {file}" for file in modified_files)
650
- tool_info = (
651
- f"建议使用以下lint工具进行检查:\n{lint_tools_info}"
652
- if lint_tools_info
653
- else ""
654
- )
655
- if lint_tools_info and is_enable_static_analysis():
656
- addon_prompt = f"""
657
- 请对以下修改的文件进行静态扫描:
658
- {file_list}
659
- {tool_info}
660
- 如果本次修改引入了警告和错误,请根据警告和错误信息修复代码
661
- 注意:如果要进行静态检查,需要在所有的修改都完成之后进行集中检查,如果文件有多个检查工具,尽量一次全部调用,不要分多次调用
662
- """
663
- agent.set_addon_prompt(addon_prompt)
1562
+
1563
+ # 添加影响范围分析报告
1564
+ final_ret = self._handle_impact_report(impact_report, self, final_ret)
1565
+
1566
+ # 构建验证
1567
+ config = BuildValidationConfig(self.root_dir)
1568
+ build_validation_result, final_ret = self._handle_build_validation(modified_files, self, final_ret)
1569
+
1570
+ # 静态分析
1571
+ final_ret = self._handle_static_analysis(modified_files, build_validation_result, config, self, final_ret)
664
1572
  else:
665
- final_ret += "\n\n修改没有生效\n"
1573
+ # 如果没有获取到commits,尝试直接从end_hash获取commit信息
1574
+ commit_info = ""
1575
+ if end_hash:
1576
+ try:
1577
+ result = subprocess.run(
1578
+ ["git", "log", "-1", "--pretty=format:%H|%s", end_hash],
1579
+ capture_output=True,
1580
+ text=True,
1581
+ encoding="utf-8",
1582
+ errors="replace",
1583
+ check=False,
1584
+ )
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
1588
+ commit_info = (
1589
+ f"\n✅ 已自动提交\n"
1590
+ f" Commit ID: {commit_short_hash} ({commit_hash})\n"
1591
+ f" 提交信息: {commit_message}\n"
1592
+ )
1593
+ except Exception:
1594
+ pass
1595
+
1596
+ if commit_info:
1597
+ final_ret += f"\n\n代码已修改完成{commit_info}\n"
1598
+ else:
1599
+ final_ret += "\n\n修改没有生效\n"
666
1600
  else:
667
1601
  final_ret += "\n修改被拒绝\n"
668
- final_ret += f"# 补丁预览:\n```diff\n{diff}\n```"
1602
+ final_ret += f"# 补丁预览(按文件):\n{per_file_preview}"
669
1603
  else:
670
1604
  return
671
1605
  # 用户确认最终结果
672
1606
  if commited:
673
- agent.session.prompt += final_ret
1607
+ self.session.prompt += final_ret
674
1608
  return
675
- PrettyOutput.print(final_ret, OutputType.USER, lang="markdown")
1609
+ PrettyOutput.print(final_ret, OutputType.USER, lang="markdown") # 保留语法高亮
676
1610
  if not is_confirm_before_apply_patch() or user_confirm(
677
1611
  "是否使用此回复?", default=True
678
1612
  ):
679
- agent.session.prompt += final_ret
1613
+ self.session.prompt += final_ret
680
1614
  return
681
1615
  # 用户未确认,允许输入自定义回复作为附加提示
682
1616
  custom_reply = get_multiline_input("请输入自定义回复")
683
1617
  if custom_reply.strip(): # 如果自定义回复为空,不设置附加提示
684
- agent.set_addon_prompt(custom_reply)
685
- agent.session.prompt += final_ret
1618
+ self.set_addon_prompt(custom_reply)
1619
+ self.session.prompt += final_ret
686
1620
  return
687
1621
 
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)
1631
+ """
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秒超时
1681
+ )
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}")
1732
+ 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
+ 格式化的错误信息字符串
1747
+ """
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}")
1758
+ 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
1776
+ """
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
1802
+
688
1803
 
689
1804
  @app.command()
690
1805
  def cli(
691
-
692
1806
  model_group: Optional[str] = typer.Option(
693
1807
  None, "-g", "--llm-group", help="使用的模型组,覆盖配置文件中的设置"
694
1808
  ),
@@ -719,12 +1833,45 @@ def cli(
719
1833
  "--suffix",
720
1834
  help="提交信息后缀(用换行分隔)",
721
1835
  ),
1836
+ non_interactive: bool = typer.Option(
1837
+ False, "-n", "--non-interactive", help="启用非交互模式:用户无法与命令交互,脚本执行超时限制为5分钟"
1838
+ ),
1839
+ rule_names: Optional[str] = typer.Option(
1840
+ None, "--rule-names", help="指定规则名称列表,用逗号分隔,从 rules.yaml 文件中读取对应的规则内容"
1841
+ ),
722
1842
  ) -> None:
723
1843
  """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
+ # 非交互模式要求从命令行传入任务
1852
+ if non_interactive and not (requirement and str(requirement).strip()):
1853
+ print("❌ 非交互模式已启用:必须使用 --requirement 传入任务内容,因多行输入不可用。")
1854
+ raise typer.Exit(code=2)
724
1855
  init_env(
725
1856
  "欢迎使用 Jarvis-CodeAgent,您的代码工程助手已准备就绪!",
726
1857
  config_file=config_file,
727
1858
  )
1859
+ # CodeAgent 单实例互斥:改为按仓库维度加锁(延后至定位仓库根目录后执行)
1860
+ # 锁的获取移动到确认并切换到git根目录之后
1861
+
1862
+ # 在初始化环境后同步 CLI 选项到全局配置,避免被 init_env 覆盖
1863
+ try:
1864
+ if model_group:
1865
+ set_config("JARVIS_LLM_GROUP", str(model_group))
1866
+ if tool_group:
1867
+ set_config("JARVIS_TOOL_GROUP", str(tool_group))
1868
+ if restore_session:
1869
+ set_config("JARVIS_RESTORE_SESSION", True)
1870
+ if non_interactive:
1871
+ set_config("JARVIS_NON_INTERACTIVE", True)
1872
+ except Exception:
1873
+ # 静默忽略同步异常,不影响主流程
1874
+ pass
728
1875
 
729
1876
  try:
730
1877
  subprocess.run(
@@ -735,12 +1882,11 @@ def cli(
735
1882
  )
736
1883
  except (subprocess.CalledProcessError, FileNotFoundError):
737
1884
  curr_dir_path = os.getcwd()
738
- PrettyOutput.print(
739
- f"警告:当前目录 '{curr_dir_path}' 不是一个git仓库。", OutputType.WARNING
740
- )
741
- if user_confirm(
1885
+ print(f"⚠️ 警告:当前目录 '{curr_dir_path}' 不是一个git仓库。")
1886
+ init_git = True if non_interactive else user_confirm(
742
1887
  f"是否要在 '{curr_dir_path}' 中初始化一个新的git仓库?", default=True
743
- ):
1888
+ )
1889
+ if init_git:
744
1890
  try:
745
1891
  subprocess.run(
746
1892
  ["git", "init"],
@@ -750,36 +1896,41 @@ def cli(
750
1896
  encoding="utf-8",
751
1897
  errors="replace",
752
1898
  )
753
- PrettyOutput.print("✅ 已成功初始化git仓库。", OutputType.SUCCESS)
1899
+ print("✅ 已成功初始化git仓库。")
754
1900
  except (subprocess.CalledProcessError, FileNotFoundError) as e:
755
- PrettyOutput.print(f"❌ 初始化git仓库失败: {e}", OutputType.ERROR)
1901
+ print(f"❌ 初始化git仓库失败: {e}")
756
1902
  sys.exit(1)
757
1903
  else:
758
- PrettyOutput.print(
759
- "操作已取消。Jarvis需要在git仓库中运行。", OutputType.INFO
760
- )
1904
+ print("ℹ️ 操作已取消。Jarvis需要在git仓库中运行。")
761
1905
  sys.exit(0)
762
1906
 
763
1907
  curr_dir = os.getcwd()
764
1908
  find_git_root_and_cd(curr_dir)
1909
+ # 在定位到 git 根目录后,按仓库维度加锁,避免跨仓库互斥
1910
+ try:
1911
+ repo_root = os.getcwd()
1912
+ lock_name = f"code_agent_{hashlib.md5(repo_root.encode('utf-8')).hexdigest()}.lock"
1913
+ _acquire_single_instance_lock(lock_name=lock_name)
1914
+ except Exception:
1915
+ # 回退到全局锁,确保至少有互斥保护
1916
+ _acquire_single_instance_lock(lock_name="code_agent.lock")
765
1917
  try:
1918
+
766
1919
  agent = CodeAgent(
767
1920
  model_group=model_group,
768
1921
  need_summary=False,
769
1922
  append_tools=append_tools,
770
1923
  tool_group=tool_group,
1924
+ non_interactive=non_interactive,
1925
+ rule_names=rule_names,
771
1926
  )
772
1927
 
773
1928
  # 尝试恢复会话
774
1929
  if restore_session:
775
- if agent.agent.restore_session():
776
- PrettyOutput.print(
777
- "已从 .jarvis/saved_session.json 恢复会话。", OutputType.SUCCESS
778
- )
1930
+ if agent.restore_session():
1931
+ print("✅ 已从 .jarvis/saved_session.json 恢复会话。")
779
1932
  else:
780
- PrettyOutput.print(
781
- "无法从 .jarvis/saved_session.json 恢复会话。", OutputType.WARNING
782
- )
1933
+ print("⚠️ 无法从 .jarvis/saved_session.json 恢复会话。")
783
1934
 
784
1935
  if requirement:
785
1936
  agent.run(requirement, prefix=prefix, suffix=suffix)
@@ -793,7 +1944,7 @@ def cli(
793
1944
  except typer.Exit:
794
1945
  raise
795
1946
  except RuntimeError as e:
796
- PrettyOutput.print(f"错误: {str(e)}", OutputType.ERROR)
1947
+ print(f"错误: {str(e)}")
797
1948
  sys.exit(1)
798
1949
 
799
1950