@agentunion/kite 1.3.2 → 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 (293) hide show
  1. package/CHANGELOG.md +302 -0
  2. package/cli.js +119 -4
  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/entry.py +111 -1
  7. package/extensions/agents/assistant/server.py +279 -215
  8. package/extensions/channels/acp_channel/entry.py +111 -1
  9. package/extensions/channels/acp_channel/module.md +23 -22
  10. package/extensions/channels/acp_channel/server.py +279 -215
  11. package/extensions/event_hub_bench/entry.py +107 -1
  12. package/extensions/services/backup/entry.py +306 -21
  13. package/extensions/services/backup/module.md +24 -22
  14. package/extensions/services/evol/auth_manager.py +443 -0
  15. package/extensions/services/evol/config.yaml +149 -0
  16. package/extensions/services/evol/config_loader.py +117 -0
  17. package/extensions/services/evol/entry.py +406 -0
  18. package/extensions/services/evol/evol_api.py +173 -0
  19. package/extensions/services/evol/evol_config.json5 +29 -0
  20. package/extensions/services/evol/migrate_tokens.py +122 -0
  21. package/extensions/services/evol/module.md +32 -0
  22. package/extensions/services/evol/pairing.py +250 -0
  23. package/extensions/services/evol/pairing_codes.jsonl +1 -0
  24. package/extensions/services/evol/relay.py +682 -0
  25. package/extensions/services/evol/relay_config.json5 +67 -0
  26. package/extensions/services/evol/routes/__init__.py +1 -0
  27. package/extensions/services/evol/routes/routes_management_ws.py +127 -0
  28. package/extensions/services/evol/routes/routes_rpc.py +89 -0
  29. package/extensions/services/evol/routes/routes_test.py +61 -0
  30. package/extensions/services/evol/server.py +875 -0
  31. package/extensions/services/evol/static/css/style.css +1200 -0
  32. package/extensions/services/evol/static/index.html +781 -0
  33. package/extensions/services/evol/static/index_evol.html +14 -0
  34. package/extensions/services/evol/static/js/app.js +6304 -0
  35. package/extensions/services/evol/static/js/auth.js +326 -0
  36. package/extensions/services/evol/static/js/dialog.js +285 -0
  37. package/extensions/services/evol/static/js/evol-app-fixed.js +50 -0
  38. package/extensions/services/evol/static/js/evol-app.js +1949 -0
  39. package/extensions/services/evol/static/js/evol-app.js.bak +1800 -0
  40. package/extensions/services/evol/static/js/kernel-client-example.js +228 -0
  41. package/extensions/services/evol/static/js/kernel-client.js +396 -0
  42. package/extensions/services/evol/static/js/main.js +141 -0
  43. package/extensions/services/evol/static/js/registry-tests.js +585 -0
  44. package/extensions/services/evol/static/js/stats.js +217 -0
  45. package/extensions/services/evol/static/js/token-manager.js +175 -0
  46. package/extensions/services/evol/static/pairing.html +248 -0
  47. package/extensions/services/evol/static/test_registry.html +262 -0
  48. package/extensions/services/evol/static/test_relay.html +462 -0
  49. package/extensions/services/evol/stats_manager.py +240 -0
  50. package/extensions/services/model_service/entry.py +167 -19
  51. package/extensions/services/model_service/module.md +21 -22
  52. package/extensions/services/proxy/.claude/settings.local.json +13 -0
  53. package/extensions/services/proxy/CHANGELOG_20260308.md +258 -0
  54. package/extensions/services/proxy/_fix_prints.py +133 -0
  55. package/extensions/services/proxy/_fix_prints2.py +87 -0
  56. package/extensions/services/proxy/agentcp/LICENCE +178 -0
  57. package/extensions/services/proxy/agentcp/README copy.md +85 -0
  58. package/extensions/services/proxy/agentcp/README.md +260 -0
  59. package/extensions/services/proxy/agentcp/__init__.py +16 -0
  60. package/extensions/services/proxy/agentcp/agent.py +4 -0
  61. package/extensions/services/proxy/agentcp/agentcp.py +2494 -0
  62. package/extensions/services/proxy/agentcp/agentprofile.json +89 -0
  63. package/extensions/services/proxy/agentcp/ap/__init__.py +16 -0
  64. package/extensions/services/proxy/agentcp/ap/ap_client.py +316 -0
  65. package/extensions/services/proxy/agentcp/assets/images/wechat_qr.png +0 -0
  66. package/extensions/services/proxy/agentcp/backup/metrics.json +31 -0
  67. package/extensions/services/proxy/agentcp/base/__init__.py +20 -0
  68. package/extensions/services/proxy/agentcp/base/auth_client.py +257 -0
  69. package/extensions/services/proxy/agentcp/base/client.py +112 -0
  70. package/extensions/services/proxy/agentcp/base/env.py +34 -0
  71. package/extensions/services/proxy/agentcp/base/html_util.py +336 -0
  72. package/extensions/services/proxy/agentcp/base/log.py +98 -0
  73. package/extensions/services/proxy/agentcp/ca/__init__.py +17 -0
  74. package/extensions/services/proxy/agentcp/ca/ca_client.py +414 -0
  75. package/extensions/services/proxy/agentcp/ca/ca_root.py +74 -0
  76. package/extensions/services/proxy/agentcp/context/__init__.py +20 -0
  77. package/extensions/services/proxy/agentcp/context/context.py +73 -0
  78. package/extensions/services/proxy/agentcp/context/exceptions.py +114 -0
  79. package/extensions/services/proxy/agentcp/create_profile.py +125 -0
  80. package/extensions/services/proxy/agentcp/create_profile_weather.py +125 -0
  81. package/extensions/services/proxy/agentcp/db/__init__.py +15 -0
  82. package/extensions/services/proxy/agentcp/db/db_mananger.py +550 -0
  83. package/extensions/services/proxy/agentcp/docs/UDP_HEARTBEAT_FIX_REPORT.md +265 -0
  84. package/extensions/services/proxy/agentcp/docs/heartbeat_issue_analysis.md +291 -0
  85. package/extensions/services/proxy/agentcp/file/__init__.py +16 -0
  86. package/extensions/services/proxy/agentcp/file/file_client.py +141 -0
  87. package/extensions/services/proxy/agentcp/file/wss_binary_message.py +137 -0
  88. package/extensions/services/proxy/agentcp/hcp.py +299 -0
  89. package/extensions/services/proxy/agentcp/heartbeat/__init__.py +16 -0
  90. package/extensions/services/proxy/agentcp/heartbeat/heartbeat_client.py +360 -0
  91. package/extensions/services/proxy/agentcp/improved_scheduler.py +498 -0
  92. package/extensions/services/proxy/agentcp/llm_agent_utils.py +249 -0
  93. package/extensions/services/proxy/agentcp/llm_server.py +172 -0
  94. package/extensions/services/proxy/agentcp/mermaid.py +210 -0
  95. package/extensions/services/proxy/agentcp/message.py +149 -0
  96. package/extensions/services/proxy/agentcp/metrics.py +256 -0
  97. package/extensions/services/proxy/agentcp/monitoring/__init__.py +20 -0
  98. package/extensions/services/proxy/agentcp/monitoring/global_monitor.py +27 -0
  99. package/extensions/services/proxy/agentcp/monitoring/metrics_store.py +325 -0
  100. package/extensions/services/proxy/agentcp/monitoring/monitoring_service.py +269 -0
  101. package/extensions/services/proxy/agentcp/monitoring/sliding_window.py +222 -0
  102. package/extensions/services/proxy/agentcp/monitoring/standalone_reader.py +224 -0
  103. package/extensions/services/proxy/agentcp/msg/__init__.py +21 -0
  104. package/extensions/services/proxy/agentcp/msg/connection_manager.py +456 -0
  105. package/extensions/services/proxy/agentcp/msg/message_client.py +2058 -0
  106. package/extensions/services/proxy/agentcp/msg/message_serialize.py +263 -0
  107. package/extensions/services/proxy/agentcp/msg/open_ai_message.py +88 -0
  108. package/extensions/services/proxy/agentcp/msg/session_manager.py +1062 -0
  109. package/extensions/services/proxy/agentcp/msg/stream_client.py +267 -0
  110. package/extensions/services/proxy/agentcp/msg/websocket_file_receiver.py +89 -0
  111. package/extensions/services/proxy/agentcp/msg/ws_logger.py +685 -0
  112. package/extensions/services/proxy/agentcp/msg/wss_binary_message.py +137 -0
  113. package/extensions/services/proxy/agentcp/requirements.txt +7 -0
  114. package/extensions/services/proxy/agentcp/samples/agent_graph/README.md +37 -0
  115. package/extensions/services/proxy/agentcp/samples/agent_graph/agentprofile.json +89 -0
  116. package/extensions/services/proxy/agentcp/samples/agent_graph/create_profile.py +138 -0
  117. package/extensions/services/proxy/agentcp/samples/agent_graph/main.py +164 -0
  118. package/extensions/services/proxy/agentcp/samples/agent_use/create_profile.py +123 -0
  119. package/extensions/services/proxy/agentcp/samples/agent_use/llm/create_profile.py +129 -0
  120. package/extensions/services/proxy/agentcp/samples/agent_use/llm/env.json +5 -0
  121. package/extensions/services/proxy/agentcp/samples/agent_use/llm/main.py +146 -0
  122. package/extensions/services/proxy/agentcp/samples/agent_use/main.py +123 -0
  123. package/extensions/services/proxy/agentcp/samples/agent_use/readme.md +379 -0
  124. package/extensions/services/proxy/agentcp/samples/agent_use/search/create_profile.py +129 -0
  125. package/extensions/services/proxy/agentcp/samples/agent_use/search/main.py +28 -0
  126. package/extensions/services/proxy/agentcp/samples/agent_use/tool/create_profile.py +129 -0
  127. package/extensions/services/proxy/agentcp/samples/agent_use/tool/main.py +20 -0
  128. package/extensions/services/proxy/agentcp/samples/ali_amap/README.md +97 -0
  129. package/extensions/services/proxy/agentcp/samples/ali_amap/amap_agent.py +88 -0
  130. package/extensions/services/proxy/agentcp/samples/ali_amap/create_profile.py +125 -0
  131. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/powershell.py +228 -0
  132. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/software.py +63 -0
  133. package/extensions/services/proxy/agentcp/samples/compute_agent/agent/tools.py +36 -0
  134. package/extensions/services/proxy/agentcp/samples/compute_agent/browser_user.py +41 -0
  135. package/extensions/services/proxy/agentcp/samples/deepseek/README.md +79 -0
  136. package/extensions/services/proxy/agentcp/samples/deepseek/create_profile.py +126 -0
  137. package/extensions/services/proxy/agentcp/samples/deepseek/deepseek.py +42 -0
  138. package/extensions/services/proxy/agentcp/samples/dify_chat/README.md +78 -0
  139. package/extensions/services/proxy/agentcp/samples/dify_chat/create_profile.py +126 -0
  140. package/extensions/services/proxy/agentcp/samples/dify_chat/dify_chat.py +47 -0
  141. package/extensions/services/proxy/agentcp/samples/dify_workflow/README.md +78 -0
  142. package/extensions/services/proxy/agentcp/samples/dify_workflow/create_profile.py +126 -0
  143. package/extensions/services/proxy/agentcp/samples/dify_workflow/dify_workflow.py +46 -0
  144. package/extensions/services/proxy/agentcp/samples/executor/README.md +44 -0
  145. package/extensions/services/proxy/agentcp/samples/executor/agentprofile.json +89 -0
  146. package/extensions/services/proxy/agentcp/samples/executor/create_profile.py +139 -0
  147. package/extensions/services/proxy/agentcp/samples/executor/main.py +160 -0
  148. package/extensions/services/proxy/agentcp/samples/filereader/README.md +45 -0
  149. package/extensions/services/proxy/agentcp/samples/filereader/agentprofile.json +90 -0
  150. package/extensions/services/proxy/agentcp/samples/filereader/create_profile.py +137 -0
  151. package/extensions/services/proxy/agentcp/samples/filereader/main.py +253 -0
  152. package/extensions/services/proxy/agentcp/samples/filewriter/README.md +38 -0
  153. package/extensions/services/proxy/agentcp/samples/filewriter/agentprofile.json +91 -0
  154. package/extensions/services/proxy/agentcp/samples/filewriter/create_profile.py +138 -0
  155. package/extensions/services/proxy/agentcp/samples/filewriter/main.py +289 -0
  156. package/extensions/services/proxy/agentcp/samples/hcp/README.md +85 -0
  157. package/extensions/services/proxy/agentcp/samples/hcp/acp_weather_agent.zip +0 -0
  158. package/extensions/services/proxy/agentcp/samples/hcp/create_profile.py +125 -0
  159. package/extensions/services/proxy/agentcp/samples/hcp/hcp.py +237 -0
  160. package/extensions/services/proxy/agentcp/samples/helloworld/README.md +68 -0
  161. package/extensions/services/proxy/agentcp/samples/helloworld/hello_world.py +40 -0
  162. package/extensions/services/proxy/agentcp/samples/llm_agent/MEADME.md +117 -0
  163. package/extensions/services/proxy/agentcp/samples/llm_agent/create_profile.py +125 -0
  164. package/extensions/services/proxy/agentcp/samples/llm_agent/qwen_agent.py +136 -0
  165. package/extensions/services/proxy/agentcp/samples/local_llm_agent/README.md +90 -0
  166. package/extensions/services/proxy/agentcp/samples/local_llm_agent/create_profile.py +125 -0
  167. package/extensions/services/proxy/agentcp/samples/local_llm_agent/main.py +49 -0
  168. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/README.md +55 -0
  169. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/create_profile.py +125 -0
  170. package/extensions/services/proxy/agentcp/samples/query_llm_from_agent/main.py +23 -0
  171. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/README.md +103 -0
  172. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/create_profile.py +125 -0
  173. package/extensions/services/proxy/agentcp/samples/query_weather_api_agent/main.py +69 -0
  174. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/README.md +58 -0
  175. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/create_profile.py +125 -0
  176. package/extensions/services/proxy/agentcp/samples/query_weather_from_agent/main.py +25 -0
  177. package/extensions/services/proxy/agentcp/samples/qwen3/README.md +71 -0
  178. package/extensions/services/proxy/agentcp/samples/qwen3/create_profile.py +126 -0
  179. package/extensions/services/proxy/agentcp/samples/qwen3/qwen3.py +37 -0
  180. package/extensions/services/proxy/agentcp/samples/qwen3_tools/README.md +133 -0
  181. package/extensions/services/proxy/agentcp/samples/qwen3_tools/create_profile.py +126 -0
  182. package/extensions/services/proxy/agentcp/samples/qwen3_tools/qwen3_tools.py +98 -0
  183. package/extensions/services/proxy/agentcp/samples/search/create_profile_qwen.py +125 -0
  184. package/extensions/services/proxy/agentcp/samples/search/create_profile_search.py +125 -0
  185. package/extensions/services/proxy/agentcp/samples/search/qwen_agent.py +136 -0
  186. package/extensions/services/proxy/agentcp/samples/search/search_agent.py +170 -0
  187. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/README.md +89 -0
  188. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/create_profile.py +125 -0
  189. package/extensions/services/proxy/agentcp/samples/wrapper_agently_to_agent/main.py +44 -0
  190. package/extensions/services/proxy/agentcp/utils/__init__.py +15 -0
  191. package/extensions/services/proxy/agentcp/utils/file_util.py +117 -0
  192. package/extensions/services/proxy/agentcp/utils/proxy_bypass.py +99 -0
  193. package/extensions/services/proxy/agentcp/workflow.py +203 -0
  194. package/extensions/services/proxy/console_auth.py +109 -0
  195. package/extensions/services/proxy/evol/__init__.py +1 -0
  196. package/extensions/services/proxy/evol/config.py +37 -0
  197. package/extensions/services/proxy/evol/http/__init__.py +1 -0
  198. package/extensions/services/proxy/evol/http/async_http.py +551 -0
  199. package/extensions/services/proxy/evol/log.py +28 -0
  200. package/extensions/services/proxy/evol/presenter/__init__.py +2 -0
  201. package/extensions/services/proxy/evol/presenter/agentIdPresenter.py +1031 -0
  202. package/extensions/services/proxy/evol/presenter/apikeyPresenter.py +106 -0
  203. package/extensions/services/proxy/evol/presenter/configPresenter.py +1281 -0
  204. package/extensions/services/proxy/evol/presenter/userPresenter.py +477 -0
  205. package/extensions/services/proxy/evol/server/__init__.py +1 -0
  206. package/extensions/services/proxy/evol/server/claude_proxy_async.py +3430 -0
  207. package/extensions/services/proxy/evol/server/openclaw_proxy.py +1861 -0
  208. package/extensions/services/proxy/evol/server/proxy_config.py +15 -0
  209. package/extensions/services/proxy/evol/server/proxy_engine.py +501 -0
  210. package/extensions/services/proxy/evol/version.py +24 -0
  211. package/extensions/services/proxy/logs/websocket.log +260 -0
  212. package/extensions/services/proxy/main.py +240 -0
  213. package/extensions/services/proxy/requirements.txt +13 -0
  214. package/extensions/services/proxy/server.py +271 -0
  215. package/extensions/services/watchdog/entry.py +215 -26
  216. package/extensions/services/watchdog/module.md +1 -0
  217. package/extensions/services/watchdog/monitor.py +178 -38
  218. package/extensions/services/web/WEBSOCKET_STATUS.md +143 -0
  219. package/extensions/services/web/config_example.py +35 -0
  220. package/extensions/services/web/config_loader.py +110 -0
  221. package/extensions/services/web/entry.py +114 -26
  222. package/extensions/services/web/module.md +35 -24
  223. package/extensions/services/web/pairing.py +250 -0
  224. package/extensions/services/web/pairing_codes.jsonl +16 -0
  225. package/extensions/services/web/relay.py +643 -0
  226. package/extensions/services/web/relay_config.json5 +67 -0
  227. package/extensions/services/web/routes/routes_management_ws.py +127 -0
  228. package/extensions/services/web/routes/routes_rpc.py +89 -0
  229. package/extensions/services/web/routes/routes_test.py +61 -0
  230. package/extensions/services/web/routes/schemas.py +0 -22
  231. package/extensions/services/web/server.py +434 -99
  232. package/extensions/services/web/static/css/style.css +67 -28
  233. package/extensions/services/web/static/index.html +234 -44
  234. package/extensions/services/web/static/js/app.js +1335 -48
  235. package/extensions/services/web/static/js/kernel-client-example.js +161 -0
  236. package/extensions/services/web/static/js/kernel-client.js +383 -0
  237. package/extensions/services/web/static/js/registry-tests.js +558 -0
  238. package/extensions/services/web/static/js/token-manager.js +175 -0
  239. package/extensions/services/web/static/pairing.html +248 -0
  240. package/extensions/services/web/static/test_registry.html +262 -0
  241. package/extensions/services/web/web_config.json5 +29 -0
  242. package/kernel/entry.py +120 -32
  243. package/kernel/event_hub.py +141 -16
  244. package/kernel/module.md +60 -33
  245. package/kernel/registry_store.py +45 -36
  246. package/kernel/rpc_router.py +152 -59
  247. package/kernel/server.py +322 -26
  248. package/kite_cli/__init__.py +3 -0
  249. package/kite_cli/__main__.py +5 -0
  250. package/kite_cli/commands/__init__.py +1 -0
  251. package/kite_cli/commands/clean.py +101 -0
  252. package/kite_cli/commands/deps_install.py +67 -0
  253. package/kite_cli/commands/doctor.py +35 -0
  254. package/kite_cli/commands/env_check.py +45 -0
  255. package/kite_cli/commands/history.py +111 -0
  256. package/kite_cli/commands/info.py +96 -0
  257. package/kite_cli/commands/install.py +313 -0
  258. package/kite_cli/commands/list.py +143 -0
  259. package/kite_cli/commands/log.py +81 -0
  260. package/kite_cli/commands/prepare.py +49 -0
  261. package/kite_cli/commands/rollback.py +88 -0
  262. package/kite_cli/commands/search.py +73 -0
  263. package/kite_cli/commands/uninstall.py +85 -0
  264. package/kite_cli/commands/update.py +118 -0
  265. package/kite_cli/commands/venv_setup.py +56 -0
  266. package/kite_cli/core/__init__.py +1 -0
  267. package/kite_cli/core/checker.py +142 -0
  268. package/kite_cli/core/dependency.py +229 -0
  269. package/kite_cli/core/downloader.py +209 -0
  270. package/kite_cli/core/install_info.py +40 -0
  271. package/kite_cli/core/tool_installer.py +397 -0
  272. package/kite_cli/core/validator.py +78 -0
  273. package/kite_cli/main.py +317 -0
  274. package/kite_cli/utils/__init__.py +1 -0
  275. package/kite_cli/utils/i18n.py +252 -0
  276. package/kite_cli/utils/interactive.py +63 -0
  277. package/kite_cli/utils/operation_log.py +77 -0
  278. package/kite_cli/utils/paths.py +34 -0
  279. package/kite_cli/utils/version.py +308 -0
  280. package/launcher/entry.py +1124 -178
  281. package/launcher/logging_setup.py +104 -0
  282. package/launcher/module.md +46 -37
  283. package/launcher/module_scanner.py +11 -1
  284. package/main.py +4 -1
  285. package/package.json +9 -1
  286. package/python_version.json +4 -0
  287. package/requirements.txt +38 -0
  288. package/scripts/env-manager.js +328 -0
  289. package/scripts/plan_manager.py +315 -0
  290. package/scripts/python-env.js +79 -0
  291. package/scripts/scan_dependencies.py +461 -0
  292. package/scripts/setup-python-env.js +191 -0
  293. package/extensions/services/web/routes/routes_modules.py +0 -249
