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.
Files changed (31) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/edit_file_handler.py +5 -0
  3. jarvis/jarvis_agent/jarvis.py +22 -25
  4. jarvis/jarvis_agent/main.py +6 -6
  5. jarvis/jarvis_code_agent/code_agent.py +279 -11
  6. jarvis/jarvis_code_analysis/code_review.py +21 -19
  7. jarvis/jarvis_data/config_schema.json +23 -10
  8. jarvis/jarvis_git_squash/main.py +3 -3
  9. jarvis/jarvis_git_utils/git_commiter.py +32 -11
  10. jarvis/jarvis_mcp/sse_mcp_client.py +4 -6
  11. jarvis/jarvis_mcp/streamable_mcp_client.py +5 -9
  12. jarvis/jarvis_rag/retriever.py +1 -1
  13. jarvis/jarvis_smart_shell/main.py +2 -2
  14. jarvis/jarvis_stats/__init__.py +13 -0
  15. jarvis/jarvis_stats/cli.py +337 -0
  16. jarvis/jarvis_stats/stats.py +433 -0
  17. jarvis/jarvis_stats/storage.py +329 -0
  18. jarvis/jarvis_stats/visualizer.py +443 -0
  19. jarvis/jarvis_tools/cli/main.py +84 -15
  20. jarvis/jarvis_tools/registry.py +35 -16
  21. jarvis/jarvis_tools/search_web.py +3 -3
  22. jarvis/jarvis_tools/virtual_tty.py +315 -26
  23. jarvis/jarvis_utils/config.py +6 -0
  24. jarvis/jarvis_utils/git_utils.py +8 -16
  25. jarvis/jarvis_utils/utils.py +210 -37
  26. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/METADATA +19 -2
  27. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/RECORD +31 -26
  28. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/entry_points.txt +2 -0
  29. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/WHEEL +0 -0
  30. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/licenses/LICENSE +0 -0
  31. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.4.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,30 @@
1
1
  # -*- coding: utf-8 -*-
2
- import fcntl
3
2
  import os
4
- import pty
5
- import select
6
- import signal
3
+ import sys
7
4
  import time
8
- from typing import Any, Dict
5
+ from typing import Any, Dict, TYPE_CHECKING
6
+
7
+ # 为了类型检查,总是导入这些模块
8
+ if TYPE_CHECKING:
9
+ import fcntl
10
+ import pty
11
+ import select
12
+ import signal
13
+ import subprocess
14
+ import threading
15
+ import queue
16
+
17
+ # 平台相关的导入
18
+ if sys.platform != "win32":
19
+ import fcntl
20
+ import pty
21
+ import select
22
+ import signal
23
+ else:
24
+ # Windows平台的导入
25
+ import subprocess
26
+ import threading
27
+ import queue
9
28
 
10
29
 
11
30
  class VirtualTTYTool:
@@ -14,6 +33,7 @@ class VirtualTTYTool:
14
33
  "控制虚拟终端执行各种操作,如启动终端、输入命令、获取输出等。"
15
34
  + "与execute_script不同,此工具会创建一个持久的虚拟终端会话,可以连续执行多个命令,并保持终端状态。"
16
35
  + "适用于需要交互式操作的场景,如运行需要用户输入的交互式程序(如:ssh连接、sftp传输、gdb/dlv调试等)。"
36
+ + "注意:Windows平台功能有限,某些Unix特有功能可能不可用。"
17
37
  )
