@agentunion/kite 1.4.0 → 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 +102 -0
- package/cli.js +44 -5
- package/core/dependency_checker.py +250 -0
- package/core/env_checker.py +490 -0
- package/dependencies_lock.json +128 -0
- package/extensions/agents/assistant/server.py +33 -17
- package/extensions/channels/acp_channel/server.py +33 -17
- package/extensions/services/backup/entry.py +23 -16
- 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 +23 -1
- 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 +42 -16
- package/extensions/services/watchdog/module.md +1 -0
- package/extensions/services/watchdog/monitor.py +34 -4
- package/extensions/services/web/module.md +1 -1
- package/extensions/services/web/server.py +30 -18
- package/extensions/services/web/static/js/token-manager.js +10 -10
- package/kernel/entry.py +1 -1
- package/kernel/module.md +25 -1
- package/kernel/registry_store.py +2 -26
- package/kernel/rpc_router.py +36 -10
- package/kernel/server.py +106 -17
- package/kite_cli/commands/deps_install.py +67 -0
- package/kite_cli/commands/env_check.py +45 -0
- package/kite_cli/commands/prepare.py +49 -0
- package/kite_cli/commands/venv_setup.py +56 -0
- package/kite_cli/main.py +29 -1
- package/launcher/entry.py +306 -21
- package/launcher/module.md +9 -0
- package/launcher/module_scanner.py +11 -1
- package/main.py +4 -1
- package/package.json +8 -1
- package/python_version.json +4 -0
- package/requirements.txt +38 -0
- package/scripts/env-manager.js +328 -0
- package/scripts/python-env.js +79 -0
- package/scripts/scan_dependencies.py +461 -0
- package/scripts/setup-python-env.js +191 -0
|
@@ -89,7 +89,7 @@ class AcpChannelServer:
|
|
|
89
89
|
"""Single WebSocket session: connect, subscribe, register, ready, receive loop."""
|
|
90
90
|
url = f"ws://127.0.0.1:{self.kernel_port}/ws?token={self.token}&id=acp_channel"
|
|
91
91
|
print(f"[acp_channel] Connecting to Kernel (port {self.kernel_port})")
|
|
92
|
-
async with websockets.connect(url, open_timeout=3, ping_interval=
|
|
92
|
+
async with websockets.connect(url, open_timeout=3, ping_interval=None, close_timeout=10) as ws:
|
|
93
93
|
self._ws = ws
|
|
94
94
|
elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
|
|
95
95
|
elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
|
|
@@ -119,17 +119,18 @@ class AcpChannelServer:
|
|
|
119
119
|
|
|
120
120
|
# Step 3: Publish module.ready (every reconnect)
|
|
121
121
|
if not self._shutting_down:
|
|
122
|
+
startup_time = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
|
|
122
123
|
await self._rpc_call(ws, "event.publish", {
|
|
123
124
|
"event_id": str(uuid.uuid4()),
|
|
124
125
|
"event": "module.ready",
|
|
125
126
|
"data": {
|
|
126
127
|
"module_id": "acp_channel",
|
|
127
128
|
"graceful_shutdown": True,
|
|
129
|
+
"startup_time": startup_time,
|
|
128
130
|
},
|
|
129
131
|
})
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
print(f"[acp_channel] module.ready published{elapsed_str}")
|
|
132
|
+
elapsed_str = self._fmt_elapsed(self.boot_t0)
|
|
133
|
+
print(f"[acp_channel] module.ready published ({elapsed_str})")
|
|
133
134
|
|
|
134
135
|
# Receive loop
|
|
135
136
|
# CRITICAL: RPC 死锁防范
|
|
@@ -138,9 +139,6 @@ class AcpChannelServer:
|
|
|
138
139
|
# - 如果接收循环被 await handler 阻塞,出站响应永远收不到 → 超时死锁
|
|
139
140
|
# - 事件通知和 RPC 响应可以同步处理(它们不会反向调用 rpc_call)
|
|
140
141
|
|
|
141
|
-
# Start heartbeat loop
|
|
142
|
-
heartbeat_task = asyncio.create_task(self._heartbeat_loop(ws))
|
|
143
|
-
|
|
144
142
|
async for raw in ws:
|
|
145
143
|
try:
|
|
146
144
|
msg = json.loads(raw)
|
|
@@ -157,6 +155,11 @@ class AcpChannelServer:
|
|
|
157
155
|
event_type = params.get("event", "")
|
|
158
156
|
data = params.get("data", {})
|
|
159
157
|
|
|
158
|
+
# Handle system.ping event
|
|
159
|
+
if event_type == "system.ping":
|
|
160
|
+
await self._handle_ping_event(data)
|
|
161
|
+
continue
|
|
162
|
+
|
|
160
163
|
# Layer 1: 处理订阅的事件
|
|
161
164
|
if event_type == "module.shutdown":
|
|
162
165
|
target = data.get("module_id", "")
|
|
@@ -223,16 +226,29 @@ class AcpChannelServer:
|
|
|
223
226
|
msg["params"] = params
|
|
224
227
|
await ws.send(json.dumps(msg))
|
|
225
228
|
|
|
226
|
-
async def
|
|
227
|
-
"""
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
229
|
+
async def _handle_ping_event(self, data: dict):
|
|
230
|
+
"""Handle system.ping event and reply with system.pong."""
|
|
231
|
+
t1 = data.get("ping_time")
|
|
232
|
+
t2 = time.time()
|
|
233
|
+
|
|
234
|
+
await self._rpc_call(self._ws, "event.publish", {
|
|
235
|
+
"event_id": str(uuid.uuid4()),
|
|
236
|
+
"event": "system.pong",
|
|
237
|
+
"data": {
|
|
238
|
+
"module_id": "acp_channel",
|
|
239
|
+
"ping_time": t1,
|
|
240
|
+
"pong_time": t2,
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
def _fmt_elapsed(self, t0: float) -> str:
|
|
245
|
+
"""Format elapsed time since t0."""
|
|
246
|
+
d = time.monotonic() - t0 if t0 else 0
|
|
247
|
+
if d < 1:
|
|
248
|
+
return f"{d * 1000:.0f}ms"
|
|
249
|
+
if d < 10:
|
|
250
|
+
return f"{d:.1f}s"
|
|
251
|
+
return f"{d:.0f}s"
|
|
236
252
|
|
|
237
253
|
async def _publish_event(self, event: dict):
|
|
238
254
|
"""Publish an event via JSON-RPC event.publish."""
|
|
@@ -494,7 +494,7 @@ async def _ws_connect(token: str, kernel_port: int, _t0: float):
|
|
|
494
494
|
ws_url = f"ws://127.0.0.1:{kernel_port}/ws?token={token}&id=backup"
|
|
495
495
|
print(f"[backup] Connecting to Kernel: {ws_url}")
|
|
496
496
|
|
|
497
|
-
async with websockets.connect(ws_url, open_timeout=5, ping_interval=
|
|
497
|
+
async with websockets.connect(ws_url, open_timeout=5, ping_interval=None, close_timeout=10) as ws:
|
|
498
498
|
_ws_global = ws
|
|
499
499
|
print(f"[backup] Connected to Kernel ({_fmt_elapsed(_t0)})")
|
|
500
500
|
|
|
@@ -541,12 +541,14 @@ async def _ws_connect(token: str, kernel_port: int, _t0: float):
|
|
|
541
541
|
|
|
542
542
|
# Publish module.ready (every reconnect)
|
|
543
543
|
if not _shutting_down:
|
|
544
|
+
startup_time = time.monotonic() - _t0
|
|
544
545
|
await _rpc_call(ws, "event.publish", {
|
|
545
546
|
"event_id": str(uuid.uuid4()),
|
|
546
547
|
"event": "module.ready",
|
|
547
548
|
"data": {
|
|
548
549
|
"module_id": "backup",
|
|
549
550
|
"graceful_shutdown": True,
|
|
551
|
+
"startup_time": startup_time,
|
|
550
552
|
},
|
|
551
553
|
})
|
|
552
554
|
print(f"[backup] module.ready published ({_fmt_elapsed(_t0)})")
|
|
@@ -554,9 +556,6 @@ async def _ws_connect(token: str, kernel_port: int, _t0: float):
|
|
|
554
556
|
# Start test event loop in background
|
|
555
557
|
test_task = asyncio.create_task(_test_event_loop(ws))
|
|
556
558
|
|
|
557
|
-
# Start heartbeat loop
|
|
558
|
-
heartbeat_task = asyncio.create_task(_heartbeat_loop(ws))
|
|
559
|
-
|
|
560
559
|
# Message loop: handle incoming RPC + events
|
|
561
560
|
# CRITICAL: RPC 死锁防范
|
|
562
561
|
# - 入站 RPC 请求必须用 create_task() 异步执行,不可 await
|
|
@@ -592,18 +591,6 @@ async def _rpc_call(ws, method: str, params: dict = None):
|
|
|
592
591
|
await ws.send(json.dumps(msg))
|
|
593
592
|
|
|
594
593
|
|
|
595
|
-
async def _heartbeat_loop(ws):
|
|
596
|
-
"""Send registry.heartbeat every 30 seconds to prevent TTL expiration."""
|
|
597
|
-
while True:
|
|
598
|
-
try:
|
|
599
|
-
await asyncio.sleep(30)
|
|
600
|
-
if not _shutting_down:
|
|
601
|
-
await _rpc_call(ws, "registry.heartbeat", {"module_id": "backup"})
|
|
602
|
-
except Exception as e:
|
|
603
|
-
print(f"[backup] Heartbeat error: {e}")
|
|
604
|
-
break
|
|
605
|
-
|
|
606
|
-
|
|
607
594
|
async def _publish_event(ws, event: dict):
|
|
608
595
|
"""Publish an event via RPC event.publish."""
|
|
609
596
|
await _rpc_call(ws, "event.publish", {
|
|
@@ -613,12 +600,32 @@ async def _publish_event(ws, event: dict):
|
|
|
613
600
|
})
|
|
614
601
|
|
|
615
602
|
|
|
603
|
+
async def _handle_ping_event(data: dict):
|
|
604
|
+
"""Handle system.ping event and reply with system.pong."""
|
|
605
|
+
t1 = data.get("ping_time")
|
|
606
|
+
t2 = time.time()
|
|
607
|
+
|
|
608
|
+
await _publish_event(_ws_global, {
|
|
609
|
+
"event": "system.pong",
|
|
610
|
+
"data": {
|
|
611
|
+
"module_id": MODULE_NAME,
|
|
612
|
+
"ping_time": t1,
|
|
613
|
+
"pong_time": t2,
|
|
614
|
+
},
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
|
|
616
618
|
async def _handle_event_notification(msg: dict):
|
|
617
619
|
"""Handle an event notification (JSON-RPC 2.0 Notification with method='event')."""
|
|
618
620
|
params = msg.get("params", {})
|
|
619
621
|
event_type = params.get("event", "")
|
|
620
622
|
data = params.get("data", {})
|
|
621
623
|
|
|
624
|
+
# Handle system.ping event
|
|
625
|
+
if event_type == "system.ping":
|
|
626
|
+
await _handle_ping_event(data)
|
|
627
|
+
return
|
|
628
|
+
|
|
622
629
|
# Special handling for module.shutdown
|
|
623
630
|
if event_type == "module.shutdown":
|
|
624
631
|
target = data.get("module_id", "")
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"""
|
|
2
|
+
认证管理模块 - Token 缓存、验证、吊销
|
|
3
|
+
|
|
4
|
+
使用 JSONL 格式存储 Token 历史记录,每次变更追加一条记录。
|
|
5
|
+
查询时使用每个 Token 的最新记录。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import secrets
|
|
11
|
+
import time
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthManager:
|
|
17
|
+
"""认证管理器"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, data_dir: str):
|
|
20
|
+
"""
|
|
21
|
+
初始化认证管理器
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
data_dir: 数据目录($KITE_DATA/evol)
|
|
25
|
+
"""
|
|
26
|
+
self.data_dir = data_dir
|
|
27
|
+
self.auth_dir = os.path.join(data_dir, "auth")
|
|
28
|
+
os.makedirs(self.auth_dir, exist_ok=True)
|
|
29
|
+
|
|
30
|
+
self.evol_tokens_file = os.path.join(self.auth_dir, "evol_tokens.jsonl")
|
|
31
|
+
self.kite_tokens_file = os.path.join(self.auth_dir, "kite_tokens.jsonl")
|
|
32
|
+
|
|
33
|
+
self.kite_token_ttl = 30 * 24 * 3600 # 30 days
|
|
34
|
+
self.evol_token_ttl = 7 * 24 * 3600 # 7 days
|
|
35
|
+
self.token_update_interval = 24 * 3600 # 1 day - Token 更新间隔
|
|
36
|
+
|
|
37
|
+
# 内存缓存
|
|
38
|
+
self._kite_tokens_cache = None
|
|
39
|
+
self._kite_tokens_cache_time = 0
|
|
40
|
+
self._evol_tokens_cache = {} # 改为字典:{phone: record}
|
|
41
|
+
self._evol_tokens_cache_time = 0
|
|
42
|
+
self.cache_ttl = 60 * 60 # 1 小时缓存
|
|
43
|
+
|
|
44
|
+
def save_evol_token(self, phone: str, token: str, data: dict):
|
|
45
|
+
"""保存 Evol Token 到 JSONL"""
|
|
46
|
+
now = time.time()
|
|
47
|
+
record = {
|
|
48
|
+
"phone": phone,
|
|
49
|
+
"token": token,
|
|
50
|
+
"action": "login",
|
|
51
|
+
"obtainedAt": now,
|
|
52
|
+
"obtainedAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
|
|
53
|
+
"expiresAt": now + self.evol_token_ttl,
|
|
54
|
+
"expiresAt_human": datetime.fromtimestamp(now + self.evol_token_ttl, timezone.utc).isoformat(),
|
|
55
|
+
"lastUsedAt": now,
|
|
56
|
+
"lastUsedAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
|
|
57
|
+
"userInfo": data.get("userInfo", {}),
|
|
58
|
+
"accountInfo": data.get("accountInfo", {}),
|
|
59
|
+
"teamInfo": data.get("teamInfo", {}),
|
|
60
|
+
"gatewayInfo": data.get("gatewayInfo", {}),
|
|
61
|
+
"timestamp": now
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
self._append_evol_record(record)
|
|
65
|
+
# 清除缓存
|
|
66
|
+
self._evol_tokens_cache = {}
|
|
67
|
+
self._evol_tokens_cache_time = 0
|
|
68
|
+
|
|
69
|
+
def get_evol_token(self, phone: str) -> Optional[dict]:
|
|
70
|
+
"""获取指定手机号的最新 Evol Token(带缓存)"""
|
|
71
|
+
now = time.time()
|
|
72
|
+
|
|
73
|
+
# 检查缓存是否有效
|
|
74
|
+
if self._evol_tokens_cache and (now - self._evol_tokens_cache_time < self.cache_ttl):
|
|
75
|
+
return self._evol_tokens_cache.get(phone)
|
|
76
|
+
|
|
77
|
+
# 从文件读取所有 Evol Token
|
|
78
|
+
if not os.path.exists(self.evol_tokens_file):
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# 按 phone 分组,保留每个 phone 的最新记录
|
|
83
|
+
phone_tokens = {}
|
|
84
|
+
with open(self.evol_tokens_file, "r", encoding="utf-8") as f:
|
|
85
|
+
for line in f:
|
|
86
|
+
line = line.strip()
|
|
87
|
+
if not line:
|
|
88
|
+
continue
|
|
89
|
+
try:
|
|
90
|
+
record = json.loads(line)
|
|
91
|
+
record_phone = record.get("phone")
|
|
92
|
+
if not record_phone:
|
|
93
|
+
continue
|
|
94
|
+
# 使用最新的记录(后面的覆盖前面的)
|
|
95
|
+
phone_tokens[record_phone] = record
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
# 过滤掉已过期和已登出的 token
|
|
100
|
+
valid_tokens = {}
|
|
101
|
+
for p, record in phone_tokens.items():
|
|
102
|
+
# 检查是否登出
|
|
103
|
+
if record.get("action") == "logout":
|
|
104
|
+
continue
|
|
105
|
+
# 检查是否过期
|
|
106
|
+
if time.time() > record.get("expiresAt", 0):
|
|
107
|
+
continue
|
|
108
|
+
valid_tokens[p] = record
|
|
109
|
+
|
|
110
|
+
# 更新缓存
|
|
111
|
+
self._evol_tokens_cache = valid_tokens
|
|
112
|
+
self._evol_tokens_cache_time = now
|
|
113
|
+
|
|
114
|
+
return valid_tokens.get(phone)
|
|
115
|
+
|
|
116
|
+
except Exception:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
def update_evol_token_usage(self, phone: str):
|
|
120
|
+
"""更新 Evol Token 使用时间(超过 1 天才记录)"""
|
|
121
|
+
latest_record = self.get_evol_token(phone)
|
|
122
|
+
if not latest_record:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
now = time.time()
|
|
126
|
+
last_used = latest_record.get("lastUsedAt", 0)
|
|
127
|
+
|
|
128
|
+
# 如果距离上次使用不到 1 天,不记录
|
|
129
|
+
if now - last_used < self.token_update_interval:
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# 追加使用记录
|
|
133
|
+
record = {
|
|
134
|
+
"phone": phone,
|
|
135
|
+
"token": latest_record.get("token"),
|
|
136
|
+
"action": "used",
|
|
137
|
+
"obtainedAt": latest_record.get("obtainedAt"),
|
|
138
|
+
"obtainedAt_human": latest_record.get("obtainedAt_human"),
|
|
139
|
+
"expiresAt": latest_record.get("expiresAt"),
|
|
140
|
+
"expiresAt_human": latest_record.get("expiresAt_human"),
|
|
141
|
+
"lastUsedAt": now,
|
|
142
|
+
"lastUsedAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
|
|
143
|
+
"userInfo": latest_record.get("userInfo", {}),
|
|
144
|
+
"accountInfo": latest_record.get("accountInfo", {}),
|
|
145
|
+
"teamInfo": latest_record.get("teamInfo", {}),
|
|
146
|
+
"gatewayInfo": latest_record.get("gatewayInfo", {}),
|
|
147
|
+
"timestamp": now
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
self._append_evol_record(record)
|
|
151
|
+
# 清除缓存
|
|
152
|
+
self._evol_tokens_cache = {}
|
|
153
|
+
self._evol_tokens_cache_time = 0
|
|
154
|
+
|
|
155
|
+
def _append_evol_record(self, record: dict):
|
|
156
|
+
"""追加 Evol Token 记录到 JSONL"""
|
|
157
|
+
try:
|
|
158
|
+
with open(self.evol_tokens_file, "a", encoding="utf-8") as f:
|
|
159
|
+
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
160
|
+
except Exception as e:
|
|
161
|
+
print(f"[auth] Failed to append evol token record: {e}")
|
|
162
|
+
|
|
163
|
+
def revoke_evol_token(self, phone: str):
|
|
164
|
+
"""吊销 Evol Token(追加 logout 记录)"""
|
|
165
|
+
latest_record = self.get_evol_token(phone)
|
|
166
|
+
if not latest_record:
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
now = time.time()
|
|
170
|
+
record = {
|
|
171
|
+
"phone": phone,
|
|
172
|
+
"token": latest_record.get("token"),
|
|
173
|
+
"action": "logout",
|
|
174
|
+
"obtainedAt": latest_record.get("obtainedAt"),
|
|
175
|
+
"obtainedAt_human": latest_record.get("obtainedAt_human"),
|
|
176
|
+
"expiresAt": latest_record.get("expiresAt"),
|
|
177
|
+
"expiresAt_human": latest_record.get("expiresAt_human"),
|
|
178
|
+
"lastUsedAt": now,
|
|
179
|
+
"lastUsedAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
|
|
180
|
+
"userInfo": latest_record.get("userInfo", {}),
|
|
181
|
+
"accountInfo": latest_record.get("accountInfo", {}),
|
|
182
|
+
"teamInfo": latest_record.get("teamInfo", {}),
|
|
183
|
+
"gatewayInfo": latest_record.get("gatewayInfo", {}),
|
|
184
|
+
"timestamp": now
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
self._append_evol_record(record)
|
|
188
|
+
# 清除缓存
|
|
189
|
+
self._evol_tokens_cache = {}
|
|
190
|
+
self._evol_tokens_cache_time = 0
|
|
191
|
+
|
|
192
|
+
def generate_kite_token(self, device_info: dict) -> str:
|
|
193
|
+
"""生成 Kite Token 并追加到 JSONL"""
|
|
194
|
+
token = "kite_" + secrets.token_urlsafe(32)
|
|
195
|
+
now = time.time()
|
|
196
|
+
|
|
197
|
+
record = {
|
|
198
|
+
"token": token,
|
|
199
|
+
"deviceId": device_info.get("deviceId", "unknown"),
|
|
200
|
+
"deviceName": device_info.get("deviceName", "Unknown Device"),
|
|
201
|
+
"phone": None, # 初始未绑定手机号
|
|
202
|
+
"action": "created",
|
|
203
|
+
"createdAt": now,
|
|
204
|
+
"createdAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
|
|
205
|
+
"lastUsedAt": now,
|
|
206
|
+
"lastUsedAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
|
|
207
|
+
"expiresAt": now + self.kite_token_ttl,
|
|
208
|
+
"expiresAt_human": datetime.fromtimestamp(now + self.kite_token_ttl, timezone.utc).isoformat(),
|
|
209
|
+
"isValid": True,
|
|
210
|
+
"timestamp": now
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# 追加到 JSONL
|
|
214
|
+
self._append_token_record(record)
|
|
215
|
+
# 清除缓存
|
|
216
|
+
self._kite_tokens_cache = None
|
|
217
|
+
self._kite_tokens_cache_time = 0
|
|
218
|
+
|
|
219
|
+
return token
|
|
220
|
+
|
|
221
|
+
def verify_kite_token(self, token: str) -> bool:
|
|
222
|
+
"""验证 Kite Token 并更新使用时间(超过 1 天才记录)"""
|
|
223
|
+
latest_tokens = self._get_latest_tokens()
|
|
224
|
+
now = time.time()
|
|
225
|
+
|
|
226
|
+
token_info = latest_tokens.get(token)
|
|
227
|
+
if not token_info:
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
if not token_info.get("isValid", True):
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
if now > token_info.get("expiresAt", 0):
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
# 检查距离上次使用是否超过 1 天
|
|
237
|
+
last_used = token_info.get("lastUsedAt", 0)
|
|
238
|
+
if now - last_used < self.token_update_interval:
|
|
239
|
+
# 不到 1 天,不记录
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
# 追加使用记录
|
|
243
|
+
expires_at = now + self.kite_token_ttl
|
|
244
|
+
record = {
|
|
245
|
+
"token": token,
|
|
246
|
+
"deviceId": token_info.get("deviceId"),
|
|
247
|
+
"deviceName": token_info.get("deviceName"),
|
|
248
|
+
"phone": token_info.get("phone"), # 保留绑定的手机号
|
|
249
|
+
"action": "used",
|
|
250
|
+
"createdAt": token_info.get("createdAt"),
|
|
251
|
+
"createdAt_human": token_info.get("createdAt_human"),
|
|
252
|
+
"lastUsedAt": now,
|
|
253
|
+
"lastUsedAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
|
|
254
|
+
"expiresAt": expires_at,
|
|
255
|
+
"expiresAt_human": datetime.fromtimestamp(expires_at, timezone.utc).isoformat(),
|
|
256
|
+
"isValid": True,
|
|
257
|
+
"timestamp": now
|
|
258
|
+
}
|
|
259
|
+
self._append_token_record(record)
|
|
260
|
+
# 清除缓存
|
|
261
|
+
self._kite_tokens_cache = None
|
|
262
|
+
self._kite_tokens_cache_time = 0
|
|
263
|
+
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
def revoke_kite_token(self, token: str):
|
|
267
|
+
"""吊销 Kite Token"""
|
|
268
|
+
latest_tokens = self._get_latest_tokens()
|
|
269
|
+
token_info = latest_tokens.get(token)
|
|
270
|
+
|
|
271
|
+
if not token_info:
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
now = time.time()
|
|
275
|
+
record = {
|
|
276
|
+
"token": token,
|
|
277
|
+
"deviceId": token_info.get("deviceId"),
|
|
278
|
+
"deviceName": token_info.get("deviceName"),
|
|
279
|
+
"phone": token_info.get("phone"), # 保留绑定的手机号
|
|
280
|
+
"action": "revoked",
|
|
281
|
+
"createdAt": token_info.get("createdAt"),
|
|
282
|
+
"createdAt_human": token_info.get("createdAt_human"),
|
|
283
|
+
"lastUsedAt": token_info.get("lastUsedAt"),
|
|
284
|
+
"lastUsedAt_human": token_info.get("lastUsedAt_human"),
|
|
285
|
+
"expiresAt": token_info.get("expiresAt"),
|
|
286
|
+
"expiresAt_human": token_info.get("expiresAt_human"),
|
|
287
|
+
"isValid": False,
|
|
288
|
+
"timestamp": now
|
|
289
|
+
}
|
|
290
|
+
self._append_token_record(record)
|
|
291
|
+
# 清除缓存
|
|
292
|
+
self._kite_tokens_cache = None
|
|
293
|
+
self._kite_tokens_cache_time = 0
|
|
294
|
+
|
|
295
|
+
def bind_kite_token_to_phone(self, token: str, phone: str):
|
|
296
|
+
"""绑定 Kite Token 到手机号"""
|
|
297
|
+
latest_tokens = self._get_latest_tokens()
|
|
298
|
+
token_info = latest_tokens.get(token)
|
|
299
|
+
|
|
300
|
+
if not token_info:
|
|
301
|
+
raise ValueError(f"Token not found: {token}")
|
|
302
|
+
|
|
303
|
+
# 如果已经绑定到同一个手机号,不需要重复记录
|
|
304
|
+
if token_info.get("phone") == phone:
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
now = time.time()
|
|
308
|
+
record = {
|
|
309
|
+
"token": token,
|
|
310
|
+
"deviceId": token_info.get("deviceId"),
|
|
311
|
+
"deviceName": token_info.get("deviceName"),
|
|
312
|
+
"phone": phone, # 绑定手机号
|
|
313
|
+
"action": "bound",
|
|
314
|
+
"createdAt": token_info.get("createdAt"),
|
|
315
|
+
"createdAt_human": token_info.get("createdAt_human"),
|
|
316
|
+
"lastUsedAt": now,
|
|
317
|
+
"lastUsedAt_human": datetime.fromtimestamp(now, timezone.utc).isoformat(),
|
|
318
|
+
"expiresAt": token_info.get("expiresAt"),
|
|
319
|
+
"expiresAt_human": token_info.get("expiresAt_human"),
|
|
320
|
+
"isValid": token_info.get("isValid", True),
|
|
321
|
+
"timestamp": now
|
|
322
|
+
}
|
|
323
|
+
self._append_token_record(record)
|
|
324
|
+
# 清除缓存
|
|
325
|
+
self._kite_tokens_cache = None
|
|
326
|
+
self._kite_tokens_cache_time = 0
|
|
327
|
+
|
|
328
|
+
def get_phone_by_kite_token(self, token: str) -> Optional[str]:
|
|
329
|
+
"""根据 Kite Token 获取绑定的手机号"""
|
|
330
|
+
latest_tokens = self._get_latest_tokens()
|
|
331
|
+
token_info = latest_tokens.get(token)
|
|
332
|
+
|
|
333
|
+
if not token_info:
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
return token_info.get("phone")
|
|
337
|
+
|
|
338
|
+
def list_all_evol_tokens(self) -> list:
|
|
339
|
+
"""列出所有有效的 Evol Token(用于前端显示)"""
|
|
340
|
+
if not os.path.exists(self.evol_tokens_file):
|
|
341
|
+
return []
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
# 按 phone 分组,保留每个 phone 的最新记录
|
|
345
|
+
phone_tokens = {}
|
|
346
|
+
with open(self.evol_tokens_file, "r", encoding="utf-8") as f:
|
|
347
|
+
for line in f:
|
|
348
|
+
line = line.strip()
|
|
349
|
+
if not line:
|
|
350
|
+
continue
|
|
351
|
+
try:
|
|
352
|
+
record = json.loads(line)
|
|
353
|
+
record_phone = record.get("phone")
|
|
354
|
+
if not record_phone:
|
|
355
|
+
continue
|
|
356
|
+
# 使用最新的记录(后面的覆盖前面的)
|
|
357
|
+
phone_tokens[record_phone] = record
|
|
358
|
+
except json.JSONDecodeError:
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
# 过滤掉已过期和已登出的 token,返回列表
|
|
362
|
+
valid_tokens = []
|
|
363
|
+
for phone, record in phone_tokens.items():
|
|
364
|
+
# 检查是否登出
|
|
365
|
+
if record.get("action") == "logout":
|
|
366
|
+
continue
|
|
367
|
+
# 检查是否过期
|
|
368
|
+
if time.time() > record.get("expiresAt", 0):
|
|
369
|
+
continue
|
|
370
|
+
valid_tokens.append(record)
|
|
371
|
+
|
|
372
|
+
return valid_tokens
|
|
373
|
+
|
|
374
|
+
except Exception:
|
|
375
|
+
return []
|
|
376
|
+
|
|
377
|
+
def list_devices(self, current_token: str) -> list:
|
|
378
|
+
"""列出所有已配对设备"""
|
|
379
|
+
latest_tokens = self._get_latest_tokens()
|
|
380
|
+
devices = []
|
|
381
|
+
|
|
382
|
+
for token, info in latest_tokens.items():
|
|
383
|
+
devices.append({
|
|
384
|
+
"deviceId": info.get("deviceId"),
|
|
385
|
+
"deviceName": info.get("deviceName"),
|
|
386
|
+
"createdAt": info.get("createdAt"),
|
|
387
|
+
"lastUsedAt": info.get("lastUsedAt"),
|
|
388
|
+
"expiresAt": info.get("expiresAt"),
|
|
389
|
+
"isValid": info.get("isValid", True),
|
|
390
|
+
"isCurrent": token == current_token
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
return devices
|
|
394
|
+
|
|
395
|
+
def _get_latest_tokens(self) -> dict:
|
|
396
|
+
"""
|
|
397
|
+
获取所有 Token 的最新状态(带缓存)
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
{token: latest_record} 字典
|
|
401
|
+
"""
|
|
402
|
+
now = time.time()
|
|
403
|
+
|
|
404
|
+
# 检查缓存是否有效
|
|
405
|
+
if self._kite_tokens_cache and (now - self._kite_tokens_cache_time < self.cache_ttl):
|
|
406
|
+
return self._kite_tokens_cache
|
|
407
|
+
|
|
408
|
+
# 从文件读取
|
|
409
|
+
if not os.path.exists(self.kite_tokens_file):
|
|
410
|
+
return {}
|
|
411
|
+
|
|
412
|
+
tokens = {}
|
|
413
|
+
try:
|
|
414
|
+
with open(self.kite_tokens_file, "r", encoding="utf-8") as f:
|
|
415
|
+
for line in f:
|
|
416
|
+
line = line.strip()
|
|
417
|
+
if not line:
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
record = json.loads(line)
|
|
422
|
+
token = record.get("token")
|
|
423
|
+
if token:
|
|
424
|
+
# 使用最新的记录(后面的覆盖前面的)
|
|
425
|
+
tokens[token] = record
|
|
426
|
+
except json.JSONDecodeError:
|
|
427
|
+
continue
|
|
428
|
+
except Exception:
|
|
429
|
+
return {}
|
|
430
|
+
|
|
431
|
+
# 更新缓存
|
|
432
|
+
self._kite_tokens_cache = tokens
|
|
433
|
+
self._kite_tokens_cache_time = now
|
|
434
|
+
|
|
435
|
+
return tokens
|
|
436
|
+
|
|
437
|
+
def _append_token_record(self, record: dict):
|
|
438
|
+
"""追加 Token 记录到 JSONL"""
|
|
439
|
+
try:
|
|
440
|
+
with open(self.kite_tokens_file, "a", encoding="utf-8") as f:
|
|
441
|
+
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
442
|
+
except Exception as e:
|
|
443
|
+
print(f"[auth] Failed to append token record: {e}")
|