jarvis-ai-assistant 0.2.3__py3-none-any.whl → 0.2.5__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 (33) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +13 -7
  3. jarvis/jarvis_agent/edit_file_handler.py +4 -0
  4. jarvis/jarvis_agent/jarvis.py +22 -25
  5. jarvis/jarvis_agent/main.py +6 -6
  6. jarvis/jarvis_code_agent/code_agent.py +273 -11
  7. jarvis/jarvis_code_analysis/code_review.py +21 -19
  8. jarvis/jarvis_data/config_schema.json +25 -29
  9. jarvis/jarvis_git_squash/main.py +3 -3
  10. jarvis/jarvis_git_utils/git_commiter.py +32 -11
  11. jarvis/jarvis_mcp/sse_mcp_client.py +4 -6
  12. jarvis/jarvis_mcp/streamable_mcp_client.py +5 -9
  13. jarvis/jarvis_rag/retriever.py +1 -1
  14. jarvis/jarvis_smart_shell/main.py +2 -2
  15. jarvis/jarvis_stats/__init__.py +13 -0
  16. jarvis/jarvis_stats/cli.py +404 -0
  17. jarvis/jarvis_stats/stats.py +538 -0
  18. jarvis/jarvis_stats/storage.py +381 -0
  19. jarvis/jarvis_stats/visualizer.py +282 -0
  20. jarvis/jarvis_tools/cli/main.py +82 -15
  21. jarvis/jarvis_tools/registry.py +32 -16
  22. jarvis/jarvis_tools/search_web.py +3 -3
  23. jarvis/jarvis_tools/virtual_tty.py +315 -26
  24. jarvis/jarvis_utils/config.py +12 -8
  25. jarvis/jarvis_utils/git_utils.py +8 -16
  26. jarvis/jarvis_utils/methodology.py +74 -67
  27. jarvis/jarvis_utils/utils.py +468 -72
  28. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/METADATA +29 -3
  29. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/RECORD +33 -28
  30. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/entry_points.txt +2 -0
  31. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/WHEEL +0 -0
  32. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/licenses/LICENSE +0 -0
  33. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/top_level.txt +0 -0
