@agentunion/kite 1.3.1 → 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 +287 -1
  2. package/cli.js +76 -0
  3. package/extensions/agents/assistant/entry.py +111 -1
  4. package/extensions/agents/assistant/server.py +263 -197
  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 -197
  8. package/extensions/event_hub_bench/entry.py +107 -1
  9. package/extensions/services/backup/entry.py +408 -72
  10. package/extensions/services/backup/module.md +24 -22
  11. package/extensions/services/model_service/entry.py +255 -71
  12. package/extensions/services/model_service/module.md +21 -22
  13. package/extensions/services/watchdog/entry.py +344 -90
  14. package/extensions/services/watchdog/monitor.py +237 -21
  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/server.py +445 -99
  28. package/extensions/services/web/static/css/style.css +138 -2
  29. package/extensions/services/web/static/index.html +295 -2
  30. package/extensions/services/web/static/js/app.js +1579 -5
  31. package/extensions/services/web/static/js/kernel-client-example.js +161 -0
  32. package/extensions/services/web/static/js/kernel-client.js +383 -0
  33. package/extensions/services/web/static/js/registry-tests.js +558 -0
  34. package/extensions/services/web/static/js/token-manager.js +175 -0
  35. package/extensions/services/web/static/pairing.html +248 -0
  36. package/extensions/services/web/static/test_registry.html +262 -0
  37. package/extensions/services/web/web_config.json5 +29 -0
  38. package/kernel/entry.py +120 -32
  39. package/kernel/event_hub.py +159 -16
  40. package/kernel/module.md +36 -33
  41. package/kernel/registry_store.py +70 -20
  42. package/kernel/rpc_router.py +134 -57
  43. package/kernel/server.py +292 -15
  44. package/kite_cli/__init__.py +3 -0
  45. package/kite_cli/__main__.py +5 -0
  46. package/kite_cli/commands/__init__.py +1 -0
  47. package/kite_cli/commands/clean.py +101 -0
  48. package/kite_cli/commands/doctor.py +35 -0
  49. package/kite_cli/commands/history.py +111 -0
  50. package/kite_cli/commands/info.py +96 -0
  51. package/kite_cli/commands/install.py +313 -0
  52. package/kite_cli/commands/list.py +143 -0
  53. package/kite_cli/commands/log.py +81 -0
  54. package/kite_cli/commands/rollback.py +88 -0
  55. package/kite_cli/commands/search.py +73 -0
  56. package/kite_cli/commands/uninstall.py +85 -0
  57. package/kite_cli/commands/update.py +118 -0
  58. package/kite_cli/core/__init__.py +1 -0
  59. package/kite_cli/core/checker.py +142 -0
  60. package/kite_cli/core/dependency.py +229 -0
  61. package/kite_cli/core/downloader.py +209 -0
  62. package/kite_cli/core/install_info.py +40 -0
  63. package/kite_cli/core/tool_installer.py +397 -0
  64. package/kite_cli/core/validator.py +78 -0
  65. package/kite_cli/main.py +289 -0
  66. package/kite_cli/utils/__init__.py +1 -0
  67. package/kite_cli/utils/i18n.py +252 -0
  68. package/kite_cli/utils/interactive.py +63 -0
  69. package/kite_cli/utils/operation_log.py +77 -0
  70. package/kite_cli/utils/paths.py +34 -0
  71. package/kite_cli/utils/version.py +308 -0
  72. package/launcher/count_lines.py +34 -0
  73. package/launcher/entry.py +905 -166
  74. package/launcher/logging_setup.py +104 -0
  75. package/launcher/module.md +37 -37
  76. package/launcher/process_manager.py +12 -1
  77. package/package.json +2 -1
  78. package/scripts/plan_manager.py +315 -0
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,19 @@ 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
+
78
+ # Debounce timers for disconnected modules (module_id -> asyncio.Task)
79
+ self._debounce_tasks: dict[str, asyncio.Task] = {}
80
+ # Launcher loss timer (35s after launcher offline)
81
+ self._launcher_loss_task: asyncio.Task | None = None
82
+
67
83
  # Build FastAPI app
68
84
  self.app = self._create_app()
69
85
 
@@ -75,6 +91,7 @@ class KernelServer:
75
91
 
76
92
  @app.on_event("startup")
77
93
  async def _startup():
94
+ server.event_hub.start_internal_senders()
78
95
  server._ttl_task = asyncio.create_task(server._ttl_loop())
79
96
  server._dedup_task = asyncio.create_task(server._dedup_loop())
80
97
 
@@ -110,13 +127,27 @@ class KernelServer:
110
127
 
111
128
  await ws.accept()
112
129
 
130
+ # Cancel debounce timer if module is reconnecting within 5s window
131
+ old_debounce = server._debounce_tasks.pop(module_id, None)
132
+ if old_debounce:
133
+ old_debounce.cancel()
134
+ print(f"[kernel] {module_id} reconnected within debounce window")
135
+
113
136
  # Register connection in both EventHub and shared connections table
114
137
  server.event_hub.add_connection(module_id, ws)
115
138
  server.connections[module_id] = ws
116
139
 
140
+ # Set connected status in registry (if module exists)
141
+ server.registry.set_connected(module_id)
142
+
117
143
  # Track Launcher connection
118
144
  if module_id == "launcher":
119
145
  server._launcher_connected = True
146
+ # Cancel launcher loss timer if reconnecting
147
+ if server._launcher_loss_task:
148
+ server._launcher_loss_task.cancel()
149
+ server._launcher_loss_task = None
150
+ print(f"[kernel] launcher reconnected, cancelled loss timer")
120
151
  print(f"[kernel] launcher connected")