@@ -13,6 +13,14 @@ import time
13
13
  from datetime import datetime, timezone
14
14
 
15
15
 
16
+ # System broadcast events (received by all modules, may not need handling)
17
+ SYSTEM_BROADCAST_EVENTS = {
18
+ "module.ready", "module.registered", "module.started", "module.stopped",
19
+ "module.crashed", "module.exiting", "module.offline",
20
+ "module.shutdown.ack", "module.shutdown.ready",
21
+ "system.ready", "registry.updated",
22
+ }
23
+
16
24
  # Module health states
17
25
  HEALTHY = "healthy"
18
26
  UNHEALTHY = "unhealthy"
@@ -47,6 +55,8 @@ class ModuleStatus:
47
55
  self.memory_samples: list[float] = [] # last 5 memory_rss samples
48
56
  self.recovery_since: float = 0 # when recovery observation started
49
57
  self.last_metrics: dict = {}
58
+ # Startup metrics
59
+ self.startup_time: float = 0 # module startup time in seconds (from module.ready event)
50
60
 
51
61
 
52
62
  class HealthMonitor:
@@ -91,6 +101,8 @@ class HealthMonitor:
91
101
  # Launcher loss tracking
92
102
  self._launcher_offline = False
93
103
  self._launcher_had_exiting = False # True if module.exiting was received for launcher
