@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
@@ -0,0 +1,1949 @@
1
+ // Evol App - Main Application Logic
2
+
3
+ class EvolApp {
4
+ constructor() {
5
+ // 优先从 Cookie 恢复 kiteToken(防止 Ctrl+F5 清除 localStorage)
6
+ this.kiteToken = this._getKiteToken();
7
+ this.userInfo = null;
8
+ this.ws = null;
9
+ this.wsConnected = false;
10
+ this.currentPage = 'account';
11
+ this.deviceInfo = this._getDeviceInfo();
12
+ this.statsRefreshTimer = null;
13
+ this.moduleActionPending = new Map(); // 模块操作防抖
14
+ this.currentModuleName = null; // 当前查看的模块名
15
+
16
+ // 事件日志相关
17
+ this.eventLogs = []; // 存储所有事件日志 {timestamp, module, event, data, raw}
18
+ this.consoleExpanded = false; // 控制台是否展开
19
+ this.eventSubscribed = false; // 是否已订阅全部事件
20
+ this.knownModules = new Set(); // 已知的模块列表
21
+ this.knownEvents = new Set(); // 已知的事件类型
22
+
23
+ // 账户信息缓存(5秒有效期,持久化到 localStorage)
24
+ this.CACHE_DURATION = 5000; // 5秒
25
+ this._loadUserInfoCache();
26
+ }
27
+
28
+ _loadUserInfoCache() {
29
+ try {
30
+ const raw = localStorage.getItem('evolUserInfoCache');
31
+ if (raw) {
32
+ const cached = JSON.parse(raw);
33
+ this.userInfoCache = cached.data;
34
+ this.userInfoCacheTime = cached.time;
35
+ } else {
36
+ this.userInfoCache = null;
37
+ this.userInfoCacheTime = 0;
38
+ }
39
+ } catch {
40
+ this.userInfoCache = null;
41
+ this.userInfoCacheTime = 0;
42
+ }
43
+ }
44
+
45
+ _saveUserInfoCache(data) {
46
+ this.userInfoCache = data;
47
+ this.userInfoCacheTime = Date.now();
48
+ localStorage.setItem('evolUserInfoCache', JSON.stringify({
49
+ data: data,
50
+ time: this.userInfoCacheTime
51
+ }));
52
+ }
53
+
54
+ _getKiteToken() {
55
+ // 1. 先从 localStorage 读取
56
+ let token = localStorage.getItem('kiteToken');
57
+ if (token) return token;
58
+
59
+ // 2. 如果 localStorage 没有,尝试从 Cookie 恢复
60
+ token = this._getCookie('kiteToken');
61
+ if (token) {
62
+ // 恢复到 localStorage
63
+ localStorage.setItem('kiteToken', token);
64
+ console.log('Restored kiteToken from cookie');
65
+ return token;
66
+ }
67
+
68
+ return null;
69
+ }
70
+
71
+ _getCookie(name) {
72
+ const value = `; ${document.cookie}`;
73
+ const parts = value.split(`; ${name}=`);
74
+ if (parts.length === 2) return parts.pop().split(';').shift();
75
+ return null;
76
+ }
77
+
78
+ _setCookie(name, value, days = 30) {
79
+ const expires = new Date(Date.now() + days * 864e5).toUTCString();
80
+ document.cookie = `${name}=${value}; expires=${expires}; path=/; SameSite=Strict`;
81
+ }
82
+
83
+ _saveKiteToken(token) {
84
+ // 同时保存到 localStorage 和 Cookie
85
+ localStorage.setItem('kiteToken', token);
86
+ this._setCookie('kiteToken', token, 30);
87
+ }
88
+
89
+ _clearKiteToken() {
90
+ // 同时清除 localStorage 和 Cookie
91
+ localStorage.removeItem('kiteToken');
92
+ this._setCookie('kiteToken', '', -1);
93
+ }
94
+
95
+ _getDeviceInfo() {
96
+ // 生成或获取持久化的设备 ID
97
+ let deviceId = localStorage.getItem('deviceId');
98
+ if (!deviceId) {
99
+ deviceId = 'device_' + this._randomString(16) + '_' + Date.now();
100
+ localStorage.setItem('deviceId', deviceId);
101
+ }
102
+
103
+ // 检测设备类型和名称
104
+ const ua = navigator.userAgent;
105
+ let deviceType = 'Desktop';
106
+ let osName = 'Unknown OS';
107
+
108
+ if (/Android/i.test(ua)) {
109
+ deviceType = 'Mobile';
110
+ osName = 'Android';
111
+ } else if (/iPhone|iPad|iPod/i.test(ua)) {
112
+ deviceType = 'Mobile';
113
+ osName = 'iOS';
114
+ } else if (/Windows/i.test(ua)) {
115
+ osName = 'Windows';
116
+ } else if (/Mac/i.test(ua)) {
117
+ osName = 'macOS';
118
+ } else if (/Linux/i.test(ua)) {
119
+ osName = 'Linux';
120
+ }
121
+
122
+ let browserName = 'Unknown Browser';
123
+ if (/Chrome/i.test(ua) && !/Edge/i.test(ua)) {
124
+ browserName = 'Chrome';
125
+ } else if (/Firefox/i.test(ua)) {
126
+ browserName = 'Firefox';
127
+ } else if (/Safari/i.test(ua) && !/Chrome/i.test(ua)) {
128
+ browserName = 'Safari';
129
+ } else if (/Edge/i.test(ua)) {
130
+ browserName = 'Edge';
131
+ }
132
+
133
+ return {
134
+ deviceId: deviceId,
135
+ deviceName: `${browserName} on ${osName}`,
136
+ deviceType: deviceType,
137
+ browser: browserName,
138
+ os: osName,
139
+ userAgent: ua
140
+ };
141
+ }
142
+
143
+ _randomString(length) {
144
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
145
+ let result = '';
146
+ for (let i = 0; i < length; i++) {
147
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
148
+ }
149
+ return result;
150
+ }
151
+
152
+ async init() {
153
+ if (this.kiteToken) {
154
+ // 有 kiteToken,立即显示主界面和占位信息
155
+ this.showMainApp();
156
+ this.showUserInfoPlaceholder();
157
+
158
+ // 异步加载 Evol 账户信息(不阻塞,失败不影响主界面)
159
+ this.loadUserInfo().catch(err => {
160
+ console.warn('Failed to load Evol user info:', err);
161
+ // evolToken 失效,显示提示但不影响 Kite 功能
162
+ this.showEvolTokenWarning();
163
+ });
164
+ } else {
165
+ // 没有 kiteToken,显示登录页
166
+ this.showLoginPage();
167
+ }
168
+ }
169
+
170
+ showUserInfoPlaceholder() {
171
+ // 立即显示占位信息,避免延迟
172
+ const loginBtn = document.getElementById('login-btn');
173
+ const userInfoArea = document.getElementById('user-info-area');
174
+
175
+ if (loginBtn) loginBtn.classList.add('hidden');
176
+ if (userInfoArea) {
177
+ userInfoArea.classList.remove('hidden');
178
+ userInfoArea.style.display = 'flex';
179
+
180
+ // 显示加载中占位
181
+ const userNameEl = userInfoArea.querySelector('.user-name');
182
+ const creditsEl = userInfoArea.querySelector('.user-credits');
183
+ if (userNameEl) userNameEl.textContent = '加载中...';
184
+ if (creditsEl) creditsEl.textContent = '';
185
+ }
186
+ }
187
+
188
+ showLoginPage() {
189
+ // 移除 has-token 类,触发 CSS 切换
190
+ document.documentElement.classList.remove('has-token');
191
+ document.getElementById('login-page').classList.remove('hidden');
192
+ document.getElementById('main-app').classList.add('hidden');
193
+ }
194
+
195
+ showMainApp() {
196
+ // 添加 has-token 类,触发 CSS 切换
197
+ document.documentElement.classList.add('has-token');
198
+ document.getElementById('login-page').classList.add('hidden');
199
+ document.getElementById('main-app').classList.remove('hidden');
200
+ this.initMainApp();
201
+ }
202
+
203
+ async loadUserInfo() {
204
+ // 检查缓存是否有效(5秒内)
205
+ const now = Date.now();
206
+ const cacheAge = now - this.userInfoCacheTime;
207
+
208
+ if (this.userInfoCache && cacheAge < this.CACHE_DURATION) {
209
+ // 缓存有效,直接使用
210
+ this.userInfo = this.userInfoCache;
211
+ this.updateUserInfoDisplay();
212
+ this.loadAccountInfo();
213
+ return;
214
+ }
215
+
216
+ // 如果有缓存数据,先渲染缓存(即使过期)
217
+ if (this.userInfoCache) {
218
+ this.userInfo = this.userInfoCache;
219
+ this.updateUserInfoDisplay();
220
+ this.loadAccountInfo();
221
+ }
222
+
223
+ // 然后异步更新数据
224
+ try {
225
+ const res = await fetch('/api/get_user_info', {
226
+ method: 'POST',
227
+ headers: { 'Content-Type': 'application/json' },
228
+ body: JSON.stringify({ kiteToken: this.kiteToken })
229
+ });
230
+ const data = await res.json();
231
+
232
+ if (data.success) {
233
+ this.userInfo = data.data;
234
+ this._saveUserInfoCache(data.data);
235
+ this.updateUserInfoDisplay();
236
+ // 更新账户信息页面
237
+ this.loadAccountInfo();
238
+ } else if (data.code === 'INVALID_KITE_TOKEN') {
239
+ // kiteToken 失效,需要重新配对
240
+ console.warn('kiteToken invalid, need re-pairing');
241
+ this._clearKiteToken();
242
+ this.kiteToken = null;
243
+ this.showLoginPage();
244
+ throw new Error('KITE_TOKEN_INVALID');
245
+ } else {
246
+ // evolToken 失效或其他错误,不影响 Kite 功能
247
+ throw new Error(data.message || 'EVOL_TOKEN_INVALID');
248
+ }
249
+ } catch (err) {
250
+ console.error('Load user info failed:', err);
251
+ throw err;
252
+ }
253
+ }
254
+
255
+ updateUserInfoDisplay() {
256
+ // 更新右上角用户信息显示
257
+ if (this.userInfo) {
258
+ const userInfo = this.userInfo.userInfo || this.userInfo;
259
+ const accountInfo = this.userInfo.accountInfo || {};
260
+
261
+ // 显示用户信息区域
262
+ const loginBtn = document.getElementById('login-btn');
263
+ const userInfoArea = document.getElementById('user-info-area');
264
+
265
+ if (loginBtn) loginBtn.classList.add('hidden');
266
+ if (userInfoArea) {
267
+ userInfoArea.classList.remove('hidden');
268
+ userInfoArea.style.display = 'flex';
269
+
270
+ // 更新用户名
271
+ const userNameEl = userInfoArea.querySelector('.user-name');
272
+ if (userNameEl) {
273
+ userNameEl.textContent = userInfo.nickName || userInfo.userName || '未知用户';
274
+ }
275
+
276
+ // 更新积分
277
+ const creditsEl = userInfoArea.querySelector('.user-credits');
278
+ if (creditsEl) {
279
+ creditsEl.textContent = `积分: ${accountInfo.credits || 0}`;
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ showEvolTokenWarning() {
286
+ // 右上角显示 Evol 未登录状态
287
+ const loginBtn = document.getElementById('login-btn');
288
+ const userInfoArea = document.getElementById('user-info-area');
289
+
290
+ if (loginBtn) {
291
+ loginBtn.classList.remove('hidden');
292
+ loginBtn.style.display = 'inline';
293
+ loginBtn.textContent = '⚠️ 未登录 Evol';
294
+ loginBtn.style.color = '#ff9800';
295
+ loginBtn.style.fontWeight = '500';
296
+ }
297
+ if (userInfoArea) {
298
+ userInfoArea.classList.add('hidden');
299
+ userInfoArea.style.display = 'none';
300
+ }
301
+
302
+ // 绑定右上角登录按钮点击事件
303
+ if (loginBtn) {
304
+ loginBtn.onclick = (e) => {
305
+ e.preventDefault();
306
+ this.logout();
307
+ };
308
+ }
309
+ }
310
+
311
+ async sendSMS(phone) {
312
+ const res = await fetch('/api/send_sms', {
313
+ method: 'POST',
314
+ headers: { 'Content-Type': 'application/json' },
315
+ body: JSON.stringify({ phone })
316
+ });
317
+ return await res.json();
318
+ }
319
+
320
+ async login(phone, code) {
321
+ const res = await fetch('/api/verify_sms', {
322
+ method: 'POST',
323
+ headers: { 'Content-Type': 'application/json' },
324
+ body: JSON.stringify({ phone, code, deviceInfo: this.deviceInfo })
325
+ });
326
+ const data = await res.json();
327
+
328
+ if (data.success) {
329
+ this.kiteToken = data.kiteToken;
330
+ this.userInfo = data.data;
331
+ this._saveKiteToken(this.kiteToken);
332
+ this.showMainApp();
333
+ // 登录成功后立即更新用户信息显示
334
+ this.updateUserInfoDisplay();
335
+ // 更新账户信息页面
336
+ this.updateAccountPage();
337
+ }
338
+
339
+ return data;
340
+ }
341
+
342
+ async logout() {
343
+ try {
344
+ await fetch('/api/logout', {
345
+ method: 'POST',
346
+ headers: { 'Content-Type': 'application/json' },
347
+ body: JSON.stringify({ kiteToken: this.kiteToken })
348
+ });
349
+ } catch (err) {}
350
+
351
+ this._clearKiteToken();
352
+ location.reload();
353
+ }
354
+
355
+ initMainApp() {
356
+ // 初始化 Kernel 客户端和导航
357
+ this.wsRetryCount = 0;
358
+ this.wsMaxRetries = 6;
359
+ this.wsRetryDelay = 1000; // 初始延迟 1 秒
360
+ this.wsGaveUp = false;
361
+ this.initKernelClient();
362
+ this.loadAccountInfo();
363
+ this.setupNavigation();
364
+
365
+ // 监听页面可见性变化,页面重新可见时如果已放弃则重新连接
366
+ document.addEventListener('visibilitychange', () => {
367
+ if (!document.hidden && this.wsGaveUp) {
368
+ console.log('[Evol] Page visible again, retrying connection');
369
+ this.wsGaveUp = false;
370
+ this.wsRetryCount = 0;
371
+ this.wsRetryDelay = 1000;
372
+ this.initKernelClient();
373
+ }
374
+ });
375
+ }
376
+
377
+ updateWsIndicator(status, text, color) {
378
+ const indicator = document.getElementById('ws-indicator');
379
+ if (indicator) {
380
+ indicator.innerHTML = `${status} ${text}`;
381
+ indicator.style.color = color;
382
+ }
383
+ }
384
+
385
+ initKernelClient() {
386
+ // 如果已放弃,不再重试(除非用户主动触发)
387
+ if (this.wsGaveUp) {
388
+ return;
389
+ }
390
+
391
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
392
+ const url = `${proto}//${location.host}/ws/management`;
393
+
394
+ console.log('[Evol] Connecting to', url, `(attempt ${this.wsRetryCount + 1}/${this.wsMaxRetries})`);
395
+
396
+ // 显示连接中状态
397
+ if (this.wsRetryCount === 0) {
398
+ this.updateWsIndicator('◐', '连接中...', '#ff9800');
399
+ } else {
400
+ this.updateWsIndicator('◐', `重连中... (${this.wsRetryCount}/${this.wsMaxRetries})`, '#ff9800');
401
+ }
402
+
403
+ this.ws = new WebSocket(url);
404
+
405
+ this.ws.onopen = () => {
406
+ console.log('[Evol] WebSocket connected');
407
+ this.wsConnected = true;
408
+ this.wsRetryCount = 0;
409
+ this.wsRetryDelay = 1000;
410
+ this.updateWsIndicator('●', 'Kite 已连接', '#27ae60');
411
+ };
412
+
413
+ this.ws.onerror = (error) => {
414
+ console.error('[Evol] WebSocket error:', error);
415
+ };
416
+
417
+ this.ws.onclose = () => {
418
+ console.log('[Evol] WebSocket closed');
419
+ this.wsConnected = false;
420
+
421
+ // 检查是否已达到最大重试次数
422
+ if (this.wsRetryCount >= this.wsMaxRetries) {
423
+ console.log('[Evol] Max retries reached, giving up');
424
+ this.wsGaveUp = true;
425
+ this.updateWsIndicator('✕', '已断开', '#e74c3c');
426
+ return;
427
+ }
428
+
429
+ // 指数退避重试
430
+ this.wsRetryCount++;
431
+ const delay = Math.min(this.wsRetryDelay * Math.pow(2, this.wsRetryCount - 1), 30000);
432
+ console.log(`[Evol] Retrying in ${delay}ms...`);
433
+
434
+ setTimeout(() => this.initKernelClient(), delay);
435
+ };
436
+
437
+ this.ws.onmessage = (event) => {
438
+ try {
439
+ const msg = JSON.parse(event.data);
440
+
441
+ // 处理事件通知(/ws/management 格式:{type: event_type, data: {...}})
442
+ if (msg.type && msg.type !== 'connected' && msg.type !== 'ping' && msg.type !== 'pong') {
443
+ const eventType = msg.type;
444
+ const eventData = msg.data || {};
445
+
446
+ // 如果控制台展开,添加到事件日志
447
+ if (this.consoleExpanded) {
448
+ this.addEventLog(eventType, eventData);
449
+ }
450
+
451
+ // 刷新模块列表(仅针对 module.* 事件)
452
+ if (eventType.startsWith('module.') && this.currentPage === 'modules') {
453
+ this.loadModules();
454
+ }
455
+ }
456
+ } catch (e) {
457
+ console.error('[Evol] Message parse error:', e);
458
+ }
459
+ };
460
+ }
461
+
462
+ async callRpc(method, params = {}) {
463
+ try {
464
+ const res = await fetch(`/api/rpc/${method}`, {
465
+ method: 'POST',
466
+ headers: { 'Content-Type': 'application/json' },
467
+ body: JSON.stringify(params)
468
+ });
469
+
470
+ if (!res.ok) {
471
+ const error = await res.json();
472
+ throw new Error(error.detail || `HTTP ${res.status}`);
473
+ }
474
+
475
+ return await res.json();
476
+ } catch (err) {
477
+ console.error(`[Evol] RPC ${method} failed:`, err);
478
+ throw err;
479
+ }
480
+ }
481
+
482
+ setupNavigation() {
483
+ const navItems = document.querySelectorAll('.nav-item');
484
+ const pages = document.querySelectorAll('.page');
485
+
486
+ navItems.forEach(item => {
487
+ item.addEventListener('click', () => {
488
+ const pageName = item.dataset.page;
489
+ this.currentPage = pageName;
490
+
491
+ navItems.forEach(i => i.classList.remove('active'));
492
+ pages.forEach(p => p.classList.remove('active'));
493
+
494
+ item.classList.add('active');
495
+ document.getElementById('page-' + pageName).classList.add('active');
496
+
497
+ if (pageName === 'modules') this.loadModules();
498
+ else if (pageName === 'credits') this.loadCreditsStats();
499
+ else if (pageName === 'tokens') this.loadTokens();
500
+ });
501
+ });
502
+
503
+ document.getElementById('sidebar-toggle').addEventListener('click', () => {
504
+ document.getElementById('sidebar').classList.toggle('collapsed');
505
+ });
506
+ }
507
+
508
+ loadAccountInfo() {
509
+ // 如果 userInfo 还没加载,显示加载中
510
+ if (!this.userInfo) {
511
+ const accountInfoEl = document.getElementById('account-info');
512
+ if (accountInfoEl) {
513
+ accountInfoEl.innerHTML = '<div style="padding:20px;text-align:center;color:#999;">加载中...</div>';
514
+ }
515
+ return;
516
+ }
517
+
518
+ const ui = this.userInfo.userInfo || {};
519
+ const ai = this.userInfo.accountInfo || {};
520
+ const ti = this.userInfo.teamInfo || {};
521
+ const gi = this.userInfo.gatewayInfo || {};
522
+
523
+ // 格式化 VIP 过期时间
524
+ const formatDate = (dateStr) => {
525
+ if (!dateStr) return '-';
526
+ const date = new Date(dateStr);
527
+ return date.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' });
528
+ };
529
+
530
+ // 积分包汇总
531
+ const pkg = ai.creditsPackageSummary || {};
532
+ const packageInfo = pkg.totalPackageCount > 0
533
+ ? `共 ${pkg.totalPackageCount} 个积分包`
534
+ : '无积分包';
535
+
536
+ const html = `
537
+ <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:20px;">
538
+ <!-- 基本信息 -->
539
+ <div style="background:#f9f9f9;padding:16px;border-radius:6px;">
540
+ <h3 style="margin:0 0 12px 0;font-size:14px;font-weight:600;color:#666;">基本信息</h3>
541
+ <table style="width:100%;border-collapse:collapse;font-size:13px;">
542
+ <tr><td style="padding:8px 0;color:#666;">手机号</td><td style="padding:8px 0;font-weight:500;">${ui.phone || '-'}</td></tr>
543
+ <tr><td style="padding:8px 0;color:#666;">昵称</td><td style="padding:8px 0;font-weight:500;">${ui.nickName || '-'}</td></tr>
544
+ <tr><td style="padding:8px 0;color:#666;">用户ID</td><td style="padding:8px 0;font-weight:500;">${ui.userId || '-'}</td></tr>
545
+ <tr><td style="padding:8px 0;color:#666;">AID</td><td style="padding:8px 0;font-family:monospace;font-size:11px;">${ui.aid || '-'}</td></tr>
546
+ </table>
547
+ </div>
548
+
549
+ <!-- 积分信息 -->
550
+ <div style="background:#f9f9f9;padding:16px;border-radius:6px;">
551
+ <h3 style="margin:0 0 12px 0;font-size:14px;font-weight:600;color:#666;">积分信息</h3>
552
+ <table style="width:100%;border-collapse:collapse;font-size:13px;">
553
+ <tr><td style="padding:8px 0;color:#666;">当前积分</td><td style="padding:8px 0;font-weight:600;color:#27ae60;font-size:16px;">${ai.credits || 0}</td></tr>
554
+ <tr><td style="padding:8px 0;color:#666;">积分上限</td><td style="padding:8px 0;font-weight:500;">${ai.creditsLimit || 0}</td></tr>
555
+ <tr><td style="padding:8px 0;color:#666;">恢复速率</td><td style="padding:8px 0;font-weight:500;">${ai.creditsRecoveryRate || 0} / 小时</td></tr>
556
+ <tr><td style="padding:8px 0;color:#666;">积分包</td><td style="padding:8px 0;font-weight:500;">${packageInfo}</td></tr>
557
+ </table>
558
+ </div>
559
+
560
+ <!-- VIP 信息 -->
561
+ <div style="background:#f9f9f9;padding:16px;border-radius:6px;">
562
+ <h3 style="margin:0 0 12px 0;font-size:14px;font-weight:600;color:#666;">VIP 信息</h3>
563
+ <table style="width:100%;border-collapse:collapse;font-size:13px;">
564
+ <tr><td style="padding:8px 0;color:#666;">VIP 类型</td><td style="padding:8px 0;font-weight:500;">${ai.vipTypeName || 'Unknown'}</td></tr>
565
+ <tr><td style="padding:8px 0;color:#666;">过期时间</td><td style="padding:8px 0;font-weight:500;">${formatDate(ai.vipExpireTime)}</td></tr>
566
+ <tr><td style="padding:8px 0;color:#666;">剩余天数</td><td style="padding:8px 0;font-weight:600;color:#ff9800;">${ai.vipRemainingDays || 0} 天</td></tr>
567
+ <tr><td style="padding:8px 0;color:#666;">账户余额</td><td style="padding:8px 0;font-weight:500;">¥ ${(ai.balance || 0).toFixed(2)}</td></tr>
568
+ </table>
569
+ </div>
570
+
571
+ <!-- 团队信息 -->
572
+ <div style="background:#f9f9f9;padding:16px;border-radius:6px;">
573
+ <h3 style="margin:0 0 12px 0;font-size:14px;font-weight:600;color:#666;">团队信息</h3>
574
+ <table style="width:100%;border-collapse:collapse;font-size:13px;">
575
+ <tr><td style="padding:8px 0;color:#666;">当前团队</td><td style="padding:8px 0;font-weight:500;">${ti.currentTeamName || '-'}</td></tr>
576
+ <tr><td style="padding:8px 0;color:#666;">团队角色</td><td style="padding:8px 0;font-weight:500;">${ti.currentTeamRole === '1' ? '管理员' : '成员'}</td></tr>
577
+ <tr><td style="padding:8px 0;color:#666;">团队数量</td><td style="padding:8px 0;font-weight:500;">${(ti.teams || []).length} 个</td></tr>
578
+ </table>
579
+ </div>
580
+
581
+ <!-- 网关信息 -->
582
+ <div style="background:#f9f9f9;padding:16px;border-radius:6px;grid-column:1/-1;">
583
+ <h3 style="margin:0 0 12px 0;font-size:14px;font-weight:600;color:#666;">网关信息</h3>
584
+ <table style="width:100%;border-collapse:collapse;font-size:13px;">
585
+ <tr><td style="padding:8px 0;color:#666;width:120px;">网关名称</td><td style="padding:8px 0;font-weight:500;">${gi.gatewayName || '-'}</td></tr>
586
+ <tr><td style="padding:8px 0;color:#666;">模型地址</td><td style="padding:8px 0;font-family:monospace;font-size:11px;">${gi.modelBaseUrl || '-'}</td></tr>
587
+ <tr><td style="padding:8px 0;color:#666;">API Key</td><td style="padding:8px 0;"><code style="background:#fff;padding:4px 8px;border-radius:4px;font-size:11px;">${gi.apiKey || '未获取'}</code></td></tr>
588
+ </table>
589
+ </div>
590
+ </div>
591
+ `;
592
+ document.getElementById('account-info').innerHTML = html;
593
+ }
594
+
595
+ async loadCreditsStats() {
596
+ try {
597
+ const res = await fetch('/api/get_credits_stats', {
598
+ method: 'POST',
599
+ headers: { 'Content-Type': 'application/json' },
600
+ body: JSON.stringify({ kiteToken: this.kiteToken, period: 'day' })
601
+ });
602
+ const data = await res.json();
603
+
604
+ if (data.success) {
605
+ const html = '<h4>今日积分消耗</h4><p style="font-size:24px;font-weight:600;color:#667eea;">' +
606
+ (data.data?.total || 0) + ' 积分</p>';
607
+ document.getElementById('credits-stats').innerHTML = html;
608
+ } else {
609
+ document.getElementById('credits-stats').innerHTML = '<p style="color:#e74c3c;">' + data.msg + '</p>';
610
+ }
611
+ } catch (err) {
612
+ document.getElementById('credits-stats').innerHTML = '<p style="color:#e74c3c;">加载失败</p>';
613
+ }
614
+ }
615
+
616
+ async loadModules() {
617
+ // Reset to list view
618
+ document.getElementById('modules-list-header')?.classList.remove('hidden');
619
+ document.getElementById('modules-table')?.closest('.panel')?.classList.remove('hidden');
620
+ document.getElementById('module-detail')?.classList.add('hidden');
621
+ document.getElementById('statistics-panel')?.classList.remove('hidden');
622
+ document.getElementById('registry-test-section')?.classList.remove('hidden');
623
+ document.getElementById('registry-test-output')?.classList.remove('hidden');
624
+
625
+ // Load statistics and start auto-refresh
626
+ this.loadModuleStats();
627
+ this.startStatsAutoRefresh();
628
+
629
+ try {
630
+ // 获取模块列表和运行状态
631
+ const result = await this.callRpc('launcher.list_modules', {});
632
+ const modules = result.modules || [];
633
+
634
+ const tbody = document.getElementById('modules-tbody');
635
+ if (modules.length === 0) {
636
+ tbody.innerHTML = '<tr><td colspan="9" class="text-muted" style="text-align:center;padding:40px;">暂无模块</td></tr>';
637
+ return;
638
+ }
639
+
640
+ let html = '';
641
+ modules.forEach(mod => {
642
+ // 状态圆点
643
+ const stateClass = mod.state === 'enabled' ? 'enabled' : mod.state === 'manual' ? 'manual' : 'disabled';
644
+
645
+ // 运行状态判断
646
+ const running = mod.actual_state ? mod.actual_state.startsWith('running') : false;
647
+ const pending = this.moduleActionPending.has(mod.name);
648
+ const pendingAction = this.moduleActionPending.get(mod.name);
649
+
650
+ const runningStatus = pending
651
+ ? `<span style="color:var(--warning);">${pendingAction === 'start' ? '启动中…' : '停止中…'}</span>`
652
+ : running
653
+ ? '<span style="color:var(--success);">运行中</span>'
654
+ : '<span style="color:var(--gray-400);">已停止</span>';
655
+
656
+ // 默认状态下拉选项
657
+ const stateOptions = `
658
+ <option value="enabled" ${mod.state === 'enabled' ? 'selected' : ''}>自动</option>
659
+ <option value="manual" ${mod.state === 'manual' ? 'selected' : ''}>手动</option>
660
+ <option value="disabled" ${mod.state === 'disabled' ? 'selected' : ''}>禁用</option>
661
+ `;
662
+
663
+ // 统一操作按钮逻辑
664
+ const isCore = ['kernel', 'launcher'].includes(mod.name);
665
+ const displayOrder = mod.display_order || 0;
666
+ const isHighOrder = displayOrder >= 80;
667
+ const isDisabledState = mod.state === 'disabled';
668
+
669
+ // 完全禁止操作的条件:核心模块 或 display_order>=80 或 disabled状态
670
+ const fullyDisabled = isCore || isHighOrder || isDisabledState;
671
+
672
+ let btnHtml = '';
673
+ if (!fullyDisabled) {
674
+ // 只有非禁用模块才显示按钮
675
+ let btnClass, btnLabel, btnAction, btnDisabled;
676
+
677
+ if (pending) {
678
+ // 操作中
679
+ btnClass = 'btn-warning';
680
+ btnLabel = pendingAction === 'start' ? '启动中…' : '停止中…';
681
+ btnAction = '';
682
+ btnDisabled = 'disabled';
683
+ } else if (running) {
684
+ // 运行中 → 可停止
685
+ btnClass = 'btn-danger';
686
+ btnLabel = '停止';
687
+ btnAction = `onclick="app.stopModule('${mod.name}')"`;
688
+ btnDisabled = '';
689
+ } else {
690
+ // 已停止 → 可启动
691
+ btnClass = 'btn-success';
692
+ btnLabel = '启动';
693
+ btnAction = `onclick="app.startModule('${mod.name}')"`;
694
+ btnDisabled = '';
695
+ }
696
+
697
+ btnHtml = `<button class="btn btn-sm ${btnClass}" ${btnAction} ${btnDisabled} style="min-width:72px;">${btnLabel}</button>`;
698
+ }
699
+
700
+ html += `<tr data-module="${mod.name}" class="module-row" onclick="app.showModuleDetails('${mod.name}')">
701
+ <td><span class="module-state-dot ${stateClass}"></span></td>
702
+ <td><strong>${mod.display_name || mod.name}</strong> <span style="color:#999;font-size:12px;">(${mod.name})</span></td>
703
+ <td><span class="module-type-badge type-${mod.type || 'unknown'}">${mod.type || '?'}</span></td>
704
+ <td>${mod.version || '-'}</td>
705
+ <td>${mod.preferred_port || '-'}</td>
706
+ <td>${runningStatus}</td>
707
+ <td onclick="event.stopPropagation()">
708
+ <select class="module-state-select" data-module="${mod.name}" onchange="app.onModuleStateChange(this)">
709
+ ${stateOptions}
710
+ </select>
711
+ </td>
712
+ <td onclick="event.stopPropagation()">
713
+ ${btnHtml}
714
+ </td>
715
+ </tr>`;
716
+ });
717
+
718
+ tbody.innerHTML = html;
719
+ } catch (err) {
720
+ console.error('[Evol] Load modules failed:', err);
721
+ document.getElementById('modules-tbody').innerHTML =
722
+ `<tr><td colspan="8" style="text-align:center;padding:40px;color:#e74c3c;">加载失败: ${err.message}</td></tr>`;
723
+ }
724
+ }
725
+
726
+ async loadModuleStats() {
727
+ try {
728
+ // 对齐 Web 模块:使用 kernel.health 获取运行时长
729
+ const health = await this.callRpc('kernel.health', {});
730
+ const eventStats = health.event_stats || {};
731
+
732
+ // 对齐 Web 模块:使用 kernel.stats 获取统计数据
733
+ const stats = await this.callRpc('kernel.stats', {});
734
+ const counters = stats.counters || {};
735
+ const rpcStats = stats.rpc || {};
736
+
737
+ // 获取模块列表(用于计算模块数量和注册记录)
738
+ let modules = [];
739
+ try {
740
+ const modulesRes = await this.callRpc('launcher.list_modules', {});
741
+ modules = modulesRes.modules || [];
742
+ } catch (err) {
743
+ if (!err.message.includes('not ready')) {
744
+ console.warn('[Evol] Failed to get modules for stats:', err);
745
+ }
746
+ }
747
+
748
+ // 计算运行时长
749
+ const uptime = eventStats.uptime_seconds || 0;
750
+
751
+ // 计算模块数量
752
+ const moduleCount = modules.length;
753
+
754
+ // 获取所有注册记录并分类统计
755
+ let registryByCategory = {
756
+ modules: 0, // module.* 字段
757
+ rpc: 0, // tools.rpc.* 字段
758
+ hook: 0, // tools.hook.* 字段
759
+ api: 0 // tools.api.* 字段
760
+ };
761
+ let totalRecords = 0;
762
+
763
+ for (const mod of modules) {
764
+ try {
765
+ // 对齐 Web 模块:使用 registry.lookup 查询每个模块的注册记录
766
+ const regRes = await this.callRpc('registry.lookup', { module: mod.name });
767
+ const records = regRes.results || [];
768
+ totalRecords += records.length;
769
+
770
+ // 按字段路径分类
771
+ for (const rec of records) {
772
+ const field = rec.field || '';
773
+ if (field.startsWith('module.')) {
774
+ registryByCategory.modules++;
775
+ } else if (field.startsWith('tools.rpc.')) {
776
+ registryByCategory.rpc++;
777
+ } else if (field.startsWith('tools.hook.')) {
778
+ registryByCategory.hook++;
779
+ } else if (field.startsWith('tools.api.')) {
780
+ registryByCategory.api++;
781
+ }
782
+ }
783
+ } catch (e) {
784
+ // 模块可能还未注册,忽略错误
785
+ }
786
+ }
787
+
788
+ // 事件统计
789
+ const eventsRouted = counters.events_routed || 0;
790
+
791
+ // RPC 调用统计
792
+ const rpcCalls = rpcStats.total || 0;
793
+
794
+ // 更新 UI
795
+ document.getElementById('stat-uptime').textContent = this.formatUptime(uptime);
796
+ document.getElementById('stat-modules').textContent = moduleCount;
797
+ document.getElementById('stat-registry').textContent = totalRecords;
798
+ document.getElementById('stat-rpc').textContent = registryByCategory.rpc;
799
+ document.getElementById('stat-hooks').textContent = registryByCategory.hook;
800
+ document.getElementById('stat-api').textContent = registryByCategory.api;
801
+ document.getElementById('stat-events').textContent = eventsRouted;
802
+ document.getElementById('stat-rpc-calls').textContent = rpcCalls;
803
+ } catch (err) {
804
+ console.error('[Evol] Load stats failed:', err);
805
+ // 不清空 UI,保留上次的值
806
+ }
807
+ }
808
+
809
+ formatUptime(seconds) {
810
+ // 对齐 Web 模块的格式化逻辑
811
+ if (seconds < 60) return `${Math.floor(seconds)}秒`;
812
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`;
813
+ if (seconds < 86400) {
814
+ const h = Math.floor(seconds / 3600);
815
+ const m = Math.floor((seconds % 3600) / 60);
816
+ return m > 0 ? `${h}小时${m}分` : `${h}小时`;
817
+ }
818
+ const d = Math.floor(seconds / 86400);
819
+ const h = Math.floor((seconds % 86400) / 3600);
820
+ return h > 0 ? `${d}天${h}小时` : `${d}天`;
821
+ }
822
+
823
+ startStatsAutoRefresh() {
824
+ if (this.statsRefreshTimer) clearInterval(this.statsRefreshTimer);
825
+ this.statsRefreshTimer = setInterval(() => {
826
+ if (this.currentPage === 'modules') {
827
+ this.loadModuleStats();
828
+ }
829
+ }, 5000);
830
+ }
831
+
832
+ async showModuleDetails(moduleName) {
833
+ try {
834
+ // 对齐 Web 模块:使用 launcher.get_module_config 获取完整配置
835
+ const mod = await this.callRpc('launcher.get_module_config', { module_name: moduleName });
836
+
837
+ // 隐藏列表,显示详情
838
+ document.getElementById('modules-list-header')?.classList.add('hidden');
839
+ document.getElementById('modules-table')?.closest('.panel')?.classList.add('hidden');
840
+ document.getElementById('statistics-panel')?.classList.add('hidden');
841
+ document.getElementById('registry-test-section')?.classList.add('hidden');
842
+ document.getElementById('registry-test-output')?.classList.add('hidden');
843
+ document.getElementById('module-detail')?.classList.remove('hidden');
844
+
845
+ // 保存当前模块名(用于后续操作)
846
+ this.currentModuleName = moduleName;
847
+
848
+ // Header
849
+ document.getElementById('module-detail-name').textContent = mod.display_name || mod.name;
850
+
851
+ // 【区块1:模块标识】
852
+ this._setVal('mod-source-path', mod.source_path || '');
853
+ this._setVal('mod-meta-name', mod.name || '');
854
+ this._setVal('mod-meta-type', mod.type || '');
855
+ this._setVal('mod-meta-runtime', mod.runtime || '');
856
+ this._setVal('mod-meta-entry', mod.entry || '');
857
+ this._setVal('mod-meta-display-name', mod.display_name || '');
858
+ this._setVal('mod-meta-version', mod.version || '');
859
+
860
+ // 【区块2:启动配置】
861
+ this._setVal('mod-meta-state', mod.state || 'enabled');
862
+ const monitorValue = mod.monitor != null ? String(mod.monitor) : 'true';
863
+ this._setVal('mod-meta-monitor', monitorValue);
864
+
865
+ // 【区块3:网络配置】
866
+ this._setVal('mod-meta-port', mod.preferred_port != null ? mod.preferred_port : '');
867
+
868
+ // 监听地址:根据模块设置默认值
869
+ let defaultIp = '127.0.0.1';
870
+ if (mod.name === 'web' || mod.name === 'evol') {
871
+ defaultIp = '0.0.0.0'; // web/evol 模块默认允许远程
872
+ }
873
+ this._setVal('mod-meta-ip', mod.advertise_ip || defaultIp);
874
+
875
+ // 运行状态
876
+ await this._updateModuleDetailStatus(moduleName);
877
+
878
+ // 【区块4:模块配置文件】
879
+ const configSection = document.getElementById('module-config-section');
880
+ const configTree = document.getElementById('module-config-tree');
881
+ if (mod.has_config && mod.config) {
882
+ configSection?.classList.remove('hidden');
883
+ if (configTree) {
884
+ configTree.innerHTML = '';
885
+ this._renderConfigTree(mod.config, configTree, '');
886
+ }
887
+ } else {
888
+ configSection?.classList.add('hidden');
889
+ }
890
+
891
+ // 绑定自动保存事件监听器
892
+ this._bindAutoSaveListeners();
893
+
894
+ } catch (err) {
895
+ alert('加载模块详情失败: ' + err.message);
896
+ console.error('[Evol] showModuleDetails failed:', err);
897
+ }
898
+ }
899
+
900
+ // 辅助方法:设置表单值
901
+ _setVal(id, value) {
902
+ const el = document.getElementById(id);
903
+ if (el) {
904
+ if (el.tagName === 'SELECT') {
905
+ el.value = value;
906
+ } else if (el.type === 'number') {
907
+ el.value = value === '' ? '' : value;
908
+ } else {
909
+ el.value = value;
910
+ }
911
+ }
912
+ }
913
+
914
+ // 更新模块详情页的运行状态
915
+ async _updateModuleDetailStatus(moduleName) {
916
+ try {
917
+ const result = await this.callRpc('launcher.list_modules', {});
918
+ const modules = result.modules || [];
919
+ const mod = modules.find(m => m.name === moduleName);
920
+
921
+ if (mod) {
922
+ const running = mod.actual_state ? mod.actual_state.startsWith('running') : false;
923
+ const pending = this.moduleActionPending.has(mod.name);
924
+ const pendingAction = this.moduleActionPending.get(mod.name);
925
+
926
+ // 更新运行状态文本和状态点
927
+ const statusEl = document.getElementById('module-detail-run-status');
928
+ const dotEl = document.getElementById('module-detail-status-dot');
929
+
930
+ if (statusEl && dotEl) {
931
+ if (pending) {
932
+ statusEl.textContent = pendingAction === 'start' ? '启动中…' : '停止中…';
933
+ statusEl.style.color = 'var(--warning)';
934
+ dotEl.style.background = 'var(--warning)';
935
+ } else if (running) {
936
+ statusEl.textContent = '运行中';
937
+ statusEl.style.color = 'var(--success)';
938
+ dotEl.style.background = 'var(--success)';
939
+ } else {
940
+ statusEl.textContent = '已停止';
941
+ statusEl.style.color = 'var(--gray-400)';
942
+ dotEl.style.background = 'var(--gray-400)';
943
+ }
944
+ }
945
+
946
+ // 更新统一操作按钮
947
+ const actionBtn = document.getElementById('btn-detail-action');
948
+ if (actionBtn) {
949
+ const isCore = ['kernel', 'launcher'].includes(mod.name);
950
+ const displayOrder = mod.display_order || 0;
951
+ const isHighOrder = displayOrder >= 80;
952
+ const isDisabledState = mod.state === 'disabled';
953
+ const fullyDisabled = isCore || isHighOrder || isDisabledState;
954
+
955
+ if (fullyDisabled) {
956
+ // 完全禁用 - 隐藏按钮
957
+ actionBtn.style.display = 'none';
958
+ } else {
959
+ actionBtn.style.display = '';
960
+
961
+ if (pending) {
962
+ // 操作中
963
+ actionBtn.className = 'btn btn-sm btn-warning';
964
+ actionBtn.textContent = pendingAction === 'start' ? '启动中…' : '停止中…';
965
+ actionBtn.disabled = true;
966
+ actionBtn.onclick = null;
967
+ } else if (running) {
968
+ // 运行中 → 可停止
969
+ actionBtn.className = 'btn btn-sm btn-danger';
970
+ actionBtn.textContent = '停止';
971
+ actionBtn.disabled = false;
972
+ actionBtn.onclick = () => this.stopModuleFromDetail();
973
+ } else {
974
+ // 已停止 → 可启动
975
+ actionBtn.className = 'btn btn-sm btn-success';
976
+ actionBtn.textContent = '启动';
977
+ actionBtn.disabled = false;
978
+ actionBtn.onclick = () => this.startModuleFromDetail();
979
+ }
980
+ }
981
+ }
982
+ }
983
+ } catch (err) {
984
+ console.error('[Evol] Update detail status failed:', err);
985
+ }
986
+ }
987
+
988
+ // 渲染配置树(简化版)
989
+ _renderConfigTree(config, container, prefix) {
990
+ if (!config || typeof config !== 'object') return;
991
+
992
+ for (const [key, value] of Object.entries(config)) {
993
+ const fullKey = prefix ? `${prefix}.${key}` : key;
994
+ const div = document.createElement('div');
995
+ div.style.marginBottom = '8px';
996
+
997
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
998
+ // 嵌套对象
999
+ div.innerHTML = `<strong style="color:#666;">${key}:</strong>`;
1000
+ container.appendChild(div);
1001
+ const subContainer = document.createElement('div');
1002
+ subContainer.style.marginLeft = '20px';
1003
+ container.appendChild(subContainer);
1004
+ this._renderConfigTree(value, subContainer, fullKey);
1005
+ } else {
1006
+ // 简单值
1007
+ div.innerHTML = `
1008
+ <label style="display:inline-block;width:200px;font-size:13px;color:#666;">${key}:</label>
1009
+ <input type="text" value="${value}" data-config-key="${fullKey}"
1010
+ style="padding:4px 8px;border:1px solid #ddd;border-radius:4px;width:300px;">
1011
+ `;
1012
+ container.appendChild(div);
1013
+ }
1014
+ }
1015
+ }
1016
+
1017
+ // 绑定自动保存监听器
1018
+ _bindAutoSaveListeners() {
1019
+ // 防抖保存函数
1020
+ if (!this._debouncedSave) {
1021
+ this._debouncedSave = this._debounce(() => this._saveModuleConfig(), 500);
1022
+ }
1023
+
1024
+ // 绑定元数据字段
1025
+ document.querySelectorAll('#module-detail [data-field]').forEach(el => {
1026
+ el.removeEventListener('input', this._debouncedSave);
1027
+ el.removeEventListener('change', this._debouncedSave);
1028
+ const event = (el.tagName === 'SELECT') ? 'change' : 'input';
1029
+ el.addEventListener(event, this._debouncedSave);
1030
+ });
1031
+
1032
+ // 绑定配置树字段
1033
+ document.querySelectorAll('#module-config-tree [data-config-key]').forEach(el => {
1034
+ el.removeEventListener('input', this._debouncedSave);
1035
+ el.addEventListener('input', this._debouncedSave);
1036
+ });
1037
+ }
1038
+
1039
+ // 防抖函数
1040
+ _debounce(func, wait) {
1041
+ let timeout;
1042
+ return function executedFunction(...args) {
1043
+ const later = () => {
1044
+ clearTimeout(timeout);
1045
+ func(...args);
1046
+ };
1047
+ clearTimeout(timeout);
1048
+ timeout = setTimeout(later, wait);
1049
+ };
1050
+ }
1051
+
1052
+ // 保存模块配置
1053
+ async _saveModuleConfig() {
1054
+ if (!this.currentModuleName) return;
1055
+
1056
+ try {
1057
+ // 收集元数据
1058
+ const metadata = {};
1059
+ document.querySelectorAll('#module-detail [data-field]').forEach(el => {
1060
+ const field = el.dataset.field;
1061
+ let value = el.value;
1062
+
1063
+ // 类型转换
1064
+ if (el.type === 'number') {
1065
+ value = value === '' ? null : parseInt(value);
1066
+ } else if (field === 'monitor') {
1067
+ value = value === 'true';
1068
+ }
1069
+
1070
+ metadata[field] = value;
1071
+ });
1072
+
1073
+ // 收集配置文件
1074
+ const config = {};
1075
+ document.querySelectorAll('#module-config-tree [data-config-key]').forEach(el => {
1076
+ const key = el.dataset.configKey;
1077
+ config[key] = el.value;
1078
+ });
1079
+
1080
+ // 调用更新 RPC
1081
+ await this.callRpc('launcher.update_module_config', {
1082
+ module_name: this.currentModuleName,
1083
+ metadata: metadata,
1084
+ config: Object.keys(config).length > 0 ? config : undefined
1085
+ });
1086
+
1087
+ // 显示保存成功提示(简单版)
1088
+ console.log('[Evol] Module config saved successfully');
1089
+
1090
+ } catch (err) {
1091
+ console.error('[Evol] Save module config failed:', err);
1092
+ alert('保存失败: ' + err.message);
1093
+ }
1094
+ }
1095
+
1096
+ async startModule(moduleName) {
1097
+ if (['kernel', 'launcher'].includes(moduleName)) return; // 静默拦截核心模块
1098
+ if (this.moduleActionPending.has(moduleName)) return; // 防抖
1099
+
1100
+ this.moduleActionPending.set(moduleName, 'start');
1101
+ this.updateModuleButtons(moduleName);
1102
+
1103
+ try {
1104
+ await this.callRpc('launcher.start_module', { name: moduleName });
1105
+ // 延迟刷新,等待模块实际启动后再查询状态
1106
+ setTimeout(async () => {
1107
+ this.moduleActionPending.delete(moduleName);
1108
+ await this.loadModules();
1109
+ // 检查启动结果
1110
+ const result = await this.callRpc('launcher.list_modules', {});
1111
+ const mod = (result.modules || []).find(m => m.name === moduleName);
1112
+ const running = mod?.actual_state?.startsWith('running');
1113
+ if (running) {
1114
+ this.showToast(`${moduleName} 启动成功`, 'success');
1115
+ } else {
1116
+ this.showToast(`${moduleName} 启动超时,请检查日志`, 'error');
1117
+ }
1118
+ }, 1500);
1119
+ } catch (err) {
1120
+ this.moduleActionPending.delete(moduleName);
1121
+ this.updateModuleButtons(moduleName);
1122
+ this.showToast(`启动失败: ${err.message}`, 'error');
1123
+ }
1124
+ }
1125
+
1126
+ async stopModule(moduleName) {
1127
+ if (['kernel', 'launcher'].includes(moduleName)) return; // 静默拦截核心模块
1128
+ if (this.moduleActionPending.has(moduleName)) return; // 防抖
1129
+
1130
+ this.moduleActionPending.set(moduleName, 'stop');
1131
+ this.updateModuleButtons(moduleName);
1132
+
1133
+ try {
1134
+ await this.callRpc('launcher.stop_module', { name: moduleName, reason: 'user_request' });
1135
+ // 延迟刷新,等待模块实际停止后再查询状态
1136
+ setTimeout(async () => {
1137
+ this.moduleActionPending.delete(moduleName);
1138
+ await this.loadModules();
1139
+ // 检查停止结果
1140
+ const result = await this.callRpc('launcher.list_modules', {});
1141
+ const mod = (result.modules || []).find(m => m.name === moduleName);
1142
+ const running = mod?.actual_state?.startsWith('running');
1143
+ if (!running) {
1144
+ this.showToast(`${moduleName} 停止成功`, 'success');
1145
+ } else {
1146
+ this.showToast(`${moduleName} 停止超时,请检查日志`, 'error');
1147
+ }
1148
+ }, 1500);
1149
+ } catch (err) {
1150
+ this.moduleActionPending.delete(moduleName);
1151
+ this.updateModuleButtons(moduleName);
1152
+ this.showToast(`停止失败: ${err.message}`, 'error');
1153
+ }
1154
+ }
1155
+
1156
+ updateModuleButtons(moduleName) {
1157
+ // 判断当前是否在详情页
1158
+ const detailPanel = document.getElementById('module-detail');
1159
+ const isDetailPage = detailPanel && !detailPanel.classList.contains('hidden');
1160
+
1161
+ if (isDetailPage && this.currentModuleName === moduleName) {
1162
+ // 在详情页,只更新详情页状态
1163
+ this._updateModuleDetailStatus(moduleName);
1164
+ } else {
1165
+ // 在列表页,重新渲染列表
1166
+ this.loadModules();
1167
+ }
1168
+ }
1169
+
1170
+ async onModuleStateChange(selectEl) {
1171
+ const moduleName = selectEl.dataset.module;
1172
+ const newState = selectEl.value;
1173
+
1174
+ // 禁用下拉,防止重复操作
1175
+ selectEl.disabled = true;
1176
+
1177
+ try {
1178
+ // 调用 RPC 更新模块配置
1179
+ await this.callRpc('launcher.update_module_config', {
1180
+ module_name: moduleName,
1181
+ metadata: { state: newState }
1182
+ });
1183
+ this.showToast(`模块 ${moduleName} 默认状态已更新为 ${newState}`, 'success');
1184
+
1185
+ // 更新状态圆点
1186
+ const row = selectEl.closest('tr');
1187
+ const dot = row?.querySelector('.module-state-dot');
1188
+ if (dot) {
1189
+ dot.className = `module-state-dot ${newState === 'enabled' ? 'enabled' : newState === 'manual' ? 'manual' : 'disabled'}`;
1190
+ }
1191
+ } catch (err) {
1192
+ this.showToast('更新失败: ' + err.message, 'error');
1193
+ // 恢复原值
1194
+ this.loadModules();
1195
+ } finally {
1196
+ selectEl.disabled = false;
1197
+ }
1198
+ }
1199
+
1200
+ async restartKite() {
1201
+ // 禁用按钮,防止重复点击
1202
+ const btn = document.getElementById('btn-restart-kite');
1203
+ if (btn) {
1204
+ btn.disabled = true;
1205
+ btn.innerHTML = '<span>⏳</span><span>重启中...</span>';
1206
+ }
1207
+
1208
+ try {
1209
+ const data = await this.callRpc('launcher.restart_launcher', { reason: 'user_request' });
1210
+
1211
+ // 检查是否有错误
1212
+ if (data.error) {
1213
+ this.showToast(`重启失败: ${data.error}`, 'error');
1214
+ if (btn) {
1215
+ btn.disabled = false;
1216
+ btn.innerHTML = '<span>🔄</span><span>重启 Kite</span>';
1217
+ }
1218
+ return;
1219
+ }
1220
+
1221
+ // 成功 - 显示重启提示并轮询检测重启完成
1222
+ this.showToast('Kite 正在重启...', 'success');
1223
+
1224
+ // 轮询检测 Kite 是否重启完成(每 0.5s 检查一次,最多 30s)
1225
+ let attempts = 0;
1226
+ const maxAttempts = 60; // 30s
1227
+ const checkInterval = setInterval(async () => {
1228
+ attempts++;
1229
+ try {
1230
+ // 尝试调用 RPC,如果成功说明重启完成
1231
+ await this.callRpc('kernel.health', {});
1232
+ clearInterval(checkInterval);
1233
+ this.showToast('Kite 重启完成', 'success');
1234
+ window.location.reload();
1235
+ } catch (err) {
1236
+ // 还在重启中,继续等待
1237
+ if (attempts >= maxAttempts) {
1238
+ clearInterval(checkInterval);
1239
+ this.showToast('重启超时,请手动刷新页面', 'warning');
1240
+ if (btn) {
1241
+ btn.disabled = false;
1242
+ btn.innerHTML = '<span>🔄</span><span>重启 Kite</span>';
1243
+ }
1244
+ }
1245
+ }
1246
+ }, 500);
1247
+ } catch (err) {
1248
+ this.showToast(`重启失败: ${err.message}`, 'error');
1249
+ if (btn) {
1250
+ btn.disabled = false;
1251
+ btn.innerHTML = '<span>🔄</span><span>重启 Kite</span>';
1252
+ }
1253
+ }
1254
+ }
1255
+
1256
+ // 详情页操作方法
1257
+ async startModuleFromDetail() {
1258
+ if (!this.currentModuleName) return;
1259
+ const moduleName = this.currentModuleName;
1260
+
1261
+ if (['kernel', 'launcher'].includes(moduleName)) return;
1262
+ if (this.moduleActionPending.has(moduleName)) return;
1263
+
1264
+ this.moduleActionPending.set(moduleName, 'start');
1265
+ await this._updateModuleDetailStatus(moduleName);
1266
+
1267
+ try {
1268
+ await this.callRpc('launcher.start_module', { name: moduleName });
1269
+ // 延迟刷新,等待模块实际启动后再查询状态
1270
+ setTimeout(async () => {
1271
+ this.moduleActionPending.delete(moduleName);
1272
+ await this._updateModuleDetailStatus(moduleName);
1273
+ // 检查启动结果
1274
+ const result = await this.callRpc('launcher.list_modules', {});
1275
+ const mod = (result.modules || []).find(m => m.name === moduleName);
1276
+ const running = mod?.actual_state?.startsWith('running');
1277
+ if (running) {
1278
+ this.showToast(`${moduleName} 启动成功`, 'success');
1279
+ } else {
1280
+ this.showToast(`${moduleName} 启动超时,请检查日志`, 'error');
1281
+ }
1282
+ }, 1500);
1283
+ } catch (err) {
1284
+ this.moduleActionPending.delete(moduleName);
1285
+ await this._updateModuleDetailStatus(moduleName);
1286
+ this.showToast(`启动失败: ${err.message}`, 'error');
1287
+ }
1288
+ }
1289
+
1290
+ async stopModuleFromDetail() {
1291
+ if (!this.currentModuleName) return;
1292
+ const moduleName = this.currentModuleName;
1293
+
1294
+ if (['kernel', 'launcher'].includes(moduleName)) return;
1295
+ if (this.moduleActionPending.has(moduleName)) return;
1296
+
1297
+ this.moduleActionPending.set(moduleName, 'stop');
1298
+ await this._updateModuleDetailStatus(moduleName);
1299
+
1300
+ try {
1301
+ await this.callRpc('launcher.stop_module', { name: moduleName, reason: 'user_request' });
1302
+ // 延迟刷新,等待模块实际停止后再查询状态
1303
+ setTimeout(async () => {
1304
+ this.moduleActionPending.delete(moduleName);
1305
+ await this._updateModuleDetailStatus(moduleName);
1306
+ // 检查停止结果
1307
+ const result = await this.callRpc('launcher.list_modules', {});
1308
+ const mod = (result.modules || []).find(m => m.name === moduleName);
1309
+ const running = mod?.actual_state?.startsWith('running');
1310
+ if (!running) {
1311
+ this.showToast(`${moduleName} 停止成功`, 'success');
1312
+ } else {
1313
+ this.showToast(`${moduleName} 停止超时,请检查日志`, 'error');
1314
+ }
1315
+ }, 1500);
1316
+ } catch (err) {
1317
+ this.moduleActionPending.delete(moduleName);
1318
+ await this._updateModuleDetailStatus(moduleName);
1319
+ this.showToast(`停止失败: ${err.message}`, 'error');
1320
+ }
1321
+ }
1322
+
1323
+ async resetModuleDefaults() {
1324
+ if (!this.currentModuleName) return;
1325
+ if (!confirm(`确定要恢复模块 ${this.currentModuleName} 的默认配置吗?`)) return;
1326
+
1327
+ try {
1328
+ await this.callRpc('launcher.reset_module_config', { module_name: this.currentModuleName });
1329
+ this.showToast('配置已恢复为默认值', 'success');
1330
+ // 重新加载详情
1331
+ await this.showModuleDetails(this.currentModuleName);
1332
+ } catch (err) {
1333
+ this.showToast(`恢复默认值失败: ${err.message}`, 'error');
1334
+ }
1335
+ }
1336
+
1337
+ async toggleConsole() {
1338
+ const console = document.getElementById('realtime-console');
1339
+ const text = document.getElementById('console-toggle-text');
1340
+
1341
+ if (console.style.display === 'none') {
1342
+ // 展开控制台
1343
+ console.style.display = 'block';
1344
+ text.textContent = '收起控制台';
1345
+ this.consoleExpanded = true;
1346
+
1347
+ // 订阅全部事件
1348
+ await this.subscribeAllEvents();
1349
+ } else {
1350
+ // 收起控制台
1351
+ console.style.display = 'none';
1352
+ text.textContent = '展开控制台';
1353
+ this.consoleExpanded = false;
1354
+
1355
+ // 清空事件日志
1356
+ this.eventLogs = [];
1357
+ document.getElementById('console-output').textContent = '';
1358
+
1359
+ // 恢复默认订阅(只订阅 module.* 事件)
1360
+ await this.subscribeDefaultEvents();
1361
+ }
1362
+ }
1363
+
1364
+ async subscribeAllEvents() {
1365
+ if (this.eventSubscribed) return;
1366
+
1367
+ try {
1368
+ // 订阅所有事件(使用通配符)
1369
+ await this.callRpc('evol.subscribe_events', { events: ['*'] });
1370
+ this.eventSubscribed = true;
1371
+ console.log('[Evol] Subscribed to all events');
1372
+ } catch (err) {
1373
+ console.error('[Evol] Failed to subscribe all events:', err);
1374
+ }
1375
+ }
1376
+
1377
+ async subscribeDefaultEvents() {
1378
+
1379
+
1380
+ try {
1381
+ // 只订阅 module.* 事件(用于刷新模块列表)
1382
+ await this.callRpc('evol.subscribe_events', { events: ['module.started', 'module.stopped', 'module.crashed', 'module.ready', 'module.exiting', 'module.shutdown', 'module.shutdown.ack', 'module.shutdown.ready'] });
1383
+ this.eventSubscribed = false;
1384
+ console.log('[Evol] Subscribed to default events (module.*)');
1385
+ } catch (err) {
1386
+ console.error('[Evol] Failed to subscribe default events:', err);
1387
+ }
1388
+ }
1389
+
1390
+ clearConsole() {
1391
+ this.eventLogs = [];
1392
+ this.renderEventLogs();
1393
+ }
1394
+
1395
+ // 添加事件日志
1396
+ addEventLog(event, data) {
1397
+ const timestamp = Date.now();
1398
+ const module = this.extractModuleFromEvent(event);
1399
+
1400
+ // 记录已知的模块和事件
1401
+ if (module) this.knownModules.add(module);
1402
+ this.knownEvents.add(event);
1403
+
1404
+ // 添加到日志数组
1405
+ this.eventLogs.unshift({
1406
+ timestamp,
1407
+ module,
1408
+ event,
1409
+ data,
1410
+ raw: JSON.stringify(data)
1411
+ });
1412
+
1413
+ // 限制日志数量(最多保留1000条)
1414
+ if (this.eventLogs.length > 1000) {
1415
+ this.eventLogs = this.eventLogs.slice(0, 1000);
1416
+ }
1417
+
1418
+ // 更新筛选器选项
1419
+ this.updateFilterOptions();
1420
+
1421
+ // 重新渲染
1422
+ this.renderEventLogs();
1423
+ }
1424
+
1425
+ // 从事件名提取模块名
1426
+ extractModuleFromEvent(event) {
1427
+ // 事件格式通常是 module.event_name 或 category.module.event_name
1428
+ const parts = event.split('.');
1429
+ if (parts.length >= 2) {
1430
+ return parts[0]; // 返回第一部分作为模块名
1431
+ }
1432
+ return null;
1433
+ }
1434
+
1435
+ // 更新筛选器选项
1436
+ updateFilterOptions() {
1437
+ // 更新模块筛选器
1438
+ const moduleSelect = document.getElementById('filter-module');
1439
+ if (moduleSelect) {
1440
+ const currentValue = moduleSelect.value;
1441
+ const modules = Array.from(this.knownModules).sort();
1442
+
1443
+ let html = '<option value="">全部模块</option>';
1444
+ modules.forEach(mod => {
1445
+ html += `<option value="${mod}">${mod}</option>`;
1446
+ });
1447
+
1448
+ moduleSelect.innerHTML = html;
1449
+ moduleSelect.value = currentValue;
1450
+ }
1451
+
1452
+ // 更新事件筛选器
1453
+ const eventSelect = document.getElementById('filter-event');
1454
+ if (eventSelect) {
1455
+ const currentValue = eventSelect.value;
1456
+ const events = Array.from(this.knownEvents).sort();
1457
+
1458
+ let html = '<option value="">全部事件</option>';
1459
+ events.forEach(evt => {
1460
+ html += `<option value="${evt}">${evt}</option>`;
1461
+ });
1462
+
1463
+ eventSelect.innerHTML = html;
1464
+ eventSelect.value = currentValue;
1465
+ }
1466
+ }
1467
+
1468
+ // 渲染事件日志
1469
+ renderEventLogs() {
1470
+ const output = document.getElementById('console-output');
1471
+ if (!output) return;
1472
+
1473
+ // 获取筛选条件
1474
+ const filterModule = document.getElementById('filter-module')?.value || '';
1475
+ const filterEvent = document.getElementById('filter-event')?.value || '';
1476
+ const filterKeyword = document.getElementById('filter-keyword')?.value || '';
1477
+ const filterTime = parseInt(document.getElementById('filter-time')?.value || '0');
1478
+
1479
+ // 筛选日志
1480
+ const now = Date.now();
1481
+ const filtered = this.eventLogs.filter(log => {
1482
+ // 时间筛选
1483
+ if (filterTime > 0) {
1484
+ const age = (now - log.timestamp) / 1000; // 秒
1485
+ if (age > filterTime) return false;
1486
+ }
1487
+
1488
+ // 模块筛选
1489
+ if (filterModule && log.module !== filterModule) return false;
1490
+
1491
+ // 事件筛选
1492
+ if (filterEvent && log.event !== filterEvent) return false;
1493
+
1494
+ // 关键词筛选(glob 模式)
1495
+ if (filterKeyword) {
1496
+ const pattern = this.globToRegex(filterKeyword);
1497
+ const searchText = `${log.event} ${log.raw}`.toLowerCase();
1498
+ if (!pattern.test(searchText)) return false;
1499
+ }
1500
+
1501
+ return true;
1502
+ });
1503
+
1504
+ // 渲染(时间倒序,最新的在最上面)
1505
+ let html = '';
1506
+ filtered.forEach(log => {
1507
+ const time = this.formatTimestamp(log.timestamp);
1508
+ const module = log.module || '?';
1509
+ const event = log.event;
1510
+ const data = this.formatEventData(log.data);
1511
+
1512
+ // 根据事件类型设置颜色
1513
+ let color = '#d4d4d4';
1514
+ if (event.includes('error') || event.includes('failed')) {
1515
+ color = '#f48771';
1516
+ } else if (event.includes('warning')) {
1517
+ color = '#dcdcaa';
1518
+ } else if (event.includes('started') || event.includes('success')) {
1519
+ color = '#4ec9b0';
1520
+ }
1521
+
1522
+ html += `<span style="color:#858585;">${time}</span> `;
1523
+ html += `<span style="color:#569cd6;">[${module}]</span> `;
1524
+ html += `<span style="color:${color};">${event}</span> `;
1525
+ html += `<span style="color:#9cdcfe;">${data}</span>\n`;
1526
+ });
1527
+
1528
+ output.innerHTML = html || '<span style="color:#858585;">暂无事件日志</span>';
1529
+
1530
+ // 保持滚动在顶部(因为是倒序显示)
1531
+ output.scrollTop = 0;
1532
+ }
1533
+
1534
+ // 格式化时间戳
1535
+ formatTimestamp(timestamp) {
1536
+ const date = new Date(timestamp);
1537
+ const h = String(date.getHours()).padStart(2, '0');
1538
+ const m = String(date.getMinutes()).padStart(2, '0');
1539
+ const s = String(date.getSeconds()).padStart(2, '0');
1540
+ const ms = String(date.getMilliseconds()).padStart(3, '0');
1541
+ return `${h}:${m}:${s}.${ms}`;
1542
+ }
1543
+
1544
+ // 格式化事件数据(人类可读)
1545
+ formatEventData(data) {
1546
+ if (!data || typeof data !== 'object') {
1547
+ return JSON.stringify(data);
1548
+ }
1549
+
1550
+ // 特殊格式化处理
1551
+ const formatted = [];
1552
+
1553
+ // 常见字段的友好显示
1554
+ if (data.module) formatted.push(`module=${data.module}`);
1555
+ if (data.state) formatted.push(`state=${data.state}`);
1556
+ if (data.status) formatted.push(`status=${data.status}`);
1557
+ if (data.port) formatted.push(`port=${data.port}`);
1558
+ if (data.pid) formatted.push(`pid=${data.pid}`);
1559
+ if (data.error) formatted.push(`error="${data.error}"`);
1560
+ if (data.message) formatted.push(`msg="${data.message}"`);
1561
+ if (data.duration !== undefined) formatted.push(`duration=${data.duration}ms`);
1562
+
1563
+ // 如果有格式化的字段,返回格式化结果
1564
+ if (formatted.length > 0) {
1565
+ return formatted.join(' ');
1566
+ }
1567
+
1568
+ // 否则返回紧凑的 JSON
1569
+ return JSON.stringify(data);
1570
+ }
1571
+
1572
+ // Glob 转正则表达式
1573
+ globToRegex(pattern) {
1574
+ const escaped = pattern
1575
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // 转义特殊字符
1576
+ .replace(/\*/g, '.*') // * 匹配任意字符
1577
+ .replace(/\?/g, '.'); // ? 匹配单个字符
1578
+ return new RegExp(escaped, 'i'); // 不区分大小写
1579
+ }
1580
+
1581
+ appendConsoleLog(message) {
1582
+ // 保留旧方法以兼容其他代码
1583
+ const output = document.getElementById('console-output');
1584
+ const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
1585
+ output.textContent += `[${timestamp}] ${message}\n`;
1586
+ output.scrollTop = output.scrollHeight;
1587
+ }
1588
+
1589
+ async runRegistryTest() {
1590
+ // 调用 registry-tests.js 中的 runAllTests() 函数
1591
+ if (typeof runAllTests === 'function') {
1592
+ await runAllTests();
1593
+ } else {
1594
+ const output = document.getElementById('test-output');
1595
+ output.textContent = '错误: registry-tests.js 未加载或 runAllTests 函数不存在';
1596
+ }
1597
+ }
1598
+
1599
+ clearTestOutput() {
1600
+ document.getElementById('test-output').textContent = '';
1601
+ }
1602
+
1603
+ async loadTokens() {
1604
+ // 根据当前激活的标签页加载对应的 token
1605
+ const activeTab = document.querySelector('.tab.active')?.dataset.tab || 'kite';
1606
+ if (activeTab === 'kite') {
1607
+ await this.loadKiteTokens();
1608
+ } else {
1609
+ await this.loadEvolTokens();
1610
+ }
1611
+ }
1612
+
1613
+ async loadKiteTokens() {
1614
+ try {
1615
+ const result = await this.callRpc('evol.list_kite_tokens', {});
1616
+ const tokens = result.tokens || [];
1617
+
1618
+ const tbody = document.getElementById('kite-tokens-tbody');
1619
+ if (tokens.length === 0) {
1620
+ tbody.innerHTML = '<tr><td colspan="9" class="text-muted" style="text-align:center;padding:40px;">暂无 Kite Token</td></tr>';
1621
+ return;
1622
+ }
1623
+
1624
+ // 缩略显示函数:头部6位...尾部8位
1625
+ const truncate = (str, headLen = 6, tailLen = 8) => {
1626
+ if (!str || str.length <= headLen + tailLen + 3) return str;
1627
+ return `${str.substring(0, headLen)}...${str.substring(str.length - tailLen)}`;
1628
+ };
1629
+
1630
+ let html = '';
1631
+ tokens.forEach(token => {
1632
+ // Token 缩略显示(前6位...后8位)
1633
+ const tokenDisplay = truncate(token.token, 6, 8);
1634
+
1635
+ // 设备信息(设备名 + 设备ID缩略)
1636
+ const deviceId = token.deviceId || 'unknown';
1637
+ const deviceIdShort = truncate(deviceId, 6, 6);
1638
+ const deviceInfo = `${token.deviceName || 'Unknown'}<br><span style="font-size:11px;color:#999;">${deviceIdShort}</span>`;
1639
+
1640
+ // 手机号显示
1641
+ const phone = token.phone || '<span style="color:#999;">未绑定</span>';
1642
+
1643
+ // 时间格式化
1644
+ const formatTime = (isoStr) => {
1645
+ if (!isoStr) return '-';
1646
+ const date = new Date(isoStr);
1647
+ return date.toLocaleString('zh-CN', {
1648
+ month: '2-digit',
1649
+ day: '2-digit',
1650
+ hour: '2-digit',
1651
+ minute: '2-digit'
1652
+ });
1653
+ };
1654
+
1655
+ const createdAt = formatTime(token.createdAt);
1656
+ const lastUsedAt = formatTime(token.lastUsedAt);
1657
+ const expiresAt = formatTime(token.expiresAt);
1658
+
1659
+ // 计算剩余有效期
1660
+ const now = new Date();
1661
+ const expireDate = token.expiresAt ? new Date(token.expiresAt) : null;
1662
+ let remainingTime = '-';
1663
+ let isExpired = false;
1664
+
1665
+ if (expireDate) {
1666
+ const diffMs = expireDate - now;
1667
+ isExpired = diffMs <= 0;
1668
+
1669
+ if (isExpired) {
1670
+ remainingTime = '已过期';
1671
+ } else {
1672
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
1673
+ const diffHours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
1674
+
1675
+ if (diffDays > 0) {
1676
+ remainingTime = `${diffDays}天`;
1677
+ } else if (diffHours > 0) {
1678
+ remainingTime = `${diffHours}小时`;
1679
+ } else {
1680
+ const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
1681
+ remainingTime = `${diffMinutes}分钟`;
1682
+ }
1683
+ }
1684
+ }
1685
+
1686
+ const status = isExpired ? '已过期' : '有效';
1687
+ const statusColor = isExpired ? '#e74c3c' : '#27ae60';
1688
+ const remainingColor = isExpired ? '#e74c3c' : (expireDate && (expireDate - now) < 7 * 24 * 60 * 60 * 1000 ? '#ff9800' : '#666');
1689
+
1690
+ html += `<tr>
1691
+ <td><code style="font-size:11px;" title="${token.token}">${tokenDisplay}</code></td>
1692
+ <td style="font-size:12px;">${deviceInfo}</td>
1693
+ <td style="font-size:12px;">${phone}</td>
1694
+ <td style="font-size:12px;">${createdAt}</td>
1695
+ <td style="font-size:12px;">${lastUsedAt}</td>
1696
+ <td style="font-size:12px;">${expiresAt}</td>
1697
+ <td style="font-size:12px;color:${remainingColor};font-weight:500;">${remainingTime}</td>
1698
+ <td style="color:${statusColor};font-weight:500;">${status}</td>
1699
+ <td>
1700
+ <button class="btn btn-sm btn-danger" onclick="app.deleteToken('${token.token}')" ${isExpired ? 'disabled' : ''}>吊销</button>
1701
+ </td>
1702
+ </tr>`;
1703
+ });
1704
+
1705
+ tbody.innerHTML = html;
1706
+ } catch (err) {
1707
+ console.error('[evol] Load Kite tokens failed:', err);
1708
+ document.getElementById('kite-tokens-tbody').innerHTML =
1709
+ `<tr><td colspan="9" style="text-align:center;padding:40px;color:#e74c3c;">加载失败: ${err.message}</td></tr>`;
1710
+ }
1711
+ }
1712
+
1713
+ async loadEvolTokens() {
1714
+ try {
1715
+ const result = await this.callRpc('evol.list_evol_tokens', {});
1716
+ const tokens = result.tokens || [];
1717
+
1718
+ const tbody = document.getElementById('evol-tokens-tbody');
1719
+ if (tokens.length === 0) {
1720
+ tbody.innerHTML = '<tr><td colspan="8" class="text-muted" style="text-align:center;padding:40px;">暂无 Evol Token</td></tr>';
1721
+ return;
1722
+ }
1723
+
1724
+ // 缩略显示函数
1725
+ const truncate = (str, headLen = 6, tailLen = 8) => {
1726
+ if (!str || str.length <= headLen + tailLen + 3) return str;
1727
+ return `${str.substring(0, headLen)}...${str.substring(str.length - tailLen)}`;
1728
+ };
1729
+
1730
+ // 时间格式化
1731
+ const formatTime = (isoStr) => {
1732
+ if (!isoStr) return '-';
1733
+ const date = new Date(isoStr);
1734
+ return date.toLocaleString('zh-CN', {
1735
+ month: '2-digit',
1736
+ day: '2-digit',
1737
+ hour: '2-digit',
1738
+ minute: '2-digit'
1739
+ });
1740
+ };
1741
+
1742
+ let html = '';
1743
+ tokens.forEach(token => {
1744
+ const tokenDisplay = truncate(token.token, 6, 8);
1745
+ const phone = token.phone || '-';
1746
+ const nickName = token.nickName || '-';
1747
+ const credits = `${token.credits || 0} / ${token.creditsLimit || 0}`;
1748
+ const vipInfo = `${token.vipTypeName || 'Unknown'}<br><span style="font-size:11px;color:#999;">剩余${token.vipRemainingDays || 0}天</span>`;
1749
+ const obtainedAt = formatTime(token.obtainedAt);
1750
+ const lastUsedAt = formatTime(token.lastUsedAt);
1751
+ const expiresAt = formatTime(token.expiresAt);
1752
+
1753
+ html += `<tr>
1754
+ <td><code style="font-size:11px;" title="${token.token}">${tokenDisplay}</code></td>
1755
+ <td style="font-size:12px;">${phone}</td>
1756
+ <td style="font-size:12px;">${nickName}</td>
1757
+ <td style="font-size:12px;">${credits}</td>
1758
+ <td style="font-size:12px;">${vipInfo}</td>
1759
+ <td style="font-size:12px;">${obtainedAt}</td>
1760
+ <td style="font-size:12px;">${lastUsedAt}</td>
1761
+ <td style="font-size:12px;">${expiresAt}</td>
1762
+ </tr>`;
1763
+ });
1764
+
1765
+ tbody.innerHTML = html;
1766
+ } catch (err) {
1767
+ console.error('[evol] Load Evol tokens failed:', err);
1768
+ document.getElementById('evol-tokens-tbody').innerHTML =
1769
+ `<tr><td colspan="8" style="text-align:center;padding:40px;color:#e74c3c;">加载失败: ${err.message}</td></tr>`;
1770
+ }
1771
+ }
1772
+
1773
+ async deleteToken(token) {
1774
+ const confirmed = await window.dialog.confirm('确定要吊销此 Token 吗?删除后该设备将无法访问。', '吊销 Token');
1775
+ if (!confirmed) return;
1776
+
1777
+ try {
1778
+ await this.callRpc('evol.revoke_token', { token });
1779
+ window.dialog.success('Token 已成功吊销');
1780
+ this.loadTokens();
1781
+ } catch (err) {
1782
+ window.dialog.error('吊销失败: ' + err.message);
1783
+ }
1784
+ }
1785
+
1786
+ async refreshTokens() {
1787
+ this.loadTokens();
1788
+ }
1789
+
1790
+ showToast(message, type = 'info') {
1791
+ // 使用全局 showMessage 函数
1792
+ showMessage(message, type);
1793
+ }
1794
+ }
1795
+
1796
+
1797
+ // Global app instance
1798
+ let app;
1799
+
1800
+ // Initialize app on load
1801
+ document.addEventListener('DOMContentLoaded', () => {
1802
+ app = new EvolApp();
1803
+ app.init();
1804
+
1805
+ // Login form handlers
1806
+ document.getElementById('btn-send-code').addEventListener('click', async () => {
1807
+ const phone = document.getElementById('login-phone').value.trim();
1808
+
1809
+ if (!phone) {
1810
+ showMessage('请输入手机号', 'error');
1811
+ return;
1812
+ }
1813
+
1814
+ if (!/^1[3-9]\d{9}$/.test(phone)) {
1815
+ showMessage('请输入有效的中国大陆手机号(11位数字,以1开头)', 'error');
1816
+ return;
1817
+ }
1818
+
1819
+ const btn = document.getElementById('btn-send-code');
1820
+ btn.disabled = true;
1821
+ btn.textContent = '发送中...';
1822
+
1823
+ try {
1824
+ const data = await app.sendSMS(phone);
1825
+ console.log('SMS API response:', data);
1826
+
1827
+ if (data.success) {
1828
+ showMessage('验证码已发送,请查收短信', 'success');
1829
+ // 焦点移到验证码输入框
1830
+ document.getElementById('login-code').focus();
1831
+ let countdown = 60;
1832
+ const timer = setInterval(() => {
1833
+ countdown--;
1834
+ btn.textContent = countdown + '秒后重试';
1835
+ if (countdown <= 0) {
1836
+ clearInterval(timer);
1837
+ btn.disabled = false;
1838
+ btn.textContent = '发送验证码';
1839
+ }
1840
+ }, 1000);
1841
+ } else {
1842
+ const errorMsg = data.msg || data.message || '发送失败,请稍后重试';
1843
+ showMessage('发送失败: ' + errorMsg, 'error');
1844
+ console.error('SMS API error:', data);
1845
+ btn.disabled = false;
1846
+ btn.textContent = '发送验证码';
1847
+ }
1848
+ } catch (err) {
1849
+ console.error('SMS request error:', err);
1850
+ showMessage('网络错误: ' + err.message, 'error');
1851
+ btn.disabled = false;
1852
+ btn.textContent = '发送验证码';
1853
+ }
1854
+ });
1855
+
1856
+ document.getElementById('btn-login').addEventListener('click', async () => {
1857
+ const phone = document.getElementById('login-phone').value.trim();
1858
+ const code = document.getElementById('login-code').value.trim();
1859
+
1860
+ if (!phone || !code) {
1861
+ showMessage('请输入手机号和验证码', 'error');
1862
+ return;
1863
+ }
1864
+
1865
+ const btn = document.getElementById('btn-login');
1866
+ btn.disabled = true;
1867
+ btn.textContent = '登录中...';
1868
+
1869
+ try {
1870
+ const data = await app.login(phone, code);
1871
+ if (data.success) {
1872
+ showMessage('登录成功', 'success');
1873
+ } else {
1874
+ showMessage(data.msg || '登录失败', 'error');
1875
+ btn.disabled = false;
1876
+ btn.textContent = '登录';
1877
+ }
1878
+ } catch (err) {
1879
+ showMessage('网络错误', 'error');
1880
+ btn.disabled = false;
1881
+ btn.textContent = '登录';
1882
+ }
1883
+ });
1884
+
1885
+ document.getElementById('btn-logout').addEventListener('click', () => {
1886
+ app.logout();
1887
+ });
1888
+
1889
+ // Module management event listeners
1890
+ document.getElementById('btn-toggle-console')?.addEventListener('click', () => {
1891
+ app.toggleConsole();
1892
+ });
1893
+
1894
+ document.getElementById('btn-clear-console')?.addEventListener('click', () => {
1895
+ app.clearConsole();
1896
+ });
1897
+
1898
+ document.getElementById('btn-restart-kite')?.addEventListener('click', () => {
1899
+ app.restartKite();
1900
+ });
1901
+
1902
+ document.getElementById('btn-test-registry')?.addEventListener('click', () => {
1903
+ app.runRegistryTest();
1904
+ });
1905
+
1906
+ document.getElementById('btn-clear-test-output')?.addEventListener('click', () => {
1907
+ app.clearTestOutput();
1908
+ });
1909
+
1910
+ document.getElementById('btn-module-back')?.addEventListener('click', () => {
1911
+ app.loadModules();
1912
+ });
1913
+
1914
+ document.getElementById('btn-reset-defaults')?.addEventListener('click', () => {
1915
+ app.resetModuleDefaults();
1916
+ });
1917
+
1918
+ // Token management event listeners
1919
+ document.getElementById('btn-refresh-tokens')?.addEventListener('click', () => {
1920
+ app.refreshTokens();
1921
+ });
1922
+
1923
+ // Tab switching event listeners
1924
+ document.querySelectorAll('.tab').forEach(tab => {
1925
+ tab.addEventListener('click', () => {
1926
+ // 移除所有 active 类
1927
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
1928
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
1929
+
1930
+ // 添加 active 类到当前标签
1931
+ tab.classList.add('active');
1932
+ const tabName = tab.dataset.tab;
1933
+ document.getElementById(`tab-${tabName}`).classList.add('active');
1934
+
1935
+ // 加载对应的数据
1936
+ if (tabName === 'kite') {
1937
+ app.loadKiteTokens();
1938
+ } else if (tabName === 'evol') {
1939
+ app.loadEvolTokens();
1940
+ }
1941
+ });
1942
+ });
1943
+ });
1944
+
1945
+ function showMessage(msg, type) {
1946
+ const el = document.getElementById('login-message');
1947
+ el.textContent = msg;
1948
+ el.className = type === 'error' ? 'error-msg' : 'success-msg';
1949
+ }