121
152
 
122
153
  # Renew heartbeat on connect
@@ -163,12 +194,19 @@ class KernelServer:
163
194
  if "not connected" not in err and "closed" not in err:
164
195
  print(f"[kernel] WebSocket error for {module_id}: {e}")
165
196
  finally:
166
- # Cleanup
197
+ # Cleanup connection but DON'T immediately set offline — debounce
167
198
  server.event_hub.remove_connection(module_id)
168
199
  server.connections.pop(module_id, None)
169
- server.registry.set_offline(module_id)
170
- server.event_hub.publish_internal(
171
- "module.offline", {"module_id": module_id})
200
+
201
+ # Cancel existing debounce for this module (if reconnecting fast)
202
+ old_task = server._debounce_tasks.pop(module_id, None)
203
+ if old_task:
204
+ old_task.cancel()
205
+
206
+ # Start 5s debounce timer
207
+ server._debounce_tasks[module_id] = asyncio.create_task(
208
+ server._debounce_offline(module_id)
209
+ )
172
210
 
173
211
  # ── HTTP endpoints (debug only) ──
174
212
 
@@ -180,7 +218,7 @@ class KernelServer:
180
218
  "module_count": len(server.registry.modules),
181
219
  "online_count": sum(
182
220
  1 for m in server.registry.modules.values()
183
- if m.get("status") == "online"
221
+ if m.get("status") in ("registered", "ready")
184
222
  ),
185
223
  "event_stats": eh_health.get("details", {}),
186
224
  }
@@ -213,7 +251,7 @@ class KernelServer:
213
251
  expired = self.registry.check_ttl()
214
252
  for mid in expired:
215
253
  self.event_hub.publish_internal(
216
- "module.offline", {"module_id": mid})
254
+ "module.offline", {"module_id": mid}, source=self.module_id)
217
255
  except Exception as e:
218
256
  print(f"[kernel] TTL loop error: {e}")
219
257
 
@@ -227,12 +265,56 @@ class KernelServer:
227
265
  except Exception as e:
228
266
  print(f"[kernel] Dedup cleanup error: {e}")
229
267
 
268
+ # ── Debounce & Launcher loss ──
269
+
270
+ async def _debounce_offline(self, module_id: str):
271
+ """Wait 5s after WS disconnect. If module doesn't reconnect, mark offline."""
272
+ try:
273
+ await asyncio.sleep(5)
274
+ except asyncio.CancelledError:
275
+ return # Module reconnected within 5s — cancelled by ws_endpoint
276
+
277
+ # 5s elapsed, module did not reconnect — mark offline
278
+ self._debounce_tasks.pop(module_id, None)
279
+ self.registry.set_offline(module_id)
280
+ self.event_hub.publish_internal("module.offline", {"module_id": module_id}, source=self.module_id)
281
+ print(f"[kernel] {module_id} offline (5s debounce expired)")
282
+
283
+ # If launcher went offline, start 35s launcher loss timer
284
+ if module_id == "launcher":
285
+ self._launcher_connected = False
286
+ if not self._launcher_loss_task:
287
+ self._launcher_loss_task = asyncio.create_task(
288
+ self._launcher_loss_timeout()
289
+ )
290
+
291
+ async def _launcher_loss_timeout(self):
292
+ """35s after launcher goes offline (post-debounce). Trigger graceful shutdown."""
293
+ try:
294
+ await asyncio.sleep(30) # 5s debounce already elapsed, total = 35s
295
+ except asyncio.CancelledError:
296
+ return # Launcher reconnected
297
+
298
+ print("[kernel] Launcher lost for 35s, triggering graceful shutdown")
299
+ self._launcher_loss_task = None
300
+
301
+ # Publish module.shutdown with reason launcher_lost to all modules
302
+ self.event_hub.publish_internal("module.shutdown", {
303
+ "reason": "launcher_lost",
304
+ }, source=self.module_id)
305
+
306
+ # Wait for modules to clean up (up to 10s)
307
+ await asyncio.sleep(10)
308
+
309
+ # Shutdown Kernel itself
310
+ await self.shutdown()
311
+
230
312
  # ── Self-registration ──
231
313
 
232
314
  def self_register(self):
233
315
  """Register Kernel itself in the registry (in-memory, no RPC needed)."""
234
316
  self.registry.register_module({
235
- "module_id": "kernel",
317
+ "module_id": self.module_id,
236
318
  "module_type": "infrastructure",
237
319
  "api_endpoint": f"http://{self.advertise_ip}:{self.port}",
238
320
  "health_endpoint": "/health",
@@ -244,24 +326,219 @@ class KernelServer:
244
326
  def publish_ready(self):
245
327
  """Publish module.ready event for Kernel (internal, no WS needed)."""
246
328
  self.event_hub.publish_internal("module.ready", {
247
- "module_id": "kernel",
329
+ "module_id": self.module_id,
248
330
  "ws_endpoint": f"ws://{self.advertise_ip}:{self.port}/ws",
249
331
  "graceful_shutdown": True,
250
- })
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] 清理完成")
251
494
 
252
495
  async def shutdown(self):
253
- """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
+ """
254
501
  if self._shutting_down:
255
502
  return
256
503
 
257
504
  self._shutting_down = True
258
- 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()
259
528
 
260
- # Brief delay to ensure RPC response is sent
261
- 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()
262
534
 
263
535
  # Trigger uvicorn shutdown
264
536
  if self._uvicorn_server:
537
+ print("[kernel] Triggering uvicorn graceful shutdown...")
265
538
  self._uvicorn_server.should_exit = True
266
539
  else:
267
- 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