18
38
  parameters = {
19
39
  "type": "object",
@@ -76,11 +96,21 @@ class VirtualTTYTool:
76
96
 
77
97
  # 如果指定的tty_id不存在,为其创建一个新的tty_data
78
98
  if tty_id not in agent.tty_sessions:
79
- agent.tty_sessions[tty_id] = {
80
- "master_fd": None,
81
- "pid": None,
82
- "shell": "/bin/bash",
83
- }
99
+ if sys.platform == "win32":
100
+ import queue as _queue # pylint: disable=import-outside-toplevel
101
+
102
+ agent.tty_sessions[tty_id] = {
103
+ "process": None,
104
+ "output_queue": _queue.Queue(),
105
+ "output_thread": None,
106
+ "shell": "cmd.exe",
107
+ }
108
+ else:
109
+ agent.tty_sessions[tty_id] = {
110
+ "master_fd": None,
111
+ "pid": None,
112
+ "shell": "/bin/bash",
113
+ }
84
114
 
85
115
  action = args.get("action", "").strip().lower()
86
116
 
@@ -164,13 +194,25 @@ class VirtualTTYTool:
164
194
 
165
195
  def _launch_tty(self, agent: Any, tty_id: str) -> Dict[str, Any]:
166
196
  """启动虚拟终端"""
197
+ if sys.platform == "win32":
198
+ return self._launch_tty_windows(agent, tty_id)
199
+ else:
200
+ return self._launch_tty_unix(agent, tty_id)
201
+
202
+ def _launch_tty_unix(self, agent: Any, tty_id: str) -> Dict[str, Any]:
203
+ """Unix/Linux平台启动虚拟终端"""
167
204
  try:
168
205
  # 如果该ID的终端已经启动,先关闭它
169
206
  if agent.tty_sessions[tty_id]["master_fd"] is not None:
170
207
  self._close_tty(agent, tty_id)
171
208
 
209
+ # 在Unix平台上导入需要的模块
210
+ import pty as _pty # pylint: disable=import-outside-toplevel
211
+ import fcntl as _fcntl # pylint: disable=import-outside-toplevel
212
+ import select as _select # pylint: disable=import-outside-toplevel
213
+
172
214
  # 创建伪终端
173
- pid, master_fd = pty.fork()
215
+ pid, master_fd = _pty.fork()
174
216
 
175
217
  if pid == 0: # 子进程
176
218
  # 执行shell
@@ -180,7 +222,7 @@ class VirtualTTYTool:
180
222
  )
181
223
  else: # 父进程
182
224
  # 设置非阻塞模式
183
- fcntl.fcntl(master_fd, fcntl.F_SETFL, os.O_NONBLOCK)
225
+ _fcntl.fcntl(master_fd, _fcntl.F_SETFL, os.O_NONBLOCK)
184
226
 
185
227
  # 保存终端状态
186
228
  agent.tty_sessions[tty_id]["master_fd"] = master_fd
@@ -191,7 +233,7 @@ class VirtualTTYTool:
191
233
  start_time = time.time()
192
234
  while time.time() - start_time < 2.0: # 最多等待2秒
193
235
  try:
194
- r, _, _ = select.select([master_fd], [], [], 0.1)
236
+ r, _, _ = _select.select([master_fd], [], [], 0.1)
195
237
  if r:
196
238
  data = os.read(master_fd, 1024)
197
239
  if data:
@@ -211,6 +253,74 @@ class VirtualTTYTool:
211
253
  "stderr": f"启动虚拟终端 [{tty_id}] 失败: {str(e)}",
212
254
  }
213
255
 