104
+ self._launcher_restart_requested = False # True if launcher requested restart
105
+ self._launcher_startup_info = None # Startup info from launcher (python, argv, cwd, env)
94
106
 
95
107
  # ── Module discovery ──
96
108
 
@@ -154,23 +166,28 @@ class HealthMonitor:
154
166
  # ── Health check ──
155
167
 
156
168
  async def _check_one(self, status: ModuleStatus):
157
- """Check a single module's /health endpoint."""
169
+ """Check a single module's health via RPC."""
158
170
  if not status.api_endpoint:
159
171
  return # Not yet registered in Registry, will be picked up on next discover
160
- url = f"{status.api_endpoint}{status.health_endpoint}"
172
+
161
173
  status.last_check = time.time()
162
174
 
163
175
  try:
164
- async with httpx.AsyncClient() as client:
165
- resp = await client.get(url, timeout=self.HEALTH_TIMEOUT)
166
- if resp.status_code == 200:
167
- body = resp.json()
168
- if body.get("status") == "healthy":
169
- await self._mark_healthy(status)
170
- return
171
- status.last_error = f"unhealthy response: {body.get('status')}"
172
- else:
173
- status.last_error = f"HTTP {resp.status_code}"
176
+ # Call module.health via RPC
177
+ resp = await self.rpc_call(f"{status.module_id}.health", {})
178
+
179
+ # Check if RPC call succeeded
180
+ if "error" in resp:
181
+ error = resp["error"]
182
+ status.last_error = f"RPC error: {error.get('message', 'unknown')}"
183
+ elif "result" in resp:
184
+ result = resp["result"]
185
+ if result.get("status") == "healthy":
186
+ await self._mark_healthy(status)
187
+ return
188
+ status.last_error = f"unhealthy response: {result.get('status')}"
189
+ else:
190
+ status.last_error = "Invalid RPC response"
174
191
  except Exception as e:
