jarvis-ai-assistant 0.2.3__py3-none-any.whl → 0.2.4__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/edit_file_handler.py +5 -0
- jarvis/jarvis_agent/jarvis.py +22 -25
- jarvis/jarvis_agent/main.py +6 -6
- jarvis/jarvis_code_agent/code_agent.py +279 -11
- jarvis/jarvis_code_analysis/code_review.py +21 -19
- jarvis/jarvis_data/config_schema.json +23 -10
- jarvis/jarvis_git_squash/main.py +3 -3
- jarvis/jarvis_git_utils/git_commiter.py +32 -11
- jarvis/jarvis_mcp/sse_mcp_client.py +4 -6
- jarvis/jarvis_mcp/streamable_mcp_client.py +5 -9
- jarvis/jarvis_rag/retriever.py +1 -1
- jarvis/jarvis_smart_shell/main.py +2 -2
- jarvis/jarvis_stats/__init__.py +13 -0
- jarvis/jarvis_stats/cli.py +337 -0
- jarvis/jarvis_stats/stats.py +433 -0
- jarvis/jarvis_stats/storage.py +329 -0
- jarvis/jarvis_stats/visualizer.py +443 -0
- jarvis/jarvis_tools/cli/main.py +84 -15
- jarvis/jarvis_tools/registry.py +35 -16
- jarvis/jarvis_tools/search_web.py +3 -3
- jarvis/jarvis_tools/virtual_tty.py +315 -26
- jarvis/jarvis_utils/config.py +6 -0
- jarvis/jarvis_utils/git_utils.py +8 -16
- jarvis/jarvis_utils/utils.py +210 -37
- {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/METADATA +19 -2
- {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/RECORD +31 -26
- {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/entry_points.txt +2 -0
- {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/top_level.txt +0 -0
jarvis/__init__.py
CHANGED
@@ -55,6 +55,11 @@ class EditFileHandler(OutputHandler):
|
|
55
55
|
patches = self._parse_patches(response)
|
56
56
|
if not patches:
|
57
57
|
return False, "未找到有效的文件编辑指令"
|
58
|
+
|
59
|
+
# 记录 edit_file 工具调用统计
|
60
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
61
|
+
stats_manager = StatsManager()
|
62
|
+
stats_manager.increment("edit_file", group="tool")
|
58
63
|
|
59
64
|
results = []
|
60
65
|
|
jarvis/jarvis_agent/jarvis.py
CHANGED
@@ -24,7 +24,7 @@ from jarvis.jarvis_tools.registry import ToolRegistry
|
|
24
24
|
from jarvis.jarvis_utils.config import get_data_dir
|
25
25
|
from jarvis.jarvis_utils.utils import init_env
|
26
26
|
|
27
|
-
app = typer.Typer(help="Jarvis AI
|
27
|
+
app = typer.Typer(help="Jarvis AI 助手")
|
28
28
|
|
29
29
|
|
30
30
|
def _load_tasks() -> Dict[str, str]:
|
@@ -91,19 +91,13 @@ def _select_task(tasks: Dict[str, str]) -> str:
|
|
91
91
|
selected_task = tasks[task_names[choice - 1]]
|
92
92
|
PrettyOutput.print(f"将要执行任务:\n {selected_task}", OutputType.INFO)
|
93
93
|
# 询问是否需要补充信息
|
94
|
-
need_additional = user_confirm(
|
95
|
-
"需要为此任务添加补充信息吗?", default=False
|
96
|
-
)
|
94
|
+
need_additional = user_confirm("需要为此任务添加补充信息吗?", default=False)
|
97
95
|
if need_additional:
|
98
96
|
additional_input = get_multiline_input("请输入补充信息:")
|
99
97
|
if additional_input:
|
100
|
-
selected_task =
|
101
|
-
f"{selected_task}\n\n补充信息:\n{additional_input}"
|
102
|
-
)
|
98
|
+
selected_task = f"{selected_task}\n\n补充信息:\n{additional_input}"
|
103
99
|
return selected_task
|
104
|
-
PrettyOutput.print(
|
105
|
-
"无效的选择。请选择列表中的一个号码。", OutputType.WARNING
|
106
|
-
)
|
100
|
+
PrettyOutput.print("无效的选择。请选择列表中的一个号码。", OutputType.WARNING)
|
107
101
|
|
108
102
|
except (KeyboardInterrupt, EOFError):
|
109
103
|
return ""
|
@@ -121,7 +115,16 @@ def _handle_edit_mode(edit: bool, config_file: Optional[str]) -> None:
|
|
121
115
|
if config_file
|
122
116
|
else Path(os.path.expanduser("~/.jarvis/config.yaml"))
|
123
117
|
)
|
124
|
-
|
118
|
+
# 根据操作系统选择合适的编辑器
|
119
|
+
import platform
|
120
|
+
|
121
|
+
if platform.system() == "Windows":
|
122
|
+
# 优先级:终端工具 -> 代码编辑器 -> 通用文本编辑器
|
123
|
+
editors = ["nvim", "vim", "nano", "code", "notepad++", "notepad"]
|
124
|
+
else:
|
125
|
+
# 优先级:终端工具 -> 代码编辑器 -> 通用文本编辑器
|
126
|
+
editors = ["nvim", "vim", "vi", "nano", "emacs", "code", "gedit", "kate"]
|
127
|
+
|
125
128
|
editor = next((e for e in editors if shutil.which(e)), None)
|
126
129
|
|
127
130
|
if editor:
|
@@ -133,7 +136,7 @@ def _handle_edit_mode(edit: bool, config_file: Optional[str]) -> None:
|
|
133
136
|
raise typer.Exit(code=1)
|
134
137
|
else:
|
135
138
|
PrettyOutput.print(
|
136
|
-
"No suitable editor found
|
139
|
+
f"No suitable editor found. Tried: {', '.join(editors)}", OutputType.ERROR
|
137
140
|
)
|
138
141
|
raise typer.Exit(code=1)
|
139
142
|
|
@@ -186,30 +189,24 @@ def run_cli(
|
|
186
189
|
llm_type: str = typer.Option(
|
187
190
|
"normal",
|
188
191
|
"--llm_type",
|
189
|
-
help="LLM
|
190
|
-
),
|
191
|
-
task: Optional[str] = typer.Option(
|
192
|
-
None, "-t", "--task", help="Directly input task content from command line"
|
192
|
+
help="使用的LLM类型,可选值:'normal'(普通)或 'thinking'(思考模式)",
|
193
193
|
),
|
194
|
+
task: Optional[str] = typer.Option(None, "-t", "--task", help="从命令行直接输入任务内容"),
|
194
195
|
model_group: Optional[str] = typer.Option(
|
195
|
-
None, "--
|
196
|
-
),
|
197
|
-
config_file: Optional[str] = typer.Option(
|
198
|
-
None, "-f", "--config", help="Path to custom config file"
|
196
|
+
None, "--llm_group", help="使用的模型组,覆盖配置文件中的设置"
|
199
197
|
),
|
198
|
+
config_file: Optional[str] = typer.Option(None, "-f", "--config", help="自定义配置文件路径"),
|
200
199
|
restore_session: bool = typer.Option(
|
201
200
|
False,
|
202
201
|
"--restore-session",
|
203
|
-
help="
|
204
|
-
),
|
205
|
-
edit: bool = typer.Option(
|
206
|
-
False, "-e", "--edit", help="Edit the configuration file"
|
202
|
+
help="从 .jarvis/saved_session.json 恢复会话",
|
207
203
|
),
|
204
|
+
edit: bool = typer.Option(False, "-e", "--edit", help="编辑配置文件"),
|
208
205
|
) -> None:
|
209
206
|
"""Jarvis AI assistant command-line interface."""
|
210
207
|
if ctx.invoked_subcommand is not None:
|
211
208
|
return
|
212
|
-
|
209
|
+
|
213
210
|
_handle_edit_mode(edit, config_file)
|
214
211
|
|
215
212
|
init_env("欢迎使用 Jarvis AI 助手,您的智能助理已准备就绪!", config_file=config_file)
|
jarvis/jarvis_agent/main.py
CHANGED
@@ -10,7 +10,7 @@ from jarvis.jarvis_utils.input import get_multiline_input
|
|
10
10
|
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
11
11
|
from jarvis.jarvis_utils.utils import init_env
|
12
12
|
|
13
|
-
app = typer.Typer(help="Jarvis AI
|
13
|
+
app = typer.Typer(help="Jarvis AI 助手")
|
14
14
|
|
15
15
|
|
16
16
|
def load_config(config_path: str) -> dict:
|
@@ -40,21 +40,21 @@ def load_config(config_path: str) -> dict:
|
|
40
40
|
@app.command()
|
41
41
|
def cli(
|
42
42
|
config_file: Optional[str] = typer.Option(
|
43
|
-
None, "-f", "--config", help="
|
43
|
+
None, "-f", "--config", help="代理配置文件路径"
|
44
44
|
),
|
45
45
|
agent_definition: Optional[str] = typer.Option(
|
46
|
-
None, "-c", "--agent_definition", help="
|
46
|
+
None, "-c", "--agent_definition", help="代理定义文件路径"
|
47
47
|
),
|
48
48
|
task: Optional[str] = typer.Option(
|
49
|
-
None, "-t", "--task", help="
|
49
|
+
None, "-t", "--task", help="初始任务内容"
|
50
50
|
),
|
51
51
|
llm_type: str = typer.Option(
|
52
52
|
"normal",
|
53
53
|
"--llm_type",
|
54
|
-
help="LLM
|
54
|
+
help="使用的LLM类型,覆盖配置文件中的设置",
|
55
55
|
),
|
56
56
|
model_group: Optional[str] = typer.Option(
|
57
|
-
None, "--
|
57
|
+
None, "--llm_group", help="使用的模型组,覆盖配置文件中的设置"
|
58
58
|
),
|
59
59
|
):
|
60
60
|
"""Main entry point for Jarvis agent"""
|
@@ -38,7 +38,7 @@ from jarvis.jarvis_utils.input import get_multiline_input, user_confirm
|
|
38
38
|
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
39
39
|
from jarvis.jarvis_utils.utils import get_loc_stats, init_env
|
40
40
|
|
41
|
-
app = typer.Typer(help="Jarvis
|
41
|
+
app = typer.Typer(help="Jarvis 代码助手")
|
42
42
|
|
43
43
|
|
44
44
|
class CodeAgent:
|
@@ -54,6 +54,9 @@ class CodeAgent:
|
|
54
54
|
need_summary: bool = True,
|
55
55
|
):
|
56
56
|
self.root_dir = os.getcwd()
|
57
|
+
|
58
|
+
# 检测 git username 和 email 是否已设置
|
59
|
+
self._check_git_config()
|
57
60
|
tool_registry = ToolRegistry() # type: ignore
|
58
61
|
tool_registry.use_tools(
|
59
62
|
[
|
@@ -84,18 +87,18 @@ class CodeAgent:
|
|
84
87
|
1. **项目分析**:分析项目结构,确定需修改的文件
|
85
88
|
2. **需求分析**:理解需求意图,选择影响最小的实现方案
|
86
89
|
3. **代码分析**:详细分析目标文件,禁止虚构现有代码
|
87
|
-
-
|
88
|
-
-
|
89
|
-
-
|
90
|
+
- 结构分析:优先使用文件搜索工具快速定位文件和目录结构
|
91
|
+
- 内容搜索:优先使用全文搜索工具进行函数、类、变量等内容的搜索,避免遗漏
|
92
|
+
- 依赖关系:如需分析依赖、调用关系,可结合代码分析工具辅助
|
90
93
|
- 代码阅读:使用 read_code 工具获取目标文件的完整内容或指定范围内容,禁止凭空假设代码
|
91
|
-
-
|
94
|
+
- 变更影响:如需分析变更影响范围,可结合版本控制工具辅助判断
|
92
95
|
- 工具优先级:优先使用自动化工具,减少人工推断,确保分析结果准确
|
93
96
|
4. **方案设计**:确定最小变更方案,保持代码结构
|
94
97
|
5. **实施修改**:遵循"先读后写"原则,保持代码风格一致性
|
95
98
|
|
96
99
|
## 工具使用
|
97
|
-
-
|
98
|
-
-
|
100
|
+
- 项目结构:优先使用文件搜索命令查找文件
|
101
|
+
- 代码搜索:优先使用内容搜索工具
|
99
102
|
- 代码阅读:优先使用read_code工具
|
100
103
|
- 仅在命令行工具不足时使用专用工具
|
101
104
|
|
@@ -133,6 +136,52 @@ class CodeAgent:
|
|
133
136
|
|
134
137
|
self.agent.set_after_tool_call_cb(self.after_tool_call_cb)
|
135
138
|
|
139
|
+
def _check_git_config(self) -> None:
|
140
|
+
"""检查 git username 和 email 是否已设置,如果没有则提示并退出"""
|
141
|
+
try:
|
142
|
+
# 检查 git user.name
|
143
|
+
result = subprocess.run(
|
144
|
+
["git", "config", "--get", "user.name"],
|
145
|
+
capture_output=True,
|
146
|
+
text=True,
|
147
|
+
check=False,
|
148
|
+
)
|
149
|
+
username = result.stdout.strip()
|
150
|
+
|
151
|
+
# 检查 git user.email
|
152
|
+
result = subprocess.run(
|
153
|
+
["git", "config", "--get", "user.email"],
|
154
|
+
capture_output=True,
|
155
|
+
text=True,
|
156
|
+
check=False,
|
157
|
+
)
|
158
|
+
email = result.stdout.strip()
|
159
|
+
|
160
|
+
# 如果任一配置未设置,提示并退出
|
161
|
+
if not username or not email:
|
162
|
+
missing_configs = []
|
163
|
+
if not username:
|
164
|
+
missing_configs.append(
|
165
|
+
' git config --global user.name "Your Name"'
|
166
|
+
)
|
167
|
+
if not email:
|
168
|
+
missing_configs.append(
|
169
|
+
' git config --global user.email "your.email@example.com"'
|
170
|
+
)
|
171
|
+
|
172
|
+
message = "❌ Git 配置不完整\n\n请运行以下命令配置 Git:\n" + "\n".join(
|
173
|
+
missing_configs
|
174
|
+
)
|
175
|
+
PrettyOutput.print(message, OutputType.WARNING)
|
176
|
+
sys.exit(1)
|
177
|
+
|
178
|
+
except FileNotFoundError:
|
179
|
+
PrettyOutput.print("❌ 未找到 git 命令,请先安装 Git", OutputType.ERROR)
|
180
|
+
sys.exit(1)
|
181
|
+
except Exception as e:
|
182
|
+
PrettyOutput.print(f"❌ 检查 Git 配置时出错: {str(e)}", OutputType.ERROR)
|
183
|
+
sys.exit(1)
|
184
|
+
|
136
185
|
def _find_git_root(self) -> str:
|
137
186
|
"""查找并切换到git根目录
|
138
187
|
|
@@ -185,13 +234,138 @@ class CodeAgent:
|
|
185
234
|
1. 查找git根目录
|
186
235
|
2. 检查并更新.gitignore文件
|
187
236
|
3. 处理未提交的修改
|
237
|
+
4. 配置git对换行符变化不敏感
|
188
238
|
"""
|
189
239
|
print("🚀 正在初始化环境...")
|
190
240
|
git_dir = self._find_git_root()
|
191
241
|
self._update_gitignore(git_dir)
|
192
242
|
self._handle_git_changes()
|
243
|
+
# 配置git对换行符变化不敏感
|
244
|
+
self._configure_line_ending_settings()
|
193
245
|
print("✅ 环境初始化完成")
|
194
246
|
|
247
|
+
def _configure_line_ending_settings(self) -> None:
|
248
|
+
"""配置git对换行符变化不敏感,只在当前设置与目标设置不一致时修改"""
|
249
|
+
target_settings = {
|
250
|
+
"core.autocrlf": "false",
|
251
|
+
"core.safecrlf": "false",
|
252
|
+
"core.whitespace": "cr-at-eol", # 忽略行尾的CR
|
253
|
+
}
|
254
|
+
|
255
|
+
# 获取当前设置并检查是否需要修改
|
256
|
+
need_change = False
|
257
|
+
current_settings = {}
|
258
|
+
for key, target_value in target_settings.items():
|
259
|
+
result = subprocess.run(
|
260
|
+
["git", "config", "--get", key], capture_output=True, text=True
|
261
|
+
)
|
262
|
+
current_value = result.stdout.strip()
|
263
|
+
current_settings[key] = current_value
|
264
|
+
if current_value != target_value:
|
265
|
+
need_change = True
|
266
|
+
|
267
|
+
if not need_change:
|
268
|
+
print("✅ git换行符敏感设置已符合要求")
|
269
|
+
return
|
270
|
+
|
271
|
+
PrettyOutput.print(
|
272
|
+
"⚠️ 即将修改git换行符敏感设置,这会影响所有文件的换行符处理方式",
|
273
|
+
OutputType.WARNING,
|
274
|
+
)
|
275
|
+
print("将进行以下设置:")
|
276
|
+
for key, value in target_settings.items():
|
277
|
+
current = current_settings.get(key, "未设置")
|
278
|
+
print(f" {key}: {current} -> {value}")
|
279
|
+
|
280
|
+
if user_confirm("是否继续修改git换行符敏感设置?", True):
|
281
|
+
for key, value in target_settings.items():
|
282
|
+
subprocess.run(["git", "config", key, value], check=True)
|
283
|
+
|
284
|
+
# 对于Windows系统,提示用户可以创建.gitattributes文件
|
285
|
+
if sys.platform.startswith("win"):
|
286
|
+
self._handle_windows_line_endings()
|
287
|
+
|
288
|
+
print("✅ git换行符敏感设置已更新")
|
289
|
+
else:
|
290
|
+
print("❌ 用户取消修改git换行符敏感设置")
|
291
|
+
sys.exit(0)
|
292
|
+
|
293
|
+
def _handle_windows_line_endings(self) -> None:
|
294
|
+
"""在Windows系统上处理换行符问题,提供建议而非强制修改"""
|
295
|
+
gitattributes_path = os.path.join(self.root_dir, ".gitattributes")
|
296
|
+
|
297
|
+
# 检查是否已存在.gitattributes文件
|
298
|
+
if os.path.exists(gitattributes_path):
|
299
|
+
with open(gitattributes_path, "r", encoding="utf-8") as f:
|
300
|
+
content = f.read()
|
301
|
+
# 如果已经有换行符相关配置,就不再提示
|
302
|
+
if any(keyword in content for keyword in ["text=", "eol=", "binary"]):
|
303
|
+
return
|
304
|
+
|
305
|
+
print(
|
306
|
+
"\n💡 提示:在Windows系统上,建议配置.gitattributes文件来避免换行符问题。"
|
307
|
+
)
|
308
|
+
print("这可以防止仅因换行符不同而导致整个文件被标记为修改。")
|
309
|
+
|
310
|
+
if user_confirm("是否要创建一个最小化的.gitattributes文件?", False):
|
311
|
+
# 最小化的内容,只影响特定类型的文件
|
312
|
+
minimal_content = """# Jarvis建议的最小化换行符配置
|
313
|
+
# 默认所有文本文件使用LF,只有Windows特定文件使用CRLF
|
314
|
+
|
315
|
+
# 默认所有文本文件使用LF
|
316
|
+
* text=auto eol=lf
|
317
|
+
|
318
|
+
# Windows批处理文件需要CRLF
|
319
|
+
*.bat text eol=crlf
|
320
|
+
*.cmd text eol=crlf
|
321
|
+
*.ps1 text eol=crlf
|
322
|
+
"""
|
323
|
+
|
324
|
+
if not os.path.exists(gitattributes_path):
|
325
|
+
with open(gitattributes_path, "w", encoding="utf-8", newline="\n") as f:
|
326
|
+
f.write(minimal_content)
|
327
|
+
print("✅ 已创建最小化的.gitattributes文件")
|
328
|
+
else:
|
329
|
+
print("📝 将以下内容追加到现有.gitattributes文件:")
|
330
|
+
print(minimal_content)
|
331
|
+
if user_confirm("是否追加到现有文件?", True):
|
332
|
+
with open(
|
333
|
+
gitattributes_path, "a", encoding="utf-8", newline="\n"
|
334
|
+
) as f:
|
335
|
+
f.write("\n" + minimal_content)
|
336
|
+
print("✅ 已更新.gitattributes文件")
|
337
|
+
else:
|
338
|
+
print(
|
339
|
+
"ℹ️ 跳过.gitattributes文件创建。如果遇到换行符问题,可以手动创建此文件。"
|
340
|
+
)
|
341
|
+
|
342
|
+
def _record_code_changes_stats(self, diff_text: str) -> None:
|
343
|
+
"""记录代码变更的统计信息。
|
344
|
+
|
345
|
+
Args:
|
346
|
+
diff_text: git diff的文本输出
|
347
|
+
"""
|
348
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
349
|
+
import re
|
350
|
+
|
351
|
+
stats_manager = StatsManager()
|
352
|
+
|
353
|
+
# 匹配插入行数
|
354
|
+
insertions_match = re.search(r"(\d+)\s+insertions?\(\+\)", diff_text)
|
355
|
+
if insertions_match:
|
356
|
+
insertions = int(insertions_match.group(1))
|
357
|
+
stats_manager.increment(
|
358
|
+
"code_lines_inserted", amount=insertions, group="code_agent"
|
359
|
+
)
|
360
|
+
|
361
|
+
# 匹配删除行数
|
362
|
+
deletions_match = re.search(r"(\d+)\s+deletions?\(\-\)", diff_text)
|
363
|
+
if deletions_match:
|
364
|
+
deletions = int(deletions_match.group(1))
|
365
|
+
stats_manager.increment(
|
366
|
+
"code_lines_deleted", amount=deletions, group="code_agent"
|
367
|
+
)
|
368
|
+
|
195
369
|
def _handle_uncommitted_changes(self) -> None:
|
196
370
|
"""处理未提交的修改,包括:
|
197
371
|
1. 提示用户确认是否提交
|
@@ -201,10 +375,31 @@ class CodeAgent:
|
|
201
375
|
5. 暂存并提交所有修改
|
202
376
|
"""
|
203
377
|
if has_uncommitted_changes():
|
378
|
+
# 获取代码变更统计
|
379
|
+
try:
|
380
|
+
diff_result = subprocess.run(
|
381
|
+
["git", "diff", "HEAD", "--shortstat"],
|
382
|
+
capture_output=True,
|
383
|
+
text=True,
|
384
|
+
encoding="utf-8",
|
385
|
+
errors="replace",
|
386
|
+
check=True,
|
387
|
+
)
|
388
|
+
if diff_result.returncode == 0 and diff_result.stdout:
|
389
|
+
self._record_code_changes_stats(diff_result.stdout)
|
390
|
+
except subprocess.CalledProcessError:
|
391
|
+
pass
|
392
|
+
|
204
393
|
PrettyOutput.print("检测到未提交的修改,是否要提交?", OutputType.WARNING)
|
205
394
|
if not user_confirm("是否要提交?", True):
|
206
395
|
return
|
207
396
|
|
397
|
+
# 用户确认修改,统计修改次数
|
398
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
399
|
+
|
400
|
+
stats_manager = StatsManager()
|
401
|
+
stats_manager.increment("code_modification_confirmed", group="code_agent")
|
402
|
+
|
208
403
|
try:
|
209
404
|
confirm_add_new_files()
|
210
405
|
|
@@ -216,6 +411,8 @@ class CodeAgent:
|
|
216
411
|
["git", "rev-list", "--count", "HEAD"],
|
217
412
|
capture_output=True,
|
218
413
|
text=True,
|
414
|
+
encoding="utf-8",
|
415
|
+
errors="replace",
|
219
416
|
check=True,
|
220
417
|
)
|
221
418
|
if commit_result.returncode != 0:
|
@@ -231,6 +428,9 @@ class CodeAgent:
|
|
231
428
|
["git", "commit", "-m", f"CheckPoint #{commit_count + 1}"],
|
232
429
|
check=True,
|
233
430
|
)
|
431
|
+
|
432
|
+
# 统计提交次数
|
433
|
+
stats_manager.increment("code_commits_accepted", group="code_agent")
|
234
434
|
except subprocess.CalledProcessError as e:
|
235
435
|
PrettyOutput.print(f"提交失败: {str(e)}", OutputType.ERROR)
|
236
436
|
|
@@ -252,6 +452,12 @@ class CodeAgent:
|
|
252
452
|
commits = []
|
253
453
|
|
254
454
|
if commits:
|
455
|
+
# 统计生成的commit数量
|
456
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
457
|
+
|
458
|
+
stats_manager = StatsManager()
|
459
|
+
stats_manager.increment("commits_generated", group="code_agent")
|
460
|
+
|
255
461
|
commit_messages = "检测到以下提交记录:\n" + "\n".join(
|
256
462
|
f"- {commit_hash[:7]}: {message}" for commit_hash, message in commits
|
257
463
|
)
|
@@ -263,6 +469,12 @@ class CodeAgent:
|
|
263
469
|
) -> None:
|
264
470
|
"""处理提交确认和可能的重置"""
|
265
471
|
if commits and user_confirm("是否接受以上提交记录?", True):
|
472
|
+
# 统计接受的commit数量
|
473
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
474
|
+
|
475
|
+
stats_manager = StatsManager()
|
476
|
+
stats_manager.increment("commits_accepted", group="code_agent")
|
477
|
+
|
266
478
|
subprocess.run(
|
267
479
|
["git", "reset", "--mixed", str(start_commit)],
|
268
480
|
stdout=subprocess.DEVNULL,
|
@@ -350,6 +562,28 @@ class CodeAgent:
|
|
350
562
|
modified_files = get_diff_file_list()
|
351
563
|
commited = handle_commit_workflow()
|
352
564
|
if commited:
|
565
|
+
# 统计代码行数变化
|
566
|
+
# 获取diff的统计信息
|
567
|
+
try:
|
568
|
+
diff_result = subprocess.run(
|
569
|
+
["git", "diff", "HEAD~1", "HEAD", "--shortstat"],
|
570
|
+
capture_output=True,
|
571
|
+
text=True,
|
572
|
+
encoding="utf-8",
|
573
|
+
errors="replace",
|
574
|
+
check=True,
|
575
|
+
)
|
576
|
+
if diff_result.returncode == 0 and diff_result.stdout:
|
577
|
+
self._record_code_changes_stats(diff_result.stdout)
|
578
|
+
except subprocess.CalledProcessError:
|
579
|
+
pass
|
580
|
+
|
581
|
+
# 统计修改次数
|
582
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
583
|
+
|
584
|
+
stats_manager = StatsManager()
|
585
|
+
stats_manager.increment("code_modifications", group="code_agent")
|
586
|
+
|
353
587
|
# 获取提交信息
|
354
588
|
end_hash = get_latest_commit_hash()
|
355
589
|
commits = get_commits_between(start_hash, end_hash)
|
@@ -409,23 +643,57 @@ def cli(
|
|
409
643
|
llm_type: str = typer.Option(
|
410
644
|
"normal",
|
411
645
|
"--llm_type",
|
412
|
-
help="LLM
|
646
|
+
help="使用的LLM类型,可选值:'normal'(普通)或 'thinking'(思考模式)",
|
413
647
|
),
|
414
648
|
model_group: Optional[str] = typer.Option(
|
415
|
-
None, "--
|
649
|
+
None, "--llm_group", help="使用的模型组,覆盖配置文件中的设置"
|
416
650
|
),
|
417
651
|
requirement: Optional[str] = typer.Option(
|
418
|
-
None, "-r", "--requirement", help="
|
652
|
+
None, "-r", "--requirement", help="要处理的需求描述"
|
419
653
|
),
|
420
654
|
restore_session: bool = typer.Option(
|
421
655
|
False,
|
422
656
|
"--restore-session",
|
423
|
-
help="
|
657
|
+
help="从 .jarvis/saved_session.json 恢复会话状态",
|
424
658
|
),
|
425
659
|
) -> None:
|
426
660
|
"""Jarvis主入口点。"""
|
427
661
|
init_env("欢迎使用 Jarvis-CodeAgent,您的代码工程助手已准备就绪!")
|
428
662
|
|
663
|
+
try:
|
664
|
+
subprocess.run(
|
665
|
+
["git", "rev-parse", "--git-dir"],
|
666
|
+
check=True,
|
667
|
+
stdout=subprocess.DEVNULL,
|
668
|
+
stderr=subprocess.DEVNULL,
|
669
|
+
)
|
670
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
671
|
+
curr_dir_path = os.getcwd()
|
672
|
+
PrettyOutput.print(
|
673
|
+
f"警告:当前目录 '{curr_dir_path}' 不是一个git仓库。", OutputType.WARNING
|
674
|
+
)
|
675
|
+
if user_confirm(
|
676
|
+
f"是否要在 '{curr_dir_path}' 中初始化一个新的git仓库?", default=True
|
677
|
+
):
|
678
|
+
try:
|
679
|
+
subprocess.run(
|
680
|
+
["git", "init"],
|
681
|
+
check=True,
|
682
|
+
capture_output=True,
|
683
|
+
text=True,
|
684
|
+
encoding="utf-8",
|
685
|
+
errors="replace",
|
686
|
+
)
|
687
|
+
PrettyOutput.print("✅ 已成功初始化git仓库。", OutputType.SUCCESS)
|
688
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
689
|
+
PrettyOutput.print(f"❌ 初始化git仓库失败: {e}", OutputType.ERROR)
|
690
|
+
sys.exit(1)
|
691
|
+
else:
|
692
|
+
PrettyOutput.print(
|
693
|
+
"操作已取消。Jarvis需要在git仓库中运行。", OutputType.INFO
|
694
|
+
)
|
695
|
+
sys.exit(0)
|
696
|
+
|
429
697
|
curr_dir = os.getcwd()
|
430
698
|
git_dir = find_git_root_and_cd(curr_dir)
|
431
699
|
PrettyOutput.print(f"当前目录: {git_dir}", OutputType.INFO)
|
@@ -17,7 +17,7 @@ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
|
17
17
|
from jarvis.jarvis_utils.tag import ct, ot
|
18
18
|
from jarvis.jarvis_utils.utils import init_env, is_context_overflow
|
19
19
|
|
20
|
-
app = typer.Typer(help="
|
20
|
+
app = typer.Typer(help="自动代码审查工具")
|
21
21
|
|
22
22
|
|
23
23
|
class CodeReviewTool:
|
@@ -300,7 +300,7 @@ class CodeReviewTool:
|
|
300
300
|
|
301
301
|
# Execute git command and get diff output
|
302
302
|
diff_output = subprocess.check_output(
|
303
|
-
diff_cmd, shell=True, text=True
|
303
|
+
diff_cmd, shell=True, text=True, encoding="utf-8", errors="replace"
|
304
304
|
)
|
305
305
|
if not diff_output:
|
306
306
|
return {
|
@@ -310,11 +310,13 @@ class CodeReviewTool:
|
|
310
310
|
}
|
311
311
|
|
312
312
|
# Extract changed files using git command
|
313
|
-
|
313
|
+
# Use git show with proper formatting to avoid needing grep
|
314
|
+
files_cmd = f"git show --name-only --pretty=format: {commit_sha}"
|
314
315
|
try:
|
315
316
|
files_output = subprocess.check_output(
|
316
317
|
files_cmd, shell=True, text=True
|
317
318
|
)
|
319
|
+
# Filter out empty lines without using grep
|
318
320
|
file_paths = [
|
319
321
|
f.strip() for f in files_output.split("\n") if f.strip()
|
320
322
|
]
|
@@ -337,7 +339,7 @@ class CodeReviewTool:
|
|
337
339
|
|
338
340
|
# Execute git command and get diff output
|
339
341
|
diff_output = subprocess.check_output(
|
340
|
-
diff_cmd, shell=True, text=True
|
342
|
+
diff_cmd, shell=True, text=True, encoding="utf-8", errors="replace"
|
341
343
|
)
|
342
344
|
if not diff_output:
|
343
345
|
return {
|
@@ -379,7 +381,7 @@ class CodeReviewTool:
|
|
379
381
|
|
380
382
|
# Execute git command and get diff output
|
381
383
|
diff_output = subprocess.check_output(
|
382
|
-
diff_cmd, shell=True, text=True
|
384
|
+
diff_cmd, shell=True, text=True, encoding="utf-8", errors="replace"
|
383
385
|
)
|
384
386
|
if not diff_output:
|
385
387
|
return {
|
@@ -578,7 +580,7 @@ class CodeReviewTool:
|
|
578
580
|
|
579
581
|
tool_registry = ToolRegistry()
|
580
582
|
tool_registry.dont_use_tools(["code_review"])
|
581
|
-
|
583
|
+
|
582
584
|
# Use the provided agent's model_group or get it from globals
|
583
585
|
calling_agent = agent or get_agent(current_agent_name)
|
584
586
|
model_group = None
|
@@ -653,7 +655,7 @@ class CodeReviewTool:
|
|
653
655
|
{ot("REPORT")}
|
654
656
|
[在此处插入完整MARKDOWN格式的审查报告]
|
655
657
|
{ct("REPORT")}""",
|
656
|
-
output_handler=[tool_registry],
|
658
|
+
output_handler=[tool_registry], # type: ignore
|
657
659
|
llm_type="thinking",
|
658
660
|
auto_complete=False,
|
659
661
|
)
|
@@ -769,10 +771,10 @@ def extract_code_report(result: str) -> str:
|
|
769
771
|
|
770
772
|
@app.command("commit")
|
771
773
|
def review_commit(
|
772
|
-
commit: str = typer.Argument(..., help="
|
773
|
-
root_dir: str = typer.Option(".", "--root-dir", help="
|
774
|
+
commit: str = typer.Argument(..., help="要审查的提交SHA"),
|
775
|
+
root_dir: str = typer.Option(".", "--root-dir", help="代码库根目录路径"),
|
774
776
|
):
|
775
|
-
"""
|
777
|
+
"""审查指定的提交"""
|
776
778
|
tool = CodeReviewTool()
|
777
779
|
tool_args = {"review_type": "commit", "commit_sha": commit, "root_dir": root_dir}
|
778
780
|
result = tool.execute(tool_args)
|
@@ -786,9 +788,9 @@ def review_commit(
|
|
786
788
|
|
787
789
|
@app.command("current")
|
788
790
|
def review_current(
|
789
|
-
root_dir: str = typer.Option(".", "--root-dir", help="
|
791
|
+
root_dir: str = typer.Option(".", "--root-dir", help="代码库根目录路径"),
|
790
792
|
):
|
791
|
-
"""
|
793
|
+
"""审查当前的变更"""
|
792
794
|
tool = CodeReviewTool()
|
793
795
|
tool_args = {"review_type": "current", "root_dir": root_dir}
|
794
796
|
result = tool.execute(tool_args)
|
@@ -802,11 +804,11 @@ def review_current(
|
|
802
804
|
|
803
805
|
@app.command("range")
|
804
806
|
def review_range(
|
805
|
-
start_commit: str = typer.Argument(..., help="
|
806
|
-
end_commit: str = typer.Argument(..., help="
|
807
|
-
root_dir: str = typer.Option(".", "--root-dir", help="
|
807
|
+
start_commit: str = typer.Argument(..., help="起始提交SHA"),
|
808
|
+
end_commit: str = typer.Argument(..., help="结束提交SHA"),
|
809
|
+
root_dir: str = typer.Option(".", "--root-dir", help="代码库根目录路径"),
|
808
810
|
):
|
809
|
-
"""
|
811
|
+
"""审查提交范围"""
|
810
812
|
tool = CodeReviewTool()
|
811
813
|
tool_args = {
|
812
814
|
"review_type": "range",
|
@@ -825,10 +827,10 @@ def review_range(
|
|
825
827
|
|
826
828
|
@app.command("file")
|
827
829
|
def review_file(
|
828
|
-
file: str = typer.Argument(..., help="
|
829
|
-
root_dir: str = typer.Option(".", "--root-dir", help="
|
830
|
+
file: str = typer.Argument(..., help="要审查的文件路径"),
|
831
|
+
root_dir: str = typer.Option(".", "--root-dir", help="代码库根目录路径"),
|
830
832
|
):
|
831
|
-
"""
|
833
|
+
"""审查指定的文件"""
|
832
834
|
tool = CodeReviewTool()
|
833
835
|
tool_args = {"review_type": "file", "file_path": file, "root_dir": root_dir}
|
834
836
|
result = tool.execute(tool_args)
|