@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.
- package/CHANGELOG.md +287 -1
- package/cli.js +76 -0
- package/extensions/agents/assistant/entry.py +111 -1
- package/extensions/agents/assistant/server.py +263 -197
- package/extensions/channels/acp_channel/entry.py +111 -1
- package/extensions/channels/acp_channel/module.md +23 -22
- package/extensions/channels/acp_channel/server.py +263 -197
- package/extensions/event_hub_bench/entry.py +107 -1
- package/extensions/services/backup/entry.py +408 -72
- package/extensions/services/backup/module.md +24 -22
- package/extensions/services/model_service/entry.py +255 -71
- package/extensions/services/model_service/module.md +21 -22
- package/extensions/services/watchdog/entry.py +344 -90
- package/extensions/services/watchdog/monitor.py +237 -21
- package/extensions/services/web/WEBSOCKET_STATUS.md +143 -0
- package/extensions/services/web/config_example.py +35 -0
- package/extensions/services/web/config_loader.py +110 -0
- package/extensions/services/web/entry.py +114 -26
- package/extensions/services/web/module.md +35 -24
- package/extensions/services/web/pairing.py +250 -0
- package/extensions/services/web/pairing_codes.jsonl +16 -0
- package/extensions/services/web/relay.py +643 -0
- package/extensions/services/web/relay_config.json5 +67 -0
- package/extensions/services/web/routes/routes_management_ws.py +127 -0
- package/extensions/services/web/routes/routes_rpc.py +89 -0
- package/extensions/services/web/routes/routes_test.py +61 -0
- package/extensions/services/web/server.py +445 -99
- package/extensions/services/web/static/css/style.css +138 -2
- package/extensions/services/web/static/index.html +295 -2
- package/extensions/services/web/static/js/app.js +1579 -5
- package/extensions/services/web/static/js/kernel-client-example.js +161 -0
- package/extensions/services/web/static/js/kernel-client.js +383 -0
- package/extensions/services/web/static/js/registry-tests.js +558 -0
- package/extensions/services/web/static/js/token-manager.js +175 -0
- package/extensions/services/web/static/pairing.html +248 -0
- package/extensions/services/web/static/test_registry.html +262 -0
- package/extensions/services/web/web_config.json5 +29 -0
- package/kernel/entry.py +120 -32
- package/kernel/event_hub.py +159 -16
- package/kernel/module.md +36 -33
- package/kernel/registry_store.py +70 -20
- package/kernel/rpc_router.py +134 -57
- package/kernel/server.py +292 -15
- package/kite_cli/__init__.py +3 -0
- package/kite_cli/__main__.py +5 -0
- package/kite_cli/commands/__init__.py +1 -0
- package/kite_cli/commands/clean.py +101 -0
- package/kite_cli/commands/doctor.py +35 -0
- package/kite_cli/commands/history.py +111 -0
- package/kite_cli/commands/info.py +96 -0
- package/kite_cli/commands/install.py +313 -0
- package/kite_cli/commands/list.py +143 -0
- package/kite_cli/commands/log.py +81 -0
- package/kite_cli/commands/rollback.py +88 -0
- package/kite_cli/commands/search.py +73 -0
- package/kite_cli/commands/uninstall.py +85 -0
- package/kite_cli/commands/update.py +118 -0
- package/kite_cli/core/__init__.py +1 -0
- package/kite_cli/core/checker.py +142 -0
- package/kite_cli/core/dependency.py +229 -0
- package/kite_cli/core/downloader.py +209 -0
- package/kite_cli/core/install_info.py +40 -0
- package/kite_cli/core/tool_installer.py +397 -0
- package/kite_cli/core/validator.py +78 -0
- package/kite_cli/main.py +289 -0
- package/kite_cli/utils/__init__.py +1 -0
- package/kite_cli/utils/i18n.py +252 -0
- package/kite_cli/utils/interactive.py +63 -0
- package/kite_cli/utils/operation_log.py +77 -0
- package/kite_cli/utils/paths.py +34 -0
- package/kite_cli/utils/version.py +308 -0
- package/launcher/count_lines.py +34 -0
- package/launcher/entry.py +905 -166
- package/launcher/logging_setup.py +104 -0
- package/launcher/module.md +37 -37
- package/launcher/process_manager.py +12 -1
- package/package.json +2 -1
- 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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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")
|
|
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":
|
|
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":
|
|
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
|
|
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
|
-
#
|
|
261
|
-
|
|
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 @@
|
|
|
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
|