@@ -40,25 +40,92 @@ def list_tools(
40
40
 
41
41
 
42
42
  @app.command("stat")
43
- def stat_tools(as_json: bool = typer.Option(False, "--json", help="以JSON格式输出")):
43
+ def stat_tools(
44
+ as_json: bool = typer.Option(False, "--json", help="以JSON格式输出"),
45
+ last_days: Optional[int] = typer.Option(None, "--days", help="显示最近N天的统计(默认显示所有历史数据)"),
46
+ format: str = typer.Option("table", "--format", help="显示格式: table, chart, summary")
47
+ ):
44
48
  """显示工具调用统计信息"""
45
- registry = ToolRegistry()
46
- stats = registry._get_tool_stats()
47
- tools = registry.get_all_tools()
48
-
49
- table_data = []
50
- for tool in tools:
51
- name = tool["name"]
52
- count = stats.get(name, 0)
53
- table_data.append([name, count])
49
+ from jarvis.jarvis_stats.stats import StatsManager
50
+
51
+ if format == "table":
52
+ registry = ToolRegistry()
53
+ stats = registry._get_tool_stats()
54
+ tools = registry.get_all_tools()
55
+
56
+ table_data = []
57
+ for tool in tools:
58
+ name = tool["name"]
59
+ count = stats.get(name, 0)
60
+ if count > 0: # 只显示有调用记录的工具
61
+ table_data.append([name, count])
54
62
 
55
- table_data.sort(key=lambda x: x[1], reverse=True)
63
+ table_data.sort(key=lambda x: x[1], reverse=True)
56
64
 
57
- if as_json:
58
- print(json.dumps(dict(table_data), indent=2))
65
+ if as_json:
66
+ print(json.dumps(dict(table_data), indent=2))
67
+ else:
68
+ time_desc = f"最近{last_days}天" if last_days else "所有历史"
69
+ PrettyOutput.section(f"工具调用统计 ({time_desc})", OutputType.SYSTEM)
70
+ if table_data:
71
+ print(tabulate(table_data, headers=["工具名称", "调用次数"], tablefmt="grid"))
72
+ print(f"\n总计: {len(table_data)} 个工具被使用,共 {sum(x[1] for x in table_data)} 次调用")
73
+ else:
74
+ print("暂无工具调用记录")
59
75
  else:
60
- PrettyOutput.section("工具调用统计", OutputType.SYSTEM)
61
- print(tabulate(table_data, headers=["工具名称", "调用次数"], tablefmt="grid"))
76
+ # 使用 stats 系统的高级功能
77
+ PrettyOutput.section("工具组统计", OutputType.SYSTEM)
78
+ # 显示所有标记为 tool 组的指标
79
+ metrics = StatsManager.list_metrics()
80
+ tool_metrics = []
81
+
82
+ for metric in metrics:
83
+ # 检查是否是工具组的指标
84
+ if last_days:
85
+ stats_data = StatsManager.get_stats(
86
+ metric_name=metric,
87
+ last_days=last_days,
88
+ tags={"group": "tool"}
89
+ )
90
+ else:
91
+ # 获取所有历史数据
92
+ from datetime import datetime
93
+ stats_data = StatsManager.get_stats(
94
+ metric_name=metric,
95
+ start_time=datetime(2000, 1, 1),
96
+ end_time=datetime.now(),
97
+ tags={"group": "tool"}
98
+ )
99
+ if stats_data and stats_data.get("records"):
100
+ tool_metrics.append(metric)
101
+
102
+ if tool_metrics:
103
+ for metric in tool_metrics:
104
+ if format == "chart":
105
+ if last_days:
106
+ StatsManager.plot(metric, last_days=last_days, tags={"group": "tool"})
107
+ else:
108
+ from datetime import datetime
109
+ StatsManager.plot(
110
+ metric,
111
+ start_time=datetime(2000, 1, 1),
112
+ end_time=datetime.now(),
113
+ tags={"group": "tool"}
114
+ )
115
+ elif format == "summary":
116
+ if last_days:
117
+ StatsManager.show(metric, last_days=last_days, format="summary", tags={"group": "tool"})
118
+ else:
119
+ from datetime import datetime
120
+ StatsManager.show(
121
+ metric,
122
+ start_time=datetime(2000, 1, 1),
123
+ end_time=datetime.now(),
124
+ format="summary",
125
+ tags={"group": "tool"}
126
+ )
127
+ else:
128
+ print("暂无工具调用记录")
62
129
 
63
130
 
64
131
  @app.command("call")
@@ -200,25 +200,41 @@ class ToolRegistry(OutputHandlerProtocol):
200
200
 
201
201
  def _get_tool_stats(self) -> Dict[str, int]:
202
202
  """从数据目录获取工具调用统计"""
203
- stats_file = Path(get_data_dir()) / "tool_stat.yaml"
204
- if stats_file.exists():
205
- try:
206
- with open(stats_file, "r", encoding="utf-8") as f:
207
- return yaml.safe_load(f) or {}
208
- except Exception as e:
209
- PrettyOutput.print(f"加载工具调用统计失败: {str(e)}", OutputType.WARNING)
210
- return {}
203
+ from jarvis.jarvis_stats.stats import StatsManager
204
+ from datetime import datetime, timedelta
205
+
206
+ # 获取所有工具的统计数据
207
+ tool_stats = {}
208
+ tools = self.get_all_tools()
209
+
210
+ # 获取所有历史数据(从很早的时间开始)
211
+ end_time = datetime.now()
212
+ start_time = datetime(2000, 1, 1) # 使用一个足够早的时间
213
+
214
+ for tool in tools:
215
+ tool_name = tool["name"]
216
+ # 获取该工具的统计数据
217
+ stats_data = StatsManager.get_stats(
218
+ metric_name=tool_name,
219
+ start_time=start_time,
220
+ end_time=end_time,
221
+ tags={"group": "tool"}
222
+ )
223
+
224
+ # 计算总调用次数
225
+ if stats_data and "records" in stats_data:
226
+ total_count = sum(record["value"] for record in stats_data["records"])
227
+ tool_stats[tool_name] = int(total_count)
228
+ else:
229
+ tool_stats[tool_name] = 0
230
+
231
+ return tool_stats
211
232
 
212
233
  def _update_tool_stats(self, name: str) -> None:
213
234
  """更新工具调用统计"""
214
- stats = self._get_tool_stats()
215
- stats[name] = stats.get(name, 0) + 1
216
- stats_file = Path(get_data_dir()) / "tool_stat.yaml"
217
- try:
218
- with open(stats_file, "w", encoding="utf-8") as f:
219
- yaml.safe_dump(stats, f, allow_unicode=True)
220
- except Exception as e:
221
- PrettyOutput.print(f"保存工具调用统计失败: {str(e)}", OutputType.WARNING)
235
+ from jarvis.jarvis_stats.stats import StatsManager
236
+
237
+ StatsManager.increment(name, group="tool")
222
238
 
223
239
  def use_tools(self, name: List[str]) -> None:
224
240
  """使用指定工具
@@ -1,5 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
- """A tool for searching the web."""
2
+ """网络搜索工具。"""
3
3
  from typing import Any, Dict
4
4
 
5
5
  import requests
@@ -17,7 +17,7 @@ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
17
17
 
18
18
 
19
19
  class SearchWebTool:
20
- """A class to handle web searches."""
20
+ """处理网络搜索的类。"""
21
21
 
22
22
  name = "search_web"
23
23
  description = "搜索互联网上的信息"
@@ -28,7 +28,7 @@ class SearchWebTool:
28
28
 
29
29
  def _search_with_ddgs(self, query: str, agent: Agent) -> Dict[str, Any]:
30
30
  # pylint: disable=too-many-locals, broad-except
31
- """Performs a web search, scrapes content, and summarizes the results."""
31
+ """执行网络搜索、抓取内容并总结结果。"""
32
32
  try:
33
33
  PrettyOutput.print("▶️ 使用 DuckDuckGo 开始网页搜索...", OutputType.INFO)
34
34
  results = list(DDGS().text(query, max_results=50))
@@ -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"