@agentunion/kite 1.3.2 → 1.4.0

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 (78) hide show
  1. package/CHANGELOG.md +200 -0
  2. package/cli.js +76 -0
  3. package/extensions/agents/assistant/entry.py +111 -1
  4. package/extensions/agents/assistant/server.py +263 -215
  5. package/extensions/channels/acp_channel/entry.py +111 -1
  6. package/extensions/channels/acp_channel/module.md +23 -22
  7. package/extensions/channels/acp_channel/server.py +263 -215
  8. package/extensions/event_hub_bench/entry.py +107 -1
  9. package/extensions/services/backup/entry.py +299 -21
  10. package/extensions/services/backup/module.md +24 -22
  11. package/extensions/services/model_service/entry.py +145 -19
  12. package/extensions/services/model_service/module.md +21 -22
  13. package/extensions/services/watchdog/entry.py +188 -25
  14. package/extensions/services/watchdog/monitor.py +144 -34
  15. package/extensions/services/web/WEBSOCKET_STATUS.md +143 -0
  16. package/extensions/services/web/config_example.py +35 -0
  17. package/extensions/services/web/config_loader.py +110 -0
  18. package/extensions/services/web/entry.py +114 -26
  19. package/extensions/services/web/module.md +35 -24
  20. package/extensions/services/web/pairing.py +250 -0
  21. package/extensions/services/web/pairing_codes.jsonl +16 -0
  22. package/extensions/services/web/relay.py +643 -0
  23. package/extensions/services/web/relay_config.json5 +67 -0
  24. package/extensions/services/web/routes/routes_management_ws.py +127 -0
  25. package/extensions/services/web/routes/routes_rpc.py +89 -0
  26. package/extensions/services/web/routes/routes_test.py +61 -0
  27. package/extensions/services/web/routes/schemas.py +0 -22
  28. package/extensions/services/web/server.py +421 -98
  29. package/extensions/services/web/static/css/style.css +67 -28
  30. package/extensions/services/web/static/index.html +234 -44
  31. package/extensions/services/web/static/js/app.js +1335 -48
  32. package/extensions/services/web/static/js/kernel-client-example.js +161 -0
  33. package/extensions/services/web/static/js/kernel-client.js +383 -0
  34. package/extensions/services/web/static/js/registry-tests.js +558 -0
  35. package/extensions/services/web/static/js/token-manager.js +175 -0
  36. package/extensions/services/web/static/pairing.html +248 -0
  37. package/extensions/services/web/static/test_registry.html +262 -0
  38. package/extensions/services/web/web_config.json5 +29 -0
  39. package/kernel/entry.py +120 -32
  40. package/kernel/event_hub.py +141 -16
  41. package/kernel/module.md +36 -33
  42. package/kernel/registry_store.py +48 -15
  43. package/kernel/rpc_router.py +120 -53
  44. package/kernel/server.py +219 -12
  45. package/kite_cli/__init__.py +3 -0
  46. package/kite_cli/__main__.py +5 -0
  47. package/kite_cli/commands/__init__.py +1 -0
  48. package/kite_cli/commands/clean.py +101 -0
  49. package/kite_cli/commands/doctor.py +35 -0
  50. package/kite_cli/commands/history.py +111 -0
  51. package/kite_cli/commands/info.py +96 -0
  52. package/kite_cli/commands/install.py +313 -0
  53. package/kite_cli/commands/list.py +143 -0
  54. package/kite_cli/commands/log.py +81 -0
  55. package/kite_cli/commands/rollback.py +88 -0
  56. package/kite_cli/commands/search.py +73 -0
  57. package/kite_cli/commands/uninstall.py +85 -0
  58. package/kite_cli/commands/update.py +118 -0
  59. package/kite_cli/core/__init__.py +1 -0
  60. package/kite_cli/core/checker.py +142 -0
  61. package/kite_cli/core/dependency.py +229 -0
  62. package/kite_cli/core/downloader.py +209 -0
  63. package/kite_cli/core/install_info.py +40 -0
  64. package/kite_cli/core/tool_installer.py +397 -0
  65. package/kite_cli/core/validator.py +78 -0
  66. package/kite_cli/main.py +289 -0
  67. package/kite_cli/utils/__init__.py +1 -0
  68. package/kite_cli/utils/i18n.py +252 -0
  69. package/kite_cli/utils/interactive.py +63 -0
  70. package/kite_cli/utils/operation_log.py +77 -0
  71. package/kite_cli/utils/paths.py +34 -0
  72. package/kite_cli/utils/version.py +308 -0
  73. package/launcher/entry.py +819 -158
  74. package/launcher/logging_setup.py +104 -0
  75. package/launcher/module.md +37 -37
  76. package/package.json +2 -1
  77. package/scripts/plan_manager.py +315 -0
  78. package/extensions/services/web/routes/routes_modules.py +0 -249