175
192
  status.last_error = str(e)
176
193
 
@@ -394,42 +411,71 @@ class HealthMonitor:
394
411
  return
395
412
 
396
413
  if not module_id or module_id == "watchdog":
414
+ # Handle registry.updated (no module_id)
415
+ if event_type == "registry.updated":
416
+ # Registry changed, re-discover modules
417
+ asyncio.create_task(self.discover_modules())
397
418
  return
398
419
 
399
420
  if event_type == "module.started":
400
421
  print(f"[watchdog] Received module.started: {module_id}")
401
422
  self._crash_counts.pop(module_id, None)
402
- await self.discover_modules()
423
+ # Don't await discover_modules() here - it blocks event processing!
424
+ # Schedule it as a background task instead
425
+ asyncio.create_task(self.discover_modules())
403
426
 
404
427
  elif event_type == "module.stopped":
405
428
  print(f"[watchdog] Received module.stopped: {module_id}")
406
429
  self.modules.pop(module_id, None)
430
+ # Re-discover to update module list
431
+ asyncio.create_task(self.discover_modules())
407
432
  await self._handle_module_stopped(module_id, data)
408
433
 
409
434
  elif event_type == "module.exiting":
410
435
  action = data.get("action", "none")
411
- print(f"[watchdog] Received module.exiting: {module_id}, action={action}")
436
+ reason = data.get("reason", "")
437
+ print(f"[watchdog] Received module.exiting: {module_id}, action={action}, reason={reason}")
412
438
  self._exit_intents[module_id] = action
