@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
@@ -20,10 +20,8 @@ class RegistryStore:
20
20
  self.launcher_token = launcher_token
21
21
  self.token_map: dict[str, str] = {} # module_id -> token
22
22
  self.modules: dict[str, dict] = {} # module_id -> registration payload
23
- self.heartbeats: dict[str, float] = {} # module_id -> last heartbeat timestamp
24
- self.ttl = 60 # seconds before marking offline
25
- self.heartbeat_interval = 30
26
23
  self.is_debug = os.environ.get("KITE_DEBUG") == "1"
24
+ self.last_update_time = time.time() # Global registry update timestamp
27
25
 
28
26
  # ── Token verification ──
29
27
 
@@ -57,45 +55,72 @@ class RegistryStore:
57
55
  _REQUIRED_FIELDS = ("module_id", "module_type") # api_endpoint now optional
58
56
 
59
57
  def register_module(self, data: dict) -> dict:
60
- """Register or update a module. Idempotent — same module_id overwrites."""
58
+ """Register or update a module. Idempotent — same module_id overwrites.
59
+ Returns dict with changed flag on success, raises exception on failure."""
61
60
  # Validate required fields
62
61
  missing = [f for f in self._REQUIRED_FIELDS if not data.get(f)]
63
62
  if missing:
64
- return {"ok": False, "error": f"Missing required fields: {', '.join(missing)}"}
63
+ raise ValueError(f"Missing required fields: {', '.join(missing)}")
65
64
 
66
65
  mid = data["module_id"]
67
66
 
68
- # Warn on tool name conflicts
67
+ # Validate nested dict structure for lookup-able fields
68
+ # Only check known wrapper fields (tools, hooks, events_publish)
69
+ # RPC methods are registered directly at top level with their actual names
70
+ # events_subscribe is a list, not a dict
71
+ for field_name in ["tools", "hooks", "events_publish"]:
72
+ field_value = data.get(field_name)
73
+ if field_value is not None:
74
+ if not isinstance(field_value, dict):
75
+ raise TypeError(f"Field '{field_name}' must be a dict (nested structure), got {type(field_value).__name__}")
76
+ # Check if it's a flat dict with dot-notation keys (common mistake)
77
+ if any("." in str(k) for k in field_value.keys()):
78
+ raise ValueError(
79
+ f"Field '{field_name}' contains dot-notation keys (e.g., 'module.config.get'). "
80
+ f"Use nested dict structure instead: {{'module': {{'config': {{'get': ...}}}}}}. "
81
+ f"See docs/模块开发指南.md section 4.3 for correct format."
82
+ )
83
+
84
+ # Warn on tool name conflicts (skip 'rpc' — it's a standard tool every module registers)
69
85
  new_tools = data.get("tools", {})
70
86
  if isinstance(new_tools, dict):
71
87
  for tool_name in new_tools:
88
+ if tool_name == "rpc":
89
+ continue
72
90
  for other_mid, other_data in self.modules.items():
73
91
  if other_mid == mid:
74
92
  continue
75
93
  if tool_name in other_data.get("tools", {}):
76
94
  print(f"[kernel] WARNING: tool '{tool_name}' registered by both '{other_mid}' and '{mid}'")
77
95
 
96
+ # Check if content changed (compare with existing registration)
97
+ changed = True
98
+ old_record = self.modules.get(mid)
99
+ if old_record:
100
+ # Strip action field from new data
101
+ new_record = {k: v for k, v in data.items() if k != "action"}
102
+ # Compare without status and registered_at fields
103
+ old_data = {k: v for k, v in old_record.items() if k not in ("status", "registered_at")}
104
+ changed = (new_record != old_data)
105
+
78
106
  # Strip action field — it's a request verb, not part of the registration payload
79
107
  record = {k: v for k, v in data.items() if k != "action"}
80
108
  record["status"] = "registered" # State machine: connected → registered (via register RPC)
81
109
  record["registered_at"] = time.time()
82
110
  self.modules[mid] = record
