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