@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
@@ -0,0 +1,127 @@
1
+ """Management WebSocket — 实时推送模块状态变更到前端管理页面"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ from datetime import datetime, timezone
9
+
10
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ router = APIRouter()
15
+
16
+ # 全局连接池 — 所有连接的管理客户端
17
+ _management_clients: set[WebSocket] = set()
18
+
19
+
20
+ @router.websocket("/ws/management")
21
+ async def management_websocket(ws: WebSocket):
22
+ """
23
+ 管理后台实时状态推送 WebSocket.
24
+
25
+ 前端连接后会收到:
26
+ - 初始 connected 消息
27
+ - 模块状态变更事件(module.started, module.stopped, module.crashed 等)
28
+ - 健康检查结果(module.health)
29
+ - Kernel 连接状态(module.connected, module.registered)
30
+ """
31
+ await ws.accept()
32
+ _management_clients.add(ws)
33
+ client_id = id(ws)
34
+ logger.info(f"Management WS: client connected (id={client_id}, total={len(_management_clients)})")
35
+
36
+ try:
37
+ # 发送初始连接确认
38
+ await ws.send_json({
39
+ "type": "connected",
40
+ "timestamp": datetime.now(timezone.utc).isoformat(),
41
+ })
42
+
43
+ # 保持连接,接收心跳和客户端消息
44
+ while True:
45
+ try:
46
+ raw = await asyncio.wait_for(ws.receive_text(), timeout=60.0)
47
+ msg = json.loads(raw)
48
+
49
+ # 心跳响应
50
+ if msg.get("type") == "ping":
51
+ await ws.send_json({
52
+ "type": "pong",
53
+ "timestamp": datetime.now(timezone.utc).isoformat(),
54
+ })
55
+
56
+ except asyncio.TimeoutError:
57
+ # 60 秒无消息,发送 ping 检测连接
58
+ try:
59
+ await ws.send_json({"type": "ping"})
60
+ except Exception:
61
+ break
62
+
63
+ except WebSocketDisconnect:
64
+ logger.info(f"Management WS: client disconnected (id={client_id})")
65
+ except Exception as e:
66
+ logger.warning(f"Management WS: client error (id={client_id}): {e}")
67
+ finally:
68
+ _management_clients.discard(ws)
69
+ logger.info(f"Management WS: client removed (id={client_id}, remaining={len(_management_clients)})")
70
+
71
+
72
+ async def broadcast_event(event_type: str, data: dict):
73
+ """
74
+ 广播事件到所有连接的管理客户端.
75
+
76
+ Args:
77
+ event_type: 事件类型(如 module.started, module.stopped)
78
+ data: 事件数据(通常包含 module_id 等字段)
79
+ """
80
+ if not _management_clients:
81
+ return
82
+
83
+ message = {
84
+ "type": event_type,
85
+ "data": data,
86
+ "timestamp": datetime.now(timezone.utc).isoformat(),
87
+ }
88
+
89
+ message_str = json.dumps(message)
90
+ dead_clients = []
91
+
92
+ # 并发发送到所有客户端
93
+ for ws in list(_management_clients):
94
+ try:
95
+ await ws.send_text(message_str)
96
+ except Exception as e:
97
+ logger.debug(f"Management WS: failed to send to client {id(ws)}: {e}")
98
+ dead_clients.append(ws)
99
+
100
+ # 清理失败的连接
101
+ for ws in dead_clients:
102
+ _management_clients.discard(ws)
103
+
104
+ if dead_clients:
105
+ logger.info(f"Management WS: removed {len(dead_clients)} dead clients, remaining={len(_management_clients)}")
106
+
107
+
108
+ def get_client_count() -> int:
109
+ """返回当前连接的管理客户端数量"""
110
+ return len(_management_clients)
111
+
112
+
113
+ async def close_all_clients():
114
+ """优雅关闭所有管理客户端(用于 shutdown)"""
115
+ if not _management_clients:
116
+ return
117
+
118
+ logger.info(f"Management WS: closing {len(_management_clients)} clients...")
119
+
120
+ for ws in list(_management_clients):
121
+ try:
122
+ await ws.close(code=1001, reason="Server shutting down")
123
+ except Exception as e:
124
+ logger.debug(f"Management WS: error closing client {id(ws)}: {e}")
125
+
126
+ _management_clients.clear()
127
+ logger.info("Management WS: all clients closed")
@@ -0,0 +1,89 @@
1
+ """RPC forwarding routes — forward HTTP requests to Kernel RPC."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ import uuid
9
+
10
+ from fastapi import APIRouter, HTTPException, Request
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ router = APIRouter(tags=["rpc"])
15
+
16
+ # Global reference to WebServer instance (set by server.py)
17
+ _web_server = None
18
+
19
+
20
+ def set_web_server(server):
21
+ """Set the WebServer instance for RPC forwarding."""
22
+ global _web_server
23
+ _web_server = server
24
+
25
+
26
+ @router.post("/rpc/{method:path}")
27
+ async def forward_rpc(method: str, request: Request):
28
+ """
29
+ Forward RPC call to Kernel via WebSocket.
30
+
31
+ Example:
32
+ POST /api/rpc/launcher.start_module
33
+ Body: {"name": "watchdog"}
34
+ """
35
+ if not _web_server:
36
+ raise HTTPException(503, "Web 模块未初始化")
37
+
38
+ if not _web_server._ws:
39
+ raise HTTPException(503, "Web 模块未连接到 Kernel,请检查 Kernel 是否正常运行")
40
+
41
+ try:
42
+ body = await request.json()
43
+ except Exception:
44
+ body = {}
45
+
46
+ # Generate unique request ID
47
+ rpc_id = str(uuid.uuid4())
48
+
49
+ # Create RPC request
50
+ rpc_request = {
51
+ "jsonrpc": "2.0",
52
+ "id": rpc_id,
53
+ "method": method,
54
+ "params": body,
55
+ }
56
+
57
+ # Create a future to wait for response
58
+ response_future = asyncio.Future()
59
+
60
+ # Store the future in a dict (keyed by rpc_id)
61
+ if not hasattr(_web_server, '_rpc_futures'):
62
+ _web_server._rpc_futures = {}
63
+ _web_server._rpc_futures[rpc_id] = response_future
64
+
65
+ # Send RPC request
66
+ try:
67
+ await _web_server._ws.send(json.dumps(rpc_request))
68
+ except Exception as e:
69
+ _web_server._rpc_futures.pop(rpc_id, None)
70
+ raise HTTPException(503, f"发送 RPC 请求失败: {str(e)}")
71
+
72
+ # Wait for response (with timeout)
73
+ try:
74
+ response = await asyncio.wait_for(response_future, timeout=30.0)
75
+ except asyncio.TimeoutError:
76
+ _web_server._rpc_futures.pop(rpc_id, None)
77
+ raise HTTPException(504, f"RPC 请求超时(30秒),目标服务可能未响应")
78
+ finally:
79
+ _web_server._rpc_futures.pop(rpc_id, None)
80
+
81
+ # Check for RPC error
82
+ if "error" in response:
83
+ error = response["error"]
84
+ error_msg = error.get('message', '未知错误')
85
+ error_code = error.get('code', -1)
86
+ raise HTTPException(500, f"RPC 调用失败: {error_msg} (code: {error_code})")
87
+
88
+ # Return result
89
+ return response.get("result", {})
@@ -0,0 +1,61 @@
1
+ """
2
+ 测试日志 API 路由
3
+ """
4
+ import os
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from fastapi import APIRouter, HTTPException
8
+ from pydantic import BaseModel
9
+
10
+ router = APIRouter()
11
+
12
+
13
+ class TestLogRequest(BaseModel):
14
+ """测试日志请求"""
15
+ content: str
16
+ test_name: str = "registry_test"
17
+
18
+
19
+ @router.post("/save_test_log")
20
+ async def save_test_log(req: TestLogRequest):
21
+ """
22
+ 保存测试日志到服务器
23
+
24
+ 日志保存路径: {KITE_MODULE_DATA}/log/registry_test.log (每次覆盖)
25
+ """
26
+ try:
27
+ # 获取模块数据目录
28
+ module_data = os.environ.get("KITE_MODULE_DATA")
29
+ if not module_data:
30
+ raise HTTPException(status_code=500, detail="KITE_MODULE_DATA not set")
31
+
32
+ # 日志保存到 log 目录(与 latest.log 同级)
33
+ log_dir = Path(module_data) / "log"
34
+ log_dir.mkdir(parents=True, exist_ok=True)
35
+
36
+ # 固定文件名,每次覆盖
37
+ log_file = log_dir / "registry_test.log"
38
+
39
+ # 写入日志(覆盖模式)
40
+ now = datetime.now()
41
+ with open(log_file, "w", encoding="utf-8") as f:
42
+ f.write(f"# Kite 注册中心测试日志\n")
43
+ f.write(f"# 测试时间: {now.isoformat()}\n")
44
+ f.write(f"# {'=' * 70}\n\n")
45
+ f.write(req.content)
46
+
47
+ # 输出到控制台(会被 Launcher 捕获)
48
+ print(f"\n{'=' * 70}")
49
+ print(f"[web] 测试日志已保存")
50
+ print(f"[web] 路径: {log_file}")
51
+ print(f"[web] 时间: {now.strftime('%Y-%m-%d %H:%M:%S')}")
52
+ print(f"{'=' * 70}\n")
53
+
54
+ return {
55
+ "success": True,
56
+ "log_path": str(log_file),
57
+ "timestamp": now.isoformat()
58
+ }
59
+
60
+ except Exception as e:
61
+ raise HTTPException(status_code=500, detail=f"Failed to save log: {str(e)}")
@@ -214,25 +214,3 @@ class ConfigUpdate(BaseModel):
214
214
  """Arbitrary config update payload — accepts any key/value pairs."""
215
215
 
216
216
  model_config = {"extra": "allow"}
217
-
218
-
219
- # ---------------------------------------------------------------------------
220
- # Modules
221
- # ---------------------------------------------------------------------------
222
-
223
- class ModuleMetadataUpdate(BaseModel):
224
- """可更新的 module.md frontmatter 字段."""
225
- display_name: Optional[str] = None
226
- version: Optional[str] = None
227
- state: Optional[str] = None # enabled | manual | disabled
228
- preferred_port: Optional[int] = None
229
- advertise_ip: Optional[str] = None
230
- monitor: Optional[bool] = None
231
- events: Optional[list[str]] = None
232
- subscriptions: Optional[list[str]] = None
233
- depends_on: Optional[list[str]] = None
234
-
235
-
236
- class ModuleConfigUpdate(BaseModel):
237
- """config.yaml 更新,接受任意 key/value."""
238
- model_config = {"extra": "allow"}