83
- self.heartbeats[mid] = time.time()
84
- return {"ok": True, "ttl": self.ttl, "heartbeat_interval": self.heartbeat_interval}
111
+
112
+ # Update global timestamp if content changed
113
+ if changed:
114
+ self.last_update_time = time.time()
115
+
116
+ return {"changed": changed}
85
117
 
86
118
  def deregister_module(self, module_id: str) -> dict:
87
- """Remove a module record immediately."""
119
+ """Remove a module record immediately. Returns empty dict."""
88
120
  self.modules.pop(module_id, None)
89
- self.heartbeats.pop(module_id, None)
90
- return {"ok": True}
91
-
92
- def heartbeat(self, module_id: str) -> dict:
93
- """Renew heartbeat for a module."""
94
- if module_id not in self.modules:
95
- return {"ok": False, "error": "module not registered"}
96
- self.heartbeats[module_id] = time.time()
97
- # Don't change status — heartbeat just keeps alive, doesn't upgrade state
98
- return {"ok": True}
121
+ # Update global timestamp
122
+ self.last_update_time = time.time()
123
+ return {}
99
124
 
100
125
  def set_connected(self, module_id: str):
101
126
  """Mark a module as connected (WS established, not yet registered).
@@ -119,17 +144,6 @@ class RegistryStore:
119
144
  mod = self.modules.get(module_id)
120
145
  return mod is not None and mod.get("status") == "ready"
121
146
 
122
- def check_ttl(self) -> list[str]:
123
- """Mark modules as offline if heartbeat expired. Returns list of newly-offline module_ids."""
124
- now = time.time()
125
- expired = []
126
- for mid, last in list(self.heartbeats.items()):
127
- if mid in self.modules and now - last > self.ttl:
128
- if self.modules[mid].get("status") not in ("offline",):
129
- self.modules[mid]["status"] = "offline"
130
- expired.append(mid)
131
- return expired
132
-
133
147
  # ── Get by dot-path ──
134
148
 
135
149
  def get_by_path(self, path: str) -> Any | None:
@@ -177,7 +191,7 @@ class RegistryStore:
177
191
  def lookup(self, field: str = None, module: str = None, value: str = None) -> list[dict]:
178
192
  """
179
193
  Search across all online modules. All three params support glob patterns.
