@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
@@ -1,12 +1,49 @@
1
1
  // ============================================================
2
2
  // API Client
3
3
  // ============================================================
4
+
5
+ // 全局注册中心缓存
6
+ const _registryCache = {
7
+ lastUpdateTime: 0,
8
+ records: {} // field -> {results, fetchedAt, lastUpdateTime}
9
+ };
10
+
11
+ // 监听缓存失效事件
12
+ window.addEventListener('registryCacheInvalidate', (event) => {
13
+ const data = event.detail;
14
+ console.log('[RegistryCache] Cache invalidate event:', data);
15
+
16
+ if (data.last_update_time) {
17
+ // 更新全局时间戳
18
+ _registryCache.lastUpdateTime = data.last_update_time;
19
+
20
+ // 清空所有过期缓存
21
+ for (const field in _registryCache.records) {
22
+ const record = _registryCache.records[field];
23
+ if (record.lastUpdateTime < data.last_update_time) {
24
+ console.log(`[RegistryCache] Invalidating cache for field="${field}"`);
25
+ delete _registryCache.records[field];
26
+ }
27
+ }
28
+ }
29
+ });
30
+
4
31
  const API = {
5
32
  async get(url) {
6
33
  const resp = await fetch(url);
7
34
  if (!resp.ok) {
8
35
  const err = await resp.json().catch(() => ({ detail: resp.statusText }));
9
- throw new Error(err.detail || resp.statusText);
36
+ let errorMsg = resp.statusText;
37
+ if (err.detail) {
38
+ if (typeof err.detail === 'string') {
39
+ errorMsg = err.detail;
40
+ } else if (Array.isArray(err.detail)) {
41
+ errorMsg = err.detail.map(e => e.msg || JSON.stringify(e)).join('; ');
42
+ } else {
43
+ errorMsg = JSON.stringify(err.detail);
44
+ }
45
+ }
46
+ throw new Error(errorMsg);
10
47
  }
11
48
  return resp.json();
12
49
  },
@@ -19,7 +56,17 @@ const API = {
19
56
  });
20
57
  if (!resp.ok) {
21
58
  const err = await resp.json().catch(() => ({ detail: resp.statusText }));
22
- throw new Error(err.detail || resp.statusText);
59
+ let errorMsg = resp.statusText;
60
+ if (err.detail) {
61
+ if (typeof err.detail === 'string') {
62
+ errorMsg = err.detail;
63
+ } else if (Array.isArray(err.detail)) {
64
+ errorMsg = err.detail.map(e => e.msg || JSON.stringify(e)).join('; ');
65
+ } else {
66
+ errorMsg = JSON.stringify(err.detail);
67
+ }
68
+ }
69
+ throw new Error(errorMsg);
23
70
  }
24
71
  return resp.json();
25
72
  },
@@ -32,7 +79,18 @@ const API = {
32
79
  });
33
80
  if (!resp.ok) {
34
81
  const err = await resp.json().catch(() => ({ detail: resp.statusText }));
35
- throw new Error(err.detail || resp.statusText);
82
+ let errorMsg = resp.statusText;
83
+ if (err.detail) {
84
+ if (typeof err.detail === 'string') {
85
+ errorMsg = err.detail;
86
+ } else if (Array.isArray(err.detail)) {
87
+ // FastAPI validation errors: [{"loc": [...], "msg": "...", "type": "..."}]
88
+ errorMsg = err.detail.map(e => e.msg || JSON.stringify(e)).join('; ');
89
+ } else {
90
+ errorMsg = JSON.stringify(err.detail);
91
+ }
92
+ }
93
+ throw new Error(errorMsg);
36
94
  }
37
95
  return resp.json();
38
96
  },
@@ -41,12 +99,118 @@ const API = {
41
99
  const resp = await fetch(url, { method: 'DELETE' });
42
100
  if (!resp.ok) {
43
101
  const err = await resp.json().catch(() => ({ detail: resp.statusText }));
44
- throw new Error(err.detail || resp.statusText);
102
+ let errorMsg = resp.statusText;
103
+ if (err.detail) {
104
+ if (typeof err.detail === 'string') {
105
+ errorMsg = err.detail;
106
+ } else if (Array.isArray(err.detail)) {
107
+ errorMsg = err.detail.map(e => e.msg || JSON.stringify(e)).join('; ');
108
+ } else {
109
+ errorMsg = JSON.stringify(err.detail);
110
+ }
111
+ }
112
+ throw new Error(errorMsg);
45
113
  }
46
114
  return resp.json();
47
115
  },
48
116
  };
49
117
 