package/kernel/server.py CHANGED
@@ -33,7 +33,10 @@ class KernelServer:
33
33
  - Event notifications (delivered to subscribers)
34
34
  """
35
35
 
36
- def __init__(self, launcher_token: str = None, advertise_ip: str = "127.0.0.1"):
36
+ def __init__(self, launcher_token: str = None, advertise_ip: str = "127.0.0.1", module_id: str = None):
37
+ if module_id is None:
38
+ raise ValueError("module_id is required")
39
+ self.module_id = module_id
37
40
  self.advertise_ip = advertise_ip
38
41
  self.port: int = 0 # set by entry.py before uvicorn.run
39
42
 
@@ -64,6 +67,14 @@ class KernelServer:
64
67
  self._launcher_subscribed = False
65
68
  self._ready_published = False
66
69
 
70
+ # Subscribe to events that Kernel needs to handle
71
+ # Kernel 通过订阅机制接收事件(与其他模块一致)
72
+ self.event_hub.handle_subscribe(self.module_id, ["module.shutdown"])
73
+
74
+ # Register internal event callback for Kernel
75
+ # Kernel 自身没有 WebSocket 连接,通过回调机制接收订阅的事件
76
+ self.event_hub.register_internal_callback(self.module_id, self._handle_internal_event)
77
+
67
78
  # Debounce timers for disconnected modules (module_id -> asyncio.Task)
68
79
  self._debounce_tasks: dict[str, asyncio.Task] = {}
69
80
  # Launcher loss timer (35s after launcher offline)
@@ -80,6 +91,7 @@ class KernelServer:
80
91
 
81
92
  @app.on_event("startup")
82
93
  async def _startup():
94
+ server.event_hub.start_internal_senders()
83
95
  server._ttl_task = asyncio.create_task(server._ttl_loop())
84
96
  server._dedup_task = asyncio.create_task(server._dedup_loop())
85
97
 
@@ -239,7 +251,7 @@ class KernelServer:
239
251
  expired = self.registry.check_ttl()
240
252
  for mid in expired:
241
253
  self.event_hub.publish_internal(
242
- "module.offline", {"module_id": mid})
254
+ "module.offline", {"module_id": mid}, source=self.module_id)
243
255
  except Exception as e:
244
256
  print(f"[kernel] TTL loop error: {e}")
245
257
 
@@ -265,7 +277,7 @@ class KernelServer:
265
277
  # 5s elapsed, module did not reconnect — mark offline
266
278
  self._debounce_tasks.pop(module_id, None)
267
279
  self.registry.set_offline(module_id)
268
- self.event_hub.publish_internal("module.offline", {"module_id": module_id})
280
+ self.event_hub.publish_internal("module.offline", {"module_id": module_id}, source=self.module_id)
269
281
  print(f"[kernel] {module_id} offline (5s debounce expired)")
270
282
 
271
283
  # If launcher went offline, start 35s launcher loss timer
@@ -289,7 +301,7 @@ class KernelServer:
289
301
  # Publish module.shutdown with reason launcher_lost to all modules
290
302
  self.event_hub.publish_internal("module.shutdown", {
291
303
  "reason": "launcher_lost",
292
- })
304
+ }, source=self.module_id)
293
305
 
294
306
  # Wait for modules to clean up (up to 10s)
295
307
  await asyncio.sleep(10)
@@ -302,7 +314,7 @@ class KernelServer:
302
314
  def self_register(self):
303
315
  """Register Kernel itself in the registry (in-memory, no RPC needed)."""
304
316
  self.registry.register_module({
305
- "module_id": "kernel",
317
+ "module_id": self.module_id,
306
318
  "module_type": "infrastructure",
307
319
  "api_endpoint": f"http://{self.advertise_ip}:{self.port}",
308
320
  "health_endpoint": "/health",
@@ -314,24 +326,219 @@ class KernelServer:
314
326
  def publish_ready(self):
315
327
  """Publish module.ready event for Kernel (internal, no WS needed)."""
316
328
  self.event_hub.publish_internal("module.ready", {
317
- "module_id": "kernel",
329
+ "module_id": self.module_id,
318
330
  "ws_endpoint": f"ws://{self.advertise_ip}:{self.port}/ws",
319
331
  "graceful_shutdown": True,
320
- })
332
+ }, source=self.module_id)
333
+
334
+ async def _handle_internal_event(self, event_type: str, data: dict):
335
+ """内部事件处理器(通过 EventHub 回调机制调用)
336
+
337
+ Kernel 自身没有 WebSocket 连接,通过此回调接收发送给自己的事件。
338
+
339
+ Args:
340
+ event_type: 事件类型(如 "module.shutdown")
341
+ data: 事件数据
342
+
343
+ Note:
344
+ 此方法由 EventHub 的 _invoke_callback_safe 包装调用,异常会被捕获并记录。
345
+ """
346
+ try:
347
+ # 处理 Kernel 订阅的事件
348
+ if event_type == "module.shutdown":
349
+ await self.handle_shutdown_event(data)
350
+ # 忽略系统广播事件(Kernel 没有订阅但会收到广播)
351
+ elif event_type in ("module.ready", "module.registered", "module.started",
352
+ "module.stopped", "module.crashed", "module.exiting",
353
+ "module.offline", "system.ready", "registry.updated"):
354
+ # 这些是系统事件,会广播给所有模块,Kernel 收到是正常的
355
+ pass
356
+ else:
357
+ # 收到未知事件,可能是订阅了但忘记实现处理器
358
+ print(f"[kernel] Warning: Received unhandled event: {event_type}")
359
+ except Exception as e:
360
+ # 双重保险:即使 EventHub 的包装器失败,这里也捕获
361
+ print(f"[kernel] Error handling internal event {event_type}: {e}")
362
+ import traceback
363
+ traceback.print_exc()
364
+ raise # 重新抛出,让 EventHub 的包装器记录
365
+
366
+ async def handle_shutdown_event(self, event_data):
367
+ """处理 module.shutdown 事件(标准优雅退出流程)
368
+
369
+ Args:
370
+ event_data: 事件数据,应包含 module_id 字段
371
+
372
+ Note:
373
+ 仅响应针对 Kernel 的 shutdown 事件(module_id == "kernel")。
374
+ 防御性检查:验证数据完整性,防止重复处理。
375
+ """
376
+ # 防御性检查:验证数据类型
377
+ if not isinstance(event_data, dict):
378
+ print(f"[kernel] Warning: Invalid shutdown event data type: {type(event_data)}")
379
+ return
380
+
381
+ # 防御性检查:验证 module_id
382
+ target_module = event_data.get("module_id")
383
+ if target_module != self.module_id:
384
+ print(f"[kernel] Warning: Received shutdown for wrong module: {target_module}")
385
+ return
386
+
387
+ # 防御性检查:防止重复处理
388
+ if self._shutting_down:
389
+ print("[kernel] Warning: Shutdown already in progress, ignoring duplicate event")
390
+ return
391
+
392
+ reason = event_data.get("reason", "unknown")
393
+ print(f"[kernel] 收到 shutdown 事件 (reason={reason}),开始优雅退出")
394
+
395
+ try:
396
+ # Step 1: 立即发送 ack
397
+ self.event_hub.publish_internal("module.shutdown.ack", {
398
+ "module_id": self.module_id
399
+ }, source=self.module_id)
400
+ print("[kernel] 已发送 shutdown.ack")
401
+
402
+ # Step 2: 发送 exiting
403
+ self.event_hub.publish_internal("module.exiting", {
404
+ "module_id": self.module_id,
405
+ "type": "passive",
406
+ "reason": f"响应 shutdown ({reason})",
407
+ "restart": "auto",
408
+ "action": "none",
409
+ "timeout": 5.0
410
+ }, source=self.module_id)
411
+ print("[kernel] 已发送 exiting")
412
+
413
+ # Step 3: 执行清理
414
+ await self._do_cleanup()
415
+
416
+ # Step 4: 发送 ready
417
+ self.event_hub.publish_internal("module.shutdown.ready", {
418
+ "module_id": self.module_id
419
+ }, source=self.module_id)
420
+ print("[kernel] 已发送 shutdown.ready")
421
+
422
+ # Step 5: 等待一小段时间确保事件发送完成
423
+ await asyncio.sleep(0.2)
424
+
425
+ # Step 6: 关闭所有 WebSocket 连接(包括 Launcher)
426
+ close_tasks = []
427
+ for mid, ws in list(self.connections.items()):
428
+ try:
429
+ close_tasks.append(ws.close(code=1000, reason="Graceful shutdown complete"))
430
+ except Exception as e:
431
+ print(f"[kernel] 关闭 {mid} 连接失败: {e}")
432
+
433
+ if close_tasks:
434
+ await asyncio.gather(*close_tasks, return_exceptions=True)
435
+ print(f"[kernel] 已关闭 {len(close_tasks)} 个 WebSocket 连接")
436
+
437
+ # Step 7: 触发 uvicorn 关闭
438
+ if self._uvicorn_server:
439
+ print("[kernel] 触发 uvicorn 关闭")
440
+ self._uvicorn_server.should_exit = True
441
+ else:
442
+ print("[kernel] uvicorn 引用未设置,直接退出")
443
+ import sys
444
+ sys.exit(0)
445
+
446
+ except Exception as e:
447
+ # 如果优雅退出失败,强制退出
448
+ print(f"[kernel] 优雅退出过程中发生错误: {e}")
449
+ import traceback
450
+ traceback.print_exc()
451
+ print("[kernel] 强制退出")
452
+ import sys
453
+ sys.exit(1)
454
+
455
+ async def _do_cleanup(self):
456
+ """执行清理工作(从原 shutdown() 方法提取)"""
457
+ if self._shutting_down:
458
+ return
459
+
460
+ self._shutting_down = True
461
+ print("[kernel] 执行清理工作...")
462
+
463
+ # 取消所有 debounce 任务
464
+ if self._debounce_tasks:
465
+ print(f"[kernel] 取消 {len(self._debounce_tasks)} 个 debounce 任务")
466
+ for task in self._debounce_tasks.values():
467
+ if not task.done():
468
+ task.cancel()
469
+ self._debounce_tasks.clear()
470
+
471
+ # 取消 launcher loss 计时器
472
+ if self._launcher_loss_task and not self._launcher_loss_task.done():
473
+ print("[kernel] 取消 launcher loss 计时器")
474
+ self._launcher_loss_task.cancel()
475
+ self._launcher_loss_task = None
476
+
477
+ # 关闭其他模块的 WebSocket 连接(保留 Launcher 连接)
478
+ other_connections = {mid: ws for mid, ws in self.connections.items() if mid != "launcher"}
479
+ if other_connections:
480
+ print(f"[kernel] 关闭 {len(other_connections)} 个其他模块的 WebSocket 连接")
481
+ for module_id, ws in other_connections.items():
482
+ try:
483
+ await ws.close(code=1001, reason="Server shutting down")
484
+ except Exception as e:
485
+ print(f"[kernel] 关闭连接失败 {module_id}: {e}")
486
+
487
+ # 清空 RPC 转发队列
488
+ pending_count = len(self.rpc_router._pending)
489
+ if pending_count > 0:
490
+ print(f"[kernel] 清空 {pending_count} 个待处理的 RPC 转发")
491
+ self.rpc_router._pending.clear()
492
+
493
+ print("[kernel] 清理完成")
321
494
 
322
495
  async def shutdown(self):
323
- """Shutdown Kernel gracefully. Called by Launcher via RPC."""
496
+ """Shutdown Kernel gracefully (legacy method for launcher_lost scenario).
497
+
498
+ This method is kept for the launcher_lost timeout scenario where Kernel
499
+ must shut down autonomously without receiving a shutdown event.
500
+ """
324
501
  if self._shutting_down:
325
502
  return
326
503
 
327
504
  self._shutting_down = True
328
- print("[kernel] Shutting down...")
505
+ print("[kernel] Shutting down (launcher_lost)...")
506
+
507
+ # Cancel all debounce tasks
508
+ print(f"[kernel] Cancelling {len(self._debounce_tasks)} debounce tasks...")
509
+ for task in self._debounce_tasks.values():
510
+ if not task.done():
511
+ task.cancel()
512
+ self._debounce_tasks.clear()
513
+
514
+ # Cancel launcher loss timer
515
+ if self._launcher_loss_task and not self._launcher_loss_task.done():
516
+ print("[kernel] Cancelling launcher loss timer...")
517
+ self._launcher_loss_task.cancel()
518
+ self._launcher_loss_task = None
519
+
520
+ # Close all WebSocket connections
521
+ print(f"[kernel] Closing {len(self.connections)} WebSocket connections...")
522
+ for module_id, ws in list(self.connections.items()):
523
+ try:
524
+ await ws.close(code=1001, reason="Server shutting down")
525
+ except Exception as e:
526
+ print(f"[kernel] Failed to close connection for {module_id}: {e}")
527
+ self.connections.clear()
329
528
 
330
- # Brief delay to ensure RPC response is sent
331
- await asyncio.sleep(0.1)
529
+ # Clear pending RPC forwards
530
+ pending_count = len(self.rpc_router._pending)
531
+ if pending_count > 0:
532
+ print(f"[kernel] Clearing {pending_count} pending RPC forwards...")
533
+ self.rpc_router._pending.clear()
332
534
 
333
535
  # Trigger uvicorn shutdown
334
536
  if self._uvicorn_server:
537
+ print("[kernel] Triggering uvicorn graceful shutdown...")
335
538
  self._uvicorn_server.should_exit = True
336
539
  else:
337
- print("[kernel] Warning: uvicorn server reference not set")
540
+ print("[kernel] Warning: uvicorn server reference not set, forcing exit")
541
+ import sys
542
+ sys.exit(0)
543
+
544
+ print("[kernel] Shutdown complete")
@@ -0,0 +1,3 @@
1
+ """Kite CLI - 模块安装管理工具"""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,5 @@
1
+ """支持 python -m kite_cli 调用"""
2
+ from kite_cli.main import main
3
+
4
+ if __name__ == "__main__":
5
+ exit(main())
@@ -0,0 +1 @@
1
+ """命令模块"""
@@ -0,0 +1,101 @@
1
+ """clean 命令实现 - 清理临时文件和缓存"""
2
+ import shutil
3
+ import tempfile
4
+ from pathlib import Path
5
+ from kite_cli.utils.interactive import confirm_action
6
+ from kite_cli.utils.operation_log import log_operation
7
+
8
+
9
+ def run_clean(args):
10
+ """执行清理命令"""
11
+ skip_confirm = args.yes if hasattr(args, 'yes') else False
12
+ clean_all = args.all if hasattr(args, 'all') else False
13
+
14
+ cleaned_items = []
15
+
16
+ # 1. 清理系统临时目录中的 kite-install-* 目录
17
+ temp_dir = Path(tempfile.gettempdir())
18
+ kite_temp_dirs = list(temp_dir.glob("kite-install-*"))
19
+
20
+ if kite_temp_dirs:
21
+ print(f"[Info] 发现 {len(kite_temp_dirs)} 个临时安装目录")
22
+ if not skip_confirm:
23
+ if not confirm_action("是否清理临时安装目录?"):
24
+ print("[Cancelled] 已取消")
25
+ return 1
26
+
27
+ for temp_path in kite_temp_dirs:
28
+ try:
29
+ shutil.rmtree(temp_path, ignore_errors=True)
30
+ cleaned_items.append({"type": "temp_dir", "path": str(temp_path)})
31
+ print(f" [Done] 已清理: {temp_path.name}")
32
+ except Exception as e:
33
+ print(f" [Warning] 清理失败: {temp_path.name} - {e}")
34
+
35
+ # 2. 如果指定 --all,清理所有模块的 .venv 和 node_modules
36
+ if clean_all:
37
+ print("\n[Warning] --all 选项会清理所有模块的依赖环境")
38
+ if not skip_confirm:
39
+ if not confirm_action("确认清理所有模块依赖?"):
40
+ print("[Cancelled] 已取消清理依赖")
41
+ else:
42
+ cleaned_items.extend(clean_module_deps())
43
+ else:
44
+ cleaned_items.extend(clean_module_deps())
45
+
46
+ # 记录操作日志
47
+ if cleaned_items:
48
+ log_operation("clean", {
49
+ "cleaned": cleaned_items
50
+ }, success=True)
51
+
52
+ print(f"\n[Done] 清理完成!共清理 {len(cleaned_items)} 项")
53
+ else:
54
+ print("[Info] 没有需要清理的内容")
55
+
56
+ return 0
57
+
58
+
59
+ def clean_module_deps() -> list:
60
+ """清理所有模块的依赖环境"""
61
+ import os
62
+ from kite_cli.utils.paths import get_install_path
63
+
64
+ cleaned = []
65
+
66
+ # 遍历所有安装位置
67
+ for location in ["dev", "local", "global"]:
68
+ try:
69
+ base_path = get_install_path(location, "")
70
+ if not base_path.exists():
71
+ continue
72
+
73
+ # 查找所有模块目录
74
+ for module_dir in base_path.iterdir():
75
+ if not module_dir.is_dir():
76
+ continue
77
+
78
+ # 清理 .venv
79
+ venv_dir = module_dir / ".venv"
80
+ if venv_dir.exists():
81
+ try:
82
+ shutil.rmtree(venv_dir)
83
+ cleaned.append({"type": "venv", "path": str(venv_dir)})
84
+ print(f" [Done] 已清理: {module_dir.name}/.venv")
85
+ except Exception as e:
86
+ print(f" [Warning] 清理失败: {module_dir.name}/.venv - {e}")
87
+
88
+ # 清理 node_modules
89
+ node_modules = module_dir / "node_modules"
90
+ if node_modules.exists():
91
+ try:
92
+ shutil.rmtree(node_modules)
93
+ cleaned.append({"type": "node_modules", "path": str(node_modules)})
94
+ print(f" [Done] 已清理: {module_dir.name}/node_modules")
95
+ except Exception as e:
96
+ print(f" [Warning] 清理失败: {module_dir.name}/node_modules - {e}")
97
+
98
+ except Exception as e:
99
+ print(f" [Warning] 扫描 {location} 失败: {e}")
100
+
101
+ return cleaned
@@ -0,0 +1,35 @@
1
+ """doctor 命令实现 - 检查下载工具状态"""
2
+ from kite_cli.core.tool_installer import ToolInstaller
3
+
4
+
5
+ def run_doctor(args):
6
+ """执行环境检查命令"""
7
+ print("[Doctor] 检查下载工具状态...\n")
8
+
9
+ tools = [
10
+ ("Python", "python"),
11
+ ("pip", "pip"),
12
+ ("Node.js", "node"),
13
+ ("npm", "npm"),
14
+ ("Git", "git")
15
+ ]
16
+
17
+ all_ok = True
18
+
19
+ for name, cmd in tools:
20
+ status = ToolInstaller.check_tool(cmd)
21
+ if status:
22
+ print(f" [OK] {name:10} 已安装")
23
+ else:
24
+ print(f" [Missing] {name:10} 未安装")
25
+ all_ok = False
26
+
27
+ print()
28
+
29
+ if all_ok:
30
+ print("[Done] 所有工具已就绪")
31
+ return 0
32
+ else:
33
+ print("[Warning] 部分工具未安装")
34
+ print("[Info] 使用 'kite install' 时会自动尝试安装缺失的工具")
35
+ return 1
@@ -0,0 +1,111 @@
1
+ """history 命令实现 - 显示最近安装的模块"""
2
+ from kite_cli.utils.operation_log import read_operations
3
+ from kite_cli.utils.i18n import t
4
+
5
+
6
+ def run_history(args):
7
+ """执行历史查看命令"""
8
+ limit = args.limit if hasattr(args, 'limit') else 6
9
+ show_all = args.all if hasattr(args, 'all') else False
10
+
11
+ # 读取操作日志
12
+ operations = read_operations(limit=100) # 读取更多记录用于过滤
13
+
14
+ if not operations:
15
+ print("[Info] 暂无安装记录")
16
+ return 0
17
+
18
+ # 只保留成功的 install 操作
19
+ install_ops = []
20
+ seen_modules = set() # 用于去重
21
+
22
+ for op in reversed(operations): # 从最新到最旧
23
+ if op.get("operation") == "install" and op.get("success"):
24
+ details = op.get("details", {})
25
+ module_name = details.get("module")
26
+
27
+ if not module_name:
28
+ continue
29
+
30
+ # 检查是否已被卸载(在此操作之后)
31
+ is_uninstalled = False
32
+ for later_op in operations:
33
+ if later_op.get("timestamp") > op.get("timestamp"):
34
+ if later_op.get("operation") == "uninstall" and later_op.get("success"):
35
+ uninstall_details = later_op.get("details", {})
36
+ if uninstall_details.get("module") == module_name:
37
+ is_uninstalled = True
38
+ break
39
+
40
+ # 如果 show_all=False,只显示未卸载的;如果 show_all=True,显示所有
41
+ if show_all or not is_uninstalled:
42
+ # 去重:同一个模块只显示最近一次安装
43
+ if module_name not in seen_modules:
44
+ seen_modules.add(module_name)
45
+ install_ops.append({
46
+ "timestamp": op.get("timestamp"),
47
+ "module": module_name,
48
+ "version": details.get("version", "unknown"),
49
+ "location": details.get("location", "unknown"),
50
+ "source_type": details.get("source", {}).get("type", "unknown"),
51
+ "path": details.get("path", ""),
52
+ "is_uninstalled": is_uninstalled
53
+ })
54
+
55
+ if len(install_ops) >= limit:
56
+ break
57
+
58
+ if not install_ops:
59
+ print("[Info] 暂无安装记录")
60
+ return 0
61
+
62
+ print(f"\n[History] 最近安装的 {len(install_ops)} 个模块:\n")
63
+
64
+ for i, op in enumerate(install_ops, 1):
65
+ timestamp = op["timestamp"][:19].replace("T", " ")
66
+ module = op["module"]
67
+ version = op["version"]
68
+ location = op["location"]
69
+ source_type = op["source_type"]
70
+ is_uninstalled = op["is_uninstalled"]
71
+
72
+ # 状态标记
73
+ status = "[已卸载]" if is_uninstalled else "[已安装]"
74
+
75
+ # 位置标记
76
+ location_map = {
77
+ "dev": "开发",
78
+ "local": "本地",
79
+ "global": "全局"
80
+ }
81
+ location_str = location_map.get(location, location)
82
+
83
+ print(f"{i}. {module} v{version}")
84
+ print(f" 时间: {timestamp}")
85
+ print(f" 位置: {location_str} ({location})")
86
+ print(f" 来源: {source_type}")
87
+ print(f" 状态: {status}")
88
+
89
+ # 如果未卸载,显示卸载命令
90
+ if not is_uninstalled:
91
+ print(f" 卸载: kite uninstall {module}")
92
+ if location != "global":
93
+ print(f" kite uninstall {module} -l {location}")
94
+
95
+ print()
96
+
97
+ # 显示快速卸载提示
98
+ if not show_all:
99
+ active_modules = [op["module"] for op in install_ops if not op["is_uninstalled"]]
100
+ if active_modules:
101
+ print("[Tip] 快速卸载最近安装的模块:")
102
+ for module in active_modules[:3]: # 只显示前3个
103
+ print(f" kite uninstall {module}")
104
+ print()
105
+
106
+ # 显示查看所有历史的提示
107
+ if not show_all and len(operations) > limit:
108
+ print("[Tip] 查看所有历史记录(包括已卸载): kite history --all")
109
+ print("[Tip] 查看更多记录: kite history -n 20")
110
+
111
+ return 0
@@ -0,0 +1,96 @@
1
+ """info 命令实现"""
2
+ from pathlib import Path
3
+ from kite_cli.utils.paths import get_install_path
4
+ from kite_cli.core.validator import Validator
5
+ from kite_cli.core.install_info import read_install_info
6
+ from kite_cli.utils.i18n import t
7
+
8
+
9
+ def run_info(args):
10
+ """执行 info 命令"""
11
+ module_name = args.name
12
+ location = args.location if hasattr(args, 'location') else None
13
+
14
+ print(t('module_info', name=module_name) + "\n")
15
+
16
+ # 查找模块
17
+ if location:
18
+ locations = [location]
19
+ else:
20
+ locations = find_module_locations(module_name)
21
+ if not locations:
22
+ print(t('module_not_found', name=module_name))
23
+ return 1
24
+
25
+ # 显示所有找到的位置
26
+ for loc in locations:
27
+ module_path = get_install_path(loc, module_name)
28
+ show_module_info(module_path, loc)
29
+ print()
30
+
31
+ return 0
32
+
33
+
34
+ def show_module_info(module_path: Path, location: str):
35
+ """显示模块详细信息"""
36
+ location_key = f"info_location_{location}"
37
+ location_name = t(location_key)
38
+
39
+ print(f"【{location_name}】")
40
+ print(f"{t('info_path')}: {module_path}")
41
+
42
+ # 读取 module.md
43
+ module_md = Validator.find_module_md(module_path)
44
+ if not module_md:
45
+ print(t('info_no_module_md'))
46
+ return
47
+
48
+ module_data = Validator.parse_module_md(module_md)
49
+ if not module_data:
50
+ print(t('info_parse_failed'))
51
+ return
52
+
53
+ # 基本信息
54
+ print(f"{t('info_name')}: {module_data.get('name', 'unknown')}")
55
+ print(f"{t('info_display_name')}: {module_data.get('display_name', module_data.get('name', 'unknown'))}")
56
+ print(f"{t('info_version')}: {module_data.get('version', 'unknown')}")
57
+ print(f"{t('info_type')}: {module_data.get('type', 'unknown')}")
58
+ print(f"{t('info_runtime')}: {module_data.get('runtime', 'unknown')}")
59
+ print(f"{t('info_entry')}: {module_data.get('entry', 'unknown')}")
60
+ print(f"{t('info_state')}: {module_data.get('state', 'unknown')}")
61
+
62
+ # 依赖信息
63
+ dependencies = module_data.get("dependencies", {})
64
+ if dependencies:
65
+ print(f"\n{t('info_dependencies')}:")
66
+ if dependencies.get("python"):
67
+ print(f" Python: {', '.join(dependencies['python'])}")
68
+ if dependencies.get("npm"):
69
+ print(f" npm: {', '.join(dependencies['npm'])}")
70
+ if dependencies.get("kite"):
71
+ print(f" Kite: {', '.join(dependencies['kite'])}")
72
+
73
+ # 安装信息
74
+ install_info = read_install_info(module_path)
75
+ if install_info:
76
+ print(f"\n{t('info_install_info')}:")
77
+ source = install_info.get("source", {})
78
+ print(f" {t('info_source')}: {source.get('type', 'unknown')}")
79
+ if source.get("package"):
80
+ print(f" {t('info_package')}: {source['package']}")
81
+ if source.get("url"):
82
+ print(f" {t('info_url')}: {source['url']}")
83
+ print(f" {t('info_installed_at')}: {install_info.get('installed_at', 'unknown')}")
84
+
85
+
86
+ def find_module_locations(module_name: str):
87
+ """查找模块在哪些位置存在"""
88
+ locations = []
89
+ for loc in ["dev", "local", "global"]:
90
+ try:
91
+ module_path = get_install_path(loc, module_name)
92
+ if module_path.exists():
93
+ locations.append(loc)
94
+ except Exception:
95
+ pass
96
+ return locations