@agentunion/kite 1.4.0 → 1.5.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 (235) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/cli.js +44 -5
  3. package/core/dependency_checker.py +250 -0
  4. package/core/env_checker.py +490 -0
  5. package/dependencies_lock.json +128 -0
  6. package/extensions/agents/assistant/server.py +33 -17
  7. package/extensions/channels/acp_channel/server.py +33 -17
  8. package/extensions/services/backup/entry.py +23 -16
  9. package/extensions/services/evol/auth_manager.py +443 -0
  10. package/extensions/services/evol/config.yaml +149 -0
  11. package/extensions/services/evol/config_loader.py +117 -0
  12. package/extensions/services/evol/entry.py +406 -0
  13. package/extensions/services/evol/evol_api.py +173 -0
  14. package/extensions/services/evol/evol_config.json5 +29 -0
  15. package/extensions/services/evol/migrate_tokens.py +122 -0
  16. package/extensions/services/evol/module.md +32 -0
  17. package/extensions/services/evol/pairing.py +250 -0
  18. package/extensions/services/evol/pairing_codes.jsonl +1 -0
  19. package/extensions/services/evol/relay.py +682 -0
  20. package/extensions/services/evol/relay_config.json5 +67 -0
  21. package/extensions/services/evol/routes/__init__.py +1 -0
  22. package/extensions/services/evol/routes/routes_management_ws.py +127 -0
  23. package/extensions/services/evol/routes/routes_rpc.py +89 -0
  24. package/extensions/services/evol/routes/routes_test.py +61 -0
  25. package/extensions/services/evol/server.py +875 -0
  26. package/extensions/services/evol/static/css/style.css +1200 -0
  27. package/extensions/services/evol/static/index.html +781 -0
  28. package/extensions/services/evol/static/index_evol.html +14 -0
  29. package/extensions/services/evol/static/js/app.js +6304 -0
  30. package/extensions/services/evol/static/js/auth.js +326 -0
  31. package/extensions/services/evol/static/js/dialog.js +285 -0
  32. package/extensions/services/evol/static/js/evol-app-fixed.js +50 -0
  33. package/extensions/services/evol/static/js/evol-app.js +1949 -0
  34. package/extensions/services/evol/static/js/evol-app.js.bak +1800 -0
  35. package/extensions/services/evol/static/js/kernel-client-example.js +228 -0
  36. package/extensions/services/evol/static/js/kernel-client.js +396 -0
  37. package/extensions/services/evol/static/js/main.js +141 -0
  38. package/extensions/services/evol/static/js/registry-tests.js +585 -0
  39. package/extensions/services/evol/static/js/stats.js +217 -0
  40. package/extensions/services/evol/static/js/token-manager.js +175 -0
  41. package/extensions/services/evol/static/pairing.html +248 -0
  42. package/extensions/services/evol/static/test_registry.html +262 -0
  43. package/extensions/services/evol/static/test_relay.html +462 -0
  44. package/extensions/services/evol/stats_manager.py +240 -0
  45. package/extensions/services/model_service/entry.py +23 -1
  46. package/extensions/services/proxy/.claude/settings.local.json +13 -0
  47. package/extensions/services/proxy/CHANGELOG_20260308.md +258 -0
  48. package/extensions/services/proxy/_fix_prints.py +133 -0
  49. package/extensions/services/proxy/_fix_prints2.py +87 -0
  50. package/extensions/services/proxy/agentcp/LICENCE +178 -0
  51. package/extensions/services/proxy/agentcp/README copy.md +85 -0
  52. package/extensions/services/proxy/agentcp/README.md +260 -0
  53. package/extensions/services/proxy/agentcp/__init__.py +16 -0
  54. package/extensions/services/proxy/agentcp/agent.py +4 -0
  55. package/extensions/services/proxy/agentcp/agentcp.py +2494 -0
  56. package/extensions/services/proxy/agentcp/agentprofile.json +89 -0
  57. package/extensions/services/proxy/agentcp/ap/__init__.py +16 -0
  58. package/extensions/services/proxy/agentcp/ap/ap_client.py +316 -0
  59. package/extensions/services/proxy/agentcp/assets/images/wechat_qr.png +0 -0
  60. package/extensions/services/proxy/agentcp/backup/metrics.json +31 -0
  61. package/extensions/services/proxy/agentcp/base/__init__.py +20 -0
  62. package/extensions/services/proxy/agentcp/base/auth_client.py +257 -0
  63. package/extensions/services/proxy/agentcp/base/client.py +112 -0
  64. package/extensions/services/proxy/agentcp/base/env.py +34 -0
  65. package/extensions/services/proxy/agentcp/base/html_util.py +336 -0
  66. package/extensions/services/proxy/agentcp/base/log.py +98 -0
  67. package/extensions/services/proxy/agentcp/ca/__init__.py +17 -0
  68. package/extensions/services/proxy/agentcp/ca/ca_client.py +414 -0
  69. package/extensions/services/proxy/agentcp/ca/ca_root.py +74 -0
  70. package/extensions/services/proxy/agentcp/context/__init__.py +20 -0
  71. package/extensions/services/proxy/agentcp/context/context.py +73 -0
  72. package/extensions/services/proxy/agentcp/context/exceptions.py +114 -0
  73. package/extensions/services/proxy/agentcp/create_profile.py +125 -0
  74. package/extensions/services/proxy/agentcp/create_profile_weather.py +125 -0
  75. package/extensions/services/proxy/agentcp/db/__init__.py +15 -0
  76. package/extensions/services/proxy/agentcp/db/db_mananger.py +550 -0
  77. package/extensions/services/proxy/agentcp/docs/UDP_HEARTBEAT_FIX_REPORT.md +265 -0
  78. package/extensions/services/proxy/agentcp/docs/heartbeat_issue_analysis.md +291 -0
  79. package/extensions/services/proxy/agentcp/file/__init__.py +16 -0
  80. package/extensions/services/proxy/agentcp/file/file_client.py +141 -0
  81. package/extensions/services/proxy/agentcp/file/wss_binary_message.py +137 -0
  82. package/extensions/services/proxy/agentcp/hcp.py +299 -0
  83. package/extensions/services/proxy/agentcp/heartbeat/__init__.py +16 -0
  84. package/extensions/services/proxy/agentcp/heartbeat/heartbeat_client.py +360 -0
  85. package/extensions/services/proxy/agentcp/improved_scheduler.py +498 -0
  86. package/extensions/services/proxy/agentcp/llm_agent_utils.py +249 -0
  87. package/extensions/services/proxy/agentcp/llm_server.py +172 -0
  88. package/extensions/services/proxy/agentcp/mermaid.py +210 -0
  89. package/extensions/services/proxy/agentcp/message.py +149 -0
  90. package/extensions/services/proxy/agentcp/metrics.py +256 -0
  91. package/extensions/services/proxy/agentcp/monitoring/__init__.py +20 -0
  92. package/extensions/services/proxy/agentcp/monitoring/global_monitor.py +27 -0
  93. package/extensions/services/proxy/agentcp/monitoring/metrics_store.py +325 -0
  94. package/extensions/services/proxy/agentcp/monitoring/monitoring_service.py +269 -0
  95. package/extensions/services/proxy/agentcp/monitoring/sliding_window.py +222 -0
  96. package/extensions/services/proxy/agentcp/monitoring/standalone_reader.py +224 -0
  97. package/extensions/services/proxy/agentcp/msg/__init__.py +21 -0
  98. package/extensions/services/proxy/agentcp/msg/connection_manager.py +456 -0
  99. package/extensions/services/proxy/agentcp/msg/message_client.py +2058 -0
  100. package/extensions/services/proxy/agentcp/msg/message_serialize.py +263 -0
  101. package/extensions/services/proxy/agentcp/msg/open_ai_message.py +88 -0
  102. package/extensions/services/proxy/agentcp/msg/session_manager.py +1062 -0
  103. package/extensions/services/proxy/agentcp/msg/stream_client.py +267 -0
  104. package/extensions/services/proxy/agentcp/msg/websocket_file_receiver.py +89 -0
  105. package/extensions/services/proxy/agentcp/msg/ws_logger.py +685 -0
  106. package/extensions/services/proxy/agentcp/msg/wss_binary_message.py +137 -0
  107. package/extensions/services/proxy/agentcp/requirements.txt +7 -0
  108. package/extensions/services/proxy/agentcp/samples/agent_graph/README.md +37 -0
  109. package/extensions/services/proxy/agentcp/samples/agent_graph/agentprofile.json +89 -0
  110. package/extensions/services/proxy/agentcp/samples/agent_graph/create_profile.py +138 -0
  111. package/extensions/services/proxy/agentcp/samples/agent_graph/main.py +164 -0
  112. package/extensions/services/proxy/agentcp/samples/agent_use/create_profile.py +123 -0
  113. package/extensions/services/proxy/agentcp/samples/agent_use/llm/create_profile.py +129 -0
  114. package/extensions/services/proxy/agentcp/samples/agent_use/llm/env.json +5 -0
  115. package/extensions/services/proxy/agentcp/samples/agent_use/llm/main.py +146 -0
  116. package/extensions/services/proxy/agentcp/samples/agent_use/main.py +123 -0
  117. package/extensions/services/proxy/agentcp/samples/agent_use/readme.md +379 -0
  118. package/extensions/services/proxy/agentcp/samples/agent_use/search/create_profile.py +129 -0
  119. package/extensions/services/proxy/agentcp/samples/agent_use/search/main.py +28 -0
  120. package/extensions/services/proxy/agentcp/samples/agent_use/tool/create_profile.py +129 -0
  121. package/extensions/services/proxy/agentcp/samples/agent_use/tool/main.py +20 -0
  122. package/extensions/services/proxy/agentcp/samples/ali_amap/README.md +97 -0
  123. package/extensions/services/proxy/agentcp/samples/ali_amap/amap_agent.py +88 -0
  124. package/extensions/services/proxy/agentcp/samples/ali_amap/create_profile.py +125 -0
  125. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/powershell.py +228 -0
  126. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/software.py +63 -0
  127. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/tools.py +36 -0
  128. package/extensions/services/proxy/agentcp/samples/compute_agent/browser_user.py +41 -0
  129. package/extensions/services/proxy/agentcp/samples/deepseek/README.md +79 -0
  130. package/extensions/services/proxy/agentcp/samples/deepseek/create_profile.py +126 -0
  131. package/extensions/services/proxy/agentcp/samples/deepseek/deepseek.py +42 -0
  132. package/extensions/services/proxy/agentcp/samples/dify_chat/README.md +78 -0
  133. package/extensions/services/proxy/agentcp/samples/dify_chat/create_profile.py +126 -0
  134. package/extensions/services/proxy/agentcp/samples/dify_chat/dify_chat.py +47 -0
  135. package/extensions/services/proxy/agentcp/samples/dify_workflow/README.md +78 -0
  136. package/extensions/services/proxy/agentcp/samples/dify_workflow/create_profile.py +126 -0
  137. package/extensions/services/proxy/agentcp/samples/dify_workflow/dify_workflow.py +46 -0
  138. package/extensions/services/proxy/agentcp/samples/executor/README.md +44 -0
  139. package/extensions/services/proxy/agentcp/samples/executor/agentprofile.json +89 -0
  140. package/extensions/services/proxy/agentcp/samples/executor/create_profile.py +139 -0
  141. package/extensions/services/proxy/agentcp/samples/executor/main.py +160 -0
  142. package/extensions/services/proxy/agentcp/samples/filereader/README.md +45 -0
  143. package/extensions/services/proxy/agentcp/samples/filereader/agentprofile.json +90 -0
  144. package/extensions/services/proxy/agentcp/samples/filereader/create_profile.py +137 -0
  145. package/extensions/services/proxy/agentcp/samples/filereader/main.py +253 -0
  146. package/extensions/services/proxy/agentcp/samples/filewriter/README.md +38 -0
  147. package/extensions/services/proxy/agentcp/samples/filewriter/agentprofile.json +91 -0
  148. package/extensions/services/proxy/agentcp/samples/filewriter/create_profile.py +138 -0
  149. package/extensions/services/proxy/agentcp/samples/filewriter/main.py +289 -0
  150. package/extensions/services/proxy/agentcp/samples/hcp/README.md +85 -0
  151. package/extensions/services/proxy/agentcp/samples/hcp/acp_weather_agent.zip +0 -0
  152. package/extensions/services/proxy/agentcp/samples/hcp/create_profile.py +125 -0
  153. package/extensions/services/proxy/agentcp/samples/hcp/hcp.py +237 -0
  154. package/extensions/services/proxy/agentcp/samples/helloworld/README.md +68 -0
  155. package/extensions/services/proxy/agentcp/samples/helloworld/hello_world.py +40 -0
  156. package/extensions/services/proxy/agentcp/samples/llm_agent/MEADME.md +117 -0
  157. package/extensions/services/proxy/agentcp/samples/llm_agent/create_profile.py +125 -0
  158. package/extensions/services/proxy/agentcp/samples/llm_agent/qwen_agent.py +136 -0
  159. package/extensions/services/proxy/agentcp/samples/local_llm_agent/README.md +90 -0
  160. package/extensions/services/proxy/agentcp/samples/local_llm_agent/create_profile.py +125 -0
  161. package/extensions/services/proxy/agentcp/samples/local_llm_agent/main.py +49 -0
  162. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/README.md +55 -0
  163. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/create_profile.py +125 -0
  164. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/main.py +23 -0
  165. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/README.md +103 -0
  166. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/create_profile.py +125 -0
  167. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/main.py +69 -0
  168. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/README.md +58 -0
  169. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/create_profile.py +125 -0
  170. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/main.py +25 -0
  171. package/extensions/services/proxy/agentcp/samples/qwen3/README.md +71 -0
  172. package/extensions/services/proxy/agentcp/samples/qwen3/create_profile.py +126 -0
  173. package/extensions/services/proxy/agentcp/samples/qwen3/qwen3.py +37 -0
  174. package/extensions/services/proxy/agentcp/samples/qwen3_tools/README.md +133 -0
  175. package/extensions/services/proxy/agentcp/samples/qwen3_tools/create_profile.py +126 -0
  176. package/extensions/services/proxy/agentcp/samples/qwen3_tools/qwen3_tools.py +98 -0
  177. package/extensions/services/proxy/agentcp/samples/search/create_profile_qwen.py +125 -0
  178. package/extensions/services/proxy/agentcp/samples/search/create_profile_search.py +125 -0
  179. package/extensions/services/proxy/agentcp/samples/search/qwen_agent.py +136 -0
  180. package/extensions/services/proxy/agentcp/samples/search/search_agent.py +170 -0
  181. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/README.md +89 -0
  182. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/create_profile.py +125 -0
  183. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/main.py +44 -0
  184. package/extensions/services/proxy/agentcp/utils/__init__.py +15 -0
  185. package/extensions/services/proxy/agentcp/utils/file_util.py +117 -0
  186. package/extensions/services/proxy/agentcp/utils/proxy_bypass.py +99 -0
  187. package/extensions/services/proxy/agentcp/workflow.py +203 -0
  188. package/extensions/services/proxy/console_auth.py +109 -0
  189. package/extensions/services/proxy/evol/__init__.py +1 -0
  190. package/extensions/services/proxy/evol/config.py +37 -0
  191. package/extensions/services/proxy/evol/http/__init__.py +1 -0
  192. package/extensions/services/proxy/evol/http/async_http.py +551 -0
  193. package/extensions/services/proxy/evol/log.py +28 -0
  194. package/extensions/services/proxy/evol/presenter/__init__.py +2 -0
  195. package/extensions/services/proxy/evol/presenter/agentIdPresenter.py +1031 -0
  196. package/extensions/services/proxy/evol/presenter/apikeyPresenter.py +106 -0
  197. package/extensions/services/proxy/evol/presenter/configPresenter.py +1281 -0
  198. package/extensions/services/proxy/evol/presenter/userPresenter.py +477 -0
  199. package/extensions/services/proxy/evol/server/__init__.py +1 -0
  200. package/extensions/services/proxy/evol/server/claude_proxy_async.py +3430 -0
  201. package/extensions/services/proxy/evol/server/openclaw_proxy.py +1861 -0
  202. package/extensions/services/proxy/evol/server/proxy_config.py +15 -0
  203. package/extensions/services/proxy/evol/server/proxy_engine.py +501 -0
  204. package/extensions/services/proxy/evol/version.py +24 -0
  205. package/extensions/services/proxy/logs/websocket.log +260 -0
  206. package/extensions/services/proxy/main.py +240 -0
  207. package/extensions/services/proxy/requirements.txt +13 -0
  208. package/extensions/services/proxy/server.py +271 -0
  209. package/extensions/services/watchdog/entry.py +42 -16
  210. package/extensions/services/watchdog/module.md +1 -0
  211. package/extensions/services/watchdog/monitor.py +34 -4
  212. package/extensions/services/web/module.md +1 -1
  213. package/extensions/services/web/server.py +30 -18
  214. package/extensions/services/web/static/js/token-manager.js +10 -10
  215. package/kernel/entry.py +1 -1
  216. package/kernel/module.md +25 -1
  217. package/kernel/registry_store.py +2 -26
  218. package/kernel/rpc_router.py +36 -10
  219. package/kernel/server.py +106 -17
  220. package/kite_cli/commands/deps_install.py +67 -0
  221. package/kite_cli/commands/env_check.py +45 -0
  222. package/kite_cli/commands/prepare.py +49 -0
  223. package/kite_cli/commands/venv_setup.py +56 -0
  224. package/kite_cli/main.py +29 -1
  225. package/launcher/entry.py +306 -21
  226. package/launcher/module.md +9 -0
  227. package/launcher/module_scanner.py +11 -1
  228. package/main.py +4 -1
  229. package/package.json +8 -1
  230. package/python_version.json +4 -0
  231. package/requirements.txt +38 -0
  232. package/scripts/env-manager.js +328 -0
  233. package/scripts/python-env.js +79 -0
  234. package/scripts/scan_dependencies.py +461 -0
  235. package/scripts/setup-python-env.js +191 -0
