@agentunion/kite 1.3.1 → 1.4.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 (78) hide show
  1. package/CHANGELOG.md +287 -1
  2. package/cli.js +76 -0
  3. package/extensions/agents/assistant/entry.py +111 -1
  4. package/extensions/agents/assistant/server.py +263 -197
  5. package/extensions/channels/acp_channel/entry.py +111 -1
  6. package/extensions/channels/acp_channel/module.md +23 -22
  7. package/extensions/channels/acp_channel/server.py +263 -197
  8. package/extensions/event_hub_bench/entry.py +107 -1
  9. package/extensions/services/backup/entry.py +408 -72
  10. package/extensions/services/backup/module.md +24 -22
  11. package/extensions/services/model_service/entry.py +255 -71
  12. package/extensions/services/model_service/module.md +21 -22
  13. package/extensions/services/watchdog/entry.py +344 -90
  14. package/extensions/services/watchdog/monitor.py +237 -21
  15. package/extensions/services/web/WEBSOCKET_STATUS.md +143 -0
  16. package/extensions/services/web/config_example.py +35 -0
  17. package/extensions/services/web/config_loader.py +110 -0
  18. package/extensions/services/web/entry.py +114 -26
  19. package/extensions/services/web/module.md +35 -24
  20. package/extensions/services/web/pairing.py +250 -0
  21. package/extensions/services/web/pairing_codes.jsonl +16 -0
  22. package/extensions/services/web/relay.py +643 -0
  23. package/extensions/services/web/relay_config.json5 +67 -0
  24. package/extensions/services/web/routes/routes_management_ws.py +127 -0
  25. package/extensions/services/web/routes/routes_rpc.py +89 -0
  26. package/extensions/services/web/routes/routes_test.py +61 -0
  27. package/extensions/services/web/server.py +445 -99
  28. package/extensions/services/web/static/css/style.css +138 -2
  29. package/extensions/services/web/static/index.html +295 -2
  30. package/extensions/services/web/static/js/app.js +1579 -5
  31. package/extensions/services/web/static/js/kernel-client-example.js +161 -0
  32. package/extensions/services/web/static/js/kernel-client.js +383 -0
  33. package/extensions/services/web/static/js/registry-tests.js +558 -0
  34. package/extensions/services/web/static/js/token-manager.js +175 -0
  35. package/extensions/services/web/static/pairing.html +248 -0
  36. package/extensions/services/web/static/test_registry.html +262 -0
  37. package/extensions/services/web/web_config.json5 +29 -0
  38. package/kernel/entry.py +120 -32
  39. package/kernel/event_hub.py +159 -16
  40. package/kernel/module.md +36 -33
  41. package/kernel/registry_store.py +70 -20
  42. package/kernel/rpc_router.py +134 -57
  43. package/kernel/server.py +292 -15
  44. package/kite_cli/__init__.py +3 -0
  45. package/kite_cli/__main__.py +5 -0
  46. package/kite_cli/commands/__init__.py +1 -0
  47. package/kite_cli/commands/clean.py +101 -0
  48. package/kite_cli/commands/doctor.py +35 -0
  49. package/kite_cli/commands/history.py +111 -0
  50. package/kite_cli/commands/info.py +96 -0
  51. package/kite_cli/commands/install.py +313 -0
  52. package/kite_cli/commands/list.py +143 -0
  53. package/kite_cli/commands/log.py +81 -0
  54. package/kite_cli/commands/rollback.py +88 -0
  55. package/kite_cli/commands/search.py +73 -0
  56. package/kite_cli/commands/uninstall.py +85 -0
  57. package/kite_cli/commands/update.py +118 -0
  58. package/kite_cli/core/__init__.py +1 -0
  59. package/kite_cli/core/checker.py +142 -0
  60. package/kite_cli/core/dependency.py +229 -0
  61. package/kite_cli/core/downloader.py +209 -0
  62. package/kite_cli/core/install_info.py +40 -0
  63. package/kite_cli/core/tool_installer.py +397 -0
  64. package/kite_cli/core/validator.py +78 -0
  65. package/kite_cli/main.py +289 -0
  66. package/kite_cli/utils/__init__.py +1 -0
  67. package/kite_cli/utils/i18n.py +252 -0
  68. package/kite_cli/utils/interactive.py +63 -0
  69. package/kite_cli/utils/operation_log.py +77 -0
  70. package/kite_cli/utils/paths.py +34 -0
  71. package/kite_cli/utils/version.py +308 -0
  72. package/launcher/count_lines.py +34 -0
  73. package/launcher/entry.py +905 -166
  74. package/launcher/logging_setup.py +104 -0
  75. package/launcher/module.md +37 -37
  76. package/launcher/process_manager.py +12 -1
  77. package/package.json +2 -1
  78. package/scripts/plan_manager.py +315 -0