413
439
  # Track launcher exiting intent
414
440
  if module_id == "launcher":
415
441
  self._launcher_had_exiting = True
442
+ if action == "restart_launcher":
443
+ print(f"[watchdog] Launcher 请求计划内重启 (reason={reason})")
444
+ self._launcher_restart_requested = True
445
+ # Save startup info for restart
446
+ self._launcher_startup_info = data.get("startup_info")
447
+ # 启动快速检测任务
448
+ asyncio.create_task(self._quick_check_launcher_exit())
416
449
 
417
450
  elif event_type == "module.ready":
418
451
  graceful = bool(data.get("graceful_shutdown"))
419
- print(f"[watchdog] Received module.ready: {module_id}, graceful_shutdown={graceful}")
452
+ startup_time = data.get("startup_time", 0)
453
+ print(f"[watchdog] Received module.ready: {module_id}, graceful_shutdown={graceful}, startup_time={startup_time:.3f}s")
420
454
  self._graceful_modules[module_id] = graceful
455
+ # Save startup time to module status
456
+ if module_id in self.modules:
457
+ self.modules[module_id].startup_time = startup_time
421
458
  # Reset launcher loss tracking when launcher reconnects
422
459
  if module_id == "launcher":
423
460
  self._launcher_offline = False
424
461
  self._launcher_had_exiting = False
425
462
 
463
+ # Layer 2: 忽略系统广播事件
464
+ elif event_type in SYSTEM_BROADCAST_EVENTS:
465
+ pass
466
+
467
+ # Layer 3: 警告未知事件
468
+ else:
469
+ print(f"[watchdog] Warning: Received unhandled event: {event_type}")
470
+
426
471
  async def _handle_module_stopped(self, module_id: str, data: dict):
427
472
  """Restart decision engine — called when module.stopped is received.
428
473
 
429
474
  Priority:
430
475
  1. System shutting down → no restart
431
- 2. Has exit_intentfollow the declared action (none/restart/restart_delay)
432
- 3. No intent (crash) increment crash count, restart up to MAX_RESTARTS
476
+ 2. stop_type == "graceful_stop" Launcher主动停止,不重启
477
+ 3. stop_type == "process_exit" + has exit_intent intent 处理
478
+ 4. stop_type == "process_exit" + no intent → 崩溃,重启
433
479
  """
434
480
  # Sync graceful_shutdown from Launcher (covers missed module.ready)
435
481
  if "graceful_shutdown" in data:
@@ -439,6 +485,18 @@ class HealthMonitor:
439
485
  print(f"[watchdog] {module_id} stopped during shutdown, skipping restart")
440
486
  return
441
487
 
488
+ # Check stop_type first (most reliable indicator)
489
+ stop_type = data.get("stop_type", "unknown")
490
+
491
+ if stop_type == "graceful_stop":
492
+ # Launcher主动停止(通过RPC或shutdown流程)
493
+ reason = data.get("reason", "unknown")
494
+ print(f"[watchdog] {module_id} stopped by Launcher (stop_type=graceful_stop, reason={reason}), no restart")
495
+ self._crash_counts.pop(module_id, None)
496
+ self._exit_intents.pop(module_id, None) # 清理可能残留的intent
497
+ return
498
+
499
+ # stop_type == "process_exit": 进程自行退出,需要判断是否崩溃
442
500
  intent = self._exit_intents.pop(module_id, None)
443
501
  if intent is not None:
444
502
  if intent == "none":
@@ -473,6 +531,27 @@ class HealthMonitor:
473
531
  "message": f"{module_id} exceeded {self.MAX_RESTARTS} crash restarts",
474
532
  })
475
533
 