@@ -89,7 +89,7 @@ class AcpChannelServer:
89
89
  """Single WebSocket session: connect, subscribe, register, ready, receive loop."""
90
90
  url = f"ws://127.0.0.1:{self.kernel_port}/ws?token={self.token}&id=acp_channel"
91
91
  print(f"[acp_channel] Connecting to Kernel (port {self.kernel_port})")
92
- async with websockets.connect(url, open_timeout=3, ping_interval=20, ping_timeout=20, close_timeout=10) as ws:
92
+ async with websockets.connect(url, open_timeout=3, ping_interval=None, close_timeout=10) as ws:
93
93
  self._ws = ws
94
94
  elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
95
95
  elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
@@ -119,17 +119,18 @@ class AcpChannelServer:
119
119
 
120
120
  # Step 3: Publish module.ready (every reconnect)
121
121
  if not self._shutting_down:
122
+ startup_time = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
122
123
  await self._rpc_call(ws, "event.publish", {
123
124
  "event_id": str(uuid.uuid4()),
124
125
  "event": "module.ready",
125
126
  "data": {
126
127
  "module_id": "acp_channel",
127
128
  "graceful_shutdown": True,
129
+ "startup_time": startup_time,
128
130
  },
129
131
  })
130
- elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
131
- elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
132
- print(f"[acp_channel] module.ready published{elapsed_str}")
132
+ elapsed_str = self._fmt_elapsed(self.boot_t0)
133
+ print(f"[acp_channel] module.ready published ({elapsed_str})")
133
134
 