180
- Returns list of {field, module, api_endpoint, value}.
194
+ Returns list of {field, module, value}.
181
195
  """
182
196
  results = []
183
197
  for mid, data in self.modules.items():
@@ -186,8 +200,6 @@ class RegistryStore:
186
200
  if module and not fnmatch.fnmatch(mid, module):
187
201
  continue
188
202
 
189
- api_ep = data.get("api_endpoint", "")
190
-
191
203
  if field:
192
204
  matches = self._match_fields(data, field)
193
205
  for fpath, fval in matches:
@@ -196,7 +208,6 @@ class RegistryStore:
196
208
  results.append({
197
209
  "field": fpath,
198
210
  "module": mid,
199
- "api_endpoint": api_ep,
200
211
  "value": fval,
201
212
  })
202
213
  elif value:
@@ -205,14 +216,12 @@ class RegistryStore:
205
216
  results.append({
206
217
  "field": k,
207
218
  "module": mid,
208
- "api_endpoint": api_ep,
209
219
  "value": v,
210
220
  })
211
221
  else:
212
222
  results.append({
213
223
  "field": "module_id",
214
224
  "module": mid,
215
- "api_endpoint": api_ep,
216
225
  "value": mid,
217
226
  })
218
227
 
@@ -73,11 +73,16 @@ class RpcRouter:
73
73
  self.connections = connections
74
74
  self.kernel_server = kernel_server # Direct reference to KernelServer
75
75
 
76
+ # RPC 统计计数器
77
+ self._rpc_total = 0 # 总 RPC 调用次数
78
+ self._rpc_builtin = 0 # 内置方法调用次数
79
+ self._rpc_forwarded = 0 # 转发调用次数
80
+ self._rpc_errors = 0 # 错误次数
81
+
76
82
  # Builtin method dispatch table
77
83
  self.methods: dict[str, callable] = {
78
84
  "registry.register": self._registry_register,
79
85
  "registry.deregister": self._registry_deregister,
80
- "registry.heartbeat": self._registry_heartbeat,
81
86
  "registry.lookup": self._registry_lookup,
82
87
  "registry.get": self._registry_get,
83
88
  "registry.verify": self._registry_verify,
@@ -87,9 +92,9 @@ class RpcRouter:
87
92
  "kernel.ping": self._kernel_ping,
88
93
  "kernel.stats": self._kernel_stats,
89
94
  "kernel.health": self._kernel_health,
95
+ "kernel.latencies": self._kernel_latencies,
90
96
  "kernel.generate_tokens": self._kernel_generate_tokens,
91
97
  "kernel.register_tokens": self._kernel_register_tokens,
92
- "kernel.shutdown": self._kernel_shutdown,
93
98
  }
94
99
 
95
100
  # Pending cross-module forwards: internal_id -> PendingForward
@@ -102,6 +107,9 @@ class RpcRouter:
102
107
  method = msg.get("method", "")
103
108
  msg_id = msg.get("id")
104
109
 
110
+ # 统计:总调用次数
111
+ self._rpc_total += 1
112
+
105
113
  # JSON-RPC Notification (no id) — currently not handled from clients
106
114
  if msg_id is None:
107
115
  return
@@ -111,12 +119,27 @@ class RpcRouter:
111
119
  # Builtin method
112
120
  handler = self.methods.get(method)
113
121
  if handler:
122
+ # 统计:内置方法调用
123
+ self._rpc_builtin += 1
114
124
  try:
115
125
  result = await handler(caller_id, params)
116
- await ws.send_text(_result_msg(msg_id, result))
117
126
  except Exception as e:
127
+ # 统计:错误次数
128
+ self._rpc_errors += 1
118
129
  print(f"[kernel] RPC handler error ({method}): {e}")
119
- await ws.send_text(_error_msg(msg_id, INTERNAL_ERROR, str(e)))
130
+ try:
131
+ await ws.send_text(_error_msg(msg_id, INTERNAL_ERROR, str(e)))
132
+ except Exception:
133
+ # Can't send error response, connection closed
134
+ pass
135
+ return
136
+
137
+ # Send result (may fail if connection closed during shutdown)
138
+ try:
139
+ await ws.send_text(_result_msg(msg_id, result))
140
+ except Exception as e:
141
+ # Connection closed during shutdown — this is normal, exit silently
142
+ pass
120
143
  return
121
144
 
122
145
  # Cross-module forward: method prefix is target module_id
@@ -126,19 +149,27 @@ class RpcRouter:
126
149
  if target in self.connections:
127
150
  # Check if target module is ready (state machine protection)
128
151
  if not self.registry.is_ready(target):
152
+ # 统计:错误次数
153
+ self._rpc_errors += 1
129
154
  await ws.send_text(_error_msg(
130
155
  msg_id, MODULE_OFFLINE,
131
156
  f"Module not ready: {target} (status: {self.registry.modules.get(target, {}).get('status', 'unknown')})"))
132
157
  return
158
+ # 统计:转发调用
159
+ self._rpc_forwarded += 1
133
160
  await self._forward(caller_id, ws, msg_id, target, method, params)
134
161
  return
135
162
  # Target not connected — check if registered but offline
136
163
  if target in self.registry.modules:
164
+ # 统计:错误次数
165
+ self._rpc_errors += 1
137
166
  await ws.send_text(_error_msg(
138
167
  msg_id, MODULE_OFFLINE, f"Module offline: {target}"))
139
168
  return
140
169
 
141
170
  # Method not found
171
+ # 统计:错误次数
172
+ self._rpc_errors += 1
142
173
  await ws.send_text(_error_msg(msg_id, METHOD_NOT_FOUND, f"Method not found: {method}"))
143
174
 
144
175
  async def handle_response(self, module_id: str, msg: dict):
@@ -187,6 +218,11 @@ class RpcRouter:
187
218
  # Strip target prefix from method for the forwarded request
188
219
  actual_method = method[len(target) + 1:] # e.g. "watchdog.get_status" -> "get_status"
189
220
 
221
+ # Inject caller_id into params for permission checking
222
+ if not isinstance(params, dict):
223
+ params = {}
224
+ params["_caller_id"] = caller_id
225
+
190
226
  # Record pending forward
191
227
  loop = asyncio.get_event_loop()
192
228
  pending = PendingForward(
@@ -243,62 +279,93 @@ class RpcRouter:
243
279
  async def _registry_register(self, caller_id: str, params: dict) -> dict:
244
280
  mid = params.get("module_id")
245
281
  if not mid:
246
- return {"ok": False, "error": "module_id required"}
282
+ raise ValueError("module_id required")
247
283
  # Permission: only Launcher or the module itself
248
284
  if caller_id != "launcher" and caller_id != mid:
249
- return {"ok": False, "error": f"Module '{caller_id}' cannot register as '{mid}'"}
285
+ raise PermissionError(f"Module '{caller_id}' cannot register as '{mid}'")
286
+
287
+ print(f"[kernel] registry.register called by {caller_id} for module {mid}")
288
+ print(f"[kernel] rpc_methods: {params.get('rpc_methods')}")
289
+
250
290
  result = self.registry.register_module(params)
251
- if result.get("ok"):
252
- self.event_hub.publish_internal("module.registered", {"module_id": mid})
291
+ self.event_hub.publish_internal("module.registered", {"module_id": mid}, source=self.kernel_server.module_id)
292
+
293
+ # Only publish registry.updated if content actually changed
294
+ if result.get("changed", True):
295
+ from datetime import datetime, timezone
296
+ self.event_hub.publish_internal("registry.updated", {
297
+ "module_id": mid,
298
+ "timestamp": datetime.now(timezone.utc).isoformat(),
299
+ "action": "register",
300
+ }, source=self.kernel_server.module_id)
301
+ print(f"[kernel] registry.updated published for {mid}")
302
+ else:
303
+ print(f"[kernel] {mid} re-registered with no changes, skipping registry.updated")
304
+
305
+ # When Launcher registers, Kernel publishes its own module.ready
306
+ if mid == "launcher" and self.kernel_server:
307
+ self.kernel_server.publish_ready()
308
+ print(f"[kernel] launcher registered → kernel module.ready published")
253
309
 
254
- # When Launcher registers, Kernel publishes its own module.ready
255
- if mid == "launcher" and self.kernel_server:
256
- self.kernel_server.publish_ready()
257
- print(f"[kernel] launcher registered → kernel module.ready published")
258
310
  return result
259
311
 
260
312
  async def _registry_deregister(self, caller_id: str, params: dict) -> dict:
261
313
  mid = params.get("module_id")
262
314
  if not mid:
263
- return {"ok": False, "error": "module_id required"}
315
+ raise ValueError("module_id required")
264
316
  if caller_id != "launcher" and caller_id != mid:
265
- return {"ok": False, "error": f"Module '{caller_id}' cannot deregister '{mid}'"}
266
- result = self.registry.deregister_module(mid)
267
- if result.get("ok"):
268
- self.event_hub.publish_internal("module.unregistered", {"module_id": mid})
269
- return result
317
+ raise PermissionError(f"Module '{caller_id}' cannot deregister '{mid}'")
270
318
 
271
- async def _registry_heartbeat(self, caller_id: str, params: dict) -> dict:
272
- mid = params.get("module_id")
273
- if not mid:
274
- return {"ok": False, "error": "module_id required"}
275
- if caller_id != "launcher" and caller_id != mid:
276
- return {"ok": False, "error": f"Module '{caller_id}' cannot heartbeat for '{mid}'"}
277
- return self.registry.heartbeat(mid)
319
+ self.registry.deregister_module(mid)
320
+ self.event_hub.publish_internal("module.unregistered", {"module_id": mid}, source=self.kernel_server.module_id)
321
+
322
+ # Publish registry.updated event for cache invalidation
323
+ from datetime import datetime, timezone
324
+ self.event_hub.publish_internal("registry.updated", {
325
+ "module_id": mid,
326
+ "timestamp": datetime.now(timezone.utc).isoformat(),
327
+ "action": "deregister",
328
+ }, source=self.kernel_server.module_id)
329
+
330
+ return {}
278
331
 
279
332
  async def _registry_lookup(self, caller_id: str, params: dict) -> dict:
333
+ field = params.get("field")
334
+ module = params.get("module")
335
+ value = params.get("value")
336
+
337
+ print(f"[kernel] registry.lookup called by {caller_id}: field={field}, module={module}, value={value}")
338
+
280
339
  results = self.registry.lookup(
281
- field=params.get("field"),
282
- module=params.get("module"),
283
- value=params.get("value"),
340
+ field=field,
341
+ module=module,
342
+ value=value,
284
343
  )
285
- return {"ok": True, "results": results}
344
+
345
+ print(f"[kernel] registry.lookup results: {len(results)} matches")
346
+ for r in results:
347
+ print(f"[kernel] - {r['module']}.{r['field']} = {r['value']}")
348
+
349
+ return {
350
+ "results": results,
351
+ "last_update_time": self.registry.last_update_time
352
+ }
286
353
 
287
354
  async def _registry_get(self, caller_id: str, params: dict) -> dict:
288
355
  path = params.get("path", "")
289
356
  if not path:
290
- return {"ok": False, "error": "path required"}
357
+ raise ValueError("path required")
291
358
  val, found = self.registry.get_by_path(path)
292
359
  if not found:
293
- return {"ok": False, "error": f"Path not found: {path}"}
294
- return {"ok": True, "value": val}
360
+ raise KeyError(f"Path not found: {path}")
361
+ return {"value": val}
295
362
 
296
363
  async def _registry_verify(self, caller_id: str, params: dict) -> dict:
297
364
  token = params.get("token", "")
298
365
  module_id = self.registry.verify_token(token)
299
- if module_id:
300
- return {"ok": True, "module_id": module_id}
301
- return {"ok": False}
366
+ if not module_id:
367
+ raise PermissionError("Invalid token")
368
+ return {"module_id": module_id}
302
369
 
303
370
  # ── Builtin handlers: event.* ──
304
371
 
@@ -311,23 +378,24 @@ class RpcRouter:
311
378
  # When a module publishes module.ready, update its status in registry
312
379
  if event_type == "module.ready":
313
380
  mid = (data or {}).get("module_id", caller_id)
381
+ print(f"[kernel] DEBUG: 收到 module.ready 事件,module_id={mid}, caller_id={caller_id}")
314
382
  self.registry.set_ready(mid)
383
+ print(f"[kernel] DEBUG: 已调用 set_ready({mid})")
315
384
 
316
385
  return self.event_hub.publish_event(caller_id, event_id, event_type, data, echo)
317
386
 
318
387
  async def _event_subscribe(self, caller_id: str, params: dict) -> dict:
319
388
  events = params.get("events", [])
320
389
  if not isinstance(events, list) or not events:
321
- return {"ok": False, "error": "events must be a non-empty list"}
390
+ raise ValueError("events must be a non-empty list")
322
391
  self.event_hub.handle_subscribe(caller_id, events)
323
- return {"ok": True}
392
+ return {}
324
393
 
325
394
  async def _event_unsubscribe(self, caller_id: str, params: dict) -> dict:
326
395
  events = params.get("events", [])
327
396
  if not isinstance(events, list) or not events:
328
- return {"ok": False, "error": "events must be a non-empty list"}
329
- self.event_hub.handle_unsubscribe(caller_id, events)
330
- return {"ok": True}
397
+ raise ValueError("events must be a non-empty list")
398
+ return self.event_hub.handle_unsubscribe(caller_id, events)
331
399
 
332
400
  # ── Builtin handlers: kernel.* ──
333
401
 
@@ -335,7 +403,16 @@ class RpcRouter:
335
403
  return {"pong": True, "timestamp": datetime.now(timezone.utc).isoformat()}
336
404
 
337
405
  async def _kernel_stats(self, caller_id: str, params: dict) -> dict:
338
- return self.event_hub.get_stats()
406
+ event_stats = self.event_hub.get_stats()
407
+ return {
408
+ **event_stats,
409
+ "rpc": {
410
+ "total": self._rpc_total,
411
+ "builtin": self._rpc_builtin,
412
+ "forwarded": self._rpc_forwarded,
413
+ "errors": self._rpc_errors
414
+ }
415
+ }
339
416
 
340
417
  async def _kernel_health(self, caller_id: str, params: dict) -> dict:
341
418
  eh_health = self.event_hub.get_health()
@@ -349,6 +426,34 @@ class RpcRouter:
349
426
  "event_stats": eh_health.get("details", {}),
350
427
  }
351
428
 
429
+ async def _kernel_latencies(self, caller_id: str, params: dict) -> dict:
430
+ """Get ping/pong latencies for all connected modules.
431
+
432
+ Returns:
433
+ {
434
+ "latencies": {
435
+ "module1": {
436
+ "outbound": 12.34, # ms, Kernel → module
437
+ "inbound": 23.45, # ms, module → Kernel
438
+ "status": "ok" | "timeout" | "never"
439
+ },
440
+ ...
441
+ }
442
+ }
443
+ """
444
+ result = {}
445
+ for module_id in self.kernel_server.connections.keys():
446
+ latency_data = self.kernel_server._pong_latencies.get(module_id, {})
447
+ status = self.kernel_server._pong_status.get(module_id, "never")
448
+
449
+ result[module_id] = {
450
+ "outbound": latency_data.get("outbound"),
451
+ "inbound": latency_data.get("inbound"),
452
+ "status": status,
453
+ }
454
+
455
+ return {"latencies": result}
456
+
352
457
  async def _kernel_generate_tokens(self, caller_id: str, params: dict) -> dict:
353
458
  """Generate tokens for a list of module names.