534
+
535
+ async def _quick_check_launcher_exit(self):
536
+ """快速检测 Launcher 退出(0.2s 间隔,最多 5s,5s 后强制重启)"""
537
+ print("[watchdog] 开始快速检测 Launcher 退出(0.2s 间隔,最多 5s)")
538
+ for i in range(25): # 25 * 0.2s = 5s
539
+ try:
540
+ await self.rpc_call("launcher.list_modules", {})
541
+ await asyncio.sleep(0.2)
542
+ except Exception:
543
+ # Launcher 已退出
544
+ print(f"[watchdog] Launcher 已退出(检测 {(i+1)*0.2:.1f}s),启动新实例")
545
+ self._start_new_instance()
546
+ print("[watchdog] 新实例已启动,watchdog 退出")
547
+ sys.exit(0)
548
+
549
+ # 5s 后仍未退出,强制重启
550
+ print("[watchdog] Launcher 5s 内未退出,强制重启")
551
+ self._start_new_instance()
552
+ print("[watchdog] 新实例已启动,watchdog 退出")
553
+ sys.exit(0)
554
+
476
555
  async def _handle_launcher_lost(self):
477
556
  """Handle launcher_lost: decide whether to start a new Kite instance.
478
557
 
@@ -489,32 +568,73 @@ class HealthMonitor:
489
568
  sys.exit(0)
490
569
 
491
570
  def _start_new_instance(self):
492
- """Start a new Kite instance by running python main.py in the project directory."""
493
- project_dir = os.environ.get("KITE_PROJECT", "")
494
- if not project_dir:
495
- print("[watchdog] ERROR: KITE_PROJECT not set, cannot start new instance")
496
- return
571
+ """Start a new Kite instance using saved startup info from launcher."""
572
+ # Use startup info if available (from module.exiting event)
573
+ if self._launcher_startup_info:
574
+ python_exe = self._launcher_startup_info.get("python", sys.executable)
575
+ argv = self._launcher_startup_info.get("argv", [])
576
+ cwd = self._launcher_startup_info.get("cwd", os.environ.get("KITE_PROJECT", ""))
577
+ env = self._launcher_startup_info.get("env", {})
578
+
579
+ # Build command based on argv[0]
580
+ if argv:
581
+ # Check if argv[0] is executable (e.g., kite.exe or python script)
582
+ argv0 = argv[0]
583
+ if os.path.isabs(argv0) and os.path.exists(argv0):
584
+ # argv[0] is absolute path to executable/script, use it directly
585
+ cmd = argv
586
+ elif argv0.endswith(('.py', '.pyw')):
587
+ # argv[0] is a Python script, prepend Python interpreter
588
+ cmd = [python_exe] + argv
589
+ else:
590
+ # argv[0] might be a command in PATH (e.g., 'kite'), use it directly
591
+ cmd = argv
592
+ else:
593
+ # Fallback to main.py
594
+ main_py = os.path.join(cwd, "main.py")
595
+ cmd = [python_exe, main_py]
497
596
 
498
- main_py = os.path.join(project_dir, "main.py")
499
- if not os.path.exists(main_py):
500
- print(f"[watchdog] ERROR: {main_py} not found, cannot start new instance")
501
- return
597
+ print(f"[watchdog] Starting new instance with saved startup info:")
598
+ print(f"[watchdog] Command: {' '.join(cmd)}")
599
+ print(f"[watchdog] CWD: {cwd}")
600
+
601
+ # Use saved environment variables
602
+ new_env = env
603
+
604
+ else:
605
+ # Fallback: use current environment (old behavior)
606
+ project_dir = os.environ.get("KITE_PROJECT", "")
607
+ if not project_dir:
608
+ print("[watchdog] ERROR: KITE_PROJECT not set, cannot start new instance")
609
+ return
610
+
611
+ main_py = os.path.join(project_dir, "main.py")
612
+ if not os.path.exists(main_py):
613
+ print(f"[watchdog] ERROR: {main_py} not found, cannot start new instance")
614
+ return
615
+
616
+ python_exe = sys.executable
617
+ cmd = [python_exe, main_py]
618
+ cwd = project_dir
619
+ new_env = os.environ.copy()
620
+ print(f"[watchdog] Starting new instance (fallback mode): python {main_py}")
502
621
 
503
- print(f"[watchdog] Starting new Kite instance: python {main_py}")
504
622
  try:
505
623
  # Start detached process with new console window
506
624
  if sys.platform == "win32":
507
625
  # Use 'start' command to force visible console window
508
626
  subprocess.Popen(
509
- ["cmd", "/c", "start", "Kite", sys.executable, main_py],
510
- cwd=project_dir,
627
+ ["cmd", "/c", "start", "Kite"] + cmd,
628
+ cwd=cwd,
629
+ env=new_env,
511
630
  shell=False,
512
631
  )
513
632
  elif sys.platform == "darwin":
514
633
  # macOS: use 'open -a Terminal' to launch in new Terminal window
515
634
  subprocess.Popen(
516
- ["open", "-a", "Terminal", main_py],
517
- cwd=project_dir,
635
+ ["open", "-a", "Terminal"] + cmd,
636
+ cwd=cwd,
637
+ env=new_env,
518
638
  )
519
639
  else:
520
640
  # Linux: try common terminal emulators
@@ -528,8 +648,9 @@ class HealthMonitor:
528
648
  for term_cmd in terminals:
529
649
  try:
530
650
  subprocess.Popen(
531
- term_cmd + [sys.executable, main_py],
532
- cwd=project_dir,
651
+ term_cmd + cmd,
652
+ cwd=cwd,
653
+ env=new_env,
533
654
  start_new_session=True,
534
655
  )
535
656
  launched = True
@@ -540,8 +661,9 @@ class HealthMonitor:
540
661
  # Fallback: headless start
541
662
  print("[watchdog] WARNING: No terminal emulator found, starting headless")
542
663
  subprocess.Popen(
543
- [sys.executable, main_py],
544
- cwd=project_dir,
664
+ cmd,
665
+ cwd=cwd,
666
+ env=new_env,
545
667
  start_new_session=True,
546
668
  stdin=subprocess.DEVNULL,
547
669
  )
@@ -584,10 +706,15 @@ class HealthMonitor:
584
706
  return
585
707
  print("[watchdog] system.ready received, starting health checks")
586
708
 
587
- while self._running:
588
- # Re-discover every cycle to pick up newly started/stopped modules
589
- await self.discover_modules()
709
+ # Initial discovery (first time)
710
+ await self.discover_modules()
590
711
 
712
+ # Track discovery count and last discovery time
713
+ discovery_count = 1
714
+ last_discovery = asyncio.get_event_loop().time()
715
+ discovery_interval = 300.0 # 5 minutes after first 2 discoveries
716
+
717
+ while self._running:
591
718
  if self.modules:
592
719
  tasks = []
593
720
  for s in self.modules.values():
@@ -599,6 +726,18 @@ class HealthMonitor:
599
726
  interval = self._min_interval()
600
727
  await asyncio.sleep(interval)
601
728
 
729
+ # Periodic re-discovery
730
+ now = asyncio.get_event_loop().time()
731
+ if discovery_count < 2:
732
+ # First 2 times: discover every cycle
733
+ await self.discover_modules()
734
+ discovery_count += 1
735
+ last_discovery = now
736
+ elif now - last_discovery >= discovery_interval:
737
+ # After that: discover every 5 minutes
738
+ await self.discover_modules()
739
+ last_discovery = now
740
+
602
741
  def _min_interval(self) -> float:
603
742
  """Return the shortest check interval needed across all modules."""
604
743
  if not self.modules:
@@ -622,6 +761,7 @@ class HealthMonitor:
622
761
  "last_error": s.last_error,
623
762
  "resource_state": s.resource_state,
624
763
  "metrics": s.last_metrics,
764
+ "startup_time": s.startup_time,
625
765
  }
626
766
  for mid, s in self.modules.items()
627
767
  }
@@ -0,0 +1,143 @@
1
+ # Web 模块实时状态推送 — WebSocket 实现
2
+
3
+ ## 概述
4
+
5
+ 为 Web 管理后台新增 `/ws/management` WebSocket 端点,实时推送模块状态变更到前端,实现无需刷新的实时状态监控。
6
+
7
+ ## 架构
8
+
9
+ ```
10
+ Kernel (事件源)
11
+ ↓ WebSocket JSON-RPC 2.0
12
+ Web 模块 (server.py)
13
+ ↓ 订阅事件 → 转发
14
+ routes_management_ws.py (广播)
15
+ ↓ WebSocket
16
+ 前端 (app.js)
17
+ ↓ 更新 UI
18
+ 模块列表页面
19
+ ```
20
+
21
+ ## 模块状态体系
22
+
23
+ ### 1. 静态状态(module.md 配置)
24
+ - `enabled` — 自动启动
25
+ - `manual` — 手动启动
26
+ - `disabled` — 禁用
27
+
28
+ ### 2. 运行时状态(Launcher 管理)
29
+ - `stopped` — 未运行
30
+ - `starting` — 启动中
31
+ - `running` — 运行中
32
+ - `stopping` — 停止中
33
+ - `crashed` — 崩溃
34
+ - `restarting` — 重启中
35
+
36
+ ### 3. 连接状态(Kernel 连接)
37
+ - `disconnected` — 未连接到 Kernel
38
+ - `connected` — 已连接到 Kernel
39
+ - `registered` — 已注册到 Kernel Registry
40
+
41
+ ### 4. 前端综合显示
42
+
43
+ | 状态 | 颜色 | 说明 |
44
+ |------|------|------|
45
+ | 离线 | 灰色 | stopped |
46
+ | 启动中 | 黄色 | starting |
47
+ | 在线 | 绿色 | running + registered |
48
+ | 停止中 | 黄色 | stopping |
49
+ | 崩溃 | 红色 | crashed |
50
+ | 未连接 | 橙色 | running 但未连接 Kernel |
51
+
52
+ ## 实现细节
53
+
54
+ ### 后端
55
+
56
+ #### 1. `routes/routes_management_ws.py`(新增)
57
+
58
+ - **端点**: `/ws/management`
59
+ - **功能**:
60
+ - 接受前端 WebSocket 连接
61
+ - 维护全局连接池 `_management_clients`
62
+ - 提供 `broadcast_event()` 函数广播事件到所有客户端
63
+ - 心跳机制(30 秒)
64
+ - **消息格式**:
65
+ ```json
66
+ {
67
+ "type": "module.started",
68
+ "data": { "module_id": "watchdog" },
69
+ "timestamp": "2026-03-05T16:00:00Z"
70
+ }
71
+ ```
72
+
73
+ #### 2. `server.py`(修改)
74
+
75
+ - **导入**: 添加 `from routes.routes_management_ws import router as management_ws_router, broadcast_event`
76
+ - **挂载路由**: `app.include_router(management_ws_router)`
77
+ - **事件订阅**: 扩展订阅列表,包含:
78
+ - `module.started`
79
+ - `module.stopped`
80
+ - `module.crashed`
81
+ - `module.ready`
82
+ - `module.exiting`
83
+ - `module.shutdown`
84
+ - `module.shutdown.ack`
85
+ - `module.shutdown.ready`
86
+ - **事件转发**: 在 `_handle_event_notification()` 中调用 `broadcast_event()` 转发模块状态事件
87
+
88
+ ### 前端
89
+
90
+ #### 1. `static/js/app.js`(修改)
91
+
92
+ **新增全局变量**:
93
+ ```javascript
94
+ let _managementWs = null;
95
+ let _managementWsConnected = false;
96
+ ```
97
+
98
+ **新增函数**:
99
+ - `connectManagementWebSocket()` — 连接 WebSocket,自动重连(3 秒)
100
+ - `_handleManagementEvent(msg)` — 处理收到的事件
101
+ - `_onModuleStatusChange(moduleName, status)` — 更新模块状态缓存和 UI
102
+ - `_updateWsIndicator()` — 更新连线状态指示器
103
+
104
+ **事件处理逻辑**:
105
+ - `module.started` / `module.ready` → 标记为 running,清除 pending,刷新 UI
106
+ - `module.stopped` / `module.crashed` → 标记为 stopped,清除 pending,刷新 UI
107
+ - `module.exiting` / `module.shutdown.ack` → 标记为 stopping(保持 pending)
108
+
109
+ **初始化**:
110
+ 在 `DOMContentLoaded` 中调用 `connectManagementWebSocket()`
111
+
112
+ #### 2. `static/index.html`(修改)
113
+
114
+ 在模块页面标题栏添加连线状态指示器:
115
+ ```html
116
+ <span id="ws-indicator" style="font-size:13px;color:var(--gray-400);">○ 未连线</span>
117
+ ```
118
+
119
+ - 已连线: `● 已连线` (绿色)
120
+ - 未连线: `○ 未连线` (灰色)
121
+
122
+ ## 使用效果
123
+
124
+ 1. **实时状态更新** — 模块启动/停止/崩溃时,前端立即更新,无需手动刷新
125
+ 2. **连线状态可见** — 页面顶部显示 WebSocket 连接状态
126
+ 3. **自动重连** — 连接断开后 3 秒自动重连
127
+ 4. **心跳保活** — 每 30 秒发送心跳,保持连接活跃
128
+
129
+ ## 端口复用
130
+
131
+ HTTP 和 WebSocket 共用同一个端口(默认 18766),通过 FastAPI 统一管理:
132
+ - HTTP: `http://localhost:18766/api/*`
133
+ - WebSocket: `ws://localhost:18766/ws/management`
134
+
135
+ ## 扩展性
136
+
137
+ 未来可以通过 `broadcast_event()` 推送更多实时事件:
138
+ - 通话状态变更
139
+ - 蓝牙连接状态
140
+ - 配置更新通知
141
+ - 系统告警
142
+
143
+ 只需在 `server.py` 的事件处理中添加对应的 `broadcast_event()` 调用即可。
@@ -0,0 +1,35 @@
1
+ """
2
+ 配置加载使用示例
3
+
4
+ 演示如何在 web 模块中使用 config_loader 加载业务配置。
5
+ """
6
+
7
+ import os
8
+ from config_loader import load_business_configs, get_business_config
9
+
10
+ # 获取模块目录
11
+ module_dir = os.path.dirname(os.path.abspath(__file__))
12
+
13
+ # 方式 1:加载所有业务配置
14
+ all_configs = load_business_configs(module_dir)
15
+
16
+ # 访问 web_server 配置
17
+ if 'web_server' in all_configs:
18
+ web_config = all_configs['web_server']['config']
19
+ server_host = web_config['server']['host']
20
+ server_port = web_config['server']['port']
21
+ print(f"Web server: {server_host}:{server_port}")
22
+
23
+ # 访问 relay_service 配置
24
+ if 'relay_service' in all_configs:
25
+ relay_config = all_configs['relay_service']['config']
26
+ base_module_id = relay_config['relay']['base_module_id']
27
+ reconnect_timeout = relay_config['relay']['reconnect_timeout']
28
+ print(f"Relay: base_module_id={base_module_id}, timeout={reconnect_timeout}s")
29
+
30
+ # 方式 2:只加载特定业务配置
31
+ relay_business = get_business_config(module_dir, 'relay_service')
32
+ if relay_business:
33
+ relay_config = relay_business['config']
34
+ permissions = relay_config['permissions']
35
+ print(f"Relay permissions: {list(permissions.keys())}")
@@ -0,0 +1,110 @@
1
+ """
2
+ 配置加载工具
3
+
4
+ 用于加载模块的业务配置。支持从 module.md 读取业务配置块,
5
+ 并动态加载对应的 JSON5 配置文件。
6
+
7
+ 零共享代码依赖 - 此文件可以独立拷贝到其他模块使用。
8
+ """
9
+
10
+ import os
11
+ import re
12
+ import json5
13
+ import yaml
14
+
15
+
16
+ def load_module_metadata(module_dir: str) -> dict:
17
+ """
18
+ 读取 module.md 的 YAML frontmatter。
19
+
20
+ Args:
21
+ module_dir: 模块目录路径
22
+
23
+ Returns:
24
+ 模块元数据字典
25
+ """
26
+ md_path = os.path.join(module_dir, "module.md")
27
+
28
+ if not os.path.exists(md_path):
29
+ return {}
30
+
31
+ try:
32
+ with open(md_path, "r", encoding="utf-8") as f:
33
+ text = f.read()
34
+
35
+ # 提取 YAML frontmatter (--- ... ---)
36
+ m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
37
+ if m:
38
+ return yaml.safe_load(m.group(1)) or {}
39
+ except Exception as e:
40
+ print(f"[config_loader] Error loading module.md: {e}")
41
+
42
+ return {}
43
+
44
+
45
+ def load_business_configs(module_dir: str) -> dict:
46
+ """
47
+ 加载所有业务配置。
48
+
49
+ 从 module.md 的 businesses 块读取业务列表,
50
+ 然后加载每个业务的配置文件(JSON5 格式)。
51
+
52
+ Args:
53
+ module_dir: 模块目录路径
54
+
55
+ Returns:
56
+ 业务配置字典,格式:
57
+ {
58
+ "business_name": {
59
+ "metadata": {
60
+ "name": "business_name",
61
+ "type": "business_type",
62
+ "description": "...",
63
+ "config_file": "config.json5"
64
+ },
65
+ "config": { ... } # JSON5 配置内容
66
+ }
67
+ }
68
+ """
69
+ metadata = load_module_metadata(module_dir)
70
+ businesses = metadata.get('businesses', [])
71
+
72
+ configs = {}
73
+ for business in businesses:
74
+ name = business.get('name')
75
+ config_file = business.get('config_file')
76
+
77
+ if not name or not config_file:
78
+ print(f"[config_loader] Warning: Invalid business entry: {business}")
79
+ continue
80
+
81
+ config_path = os.path.join(module_dir, config_file)
82
+
83
+ if os.path.exists(config_path):
84
+ try:
85
+ with open(config_path, "r", encoding="utf-8") as f:
86
+ configs[name] = {
87
+ 'metadata': business,
88
+ 'config': json5.load(f)
89
+ }
90
+ except Exception as e:
91
+ print(f"[config_loader] Error loading {config_file}: {e}")
92
+ else:
93
+ print(f"[config_loader] Warning: Config file not found: {config_file}")
94
+
95
+ return configs
96
+
97
+
98
+ def get_business_config(module_dir: str, business_name: str) -> dict | None:
99
+ """
100
+ 获取指定业务的配置。
101
+
102
+ Args:
103
+ module_dir: 模块目录路径
104
+ business_name: 业务名称
105
+
106
+ Returns:
107
+ 业务配置字典,如果不存在则返回 None
108
+ """
109
+ configs = load_business_configs(module_dir)
110
+ return configs.get(business_name)