134
135
  # Receive loop
135
136
  # CRITICAL: RPC 死锁防范
@@ -138,9 +139,6 @@ class AcpChannelServer:
138
139
  # - 如果接收循环被 await handler 阻塞,出站响应永远收不到 → 超时死锁
139
140
  # - 事件通知和 RPC 响应可以同步处理(它们不会反向调用 rpc_call)
140
141
 
141
- # Start heartbeat loop
142
- heartbeat_task = asyncio.create_task(self._heartbeat_loop(ws))
143
-
144
142
  async for raw in ws:
145
143
  try:
146
144
  msg = json.loads(raw)
@@ -157,6 +155,11 @@ class AcpChannelServer:
157
155
  event_type = params.get("event", "")
158
156
  data = params.get("data", {})
159
157
 
158
+ # Handle system.ping event
159
+ if event_type == "system.ping":
160
+ await self._handle_ping_event(data)
161
+ continue
162
+
160
163
  # Layer 1: 处理订阅的事件
161
164
  if event_type == "module.shutdown":
162
165
  target = data.get("module_id", "")
@@ -223,16 +226,29 @@ class AcpChannelServer:
223
226
  msg["params"] = params
224
227
  await ws.send(json.dumps(msg))
225
228
 
226
- async def _heartbeat_loop(self, ws):
227
- """Send registry.heartbeat every 30 seconds to prevent TTL expiration."""
228
- while True:
229
- try:
230
- await asyncio.sleep(30)
231
- if not self._shutting_down:
232
- await self._rpc_call(ws, "registry.heartbeat", {"module_id": "acp_channel"})
233
- except Exception as e:
234
- print(f"[acp_channel] Heartbeat error: {e}")
235
- break
229
+ async def _handle_ping_event(self, data: dict):
230
+ """Handle system.ping event and reply with system.pong."""
231
+ t1 = data.get("ping_time")
232
+ t2 = time.time()
233
+
234
+ await self._rpc_call(self._ws, "event.publish", {
235
+ "event_id": str(uuid.uuid4()),
236
+ "event": "system.pong",
237
+ "data": {
238
+ "module_id": "acp_channel",
239
+ "ping_time": t1,
240
+ "pong_time": t2,
241
+ },
242
+ })
243
+
244
+ def _fmt_elapsed(self, t0: float) -> str:
245
+ """Format elapsed time since t0."""
246
+ d = time.monotonic() - t0 if t0 else 0
247
+ if d < 1:
248
+ return f"{d * 1000:.0f}ms"
249
+ if d < 10:
250
+ return f"{d:.1f}s"
251
+ return f"{d:.0f}s"
236
252
 
