jarvis-ai-assistant 0.1.222__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +1143 -245
- jarvis/jarvis_agent/agent_manager.py +97 -0
- jarvis/jarvis_agent/builtin_input_handler.py +12 -10
- jarvis/jarvis_agent/config_editor.py +57 -0
- jarvis/jarvis_agent/edit_file_handler.py +392 -99
- jarvis/jarvis_agent/event_bus.py +48 -0
- jarvis/jarvis_agent/events.py +157 -0
- jarvis/jarvis_agent/file_context_handler.py +79 -0
- jarvis/jarvis_agent/file_methodology_manager.py +117 -0
- jarvis/jarvis_agent/jarvis.py +1117 -147
- jarvis/jarvis_agent/main.py +78 -34
- jarvis/jarvis_agent/memory_manager.py +195 -0
- jarvis/jarvis_agent/methodology_share_manager.py +174 -0
- jarvis/jarvis_agent/prompt_manager.py +82 -0
- jarvis/jarvis_agent/prompts.py +46 -9
- jarvis/jarvis_agent/protocols.py +4 -1
- jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
- jarvis/jarvis_agent/run_loop.py +146 -0
- jarvis/jarvis_agent/session_manager.py +9 -9
- jarvis/jarvis_agent/share_manager.py +228 -0
- jarvis/jarvis_agent/shell_input_handler.py +23 -3
- jarvis/jarvis_agent/stdio_redirect.py +295 -0
- jarvis/jarvis_agent/task_analyzer.py +212 -0
- jarvis/jarvis_agent/task_manager.py +154 -0
- jarvis/jarvis_agent/task_planner.py +496 -0
- jarvis/jarvis_agent/tool_executor.py +8 -4
- jarvis/jarvis_agent/tool_share_manager.py +139 -0
- jarvis/jarvis_agent/user_interaction.py +42 -0
- jarvis/jarvis_agent/utils.py +54 -0
- jarvis/jarvis_agent/web_bridge.py +189 -0
- jarvis/jarvis_agent/web_output_sink.py +53 -0
- jarvis/jarvis_agent/web_server.py +751 -0
- jarvis/jarvis_c2rust/__init__.py +26 -0
- jarvis/jarvis_c2rust/cli.py +613 -0
- jarvis/jarvis_c2rust/collector.py +258 -0
- jarvis/jarvis_c2rust/library_replacer.py +1122 -0
- jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
- jarvis/jarvis_c2rust/optimizer.py +960 -0
- jarvis/jarvis_c2rust/scanner.py +1681 -0
- jarvis/jarvis_c2rust/transpiler.py +2325 -0
- jarvis/jarvis_code_agent/build_validation_config.py +133 -0
- jarvis/jarvis_code_agent/code_agent.py +1605 -178
- jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
- jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
- jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
- jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
- jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
- jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
- jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
- jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
- jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
- jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
- jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
- jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
- jarvis/jarvis_code_agent/lint.py +275 -13
- jarvis/jarvis_code_agent/utils.py +142 -0
- jarvis/jarvis_code_analysis/checklists/loader.py +20 -6
- jarvis/jarvis_code_analysis/code_review.py +583 -548
- jarvis/jarvis_data/config_schema.json +339 -28
- jarvis/jarvis_git_squash/main.py +22 -13
- jarvis/jarvis_git_utils/git_commiter.py +171 -55
- jarvis/jarvis_mcp/sse_mcp_client.py +22 -15
- jarvis/jarvis_mcp/stdio_mcp_client.py +4 -4
- jarvis/jarvis_mcp/streamable_mcp_client.py +36 -16
- jarvis/jarvis_memory_organizer/memory_organizer.py +753 -0
- jarvis/jarvis_methodology/main.py +48 -63
- jarvis/jarvis_multi_agent/__init__.py +302 -43
- jarvis/jarvis_multi_agent/main.py +70 -24
- jarvis/jarvis_platform/ai8.py +40 -23
- jarvis/jarvis_platform/base.py +210 -49
- jarvis/jarvis_platform/human.py +11 -1
- jarvis/jarvis_platform/kimi.py +82 -76
- jarvis/jarvis_platform/openai.py +73 -1
- jarvis/jarvis_platform/registry.py +8 -15
- jarvis/jarvis_platform/tongyi.py +115 -101
- jarvis/jarvis_platform/yuanbao.py +89 -63
- jarvis/jarvis_platform_manager/main.py +194 -132
- jarvis/jarvis_platform_manager/service.py +122 -86
- jarvis/jarvis_rag/cli.py +156 -53
- jarvis/jarvis_rag/embedding_manager.py +155 -12
- jarvis/jarvis_rag/llm_interface.py +10 -13
- jarvis/jarvis_rag/query_rewriter.py +63 -12
- jarvis/jarvis_rag/rag_pipeline.py +222 -40
- jarvis/jarvis_rag/reranker.py +26 -3
- jarvis/jarvis_rag/retriever.py +270 -14
- jarvis/jarvis_sec/__init__.py +3605 -0
- jarvis/jarvis_sec/checkers/__init__.py +32 -0
- jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
- jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
- jarvis/jarvis_sec/cli.py +116 -0
- jarvis/jarvis_sec/report.py +257 -0
- jarvis/jarvis_sec/status.py +264 -0
- jarvis/jarvis_sec/types.py +20 -0
- jarvis/jarvis_sec/workflow.py +219 -0
- jarvis/jarvis_smart_shell/main.py +405 -137
- jarvis/jarvis_stats/__init__.py +13 -0
- jarvis/jarvis_stats/cli.py +387 -0
- jarvis/jarvis_stats/stats.py +711 -0
- jarvis/jarvis_stats/storage.py +612 -0
- jarvis/jarvis_stats/visualizer.py +282 -0
- jarvis/jarvis_tools/ask_user.py +1 -0
- jarvis/jarvis_tools/base.py +18 -2
- jarvis/jarvis_tools/clear_memory.py +239 -0
- jarvis/jarvis_tools/cli/main.py +220 -144
- jarvis/jarvis_tools/execute_script.py +52 -12
- jarvis/jarvis_tools/file_analyzer.py +17 -12
- jarvis/jarvis_tools/generate_new_tool.py +46 -24
- jarvis/jarvis_tools/read_code.py +277 -18
- jarvis/jarvis_tools/read_symbols.py +141 -0
- jarvis/jarvis_tools/read_webpage.py +86 -13
- jarvis/jarvis_tools/registry.py +294 -90
- jarvis/jarvis_tools/retrieve_memory.py +227 -0
- jarvis/jarvis_tools/save_memory.py +194 -0
- jarvis/jarvis_tools/search_web.py +62 -28
- jarvis/jarvis_tools/sub_agent.py +205 -0
- jarvis/jarvis_tools/sub_code_agent.py +217 -0
- jarvis/jarvis_tools/virtual_tty.py +330 -62
- jarvis/jarvis_utils/builtin_replace_map.py +4 -5
- jarvis/jarvis_utils/clipboard.py +90 -0
- jarvis/jarvis_utils/config.py +607 -50
- jarvis/jarvis_utils/embedding.py +3 -0
- jarvis/jarvis_utils/fzf.py +57 -0
- jarvis/jarvis_utils/git_utils.py +251 -29
- jarvis/jarvis_utils/globals.py +174 -17
- jarvis/jarvis_utils/http.py +58 -79
- jarvis/jarvis_utils/input.py +899 -153
- jarvis/jarvis_utils/methodology.py +210 -83
- jarvis/jarvis_utils/output.py +220 -137
- jarvis/jarvis_utils/utils.py +1906 -135
- jarvis_ai_assistant-0.7.0.dist-info/METADATA +465 -0
- jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +8 -2
- jarvis/jarvis_git_details/main.py +0 -265
- jarvis/jarvis_platform/oyi.py +0 -357
- jarvis/jarvis_tools/edit_file.py +0 -255
- jarvis/jarvis_tools/rewrite_file.py +0 -195
- jarvis_ai_assistant-0.1.222.dist-info/METADATA +0 -767
- jarvis_ai_assistant-0.1.222.dist-info/RECORD +0 -110
- /jarvis/{jarvis_git_details → jarvis_memory_organizer}/__init__.py +0 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -4,23 +4,43 @@
|
|
|
4
4
|
该模块提供CodeAgent类,用于处理代码修改任务。
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import argparse
|
|
8
7
|
import os
|
|
9
8
|
import subprocess
|
|
10
9
|
import sys
|
|
11
|
-
|
|
10
|
+
import hashlib
|
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
import typer
|
|
12
14
|
|
|
13
15
|
from jarvis.jarvis_agent import Agent
|
|
14
|
-
from jarvis.jarvis_agent.
|
|
15
|
-
from jarvis.
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
from jarvis.jarvis_agent.events import AFTER_TOOL_CALL
|
|
17
|
+
from jarvis.jarvis_code_agent.lint import (
|
|
18
|
+
get_lint_tools,
|
|
19
|
+
get_lint_commands_for_files,
|
|
20
|
+
group_commands_by_tool,
|
|
21
|
+
)
|
|
22
|
+
from jarvis.jarvis_code_agent.code_analyzer.build_validator import BuildValidator, BuildResult, FallbackBuildValidator
|
|
23
|
+
from jarvis.jarvis_code_agent.build_validation_config import BuildValidationConfig
|
|
18
24
|
from jarvis.jarvis_git_utils.git_commiter import GitCommitTool
|
|
19
|
-
from jarvis.
|
|
20
|
-
from jarvis.
|
|
21
|
-
from jarvis.
|
|
25
|
+
from jarvis.jarvis_code_agent.code_analyzer import ContextManager
|
|
26
|
+
from jarvis.jarvis_code_agent.code_analyzer.llm_context_recommender import ContextRecommender
|
|
27
|
+
from jarvis.jarvis_code_agent.code_analyzer import ImpactAnalyzer, parse_git_diff_to_edits
|
|
28
|
+
from jarvis.jarvis_utils.config import (
|
|
29
|
+
is_confirm_before_apply_patch,
|
|
30
|
+
is_enable_static_analysis,
|
|
31
|
+
is_enable_build_validation,
|
|
32
|
+
get_build_validation_timeout,
|
|
33
|
+
get_git_check_mode,
|
|
34
|
+
set_config,
|
|
35
|
+
get_data_dir,
|
|
36
|
+
is_plan_enabled,
|
|
37
|
+
is_enable_intent_recognition,
|
|
38
|
+
is_enable_impact_analysis,
|
|
39
|
+
)
|
|
40
|
+
from jarvis.jarvis_code_agent.utils import get_project_overview
|
|
22
41
|
from jarvis.jarvis_utils.git_utils import (
|
|
23
42
|
confirm_add_new_files,
|
|
43
|
+
detect_large_code_deletion,
|
|
24
44
|
find_git_root_and_cd,
|
|
25
45
|
get_commits_between,
|
|
26
46
|
get_diff,
|
|
@@ -29,13 +49,28 @@ from jarvis.jarvis_utils.git_utils import (
|
|
|
29
49
|
get_recent_commits_with_files,
|
|
30
50
|
handle_commit_workflow,
|
|
31
51
|
has_uncommitted_changes,
|
|
52
|
+
revert_change,
|
|
32
53
|
)
|
|
33
54
|
from jarvis.jarvis_utils.input import get_multiline_input, user_confirm
|
|
34
55
|
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
|
35
|
-
from jarvis.jarvis_utils.utils import
|
|
56
|
+
from jarvis.jarvis_utils.utils import init_env, _acquire_single_instance_lock
|
|
57
|
+
|
|
58
|
+
app = typer.Typer(help="Jarvis 代码助手")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _format_build_error(result: BuildResult, max_len: int = 2000) -> str:
|
|
62
|
+
"""格式化构建错误信息,限制输出长度"""
|
|
63
|
+
error_msg = result.error_message or ""
|
|
64
|
+
output = result.output or ""
|
|
65
|
+
|
|
66
|
+
full_error = f"{error_msg}\n{output}".strip()
|
|
67
|
+
|
|
68
|
+
if len(full_error) > max_len:
|
|
69
|
+
return full_error[:max_len] + "\n... (输出已截断)"
|
|
70
|
+
return full_error
|
|
36
71
|
|
|
37
72
|
|
|
38
|
-
class CodeAgent:
|
|
73
|
+
class CodeAgent(Agent):
|
|
39
74
|
"""Jarvis系统的代码修改代理。
|
|
40
75
|
|
|
41
76
|
负责处理代码分析、修改和git操作。
|
|
@@ -43,22 +78,104 @@ class CodeAgent:
|
|
|
43
78
|
|
|
44
79
|
def __init__(
|
|
45
80
|
self,
|
|
46
|
-
|
|
81
|
+
model_group: Optional[str] = None,
|
|
47
82
|
need_summary: bool = True,
|
|
83
|
+
append_tools: Optional[str] = None,
|
|
84
|
+
tool_group: Optional[str] = None,
|
|
85
|
+
non_interactive: Optional[bool] = None,
|
|
86
|
+
plan: Optional[bool] = None,
|
|
87
|
+
**kwargs,
|
|
48
88
|
):
|
|
49
89
|
self.root_dir = os.getcwd()
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
90
|
+
self.tool_group = tool_group
|
|
91
|
+
|
|
92
|
+
# 初始化上下文管理器
|
|
93
|
+
self.context_manager = ContextManager(self.root_dir)
|
|
94
|
+
# 上下文推荐器将在Agent创建后初始化(需要LLM模型)
|
|
95
|
+
self.context_recommender: Optional[ContextRecommender] = None
|
|
96
|
+
|
|
97
|
+
# 检测 git username 和 email 是否已设置
|
|
98
|
+
self._check_git_config()
|
|
99
|
+
base_tools = [
|
|
100
|
+
"execute_script",
|
|
101
|
+
"search_web",
|
|
102
|
+
"ask_user",
|
|
103
|
+
"read_code",
|
|
104
|
+
"save_memory",
|
|
105
|
+
"retrieve_memory",
|
|
106
|
+
"clear_memory",
|
|
107
|
+
"sub_code_agent",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
if append_tools:
|
|
111
|
+
additional_tools = [
|
|
112
|
+
t for t in (tool.strip() for tool in append_tools.split(",")) if t
|
|
59
113
|
]
|
|
114
|
+
base_tools.extend(additional_tools)
|
|
115
|
+
# 去重
|
|
116
|
+
base_tools = list(dict.fromkeys(base_tools))
|
|
117
|
+
|
|
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
|
+
if global_rules:
|
|
125
|
+
combined_parts.append(global_rules)
|
|
126
|
+
if project_rules:
|
|
127
|
+
combined_parts.append(project_rules)
|
|
128
|
+
|
|
129
|
+
if combined_parts:
|
|
130
|
+
merged_rules = "\n\n".join(combined_parts)
|
|
131
|
+
code_system_prompt = (
|
|
132
|
+
f"{code_system_prompt}\n\n"
|
|
133
|
+
f"<rules>\n{merged_rules}\n</rules>"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# 调用父类 Agent 的初始化
|
|
137
|
+
# 默认禁用方法论和分析,但允许通过 kwargs 覆盖
|
|
138
|
+
use_methodology = kwargs.pop("use_methodology", False)
|
|
139
|
+
use_analysis = kwargs.pop("use_analysis", False)
|
|
140
|
+
super().__init__(
|
|
141
|
+
system_prompt=code_system_prompt,
|
|
142
|
+
name="CodeAgent",
|
|
143
|
+
auto_complete=False,
|
|
144
|
+
model_group=model_group,
|
|
145
|
+
need_summary=need_summary,
|
|
146
|
+
use_methodology=use_methodology,
|
|
147
|
+
use_analysis=use_analysis,
|
|
148
|
+
non_interactive=non_interactive,
|
|
149
|
+
plan=bool(plan) if plan is not None else is_plan_enabled(),
|
|
150
|
+
use_tools=base_tools, # 仅启用限定工具
|
|
151
|
+
**kwargs,
|
|
60
152
|
)
|
|
61
|
-
|
|
153
|
+
|
|
154
|
+
# 建立CodeAgent与Agent的关联,便于工具获取上下文管理器
|
|
155
|
+
self._code_agent = self
|
|
156
|
+
|
|
157
|
+
# 初始化上下文推荐器(自己创建LLM模型,使用父Agent的配置)
|
|
158
|
+
try:
|
|
159
|
+
# 获取当前Agent的model实例
|
|
160
|
+
parent_model = None
|
|
161
|
+
if hasattr(self, 'model') and self.model:
|
|
162
|
+
parent_model = self.model
|
|
163
|
+
|
|
164
|
+
self.context_recommender = ContextRecommender(
|
|
165
|
+
self.context_manager,
|
|
166
|
+
parent_model=parent_model
|
|
167
|
+
)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
# LLM推荐器初始化失败
|
|
170
|
+
import logging
|
|
171
|
+
logger = logging.getLogger(__name__)
|
|
172
|
+
logger.warning(f"上下文推荐器初始化失败: {e},将跳过上下文推荐功能")
|
|
173
|
+
|
|
174
|
+
self.event_bus.subscribe(AFTER_TOOL_CALL, self._on_after_tool_call)
|
|
175
|
+
|
|
176
|
+
def _get_system_prompt(self) -> str:
|
|
177
|
+
"""获取代码工程师的系统提示词"""
|
|
178
|
+
return """
|
|
62
179
|
<code_engineer_guide>
|
|
63
180
|
## 角色定位
|
|
64
181
|
你是Jarvis系统的代码工程师,一个专业的代码分析和修改助手。你的职责是:
|
|
@@ -78,53 +195,132 @@ class CodeAgent:
|
|
|
78
195
|
1. **项目分析**:分析项目结构,确定需修改的文件
|
|
79
196
|
2. **需求分析**:理解需求意图,选择影响最小的实现方案
|
|
80
197
|
3. **代码分析**:详细分析目标文件,禁止虚构现有代码
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
198
|
+
- 结构分析:优先使用文件搜索工具快速定位文件和目录结构
|
|
199
|
+
- 内容搜索:优先使用全文搜索工具进行函数、类、变量等内容的搜索,避免遗漏
|
|
200
|
+
- 依赖关系:如需分析依赖、调用关系,可结合代码分析工具辅助
|
|
84
201
|
- 代码阅读:使用 read_code 工具获取目标文件的完整内容或指定范围内容,禁止凭空假设代码
|
|
85
|
-
-
|
|
202
|
+
- 变更影响:如需分析变更影响范围,可结合版本控制工具辅助判断
|
|
203
|
+
- 上下文理解:系统已维护项目的符号表和依赖关系图,可以帮助理解代码结构和依赖关系
|
|
86
204
|
- 工具优先级:优先使用自动化工具,减少人工推断,确保分析结果准确
|
|
87
205
|
4. **方案设计**:确定最小变更方案,保持代码结构
|
|
88
206
|
5. **实施修改**:遵循"先读后写"原则,保持代码风格一致性
|
|
89
207
|
|
|
90
208
|
## 工具使用
|
|
91
|
-
-
|
|
92
|
-
-
|
|
209
|
+
- 项目结构:优先使用文件搜索命令查找文件
|
|
210
|
+
- 代码搜索:优先使用内容搜索工具
|
|
93
211
|
- 代码阅读:优先使用read_code工具
|
|
94
212
|
- 仅在命令行工具不足时使用专用工具
|
|
95
213
|
|
|
96
214
|
## 文件编辑工具使用规范
|
|
97
|
-
- 对于部分文件内容修改,使用
|
|
98
|
-
- 对于需要重写整个文件内容,使用
|
|
215
|
+
- 对于部分文件内容修改,使用edit_file工具
|
|
216
|
+
- 对于需要重写整个文件内容,使用 REWRITE 操作
|
|
99
217
|
- 对于简单的修改,可以使用execute_script工具执行shell命令完成
|
|
218
|
+
|
|
219
|
+
## 子任务与子CodeAgent
|
|
220
|
+
- 当出现以下情况时,优先使用 sub_code_agent 工具将子任务托管给子 CodeAgent(自动完成并生成总结):
|
|
221
|
+
- 需要在当前任务下并行推进较大且相对独立的代码改造
|
|
222
|
+
- 涉及多文件/多模块的大范围变更,或需要较长的工具调用链
|
|
223
|
+
- 需要隔离上下文以避免污染当前对话(如探索性改动、PoC)
|
|
224
|
+
- 需要专注于单一子问题,阶段性产出可独立复用的结果
|
|
225
|
+
- 其余常规、小粒度改动直接在当前 Agent 中完成即可
|
|
100
226
|
</code_engineer_guide>
|
|
101
227
|
|
|
102
228
|
<say_to_llm>
|
|
103
|
-
1.
|
|
104
|
-
2.
|
|
105
|
-
3.
|
|
106
|
-
4.
|
|
107
|
-
5.
|
|
108
|
-
6.
|
|
109
|
-
7.
|
|
110
|
-
8.
|
|
111
|
-
9.
|
|
112
|
-
10.
|
|
229
|
+
1. 保持专注与耐心,先分析再行动;将复杂问题拆解为可执行的小步骤
|
|
230
|
+
2. 以结果为导向,同时简明呈现关键推理依据,避免无关噪音
|
|
231
|
+
3. 信息不足时,主动提出最少且关键的问题以澄清需求
|
|
232
|
+
4. 输出前自检:一致性、边界条件、依赖关系、回滚与风险提示
|
|
233
|
+
5. 选择对现有系统影响最小且可回退的方案,确保稳定性与可维护性
|
|
234
|
+
6. 保持项目风格:结构、命名、工具使用与现有规范一致
|
|
235
|
+
7. 工具优先:使用搜索、read_code、版本控制与静态分析验证结论,拒绝臆测
|
|
236
|
+
8. 面对错误与不确定,给出修复计划与备选路径,持续迭代优于停滞
|
|
237
|
+
9. 沟通清晰:用要点列出结论、变更范围、影响评估与下一步行动
|
|
238
|
+
10. 持续改进:沉淀经验为可复用清单,下一次做得更快更稳
|
|
113
239
|
</say_to_llm>
|
|
114
240
|
"""
|
|
115
|
-
self.agent = Agent(
|
|
116
|
-
system_prompt=code_system_prompt,
|
|
117
|
-
name="CodeAgent",
|
|
118
|
-
auto_complete=False,
|
|
119
|
-
output_handler=[tool_registry, EditFileHandler()],
|
|
120
|
-
llm_type=llm_type,
|
|
121
|
-
input_handler=[shell_input_handler, builtin_input_handler],
|
|
122
|
-
need_summary=need_summary,
|
|
123
|
-
use_methodology=False, # 禁用方法论
|
|
124
|
-
use_analysis=False, # 禁用分析
|
|
125
|
-
)
|
|
126
241
|
|
|
127
|
-
|
|
242
|
+
def _read_project_rules(self) -> Optional[str]:
|
|
243
|
+
"""读取 .jarvis/rules 内容,如果存在则返回字符串,否则返回 None"""
|
|
244
|
+
try:
|
|
245
|
+
rules_path = os.path.join(self.root_dir, ".jarvis", "rules")
|
|
246
|
+
if os.path.exists(rules_path) and os.path.isfile(rules_path):
|
|
247
|
+
with open(rules_path, "r", encoding="utf-8", errors="replace") as f:
|
|
248
|
+
content = f.read().strip()
|
|
249
|
+
return content if content else None
|
|
250
|
+
except Exception:
|
|
251
|
+
# 读取规则失败时忽略,不影响主流程
|
|
252
|
+
pass
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
def _read_global_rules(self) -> Optional[str]:
|
|
256
|
+
"""读取数据目录 rules 内容,如果存在则返回字符串,否则返回 None"""
|
|
257
|
+
try:
|
|
258
|
+
rules_path = os.path.join(get_data_dir(), "rules")
|
|
259
|
+
if os.path.exists(rules_path) and os.path.isfile(rules_path):
|
|
260
|
+
with open(rules_path, "r", encoding="utf-8", errors="replace") as f:
|
|
261
|
+
content = f.read().strip()
|
|
262
|
+
return content if content else None
|
|
263
|
+
except Exception:
|
|
264
|
+
# 读取规则失败时忽略,不影响主流程
|
|
265
|
+
pass
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
def _check_git_config(self) -> None:
|
|
269
|
+
"""检查 git username 和 email 是否已设置,如果没有则提示并退出"""
|
|
270
|
+
try:
|
|
271
|
+
# 检查 git user.name
|
|
272
|
+
result = subprocess.run(
|
|
273
|
+
["git", "config", "--get", "user.name"],
|
|
274
|
+
capture_output=True,
|
|
275
|
+
text=True,
|
|
276
|
+
check=False,
|
|
277
|
+
)
|
|
278
|
+
username = result.stdout.strip()
|
|
279
|
+
|
|
280
|
+
# 检查 git user.email
|
|
281
|
+
result = subprocess.run(
|
|
282
|
+
["git", "config", "--get", "user.email"],
|
|
283
|
+
capture_output=True,
|
|
284
|
+
text=True,
|
|
285
|
+
check=False,
|
|
286
|
+
)
|
|
287
|
+
email = result.stdout.strip()
|
|
288
|
+
|
|
289
|
+
# 如果任一配置未设置,提示并退出
|
|
290
|
+
if not username or not email:
|
|
291
|
+
missing_configs = []
|
|
292
|
+
if not username:
|
|
293
|
+
missing_configs.append(
|
|
294
|
+
' git config --global user.name "Your Name"'
|
|
295
|
+
)
|
|
296
|
+
if not email:
|
|
297
|
+
missing_configs.append(
|
|
298
|
+
' git config --global user.email "your.email@example.com"'
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
message = "❌ Git 配置不完整\n\n请运行以下命令配置 Git:\n" + "\n".join(
|
|
302
|
+
missing_configs
|
|
303
|
+
)
|
|
304
|
+
PrettyOutput.print(message, OutputType.WARNING)
|
|
305
|
+
# 通过配置控制严格校验模式(JARVIS_GIT_CHECK_MODE):
|
|
306
|
+
# - warn: 仅告警并继续,后续提交可能失败
|
|
307
|
+
# - strict: 严格模式(默认),直接退出
|
|
308
|
+
mode = get_git_check_mode().lower()
|
|
309
|
+
if mode == "warn":
|
|
310
|
+
PrettyOutput.print(
|
|
311
|
+
"已启用 Git 校验警告模式(JARVIS_GIT_CHECK_MODE=warn),将继续运行。"
|
|
312
|
+
"注意:后续提交可能失败,请尽快配置 git user.name 与 user.email。",
|
|
313
|
+
OutputType.INFO,
|
|
314
|
+
)
|
|
315
|
+
return
|
|
316
|
+
sys.exit(1)
|
|
317
|
+
|
|
318
|
+
except FileNotFoundError:
|
|
319
|
+
PrettyOutput.print("❌ 未找到 git 命令,请先安装 Git", OutputType.ERROR)
|
|
320
|
+
sys.exit(1)
|
|
321
|
+
except Exception as e:
|
|
322
|
+
PrettyOutput.print(f"❌ 检查 Git 配置时出错: {str(e)}", OutputType.ERROR)
|
|
323
|
+
sys.exit(1)
|
|
128
324
|
|
|
129
325
|
def _find_git_root(self) -> str:
|
|
130
326
|
"""查找并切换到git根目录
|
|
@@ -132,58 +328,308 @@ class CodeAgent:
|
|
|
132
328
|
返回:
|
|
133
329
|
str: git根目录路径
|
|
134
330
|
"""
|
|
135
|
-
|
|
331
|
+
|
|
136
332
|
curr_dir = os.getcwd()
|
|
137
333
|
git_dir = find_git_root_and_cd(curr_dir)
|
|
138
334
|
self.root_dir = git_dir
|
|
139
|
-
|
|
335
|
+
|
|
140
336
|
return git_dir
|
|
141
337
|
|
|
142
338
|
def _update_gitignore(self, git_dir: str) -> None:
|
|
143
|
-
"""检查并更新.gitignore文件,确保忽略.jarvis
|
|
339
|
+
"""检查并更新.gitignore文件,确保忽略.jarvis目录,并追加常用语言的忽略规则(若缺失)
|
|
144
340
|
|
|
145
341
|
参数:
|
|
146
342
|
git_dir: git根目录路径
|
|
147
343
|
"""
|
|
148
|
-
print("📝 正在检查.gitignore文件...")
|
|
149
344
|
gitignore_path = os.path.join(git_dir, ".gitignore")
|
|
150
|
-
|
|
345
|
+
|
|
346
|
+
# 常用忽略规则(按语言/场景分组)
|
|
347
|
+
sections = {
|
|
348
|
+
"General": [
|
|
349
|
+
".jarvis",
|
|
350
|
+
".DS_Store",
|
|
351
|
+
"Thumbs.db",
|
|
352
|
+
"*.log",
|
|
353
|
+
"*.tmp",
|
|
354
|
+
"*.swp",
|
|
355
|
+
"*.swo",
|
|
356
|
+
".idea/",
|
|
357
|
+
".vscode/",
|
|
358
|
+
],
|
|
359
|
+
"Python": [
|
|
360
|
+
"__pycache__/",
|
|
361
|
+
"*.py[cod]",
|
|
362
|
+
"*$py.class",
|
|
363
|
+
".Python",
|
|
364
|
+
"env/",
|
|
365
|
+
"venv/",
|
|
366
|
+
".venv/",
|
|
367
|
+
"build/",
|
|
368
|
+
"dist/",
|
|
369
|
+
"develop-eggs/",
|
|
370
|
+
"downloads/",
|
|
371
|
+
"eggs/",
|
|
372
|
+
".eggs/",
|
|
373
|
+
"lib/",
|
|
374
|
+
"lib64/",
|
|
375
|
+
"parts/",
|
|
376
|
+
"sdist/",
|
|
377
|
+
"var/",
|
|
378
|
+
"wheels/",
|
|
379
|
+
"pip-wheel-metadata/",
|
|
380
|
+
"share/python-wheels/",
|
|
381
|
+
"*.egg-info/",
|
|
382
|
+
".installed.cfg",
|
|
383
|
+
"*.egg",
|
|
384
|
+
"MANIFEST",
|
|
385
|
+
".mypy_cache/",
|
|
386
|
+
".pytest_cache/",
|
|
387
|
+
".ruff_cache/",
|
|
388
|
+
".tox/",
|
|
389
|
+
".coverage",
|
|
390
|
+
".coverage.*",
|
|
391
|
+
"htmlcov/",
|
|
392
|
+
".hypothesis/",
|
|
393
|
+
".ipynb_checkpoints",
|
|
394
|
+
".pyre/",
|
|
395
|
+
".pytype/",
|
|
396
|
+
],
|
|
397
|
+
"Rust": [
|
|
398
|
+
"target/",
|
|
399
|
+
],
|
|
400
|
+
"Node": [
|
|
401
|
+
"node_modules/",
|
|
402
|
+
"npm-debug.log*",
|
|
403
|
+
"yarn-debug.log*",
|
|
404
|
+
"yarn-error.log*",
|
|
405
|
+
"pnpm-debug.log*",
|
|
406
|
+
"lerna-debug.log*",
|
|
407
|
+
"dist/",
|
|
408
|
+
"coverage/",
|
|
409
|
+
".turbo/",
|
|
410
|
+
".next/",
|
|
411
|
+
".nuxt/",
|
|
412
|
+
"out/",
|
|
413
|
+
],
|
|
414
|
+
"Go": [
|
|
415
|
+
"bin/",
|
|
416
|
+
"vendor/",
|
|
417
|
+
"coverage.out",
|
|
418
|
+
],
|
|
419
|
+
"Java": [
|
|
420
|
+
"target/",
|
|
421
|
+
"*.class",
|
|
422
|
+
".gradle/",
|
|
423
|
+
"build/",
|
|
424
|
+
"out/",
|
|
425
|
+
],
|
|
426
|
+
"C/C++": [
|
|
427
|
+
"build/",
|
|
428
|
+
"cmake-build-*/",
|
|
429
|
+
"*.o",
|
|
430
|
+
"*.a",
|
|
431
|
+
"*.so",
|
|
432
|
+
"*.obj",
|
|
433
|
+
"*.dll",
|
|
434
|
+
"*.dylib",
|
|
435
|
+
"*.exe",
|
|
436
|
+
"*.pdb",
|
|
437
|
+
],
|
|
438
|
+
".NET": [
|
|
439
|
+
"bin/",
|
|
440
|
+
"obj/",
|
|
441
|
+
],
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
existing_content = ""
|
|
445
|
+
if os.path.exists(gitignore_path):
|
|
446
|
+
with open(gitignore_path, "r", encoding="utf-8", errors="replace") as f:
|
|
447
|
+
existing_content = f.read()
|
|
448
|
+
|
|
449
|
+
# 已存在的忽略项(去除注释与空行)
|
|
450
|
+
existing_set = set(
|
|
451
|
+
ln.strip()
|
|
452
|
+
for ln in existing_content.splitlines()
|
|
453
|
+
if ln.strip() and not ln.strip().startswith("#")
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# 计算缺失项并准备追加内容
|
|
457
|
+
new_lines: List[str] = []
|
|
458
|
+
for name, patterns in sections.items():
|
|
459
|
+
missing = [p for p in patterns if p not in existing_set]
|
|
460
|
+
if missing:
|
|
461
|
+
new_lines.append(f"# {name}")
|
|
462
|
+
new_lines.extend(missing)
|
|
463
|
+
new_lines.append("") # 分组空行
|
|
151
464
|
|
|
152
465
|
if not os.path.exists(gitignore_path):
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
466
|
+
# 新建 .gitignore(仅包含缺失项;此处即为全部常用规则)
|
|
467
|
+
with open(gitignore_path, "w", encoding="utf-8", newline="\n") as f:
|
|
468
|
+
content_to_write = "\n".join(new_lines).rstrip()
|
|
469
|
+
if content_to_write:
|
|
470
|
+
f.write(content_to_write + "\n")
|
|
471
|
+
PrettyOutput.print("已创建 .gitignore 并添加常用忽略规则", OutputType.SUCCESS)
|
|
156
472
|
else:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
473
|
+
if new_lines:
|
|
474
|
+
# 追加缺失的规则
|
|
475
|
+
with open(gitignore_path, "a", encoding="utf-8", newline="\n") as f:
|
|
476
|
+
# 若原文件不以换行结尾,先补一行
|
|
477
|
+
if existing_content and not existing_content.endswith("\n"):
|
|
478
|
+
f.write("\n")
|
|
479
|
+
f.write("\n".join(new_lines).rstrip() + "\n")
|
|
480
|
+
PrettyOutput.print("已更新 .gitignore,追加常用忽略规则", OutputType.SUCCESS)
|
|
164
481
|
|
|
165
|
-
def _handle_git_changes(self) -> None:
|
|
482
|
+
def _handle_git_changes(self, prefix: str, suffix: str) -> None:
|
|
166
483
|
"""处理git仓库中的未提交修改"""
|
|
167
|
-
|
|
484
|
+
|
|
168
485
|
if has_uncommitted_changes():
|
|
169
|
-
|
|
486
|
+
|
|
170
487
|
git_commiter = GitCommitTool()
|
|
171
|
-
git_commiter.execute({})
|
|
172
|
-
print("✅ 未提交修改已处理完成")
|
|
173
|
-
else:
|
|
174
|
-
print("✅ 没有未提交的修改")
|
|
488
|
+
git_commiter.execute({"prefix": prefix, "suffix": suffix, "agent": self, "model_group": getattr(self.model, "model_group", None)})
|
|
175
489
|
|
|
176
|
-
def _init_env(self) -> None:
|
|
490
|
+
def _init_env(self, prefix: str, suffix: str) -> None:
|
|
177
491
|
"""初始化环境,组合以下功能:
|
|
178
492
|
1. 查找git根目录
|
|
179
493
|
2. 检查并更新.gitignore文件
|
|
180
494
|
3. 处理未提交的修改
|
|
495
|
+
4. 配置git对换行符变化不敏感
|
|
181
496
|
"""
|
|
182
|
-
|
|
497
|
+
|
|
183
498
|
git_dir = self._find_git_root()
|
|
184
499
|
self._update_gitignore(git_dir)
|
|
185
|
-
self._handle_git_changes()
|
|
186
|
-
|
|
500
|
+
self._handle_git_changes(prefix, suffix)
|
|
501
|
+
# 配置git对换行符变化不敏感
|
|
502
|
+
self._configure_line_ending_settings()
|
|
503
|
+
|
|
504
|
+
def _configure_line_ending_settings(self) -> None:
|
|
505
|
+
"""配置git对换行符变化不敏感,只在当前设置与目标设置不一致时修改"""
|
|
506
|
+
target_settings = {
|
|
507
|
+
"core.autocrlf": "false",
|
|
508
|
+
"core.safecrlf": "false",
|
|
509
|
+
"core.whitespace": "cr-at-eol", # 忽略行尾的CR
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
# 获取当前设置并检查是否需要修改
|
|
513
|
+
need_change = False
|
|
514
|
+
current_settings = {}
|
|
515
|
+
for key, target_value in target_settings.items():
|
|
516
|
+
result = subprocess.run(
|
|
517
|
+
["git", "config", "--get", key],
|
|
518
|
+
capture_output=True,
|
|
519
|
+
text=True,
|
|
520
|
+
check=False,
|
|
521
|
+
)
|
|
522
|
+
current_value = result.stdout.strip()
|
|
523
|
+
current_settings[key] = current_value
|
|
524
|
+
if current_value != target_value:
|
|
525
|
+
need_change = True
|
|
526
|
+
|
|
527
|
+
if not need_change:
|
|
528
|
+
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
PrettyOutput.print(
|
|
532
|
+
"⚠️ 正在修改git换行符敏感设置,这会影响所有文件的换行符处理方式",
|
|
533
|
+
OutputType.WARNING,
|
|
534
|
+
)
|
|
535
|
+
# 避免在循环中逐条打印,先拼接后统一打印
|
|
536
|
+
lines = ["将进行以下设置:"]
|
|
537
|
+
for key, value in target_settings.items():
|
|
538
|
+
current = current_settings.get(key, "未设置")
|
|
539
|
+
lines.append(f"{key}: {current} -> {value}")
|
|
540
|
+
PrettyOutput.print("\n".join(lines), OutputType.INFO)
|
|
541
|
+
|
|
542
|
+
# 直接执行设置,不需要用户确认
|
|
543
|
+
for key, value in target_settings.items():
|
|
544
|
+
subprocess.run(["git", "config", key, value], check=True)
|
|
545
|
+
|
|
546
|
+
# 对于Windows系统,提示用户可以创建.gitattributes文件
|
|
547
|
+
if sys.platform.startswith("win"):
|
|
548
|
+
self._handle_windows_line_endings()
|
|
549
|
+
|
|
550
|
+
PrettyOutput.print("git换行符敏感设置已更新", OutputType.SUCCESS)
|
|
551
|
+
|
|
552
|
+
def _handle_windows_line_endings(self) -> None:
|
|
553
|
+
"""在Windows系统上处理换行符问题,提供建议而非强制修改"""
|
|
554
|
+
gitattributes_path = os.path.join(self.root_dir, ".gitattributes")
|
|
555
|
+
|
|
556
|
+
# 检查是否已存在.gitattributes文件
|
|
557
|
+
if os.path.exists(gitattributes_path):
|
|
558
|
+
with open(gitattributes_path, "r", encoding="utf-8") as f:
|
|
559
|
+
content = f.read()
|
|
560
|
+
# 如果已经有换行符相关配置,就不再提示
|
|
561
|
+
if any(keyword in content for keyword in ["text=", "eol=", "binary"]):
|
|
562
|
+
return
|
|
563
|
+
|
|
564
|
+
PrettyOutput.print(
|
|
565
|
+
"提示:在Windows系统上,建议配置 .gitattributes 文件来避免换行符问题。",
|
|
566
|
+
OutputType.INFO,
|
|
567
|
+
)
|
|
568
|
+
PrettyOutput.print(
|
|
569
|
+
"这可以防止仅因换行符不同而导致整个文件被标记为修改。", OutputType.INFO
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
if user_confirm("是否要创建一个最小化的.gitattributes文件?", False):
|
|
573
|
+
# 最小化的内容,只影响特定类型的文件
|
|
574
|
+
minimal_content = """# Jarvis建议的最小化换行符配置
|
|
575
|
+
# 默认所有文本文件使用LF,只有Windows特定文件使用CRLF
|
|
576
|
+
|
|
577
|
+
# 默认所有文本文件使用LF
|
|
578
|
+
* text=auto eol=lf
|
|
579
|
+
|
|
580
|
+
# Windows批处理文件需要CRLF
|
|
581
|
+
*.bat text eol=crlf
|
|
582
|
+
*.cmd text eol=crlf
|
|
583
|
+
*.ps1 text eol=crlf
|
|
584
|
+
"""
|
|
585
|
+
|
|
586
|
+
if not os.path.exists(gitattributes_path):
|
|
587
|
+
with open(gitattributes_path, "w", encoding="utf-8", newline="\n") as f:
|
|
588
|
+
f.write(minimal_content)
|
|
589
|
+
PrettyOutput.print(
|
|
590
|
+
"已创建最小化的 .gitattributes 文件", OutputType.SUCCESS
|
|
591
|
+
)
|
|
592
|
+
else:
|
|
593
|
+
PrettyOutput.print(
|
|
594
|
+
"将以下内容追加到现有 .gitattributes 文件:", OutputType.INFO
|
|
595
|
+
)
|
|
596
|
+
PrettyOutput.print(minimal_content, OutputType.CODE, lang="text")
|
|
597
|
+
if user_confirm("是否追加到现有文件?", True):
|
|
598
|
+
with open(
|
|
599
|
+
gitattributes_path, "a", encoding="utf-8", newline="\n"
|
|
600
|
+
) as f:
|
|
601
|
+
f.write("\n" + minimal_content)
|
|
602
|
+
PrettyOutput.print("已更新 .gitattributes 文件", OutputType.SUCCESS)
|
|
603
|
+
else:
|
|
604
|
+
PrettyOutput.print(
|
|
605
|
+
"跳过 .gitattributes 文件创建。如遇换行符问题,可手动创建此文件。",
|
|
606
|
+
OutputType.INFO,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
def _record_code_changes_stats(self, diff_text: str) -> None:
|
|
610
|
+
"""记录代码变更的统计信息。
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
diff_text: git diff的文本输出
|
|
614
|
+
"""
|
|
615
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
|
616
|
+
import re
|
|
617
|
+
|
|
618
|
+
# 匹配插入行数
|
|
619
|
+
insertions_match = re.search(r"(\d+)\s+insertions?\(\+\)", diff_text)
|
|
620
|
+
if insertions_match:
|
|
621
|
+
insertions = int(insertions_match.group(1))
|
|
622
|
+
StatsManager.increment(
|
|
623
|
+
"code_lines_inserted", amount=insertions, group="code_agent"
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
# 匹配删除行数
|
|
627
|
+
deletions_match = re.search(r"(\d+)\s+deletions?\(\-\)", diff_text)
|
|
628
|
+
if deletions_match:
|
|
629
|
+
deletions = int(deletions_match.group(1))
|
|
630
|
+
StatsManager.increment(
|
|
631
|
+
"code_lines_deleted", amount=deletions, group="code_agent"
|
|
632
|
+
)
|
|
187
633
|
|
|
188
634
|
def _handle_uncommitted_changes(self) -> None:
|
|
189
635
|
"""处理未提交的修改,包括:
|
|
@@ -194,6 +640,21 @@ class CodeAgent:
|
|
|
194
640
|
5. 暂存并提交所有修改
|
|
195
641
|
"""
|
|
196
642
|
if has_uncommitted_changes():
|
|
643
|
+
# 获取代码变更统计
|
|
644
|
+
try:
|
|
645
|
+
diff_result = subprocess.run(
|
|
646
|
+
["git", "diff", "HEAD", "--shortstat"],
|
|
647
|
+
capture_output=True,
|
|
648
|
+
text=True,
|
|
649
|
+
encoding="utf-8",
|
|
650
|
+
errors="replace",
|
|
651
|
+
check=True,
|
|
652
|
+
)
|
|
653
|
+
if diff_result.returncode == 0 and diff_result.stdout:
|
|
654
|
+
self._record_code_changes_stats(diff_result.stdout)
|
|
655
|
+
except subprocess.CalledProcessError:
|
|
656
|
+
pass
|
|
657
|
+
|
|
197
658
|
PrettyOutput.print("检测到未提交的修改,是否要提交?", OutputType.WARNING)
|
|
198
659
|
if not user_confirm("是否要提交?", True):
|
|
199
660
|
return
|
|
@@ -205,16 +666,23 @@ class CodeAgent:
|
|
|
205
666
|
return
|
|
206
667
|
|
|
207
668
|
# 获取当前分支的提交总数
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
669
|
+
# 兼容空仓库或无 HEAD 的场景:失败时将提交计数视为 0,继续执行提交流程
|
|
670
|
+
commit_count = 0
|
|
671
|
+
try:
|
|
672
|
+
commit_result = subprocess.run(
|
|
673
|
+
["git", "rev-list", "--count", "HEAD"],
|
|
674
|
+
capture_output=True,
|
|
675
|
+
text=True,
|
|
676
|
+
encoding="utf-8",
|
|
677
|
+
errors="replace",
|
|
678
|
+
check=False,
|
|
679
|
+
)
|
|
680
|
+
if commit_result.returncode == 0:
|
|
681
|
+
out = commit_result.stdout.strip()
|
|
682
|
+
if out.isdigit():
|
|
683
|
+
commit_count = int(out)
|
|
684
|
+
except Exception:
|
|
685
|
+
commit_count = 0
|
|
218
686
|
|
|
219
687
|
# 暂存所有修改
|
|
220
688
|
subprocess.run(["git", "add", "."], check=True)
|
|
@@ -245,6 +713,11 @@ class CodeAgent:
|
|
|
245
713
|
commits = []
|
|
246
714
|
|
|
247
715
|
if commits:
|
|
716
|
+
# 统计生成的commit数量
|
|
717
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
|
718
|
+
|
|
719
|
+
StatsManager.increment("commits_generated", group="code_agent")
|
|
720
|
+
|
|
248
721
|
commit_messages = "检测到以下提交记录:\n" + "\n".join(
|
|
249
722
|
f"- {commit_hash[:7]}: {message}" for commit_hash, message in commits
|
|
250
723
|
)
|
|
@@ -252,10 +725,19 @@ class CodeAgent:
|
|
|
252
725
|
return commits
|
|
253
726
|
|
|
254
727
|
def _handle_commit_confirmation(
|
|
255
|
-
self,
|
|
728
|
+
self,
|
|
729
|
+
commits: List[Tuple[str, str]],
|
|
730
|
+
start_commit: Optional[str],
|
|
731
|
+
prefix: str,
|
|
732
|
+
suffix: str,
|
|
256
733
|
) -> None:
|
|
257
734
|
"""处理提交确认和可能的重置"""
|
|
258
735
|
if commits and user_confirm("是否接受以上提交记录?", True):
|
|
736
|
+
# 统计接受的commit数量
|
|
737
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
|
738
|
+
|
|
739
|
+
StatsManager.increment("commits_accepted", group="code_agent")
|
|
740
|
+
|
|
259
741
|
subprocess.run(
|
|
260
742
|
["git", "reset", "--mixed", str(start_commit)],
|
|
261
743
|
stdout=subprocess.DEVNULL,
|
|
@@ -263,12 +745,17 @@ class CodeAgent:
|
|
|
263
745
|
check=True,
|
|
264
746
|
)
|
|
265
747
|
git_commiter = GitCommitTool()
|
|
266
|
-
git_commiter.execute({})
|
|
748
|
+
git_commiter.execute({"prefix": prefix, "suffix": suffix, "agent": self, "model_group": getattr(self.model, "model_group", None)})
|
|
749
|
+
|
|
750
|
+
# 在用户接受commit后,根据配置决定是否保存记忆
|
|
751
|
+
if self.force_save_memory:
|
|
752
|
+
self.memory_manager.prompt_memory_save()
|
|
267
753
|
elif start_commit:
|
|
268
|
-
|
|
269
|
-
|
|
754
|
+
if user_confirm("是否要重置到初始提交?", True):
|
|
755
|
+
os.system(f"git reset --hard {str(start_commit)}") # 确保转换为字符串
|
|
756
|
+
PrettyOutput.print("已重置到初始提交", OutputType.INFO)
|
|
270
757
|
|
|
271
|
-
def run(self, user_input: str) -> Optional[str]:
|
|
758
|
+
def run(self, user_input: str, prefix: str = "", suffix: str = "") -> Optional[str]:
|
|
272
759
|
"""使用给定的用户输入运行代码代理。
|
|
273
760
|
|
|
274
761
|
参数:
|
|
@@ -277,159 +764,1092 @@ class CodeAgent:
|
|
|
277
764
|
返回:
|
|
278
765
|
str: 描述执行结果的输出,成功时返回None
|
|
279
766
|
"""
|
|
767
|
+
prev_dir = os.getcwd()
|
|
280
768
|
try:
|
|
281
|
-
self._init_env()
|
|
769
|
+
self._init_env(prefix, suffix)
|
|
282
770
|
start_commit = get_latest_commit_hash()
|
|
283
771
|
|
|
284
|
-
#
|
|
285
|
-
|
|
286
|
-
commits_info = get_recent_commits_with_files()
|
|
287
|
-
|
|
288
|
-
project_info = []
|
|
289
|
-
if loc_stats:
|
|
290
|
-
project_info.append(f"代码统计:\n{loc_stats}")
|
|
291
|
-
if commits_info:
|
|
292
|
-
commits_str = "\n".join(
|
|
293
|
-
f"提交 {i+1}: {commit['hash'][:7]} - {commit['message']} ({len(commit['files'])}个文件)\n"
|
|
294
|
-
+ "\n".join(f" - {file}" for file in commit["files"][:5])
|
|
295
|
-
+ ("\n ..." if len(commit["files"]) > 5 else "")
|
|
296
|
-
for i, commit in enumerate(commits_info)
|
|
297
|
-
)
|
|
298
|
-
project_info.append(f"最近提交:\n{commits_str}")
|
|
772
|
+
# 获取项目概况信息
|
|
773
|
+
project_overview = get_project_overview(self.root_dir)
|
|
299
774
|
|
|
300
775
|
first_tip = """请严格遵循以下规范进行代码修改任务:
|
|
301
776
|
1. 每次响应仅执行一步操作,先分析再修改,避免一步多改。
|
|
302
777
|
2. 充分利用工具理解用户需求和现有代码,禁止凭空假设。
|
|
303
778
|
3. 如果不清楚要修改的文件,必须先分析并找出需要修改的文件,明确目标后再进行编辑。
|
|
304
|
-
4. 代码编辑任务优先使用
|
|
305
|
-
5. 如需大范围重写,才可使用
|
|
779
|
+
4. 代码编辑任务优先使用 PATCH 操作,确保搜索文本在目标文件中有且仅有一次精确匹配,保证修改的准确性和安全性。
|
|
780
|
+
5. 如需大范围重写,才可使用 REWRITE 操作。
|
|
306
781
|
6. 如遇信息不明,优先调用工具补充分析,不要主观臆断。
|
|
307
782
|
"""
|
|
308
783
|
|
|
309
|
-
|
|
784
|
+
# 智能上下文推荐:根据用户输入推荐相关上下文
|
|
785
|
+
context_recommendation_text = ""
|
|
786
|
+
if self.context_recommender and is_enable_intent_recognition():
|
|
787
|
+
# 在意图识别和上下文推荐期间抑制模型输出
|
|
788
|
+
was_suppressed = False
|
|
789
|
+
if self.model:
|
|
790
|
+
was_suppressed = getattr(self.model, '_suppress_output', False)
|
|
791
|
+
self.model.set_suppress_output(True)
|
|
792
|
+
try:
|
|
793
|
+
PrettyOutput.print("🔍 正在进行智能上下文推荐....", OutputType.INFO)
|
|
794
|
+
|
|
795
|
+
# 生成上下文推荐(基于关键词和项目上下文)
|
|
796
|
+
recommendation = self.context_recommender.recommend_context(
|
|
797
|
+
user_input=user_input,
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
# 格式化推荐结果
|
|
801
|
+
context_recommendation_text = self.context_recommender.format_recommendation(recommendation)
|
|
802
|
+
|
|
803
|
+
# 打印推荐的上下文
|
|
804
|
+
if context_recommendation_text:
|
|
805
|
+
PrettyOutput.print(context_recommendation_text, OutputType.INFO)
|
|
806
|
+
except Exception as e:
|
|
807
|
+
# 上下文推荐失败不应该影响主流程
|
|
808
|
+
import logging
|
|
809
|
+
logger = logging.getLogger(__name__)
|
|
810
|
+
logger.debug(f"上下文推荐失败: {e}", exc_info=True)
|
|
811
|
+
finally:
|
|
812
|
+
# 恢复模型输出设置
|
|
813
|
+
if self.model:
|
|
814
|
+
self.model.set_suppress_output(was_suppressed)
|
|
815
|
+
|
|
816
|
+
if project_overview:
|
|
310
817
|
enhanced_input = (
|
|
311
|
-
|
|
312
|
-
+ "\n\n".join(project_info)
|
|
818
|
+
project_overview
|
|
313
819
|
+ "\n\n"
|
|
314
820
|
+ first_tip
|
|
821
|
+
+ context_recommendation_text
|
|
315
822
|
+ "\n\n任务描述:\n"
|
|
316
823
|
+ user_input
|
|
317
824
|
)
|
|
318
825
|
else:
|
|
319
|
-
enhanced_input = first_tip + "\n\n任务描述:\n" + user_input
|
|
826
|
+
enhanced_input = first_tip + context_recommendation_text + "\n\n任务描述:\n" + user_input
|
|
320
827
|
|
|
321
828
|
try:
|
|
322
|
-
self.
|
|
829
|
+
if self.model:
|
|
830
|
+
self.model.set_suppress_output(False)
|
|
831
|
+
super().run(enhanced_input)
|
|
323
832
|
except RuntimeError as e:
|
|
324
833
|
PrettyOutput.print(f"执行失败: {str(e)}", OutputType.WARNING)
|
|
325
834
|
return str(e)
|
|
326
835
|
|
|
836
|
+
|
|
837
|
+
|
|
327
838
|
self._handle_uncommitted_changes()
|
|
328
839
|
end_commit = get_latest_commit_hash()
|
|
329
840
|
commits = self._show_commit_history(start_commit, end_commit)
|
|
330
|
-
self._handle_commit_confirmation(commits, start_commit)
|
|
841
|
+
self._handle_commit_confirmation(commits, start_commit, prefix, suffix)
|
|
331
842
|
return None
|
|
332
843
|
|
|
333
844
|
except RuntimeError as e:
|
|
334
845
|
return f"Error during execution: {str(e)}"
|
|
846
|
+
finally:
|
|
847
|
+
# Ensure switching back to the original working directory after CodeAgent completes
|
|
848
|
+
try:
|
|
849
|
+
os.chdir(prev_dir)
|
|
850
|
+
except Exception:
|
|
851
|
+
pass
|
|
335
852
|
|
|
336
|
-
def
|
|
853
|
+
def _build_name_status_map(self) -> dict:
|
|
854
|
+
"""构造按文件的状态映射与差异文本,删除文件不展示diff,仅提示删除"""
|
|
855
|
+
status_map = {}
|
|
856
|
+
try:
|
|
857
|
+
head_exists = bool(get_latest_commit_hash())
|
|
858
|
+
# 临时 -N 以包含未跟踪文件的差异检测
|
|
859
|
+
subprocess.run(["git", "add", "-N", "."], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
860
|
+
cmd = ["git", "diff", "--name-status"] + (["HEAD"] if head_exists else [])
|
|
861
|
+
res = subprocess.run(
|
|
862
|
+
cmd,
|
|
863
|
+
capture_output=True,
|
|
864
|
+
text=True,
|
|
865
|
+
encoding="utf-8",
|
|
866
|
+
errors="replace",
|
|
867
|
+
check=False,
|
|
868
|
+
)
|
|
869
|
+
finally:
|
|
870
|
+
subprocess.run(["git", "reset"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
871
|
+
|
|
872
|
+
if res.returncode == 0 and res.stdout:
|
|
873
|
+
for line in res.stdout.splitlines():
|
|
874
|
+
if not line.strip():
|
|
875
|
+
continue
|
|
876
|
+
parts = line.split("\t")
|
|
877
|
+
if not parts:
|
|
878
|
+
continue
|
|
879
|
+
status = parts[0]
|
|
880
|
+
if status.startswith("R") or status.startswith("C"):
|
|
881
|
+
# 重命名/复制:使用新路径作为键
|
|
882
|
+
if len(parts) >= 3:
|
|
883
|
+
old_path, new_path = parts[1], parts[2]
|
|
884
|
+
status_map[new_path] = status
|
|
885
|
+
# 也记录旧路径,便于匹配 name-only 的结果
|
|
886
|
+
status_map[old_path] = status
|
|
887
|
+
elif len(parts) >= 2:
|
|
888
|
+
status_map[parts[-1]] = status
|
|
889
|
+
else:
|
|
890
|
+
if len(parts) >= 2:
|
|
891
|
+
status_map[parts[1]] = status
|
|
892
|
+
return status_map
|
|
893
|
+
|
|
894
|
+
def _get_file_diff(self, file_path: str) -> str:
|
|
895
|
+
"""获取单文件的diff,包含新增文件内容;失败时返回空字符串"""
|
|
896
|
+
head_exists = bool(get_latest_commit_hash())
|
|
897
|
+
try:
|
|
898
|
+
# 为了让未跟踪文件也能展示diff,临时 -N 该文件
|
|
899
|
+
subprocess.run(["git", "add", "-N", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
900
|
+
cmd = ["git", "diff"] + (["HEAD"] if head_exists else []) + ["--", file_path]
|
|
901
|
+
res = subprocess.run(
|
|
902
|
+
cmd,
|
|
903
|
+
capture_output=True,
|
|
904
|
+
text=True,
|
|
905
|
+
encoding="utf-8",
|
|
906
|
+
errors="replace",
|
|
907
|
+
check=False,
|
|
908
|
+
)
|
|
909
|
+
if res.returncode == 0:
|
|
910
|
+
return res.stdout or ""
|
|
911
|
+
return ""
|
|
912
|
+
finally:
|
|
913
|
+
subprocess.run(["git", "reset", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
914
|
+
|
|
915
|
+
def _build_per_file_patch_preview(self, modified_files: List[str]) -> str:
|
|
916
|
+
"""构建按文件的补丁预览"""
|
|
917
|
+
status_map = self._build_name_status_map()
|
|
918
|
+
lines: List[str] = []
|
|
919
|
+
|
|
920
|
+
def _get_file_numstat(file_path: str) -> Tuple[int, int]:
|
|
921
|
+
"""获取单文件的新增/删除行数,失败时返回(0,0)"""
|
|
922
|
+
head_exists = bool(get_latest_commit_hash())
|
|
923
|
+
try:
|
|
924
|
+
# 让未跟踪文件也能统计到新增行数
|
|
925
|
+
subprocess.run(["git", "add", "-N", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
926
|
+
cmd = ["git", "diff", "--numstat"] + (["HEAD"] if head_exists else []) + ["--", file_path]
|
|
927
|
+
res = subprocess.run(
|
|
928
|
+
cmd,
|
|
929
|
+
capture_output=True,
|
|
930
|
+
text=True,
|
|
931
|
+
encoding="utf-8",
|
|
932
|
+
errors="replace",
|
|
933
|
+
check=False,
|
|
934
|
+
)
|
|
935
|
+
if res.returncode == 0 and res.stdout:
|
|
936
|
+
for line in res.stdout.splitlines():
|
|
937
|
+
parts = line.strip().split("\t")
|
|
938
|
+
if len(parts) >= 3:
|
|
939
|
+
add_s, del_s = parts[0], parts[1]
|
|
940
|
+
|
|
941
|
+
def to_int(x: str) -> int:
|
|
942
|
+
try:
|
|
943
|
+
return int(x)
|
|
944
|
+
except Exception:
|
|
945
|
+
# 二进制或无法解析时显示为0
|
|
946
|
+
return 0
|
|
947
|
+
|
|
948
|
+
return to_int(add_s), to_int(del_s)
|
|
949
|
+
finally:
|
|
950
|
+
subprocess.run(["git", "reset", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
951
|
+
return (0, 0)
|
|
952
|
+
|
|
953
|
+
for f in modified_files:
|
|
954
|
+
status = status_map.get(f, "")
|
|
955
|
+
adds, dels = _get_file_numstat(f)
|
|
956
|
+
total_changes = adds + dels
|
|
957
|
+
|
|
958
|
+
# 删除文件:不展示diff,仅提示(附带删除行数信息如果可用)
|
|
959
|
+
if (status.startswith("D")) or (not os.path.exists(f)):
|
|
960
|
+
if dels > 0:
|
|
961
|
+
lines.append(f"- {f} 文件被删除(删除{dels}行)")
|
|
962
|
+
else:
|
|
963
|
+
lines.append(f"- {f} 文件被删除")
|
|
964
|
+
continue
|
|
965
|
+
|
|
966
|
+
# 变更过大:仅提示新增/删除行数,避免输出超长diff
|
|
967
|
+
if total_changes > 300:
|
|
968
|
+
lines.append(f"- {f} 新增{adds}行/删除{dels}行(变更过大,预览已省略)")
|
|
969
|
+
continue
|
|
970
|
+
|
|
971
|
+
# 其它情况:展示该文件的diff
|
|
972
|
+
file_diff = self._get_file_diff(f)
|
|
973
|
+
if file_diff.strip():
|
|
974
|
+
lines.append(f"文件: {f}\n```diff\n{file_diff}\n```")
|
|
975
|
+
else:
|
|
976
|
+
# 当无法获取到diff(例如重命名或特殊状态),避免空输出
|
|
977
|
+
lines.append(f"- {f} 变更已记录(无可展示的文本差异)")
|
|
978
|
+
return "\n".join(lines)
|
|
979
|
+
|
|
980
|
+
def _update_context_for_modified_files(self, modified_files: List[str]) -> None:
|
|
981
|
+
"""更新上下文管理器:当文件被修改后,更新符号表和依赖图"""
|
|
982
|
+
if not modified_files:
|
|
983
|
+
return
|
|
984
|
+
PrettyOutput.print("🔄 正在更新代码上下文...", OutputType.INFO)
|
|
985
|
+
for file_path in modified_files:
|
|
986
|
+
if os.path.exists(file_path):
|
|
987
|
+
try:
|
|
988
|
+
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
989
|
+
content = f.read()
|
|
990
|
+
self.context_manager.update_context_for_file(file_path, content)
|
|
991
|
+
except Exception:
|
|
992
|
+
# 如果读取文件失败,跳过更新
|
|
993
|
+
pass
|
|
994
|
+
|
|
995
|
+
def _analyze_edit_impact(self, modified_files: List[str]) -> Optional[Any]:
|
|
996
|
+
"""进行影响范围分析(如果启用)
|
|
997
|
+
|
|
998
|
+
Returns:
|
|
999
|
+
ImpactReport: 影响分析报告,如果未启用或失败则返回None
|
|
1000
|
+
"""
|
|
1001
|
+
if not is_enable_impact_analysis():
|
|
1002
|
+
return None
|
|
1003
|
+
|
|
1004
|
+
PrettyOutput.print("🔍 正在进行变更影响分析...", OutputType.INFO)
|
|
1005
|
+
try:
|
|
1006
|
+
impact_analyzer = ImpactAnalyzer(self.context_manager)
|
|
1007
|
+
all_edits = []
|
|
1008
|
+
for file_path in modified_files:
|
|
1009
|
+
if os.path.exists(file_path):
|
|
1010
|
+
edits = parse_git_diff_to_edits(file_path, self.root_dir)
|
|
1011
|
+
all_edits.extend(edits)
|
|
1012
|
+
|
|
1013
|
+
if not all_edits:
|
|
1014
|
+
return None
|
|
1015
|
+
|
|
1016
|
+
# 按文件分组编辑
|
|
1017
|
+
edits_by_file = {}
|
|
1018
|
+
for edit in all_edits:
|
|
1019
|
+
if edit.file_path not in edits_by_file:
|
|
1020
|
+
edits_by_file[edit.file_path] = []
|
|
1021
|
+
edits_by_file[edit.file_path].append(edit)
|
|
1022
|
+
|
|
1023
|
+
# 对每个文件进行影响分析
|
|
1024
|
+
impact_report = None
|
|
1025
|
+
for file_path, edits in edits_by_file.items():
|
|
1026
|
+
report = impact_analyzer.analyze_edit_impact(file_path, edits)
|
|
1027
|
+
if report:
|
|
1028
|
+
# 合并报告
|
|
1029
|
+
if impact_report is None:
|
|
1030
|
+
impact_report = report
|
|
1031
|
+
else:
|
|
1032
|
+
# 合并多个报告,去重
|
|
1033
|
+
impact_report.affected_files = list(set(impact_report.affected_files + report.affected_files))
|
|
1034
|
+
|
|
1035
|
+
# 合并符号(基于文件路径和名称去重)
|
|
1036
|
+
symbol_map = {}
|
|
1037
|
+
for symbol in impact_report.affected_symbols + report.affected_symbols:
|
|
1038
|
+
key = (symbol.file_path, symbol.name, symbol.line_start)
|
|
1039
|
+
if key not in symbol_map:
|
|
1040
|
+
symbol_map[key] = symbol
|
|
1041
|
+
impact_report.affected_symbols = list(symbol_map.values())
|
|
1042
|
+
|
|
1043
|
+
impact_report.affected_tests = list(set(impact_report.affected_tests + report.affected_tests))
|
|
1044
|
+
|
|
1045
|
+
# 合并接口变更(基于符号名和文件路径去重)
|
|
1046
|
+
interface_map = {}
|
|
1047
|
+
for change in impact_report.interface_changes + report.interface_changes:
|
|
1048
|
+
key = (change.file_path, change.symbol_name, change.change_type)
|
|
1049
|
+
if key not in interface_map:
|
|
1050
|
+
interface_map[key] = change
|
|
1051
|
+
impact_report.interface_changes = list(interface_map.values())
|
|
1052
|
+
|
|
1053
|
+
impact_report.impacts.extend(report.impacts)
|
|
1054
|
+
|
|
1055
|
+
# 合并建议
|
|
1056
|
+
impact_report.recommendations = list(set(impact_report.recommendations + report.recommendations))
|
|
1057
|
+
|
|
1058
|
+
# 使用更高的风险等级
|
|
1059
|
+
if report.risk_level.value == 'high' or impact_report.risk_level.value == 'high':
|
|
1060
|
+
impact_report.risk_level = report.risk_level if report.risk_level.value == 'high' else impact_report.risk_level
|
|
1061
|
+
elif report.risk_level.value == 'medium':
|
|
1062
|
+
impact_report.risk_level = report.risk_level
|
|
1063
|
+
|
|
1064
|
+
return impact_report
|
|
1065
|
+
except Exception as e:
|
|
1066
|
+
# 影响分析失败不应该影响主流程,仅记录日志
|
|
1067
|
+
import logging
|
|
1068
|
+
logger = logging.getLogger(__name__)
|
|
1069
|
+
logger.warning(f"影响范围分析失败: {e}", exc_info=True)
|
|
1070
|
+
return None
|
|
1071
|
+
|
|
1072
|
+
def _handle_impact_report(self, impact_report: Optional[Any], agent: Agent, final_ret: str) -> str:
|
|
1073
|
+
"""处理影响范围分析报告
|
|
1074
|
+
|
|
1075
|
+
Args:
|
|
1076
|
+
impact_report: 影响分析报告
|
|
1077
|
+
agent: Agent实例
|
|
1078
|
+
final_ret: 当前的结果字符串
|
|
1079
|
+
|
|
1080
|
+
Returns:
|
|
1081
|
+
更新后的结果字符串
|
|
1082
|
+
"""
|
|
1083
|
+
if not impact_report:
|
|
1084
|
+
return final_ret
|
|
1085
|
+
|
|
1086
|
+
impact_summary = impact_report.to_string(self.root_dir)
|
|
1087
|
+
final_ret += f"\n\n{impact_summary}\n"
|
|
1088
|
+
|
|
1089
|
+
# 如果是高风险,在提示词中提醒
|
|
1090
|
+
if impact_report.risk_level.value == 'high':
|
|
1091
|
+
agent.set_addon_prompt(
|
|
1092
|
+
f"{agent.get_addon_prompt() or ''}\n\n"
|
|
1093
|
+
f"⚠️ 高风险编辑警告:\n"
|
|
1094
|
+
f"检测到此编辑为高风险操作,请仔细检查以下内容:\n"
|
|
1095
|
+
f"- 受影响文件: {len(impact_report.affected_files)} 个\n"
|
|
1096
|
+
f"- 接口变更: {len(impact_report.interface_changes)} 个\n"
|
|
1097
|
+
f"- 相关测试: {len(impact_report.affected_tests)} 个\n"
|
|
1098
|
+
f"建议运行相关测试并检查所有受影响文件。"
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
return final_ret
|
|
1102
|
+
|
|
1103
|
+
def _handle_build_validation_disabled(self, modified_files: List[str], config: Any, agent: Agent, final_ret: str) -> str:
|
|
1104
|
+
"""处理构建验证已禁用的情况
|
|
1105
|
+
|
|
1106
|
+
Returns:
|
|
1107
|
+
更新后的结果字符串
|
|
1108
|
+
"""
|
|
1109
|
+
reason = config.get_disable_reason()
|
|
1110
|
+
reason_text = f"(原因: {reason})" if reason else ""
|
|
1111
|
+
final_ret += f"\n\nℹ️ 构建验证已禁用{reason_text},仅进行基础静态检查\n"
|
|
1112
|
+
|
|
1113
|
+
# 输出基础静态检查日志
|
|
1114
|
+
file_count = len(modified_files)
|
|
1115
|
+
files_str = ", ".join(os.path.basename(f) for f in modified_files[:3])
|
|
1116
|
+
if file_count > 3:
|
|
1117
|
+
files_str += f" 等{file_count}个文件"
|
|
1118
|
+
PrettyOutput.print(f"🔍 正在进行基础静态检查 ({files_str})...", OutputType.INFO)
|
|
1119
|
+
|
|
1120
|
+
# 使用兜底验证器进行基础静态检查
|
|
1121
|
+
fallback_validator = FallbackBuildValidator(self.root_dir, timeout=get_build_validation_timeout())
|
|
1122
|
+
static_check_result = fallback_validator.validate(modified_files)
|
|
1123
|
+
if not static_check_result.success:
|
|
1124
|
+
final_ret += f"\n⚠️ 基础静态检查失败:\n{static_check_result.error_message or static_check_result.output}\n"
|
|
1125
|
+
agent.set_addon_prompt(
|
|
1126
|
+
f"基础静态检查失败,请根据以下错误信息修复代码:\n{static_check_result.error_message or static_check_result.output}\n"
|
|
1127
|
+
)
|
|
1128
|
+
else:
|
|
1129
|
+
final_ret += f"\n✅ 基础静态检查通过(耗时 {static_check_result.duration:.2f}秒)\n"
|
|
1130
|
+
|
|
1131
|
+
return final_ret
|
|
1132
|
+
|
|
1133
|
+
def _handle_build_validation_failure(self, build_validation_result: Any, config: Any, modified_files: List[str], agent: Agent, final_ret: str) -> str:
|
|
1134
|
+
"""处理构建验证失败的情况
|
|
1135
|
+
|
|
1136
|
+
Returns:
|
|
1137
|
+
更新后的结果字符串
|
|
1138
|
+
"""
|
|
1139
|
+
if not config.has_been_asked():
|
|
1140
|
+
# 首次失败,询问用户
|
|
1141
|
+
error_preview = _format_build_error(build_validation_result)
|
|
1142
|
+
PrettyOutput.print(
|
|
1143
|
+
f"\n⚠️ 构建验证失败:\n{error_preview}\n",
|
|
1144
|
+
OutputType.WARNING,
|
|
1145
|
+
)
|
|
1146
|
+
PrettyOutput.print(
|
|
1147
|
+
"提示:如果此项目需要在特殊环境(如容器)中构建,或使用独立构建脚本,"
|
|
1148
|
+
"可以选择禁用构建验证,后续将仅进行基础静态检查。",
|
|
1149
|
+
OutputType.INFO,
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
if user_confirm(
|
|
1153
|
+
"是否要禁用构建验证,后续仅进行基础静态检查?",
|
|
1154
|
+
default=False,
|
|
1155
|
+
):
|
|
1156
|
+
# 用户选择禁用
|
|
1157
|
+
config.disable_build_validation(
|
|
1158
|
+
reason="用户选择禁用(项目可能需要在特殊环境中构建)"
|
|
1159
|
+
)
|
|
1160
|
+
config.mark_as_asked()
|
|
1161
|
+
final_ret += "\n\nℹ️ 已禁用构建验证,后续将仅进行基础静态检查\n"
|
|
1162
|
+
|
|
1163
|
+
# 输出基础静态检查日志
|
|
1164
|
+
file_count = len(modified_files)
|
|
1165
|
+
files_str = ", ".join(os.path.basename(f) for f in modified_files[:3])
|
|
1166
|
+
if file_count > 3:
|
|
1167
|
+
files_str += f" 等{file_count}个文件"
|
|
1168
|
+
PrettyOutput.print(f"🔍 正在进行基础静态检查 ({files_str})...", OutputType.INFO)
|
|
1169
|
+
|
|
1170
|
+
# 立即进行基础静态检查
|
|
1171
|
+
fallback_validator = FallbackBuildValidator(self.root_dir, timeout=get_build_validation_timeout())
|
|
1172
|
+
static_check_result = fallback_validator.validate(modified_files)
|
|
1173
|
+
if not static_check_result.success:
|
|
1174
|
+
final_ret += f"\n⚠️ 基础静态检查失败:\n{static_check_result.error_message or static_check_result.output}\n"
|
|
1175
|
+
agent.set_addon_prompt(
|
|
1176
|
+
f"基础静态检查失败,请根据以下错误信息修复代码:\n{static_check_result.error_message or static_check_result.output}\n"
|
|
1177
|
+
)
|
|
1178
|
+
else:
|
|
1179
|
+
final_ret += f"\n✅ 基础静态检查通过(耗时 {static_check_result.duration:.2f}秒)\n"
|
|
1180
|
+
else:
|
|
1181
|
+
# 用户选择继续验证,标记为已询问
|
|
1182
|
+
config.mark_as_asked()
|
|
1183
|
+
final_ret += f"\n\n⚠️ 构建验证失败:\n{_format_build_error(build_validation_result)}\n"
|
|
1184
|
+
# 如果构建失败,添加修复提示
|
|
1185
|
+
agent.set_addon_prompt(
|
|
1186
|
+
f"构建验证失败,请根据以下错误信息修复代码:\n{_format_build_error(build_validation_result)}\n"
|
|
1187
|
+
"请仔细检查错误信息,修复编译/构建错误后重新提交。"
|
|
1188
|
+
)
|
|
1189
|
+
else:
|
|
1190
|
+
# 已经询问过,直接显示错误
|
|
1191
|
+
final_ret += f"\n\n⚠️ 构建验证失败:\n{_format_build_error(build_validation_result)}\n"
|
|
1192
|
+
# 如果构建失败,添加修复提示
|
|
1193
|
+
agent.set_addon_prompt(
|
|
1194
|
+
f"构建验证失败,请根据以下错误信息修复代码:\n{_format_build_error(build_validation_result)}\n"
|
|
1195
|
+
"请仔细检查错误信息,修复编译/构建错误后重新提交。"
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
return final_ret
|
|
1199
|
+
|
|
1200
|
+
def _handle_build_validation(self, modified_files: List[str], agent: Agent, final_ret: str) -> Tuple[Optional[Any], str]:
|
|
1201
|
+
"""处理构建验证
|
|
1202
|
+
|
|
1203
|
+
Returns:
|
|
1204
|
+
(build_validation_result, updated_final_ret)
|
|
1205
|
+
"""
|
|
1206
|
+
if not is_enable_build_validation():
|
|
1207
|
+
return None, final_ret
|
|
1208
|
+
|
|
1209
|
+
config = BuildValidationConfig(self.root_dir)
|
|
1210
|
+
|
|
1211
|
+
# 检查是否已禁用构建验证
|
|
1212
|
+
if config.is_build_validation_disabled():
|
|
1213
|
+
final_ret = self._handle_build_validation_disabled(modified_files, config, agent, final_ret)
|
|
1214
|
+
return None, final_ret
|
|
1215
|
+
|
|
1216
|
+
# 未禁用,进行构建验证
|
|
1217
|
+
build_validation_result = self._validate_build_after_edit(modified_files)
|
|
1218
|
+
if build_validation_result:
|
|
1219
|
+
if not build_validation_result.success:
|
|
1220
|
+
final_ret = self._handle_build_validation_failure(
|
|
1221
|
+
build_validation_result, config, modified_files, agent, final_ret
|
|
1222
|
+
)
|
|
1223
|
+
else:
|
|
1224
|
+
build_system_info = f" ({build_validation_result.build_system.value})" if build_validation_result.build_system else ""
|
|
1225
|
+
final_ret += f"\n\n✅ 构建验证通过{build_system_info}(耗时 {build_validation_result.duration:.2f}秒)\n"
|
|
1226
|
+
|
|
1227
|
+
return build_validation_result, final_ret
|
|
1228
|
+
|
|
1229
|
+
def _handle_static_analysis(self, modified_files: List[str], build_validation_result: Optional[Any], config: Any, agent: Agent, final_ret: str) -> str:
|
|
1230
|
+
"""处理静态分析
|
|
1231
|
+
|
|
1232
|
+
Returns:
|
|
1233
|
+
更新后的结果字符串
|
|
1234
|
+
"""
|
|
1235
|
+
# 检查是否启用静态分析
|
|
1236
|
+
if not is_enable_static_analysis():
|
|
1237
|
+
PrettyOutput.print("ℹ️ 静态分析已禁用,跳过静态检查", OutputType.INFO)
|
|
1238
|
+
return final_ret
|
|
1239
|
+
|
|
1240
|
+
# 检查是否有可用的lint工具
|
|
1241
|
+
lint_tools_info = "\n".join(
|
|
1242
|
+
f" - {file}: 使用 {'、'.join(get_lint_tools(file))}"
|
|
1243
|
+
for file in modified_files
|
|
1244
|
+
if get_lint_tools(file)
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
if not lint_tools_info:
|
|
1248
|
+
PrettyOutput.print("ℹ️ 未找到可用的静态检查工具,跳过静态检查", OutputType.INFO)
|
|
1249
|
+
return final_ret
|
|
1250
|
+
|
|
1251
|
+
# 如果构建验证失败且未禁用,不进行静态分析(避免重复错误)
|
|
1252
|
+
# 如果构建验证已禁用,则进行静态分析(因为只做了基础静态检查)
|
|
1253
|
+
should_skip_static = (
|
|
1254
|
+
build_validation_result
|
|
1255
|
+
and not build_validation_result.success
|
|
1256
|
+
and not config.is_build_validation_disabled()
|
|
1257
|
+
)
|
|
1258
|
+
|
|
1259
|
+
if should_skip_static:
|
|
1260
|
+
PrettyOutput.print("ℹ️ 构建验证失败,跳过静态分析(避免重复错误)", OutputType.INFO)
|
|
1261
|
+
return final_ret
|
|
1262
|
+
|
|
1263
|
+
# 直接执行静态扫描
|
|
1264
|
+
lint_results = self._run_static_analysis(modified_files)
|
|
1265
|
+
if lint_results:
|
|
1266
|
+
# 有错误或警告,让大模型修复
|
|
1267
|
+
errors_summary = self._format_lint_results(lint_results)
|
|
1268
|
+
addon_prompt = f"""
|
|
1269
|
+
静态扫描发现以下问题,请根据错误信息修复代码:
|
|
1270
|
+
|
|
1271
|
+
{errors_summary}
|
|
1272
|
+
|
|
1273
|
+
请仔细检查并修复所有问题。
|
|
1274
|
+
"""
|
|
1275
|
+
agent.set_addon_prompt(addon_prompt)
|
|
1276
|
+
final_ret += "\n\n⚠️ 静态扫描发现问题,已提示修复\n"
|
|
1277
|
+
else:
|
|
1278
|
+
final_ret += "\n\n✅ 静态扫描通过\n"
|
|
1279
|
+
|
|
1280
|
+
return final_ret
|
|
1281
|
+
|
|
1282
|
+
def _ask_llm_about_large_deletion(self, detection_result: Dict[str, int], preview: str) -> bool:
|
|
1283
|
+
"""询问大模型大量代码删除是否合理
|
|
1284
|
+
|
|
1285
|
+
参数:
|
|
1286
|
+
detection_result: 检测结果字典,包含 'insertions', 'deletions', 'net_deletions'
|
|
1287
|
+
preview: 补丁预览内容
|
|
1288
|
+
|
|
1289
|
+
返回:
|
|
1290
|
+
bool: 如果大模型认为合理返回True,否则返回False
|
|
1291
|
+
"""
|
|
1292
|
+
if not self.model:
|
|
1293
|
+
# 如果没有模型,默认认为合理
|
|
1294
|
+
return True
|
|
1295
|
+
|
|
1296
|
+
insertions = detection_result['insertions']
|
|
1297
|
+
deletions = detection_result['deletions']
|
|
1298
|
+
net_deletions = detection_result['net_deletions']
|
|
1299
|
+
|
|
1300
|
+
prompt = f"""检测到大量代码删除,请判断是否合理:
|
|
1301
|
+
|
|
1302
|
+
统计信息:
|
|
1303
|
+
- 新增行数: {insertions}
|
|
1304
|
+
- 删除行数: {deletions}
|
|
1305
|
+
- 净删除行数: {net_deletions}
|
|
1306
|
+
|
|
1307
|
+
补丁预览:
|
|
1308
|
+
{preview}
|
|
1309
|
+
|
|
1310
|
+
请仔细分析以上代码变更,判断这些大量代码删除是否合理。可能的情况包括:
|
|
1311
|
+
1. 重构代码,删除冗余或过时的代码
|
|
1312
|
+
2. 简化实现,用更简洁的代码替换复杂的实现
|
|
1313
|
+
3. 删除未使用的代码或功能
|
|
1314
|
+
4. 错误地删除了重要代码
|
|
1315
|
+
|
|
1316
|
+
请使用以下协议回答(必须包含且仅包含以下标记之一):
|
|
1317
|
+
- 如果认为这些删除是合理的,回答: <!!!YES!!!>
|
|
1318
|
+
- 如果认为这些删除不合理或存在风险,回答: <!!!NO!!!>
|
|
1319
|
+
|
|
1320
|
+
请严格按照协议格式回答,不要添加其他内容。
|
|
1321
|
+
"""
|
|
1322
|
+
|
|
1323
|
+
try:
|
|
1324
|
+
PrettyOutput.print("🤖 正在询问大模型判断大量代码删除是否合理...", OutputType.INFO)
|
|
1325
|
+
response = self.model.chat_until_success(prompt) # type: ignore
|
|
1326
|
+
|
|
1327
|
+
# 使用确定的协议标记解析回答
|
|
1328
|
+
if "<!!!YES!!!>" in response:
|
|
1329
|
+
PrettyOutput.print("✅ 大模型确认:代码删除合理", OutputType.SUCCESS)
|
|
1330
|
+
return True
|
|
1331
|
+
elif "<!!!NO!!!>" in response:
|
|
1332
|
+
PrettyOutput.print("❌ 大模型确认:代码删除不合理", OutputType.WARNING)
|
|
1333
|
+
return False
|
|
1334
|
+
else:
|
|
1335
|
+
# 如果无法找到协议标记,默认认为不合理(保守策略)
|
|
1336
|
+
PrettyOutput.print(
|
|
1337
|
+
f"⚠️ 无法找到协议标记,默认认为不合理。回答内容: {response[:200]}",
|
|
1338
|
+
OutputType.WARNING
|
|
1339
|
+
)
|
|
1340
|
+
return False
|
|
1341
|
+
except Exception as e:
|
|
1342
|
+
# 如果询问失败,默认认为不合理(保守策略)
|
|
1343
|
+
PrettyOutput.print(
|
|
1344
|
+
f"⚠️ 询问大模型失败: {str(e)},默认认为不合理",
|
|
1345
|
+
OutputType.WARNING
|
|
1346
|
+
)
|
|
1347
|
+
return False
|
|
1348
|
+
|
|
1349
|
+
def _on_after_tool_call(self, agent: Agent, current_response=None, need_return=None, tool_prompt=None, **kwargs) -> None:
|
|
337
1350
|
"""工具调用后回调函数。"""
|
|
338
1351
|
final_ret = ""
|
|
339
1352
|
diff = get_diff()
|
|
1353
|
+
|
|
340
1354
|
if diff:
|
|
341
1355
|
start_hash = get_latest_commit_hash()
|
|
342
1356
|
PrettyOutput.print(diff, OutputType.CODE, lang="diff")
|
|
343
1357
|
modified_files = get_diff_file_list()
|
|
1358
|
+
|
|
1359
|
+
# 更新上下文管理器
|
|
1360
|
+
self._update_context_for_modified_files(modified_files)
|
|
1361
|
+
|
|
1362
|
+
# 进行影响范围分析
|
|
1363
|
+
impact_report = self._analyze_edit_impact(modified_files)
|
|
1364
|
+
|
|
1365
|
+
per_file_preview = self._build_per_file_patch_preview(modified_files)
|
|
1366
|
+
|
|
1367
|
+
# 非交互模式下,在提交前检测大量代码删除
|
|
1368
|
+
if self.non_interactive:
|
|
1369
|
+
detection_result = detect_large_code_deletion()
|
|
1370
|
+
if detection_result is not None:
|
|
1371
|
+
# 检测到大量代码删除,询问大模型是否合理
|
|
1372
|
+
is_reasonable = self._ask_llm_about_large_deletion(detection_result, per_file_preview)
|
|
1373
|
+
if not is_reasonable:
|
|
1374
|
+
# 大模型认为不合理,撤销修改
|
|
1375
|
+
PrettyOutput.print("已撤销修改(大模型认为代码删除不合理)", OutputType.INFO)
|
|
1376
|
+
revert_change()
|
|
1377
|
+
final_ret += "\n\n修改被撤销(检测到大量代码删除且大模型判断不合理)\n"
|
|
1378
|
+
final_ret += f"# 补丁预览(按文件):\n{per_file_preview}"
|
|
1379
|
+
PrettyOutput.print(final_ret, OutputType.USER, lang="markdown")
|
|
1380
|
+
self.session.prompt += final_ret
|
|
1381
|
+
return
|
|
1382
|
+
|
|
344
1383
|
commited = handle_commit_workflow()
|
|
345
1384
|
if commited:
|
|
1385
|
+
# 统计代码行数变化
|
|
1386
|
+
# 获取diff的统计信息
|
|
1387
|
+
try:
|
|
1388
|
+
diff_result = subprocess.run(
|
|
1389
|
+
["git", "diff", "HEAD~1", "HEAD", "--shortstat"],
|
|
1390
|
+
capture_output=True,
|
|
1391
|
+
text=True,
|
|
1392
|
+
encoding="utf-8",
|
|
1393
|
+
errors="replace",
|
|
1394
|
+
check=True,
|
|
1395
|
+
)
|
|
1396
|
+
if diff_result.returncode == 0 and diff_result.stdout:
|
|
1397
|
+
self._record_code_changes_stats(diff_result.stdout)
|
|
1398
|
+
except subprocess.CalledProcessError:
|
|
1399
|
+
pass
|
|
1400
|
+
|
|
1401
|
+
# 统计修改次数
|
|
1402
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
|
1403
|
+
|
|
1404
|
+
StatsManager.increment("code_modifications", group="code_agent")
|
|
1405
|
+
|
|
346
1406
|
# 获取提交信息
|
|
347
1407
|
end_hash = get_latest_commit_hash()
|
|
348
1408
|
commits = get_commits_between(start_hash, end_hash)
|
|
349
1409
|
|
|
350
|
-
# 添加提交信息到final_ret
|
|
1410
|
+
# 添加提交信息到final_ret(按文件展示diff;删除文件仅提示)
|
|
351
1411
|
if commits:
|
|
352
1412
|
final_ret += (
|
|
353
|
-
f"\n\n代码已修改完成\n
|
|
354
|
-
)
|
|
355
|
-
# 修改后的提示逻辑
|
|
356
|
-
lint_tools_info = "\n".join(
|
|
357
|
-
f" - {file}: 使用 {'、'.join(get_lint_tools(file))}"
|
|
358
|
-
for file in modified_files
|
|
359
|
-
if get_lint_tools(file)
|
|
1413
|
+
f"\n\n代码已修改完成\n补丁内容(按文件):\n{per_file_preview}\n"
|
|
360
1414
|
)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
{tool_info}
|
|
372
|
-
如果本次修改引入了警告和错误,请根据警告和错误信息修复代码
|
|
373
|
-
注意:如果要进行静态检查,需要在所有的修改都完成之后进行集中检查,如果文件有多个检查工具,尽量一次全部调用,不要分多次调用
|
|
374
|
-
"""
|
|
375
|
-
agent.set_addon_prompt(addon_prompt)
|
|
1415
|
+
|
|
1416
|
+
# 添加影响范围分析报告
|
|
1417
|
+
final_ret = self._handle_impact_report(impact_report, self, final_ret)
|
|
1418
|
+
|
|
1419
|
+
# 构建验证
|
|
1420
|
+
config = BuildValidationConfig(self.root_dir)
|
|
1421
|
+
build_validation_result, final_ret = self._handle_build_validation(modified_files, self, final_ret)
|
|
1422
|
+
|
|
1423
|
+
# 静态分析
|
|
1424
|
+
final_ret = self._handle_static_analysis(modified_files, build_validation_result, config, self, final_ret)
|
|
376
1425
|
else:
|
|
377
1426
|
final_ret += "\n\n修改没有生效\n"
|
|
378
1427
|
else:
|
|
379
1428
|
final_ret += "\n修改被拒绝\n"
|
|
380
|
-
final_ret += f"#
|
|
1429
|
+
final_ret += f"# 补丁预览(按文件):\n{per_file_preview}"
|
|
381
1430
|
else:
|
|
382
1431
|
return
|
|
383
1432
|
# 用户确认最终结果
|
|
384
1433
|
if commited:
|
|
385
|
-
|
|
1434
|
+
self.session.prompt += final_ret
|
|
386
1435
|
return
|
|
387
1436
|
PrettyOutput.print(final_ret, OutputType.USER, lang="markdown")
|
|
388
1437
|
if not is_confirm_before_apply_patch() or user_confirm(
|
|
389
1438
|
"是否使用此回复?", default=True
|
|
390
1439
|
):
|
|
391
|
-
|
|
1440
|
+
self.session.prompt += final_ret
|
|
392
1441
|
return
|
|
393
|
-
|
|
1442
|
+
# 用户未确认,允许输入自定义回复作为附加提示
|
|
394
1443
|
custom_reply = get_multiline_input("请输入自定义回复")
|
|
395
|
-
if custom_reply.strip(): #
|
|
396
|
-
|
|
397
|
-
|
|
1444
|
+
if custom_reply.strip(): # 如果自定义回复为空,不设置附加提示
|
|
1445
|
+
self.set_addon_prompt(custom_reply)
|
|
1446
|
+
self.session.prompt += final_ret
|
|
1447
|
+
return
|
|
398
1448
|
|
|
1449
|
+
def _run_static_analysis(self, modified_files: List[str]) -> List[Tuple[str, str, str, int, str]]:
|
|
1450
|
+
"""执行静态分析
|
|
1451
|
+
|
|
1452
|
+
Args:
|
|
1453
|
+
modified_files: 修改的文件列表
|
|
1454
|
+
|
|
1455
|
+
Returns:
|
|
1456
|
+
[(tool_name, file_path, command, returncode, output), ...] 格式的结果列表
|
|
1457
|
+
只返回有错误或警告的结果(returncode != 0)
|
|
1458
|
+
"""
|
|
1459
|
+
if not modified_files:
|
|
1460
|
+
return []
|
|
1461
|
+
|
|
1462
|
+
# 获取所有lint命令
|
|
1463
|
+
commands = get_lint_commands_for_files(modified_files, self.root_dir)
|
|
1464
|
+
if not commands:
|
|
1465
|
+
return []
|
|
1466
|
+
|
|
1467
|
+
# 输出静态检查日志
|
|
1468
|
+
file_count = len(modified_files)
|
|
1469
|
+
files_str = ", ".join(os.path.basename(f) for f in modified_files[:3])
|
|
1470
|
+
if file_count > 3:
|
|
1471
|
+
files_str += f" 等{file_count}个文件"
|
|
1472
|
+
tool_names = list(set(cmd[0] for cmd in commands))
|
|
1473
|
+
tools_str = ", ".join(tool_names[:3])
|
|
1474
|
+
if len(tool_names) > 3:
|
|
1475
|
+
tools_str += f" 等{len(tool_names)}个工具"
|
|
1476
|
+
PrettyOutput.print(f"🔍 正在进行静态检查 ({files_str}, 使用 {tools_str})...", OutputType.INFO)
|
|
1477
|
+
|
|
1478
|
+
results = []
|
|
1479
|
+
# 记录每个文件的检查结果
|
|
1480
|
+
file_results = [] # [(file_path, tool_name, status, message), ...]
|
|
1481
|
+
|
|
1482
|
+
# 按工具分组,相同工具可以批量执行
|
|
1483
|
+
grouped = group_commands_by_tool(commands)
|
|
1484
|
+
|
|
1485
|
+
for tool_name, file_commands in grouped.items():
|
|
1486
|
+
for file_path, command in file_commands:
|
|
1487
|
+
file_name = os.path.basename(file_path)
|
|
1488
|
+
try:
|
|
1489
|
+
# 检查文件是否存在
|
|
1490
|
+
abs_file_path = os.path.join(self.root_dir, file_path) if not os.path.isabs(file_path) else file_path
|
|
1491
|
+
if not os.path.exists(abs_file_path):
|
|
1492
|
+
file_results.append((file_name, tool_name, "跳过", "文件不存在"))
|
|
1493
|
+
continue
|
|
1494
|
+
|
|
1495
|
+
# 执行命令
|
|
1496
|
+
result = subprocess.run(
|
|
1497
|
+
command,
|
|
1498
|
+
shell=True,
|
|
1499
|
+
cwd=self.root_dir,
|
|
1500
|
+
capture_output=True,
|
|
1501
|
+
text=True,
|
|
1502
|
+
encoding="utf-8",
|
|
1503
|
+
errors="replace",
|
|
1504
|
+
timeout=30, # 30秒超时
|
|
1505
|
+
)
|
|
1506
|
+
|
|
1507
|
+
# 只记录有错误或警告的结果
|
|
1508
|
+
if result.returncode != 0:
|
|
1509
|
+
output = result.stdout + result.stderr
|
|
1510
|
+
if output.strip(): # 有输出才记录
|
|
1511
|
+
results.append((tool_name, file_path, command, result.returncode, output))
|
|
1512
|
+
file_results.append((file_name, tool_name, "失败", "发现问题"))
|
|
1513
|
+
else:
|
|
1514
|
+
file_results.append((file_name, tool_name, "通过", ""))
|
|
1515
|
+
else:
|
|
1516
|
+
file_results.append((file_name, tool_name, "通过", ""))
|
|
1517
|
+
|
|
1518
|
+
except subprocess.TimeoutExpired:
|
|
1519
|
+
results.append((tool_name, file_path, command, -1, "执行超时(30秒)"))
|
|
1520
|
+
file_results.append((file_name, tool_name, "超时", "执行超时(30秒)"))
|
|
1521
|
+
except FileNotFoundError:
|
|
1522
|
+
# 工具未安装,跳过
|
|
1523
|
+
file_results.append((file_name, tool_name, "跳过", "工具未安装"))
|
|
1524
|
+
continue
|
|
1525
|
+
except Exception as e:
|
|
1526
|
+
# 其他错误,记录但继续
|
|
1527
|
+
import logging
|
|
1528
|
+
logger = logging.getLogger(__name__)
|
|
1529
|
+
logger.warning(f"执行lint命令失败: {command}, 错误: {e}")
|
|
1530
|
+
file_results.append((file_name, tool_name, "失败", f"执行失败: {str(e)[:50]}"))
|
|
1531
|
+
continue
|
|
1532
|
+
|
|
1533
|
+
# 一次性打印所有检查结果
|
|
1534
|
+
if file_results:
|
|
1535
|
+
total_files = len(file_results)
|
|
1536
|
+
passed_count = sum(1 for _, _, status, _ in file_results if status == "通过")
|
|
1537
|
+
failed_count = sum(1 for _, _, status, _ in file_results if status == "失败")
|
|
1538
|
+
timeout_count = sum(1 for _, _, status, _ in file_results if status == "超时")
|
|
1539
|
+
skipped_count = sum(1 for _, _, status, _ in file_results if status == "跳过")
|
|
1540
|
+
|
|
1541
|
+
# 构建结果摘要
|
|
1542
|
+
summary_lines = [f"🔍 静态检查完成: 共检查 {total_files} 个文件"]
|
|
1543
|
+
if passed_count > 0:
|
|
1544
|
+
summary_lines.append(f" ✅ 通过: {passed_count}")
|
|
1545
|
+
if failed_count > 0:
|
|
1546
|
+
summary_lines.append(f" ❌ 失败: {failed_count}")
|
|
1547
|
+
if timeout_count > 0:
|
|
1548
|
+
summary_lines.append(f" ⏱️ 超时: {timeout_count}")
|
|
1549
|
+
if skipped_count > 0:
|
|
1550
|
+
summary_lines.append(f" ⚠️ 跳过: {skipped_count}")
|
|
1551
|
+
|
|
1552
|
+
# 添加详细结果(只显示失败和超时的文件)
|
|
1553
|
+
if failed_count > 0 or timeout_count > 0:
|
|
1554
|
+
summary_lines.append("\n详细结果:")
|
|
1555
|
+
for file_name, tool_name, status, message in file_results:
|
|
1556
|
+
if status not in ("失败", "超时"):
|
|
1557
|
+
continue # 只显示失败和超时的文件
|
|
1558
|
+
status_icon = {
|
|
1559
|
+
"失败": "❌",
|
|
1560
|
+
"超时": "⏱️"
|
|
1561
|
+
}.get(status, "•")
|
|
1562
|
+
if message:
|
|
1563
|
+
summary_lines.append(f" {status_icon} {file_name} ({tool_name}): {message}")
|
|
1564
|
+
else:
|
|
1565
|
+
summary_lines.append(f" {status_icon} {file_name} ({tool_name})")
|
|
1566
|
+
|
|
1567
|
+
output_type = OutputType.WARNING if (failed_count > 0 or timeout_count > 0) else OutputType.SUCCESS
|
|
1568
|
+
PrettyOutput.print("\n".join(summary_lines), output_type)
|
|
1569
|
+
else:
|
|
1570
|
+
PrettyOutput.print("🔍 静态检查完成: 全部通过", OutputType.SUCCESS)
|
|
1571
|
+
|
|
1572
|
+
return results
|
|
1573
|
+
|
|
1574
|
+
def _format_lint_results(self, results: List[Tuple[str, str, str, int, str]]) -> str:
|
|
1575
|
+
"""格式化lint结果
|
|
1576
|
+
|
|
1577
|
+
Args:
|
|
1578
|
+
results: [(tool_name, file_path, command, returncode, output), ...]
|
|
1579
|
+
|
|
1580
|
+
Returns:
|
|
1581
|
+
格式化的错误信息字符串
|
|
1582
|
+
"""
|
|
1583
|
+
if not results:
|
|
1584
|
+
return ""
|
|
1585
|
+
|
|
1586
|
+
lines = []
|
|
1587
|
+
for tool_name, file_path, command, returncode, output in results:
|
|
1588
|
+
lines.append(f"工具: {tool_name}")
|
|
1589
|
+
lines.append(f"文件: {file_path}")
|
|
1590
|
+
lines.append(f"命令: {command}")
|
|
1591
|
+
if returncode == -1:
|
|
1592
|
+
lines.append(f"错误: {output}")
|
|
1593
|
+
else:
|
|
1594
|
+
# 限制输出长度,避免过长
|
|
1595
|
+
output_preview = output[:1000] if len(output) > 1000 else output
|
|
1596
|
+
lines.append(f"输出:\n{output_preview}")
|
|
1597
|
+
if len(output) > 1000:
|
|
1598
|
+
lines.append(f"... (输出已截断,共 {len(output)} 字符)")
|
|
1599
|
+
lines.append("") # 空行分隔
|
|
1600
|
+
|
|
1601
|
+
return "\n".join(lines)
|
|
1602
|
+
|
|
1603
|
+
def _extract_file_paths_from_input(self, user_input: str) -> List[str]:
|
|
1604
|
+
"""从用户输入中提取文件路径
|
|
1605
|
+
|
|
1606
|
+
Args:
|
|
1607
|
+
user_input: 用户输入文本
|
|
1608
|
+
|
|
1609
|
+
Returns:
|
|
1610
|
+
文件路径列表
|
|
1611
|
+
"""
|
|
1612
|
+
import re
|
|
1613
|
+
file_paths = []
|
|
1614
|
+
|
|
1615
|
+
# 匹配常见的文件路径模式
|
|
1616
|
+
# 1. 引号中的路径: "path/to/file.py" 或 'path/to/file.py'
|
|
1617
|
+
quoted_paths = re.findall(r'["\']([^"\']+\.(?:py|js|ts|rs|go|java|cpp|c|h|hpp))["\']', user_input)
|
|
1618
|
+
file_paths.extend(quoted_paths)
|
|
1619
|
+
|
|
1620
|
+
# 2. 相对路径: ./path/to/file.py 或 path/to/file.py
|
|
1621
|
+
relative_paths = re.findall(r'(?:\./)?[\w/]+\.(?:py|js|ts|rs|go|java|cpp|c|h|hpp)', user_input)
|
|
1622
|
+
file_paths.extend(relative_paths)
|
|
1623
|
+
|
|
1624
|
+
# 3. 绝对路径(简化匹配)
|
|
1625
|
+
absolute_paths = re.findall(r'/(?:[\w\-\.]+/)+[\w\-\.]+\.(?:py|js|ts|rs|go|java|cpp|c|h|hpp)', user_input)
|
|
1626
|
+
file_paths.extend(absolute_paths)
|
|
1627
|
+
|
|
1628
|
+
# 转换为绝对路径并去重
|
|
1629
|
+
unique_paths = []
|
|
1630
|
+
seen = set()
|
|
1631
|
+
for path in file_paths:
|
|
1632
|
+
abs_path = os.path.abspath(path) if not os.path.isabs(path) else path
|
|
1633
|
+
if abs_path not in seen and os.path.exists(abs_path):
|
|
1634
|
+
seen.add(abs_path)
|
|
1635
|
+
unique_paths.append(abs_path)
|
|
1636
|
+
|
|
1637
|
+
return unique_paths
|
|
399
1638
|
|
|
400
|
-
def
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
1639
|
+
def _extract_symbols_from_input(self, user_input: str) -> List[str]:
|
|
1640
|
+
"""从用户输入中提取符号名称(函数名、类名等)
|
|
1641
|
+
|
|
1642
|
+
Args:
|
|
1643
|
+
user_input: 用户输入文本
|
|
1644
|
+
|
|
1645
|
+
Returns:
|
|
1646
|
+
符号名称列表
|
|
1647
|
+
"""
|
|
1648
|
+
import re
|
|
1649
|
+
symbols = []
|
|
1650
|
+
|
|
1651
|
+
# 匹配常见的符号命名模式
|
|
1652
|
+
# 1. 驼峰命名(类名): MyClass, ProcessData
|
|
1653
|
+
camel_case = re.findall(r'\b[A-Z][a-zA-Z0-9]+\b', user_input)
|
|
1654
|
+
symbols.extend(camel_case)
|
|
1655
|
+
|
|
1656
|
+
# 2. 下划线命名(函数名、变量名): process_data, get_user_info
|
|
1657
|
+
snake_case = re.findall(r'\b[a-z][a-z0-9_]+[a-z0-9]\b', user_input)
|
|
1658
|
+
symbols.extend(snake_case)
|
|
1659
|
+
|
|
1660
|
+
# 3. 在引号中的符号名: "function_name" 或 'ClassName'
|
|
1661
|
+
quoted_symbols = re.findall(r'["\']([A-Za-z][A-Za-z0-9_]*?)["\']', user_input)
|
|
1662
|
+
symbols.extend(quoted_symbols)
|
|
1663
|
+
|
|
1664
|
+
# 过滤常见停用词和过短的符号
|
|
1665
|
+
stop_words = {
|
|
1666
|
+
'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'her', 'was', 'one',
|
|
1667
|
+
'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'its', 'may', 'new', 'now',
|
|
1668
|
+
'old', 'see', 'two', 'way', 'who', 'boy', 'did', 'its', 'let', 'put', 'say', 'she',
|
|
1669
|
+
'too', 'use', '添加', '修改', '实现', '修复', '更新', '删除', '创建', '文件', '代码',
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
unique_symbols = []
|
|
1673
|
+
seen = set()
|
|
1674
|
+
for symbol in symbols:
|
|
1675
|
+
symbol_lower = symbol.lower()
|
|
1676
|
+
if (symbol_lower not in stop_words and
|
|
1677
|
+
len(symbol) > 2 and
|
|
1678
|
+
symbol_lower not in seen):
|
|
1679
|
+
seen.add(symbol_lower)
|
|
1680
|
+
unique_symbols.append(symbol)
|
|
1681
|
+
|
|
1682
|
+
return unique_symbols[:10] # 限制数量
|
|
1683
|
+
|
|
1684
|
+
def _validate_build_after_edit(self, modified_files: List[str]) -> Optional[BuildResult]:
|
|
1685
|
+
"""编辑后验证构建
|
|
1686
|
+
|
|
1687
|
+
Args:
|
|
1688
|
+
modified_files: 修改的文件列表
|
|
1689
|
+
|
|
1690
|
+
Returns:
|
|
1691
|
+
BuildResult: 验证结果,如果验证被禁用或出错则返回None
|
|
1692
|
+
"""
|
|
1693
|
+
if not is_enable_build_validation():
|
|
1694
|
+
return None
|
|
1695
|
+
|
|
1696
|
+
# 检查项目配置,看是否已禁用构建验证
|
|
1697
|
+
config = BuildValidationConfig(self.root_dir)
|
|
1698
|
+
if config.is_build_validation_disabled():
|
|
1699
|
+
# 已禁用,返回None,由调用方处理基础静态检查
|
|
1700
|
+
return None
|
|
1701
|
+
|
|
1702
|
+
# 输出编译检查日志
|
|
1703
|
+
file_count = len(modified_files)
|
|
1704
|
+
files_str = ", ".join(os.path.basename(f) for f in modified_files[:3])
|
|
1705
|
+
if file_count > 3:
|
|
1706
|
+
files_str += f" 等{file_count}个文件"
|
|
1707
|
+
PrettyOutput.print(f"🔨 正在进行编译检查 ({files_str})...", OutputType.INFO)
|
|
1708
|
+
|
|
1709
|
+
try:
|
|
1710
|
+
timeout = get_build_validation_timeout()
|
|
1711
|
+
validator = BuildValidator(self.root_dir, timeout=timeout)
|
|
1712
|
+
result = validator.validate(modified_files)
|
|
1713
|
+
return result
|
|
1714
|
+
except Exception as e:
|
|
1715
|
+
# 构建验证失败不应该影响主流程,仅记录日志
|
|
1716
|
+
import logging
|
|
1717
|
+
logger = logging.getLogger(__name__)
|
|
1718
|
+
logger.warning(f"构建验证执行失败: {e}", exc_info=True)
|
|
1719
|
+
return None
|
|
1720
|
+
|
|
1721
|
+
|
|
1722
|
+
@app.command()
|
|
1723
|
+
def cli(
|
|
1724
|
+
model_group: Optional[str] = typer.Option(
|
|
1725
|
+
None, "-g", "--llm-group", help="使用的模型组,覆盖配置文件中的设置"
|
|
1726
|
+
),
|
|
1727
|
+
tool_group: Optional[str] = typer.Option(
|
|
1728
|
+
None, "-G", "--tool-group", help="使用的工具组,覆盖配置文件中的设置"
|
|
1729
|
+
),
|
|
1730
|
+
config_file: Optional[str] = typer.Option(
|
|
1731
|
+
None, "-f", "--config", help="配置文件路径"
|
|
1732
|
+
),
|
|
1733
|
+
requirement: Optional[str] = typer.Option(
|
|
1734
|
+
None, "-r", "--requirement", help="要处理的需求描述"
|
|
1735
|
+
),
|
|
1736
|
+
append_tools: Optional[str] = typer.Option(
|
|
1737
|
+
None, "--append-tools", help="要追加的工具列表,用逗号分隔"
|
|
1738
|
+
),
|
|
1739
|
+
restore_session: bool = typer.Option(
|
|
1740
|
+
False,
|
|
416
1741
|
"--restore-session",
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
1742
|
+
help="从 .jarvis/saved_session.json 恢复会话状态",
|
|
1743
|
+
),
|
|
1744
|
+
prefix: str = typer.Option(
|
|
1745
|
+
"",
|
|
1746
|
+
"--prefix",
|
|
1747
|
+
help="提交信息前缀(用空格分隔)",
|
|
1748
|
+
),
|
|
1749
|
+
suffix: str = typer.Option(
|
|
1750
|
+
"",
|
|
1751
|
+
"--suffix",
|
|
1752
|
+
help="提交信息后缀(用换行分隔)",
|
|
1753
|
+
),
|
|
1754
|
+
non_interactive: bool = typer.Option(
|
|
1755
|
+
False, "-n", "--non-interactive", help="启用非交互模式:用户无法与命令交互,脚本执行超时限制为5分钟"
|
|
1756
|
+
),
|
|
1757
|
+
plan: bool = typer.Option(False, "--plan/--no-plan", help="启用或禁用任务规划(子任务拆分与汇总执行)"),
|
|
1758
|
+
) -> None:
|
|
1759
|
+
"""Jarvis主入口点。"""
|
|
1760
|
+
# CLI 标志:非交互模式(不依赖配置文件)
|
|
1761
|
+
if non_interactive:
|
|
1762
|
+
try:
|
|
1763
|
+
os.environ["JARVIS_NON_INTERACTIVE"] = "true"
|
|
1764
|
+
except Exception:
|
|
1765
|
+
pass
|
|
1766
|
+
# 注意:全局配置同步放在 init_env 之后执行,避免被 init_env 覆盖
|
|
1767
|
+
# 非交互模式要求从命令行传入任务
|
|
1768
|
+
if non_interactive and not (requirement and str(requirement).strip()):
|
|
1769
|
+
PrettyOutput.print(
|
|
1770
|
+
"非交互模式已启用:必须使用 --requirement 传入任务内容,因多行输入不可用。",
|
|
1771
|
+
OutputType.ERROR,
|
|
1772
|
+
)
|
|
1773
|
+
raise typer.Exit(code=2)
|
|
1774
|
+
init_env(
|
|
1775
|
+
"欢迎使用 Jarvis-CodeAgent,您的代码工程助手已准备就绪!",
|
|
1776
|
+
config_file=config_file,
|
|
420
1777
|
)
|
|
421
|
-
|
|
1778
|
+
# CodeAgent 单实例互斥:改为按仓库维度加锁(延后至定位仓库根目录后执行)
|
|
1779
|
+
# 锁的获取移动到确认并切换到git根目录之后
|
|
422
1780
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
1781
|
+
# 在初始化环境后同步 CLI 选项到全局配置,避免被 init_env 覆盖
|
|
1782
|
+
try:
|
|
1783
|
+
if model_group:
|
|
1784
|
+
set_config("JARVIS_LLM_GROUP", str(model_group))
|
|
1785
|
+
if tool_group:
|
|
1786
|
+
set_config("JARVIS_TOOL_GROUP", str(tool_group))
|
|
1787
|
+
if restore_session:
|
|
1788
|
+
set_config("JARVIS_RESTORE_SESSION", True)
|
|
1789
|
+
if non_interactive:
|
|
1790
|
+
set_config("JARVIS_NON_INTERACTIVE", True)
|
|
1791
|
+
except Exception:
|
|
1792
|
+
# 静默忽略同步异常,不影响主流程
|
|
1793
|
+
pass
|
|
1794
|
+
|
|
1795
|
+
try:
|
|
1796
|
+
subprocess.run(
|
|
1797
|
+
["git", "rev-parse", "--git-dir"],
|
|
1798
|
+
check=True,
|
|
1799
|
+
stdout=subprocess.DEVNULL,
|
|
1800
|
+
stderr=subprocess.DEVNULL,
|
|
1801
|
+
)
|
|
1802
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
1803
|
+
curr_dir_path = os.getcwd()
|
|
1804
|
+
PrettyOutput.print(
|
|
1805
|
+
f"警告:当前目录 '{curr_dir_path}' 不是一个git仓库。", OutputType.WARNING
|
|
1806
|
+
)
|
|
1807
|
+
init_git = True if non_interactive else user_confirm(
|
|
1808
|
+
f"是否要在 '{curr_dir_path}' 中初始化一个新的git仓库?", default=True
|
|
1809
|
+
)
|
|
1810
|
+
if init_git:
|
|
1811
|
+
try:
|
|
1812
|
+
subprocess.run(
|
|
1813
|
+
["git", "init"],
|
|
1814
|
+
check=True,
|
|
1815
|
+
capture_output=True,
|
|
1816
|
+
text=True,
|
|
1817
|
+
encoding="utf-8",
|
|
1818
|
+
errors="replace",
|
|
1819
|
+
)
|
|
1820
|
+
PrettyOutput.print("✅ 已成功初始化git仓库。", OutputType.SUCCESS)
|
|
1821
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
1822
|
+
PrettyOutput.print(f"❌ 初始化git仓库失败: {e}", OutputType.ERROR)
|
|
1823
|
+
sys.exit(1)
|
|
1824
|
+
else:
|
|
1825
|
+
PrettyOutput.print(
|
|
1826
|
+
"操作已取消。Jarvis需要在git仓库中运行。", OutputType.INFO
|
|
1827
|
+
)
|
|
1828
|
+
sys.exit(0)
|
|
426
1829
|
|
|
1830
|
+
curr_dir = os.getcwd()
|
|
1831
|
+
find_git_root_and_cd(curr_dir)
|
|
1832
|
+
# 在定位到 git 根目录后,按仓库维度加锁,避免跨仓库互斥
|
|
1833
|
+
try:
|
|
1834
|
+
repo_root = os.getcwd()
|
|
1835
|
+
lock_name = f"code_agent_{hashlib.md5(repo_root.encode('utf-8')).hexdigest()}.lock"
|
|
1836
|
+
_acquire_single_instance_lock(lock_name=lock_name)
|
|
1837
|
+
except Exception:
|
|
1838
|
+
# 回退到全局锁,确保至少有互斥保护
|
|
1839
|
+
_acquire_single_instance_lock(lock_name="code_agent.lock")
|
|
427
1840
|
try:
|
|
428
|
-
agent = CodeAgent(
|
|
1841
|
+
agent = CodeAgent(
|
|
1842
|
+
model_group=model_group,
|
|
1843
|
+
need_summary=False,
|
|
1844
|
+
append_tools=append_tools,
|
|
1845
|
+
tool_group=tool_group,
|
|
1846
|
+
non_interactive=non_interactive,
|
|
1847
|
+
plan=plan,
|
|
1848
|
+
)
|
|
429
1849
|
|
|
430
1850
|
# 尝试恢复会话
|
|
431
|
-
if
|
|
432
|
-
if agent.
|
|
1851
|
+
if restore_session:
|
|
1852
|
+
if agent.restore_session():
|
|
433
1853
|
PrettyOutput.print(
|
|
434
1854
|
"已从 .jarvis/saved_session.json 恢复会话。", OutputType.SUCCESS
|
|
435
1855
|
)
|
|
@@ -438,19 +1858,26 @@ def main() -> None:
|
|
|
438
1858
|
"无法从 .jarvis/saved_session.json 恢复会话。", OutputType.WARNING
|
|
439
1859
|
)
|
|
440
1860
|
|
|
441
|
-
if
|
|
442
|
-
agent.run(
|
|
1861
|
+
if requirement:
|
|
1862
|
+
agent.run(requirement, prefix=prefix, suffix=suffix)
|
|
443
1863
|
else:
|
|
444
1864
|
while True:
|
|
445
1865
|
user_input = get_multiline_input("请输入你的需求(输入空行退出):")
|
|
446
1866
|
if not user_input:
|
|
447
|
-
|
|
448
|
-
agent.run(user_input)
|
|
1867
|
+
raise typer.Exit(code=0)
|
|
1868
|
+
agent.run(user_input, prefix=prefix, suffix=suffix)
|
|
449
1869
|
|
|
1870
|
+
except typer.Exit:
|
|
1871
|
+
raise
|
|
450
1872
|
except RuntimeError as e:
|
|
451
1873
|
PrettyOutput.print(f"错误: {str(e)}", OutputType.ERROR)
|
|
452
1874
|
sys.exit(1)
|
|
453
1875
|
|
|
454
1876
|
|
|
1877
|
+
def main() -> None:
|
|
1878
|
+
"""Application entry point."""
|
|
1879
|
+
app()
|
|
1880
|
+
|
|
1881
|
+
|
|
455
1882
|
if __name__ == "__main__":
|
|
456
1883
|
main()
|