256
+ def _launch_tty_windows(self, agent: Any, tty_id: str) -> Dict[str, Any]:
257
+ """Windows平台启动虚拟终端"""
258
+ try:
259
+ # 如果该ID的终端已经启动,先关闭它
260
+ if agent.tty_sessions[tty_id]["process"] is not None:
261
+ self._close_tty(agent, tty_id)
262
+
263
+ # 在Windows平台上导入需要的模块
264
+ import subprocess as _subprocess # pylint: disable=import-outside-toplevel
265
+ import threading as _threading # pylint: disable=import-outside-toplevel
266
+ import queue as _queue # pylint: disable=import-outside-toplevel
267
+
268
+ # 创建子进程
269
+ process = _subprocess.Popen(
270
+ agent.tty_sessions[tty_id]["shell"],
271
+ stdin=_subprocess.PIPE,
272
+ stdout=_subprocess.PIPE,
273
+ stderr=_subprocess.STDOUT,
274
+ shell=True,
275
+ text=True,
276
+ bufsize=0,
277
+ encoding="utf-8",
278
+ errors="replace",
279
+ )
280
+
281
+ # 保存进程对象
282
+ agent.tty_sessions[tty_id]["process"] = process
283
+
284
+ # 创建输出读取线程
285
+ def read_output():
286
+ while True:
287
+ if process is None or process.poll() is not None:
288
+ break
289
+ try:
290
+ if process.stdout is None:
291
+ break
292
+ line = process.stdout.readline()
293
+ if line:
294
+ agent.tty_sessions[tty_id]["output_queue"].put(line)
295
+ except:
296
+ break
297
+
298
+ output_thread = _threading.Thread(target=read_output, daemon=True)
299
+ output_thread.start()
300
+ agent.tty_sessions[tty_id]["output_thread"] = output_thread
301
+
302
+ # 读取初始输出
303
+ output = ""
304
+ start_time = time.time()
305
+ while time.time() - start_time < 2.0: # 最多等待2秒
306
+ try:
307
+ line = agent.tty_sessions[tty_id]["output_queue"].get(timeout=0.1)
308
+ output += line
309
+ except _queue.Empty:
310
+ continue
311
+
312
+ if output:
313
+ print(f"📤 终端 [{tty_id}]: {output}")
314
+
315
+ return {"success": True, "stdout": output, "stderr": ""}
316
+
317
+ except Exception as e:
318
+ return {
319
+ "success": False,
320
+ "stdout": "",
321
+ "stderr": f"启动虚拟终端 [{tty_id}] 失败: {str(e)}",
322
+ }
323
+
214
324
  def _input_command(
215
325
  self,
216
326
  agent: Any,
@@ -225,6 +335,22 @@ class VirtualTTYTool:
225
335
  command: 要输入的单行命令
226
336
  add_enter: 是否在命令末尾添加回车符
227
337
  """
338
+ if sys.platform == "win32":
339
+ return self._input_command_windows(
340
+ agent, tty_id, command, timeout, add_enter
341
+ )
342
+ else:
343
+ return self._input_command_unix(agent, tty_id, command, timeout, add_enter)
344
+
345
+ def _input_command_unix(
346
+ self,
347
+ agent: Any,
348
+ tty_id: str,
349
+ command: str,
350
+ timeout: float,
351
+ add_enter: bool = True,
352
+ ) -> Dict[str, Any]:
353
+ """Unix/Linux平台输入命令"""
228
354
  if agent.tty_sessions[tty_id]["master_fd"] is None:
229
355
  return {
230
356
  "success": False,
@@ -251,7 +377,9 @@ class VirtualTTYTool:
251
377
  while time.time() - start_time < timeout:
252
378
  try:
253
379
  # 使用select等待数据可读
254
- r, _, _ = select.select(
380
+ import select as _select # pylint: disable=import-outside-toplevel
381
+
382
+ r, _, _ = _select.select(
255
383
  [agent.tty_sessions[tty_id]["master_fd"]], [], [], 0.1
256
384
  )
257
385
  if r:
@@ -270,10 +398,68 @@ class VirtualTTYTool:
270
398
  "stderr": f"在终端 [{tty_id}] 执行命令失败: {str(e)}",
271
399
  }
272
400
 
401
+ def _input_command_windows(
402
+ self,
403
+ agent: Any,
404
+ tty_id: str,
405
+ command: str,
406
+ timeout: float,
407
+ add_enter: bool = True,
408
+ ) -> Dict[str, Any]:
409
+ """Windows平台输入命令"""
410
+ if agent.tty_sessions[tty_id]["process"] is None:
411
+ return {
412
+ "success": False,
413
+ "stdout": "",
414
+ "stderr": f"虚拟终端 [{tty_id}] 未启动",
415
+ }
416
+
417
+ # 严格检查并拒绝多行输入
418
+ if "\n" in command:
419
+ return {"success": False, "stdout": "", "stderr": "错误:禁止多行输入"}
420
+
421
+ try:
422
+ # 根据add_enter参数决定是否添加回车符
423
+ if add_enter:
424
+ command = command + "\n"
425
+
426
+ # 发送命令
427
+ agent.tty_sessions[tty_id]["process"].stdin.write(command)
428
+ agent.tty_sessions[tty_id]["process"].stdin.flush()
429
+
430
+ # 等待输出
431
+ output = ""
432
+ start_time = time.time()
433
+ while time.time() - start_time < timeout:
434
+ try:
435
+ line = agent.tty_sessions[tty_id]["output_queue"].get(timeout=0.1)
436
+ output += line
437
+ except Exception: # queue.Empty
438
+ continue
439
+
440
+ print(f"📤 终端 [{tty_id}]: {output}")
441
+ return {"success": True, "stdout": output, "stderr": ""}
442
+
443
+ except Exception as e:
444
+ return {
445
+ "success": False,
446
+ "stdout": "",
447
+ "stderr": f"在终端 [{tty_id}] 执行命令失败: {str(e)}",
448
+ }
449
+
273
450
  def _get_output(
274
451
  self, agent: Any, tty_id: str, timeout: float = 5.0
275
452
  ) -> Dict[str, Any]:
276
453
  """获取终端输出"""
454
+ if sys.platform == "win32":
455
+ return self._get_output_windows(agent, tty_id, timeout)
456
+ else:
457
+ return self._get_output_unix(agent, tty_id, timeout)
458
+
459
+ def _get_output_unix(
460
+ self, agent: Any, tty_id: str, timeout: float = 5.0
461
+ ) -> Dict[str, Any]:
462
+ """Unix/Linux平台获取输出"""
277
463
  if agent.tty_sessions[tty_id]["master_fd"] is None:
278
464
  return {
279
465
  "success": False,
@@ -287,7 +473,9 @@ class VirtualTTYTool:
287
473
 
288
474
  while time.time() - start_time < timeout:
289
475
  # 使用select等待数据可读
290
- r, _, _ = select.select(
476
+ import select as _select # pylint: disable=import-outside-toplevel
477
+
478
+ r, _, _ = _select.select(
291
479
  [agent.tty_sessions[tty_id]["master_fd"]], [], [], 0.1
292
480
  )
293
481
  if r:
@@ -313,8 +501,47 @@ class VirtualTTYTool:
313
501
  "stderr": f"获取终端 [{tty_id}] 输出失败: {str(e)}",
314
502
  }
315
503
 
504
+ def _get_output_windows(
505
+ self, agent: Any, tty_id: str, timeout: float = 5.0
506
+ ) -> Dict[str, Any]:
507
+ """Windows平台获取输出"""
508
+ if agent.tty_sessions[tty_id]["process"] is None:
509
+ return {
510
+ "success": False,
511
+ "stdout": "",
512
+ "stderr": f"虚拟终端 [{tty_id}] 未启动",
513
+ }
514
+
515
+ try:
516
+ output = ""
517
+ start_time = time.time()
518
+
519
+ while time.time() - start_time < timeout:
520
+ try:
521
+ line = agent.tty_sessions[tty_id]["output_queue"].get(timeout=0.1)
522
+ output += line
523
+ except Exception: # queue.Empty
524
+ continue
525
+
526
+ print(f"📤 终端 [{tty_id}]: {output}")
527
+ return {"success": True, "stdout": output, "stderr": ""}
528
+
529
+ except Exception as e:
530
+ return {
531
+ "success": False,
532
+ "stdout": "",
533
+ "stderr": f"获取终端 [{tty_id}] 输出失败: {str(e)}",
534
+ }
535
+
316
536
  def _close_tty(self, agent: Any, tty_id: str) -> Dict[str, Any]:
317
537
  """关闭虚拟终端"""
538
+ if sys.platform == "win32":
539
+ return self._close_tty_windows(agent, tty_id)
540
+ else:
541
+ return self._close_tty_unix(agent, tty_id)
542
+
543
+ def _close_tty_unix(self, agent: Any, tty_id: str) -> Dict[str, Any]:
544
+ """Unix/Linux平台关闭终端"""
318
545
  if agent.tty_sessions[tty_id]["master_fd"] is None:
319
546
  return {
320
547
  "success": True,
@@ -328,7 +555,9 @@ class VirtualTTYTool:
328
555
 
329
556
  # 终止子进程
330
557
  if agent.tty_sessions[tty_id]["pid"]:
331
- os.kill(agent.tty_sessions[tty_id]["pid"], signal.SIGTERM)
558
+ import signal as _signal # pylint: disable=import-outside-toplevel
559
+
560
+ os.kill(agent.tty_sessions[tty_id]["pid"], _signal.SIGTERM)
332
561
 
333
562
  # 重置终端数据
334
563
  agent.tty_sessions[tty_id] = {
@@ -350,8 +579,53 @@ class VirtualTTYTool:
350
579
  "stderr": f"关闭虚拟终端 [{tty_id}] 失败: {str(e)}",
351
580
  }
352
581
 
582
+ def _close_tty_windows(self, agent: Any, tty_id: str) -> Dict[str, Any]:
583
+ """Windows平台关闭终端"""
584
+ if agent.tty_sessions[tty_id]["process"] is None:
585
+ return {
586
+ "success": True,
587
+ "stdout": f"没有正在运行的虚拟终端 [{tty_id}]",
588
+ "stderr": "",
589
+ }
590
+
591
+ try:
592
+ # 终止进程
593
+ agent.tty_sessions[tty_id]["process"].terminate()
594
+ agent.tty_sessions[tty_id]["process"].wait()
595
+
596
+ # 重置终端数据
597
+ import queue as _queue # pylint: disable=import-outside-toplevel
598
+
599
+ agent.tty_sessions[tty_id] = {
600
+ "process": None,
601
+ "output_queue": _queue.Queue(),
602
+ "output_thread": None,
603
+ "shell": "cmd.exe",
604
+ }
605
+
606
+ return {
607
+ "success": True,
608
+ "stdout": f"虚拟终端 [{tty_id}] 已关闭",
609
+ "stderr": "",
610
+ }
611
+
612
+ except Exception as e:
613
+ return {
614
+ "success": False,
615
+ "stdout": "",
616
+ "stderr": f"关闭虚拟终端 [{tty_id}] 失败: {str(e)}",
617
+ }
618
+
353
619
  def _get_screen(self, agent: Any, tty_id: str) -> Dict[str, Any]:
354
620
  """获取当前终端屏幕内容"""
621
+ if sys.platform == "win32":
622
+ # Windows平台暂不支持获取屏幕内容
623
+ return {
624
+ "success": False,
625
+ "stdout": "",
626
+ "stderr": "Windows平台暂不支持获取屏幕内容功能",
627
+ }
628
+
355
629
  if agent.tty_sessions[tty_id]["master_fd"] is None:
356
630
  return {
357
631
  "success": False,
@@ -371,7 +645,9 @@ class VirtualTTYTool:
371
645
  start_time = time.time()
372
646
  while time.time() - start_time < 2.0: # 最多等待2秒
373
647
  try:
374
- r, _, _ = select.select(
648
+ import select as _select # pylint: disable=import-outside-toplevel
649
+
650
+ r, _, _ = _select.select(
375
651
  [agent.tty_sessions[tty_id]["master_fd"]], [], [], 0.1
376
652
  )
377
653
  if r:
@@ -404,15 +680,28 @@ class VirtualTTYTool:
404
680
  active_ttys = []
405
681
 
406
682
  for tty_id, tty_data in agent.tty_sessions.items():
407
- status = "活动" if tty_data["master_fd"] is not None else "关闭"
408
- active_ttys.append(
409
- {
410
- "id": tty_id,
411
- "status": status,
412
- "pid": tty_data["pid"] if tty_data["pid"] else None,
413
- "shell": tty_data["shell"],
414
- }
415
- )
683
+ if sys.platform == "win32":
684
+ status = "活动" if tty_data["process"] is not None else "关闭"
685
+ active_ttys.append(
686
+ {
687
+ "id": tty_id,
688
+ "status": status,
689
+ "pid": tty_data["process"].pid
690
+ if tty_data["process"]
691
+ else None,
692
+ "shell": tty_data["shell"],
693
+ }
694
+ )
695
+ else:
696
+ status = "活动" if tty_data["master_fd"] is not None else "关闭"
697
+ active_ttys.append(
698
+ {
699
+ "id": tty_id,
700
+ "status": status,
701
+ "pid": tty_data["pid"] if tty_data["pid"] else None,
702
+ "shell": tty_data["shell"],
703
+ }
704
+ )
416
705
 
417
706
  # 格式化输出
418
707
  output = "虚拟终端列表:\n"
@@ -262,6 +262,12 @@ def get_pretty_output() -> bool:
262
262
  返回:
263
263
  bool: 如果启用PrettyOutput则返回True,默认为True
264
264
  """
265
+ import platform
266
+
267
+ # Windows系统强制设置为False
268
+ if platform.system() == "Windows":
269
+ return False
270
+
265
271
  return GLOBAL_CONFIG_DATA.get("JARVIS_PRETTY_OUTPUT", False) == True
266
272
 
267
273
 
@@ -214,9 +214,7 @@ def handle_commit_workflow() -> bool:
214
214
  Returns:
215
215
  bool: 提交是否成功
216
216
  """
217
- if is_confirm_before_apply_patch() and not user_confirm(
218
- "是否要提交代码?", default=True
219
- ):
217
+ if is_confirm_before_apply_patch() and not user_confirm("是否要提交代码?", default=True):
220
218
  revert_change()
221
219
  return False
222
220
 
@@ -429,9 +427,7 @@ def check_and_update_git_repo(repo_path: str) -> bool:
429
427
  if not in_venv and (
430
428
  "Permission denied" in error_msg or "not writeable" in error_msg
431
429
  ):
432
- if user_confirm(
433
- "检测到权限问题,是否尝试用户级安装(--user)?", True
434
- ):
430
+ if user_confirm("检测到权限问题,是否尝试用户级安装(--user)?", True):
435
431
  user_result = subprocess.run(
436
432
  install_cmd + ["--user"],
437
433
  cwd=git_root,
@@ -446,9 +442,7 @@ def check_and_update_git_repo(repo_path: str) -> bool:
446
442
  PrettyOutput.print(f"代码安装失败: {error_msg}", OutputType.ERROR)
447
443
  return False
448
444
  except Exception as e:
449
- PrettyOutput.print(
450
- f"安装过程中发生意外错误: {str(e)}", OutputType.ERROR
451
- )
445
+ PrettyOutput.print(f"安装过程中发生意外错误: {str(e)}", OutputType.ERROR)
452
446
  return False
453
447
  # 更新检查日期文件
454
448
  with open(last_check_file, "w") as f:
@@ -482,9 +476,7 @@ def get_diff_file_list() -> List[str]:
482
476
  subprocess.run(["git", "reset"], check=True)
483
477
 
484
478
  if result.returncode != 0:
485
- PrettyOutput.print(
486
- f"获取差异文件列表失败: {result.stderr}", OutputType.ERROR
487
- )
479
+ PrettyOutput.print(f"获取差异文件列表失败: {result.stderr}", OutputType.ERROR)
488
480
  return []
489
481
 
490
482
  return [f for f in result.stdout.splitlines() if f]
@@ -533,8 +525,10 @@ def get_recent_commits_with_files() -> List[Dict[str, Any]]:
533
525
  ],
534
526
  capture_output=True,
535
527
  text=True,
528
+ encoding="utf-8",
529
+ errors="replace",
536
530
  )
537
- if result.returncode != 0:
531
+ if result.returncode != 0 or result.stdout is None:
538
532
  return []
539
533
 
540
534
  # 解析提交信息
@@ -632,9 +626,7 @@ def confirm_add_new_files() -> None:
632
626
  need_confirm = True
633
627
 
634
628
  if binary_files:
635
- output_lines.append(
636
- f"检测到{len(binary_files)}个二进制文件(选择N将重新检测)"
637
- )
629
+ output_lines.append(f"检测到{len(binary_files)}个二进制文件(选择N将重新检测)")
638
630
  output_lines.append("二进制文件列表:")
639
631
  output_lines.extend(f" - {file}" for file in binary_files)
640
632
  need_confirm = True