237
253
  async def _publish_event(self, event: dict):
238
254
  """Publish an event via JSON-RPC event.publish."""
@@ -494,7 +494,7 @@ async def _ws_connect(token: str, kernel_port: int, _t0: float):
494
494
  ws_url = f"ws://127.0.0.1:{kernel_port}/ws?token={token}&id=backup"
495
495
  print(f"[backup] Connecting to Kernel: {ws_url}")
496
496
 
497
- async with websockets.connect(ws_url, open_timeout=5, ping_interval=20, ping_timeout=20, close_timeout=10) as ws:
497
+ async with websockets.connect(ws_url, open_timeout=5, ping_interval=None, close_timeout=10) as ws:
498
498
  _ws_global = ws
499
499
  print(f"[backup] Connected to Kernel ({_fmt_elapsed(_t0)})")
500
500
 
@@ -541,12 +541,14 @@ async def _ws_connect(token: str, kernel_port: int, _t0: float):
541
541
 
542
542
  # Publish module.ready (every reconnect)
543
543
  if not _shutting_down:
544
+ startup_time = time.monotonic() - _t0
544
545
  await _rpc_call(ws, "event.publish", {
545
546
  "event_id": str(uuid.uuid4()),
546
547
  "event": "module.ready",
547
548
  "data": {
548
549
  "module_id": "backup",
549
550
  "graceful_shutdown": True,
551
+ "startup_time": startup_time,
550
552
  },
551
553
  })