118
+ // ============================================================
119
+ // Module RPC Lookup Helper
120
+ // ============================================================
121
+ /**
122
+ * 通过字段名查询模块的 RPC 方法(支持智能降级)
123
+ *
124
+ * 优先级:
125
+ * 1. 目标模块自己的 RPC
126
+ * 2. 目标模块所属的 Launcher RPC(通过 launcher_id)
127
+ * 3. 本地 Launcher RPC(launcher)
128
+ *
129
+ * @param {string} moduleName - 模块名
130
+ * @param {string} field - 字段名(如 "tools.rpc.module.config.get")
131
+ * @returns {Promise<{module: string, method: string, needsModuleName: boolean}|null>}
132
+ */
133
+ async function lookupModuleRpc(moduleName, field) {
134
+ try {
135
+ console.log(`[lookupModuleRpc] Looking up field="${field}" for module="${moduleName}"`);
136
+
137
+ // 检查缓存
138
+ const cached = _registryCache.records[field];
139
+ if (cached && cached.lastUpdateTime >= _registryCache.lastUpdateTime) {
140
+ console.log(`[lookupModuleRpc] Using cached result for field="${field}"`);
141
+ // 使用缓存的 results 进行后续处理
142
+ const result = { results: cached.results, last_update_time: cached.lastUpdateTime };
143
+ return _processLookupResult(result, moduleName, field);
144
+ }
145
+
146
+ // 查询所有注册了该字段的模块(不限定 module)
147
+ const result = await kernelClient.call("registry.lookup", {
148
+ field: field
149
+ });
150
+
151
+ console.log(`[lookupModuleRpc] Lookup result:`, result);
152
+
153
+ // 保存到缓存
154
+ if (result && result.last_update_time) {
155
+ _registryCache.records[field] = {
156
+ results: result.results || [],
157
+ fetchedAt: Date.now(),
158
+ lastUpdateTime: result.last_update_time
159
+ };
160
+ // 更新全局时间戳
161
+ if (result.last_update_time > _registryCache.lastUpdateTime) {
162
+ _registryCache.lastUpdateTime = result.last_update_time;
163
+ }
164
+ }
165
+
166
+ return _processLookupResult(result, moduleName, field);
167
+ } catch (err) {
168
+ console.error(`[lookupModuleRpc] Exception:`, err);
169
+ return null;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * 处理 lookup 结果,提取合适的 RPC 方法
175
+ */
176
+ function _processLookupResult(result, moduleName, field) {
177
+ // kernelClient.call() 返回的是 msg.result,不是整个 msg
178
+ if (!result || !result.results?.length) {
179
+ console.warn(`[lookupModuleRpc] No results found for field="${field}"`);
180
+ return null;
181
+ }
182
+
183
+ // 优先级 1:目标模块自己的 RPC
184
+ const selfRpc = result.results.find(r => r.module === moduleName);
185
+ if (selfRpc) {
186
+ // value 现在是对象 {method: "xxx", description: "xxx"}
187
+ const method = typeof selfRpc.value === 'string' ? selfRpc.value : selfRpc.value.method;
188
+ console.log(`[lookupModuleRpc] Found self RPC: ${moduleName}.${method}`);
189
+ return {
190
+ module: moduleName,
191
+ method: method,
192
+ needsModuleName: false
193
+ };
194
+ }
195
+
196
+ // 优先级 2 & 3:使用 Launcher 的 RPC(简化版,直接查找 launcher)
197
+ const launcherRpc = result.results.find(r => r.module === "launcher");
198
+ if (launcherRpc) {
199
+ // value 现在是对象 {method: "xxx", description: "xxx"}
200
+ const method = typeof launcherRpc.value === 'string' ? launcherRpc.value : launcherRpc.value.method;
201
+ console.log(`[lookupModuleRpc] Fallback to launcher RPC: launcher.${method}`);
202
+ return {
203
+ module: "launcher",
204
+ method: method,
205
+ needsModuleName: true
206
+ };
207
+ }
208
+
209
+ console.warn(`[lookupModuleRpc] No suitable RPC found for field="${field}"`);
210
+ return null;
211
+ }
212
+
213
+
50
214
  // ============================================================
51
215
  // Toast Notifications
52
216
  // ============================================================
@@ -72,6 +236,15 @@ function showToast(message, type = 'info') {
72
236
  toast.className = `toast toast-${type}`;
73
237
  toast.textContent = message;
74
238
 
239
+ // 错误类型自动复制到剪贴板
240
+ if (type === 'error') {
241
+ try {
242
+ navigator.clipboard.writeText(message);
243
+ } catch (e) {
244
+ // 剪贴板 API 不可用时忽略
245
+ }
246
+ }
247
+
75
248
  const colors = { info: '#3b82f6', success: '#10b981', error: '#ef4444' };
76
249
  toast.style.cssText =
77
250
  `padding:12px 20px;border-radius:8px;color:#fff;font-size:14px;` +
@@ -103,6 +276,11 @@ let currentPage = '';
103
276
  function navigate(page) {
104
277
  if (!pages.includes(page)) page = 'dashboard';
105
278
 
279
+ // Stop stats auto-refresh when leaving modules page
280
+ if (currentPage === 'modules' && page !== 'modules') {
281
+ stopStatsAutoRefresh();
282
+ }
283
+
106
284
  // Toggle .active class on page sections (CSS: .page { display:none } .page.active { display:block })
107
285
  pages.forEach((p) => {
108
286
  const el = document.getElementById(`page-${p}`);
@@ -3085,6 +3263,12 @@ function _setVal(id, value) {
3085
3263
  if (el) el.value = value != null ? String(value) : '';
3086
3264
  }
3087
3265
 
3266
+ /** Get value from an input/select element. */
3267
+ function _getVal(id) {
3268
+ const el = document.getElementById(id);
3269
+ return el ? el.value : '';
3270
+ }
3271
+
3088
3272
  /** Set a <select> value, adding the option dynamically if it doesn't exist. */
3089
3273
  function _setSelectVal(id, value) {
3090
3274
  const el = document.getElementById(id);
@@ -4208,61 +4392,1060 @@ function closeDevLogArchiveModal() {
4208
4392
  // ============================================================
4209
4393
  let _moduleSaveTimer = null;
4210
4394
  let _currentModuleName = '';
4395
+ // 模块运行状态缓存: { moduleName: { running: bool, pid: number|null } }
4396
+ let _moduleRunStates = {};
4397
+ // 操作中的模块: moduleName → 'start' | 'stop'
4398
+ let _moduleActionPending = new Map();
4399
+ // 管理 WebSocket 连接
4400
+ let _managementWs = null;
4401
+ let _managementWsConnected = false;
4402
+ // 统计数据刷新定时器
4403
+ let _statsRefreshTimer = null;
4404
+ // 统计详情数据缓存
4405
+ let _statsDetailCache = {};
4406
+ // 控制台状态
4407
+ let _consoleExpanded = false;
4408
+ let _consoleEventSubscribed = false;
4409
+
4410
+ async function _fetchModuleRunStates() {
4411
+ /**
4412
+ * 通过 WebSocket RPC 查询 Launcher 获取各模块实际运行状态。
4413
+ * 失败时(如 Kernel 未连接)返回空对象,不影响页面其余渲染。
4414
+ */
4415
+ try {
4416
+ if (!kernelClient || !kernelClient.connected) {
4417
+ console.warn('[modules] kernelClient not connected, cannot fetch run states');
4418
+ return {};
4419
+ }
4420
+ console.log('[modules] Calling launcher.list_modules...');
4421
+ const res = await kernelClient.call('launcher.list_modules', {});
4422
+ console.log('[modules] launcher.list_modules result:', res);
4423
+ const map = {};
4424
+ for (const m of (res.modules || [])) {
4425
+ const running = m.actual_state ? m.actual_state.startsWith('running') : false;
4426
+ map[m.name] = { running, pid: m.pid || null };
4427
+ console.log(`[modules] Module ${m.name}: running=${running}, actual_state=${m.actual_state}`);
4428
+ }
4429
+ // RPC 调用成功说明 Kernel 和 Launcher 必然在运行,但它们不在自己的列表中
4430
+ if (!map['kernel']) map['kernel'] = { running: true, pid: null };
4431
+ if (!map['launcher']) map['launcher'] = { running: true, pid: null };
4432
+ console.log('[modules] Final runStates map:', map);
4433
+ return map;
4434
+ } catch (err) {
4435
+ console.error('[modules] 获取运行状态失败:', err);
4436
+ return {};
4437
+ }
4438
+ }
4439
+
4440
+ // 基础设施模块(infrastructure),不可由用户手动启动/停止
4441
+ const _CORE_MODULES = new Set(['kernel', 'launcher']);
4442
+
4443
+ function _isCoreModule(name) {
4444
+ return _CORE_MODULES.has(name);
4445
+ }
4446
+
4447
+ function _isModuleRunning(name) {
4448
+ const s = _moduleRunStates[name];
4449
+ return s ? s.running : false;
4450
+ }
4451
+
4452
+ // ============================================================
4453
+ // Management WebSocket — 实时接收模块状态变更
4454
+ // ============================================================
4455
+
4456
+ function connectManagementWebSocket() {
4457
+ if (_managementWs && _managementWs.readyState === WebSocket.OPEN) {
4458
+ return; // 已连接
4459
+ }
4460
+
4461
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
4462
+ const url = `${proto}//${location.host}/ws/management`;
4463
+
4464
+ console.log('[Management WS] Connecting to', url);
4465
+ _managementWs = new WebSocket(url);
4466
+
4467
+ _managementWs.onopen = () => {
4468
+ console.log('[Management WS] Connected');
4469
+ _managementWsConnected = true;
4470
+ _updateWsIndicator();
4471
+ };
4472
+
4473
+ _managementWs.onmessage = (event) => {
4474
+ try {
4475
+ const msg = JSON.parse(event.data);
4476
+ _handleManagementEvent(msg);
4477
+ } catch (err) {
4478
+ console.error('[Management WS] Parse error:', err);
4479
+ }
4480
+ };
4481
+
4482
+ _managementWs.onclose = () => {
4483
+ console.log('[Management WS] Disconnected, reconnecting in 3s...');
4484
+ _managementWsConnected = false;
4485
+ _updateWsIndicator();
4486
+ _managementWs = null;
4487
+ setTimeout(connectManagementWebSocket, 3000);
4488
+ };
4489
+
4490
+ _managementWs.onerror = (err) => {
4491
+ console.error('[Management WS] Error:', err);
4492
+ };
4493
+
4494
+ // 心跳(每 30 秒)
4495
+ setInterval(() => {
4496
+ if (_managementWs && _managementWs.readyState === WebSocket.OPEN) {
4497
+ _managementWs.send(JSON.stringify({ type: 'ping' }));
4498
+ }
4499
+ }, 30000);
4500
+ }
4501
+
4502
+ function _handleManagementEvent(msg) {
4503
+ const { type, data } = msg;
4504
+
4505
+ console.log('[Management WS] Event:', type, data);
4506
+
4507
+ switch (type) {
4508
+ case 'connected':
4509
+ console.log('[Management WS] Server confirmed connection');
4510
+ break;
4511
+
4512
+ case 'pong':
4513
+ // 心跳响应
4514
+ break;
4515
+
4516
+ case 'module.started':
4517
+ case 'module.ready':
4518
+ // 模块启动完成
4519
+ if (data.module_id) {
4520
+ _onModuleStatusChange(data.module_id, 'running');
4521
+ }
4522
+ break;
4523
+
4524
+ case 'module.stopped':
4525
+ case 'module.crashed':
4526
+ // 模块停止或崩溃
4527
+ if (data.module_id) {
4528
+ _onModuleStatusChange(data.module_id, 'stopped');
4529
+ }
4530
+ break;
4531
+
4532
+ case 'module.exiting':
4533
+ case 'module.shutdown.ack':
4534
+ // 模块正在关闭
4535
+ if (data.module_id) {
4536
+ _onModuleStatusChange(data.module_id, 'stopping');
4537
+ }
4538
+ break;
4539
+
4540
+ default:
4541
+ // 其他事件暂不处理
4542
+ break;
4543
+ }
4544
+ }
4545
+
4546
+ function _onModuleStatusChange(moduleName, status) {
4547
+ console.log(`[Management WS] Module ${moduleName} → ${status}`);
4548
+
4549
+ // 更新缓存
4550
+ if (status === 'running') {
4551
+ _moduleRunStates[moduleName] = { running: true, pid: null };
4552
+ _moduleActionPending.delete(moduleName);
4553
+ } else if (status === 'stopped') {
4554
+ _moduleRunStates[moduleName] = { running: false, pid: null };
4555
+ _moduleActionPending.delete(moduleName);
4556
+ } else if (status === 'stopping') {
4557
+ // 保持 pending 状态,不清除
4558
+ }
4559
+
4560
+ // 刷新 UI
4561
+ _refreshModuleButtonsEverywhere(moduleName);
4562
+
4563
+ // 如果当前在模块详情页,刷新详情
4564
+ if (_currentModuleName === moduleName) {
4565
+ openModuleDetail(moduleName);
4566
+ }
4567
+ }
4568
+
4569
+ function _updateWsIndicator() {
4570
+ // 更新页面上的 WebSocket 连接指示器(仅 header)
4571
+ const indicator = document.getElementById('ws-indicator');
4572
+ if (indicator) {
4573
+ if (_managementWsConnected) {
4574
+ indicator.textContent = '● 已连线';
4575
+ indicator.style.color = 'var(--success)';
4576
+ } else {
4577
+ indicator.textContent = '○ 未连线';
4578
+ indicator.style.color = 'var(--gray-400)';
4579
+ }
4580
+ }
4581
+ }
4211
4582
 
4212
4583
  async function loadModules() {
4213
- // Reset to grid view
4214
- document.getElementById('modules-grid')?.classList.remove('hidden');
4584
+ // Reset to list view
4585
+ document.getElementById('modules-list-header')?.classList.remove('hidden');
4586
+ document.getElementById('modules-table')?.closest('.panel')?.classList.remove('hidden');
4215
4587
  document.getElementById('module-detail')?.classList.add('hidden');
4588
+ document.getElementById('token-management-section')?.classList.remove('hidden');
4589
+ document.getElementById('statistics-panel')?.classList.remove('hidden');
4590
+ document.getElementById('registry-test-section')?.classList.remove('hidden');
4591
+ document.getElementById('registry-test-output')?.classList.remove('hidden');
4592
+
4593
+ // Load statistics and start auto-refresh
4594
+ loadModuleStats();
4595
+ startStatsAutoRefresh();
4216
4596
 
4217
4597
  try {
4218
- const modules = await API.get('/api/modules');
4219
- renderModulesGrid(modules);
4598
+ // 等待 kernelClient 连接(最多等待 5 秒)
4599
+ if (!kernelClient || !kernelClient.connected) {
4600
+ console.log('[modules] Waiting for kernelClient to connect...');
4601
+ await new Promise((resolve, reject) => {
4602
+ const timeout = setTimeout(() => {
4603
+ reject(new Error('kernelClient 连接超时'));
4604
+ }, 5000);
4605
+
4606
+ const checkAndResolve = () => {
4607
+ if (kernelClient && kernelClient.connected) {
4608
+ clearTimeout(timeout);
4609
+ resolve();
4610
+ return true;
4611
+ }
4612
+ return false;
4613
+ };
4614
+
4615
+ // 立即检查一次
4616
+ if (checkAndResolve()) return;
4617
+
4618
+ // 监听连接事件
4619
+ window.addEventListener('kernelClientReady', () => {
4620
+ checkAndResolve();
4621
+ }, { once: true });
4622
+ });
4623
+ }
4624
+
4625
+ console.log('[modules] kernelClient connected, fetching modules...');
4626
+
4627
+ // 通过 RPC 获取模块列表(包含元数据和运行状态)
4628
+ let res;
4629
+ try {
4630
+ res = await kernelClient.call('launcher.list_modules', {});
4631
+ } catch (err) {
4632
+ if (err.message && err.message.includes('not ready')) {
4633
+ // Launcher 未就绪,等待 1 秒后重试
4634
+ console.warn('[modules] Launcher not ready, retrying in 1s...');
4635
+ await new Promise(resolve => setTimeout(resolve, 1000));
4636
+ res = await kernelClient.call('launcher.list_modules', {});
4637
+ } else {
4638
+ throw err;
4639
+ }
4640
+ }
4641
+ const modules = res.modules || [];
4642
+
4643
+ console.log('[modules] Fetched modules:', modules.length);
4644
+
4645
+ // 构建运行状态映射
4646
+ const runStates = {};
4647
+ for (const m of modules) {
4648
+ const running = m.actual_state ? m.actual_state.startsWith('running') : false;
4649
+ runStates[m.name] = { running, pid: m.pid || null };
4650
+ console.log(`[modules] Module ${m.name}: running=${running}, actual_state=${m.actual_state}`);
4651
+ }
4652
+ // RPC 调用成功说明 Kernel 和 Launcher 必然在运行
4653
+ if (!runStates['kernel']) runStates['kernel'] = { running: true, pid: null };
4654
+ if (!runStates['launcher']) runStates['launcher'] = { running: true, pid: null };
4655
+
4656
+ _moduleRunStates = runStates;
4657
+ renderModulesTable(modules);
4658
+ } catch (err) {
4659
+ console.error('[modules] Load failed:', err);
4660
+ const tbody = document.getElementById('modules-tbody');
4661
+ if (tbody) tbody.innerHTML = `<tr><td colspan="9" class="text-muted" style="text-align:center;padding:40px;">加载失败: ${escapeHtml(err.message)}</td></tr>`;
4662
+ }
4663
+ }
4664
+
4665
+ async function loadModuleStats() {
4666
+ try {
4667
+ // Wait for kernelClient
4668
+ if (!kernelClient || !kernelClient.connected) {
4669
+ await new Promise((resolve, reject) => {
4670
+ const timeout = setTimeout(() => reject(new Error('timeout')), 3000);
4671
+ const check = () => {
4672
+ if (kernelClient?.connected) {
4673
+ clearTimeout(timeout);
4674
+ resolve();
4675
+ return true;
4676
+ }
4677
+ return false;
4678
+ };
4679
+ if (check()) return;
4680
+ window.addEventListener('kernelClientReady', check, { once: true });
4681
+ });
4682
+ }
4683
+
4684
+ // Get kernel health (includes uptime)
4685
+ const health = await kernelClient.call('kernel.health', {});
4686
+ const eventStats = health.event_stats || {};
4687
+
4688
+ // Get kernel stats (includes event counters)
4689
+ const stats = await kernelClient.call('kernel.stats', {});
4690
+ const counters = stats.counters || {};
4691
+ const rpcStats = stats.rpc || {};
4692
+
4693
+ // Get all modules (with retry for launcher not ready)
4694
+ let modules = [];
4695
+ let modulesRes = null;
4696
+ try {
4697
+ modulesRes = await kernelClient.call('launcher.list_modules', {});
4698
+ modules = modulesRes.modules || [];
4699
+ } catch (err) {
4700
+ if (err.message.includes('not ready')) {
4701
+ // Launcher not ready yet, skip module stats (silent)
4702
+ // This is normal during startup, no need to log
4703
+ } else {
4704
+ throw err;
4705
+ }
4706
+ }
4707
+
4708
+ // Calculate uptime
4709
+ const uptime = eventStats.uptime_seconds || 0;
4710
+ const uptimeStr = formatUptime(uptime);
4711
+
4712
+ // Count modules
4713
+ const moduleCount = modules.length;
4714
+
4715
+ // Get all registry records and categorize
4716
+ let registryByCategory = {
4717
+ modules: 0, // module.* 字段
4718
+ rpc: 0, // tools.rpc.* 字段
4719
+ hook: 0, // tools.hook.* 字段
4720
+ api: 0, // tools.api.* 字段
4721
+ other: 0 // 其他字段
4722
+ };
4723
+ let registryDetails = [];
4724
+ let totalRecords = 0;
4725
+
4726
+ for (const mod of modules) {
4727
+ const modName = mod.name;
4728
+ try {
4729
+ // Query registry for this module
4730
+ const regRes = await kernelClient.call('registry.lookup', { module: modName });
4731
+ const records = regRes.results || [];
4732
+ totalRecords += records.length;
4733
+
4734
+ // Categorize by field path
4735
+ for (const rec of records) {
4736
+ const field = rec.field || '';
4737
+ registryDetails.push({ module: modName, field, value: rec.value });
4738
+
4739
+ if (field.startsWith('module.')) {
4740
+ registryByCategory.modules++;
4741
+ } else if (field.startsWith('tools.rpc.')) {
4742
+ registryByCategory.rpc++;
4743
+ } else if (field.startsWith('tools.hook.')) {
4744
+ registryByCategory.hook++;
4745
+ } else if (field.startsWith('tools.api.')) {
4746
+ registryByCategory.api++;
4747
+ } else {
4748
+ registryByCategory.other++;
4749
+ }
4750
+ }
4751
+ } catch (e) {
4752
+ // Module may not have registered yet
4753
+ console.warn(`[stats] Failed to query registry for ${modName}:`, e.message);
4754
+ }
4755
+ }
4756
+
4757
+ // Event stats
4758
+ const eventsRouted = counters.events_routed || 0;
4759
+
4760
+ // RPC stats
4761
+ const rpcCalls = rpcStats.total || 0;
4762
+
4763
+ // Update UI
4764
+ document.getElementById('stat-uptime').textContent = uptimeStr;
4765
+ document.getElementById('stat-modules').textContent = moduleCount;
4766
+ document.getElementById('stat-registry').textContent = totalRecords;
4767
+ document.getElementById('stat-rpc').textContent = registryByCategory.rpc;
4768
+ document.getElementById('stat-hooks').textContent = registryByCategory.hook;
4769
+ document.getElementById('stat-api').textContent = registryByCategory.api;
4770
+ document.getElementById('stat-events').textContent = eventsRouted;
4771
+ document.getElementById('stat-rpc-calls').textContent = rpcCalls;
4772
+
4773
+ // Cache detail data for tooltips (always update)
4774
+ _statsDetailCache = {
4775
+ uptime: { startTime: Date.now() - uptime * 1000, uptime },
4776
+ modules,
4777
+ registry: {
4778
+ totalRecords,
4779
+ byCategory: registryByCategory,
4780
+ details: registryDetails
4781
+ },
4782
+ events: counters,
4783
+ rpcStats
4784
+ };
4220
4785
  } catch (err) {
4221
- const grid = document.getElementById('modules-grid');
4222
- if (grid) grid.innerHTML = `<p class="text-muted" style="padding:40px;text-align:center;">加载失败: ${escapeHtml(err.message)}</p>`;
4786
+ console.error('[stats] Load failed:', err);
4787
+ // Don't clear UI on error, keep last known values
4788
+ }
4789
+ }
4790
+
4791
+ function formatUptime(seconds) {
4792
+ if (seconds < 60) return `${seconds}秒`;
4793
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`;
4794
+ if (seconds < 86400) {
4795
+ const h = Math.floor(seconds / 3600);
4796
+ const m = Math.floor((seconds % 3600) / 60);
4797
+ return m > 0 ? `${h}小时${m}分` : `${h}小时`;
4798
+ }
4799
+ const d = Math.floor(seconds / 86400);
4800
+ const h = Math.floor((seconds % 86400) / 3600);
4801
+ return h > 0 ? `${d}天${h}小时` : `${d}天`;
4802
+ }
4803
+
4804
+ function startStatsAutoRefresh() {
4805
+ // Clear existing timer
4806
+ if (_statsRefreshTimer) {
4807
+ clearInterval(_statsRefreshTimer);
4808
+ }
4809
+ // Refresh every 1 second
4810
+ _statsRefreshTimer = setInterval(() => {
4811
+ loadModuleStats();
4812
+ }, 1000);
4813
+ }
4814
+
4815
+ function stopStatsAutoRefresh() {
4816
+ if (_statsRefreshTimer) {
4817
+ clearInterval(_statsRefreshTimer);
4818
+ _statsRefreshTimer = null;
4819
+ }
4820
+ }
4821
+
4822
+ // ============================================================
4823
+ // Statistics Tooltip
4824
+ // ============================================================
4825
+
4826
+ function initStatsTooltip() {
4827
+ const tooltip = document.getElementById('stat-tooltip');
4828
+ if (!tooltip) return;
4829
+
4830
+ document.querySelectorAll('.stat-item').forEach(item => {
4831
+ item.addEventListener('mouseenter', (e) => showStatTooltip(e, item));
4832
+ item.addEventListener('mousemove', (e) => moveStatTooltip(e));
4833
+ item.addEventListener('mouseleave', () => hideStatTooltip());
4834
+ });
4835
+ }
4836
+
4837
+ function showStatTooltip(e, item) {
4838
+ const tooltip = document.getElementById('stat-tooltip');
4839
+ const type = item.dataset.statType;
4840
+ const content = getStatTooltipContent(type);
4841
+
4842
+ if (!content) return;
4843
+
4844
+ tooltip.innerHTML = content;
4845
+ tooltip.classList.add('show');
4846
+ moveStatTooltip(e);
4847
+ }
4848
+
4849
+ function moveStatTooltip(e) {
4850
+ const tooltip = document.getElementById('stat-tooltip');
4851
+ const offset = 15;
4852
+ tooltip.style.left = (e.clientX + offset) + 'px';
4853
+ tooltip.style.top = (e.clientY + offset) + 'px';
4854
+ }
4855
+
4856
+ function hideStatTooltip() {
4857
+ const tooltip = document.getElementById('stat-tooltip');
4858
+ tooltip.classList.remove('show');
4859
+ }
4860
+
4861
+ function getStatTooltipContent(type) {
4862
+ const cache = _statsDetailCache;
4863
+
4864
+ switch (type) {
4865
+ case 'uptime':
4866
+ if (!cache.uptime) return null;
4867
+ const startTime = new Date(cache.uptime.startTime).toLocaleString('zh-CN');
4868
+ return `<strong>启动时间:</strong> ${startTime}\n<strong>运行时长:</strong> ${formatUptime(cache.uptime.uptime)}`;
4869
+
4870
+ case 'modules':
4871
+ if (!cache.modules) return null;
4872
+ const moduleList = cache.modules.map(m => {
4873
+ const state = m.state || 'unknown';
4874
+ const actualState = m.actual_state || 'unknown';
4875
+ return ` ${m.name.padEnd(15)} [${state}] (${actualState})`;
4876
+ }).join('\n');
4877
+ return `<strong>模块列表 (${cache.modules.length}):</strong>\n${moduleList}`;
4878
+
4879
+ case 'registry':
4880
+ if (!cache.registry) return null;
4881
+ const cat = cache.registry.byCategory;
4882
+ return `<strong>注册记录分类统计:</strong>\n 总记录: ${cache.registry.totalRecords}\n 模块字段: ${cat.modules}\n RPC 方法: ${cat.rpc}\n Hook: ${cat.hook}\n API 端点: ${cat.api}\n 其他: ${cat.other}`;
4883
+
4884
+ case 'rpc':
4885
+ if (!cache.registry || !cache.registry.details) return null;
4886
+ const rpcList = cache.registry.details
4887
+ .filter(r => r.field.startsWith('tools.rpc.'))
4888
+ .slice(0, 20)
4889
+ .map(r => ` ${r.module}: ${r.field}`)
4890
+ .join('\n');
4891
+ const rpcTotal = cache.registry.byCategory.rpc;
4892
+ return `<strong>RPC 方法列表 (${rpcTotal}):</strong>\n${rpcList || ' (无)'}${rpcTotal > 20 ? '\n ...' : ''}`;
4893
+
4894
+ case 'hooks':
4895
+ if (!cache.registry || !cache.registry.details) return null;
4896
+ const hookList = cache.registry.details
4897
+ .filter(r => r.field.startsWith('tools.hook.'))
4898
+ .slice(0, 20)
4899
+ .map(r => ` ${r.module}: ${r.field}`)
4900
+ .join('\n');
4901
+ const hookTotal = cache.registry.byCategory.hook;
4902
+ return `<strong>Hook 列表 (${hookTotal}):</strong>\n${hookList || ' (无)'}${hookTotal > 20 ? '\n ...' : ''}`;
4903
+
4904
+ case 'api':
4905
+ if (!cache.registry || !cache.registry.details) return null;
4906
+ const apiList = cache.registry.details
4907
+ .filter(r => r.field.startsWith('tools.api.'))
4908
+ .slice(0, 20)
4909
+ .map(r => ` ${r.module}: ${r.field}`)
4910
+ .join('\n');
4911
+ const apiTotal = cache.registry.byCategory.api;
4912
+ return `<strong>API 端点列表 (${apiTotal}):</strong>\n${apiList || ' (无)'}${apiTotal > 20 ? '\n ...' : ''}`;
4913
+
4914
+ case 'events':
4915
+ if (!cache.events) return null;
4916
+ return `<strong>事件统计:</strong>\n 已接收: ${cache.events.events_received || 0}\n 已路由: ${cache.events.events_routed || 0}\n 已排队: ${cache.events.events_queued || 0}\n 已去重: ${cache.events.events_deduplicated || 0}\n 错误: ${cache.events.errors || 0}`;
4917
+
4918
+ case 'rpc-calls':
4919
+ if (!cache.rpcStats) return null;
4920
+ return `<strong>RPC 调用统计:</strong>\n 总调用: ${cache.rpcStats.total || 0}\n 成功: ${cache.rpcStats.success || 0}\n 失败: ${cache.rpcStats.failed || 0}`;
4921
+
4922
+ default:
4923
+ return null;
4924
+ }
4925
+ }
4926
+
4927
+ // ============================================================
4928
+ // Real-time Console
4929
+ // ============================================================
4930
+
4931
+ function toggleConsole() {
4932
+ _consoleExpanded = !_consoleExpanded;
4933
+ const consoleEl = document.getElementById('realtime-console');
4934
+ const toggleBtn = document.getElementById('console-toggle-text');
4935
+
4936
+ if (_consoleExpanded) {
4937
+ consoleEl.style.display = 'block';
4938
+ toggleBtn.textContent = '折叠控制台';
4939
+ subscribeConsoleEvents();
4940
+ } else {
4941
+ consoleEl.style.display = 'none';
4942
+ toggleBtn.textContent = '展开控制台';
4943
+ unsubscribeConsoleEvents();
4223
4944
  }
4224
4945
  }
4225
4946
 
4226
- function renderModulesGrid(modules) {
4227
- const grid = document.getElementById('modules-grid');
4228
- if (!grid) return;
4947
+ function subscribeConsoleEvents() {
4948
+ if (_consoleEventSubscribed || !kernelClient || !kernelClient.connected) return;
4949
+
4950
+ // Subscribe to all events
4951
+ kernelClient.call('event.subscribe', { events: ['*'] })
4952
+ .then(() => {
4953
+ _consoleEventSubscribed = true;
4954
+ console.log('[console] Subscribed to all events');
4955
+
4956
+ // Register event listener for all events
4957
+ kernelClient.on('*', (data) => {
4958
+ handleConsoleEvent({ event: '*', data });
4959
+ });
4960
+ })
4961
+ .catch(err => {
4962
+ console.error('[console] Failed to subscribe:', err);
4963
+ });
4964
+ }
4965
+
4966
+ function unsubscribeConsoleEvents() {
4967
+ if (!_consoleEventSubscribed || !kernelClient || !kernelClient.connected) return;
4968
+
4969
+ kernelClient.call('event.unsubscribe', { events: ['*'] })
4970
+ .then(() => {
4971
+ _consoleEventSubscribed = false;
4972
+ console.log('[console] Unsubscribed from all events');
4973
+
4974
+ // Remove event listener
4975
+ kernelClient.off('*');
4976
+ })
4977
+ .catch(err => {
4978
+ console.error('[console] Failed to unsubscribe:', err);
4979
+ });
4980
+ }
4981
+
4982
+ function appendConsoleLog(message, color = '#d4d4d4') {
4983
+ if (!_consoleExpanded) return;
4984
+
4985
+ const output = document.getElementById('console-output');
4986
+ if (!output) return;
4987
+
4988
+ const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
4989
+ const line = document.createElement('div');
4990
+ line.style.color = color;
4991
+ line.textContent = `[${timestamp}] ${message}`;
4992
+
4993
+ output.appendChild(line);
4994
+ output.scrollTop = output.scrollHeight;
4995
+
4996
+ // Limit to 500 lines
4997
+ while (output.children.length > 500) {
4998
+ output.removeChild(output.firstChild);
4999
+ }
5000
+ }
5001
+
5002
+ function clearConsole() {
5003
+ const output = document.getElementById('console-output');
5004
+ if (output) output.innerHTML = '';
5005
+ }
5006
+
5007
+ // Handle incoming events for console
5008
+ function handleConsoleEvent(event) {
5009
+ if (!_consoleExpanded) return;
5010
+
5011
+ const eventType = event.event || 'unknown';
5012
+ const data = event.data || {};
5013
+ const moduleId = data.module_id || 'unknown';
5014
+
5015
+ let color = '#9cdcfe';
5016
+ if (eventType.includes('error') || eventType.includes('crash')) {
5017
+ color = '#f48771';
5018
+ } else if (eventType.includes('ready') || eventType.includes('registered')) {
5019
+ color = '#4ec9b0';
5020
+ }
5021
+
5022
+ const message = `[${eventType}] ${moduleId}: ${JSON.stringify(data)}`;
5023
+ appendConsoleLog(message, color);
5024
+ }
5025
+
5026
+ function renderModulesTable(modules) {
5027
+ const tbody = document.getElementById('modules-tbody');
5028
+ if (!tbody) return;
4229
5029
 
4230
5030
  if (!modules.length) {
4231
- grid.innerHTML = '<p class="text-muted" style="padding:40px;text-align:center;">未发现模块</p>';
5031
+ tbody.innerHTML = '<tr><td colspan="9" class="text-muted" style="text-align:center;padding:40px;">未发现模块</td></tr>';
4232
5032
  return;
4233
5033
  }
4234
5034
 
4235
- grid.innerHTML = modules.map(m => {
5035
+ tbody.innerHTML = modules.map(m => {
4236
5036
  const stateClass = m.state === 'enabled' ? 'enabled' : m.state === 'manual' ? 'manual' : 'disabled';
4237
5037
  const typeClass = m.type || 'unknown';
4238
5038
  const displayName = m.display_name || m.name;
4239
- const version = m.version ? `<span class="module-version">v${escapeHtml(String(m.version))}</span>` : '';
4240
- const configBadge = m.has_config ? '<span class="badge badge-info" style="font-size:10px;">config</span>' : '';
4241
- return `<div class="module-card" data-name="${escapeHtml(m.name)}" onclick="openModuleDetail('${escapeHtml(m.name)}')">
4242
- <div class="module-card-header">
4243
- <span class="module-state-dot ${stateClass}" title="${escapeHtml(m.state || 'enabled')}"></span>
4244
- <span class="module-type-badge type-${escapeHtml(typeClass)}">${escapeHtml(m.type || '?')}</span>
4245
- ${version}
4246
- </div>
4247
- <div class="module-card-name">${escapeHtml(m.name)}</div>
4248
- <div class="module-card-display-name">${escapeHtml(displayName)}</div>
4249
- <div class="module-card-footer">
4250
- ${m.preferred_port ? '<span style="font-size:11px;color:var(--gray-400);">:' + m.preferred_port + '</span>' : ''}
4251
- ${configBadge}
4252
- </div>
4253
- </div>`;
5039
+ const version = m.version || '-';
5040
+ const port = m.preferred_port || '-';
5041
+
5042
+ // 运行状态
5043
+ const running = _isModuleRunning(m.name);
5044
+ const pending = _moduleActionPending.has(m.name);
5045
+ const pendingAction = _moduleActionPending.get(m.name); // 'start' | 'stop'
5046
+ const runningStatus = pending
5047
+ ? `<span style="color:var(--warning);">${pendingAction === 'start' ? '启动中…' : '停止中…'}</span>`
5048
+ : running
5049
+ ? '<span style="color:var(--success);">运行中</span>'
5050
+ : '<span style="color:var(--gray-400);">已停止</span>';
5051
+
5052
+ // 按钮禁用逻辑:操作中 → 全禁;运行中 → 禁启动;已停止 → 禁停止;内核模块 → 禁停止
5053
+ const isCore = _isCoreModule(m.name);
5054
+ const startDisabled = pending || running ? 'disabled' : '';
5055
+ const stopDisabled = isCore || pending || !running ? 'disabled' : '';
5056
+ const startLabel = pendingAction === 'start' ? '启动中' : '启动';
5057
+ const stopLabel = pendingAction === 'stop' ? '停止中' : '停止';
5058
+
5059
+ // 默认状态下拉
5060
+ const stateOptions = `
5061
+ <option value="enabled" ${m.state === 'enabled' ? 'selected' : ''}>自动</option>
5062
+ <option value="manual" ${m.state === 'manual' ? 'selected' : ''}>手动</option>
5063
+ <option value="disabled" ${m.state === 'disabled' ? 'selected' : ''}>禁用</option>
5064
+ `;
5065
+
5066
+ return `<tr data-module="${escapeHtml(m.name)}" class="module-row" onclick="openModuleDetail('${escapeHtml(m.name)}')">
5067
+ <td><span class="module-state-dot ${stateClass}"></span></td>
5068
+ <td><strong>${escapeHtml(m.name)}</strong></td>
5069
+ <td>${escapeHtml(displayName)}</td>
5070
+ <td><span class="module-type-badge type-${escapeHtml(typeClass)}">${escapeHtml(m.type || '?')}</span></td>
5071
+ <td>${escapeHtml(String(version))}</td>
5072
+ <td>${escapeHtml(String(port))}</td>
5073
+ <td>${runningStatus}</td>
5074
+ <td onclick="event.stopPropagation()">
5075
+ <select class="module-state-select" data-module="${escapeHtml(m.name)}" onchange="onModuleStateChange(this)">
5076
+ ${stateOptions}
5077
+ </select>
5078
+ </td>
5079
+ <td onclick="event.stopPropagation()">
5080
+ <button class="btn btn-sm btn-success" onclick="startModule('${escapeHtml(m.name)}')" ${startDisabled}>${startLabel}</button>
5081
+ <button class="btn btn-sm btn-danger" onclick="stopModule('${escapeHtml(m.name)}')" ${stopDisabled}>${stopLabel}</button>
5082
+ </td>
5083
+ </tr>`;
4254
5084
  }).join('');
4255
5085
  }
4256
5086
 
5087
+ async function onModuleStateChange(selectEl) {
5088
+ const moduleName = selectEl.dataset.module;
5089
+ const newState = selectEl.value;
5090
+
5091
+ // 禁用下拉,防止重复操作
5092
+ selectEl.disabled = true;
5093
+
5094
+ try {
5095
+ // 使用 RPC 更新模块配置
5096
+ const rpc = await lookupModuleRpc(moduleName, "tools.rpc.module.config.update");
5097
+ if (!rpc) {
5098
+ throw new Error(`模块 ${moduleName} 不支持配置更新`);
5099
+ }
5100
+ const params = rpc.needsModuleName
5101
+ ? { module_name: moduleName, metadata: { state: newState } }
5102
+ : { metadata: { state: newState } };
5103
+ await kernelClient.call(`${rpc.module}.${rpc.method}`, params);
5104
+ showToast(`模块 ${moduleName} 默认状态已更新为 ${newState}`, 'success');
5105
+
5106
+ // 更新状态圆点
5107
+ const row = selectEl.closest('tr');
5108
+ const dot = row?.querySelector('.module-state-dot');
5109
+ if (dot) {
5110
+ dot.className = `module-state-dot ${newState === 'enabled' ? 'enabled' : newState === 'manual' ? 'manual' : 'disabled'}`;
5111
+ }
5112
+ } catch (err) {
5113
+ showToast('更新失败: ' + err.message, 'error');
5114
+ // 恢复原值
5115
+ loadModules();
5116
+ } finally {
5117
+ selectEl.disabled = false;
5118
+ }
5119
+ }
5120
+
5121
+ async function startModule(moduleName) {
5122
+ if (_isCoreModule(moduleName)) return; // 静默拦截
5123
+ if (_moduleActionPending.has(moduleName)) return; // 防抖
5124
+ _moduleActionPending.set(moduleName, 'start');
5125
+ _updateModuleButtons(moduleName);
5126
+
5127
+ try {
5128
+ if (!kernelClient || !kernelClient.connected) {
5129
+ throw new Error('WebSocket 未连接');
5130
+ }
5131
+ await kernelClient.call('launcher.start_module', { name: moduleName });
5132
+ // 延迟刷新,等待模块实际启动后再查询状态
5133
+ setTimeout(async () => {
5134
+ _moduleActionPending.delete(moduleName);
5135
+ _moduleRunStates = await _fetchModuleRunStates();
5136
+ _refreshModuleButtonsEverywhere(moduleName);
5137
+ // 启动成功提示
5138
+ if (_isModuleRunning(moduleName)) {
5139
+ showToast(`${moduleName} 启动成功`, 'success');
5140
+ } else {
5141
+ showToast(`${moduleName} 启动超时,请检查日志`, 'error');
5142
+ }
5143
+ }, 1500);
5144
+ } catch (err) {
5145
+ _moduleActionPending.delete(moduleName);
5146
+ _refreshModuleButtonsEverywhere(moduleName);
5147
+ showToast(`启动失败: ${err.message}`, 'error');
5148
+ }
5149
+ }
5150
+
5151
+ async function stopModule(moduleName) {
5152
+ if (_isCoreModule(moduleName)) return; // 静默拦截
5153
+ if (_moduleActionPending.has(moduleName)) return; // 防抖
5154
+ _moduleActionPending.set(moduleName, 'stop');
5155
+ _updateModuleButtons(moduleName);
5156
+
5157
+ try {
5158
+ if (!kernelClient || !kernelClient.connected) {
5159
+ throw new Error('WebSocket 未连接');
5160
+ }
5161
+ await kernelClient.call('launcher.stop_module', { name: moduleName, reason: 'user_request' });
5162
+ // 延迟刷新,等待模块实际停止后再查询状态
5163
+ setTimeout(async () => {
5164
+ _moduleActionPending.delete(moduleName);
5165
+ _moduleRunStates = await _fetchModuleRunStates();
5166
+ _refreshModuleButtonsEverywhere(moduleName);
5167
+ // 停止成功提示
5168
+ if (!_isModuleRunning(moduleName)) {
5169
+ showToast(`${moduleName} 停止成功`, 'success');
5170
+ } else {
5171
+ showToast(`${moduleName} 停止超时,请检查日志`, 'error');
5172
+ }
5173
+ }, 1500);
5174
+ } catch (err) {
5175
+ _moduleActionPending.delete(moduleName);
5176
+ _refreshModuleButtonsEverywhere(moduleName);
5177
+ showToast(`停止失败: ${err.message}`, 'error');
5178
+ }
5179
+ }
5180
+
5181
+ /**
5182
+ * 重启整个 Kite 系统(通过 watchdog)
5183
+ */
5184
+ async function restartKite() {
5185
+ // 禁用按钮,防止重复点击
5186
+ const btn = document.getElementById('btn-restart-kite');
5187
+ if (btn) {
5188
+ btn.disabled = true;
5189
+ btn.innerHTML = '<span>⏳</span><span>重启中...</span>';
5190
+ }
5191
+
5192
+ try {
5193
+ if (!kernelClient || !kernelClient.connected) {
5194
+ throw new Error('WebSocket 未连接');
5195
+ }
5196
+ const data = await kernelClient.call('launcher.restart_launcher', { reason: 'user_request' });
5197
+
5198
+ // 检查是否有错误
5199
+ if (data.error) {
5200
+ showToast(`重启失败: ${data.error}`, 'error');
5201
+ if (btn) {
5202
+ btn.disabled = false;
5203
+ btn.innerHTML = '<span>🔄</span><span>重启 Kite</span>';
5204
+ }
5205
+ return;
5206
+ }
5207
+
5208
+ // 成功 - 显示重启提示并轮询检测重启完成
5209
+ showToast('Kite 正在重启...', 'success');
5210
+
5211
+ // 轮询检测 Kite 是否重启完成(每 0.5s 检查一次,最多 30s)
5212
+ let attempts = 0;
5213
+ const maxAttempts = 60; // 30s
5214
+ const checkInterval = setInterval(async () => {
5215
+ attempts++;
5216
+ try {
5217
+ // 尝试调用 API,如果成功说明重启完成
5218
+ await API.get('/api/stats');
5219
+ clearInterval(checkInterval);
5220
+ showToast('Kite 重启完成', 'success');
5221
+ window.location.reload();
5222
+ } catch (err) {
5223
+ // 还在重启中,继续等待
5224
+ if (attempts >= maxAttempts) {
5225
+ clearInterval(checkInterval);
5226
+ showToast('重启超时,请手动刷新页面', 'warning');
5227
+ if (btn) {
5228
+ btn.disabled = false;
5229
+ btn.innerHTML = '<span>🔄</span><span>重启 Kite</span>';
5230
+ }
5231
+ }
5232
+ }
5233
+ }, 500);
5234
+
5235
+ } catch (err) {
5236
+ showToast(`重启失败: ${err.message}`, 'error');
5237
+ if (btn) {
5238
+ btn.disabled = false;
5239
+ btn.innerHTML = '<span>🔄</span><span>重启 Kite</span>';
5240
+ }
5241
+ }
5242
+ }
5243
+
5244
+ /**
5245
+ * 统一更新指定模块在列表行 + 详情页面的按钮启用/禁用状态与运行状态文本。
5246
+ */
5247
+ function _updateModuleButtons(moduleName) {
5248
+ const running = _isModuleRunning(moduleName);
5249
+ const pending = _moduleActionPending.has(moduleName);
5250
+ const pendingAction = _moduleActionPending.get(moduleName); // 'start' | 'stop'
5251
+ const isCore = _isCoreModule(moduleName);
5252
+ const startDis = pending || running;
5253
+ const stopDis = isCore || pending || !running;
5254
+ const startLabel = pendingAction === 'start' ? '启动中' : '启动';
5255
+ const stopLabel = pendingAction === 'stop' ? '停止中' : '停止';
5256
+
5257
+ // ── 列表行 ──
5258
+ const row = document.querySelector(`tr[data-module="${moduleName}"]`);
5259
+ if (row) {
5260
+ const btns = row.querySelectorAll('button');
5261
+ // 约定:第一个按钮=启动,第二个=停止
5262
+ if (btns[0]) { btns[0].disabled = startDis; btns[0].textContent = startLabel; }
5263
+ if (btns[1]) { btns[1].disabled = stopDis; btns[1].textContent = stopLabel; }
5264
+
5265
+ // 运行状态列(第 7 列,index 6)
5266
+ const statusTd = row.children[6];
5267
+ if (statusTd) {
5268
+ statusTd.innerHTML = pending
5269
+ ? `<span style="color:var(--warning);">${pendingAction === 'start' ? '启动中…' : '停止中…'}</span>`
5270
+ : running
5271
+ ? '<span style="color:var(--success);">运行中</span>'
5272
+ : '<span style="color:var(--gray-400);">已停止</span>';
5273
+ }
5274
+ }
5275
+
5276
+ // ── 详情页面 ──
5277
+ if (_currentModuleName === moduleName) {
5278
+ const btnStart = document.getElementById('btn-detail-start');
5279
+ const btnStop = document.getElementById('btn-detail-stop');
5280
+ if (btnStart) { btnStart.disabled = startDis; btnStart.textContent = startLabel; }
5281
+ if (btnStop) { btnStop.disabled = stopDis; btnStop.textContent = stopLabel; }
5282
+ _updateModuleDetailStatus(moduleName);
5283
+ }
5284
+ }
5285
+
5286
+ function _updateModuleDetailStatus(moduleName) {
5287
+ const el = document.getElementById('module-detail-run-status');
5288
+ if (!el) return;
5289
+ const running = _isModuleRunning(moduleName);
5290
+ const pending = _moduleActionPending.has(moduleName);
5291
+ const pendingAction = _moduleActionPending.get(moduleName);
5292
+ const rs = _moduleRunStates[moduleName];
5293
+ if (pending) {
5294
+ el.innerHTML = `<span style="color:var(--warning);">${pendingAction === 'start' ? '启动中…' : '停止中…'}</span>`;
5295
+ } else if (running) {
5296
+ const pid = rs && rs.pid ? ` (PID: ${rs.pid})` : '';
5297
+ el.innerHTML = `<span style="color:var(--success);">运行中${escapeHtml(pid)}</span>`;
5298
+ } else {
5299
+ el.innerHTML = '<span style="color:var(--gray-400);">已停止</span>';
5300
+ }
5301
+ }
5302
+
5303
+ function _refreshModuleButtonsEverywhere(moduleName) {
5304
+ _updateModuleButtons(moduleName);
5305
+ }
5306
+
5307
+ async function startModuleFromDetail() {
5308
+ if (_currentModuleName) {
5309
+ await startModule(_currentModuleName);
5310
+ }
5311
+ }
5312
+
5313
+ async function stopModuleFromDetail() {
5314
+ if (_currentModuleName) {
5315
+ await stopModule(_currentModuleName);
5316
+ }
5317
+ }
5318
+
5319
+ async function resetModuleDefaults() {
5320
+ if (!_currentModuleName) return;
5321
+
5322
+ // 获取当前值
5323
+ const currentState = _getVal('mod-meta-state');
5324
+ const currentIp = _getVal('mod-meta-ip');
5325
+ const currentPort = _getVal('mod-meta-port');
5326
+ const currentMonitor = document.getElementById('mod-meta-monitor')?.value;
5327
+
5328
+ // 计算默认值
5329
+ const defaultIp = _currentModuleName === 'web' ? '0.0.0.0' : '127.0.0.1';
5330
+ const defaultState = 'enabled';
5331
+ const defaultMonitor = 'true';
5332
+ const defaultPort = '';
5333
+
5334
+ // 构建对比表格
5335
+ const comparison = [
5336
+ { field: 'state (默认状态)', current: currentState, default: defaultState },
5337
+ { field: 'advertise_ip (监听地址)', current: currentIp, default: defaultIp },
5338
+ { field: 'monitor (监控)', current: currentMonitor, default: defaultMonitor },
5339
+ { field: 'preferred_port (首选端口)', current: currentPort || '(空)', default: '(空)' },
5340
+ ];
5341
+
5342
+ // 填充对比表格
5343
+ const tbody = document.getElementById('reset-defaults-comparison');
5344
+ tbody.innerHTML = comparison.map(item => {
5345
+ const changed = item.current !== item.default;
5346
+ const rowStyle = changed ? 'background:var(--warning-light);' : '';
5347
+ return `
5348
+ <tr style="${rowStyle}">
5349
+ <td style="padding:8px;border-bottom:1px solid var(--gray-200);">${escapeHtml(item.field)}</td>
5350
+ <td style="padding:8px;border-bottom:1px solid var(--gray-200);font-family:monospace;font-size:13px;">${escapeHtml(item.current)}</td>
5351
+ <td style="padding:8px;border-bottom:1px solid var(--gray-200);font-family:monospace;font-size:13px;color:var(--primary);">${escapeHtml(item.default)}</td>
5352
+ </tr>
5353
+ `;
5354
+ }).join('');
5355
+
5356
+ // 显示对话框
5357
+ const modal = document.getElementById('reset-defaults-modal');
5358
+ modal.classList.remove('hidden');
5359
+
5360
+ // 等待用户确认或取消
5361
+ return new Promise((resolve) => {
5362
+ const confirm = document.getElementById('reset-defaults-confirm');
5363
+ const cancel = document.getElementById('reset-defaults-cancel');
5364
+ const close = document.getElementById('reset-defaults-close');
5365
+
5366
+ const cleanup = () => {
5367
+ modal.classList.add('hidden');
5368
+ confirm.removeEventListener('click', onConfirm);
5369
+ cancel.removeEventListener('click', onCancel);
5370
+ close.removeEventListener('click', onCancel);
5371
+ };
5372
+
5373
+ const onConfirm = async () => {
5374
+ cleanup();
5375
+ try {
5376
+ console.log('[modules] Resetting defaults for:', _currentModuleName);
5377
+
5378
+ // 使用 RPC 恢复默认值
5379
+ const rpc = await lookupModuleRpc(_currentModuleName, "tools.rpc.module.config.reset");
5380
+ if (!rpc) {
5381
+ throw new Error(`模块 ${_currentModuleName} 不支持恢复默认值`);
5382
+ }
5383
+ const params = rpc.needsModuleName
5384
+ ? { module_name: _currentModuleName, fields: ['state', 'advertise_ip', 'monitor', 'preferred_port'] }
5385
+ : { fields: ['state', 'advertise_ip', 'monitor', 'preferred_port'] };
5386
+ const result = await kernelClient.call(`${rpc.module}.${rpc.method}`, params);
5387
+ console.log('[modules] Reset defaults result:', result);
5388
+
5389
+ showToast('已恢复默认值', 'success');
5390
+
5391
+ // 更新 UI
5392
+ _setVal('mod-meta-state', defaultState);
5393
+ _setVal('mod-meta-ip', defaultIp);
5394
+ _setVal('mod-meta-port', '');
5395
+ const monitorEl = document.getElementById('mod-meta-monitor');
5396
+ if (monitorEl) monitorEl.value = 'true';
5397
+
5398
+ resolve(true);
5399
+ } catch (err) {
5400
+ console.error('[modules] Reset defaults failed:', err);
5401
+ const errorMsg = err.message || (typeof err === 'string' ? err : JSON.stringify(err));
5402
+ showToast('恢复默认值失败: ' + errorMsg, 'error');
5403
+ resolve(false);
5404
+ }
5405
+ };
5406
+
5407
+ const onCancel = () => {
5408
+ cleanup();
5409
+ resolve(false);
5410
+ };
5411
+
5412
+ confirm.addEventListener('click', onConfirm);
5413
+ cancel.addEventListener('click', onCancel);
5414
+ close.addEventListener('click', onCancel);
5415
+ });
5416
+ }
5417
+
4257
5418
  async function openModuleDetail(name) {
4258
5419
  _currentModuleName = name;
4259
5420
 
4260
5421
  // Switch view
4261
- document.getElementById('modules-grid')?.classList.add('hidden');
5422
+ document.getElementById('modules-list-header')?.classList.add('hidden');
5423
+ document.getElementById('modules-table')?.closest('.panel')?.classList.add('hidden');
4262
5424
  document.getElementById('module-detail')?.classList.remove('hidden');
5425
+ document.getElementById('token-management-section')?.classList.add('hidden');
5426
+ document.getElementById('statistics-panel')?.classList.add('hidden');
5427
+ document.getElementById('registry-test-section')?.classList.add('hidden');
5428
+ document.getElementById('registry-test-output')?.classList.add('hidden');
5429
+
5430
+ // 立即更新按钮状态(使用当前缓存的运行状态)
5431
+ _updateModuleButtons(name);
4263
5432
 
4264
5433
  try {
4265
- const mod = await API.get(`/api/modules/${encodeURIComponent(name)}`);
5434
+ // Check if kernelClient is connected
5435
+ if (!kernelClient || !kernelClient.connected) {
5436
+ throw new Error('未连接到 Kernel,请检查连接状态');
5437
+ }
5438
+
5439
+ // 使用 RPC 获取模块配置
5440
+ const rpc = await lookupModuleRpc(name, "tools.rpc.module.config.get");
5441
+ if (!rpc) {
5442
+ throw new Error(`模块 ${name} 不支持配置查询`);
5443
+ }
5444
+ console.log(`[modules] Using RPC: ${rpc.module}.${rpc.method}, needsModuleName=${rpc.needsModuleName}`);
5445
+
5446
+ const params = rpc.needsModuleName ? { module_name: name } : {};
5447
+ const mod = await kernelClient.call(`${rpc.module}.${rpc.method}`, params);
5448
+ console.log(`[modules] RPC result:`, mod);
4266
5449
 
4267
5450
  // Header
4268
5451
  document.getElementById('module-detail-name').textContent = mod.display_name || mod.name;
@@ -4272,22 +5455,41 @@ async function openModuleDetail(name) {
4272
5455
  badge.className = `module-type-badge type-${mod.type || 'unknown'}`;
4273
5456
  }
4274
5457
 
4275
- // Read-only fields
5458
+ // 【区块1:基本信息与元数据】
5459
+ _setVal('mod-source-path', mod.source_path || '');
5460
+
5461
+ // 基础字段(现在可编辑)
4276
5462
  _setVal('mod-meta-name', mod.name || '');
4277
5463
  _setVal('mod-meta-type', mod.type || '');
4278
5464
  _setVal('mod-meta-runtime', mod.runtime || '');
4279
5465
  _setVal('mod-meta-entry', mod.entry || '');
4280
5466
 
4281
- // Editable fields
5467
+ // 其他字段
4282
5468
  _setVal('mod-meta-display-name', mod.display_name || '');
4283
5469
  _setVal('mod-meta-version', mod.version || '');
4284
5470
  _setVal('mod-meta-state', mod.state || 'enabled');
4285
5471
  _setVal('mod-meta-port', mod.preferred_port != null ? mod.preferred_port : '');
4286
- _setVal('mod-meta-ip', mod.advertise_ip || '');
5472
+
5473
+ // 监听地址:根据模块设置默认值
5474
+ let defaultIp = '127.0.0.1'; // 默认仅本地
5475
+ if (mod.name === 'web') {
5476
+ defaultIp = '0.0.0.0'; // web 模块默认允许远程
5477
+ }
5478
+ // TODO: 如果 kernel 允许远程模块,也设置为 0.0.0.0
5479
+ _setVal('mod-meta-ip', mod.advertise_ip || defaultIp);
5480
+
5481
+ // 监控:默认为 true
4287
5482
  const monitorEl = document.getElementById('mod-meta-monitor');
4288
- if (monitorEl) monitorEl.value = mod.monitor != null ? String(mod.monitor) : '';
5483
+ if (monitorEl) {
5484
+ const monitorValue = mod.monitor != null ? String(mod.monitor) : 'true';
5485
+ monitorEl.value = monitorValue;
5486
+ }
5487
+
5488
+ // 运行状态 + 按钮状态
5489
+ _moduleRunStates = await _fetchModuleRunStates();
5490
+ _updateModuleDetailStatus(name);
4289
5491
 
4290
- // Config section
5492
+ // 【区块2:模块配置文件】
4291
5493
  const configSection = document.getElementById('module-config-section');
4292
5494
  const configTree = document.getElementById('module-config-tree');
4293
5495
  if (mod.has_config && mod.config) {
@@ -4299,8 +5501,54 @@ async function openModuleDetail(name) {
4299
5501
  } else {
4300
5502
  configSection?.classList.add('hidden');
4301
5503
  }
5504
+
5505
+ // 绑定自动保存事件监听器(每次加载详情时重新绑定)
5506
+ document.querySelectorAll('#module-detail [data-field]').forEach(el => {
5507
+ // 移除旧的监听器(如果有)
5508
+ el.removeEventListener('input', _debouncedSaveModule);
5509
+ el.removeEventListener('change', _debouncedSaveModule);
5510
+ // 添加新的监听器
5511
+ const event = (el.tagName === 'SELECT') ? 'change' : 'input';
5512
+ el.addEventListener(event, _debouncedSaveModule);
5513
+ });
5514
+
5515
+ // 配置树的输入框也需要绑定
5516
+ if (configTree) {
5517
+ configTree.querySelectorAll('input, select, textarea').forEach(el => {
5518
+ el.removeEventListener('input', _debouncedSaveModule);
5519
+ el.removeEventListener('change', _debouncedSaveModule);
5520
+ const event = (el.tagName === 'SELECT') ? 'change' : 'input';
5521
+ el.addEventListener(event, _debouncedSaveModule);
5522
+ });
5523
+ }
5524
+
5525
+ // 同步详情页面启动/停止按钮状态
5526
+ _updateModuleButtons(name);
4302
5527
  } catch (err) {
4303
- showToast('加载模块详情失败: ' + err.message, 'error');
5528
+ console.error('[modules] Failed to load module detail:', err);
5529
+
5530
+ // Generate user-friendly error message
5531
+ let errorMsg = err.message;
5532
+ if (!kernelClient || !kernelClient.connected) {
5533
+ errorMsg = '未连接到 Kernel,请检查连接状态';
5534
+ } else if (err.message.includes('not ready')) {
5535
+ errorMsg = `模块 ${name} 尚未就绪,请稍后重试`;
5536
+ } else if (err.message.includes('不支持配置查询')) {
5537
+ errorMsg = `模块 ${name} 不支持配置查询`;
5538
+ } else if (err.message.includes('timeout')) {
5539
+ errorMsg = '请求超时,请检查网络连接';
5540
+ }
5541
+
5542
+ showToast('加载模块详情失败: ' + errorMsg, 'error');
5543
+
5544
+ // 恢复列表视图
5545
+ document.getElementById('modules-list-header')?.classList.remove('hidden');
5546
+ document.getElementById('modules-table')?.closest('.panel')?.classList.remove('hidden');
5547
+ document.getElementById('module-detail')?.classList.add('hidden');
5548
+ document.getElementById('token-management-section')?.classList.remove('hidden');
5549
+ document.getElementById('statistics-panel')?.classList.remove('hidden');
5550
+ document.getElementById('registry-test-section')?.classList.remove('hidden');
5551
+ document.getElementById('registry-test-output')?.classList.remove('hidden');
4304
5552
  }
4305
5553
  }
4306
5554
 
@@ -4419,34 +5667,42 @@ function _debouncedSaveModule() {
4419
5667
  _moduleSaveTimer = setTimeout(async () => {
4420
5668
  _showModuleSaveStatus('saving');
4421
5669
  try {
4422
- // Collect metadata
5670
+ // Collect metadata from all [data-field] inputs in #module-detail
4423
5671
  const meta = {};
4424
- document.querySelectorAll('#module-meta-form [data-field]').forEach(el => {
5672
+ document.querySelectorAll('#module-detail [data-field]').forEach(el => {
4425
5673
  const field = el.dataset.field;
4426
5674
  let val;
4427
5675
  if (field === 'preferred_port') {
4428
5676
  val = el.value === '' ? null : parseInt(el.value);
4429
5677
  } else if (field === 'monitor') {
4430
- val = el.value === '' ? null : el.value === 'true';
5678
+ val = el.value === 'true';
4431
5679
  } else {
4432
5680
  val = el.value || null;
4433
5681
  }
4434
5682
  if (val !== null) meta[field] = val;
4435
5683
  });
4436
5684
 
4437
- // Save metadata
5685
+ // Save metadata and config
5686
+ const rpc = await lookupModuleRpc(_currentModuleName, "tools.rpc.module.config.update");
5687
+ if (!rpc) {
5688
+ throw new Error(`模块 ${_currentModuleName} 不支持配置更新`);
5689
+ }
5690
+
5691
+ const baseParams = rpc.needsModuleName ? { module_name: _currentModuleName } : {};
5692
+
4438
5693
  const metaPromise = Object.keys(meta).length > 0
4439
- ? API.put(`/api/modules/${encodeURIComponent(_currentModuleName)}/metadata`, meta)
5694
+ ? kernelClient.call(`${rpc.module}.${rpc.method}`, { ...baseParams, metadata: meta })
4440
5695
  : Promise.resolve();
4441
5696
 
4442
5697
  // Save config if visible
4443
5698
  const configSection = document.getElementById('module-config-section');
4444
5699
  const configObj = _buildModuleConfigObject();
4445
5700
  const configPromise = configSection && !configSection.classList.contains('hidden') && configObj
4446
- ? API.put(`/api/modules/${encodeURIComponent(_currentModuleName)}/config`, configObj)
5701
+ ? kernelClient.call(`${rpc.module}.${rpc.method}`, { ...baseParams, config: configObj })
4447
5702
  : Promise.resolve();
4448
5703
 
4449
5704
  await Promise.all([metaPromise, configPromise]);
5705
+
4450
5706
  _showModuleSaveStatus('saved');
4451
5707
  } catch (err) {
4452
5708
  _showModuleSaveStatus('error');
@@ -4944,15 +6200,46 @@ document.addEventListener('DOMContentLoaded', () => {
4944
6200
  const btnModuleBack = document.getElementById('btn-module-back');
4945
6201
  if (btnModuleBack) btnModuleBack.addEventListener('click', loadModules);
4946
6202
 
4947
- // Module metadata form auto-save
4948
- document.querySelectorAll('#module-meta-form [data-field]').forEach(el => {
4949
- const event = (el.tagName === 'SELECT') ? 'change' : 'input';
4950
- el.addEventListener(event, _debouncedSaveModule);
6203
+ // Restart Kite button
6204
+ const btnRestartKite = document.getElementById('btn-restart-kite');
6205
+ if (btnRestartKite) btnRestartKite.addEventListener('click', restartKite);
6206
+
6207
+ // Console toggle button
6208
+ const btnToggleConsole = document.getElementById('btn-toggle-console');
6209
+ if (btnToggleConsole) btnToggleConsole.addEventListener('click', toggleConsole);
6210
+
6211
+ // Clear console button
6212
+ const btnClearConsole = document.getElementById('btn-clear-console');
6213
+ if (btnClearConsole) btnClearConsole.addEventListener('click', clearConsole);
6214
+
6215
+ // Initialize stats tooltip
6216
+ initStatsTooltip();
6217
+
6218
+ // Registry test buttons
6219
+ const btnTestRegistry = document.getElementById('btn-test-registry');
6220
+ if (btnTestRegistry) btnTestRegistry.addEventListener('click', runRegistryTests);
6221
+ const btnClearTestOutput = document.getElementById('btn-clear-test-output');
6222
+ if (btnClearTestOutput) btnClearTestOutput.addEventListener('click', () => {
6223
+ document.getElementById('test-output').textContent = '';
4951
6224
  });
4952
6225
 
6226
+ // Module metadata form auto-save is handled dynamically in openModuleDetail()
6227
+
6228
+ // Connect to management WebSocket for real-time module status updates
6229
+ connectManagementWebSocket();
6230
+
4953
6231
  // Navigate to last active page (or dashboard)
4954
6232
  navigate(localStorage.getItem('activePage') || 'dashboard');
4955
6233
 
4956
6234
  // Initial status bar update
4957
6235
  updateStatusBar();
4958
6236
  });
6237
+
6238
+ // ============================================================
6239
+ // Registry Tests
6240
+ // ============================================================
6241
+
6242
+ async function runRegistryTests() {
6243
+ // 调用 registry-tests.js 中的全面测试套件
6244
+ await runAllTests();
6245
+ }