@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
|
@@ -8,13 +8,14 @@ Connects to Kernel via WebSocket JSON-RPC 2.0 for event publishing and subscript
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
|
+
import os
|
|
11
12
|
import time
|
|
12
13
|
import uuid
|
|
13
14
|
from datetime import datetime, timezone
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
|
|
16
17
|
import websockets
|
|
17
|
-
from fastapi import FastAPI
|
|
18
|
+
from fastapi import FastAPI, WebSocket
|
|
18
19
|
from fastapi.staticfiles import StaticFiles
|
|
19
20
|
|
|
20
21
|
from vendor import config as cfg
|
|
@@ -30,11 +31,26 @@ from routes.routes_contacts import router as contacts_router
|
|
|
30
31
|
from routes.routes_stats import router as stats_router
|
|
31
32
|
from routes.routes_voicechat import router as voicechat_router
|
|
32
33
|
from routes.routes_devlog import router as devlog_router
|
|
33
|
-
from routes.
|
|
34
|
+
from routes.routes_rpc import router as rpc_router, set_web_server
|
|
35
|
+
from routes.routes_management_ws import router as management_ws_router, broadcast_event
|
|
36
|
+
from routes.routes_test import router as test_router
|
|
37
|
+
|
|
38
|
+
from config_loader import load_business_configs
|
|
39
|
+
from pairing import PairingManager
|
|
40
|
+
from relay import KernelRelay
|
|
34
41
|
|
|
35
42
|
logger = logging.getLogger(__name__)
|
|
36
43
|
|
|
37
44
|
|
|
45
|
+
# System broadcast events (received by all modules, may not need handling)
|
|
46
|
+
SYSTEM_BROADCAST_EVENTS = {
|
|
47
|
+
"module.ready", "module.registered", "module.started", "module.stopped",
|
|
48
|
+
"module.crashed", "module.exiting", "module.offline",
|
|
49
|
+
"module.shutdown.ack", "module.shutdown.ready",
|
|
50
|
+
"system.ready", "registry.updated",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
38
54
|
class WebServer:
|
|
39
55
|
|
|
40
56
|
def __init__(self, token: str = "", kernel_port: int = 0,
|
|
@@ -48,8 +64,10 @@ class WebServer:
|
|
|
48
64
|
self._test_task: asyncio.Task | None = None
|
|
49
65
|
self._ws: object | None = None
|
|
50
66
|
self._shutting_down = False
|
|
67
|
+
self._exit_code = 0 # Exit code for main() to use
|
|
51
68
|
self._uvicorn_server = None # set by entry.py for graceful shutdown
|
|
52
69
|
self._start_time = time.time()
|
|
70
|
+
self._rpc_futures = {} # Store pending RPC request futures
|
|
53
71
|
self.bt_manager: BluetoothManager | None = None
|
|
54
72
|
self.task_manager: TaskManager | None = None
|
|
55
73
|
self.app = self._create_app()
|
|
@@ -60,6 +78,40 @@ class WebServer:
|
|
|
60
78
|
|
|
61
79
|
@app.on_event("startup")
|
|
62
80
|
async def _startup():
|
|
81
|
+
# Load business configurations
|
|
82
|
+
module_dir = Path(__file__).parent
|
|
83
|
+
business_configs = load_business_configs(str(module_dir))
|
|
84
|
+
|
|
85
|
+
# Get relay service config
|
|
86
|
+
relay_business = business_configs.get('relay_service')
|
|
87
|
+
if relay_business:
|
|
88
|
+
relay_config = relay_business['config']
|
|
89
|
+
|
|
90
|
+
# Initialize pairing manager
|
|
91
|
+
auth_config = relay_config['auth']
|
|
92
|
+
pairing_file = module_dir / auth_config['pairing_code_file']
|
|
93
|
+
pairing_manager = PairingManager(
|
|
94
|
+
pairing_file=str(pairing_file),
|
|
95
|
+
code_length=auth_config['pairing_code_length'],
|
|
96
|
+
token_expiry=auth_config['token_expiry']
|
|
97
|
+
)
|
|
98
|
+
app.state.pairing_manager = pairing_manager
|
|
99
|
+
print(f"[web] Pairing manager initialized")
|
|
100
|
+
|
|
101
|
+
# Initialize relay service
|
|
102
|
+
relay_service = KernelRelay(
|
|
103
|
+
kernel_host="127.0.0.1",
|
|
104
|
+
kernel_port=server.kernel_port,
|
|
105
|
+
kernel_token=server.token,
|
|
106
|
+
base_module_id=relay_config['relay']['base_module_id'],
|
|
107
|
+
reconnect_timeout=relay_config['relay']['reconnect_timeout'],
|
|
108
|
+
permissions=relay_config['permissions'],
|
|
109
|
+
pairing_manager=pairing_manager,
|
|
110
|
+
web_server=server # 传递 web server 实例
|
|
111
|
+
)
|
|
112
|
+
app.state.relay_service = relay_service
|
|
113
|
+
print(f"[web] Relay service initialized")
|
|
114
|
+
|
|
63
115
|
# Load configuration
|
|
64
116
|
cfg.load_config()
|
|
65
117
|
load_err = cfg.get_load_error()
|
|
@@ -142,12 +194,63 @@ class WebServer:
|
|
|
142
194
|
app.include_router(stats_router, prefix="/api")
|
|
143
195
|
app.include_router(voicechat_router) # no prefix (has own /ws/ and /api/ paths)
|
|
144
196
|
app.include_router(devlog_router, prefix="/api")
|
|
145
|
-
app.include_router(
|
|
197
|
+
app.include_router(rpc_router, prefix="/api")
|
|
198
|
+
app.include_router(test_router, prefix="/api")
|
|
199
|
+
app.include_router(management_ws_router) # /ws/management
|
|
200
|
+
|
|
201
|
+
# Relay WebSocket endpoint
|
|
202
|
+
@app.websocket("/ws/relay")
|
|
203
|
+
async def relay_endpoint(ws: WebSocket):
|
|
204
|
+
"""Kernel 中转服务 WebSocket 端点"""
|
|
205
|
+
relay_service = app.state.relay_service
|
|
206
|
+
if relay_service:
|
|
207
|
+
await relay_service.handle_client(ws)
|
|
208
|
+
else:
|
|
209
|
+
await ws.close(code=1011, reason="Relay service not initialized")
|
|
210
|
+
|
|
211
|
+
# Set web server reference for RPC forwarding
|
|
212
|
+
set_web_server(server)
|
|
146
213
|
|
|
147
214
|
# Serve frontend static files
|
|
215
|
+
# IMPORTANT: Do NOT use app.mount("/", ...) as it will intercept WebSocket routes
|
|
216
|
+
# Instead, explicitly serve HTML files and static assets
|
|
148
217
|
static_dir = Path(__file__).parent / "static"
|
|
149
218
|
if static_dir.exists():
|
|
150
|
-
|
|
219
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
220
|
+
|
|
221
|
+
@app.get("/")
|
|
222
|
+
async def serve_index():
|
|
223
|
+
"""Serve index.html at root path."""
|
|
224
|
+
index_path = static_dir / "index.html"
|
|
225
|
+
if index_path.exists():
|
|
226
|
+
return FileResponse(index_path)
|
|
227
|
+
return {"message": "Kite Web Management"}
|
|
228
|
+
|
|
229
|
+
@app.get("/pairing.html")
|
|
230
|
+
async def serve_pairing():
|
|
231
|
+
"""Serve pairing.html."""
|
|
232
|
+
pairing_path = static_dir / "pairing.html"
|
|
233
|
+
if pairing_path.exists():
|
|
234
|
+
return FileResponse(pairing_path)
|
|
235
|
+
return JSONResponse({"error": "Not found"}, status_code=404)
|
|
236
|
+
|
|
237
|
+
# Mount static files at /static prefix to avoid route conflicts
|
|
238
|
+
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
|
239
|
+
|
|
240
|
+
# Also serve common static paths directly from root for backward compatibility
|
|
241
|
+
@app.get("/js/{file_path:path}")
|
|
242
|
+
async def serve_js(file_path: str):
|
|
243
|
+
file = static_dir / "js" / file_path
|
|
244
|
+
if file.exists() and file.is_file():
|
|
245
|
+
return FileResponse(file)
|
|
246
|
+
return JSONResponse({"error": "Not found"}, status_code=404)
|
|
247
|
+
|
|
248
|
+
@app.get("/css/{file_path:path}")
|
|
249
|
+
async def serve_css(file_path: str):
|
|
250
|
+
file = static_dir / "css" / file_path
|
|
251
|
+
if file.exists() and file.is_file():
|
|
252
|
+
return FileResponse(file)
|
|
253
|
+
return JSONResponse({"error": "Not found"}, status_code=404)
|
|
151
254
|
|
|
152
255
|
return app
|
|
153
256
|
|
|
@@ -165,6 +268,7 @@ class WebServer:
|
|
|
165
268
|
retry_delay = 0.3 # reset on successful connection
|
|
166
269
|
attempt = 0
|
|
167
270
|
except asyncio.CancelledError:
|
|
271
|
+
print(f"[web] WS loop cancelled")
|
|
168
272
|
return
|
|
169
273
|
except Exception as e:
|
|
170
274
|
attempt += 1
|
|
@@ -173,13 +277,25 @@ class WebServer:
|
|
|
173
277
|
code = e.rcvd.code if hasattr(e.rcvd, 'code') else 0
|
|
174
278
|
if code in (4001, 4003):
|
|
175
279
|
print(f"[web] Kernel 认证失败 (code {code}),退出")
|
|
176
|
-
|
|
280
|
+
self._exit_code = 1
|
|
281
|
+
self._shutting_down = True
|
|
282
|
+
if self._uvicorn_server:
|
|
283
|
+
self._uvicorn_server.should_exit = True
|
|
284
|
+
return
|
|
177
285
|
if attempt >= max_retries:
|
|
178
286
|
print(f"[web] Kernel 重连失败 {max_retries} 次,退出")
|
|
179
|
-
|
|
287
|
+
self._exit_code = 1
|
|
288
|
+
self._shutting_down = True
|
|
289
|
+
if self._uvicorn_server:
|
|
290
|
+
self._uvicorn_server.should_exit = True
|
|
291
|
+
return
|
|
292
|
+
if self._shutting_down:
|
|
293
|
+
print(f"[web] Shutting down, not retrying connection")
|
|
294
|
+
return
|
|
180
295
|
print(f"[web] Kernel connection error: {e}, retrying in {retry_delay:.1f}s ({attempt}/{max_retries})")
|
|
181
296
|
self._ws = None
|
|
182
297
|
if self._shutting_down:
|
|
298
|
+
print(f"[web] Shutting down, exiting WS loop")
|
|
183
299
|
return
|
|
184
300
|
await asyncio.sleep(retry_delay)
|
|
185
301
|
retry_delay = min(retry_delay * 2, max_delay)
|
|
@@ -188,86 +304,135 @@ class WebServer:
|
|
|
188
304
|
"""Single WebSocket session: connect, register, subscribe, receive loop."""
|
|
189
305
|
url = f"ws://127.0.0.1:{self.kernel_port}/ws?token={self.token}&id=web"
|
|
190
306
|
print(f"[web] WS connecting to Kernel")
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
|
|
195
|
-
print(f"[web] Connected to Kernel{elapsed_str}")
|
|
196
|
-
|
|
197
|
-
# Subscribe to events
|
|
198
|
-
await self._rpc_call(ws, "event.subscribe", {
|
|
199
|
-
"events": [
|
|
200
|
-
"module.started",
|
|
201
|
-
"module.stopped",
|
|
202
|
-
"module.shutdown",
|
|
203
|
-
],
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
# Register to Kernel Registry via RPC
|
|
207
|
-
await self._rpc_call(ws, "registry.register", {
|
|
208
|
-
"module_id": "web",
|
|
209
|
-
"module_type": "service",
|
|
210
|
-
"api_endpoint": f"http://127.0.0.1:{self.port}",
|
|
211
|
-
"health_endpoint": "/health",
|
|
212
|
-
"events_publish": {
|
|
213
|
-
"web.test": {"description": "Test event from web module"},
|
|
214
|
-
"web.started": {"description": "Web UI started with access URL"},
|
|
215
|
-
},
|
|
216
|
-
"events_subscribe": [
|
|
217
|
-
"module.started",
|
|
218
|
-
"module.stopped",
|
|
219
|
-
"module.shutdown",
|
|
220
|
-
],
|
|
221
|
-
})
|
|
222
|
-
print(f"[web] Registered to Kernel{elapsed_str}")
|
|
223
|
-
|
|
224
|
-
# Send module.ready (every reconnect, not just first time)
|
|
225
|
-
if not self._shutting_down:
|
|
226
|
-
await self._rpc_call(ws, "event.publish", {
|
|
227
|
-
"event_id": str(uuid.uuid4()),
|
|
228
|
-
"event": "module.ready",
|
|
229
|
-
"data": {
|
|
230
|
-
"module_id": "web",
|
|
231
|
-
"graceful_shutdown": True,
|
|
232
|
-
},
|
|
233
|
-
})
|
|
307
|
+
try:
|
|
308
|
+
async with websockets.connect(url, open_timeout=5, ping_interval=None, close_timeout=10) as ws:
|
|
309
|
+
self._ws = ws
|
|
234
310
|
elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
|
|
235
311
|
elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
|
|
236
|
-
print(f"[web]
|
|
237
|
-
|
|
238
|
-
#
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
"
|
|
245
|
-
"
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
|
|
312
|
+
print(f"[web] Connected to Kernel{elapsed_str}")
|
|
313
|
+
|
|
314
|
+
# Subscribe to events
|
|
315
|
+
await self._rpc_call(ws, "event.subscribe", {
|
|
316
|
+
"events": [
|
|
317
|
+
"module.started",
|
|
318
|
+
"module.stopped",
|
|
319
|
+
"module.crashed",
|
|
320
|
+
"module.ready",
|
|
321
|
+
"module.exiting",
|
|
322
|
+
"module.shutdown",
|
|
323
|
+
"module.shutdown.ack",
|
|
324
|
+
"module.shutdown.ready",
|
|
325
|
+
],
|
|
249
326
|
})
|
|
250
327
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
328
|
+
# Register to Kernel Registry via RPC
|
|
329
|
+
await self._rpc_call(ws, "registry.register", {
|
|
330
|
+
"module_id": "web",
|
|
331
|
+
"module_type": "service",
|
|
332
|
+
"api_endpoint": f"http://127.0.0.1:{self.port}",
|
|
333
|
+
"health_endpoint": "/health",
|
|
334
|
+
"tools": {
|
|
335
|
+
"rpc": {
|
|
336
|
+
"module": {
|
|
337
|
+
"health": {"method": "health", "description": "健康检查"},
|
|
338
|
+
"status": {"method": "status", "description": "状态查询"}
|
|
339
|
+
},
|
|
340
|
+
"web": {
|
|
341
|
+
"list_tokens": {"method": "list_tokens", "description": "列出所有令牌"},
|
|
342
|
+
"revoke_token": {"method": "revoke_token", "description": "撤销令牌"}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
"events_publish": {
|
|
347
|
+
"web": {
|
|
348
|
+
"test": {"description": "Test event from web module"},
|
|
349
|
+
"started": {"description": "Web UI started with access URL"},
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
"events_subscribe": [
|
|
353
|
+
"module.started",
|
|
354
|
+
"module.stopped",
|
|
355
|
+
"module.crashed",
|
|
356
|
+
"module.ready",
|
|
357
|
+
"module.exiting",
|
|
358
|
+
"module.shutdown",
|
|
359
|
+
"module.shutdown.ack",
|
|
360
|
+
"module.shutdown.ready",
|
|
361
|
+
],
|
|
362
|
+
})
|
|
363
|
+
print(f"[web] Registered to Kernel{elapsed_str}")
|
|
364
|
+
|
|
365
|
+
# Send module.ready (every reconnect, not just first time)
|
|
366
|
+
if not self._shutting_down:
|
|
367
|
+
startup_time = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
|
|
368
|
+
await self._rpc_call(ws, "event.publish", {
|
|
369
|
+
"event_id": str(uuid.uuid4()),
|
|
370
|
+
"event": "module.ready",
|
|
371
|
+
"data": {
|
|
372
|
+
"module_id": "web",
|
|
373
|
+
"graceful_shutdown": True,
|
|
374
|
+
"startup_time": startup_time,
|
|
375
|
+
},
|
|
376
|
+
})
|
|
377
|
+
elapsed_str = self._fmt_elapsed(self.boot_t0)
|
|
378
|
+
print(f"[web] module.ready published ({elapsed_str})")
|
|
379
|
+
|
|
380
|
+
# Publish web.started event with access URL
|
|
381
|
+
display_host = "localhost" if self.host == "0.0.0.0" else self.host
|
|
382
|
+
access_url = f"http://{display_host}:{self.port}"
|
|
383
|
+
await self._publish_event({
|
|
384
|
+
"event": "web.started",
|
|
385
|
+
"data": {
|
|
386
|
+
"module_id": "web",
|
|
387
|
+
"url": access_url,
|
|
388
|
+
"host": self.host,
|
|
389
|
+
"port": self.port,
|
|
390
|
+
},
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
# Receive loop
|
|
394
|
+
# CRITICAL: RPC 死锁防范
|
|
395
|
+
# - 入站 RPC 请求必须用 create_task() 异步执行,不可 await
|
|
396
|
+
# - 原因:如果 handler 内部调用 rpc_call() 发出站请求,出站响应需要本接收循环来分发
|
|
397
|
+
# - 如果接收循环被 await handler 阻塞,出站响应永远收不到 → 超时死锁
|
|
398
|
+
# - 事件通知和 RPC 响应可以同步处理(它们不会反向调用 rpc_call)
|
|
399
|
+
print(f"[web] Entering receive loop")
|
|
257
400
|
|
|
258
401
|
try:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
402
|
+
async for raw in ws:
|
|
403
|
+
try:
|
|
404
|
+
msg = json.loads(raw)
|
|
405
|
+
except (json.JSONDecodeError, TypeError):
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
has_method = "method" in msg
|
|
410
|
+
has_id = "id" in msg
|
|
411
|
+
has_result_or_error = "result" in msg or "error" in msg
|
|
412
|
+
|
|
413
|
+
if has_method and not has_id:
|
|
414
|
+
# Event Notification
|
|
415
|
+
await self._handle_event_notification(msg)
|
|
416
|
+
elif has_method and has_id:
|
|
417
|
+
# Incoming RPC request — run in background to prevent deadlock
|
|
418
|
+
asyncio.create_task(self._handle_rpc_request(ws, msg))
|
|
419
|
+
elif has_id and has_result_or_error:
|
|
420
|
+
# RPC response — resolve pending future
|
|
421
|
+
rpc_id = msg.get("id")
|
|
422
|
+
if rpc_id in self._rpc_futures:
|
|
423
|
+
self._rpc_futures[rpc_id].set_result(msg)
|
|
424
|
+
except Exception as e:
|
|
425
|
+
print(f"[web] 消息处理异常(已忽略): {e}")
|
|
269
426
|
except Exception as e:
|
|
270
|
-
print(f"[web]
|
|
427
|
+
print(f"[web] Receive loop exited with exception: {e}")
|
|
428
|
+
finally:
|
|
429
|
+
print(f"[web] Receive loop ended")
|
|
430
|
+
except Exception as e:
|
|
431
|
+
print(f"[web] WebSocket connection error: {e}")
|
|
432
|
+
raise
|
|
433
|
+
finally:
|
|
434
|
+
print(f"[web] WebSocket connection closed")
|
|
435
|
+
self._ws = None
|
|
271
436
|
|
|
272
437
|
async def _rpc_call(self, ws, method: str, params: dict = None):
|
|
273
438
|
"""Send a JSON-RPC 2.0 request (fire-and-forget, no response awaited)."""
|
|
@@ -276,23 +441,69 @@ class WebServer:
|
|
|
276
441
|
msg["params"] = params
|
|
277
442
|
await ws.send(json.dumps(msg))
|
|
278
443
|
|
|
444
|
+
async def _handle_ping_event(self, data: dict):
|
|
445
|
+
"""Handle system.ping event and reply with system.pong."""
|
|
446
|
+
import time
|
|
447
|
+
t1 = data.get("ping_time")
|
|
448
|
+
t2 = time.time()
|
|
449
|
+
|
|
450
|
+
await self._publish_event({
|
|
451
|
+
"event": "system.pong",
|
|
452
|
+
"data": {
|
|
453
|
+
"module_id": "web",
|
|
454
|
+
"ping_time": t1,
|
|
455
|
+
"pong_time": t2,
|
|
456
|
+
},
|
|
457
|
+
})
|
|
458
|
+
|
|
279
459
|
async def _handle_event_notification(self, msg: dict):
|
|
280
460
|
"""Handle an event notification (JSON-RPC 2.0 Notification with method='event')."""
|
|
281
461
|
params = msg.get("params", {})
|
|
282
462
|
event_type = params.get("event", "")
|
|
283
463
|
data = params.get("data", {})
|
|
284
464
|
|
|
465
|
+
# Handle system.ping event
|
|
466
|
+
if event_type == "system.ping":
|
|
467
|
+
await self._handle_ping_event(data)
|
|
468
|
+
return
|
|
469
|
+
|
|
470
|
+
# Log all events for debugging
|
|
471
|
+
print(f"[web] Event received: {event_type}, data: {data}")
|
|
472
|
+
|
|
285
473
|
# Special handling for module.shutdown
|
|
286
474
|
if event_type == "module.shutdown":
|
|
287
475
|
target = data.get("module_id", "")
|
|
288
476
|
reason = data.get("reason", "")
|
|
477
|
+
print(f"[web] Shutdown event: target={target}, reason={reason}")
|
|
289
478
|
# Handle both targeted shutdown (module_id == "web") and broadcast shutdown (no module_id or launcher_lost)
|
|
290
479
|
if target == "web" or not target or reason == "launcher_lost":
|
|
480
|
+
print(f"[web] Handling shutdown...")
|
|
291
481
|
await self._handle_shutdown()
|
|
292
482
|
return
|
|
483
|
+
else:
|
|
484
|
+
print(f"[web] Ignoring shutdown (not for us)")
|
|
485
|
+
return
|
|
293
486
|
|
|
294
|
-
#
|
|
295
|
-
|
|
487
|
+
# Forward module status events to management WebSocket clients
|
|
488
|
+
if event_type in (
|
|
489
|
+
"module.started",
|
|
490
|
+
"module.stopped",
|
|
491
|
+
"module.crashed",
|
|
492
|
+
"module.ready",
|
|
493
|
+
"module.exiting",
|
|
494
|
+
"module.shutdown.ack",
|
|
495
|
+
"module.shutdown.ready",
|
|
496
|
+
):
|
|
497
|
+
await broadcast_event(event_type, data)
|
|
498
|
+
return
|
|
499
|
+
|
|
500
|
+
# Layer 2: 忽略其他系统广播事件
|
|
501
|
+
if event_type in SYSTEM_BROADCAST_EVENTS:
|
|
502
|
+
return
|
|
503
|
+
|
|
504
|
+
# Layer 3: 警告未知事件(仅开发环境)
|
|
505
|
+
if os.environ.get("KITE_ENV") == "development":
|
|
506
|
+
print(f"[web] Debug: Unhandled event: {event_type}")
|
|
296
507
|
|
|
297
508
|
async def _handle_rpc_request(self, ws, msg: dict):
|
|
298
509
|
"""Handle an incoming RPC request (web.* methods)."""
|
|
@@ -300,9 +511,15 @@ class WebServer:
|
|
|
300
511
|
method = msg.get("method", "")
|
|
301
512
|
params = msg.get("params", {})
|
|
302
513
|
|
|
514
|
+
# Strip "web." prefix if present
|
|
515
|
+
if method.startswith("web."):
|
|
516
|
+
method = method[4:]
|
|
517
|
+
|
|
303
518
|
handlers = {
|
|
304
519
|
"health": lambda p: self._rpc_health(),
|
|
305
520
|
"status": lambda p: self._rpc_status(),
|
|
521
|
+
"list_tokens": lambda p: self._rpc_list_tokens(),
|
|
522
|
+
"revoke_token": lambda p: self._rpc_revoke_token(p),
|
|
306
523
|
}
|
|
307
524
|
handler = handlers.get(method)
|
|
308
525
|
if handler:
|
|
@@ -322,10 +539,15 @@ class WebServer:
|
|
|
322
539
|
|
|
323
540
|
async def _rpc_health(self) -> dict:
|
|
324
541
|
"""RPC handler for web.health."""
|
|
542
|
+
# 获取 WebSocket 连接数
|
|
543
|
+
from routes.routes_management_ws import _management_clients
|
|
544
|
+
active_connections = len(_management_clients)
|
|
545
|
+
|
|
325
546
|
return {
|
|
326
547
|
"status": "healthy",
|
|
548
|
+
"uptime_seconds": round(time.time() - self._start_time),
|
|
327
549
|
"details": {
|
|
328
|
-
"
|
|
550
|
+
"active_ws_connections": active_connections,
|
|
329
551
|
},
|
|
330
552
|
}
|
|
331
553
|
|
|
@@ -337,50 +559,163 @@ class WebServer:
|
|
|
337
559
|
"uptime_seconds": round(time.time() - self._start_time),
|
|
338
560
|
}
|
|
339
561
|
|
|
562
|
+
async def _rpc_list_tokens(self) -> dict:
|
|
563
|
+
"""RPC handler for web.list_tokens."""
|
|
564
|
+
from extensions.services.web.pairing import PairingManager
|
|
565
|
+
from pathlib import Path
|
|
566
|
+
|
|
567
|
+
# Get pairing manager from app state
|
|
568
|
+
pairing_manager = self.app.state.pairing_manager
|
|
569
|
+
if not pairing_manager:
|
|
570
|
+
# Fallback: create temporary instance
|
|
571
|
+
pairing_file = Path(__file__).parent / "pairing_codes.jsonl"
|
|
572
|
+
pairing_manager = PairingManager(str(pairing_file))
|
|
573
|
+
|
|
574
|
+
# Read all token records
|
|
575
|
+
codes = pairing_manager._read_codes()
|
|
576
|
+
|
|
577
|
+
# Build a set of revoked tokens
|
|
578
|
+
revoked_tokens = {
|
|
579
|
+
record.get("token")
|
|
580
|
+
for record in codes
|
|
581
|
+
if record.get("status") == "revoked" and record.get("token")
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
# Filter only used tokens (status="used") that are not revoked
|
|
585
|
+
tokens = [
|
|
586
|
+
{
|
|
587
|
+
"token": record.get("token"),
|
|
588
|
+
"role": record.get("role", "viewer"),
|
|
589
|
+
"paired_at": record.get("paired_at"),
|
|
590
|
+
"expires_at": record.get("expires_at"),
|
|
591
|
+
"code": record.get("code", "")
|
|
592
|
+
}
|
|
593
|
+
for record in codes
|
|
594
|
+
if record.get("status") == "used"
|
|
595
|
+
and record.get("token")
|
|
596
|
+
and record.get("token") not in revoked_tokens
|
|
597
|
+
]
|
|
598
|
+
|
|
599
|
+
return {"tokens": tokens}
|
|
600
|
+
|
|
601
|
+
async def _rpc_revoke_token(self, params: dict) -> dict:
|
|
602
|
+
"""RPC handler for web.revoke_token."""
|
|
603
|
+
from extensions.services.web.pairing import PairingManager
|
|
604
|
+
from pathlib import Path
|
|
605
|
+
|
|
606
|
+
token = params.get("token")
|
|
607
|
+
if not token:
|
|
608
|
+
raise ValueError("Missing token parameter")
|
|
609
|
+
|
|
610
|
+
# Get pairing manager from app state
|
|
611
|
+
pairing_manager = self.app.state.pairing_manager
|
|
612
|
+
if not pairing_manager:
|
|
613
|
+
# Fallback: create temporary instance
|
|
614
|
+
pairing_file = Path(__file__).parent / "pairing_codes.jsonl"
|
|
615
|
+
pairing_manager = PairingManager(str(pairing_file))
|
|
616
|
+
|
|
617
|
+
# Verify token exists
|
|
618
|
+
token_info = pairing_manager.verify_token(token)
|
|
619
|
+
if not token_info:
|
|
620
|
+
raise ValueError("Token not found or already expired")
|
|
621
|
+
|
|
622
|
+
# Write a revoked record
|
|
623
|
+
revoked_record = {
|
|
624
|
+
"token": token,
|
|
625
|
+
"status": "revoked",
|
|
626
|
+
"revoked_at": datetime.now(timezone.utc).isoformat()
|
|
627
|
+
}
|
|
628
|
+
pairing_manager._write_code(revoked_record)
|
|
629
|
+
|
|
630
|
+
return {"success": True, "message": "Token revoked successfully"}
|
|
631
|
+
|
|
340
632
|
async def _handle_shutdown(self):
|
|
341
|
-
"""Handle module.shutdown:
|
|
633
|
+
"""Handle module.shutdown: ack → exiting → cleanup → ready → close connections → exit."""
|
|
634
|
+
print("[web] ========== SHUTDOWN STARTED ==========")
|
|
342
635
|
print("[web] Received module.shutdown")
|
|
343
636
|
self._shutting_down = True
|
|
344
637
|
|
|
345
|
-
# Step
|
|
638
|
+
# Step 1: Send ack (立即确认收到)
|
|
639
|
+
print("[web] Sending shutdown ack...")
|
|
346
640
|
await self._publish_event({
|
|
347
|
-
"event": "module.
|
|
348
|
-
"data": {"module_id": "web"
|
|
641
|
+
"event": "module.shutdown.ack",
|
|
642
|
+
"data": {"module_id": "web"},
|
|
349
643
|
})
|
|
644
|
+
print("[web] shutdown ack sent")
|
|
350
645
|
|
|
351
|
-
# Step
|
|
646
|
+
# Step 2: Send module.exiting (开始清理)
|
|
647
|
+
print("[web] Sending module.exiting...")
|
|
352
648
|
await self._publish_event({
|
|
353
|
-
"event": "module.
|
|
354
|
-
"data": {
|
|
649
|
+
"event": "module.exiting",
|
|
650
|
+
"data": {
|
|
651
|
+
"module_id": "web",
|
|
652
|
+
"type": "passive",
|
|
653
|
+
"reason": "shutdown_requested",
|
|
654
|
+
"restart": "auto",
|
|
655
|
+
"action": "none",
|
|
656
|
+
"timeout": 3.0,
|
|
657
|
+
"restart_delay": 0.0,
|
|
658
|
+
},
|
|
355
659
|
})
|
|
356
|
-
print("[web]
|
|
660
|
+
print("[web] module.exiting sent")
|
|
357
661
|
|
|
358
|
-
# Step
|
|
662
|
+
# Step 3: Cleanup (cancel background tasks)
|
|
663
|
+
print("[web] Cleaning up background tasks...")
|
|
359
664
|
if self._test_task:
|
|
360
665
|
self._test_task.cancel()
|
|
666
|
+
print("[web] Test task cancelled")
|
|
361
667
|
if self.bt_manager:
|
|
362
668
|
await self.bt_manager.stop()
|
|
669
|
+
print("[web] Bluetooth manager stopped")
|
|
670
|
+
|
|
671
|
+
# Step 3: Close all WebSocket connections gracefully
|
|
672
|
+
print("[web] Closing WebSocket connections...")
|
|
363
673
|
|
|
364
|
-
#
|
|
674
|
+
# Close relay sessions
|
|
675
|
+
if hasattr(self.app.state, 'relay_service'):
|
|
676
|
+
await self.app.state.relay_service.close_all_sessions()
|
|
677
|
+
|
|
678
|
+
# Close management clients
|
|
679
|
+
from .routes.routes_management_ws import close_all_clients
|
|
680
|
+
await close_all_clients()
|
|
681
|
+
|
|
682
|
+
print("[web] All WebSocket connections closed")
|
|
683
|
+
|
|
684
|
+
# Step 4: Send ready (after closing connections)
|
|
685
|
+
print("[web] Sending shutdown ready...")
|
|
365
686
|
await self._publish_event({
|
|
366
687
|
"event": "module.shutdown.ready",
|
|
367
688
|
"data": {"module_id": "web"},
|
|
368
689
|
})
|
|
369
|
-
print("[web]
|
|
690
|
+
print("[web] shutdown ready sent")
|
|
370
691
|
|
|
371
|
-
# Step
|
|
692
|
+
# Step 5: Trigger uvicorn graceful shutdown
|
|
693
|
+
print("[web] Triggering uvicorn graceful shutdown...")
|
|
372
694
|
if self._uvicorn_server:
|
|
373
695
|
self._uvicorn_server.should_exit = True
|
|
696
|
+
print("[web] uvicorn.should_exit = True")
|
|
697
|
+
|
|
698
|
+
print("[web] ========== SHUTDOWN COMPLETE ==========")
|
|
699
|
+
|
|
700
|
+
# Give uvicorn a moment to start shutdown, then force exit
|
|
701
|
+
# This prevents hanging on lingering connections
|
|
702
|
+
await asyncio.sleep(0.5)
|
|
703
|
+
import sys
|
|
704
|
+
sys.exit(0)
|
|
374
705
|
|
|
375
706
|
async def _publish_event(self, event: dict):
|
|
376
707
|
"""Publish an event via RPC event.publish."""
|
|
377
708
|
if not self._ws:
|
|
709
|
+
print(f"[web] WARNING: Cannot publish event {event.get('event')}, WebSocket not connected")
|
|
378
710
|
return
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
711
|
+
try:
|
|
712
|
+
await self._rpc_call(self._ws, "event.publish", {
|
|
713
|
+
"event_id": str(uuid.uuid4()),
|
|
714
|
+
"event": event.get("event", ""),
|
|
715
|
+
"data": event.get("data", {}),
|
|
716
|
+
})
|
|
717
|
+
except Exception as e:
|
|
718
|
+
print(f"[web] ERROR: Failed to publish event {event.get('event')}: {e}")
|
|
384
719
|
|
|
385
720
|
# ── Test event loop ──
|
|
386
721
|
|