552
554
  print(f"[backup] module.ready published ({_fmt_elapsed(_t0)})")
@@ -554,9 +556,6 @@ async def _ws_connect(token: str, kernel_port: int, _t0: float):
554
556
  # Start test event loop in background
555
557
  test_task = asyncio.create_task(_test_event_loop(ws))
556
558
 
557
- # Start heartbeat loop
558
- heartbeat_task = asyncio.create_task(_heartbeat_loop(ws))
559
-
560
559
  # Message loop: handle incoming RPC + events
561
560
  # CRITICAL: RPC 死锁防范
562
561
  # - 入站 RPC 请求必须用 create_task() 异步执行,不可 await
@@ -592,18 +591,6 @@ async def _rpc_call(ws, method: str, params: dict = None):
592
591
  await ws.send(json.dumps(msg))
593
592
 
594
593
 
595
- async def _heartbeat_loop(ws):
596
- """Send registry.heartbeat every 30 seconds to prevent TTL expiration."""
597
- while True:
598
- try:
599
- await asyncio.sleep(30)
600
- if not _shutting_down:
601
- await _rpc_call(ws, "registry.heartbeat", {"module_id": "backup"})
602
- except Exception as e:
603
- print(f"[backup] Heartbeat error: {e}")
604
- break
605
-
606
-
607
594
  async def _publish_event(ws, event: dict):
608
595
  """Publish an event via RPC event.publish."""
609
596
  await _rpc_call(ws, "event.publish", {
@@ -613,12 +600,32 @@ async def _publish_event(ws, event: dict):
613
600
  })
614
601
 
615
602
 
603
+ async def _handle_ping_event(data: dict):
604
+ """Handle system.ping event and reply with system.pong."""
605
+ t1 = data.get("ping_time")
606
+ t2 = time.time()
607
+
608
+ await _publish_event(_ws_global, {
609
+ "event": "system.pong",
610
+ "data": {
611
+ "module_id": MODULE_NAME,
612
+ "ping_time": t1,
613
+ "pong_time": t2,
614
+ },
615
+ })
616
+
617
+
616
618
  async def _handle_event_notification(msg: dict):
617
619
  """Handle an event notification (JSON-RPC 2.0 Notification with method='event')."""
618
620
  params = msg.get("params", {})
619
621
  event_type = params.get("event", "")
620
622
  data = params.get("data", {})
621
623
 
624
+ # Handle system.ping event
625
+ if event_type == "system.ping":
626
+ await _handle_ping_event(data)
627
+ return
628
+
622
629
  # Special handling for module.shutdown
623
630
  if event_type == "module.shutdown":
624
631
  target = data.get("module_id", "")