354
459
 
@@ -356,15 +461,15 @@ class RpcRouter:
356
461
  params: {"modules": ["mod1", "mod2", ...]}
357
462
 
358
463
  Returns:
359
- {"ok": True, "tokens": {"mod1": "token1", "mod2": "token2", ...}}
464
+ {"tokens": {"mod1": "token1", "mod2": "token2", ...}}
360
465
  """
361
466
  # Only Launcher may request token generation
362
467
  if caller_id != "launcher":
363
- return {"ok": False, "error": "Only Launcher may generate tokens"}
468
+ raise PermissionError("Only Launcher may generate tokens")
364
469
 
365
470
  modules = params.get("modules", [])
366
471
  if not isinstance(modules, list):
367
- return {"ok": False, "error": "modules must be a list"}
472
+ raise ValueError("modules must be a list")
368
473
 
369
474
  import secrets
370
475
  tokens = {}
@@ -374,25 +479,13 @@ class RpcRouter:
374
479
  # Register tokens in registry
375
480
  self.registry.register_tokens(tokens)
376
481
 
377
- return {"ok": True, "tokens": tokens}
482
+ return {"tokens": tokens}
378
483
 
379
484
  async def _kernel_register_tokens(self, caller_id: str, params: dict) -> dict:
380
485
  # Only Launcher may register tokens
381
486
  if caller_id != "launcher":
382
- return {"ok": False, "error": "Only Launcher may register tokens"}
487
+ raise PermissionError("Only Launcher may register tokens")
383
488
  self.registry.register_tokens(params)
384
- return {"ok": True}
385
-
386
- async def _kernel_shutdown(self, caller_id: str, params: dict) -> dict:
387
- """Shutdown Kernel. Only Launcher may call this."""
388
- if caller_id != "launcher":
389
- return {"ok": False, "error": "Only Launcher may shutdown Kernel"}
390
-
391
- print("[kernel] Received shutdown request from Launcher")
392
-
393
- # Schedule shutdown (don't block RPC response)
394
- if self.kernel_server:
395
- asyncio.create_task(self.kernel_server.shutdown())
489
+ return {}
396
490
 
397
- return {"ok": True}
398
491