@@ -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;` +
@@ -97,12 +270,17 @@ function showToast(message, type = 'info') {
97
270
  // ============================================================
98
271
  // Navigation / Router
99
272
  // ============================================================
100
- const pages = ['dashboard', 'calls', 'sms', 'contacts', 'config', 'bluetooth', 'voicechat', 'devlog'];
273
+ const pages = ['dashboard', 'calls', 'sms', 'contacts', 'config', 'bluetooth', 'modules', 'voicechat', 'devlog'];
101
274
  let currentPage = '';
102
275
 
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}`);
@@ -128,6 +306,7 @@ function navigate(page) {
128
306
  case 'contacts': loadContacts(); break;
129
307
  case 'config': loadConfig(); break;
130
308
  case 'bluetooth': loadBluetooth(); break;
309
+ case 'modules': loadModules(); break;
131
310
  case 'voicechat': loadVoiceChatConfig(); break;
132
311
  case 'devlog': loadDevLog(); break;
133
312
  }
@@ -3084,6 +3263,12 @@ function _setVal(id, value) {
3084
3263
  if (el) el.value = value != null ? String(value) : '';
3085
3264
  }
3086
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
+
3087
3272
  /** Set a <select> value, adding the option dynamically if it doesn't exist. */
3088
3273
  function _setSelectVal(id, value) {
3089
3274
  const el = document.getElementById(id);
@@ -4202,6 +4387,1354 @@ function closeDevLogArchiveModal() {
4202
4387
  document.getElementById('devlog-archive-modal')?.classList.add('hidden');
4203
4388
  }
4204
4389
 
4390
+ // ============================================================
4391
+ // Modules Page
4392
+ // ============================================================
4393
+ let _moduleSaveTimer = null;
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
+ }
4582
+
4583
+ async function loadModules() {
4584
+ // Reset to list view
4585
+ document.getElementById('modules-list-header')?.classList.remove('hidden');
4586
+ document.getElementById('modules-table')?.closest('.panel')?.classList.remove('hidden');
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();
4596
+
4597
+ try {
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
+ };
4785
+ } catch (err) {
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();
4944
+ }
4945
+ }
4946
+
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;
5029
+
5030
+ if (!modules.length) {
5031
+ tbody.innerHTML = '<tr><td colspan="9" class="text-muted" style="text-align:center;padding:40px;">未发现模块</td></tr>';
5032
+ return;
5033
+ }
5034
+
5035
+ tbody.innerHTML = modules.map(m => {
5036
+ const stateClass = m.state === 'enabled' ? 'enabled' : m.state === 'manual' ? 'manual' : 'disabled';
5037
+ const typeClass = m.type || 'unknown';
5038
+ const displayName = m.display_name || m.name;
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>`;
5084
+ }).join('');
5085
+ }
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
+
5418
+ async function openModuleDetail(name) {
5419
+ _currentModuleName = name;
5420
+
5421
+ // Switch view
5422
+ document.getElementById('modules-list-header')?.classList.add('hidden');
5423
+ document.getElementById('modules-table')?.closest('.panel')?.classList.add('hidden');
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);
5432
+
5433
+ try {
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);
5449
+
5450
+ // Header
5451
+ document.getElementById('module-detail-name').textContent = mod.display_name || mod.name;
5452
+ const badge = document.getElementById('module-detail-type-badge');
5453
+ if (badge) {
5454
+ badge.textContent = mod.type || '?';
5455
+ badge.className = `module-type-badge type-${mod.type || 'unknown'}`;
5456
+ }
5457
+
5458
+ // 【区块1:基本信息与元数据】
5459
+ _setVal('mod-source-path', mod.source_path || '');
5460
+
5461
+ // 基础字段(现在可编辑)
5462
+ _setVal('mod-meta-name', mod.name || '');
5463
+ _setVal('mod-meta-type', mod.type || '');
5464
+ _setVal('mod-meta-runtime', mod.runtime || '');
5465
+ _setVal('mod-meta-entry', mod.entry || '');
5466
+
5467
+ // 其他字段
5468
+ _setVal('mod-meta-display-name', mod.display_name || '');
5469
+ _setVal('mod-meta-version', mod.version || '');
5470
+ _setVal('mod-meta-state', mod.state || 'enabled');
5471
+ _setVal('mod-meta-port', mod.preferred_port != null ? mod.preferred_port : '');
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
5482
+ const monitorEl = document.getElementById('mod-meta-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);
5491
+
5492
+ // 【区块2:模块配置文件】
5493
+ const configSection = document.getElementById('module-config-section');
5494
+ const configTree = document.getElementById('module-config-tree');
5495
+ if (mod.has_config && mod.config) {
5496
+ configSection?.classList.remove('hidden');
5497
+ if (configTree) {
5498
+ configTree.innerHTML = '';
5499
+ renderConfigTree(mod.config, configTree, '');
5500
+ }
5501
+ } else {
5502
+ configSection?.classList.add('hidden');
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);
5527
+ } catch (err) {
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');
5552
+ }
5553
+ }
5554
+
5555
+ function renderConfigTree(obj, container, pathPrefix) {
5556
+ if (!obj || typeof obj !== 'object') return;
5557
+
5558
+ // Secret field patterns
5559
+ const secretKeys = /(?:password|secret|token|api_key|access_key|private_key)/i;
5560
+
5561
+ Object.entries(obj).forEach(([key, val]) => {
5562
+ const fullPath = pathPrefix ? `${pathPrefix}.${key}` : key;
5563
+
5564
+ if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
5565
+ // Nested object → collapsible group
5566
+ const group = document.createElement('div');
5567
+ group.className = 'config-tree-group';
5568
+
5569
+ const header = document.createElement('div');
5570
+ header.className = 'config-tree-key';
5571
+ header.textContent = key;
5572
+ header.addEventListener('click', () => {
5573
+ group.classList.toggle('collapsed');
5574
+ });
5575
+
5576
+ const nested = document.createElement('div');
5577
+ nested.className = 'config-tree-nested';
5578
+
5579
+ group.appendChild(header);
5580
+ group.appendChild(nested);
5581
+ container.appendChild(group);
5582
+
5583
+ renderConfigTree(val, nested, fullPath);
5584
+ } else {
5585
+ // Leaf value → form input
5586
+ const row = document.createElement('div');
5587
+ row.className = 'config-tree-leaf';
5588
+
5589
+ const label = document.createElement('label');
5590
+ label.className = 'config-tree-label';
5591
+ label.textContent = key;
5592
+
5593
+ let input;
5594
+ if (typeof val === 'boolean') {
5595
+ input = document.createElement('input');
5596
+ input.type = 'checkbox';
5597
+ input.checked = val;
5598
+ input.className = 'config-tree-checkbox';
5599
+ } else if (typeof val === 'number') {
5600
+ input = document.createElement('input');
5601
+ input.type = 'number';
5602
+ input.value = val;
5603
+ input.step = 'any';
5604
+ input.className = 'config-tree-input';
5605
+ } else if (Array.isArray(val)) {
5606
+ input = document.createElement('input');
5607
+ input.type = 'text';
5608
+ input.value = val.join(', ');
5609
+ input.className = 'config-tree-input';
5610
+ input.placeholder = 'comma separated';
5611
+ } else {
5612
+ input = document.createElement('input');
5613
+ input.type = secretKeys.test(key) ? 'password' : 'text';
5614
+ input.value = val != null ? String(val) : '';
5615
+ input.className = 'config-tree-input';
5616
+ }
5617
+
5618
+ input.dataset.path = fullPath;
5619
+ input.dataset.origType = Array.isArray(val) ? 'array' : typeof val;
5620
+
5621
+ // Debounced save on input/change
5622
+ const saveEvent = (input.type === 'checkbox' || input.tagName === 'SELECT') ? 'change' : 'input';
5623
+ input.addEventListener(saveEvent, _debouncedSaveModule);
5624
+
5625
+ row.appendChild(label);
5626
+ row.appendChild(input);
5627
+ container.appendChild(row);
5628
+ }
5629
+ });
5630
+ }
5631
+
5632
+ function _buildModuleConfigObject() {
5633
+ const tree = document.getElementById('module-config-tree');
5634
+ if (!tree) return null;
5635
+
5636
+ const obj = {};
5637
+ tree.querySelectorAll('[data-path]').forEach(input => {
5638
+ const path = input.dataset.path;
5639
+ const origType = input.dataset.origType;
5640
+ let val;
5641
+
5642
+ if (input.type === 'checkbox') {
5643
+ val = input.checked;
5644
+ } else if (origType === 'number') {
5645
+ val = input.value === '' ? 0 : Number(input.value);
5646
+ } else if (origType === 'array') {
5647
+ val = input.value.split(',').map(s => s.trim()).filter(Boolean);
5648
+ } else {
5649
+ val = input.value;
5650
+ }
5651
+
5652
+ // Build nested object from dot path
5653
+ const parts = path.split('.');
5654
+ let target = obj;
5655
+ for (let i = 0; i < parts.length - 1; i++) {
5656
+ if (!(parts[i] in target)) target[parts[i]] = {};
5657
+ target = target[parts[i]];
5658
+ }
5659
+ target[parts[parts.length - 1]] = val;
5660
+ });
5661
+
5662
+ return obj;
5663
+ }
5664
+
5665
+ function _debouncedSaveModule() {
5666
+ clearTimeout(_moduleSaveTimer);
5667
+ _moduleSaveTimer = setTimeout(async () => {
5668
+ _showModuleSaveStatus('saving');
5669
+ try {
5670
+ // Collect metadata from all [data-field] inputs in #module-detail
5671
+ const meta = {};
5672
+ document.querySelectorAll('#module-detail [data-field]').forEach(el => {
5673
+ const field = el.dataset.field;
5674
+ let val;
5675
+ if (field === 'preferred_port') {
5676
+ val = el.value === '' ? null : parseInt(el.value);
5677
+ } else if (field === 'monitor') {
5678
+ val = el.value === 'true';
5679
+ } else {
5680
+ val = el.value || null;
5681
+ }
5682
+ if (val !== null) meta[field] = val;
5683
+ });
5684
+
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
+
5693
+ const metaPromise = Object.keys(meta).length > 0
5694
+ ? kernelClient.call(`${rpc.module}.${rpc.method}`, { ...baseParams, metadata: meta })
5695
+ : Promise.resolve();
5696
+
5697
+ // Save config if visible
5698
+ const configSection = document.getElementById('module-config-section');
5699
+ const configObj = _buildModuleConfigObject();
5700
+ const configPromise = configSection && !configSection.classList.contains('hidden') && configObj
5701
+ ? kernelClient.call(`${rpc.module}.${rpc.method}`, { ...baseParams, config: configObj })
5702
+ : Promise.resolve();
5703
+
5704
+ await Promise.all([metaPromise, configPromise]);
5705
+
5706
+ _showModuleSaveStatus('saved');
5707
+ } catch (err) {
5708
+ _showModuleSaveStatus('error');
5709
+ showToast('保存失败: ' + err.message, 'error');
5710
+ }
5711
+ }, 800);
5712
+ }
5713
+
5714
+ function _showModuleSaveStatus(status) {
5715
+ const el = document.getElementById('module-save-status');
5716
+ if (!el) return;
5717
+ el.classList.remove('saving', 'saved', 'error', 'fade-out');
5718
+ if (status === 'saving') {
5719
+ el.textContent = '保存中...';
5720
+ el.classList.add('saving');
5721
+ } else if (status === 'saved') {
5722
+ el.textContent = '已保存';
5723
+ el.classList.add('saved');
5724
+ setTimeout(() => {
5725
+ el.classList.add('fade-out');
5726
+ setTimeout(() => { el.textContent = ''; el.classList.remove('saved', 'fade-out'); }, 300);
5727
+ }, 2000);
5728
+ } else if (status === 'error') {
5729
+ el.textContent = '保存失败';
5730
+ el.classList.add('error');
5731
+ setTimeout(() => {
5732
+ el.classList.add('fade-out');
5733
+ setTimeout(() => { el.textContent = ''; el.classList.remove('error', 'fade-out'); }, 300);
5734
+ }, 3000);
5735
+ }
5736
+ }
5737
+
4205
5738
  // ============================================================
4206
5739
  // Initialization
4207
5740
  // ============================================================
@@ -4663,9 +6196,50 @@ document.addEventListener('DOMContentLoaded', () => {
4663
6196
  if (dateEl) loadDevLogArchiveDetail(dateEl.dataset.date);
4664
6197
  });
4665
6198
 
6199
+ // Modules page
6200
+ const btnModuleBack = document.getElementById('btn-module-back');
6201
+ if (btnModuleBack) btnModuleBack.addEventListener('click', loadModules);
6202
+
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 = '';
6224
+ });
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
+
4666
6231
  // Navigate to last active page (or dashboard)
4667
6232
  navigate(localStorage.getItem('activePage') || 'dashboard');
4668
6233
 
4669
6234
  // Initial status bar update
4670
6235
  updateStatusBar();
4671
6236
  });
6237
+
6238
+ // ============================================================
6239
+ // Registry Tests
6240
+ // ============================================================
6241
+
6242
+ async function runRegistryTests() {
6243
+ // 调用 registry-tests.js 中的全面测试套件
6244
+ await runAllTests();
6245
+ }