@@ -0,0 +1,443 @@
1
+ """
2
+ 认证管理模块 - Token 缓存、验证、吊销
3
+
4
+ 使用 JSONL 格式存储 Token 历史记录,每次变更追加一条记录。
5
+ 查询时使用每个 Token 的最新记录。
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import secrets
11
+ import time
12
+ from datetime import datetime, timezone
13
+ from typing import Optional
14
+
15
+
16
+ class AuthManager:
17
+ """认证管理器"""
18
+
19
+ def __init__(self, data_dir: str):
20
+ """
21
+ 初始化认证管理器
22
+
23
+ Args:
24
+ data_dir: 数据目录($KITE_DATA/evol)
25
+ """
26
+ self.data_dir = data_dir
27
+ self.auth_dir = os.path.join(data_dir, "auth")
28
+ os.makedirs(self.auth_dir, exist_ok=True)
29
+
30
+ self.evol_tokens_file = os.path.join(self.auth_dir, "evol_tokens.jsonl")
31
+ self.kite_tokens_file = os.path.join(self.auth_dir, "kite_tokens.jsonl")
32
+
33
+ self.kite_token_ttl = 30 * 24 * 3600 # 30 days
34
+ self.evol_token_ttl = 7 * 24 * 3600 # 7 days
35
+ self.token_update_interval = 24 * 3600 # 1 day - Token 更新间隔
36
+
37
+ # 内存缓存
38
+ self._kite_tokens_cache = None
39
+ self._kite_tokens_cache_time = 0
40
+ self._evol_tokens_cache = {} # 改为字典:{phone: record}
41
+ self._evol_tokens_cache_time = 0
42
+ self.cache_ttl = 60 * 60 # 1 小时缓存
43
+
44
+ def save_evol_token(self, phone: str, token: str, data: dict):
45
+ """保存 Evol Token 到 JSONL"""
46
+ now = time.time()
47
+ record = {
48
+ "phone": phone,
49
+ "token": token,
50
+ "action": "login",
51
+ "obtainedAt": now,
52
+ "obtainedAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
53
+ "expiresAt": now + self.evol_token_ttl,
54
+ "expiresAt_human": datetime.fromtimestamp(now + self.evol_token_ttl, timezone.utc).isoformat(),
55
+ "lastUsedAt": now,
56
+ "lastUsedAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
57
+ "userInfo": data.get("userInfo", {}),
58
+ "accountInfo": data.get("accountInfo", {}),
59
+ "teamInfo": data.get("teamInfo", {}),
60
+ "gatewayInfo": data.get("gatewayInfo", {}),
61
+ "timestamp": now
62
+ }
63
+
64
+ self._append_evol_record(record)
65
+ # 清除缓存
66
+ self._evol_tokens_cache = {}
67
+ self._evol_tokens_cache_time = 0
68
+
69
+ def get_evol_token(self, phone: str) -> Optional[dict]:
70
+ """获取指定手机号的最新 Evol Token(带缓存)"""
71
+ now = time.time()
72
+
73
+ # 检查缓存是否有效
74
+ if self._evol_tokens_cache and (now - self._evol_tokens_cache_time < self.cache_ttl):
75
+ return self._evol_tokens_cache.get(phone)
76
+
77
+ # 从文件读取所有 Evol Token
78
+ if not os.path.exists(self.evol_tokens_file):
79
+ return None
80
+
81
+ try:
82
+ # 按 phone 分组,保留每个 phone 的最新记录
83
+ phone_tokens = {}
84
+ with open(self.evol_tokens_file, "r", encoding="utf-8") as f:
85
+ for line in f:
86
+ line = line.strip()
87
+ if not line:
88
+ continue
89
+ try:
90
+ record = json.loads(line)
91
+ record_phone = record.get("phone")
92
+ if not record_phone:
93
+ continue
94
+ # 使用最新的记录(后面的覆盖前面的)
95
+ phone_tokens[record_phone] = record
96
+ except json.JSONDecodeError:
97
+ continue
98
+
99
+ # 过滤掉已过期和已登出的 token
100
+ valid_tokens = {}
101
+ for p, record in phone_tokens.items():
102
+ # 检查是否登出
103
+ if record.get("action") == "logout":
104
+ continue
105
+ # 检查是否过期
106
+ if time.time() > record.get("expiresAt", 0):
107
+ continue
108
+ valid_tokens[p] = record
109
+
110
+ # 更新缓存
111
+ self._evol_tokens_cache = valid_tokens
112
+ self._evol_tokens_cache_time = now
113
+
114
+ return valid_tokens.get(phone)
115
+
116
+ except Exception:
117
+ return None
118
+
119
+ def update_evol_token_usage(self, phone: str):
120
+ """更新 Evol Token 使用时间(超过 1 天才记录)"""
121
+ latest_record = self.get_evol_token(phone)
122
+ if not latest_record:
123
+ return
124
+
125
+ now = time.time()
126
+ last_used = latest_record.get("lastUsedAt", 0)
127
+
128
+ # 如果距离上次使用不到 1 天,不记录
129
+ if now - last_used < self.token_update_interval:
130
+ return
131
+
132
+ # 追加使用记录
133
+ record = {
134
+ "phone": phone,
135
+ "token": latest_record.get("token"),
136
+ "action": "used",
137
+ "obtainedAt": latest_record.get("obtainedAt"),
138
+ "obtainedAt_human": latest_record.get("obtainedAt_human"),
139
+ "expiresAt": latest_record.get("expiresAt"),
140
+ "expiresAt_human": latest_record.get("expiresAt_human"),
141
+ "lastUsedAt": now,
142
+ "lastUsedAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
143
+ "userInfo": latest_record.get("userInfo", {}),
144
+ "accountInfo": latest_record.get("accountInfo", {}),
145
+ "teamInfo": latest_record.get("teamInfo", {}),
146
+ "gatewayInfo": latest_record.get("gatewayInfo", {}),
147
+ "timestamp": now
148
+ }
149
+
150
+ self._append_evol_record(record)
151
+ # 清除缓存
152
+ self._evol_tokens_cache = {}
153
+ self._evol_tokens_cache_time = 0
154
+
155
+ def _append_evol_record(self, record: dict):
156
+ """追加 Evol Token 记录到 JSONL"""
157
+ try:
158
+ with open(self.evol_tokens_file, "a", encoding="utf-8") as f:
159
+ f.write(json.dumps(record, ensure_ascii=False) + "\n")
160
+ except Exception as e:
161
+ print(f"[auth] Failed to append evol token record: {e}")
162
+
163
+ def revoke_evol_token(self, phone: str):
164
+ """吊销 Evol Token(追加 logout 记录)"""
165
+ latest_record = self.get_evol_token(phone)
166
+ if not latest_record:
167
+ return
168
+
169
+ now = time.time()
170
+ record = {
171
+ "phone": phone,
172
+ "token": latest_record.get("token"),
173
+ "action": "logout",
174
+ "obtainedAt": latest_record.get("obtainedAt"),
175
+ "obtainedAt_human": latest_record.get("obtainedAt_human"),
176
+ "expiresAt": latest_record.get("expiresAt"),
177
+ "expiresAt_human": latest_record.get("expiresAt_human"),
178
+ "lastUsedAt": now,
179
+ "lastUsedAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
180
+ "userInfo": latest_record.get("userInfo", {}),
181
+ "accountInfo": latest_record.get("accountInfo", {}),
182
+ "teamInfo": latest_record.get("teamInfo", {}),
183
+ "gatewayInfo": latest_record.get("gatewayInfo", {}),
184
+ "timestamp": now
185
+ }
186
+
187
+ self._append_evol_record(record)
188
+ # 清除缓存
189
+ self._evol_tokens_cache = {}
190
+ self._evol_tokens_cache_time = 0
191
+
192
+ def generate_kite_token(self, device_info: dict) -> str:
193
+ """生成 Kite Token 并追加到 JSONL"""
194
+ token = "kite_" + secrets.token_urlsafe(32)
195
+ now = time.time()
196
+
197
+ record = {
198
+ "token": token,
199
+ "deviceId": device_info.get("deviceId", "unknown"),
200
+ "deviceName": device_info.get("deviceName", "Unknown Device"),
201
+ "phone": None, # 初始未绑定手机号
202
+ "action": "created",
203
+ "createdAt": now,
204
+ "createdAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
205
+ "lastUsedAt": now,
206
+ "lastUsedAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
207
+ "expiresAt": now + self.kite_token_ttl,
208
+ "expiresAt_human": datetime.fromtimestamp(now + self.kite_token_ttl, timezone.utc).isoformat(),
209
+ "isValid": True,
210
+ "timestamp": now
211
+ }
212
+
213
+ # 追加到 JSONL
214
+ self._append_token_record(record)
215
+ # 清除缓存
216
+ self._kite_tokens_cache = None
217
+ self._kite_tokens_cache_time = 0
218
+
219
+ return token
220
+
221
+ def verify_kite_token(self, token: str) -> bool:
222
+ """验证 Kite Token 并更新使用时间(超过 1 天才记录)"""
223
+ latest_tokens = self._get_latest_tokens()
224
+ now = time.time()
225
+
226
+ token_info = latest_tokens.get(token)
227
+ if not token_info:
228
+ return False
229
+
230
+ if not token_info.get("isValid", True):
231
+ return False
232
+
233
+ if now > token_info.get("expiresAt", 0):
234
+ return False
235
+
236
+ # 检查距离上次使用是否超过 1 天
237
+ last_used = token_info.get("lastUsedAt", 0)
238
+ if now - last_used < self.token_update_interval:
239
+ # 不到 1 天,不记录
240
+ return True
241
+
242
+ # 追加使用记录
243
+ expires_at = now + self.kite_token_ttl
244
+ record = {
245
+ "token": token,
246
+ "deviceId": token_info.get("deviceId"),
247
+ "deviceName": token_info.get("deviceName"),
248
+ "phone": token_info.get("phone"), # 保留绑定的手机号
249
+ "action": "used",
250
+ "createdAt": token_info.get("createdAt"),
251
+ "createdAt_human": token_info.get("createdAt_human"),
252
+ "lastUsedAt": now,
253
+ "lastUsedAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
254
+ "expiresAt": expires_at,
255
+ "expiresAt_human": datetime.fromtimestamp(expires_at, timezone.utc).isoformat(),
256
+ "isValid": True,
257
+ "timestamp": now
258
+ }
259
+ self._append_token_record(record)
260
+ # 清除缓存
261
+ self._kite_tokens_cache = None
262
+ self._kite_tokens_cache_time = 0
263
+
264
+ return True
265
+
266
+ def revoke_kite_token(self, token: str):
267
+ """吊销 Kite Token"""
268
+ latest_tokens = self._get_latest_tokens()
269
+ token_info = latest_tokens.get(token)
270
+
271
+ if not token_info:
272
+ return
273
+
274
+ now = time.time()
275
+ record = {
276
+ "token": token,
277
+ "deviceId": token_info.get("deviceId"),
278
+ "deviceName": token_info.get("deviceName"),
279
+ "phone": token_info.get("phone"), # 保留绑定的手机号
280
+ "action": "revoked",
281
+ "createdAt": token_info.get("createdAt"),
282
+ "createdAt_human": token_info.get("createdAt_human"),
283
+ "lastUsedAt": token_info.get("lastUsedAt"),
284
+ "lastUsedAt_human": token_info.get("lastUsedAt_human"),
285
+ "expiresAt": token_info.get("expiresAt"),
286
+ "expiresAt_human": token_info.get("expiresAt_human"),
287
+ "isValid": False,
288
+ "timestamp": now
289
+ }
290
+ self._append_token_record(record)
291
+ # 清除缓存
292
+ self._kite_tokens_cache = None
293
+ self._kite_tokens_cache_time = 0
294
+
295
+ def bind_kite_token_to_phone(self, token: str, phone: str):
296
+ """绑定 Kite Token 到手机号"""
297
+ latest_tokens = self._get_latest_tokens()
298
+ token_info = latest_tokens.get(token)
299
+
300
+ if not token_info:
301
+ raise ValueError(f"Token not found: {token}")
302
+
303
+ # 如果已经绑定到同一个手机号,不需要重复记录
304
+ if token_info.get("phone") == phone:
305
+ return
306
+
307
+ now = time.time()
308
+ record = {
309
+ "token": token,
310
+ "deviceId": token_info.get("deviceId"),
311
+ "deviceName": token_info.get("deviceName"),
312
+ "phone": phone, # 绑定手机号
313
+ "action": "bound",
314
+ "createdAt": token_info.get("createdAt"),
315
+ "createdAt_human": token_info.get("createdAt_human"),
316
+ "lastUsedAt": now,
317
+ "lastUsedAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
318
+ "expiresAt": token_info.get("expiresAt"),
319
+ "expiresAt_human": token_info.get("expiresAt_human"),
320
+ "isValid": token_info.get("isValid", True),
321
+ "timestamp": now
322
+ }
323
+ self._append_token_record(record)
324
+ # 清除缓存
325
+ self._kite_tokens_cache = None
326
+ self._kite_tokens_cache_time = 0
327
+
328
+ def get_phone_by_kite_token(self, token: str) -> Optional[str]:
329
+ """根据 Kite Token 获取绑定的手机号"""
330
+ latest_tokens = self._get_latest_tokens()
331
+ token_info = latest_tokens.get(token)
332
+
333
+ if not token_info:
334
+ return None
335
+
336
+ return token_info.get("phone")
337
+
338
+ def list_all_evol_tokens(self) -> list:
339
+ """列出所有有效的 Evol Token(用于前端显示)"""
340
+ if not os.path.exists(self.evol_tokens_file):
341
+ return []
342
+
343
+ try:
344
+ # 按 phone 分组,保留每个 phone 的最新记录
345
+ phone_tokens = {}
346
+ with open(self.evol_tokens_file, "r", encoding="utf-8") as f:
347
+ for line in f:
348
+ line = line.strip()
349
+ if not line:
350
+ continue
351
+ try:
352
+ record = json.loads(line)
353
+ record_phone = record.get("phone")
354
+ if not record_phone:
355
+ continue
356
+ # 使用最新的记录(后面的覆盖前面的)
357
+ phone_tokens[record_phone] = record
358
+ except json.JSONDecodeError:
359
+ continue
360
+
361
+ # 过滤掉已过期和已登出的 token,返回列表
362
+ valid_tokens = []
363
+ for phone, record in phone_tokens.items():
364
+ # 检查是否登出
365
+ if record.get("action") == "logout":
366
+ continue
367
+ # 检查是否过期
368
+ if time.time() > record.get("expiresAt", 0):
369
+ continue
370
+ valid_tokens.append(record)
371
+
372
+ return valid_tokens
373
+
374
+ except Exception:
375
+ return []
376
+
377
+ def list_devices(self, current_token: str) -> list:
378
+ """列出所有已配对设备"""
379
+ latest_tokens = self._get_latest_tokens()
380
+ devices = []
381
+
382
+ for token, info in latest_tokens.items():
383
+ devices.append({
384
+ "deviceId": info.get("deviceId"),
385
+ "deviceName": info.get("deviceName"),
386
+ "createdAt": info.get("createdAt"),
387
+ "lastUsedAt": info.get("lastUsedAt"),
388
+ "expiresAt": info.get("expiresAt"),
389
+ "isValid": info.get("isValid", True),
390
+ "isCurrent": token == current_token
391
+ })
392
+
393
+ return devices
394
+
395
+ def _get_latest_tokens(self) -> dict:
396
+ """
397
+ 获取所有 Token 的最新状态(带缓存)
398
+
399
+ Returns:
400
+ {token: latest_record} 字典
401
+ """
402
+ now = time.time()
403
+
404
+ # 检查缓存是否有效
405
+ if self._kite_tokens_cache and (now - self._kite_tokens_cache_time < self.cache_ttl):
406
+ return self._kite_tokens_cache
407
+
408
+ # 从文件读取
409
+ if not os.path.exists(self.kite_tokens_file):
410
+ return {}
411
+
412
+ tokens = {}
413
+ try:
414
+ with open(self.kite_tokens_file, "r", encoding="utf-8") as f:
415
+ for line in f:
416
+ line = line.strip()
417
+ if not line:
418
+ continue
419
+
420
+ try:
421
+ record = json.loads(line)
422
+ token = record.get("token")
423
+ if token:
424
+ # 使用最新的记录(后面的覆盖前面的)
425
+ tokens[token] = record
426
+ except json.JSONDecodeError:
427
+ continue
428
+ except Exception:
429
+ return {}
430
+
431
+ # 更新缓存
432
+ self._kite_tokens_cache = tokens
433
+ self._kite_tokens_cache_time = now
434
+
435
+ return tokens
436
+
437
+ def _append_token_record(self, record: dict):
438
+ """追加 Token 记录到 JSONL"""
439
+ try:
440
+ with open(self.kite_tokens_file, "a", encoding="utf-8") as f:
441
+ f.write(json.dumps(record, ensure_ascii=False) + "\n")
442
+ except Exception as e:
443
+ print(f"[auth] Failed to append token record: {e}")