@agentunion/kite 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +200 -0
- package/cli.js +76 -0
- package/extensions/agents/assistant/entry.py +111 -1
- package/extensions/agents/assistant/server.py +263 -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 +263 -215
- package/extensions/event_hub_bench/entry.py +107 -1
- package/extensions/services/backup/entry.py +299 -21
- package/extensions/services/backup/module.md +24 -22
- package/extensions/services/model_service/entry.py +145 -19
- package/extensions/services/model_service/module.md +21 -22
- package/extensions/services/watchdog/entry.py +188 -25
- package/extensions/services/watchdog/monitor.py +144 -34
- 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 +421 -98
- 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 +36 -33
- package/kernel/registry_store.py +48 -15
- package/kernel/rpc_router.py +120 -53
- package/kernel/server.py +219 -12
- 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/doctor.py +35 -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/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/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 +289 -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 +819 -158
- package/launcher/logging_setup.py +104 -0
- package/launcher/module.md +37 -37
- package/package.json +2 -1
- package/scripts/plan_manager.py +315 -0
- package/extensions/services/web/routes/routes_modules.py +0 -249
|
@@ -26,7 +26,113 @@ import uvicorn
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
# ── Module configuration ──
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
def _load_module_config() -> dict:
|
|
31
|
+
"""Load module configuration from module.md frontmatter.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Dict with keys: name, preferred_port, advertise_ip
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
SystemExit: If module.md is invalid or name is non-compliant
|
|
38
|
+
"""
|
|
39
|
+
_this_dir = os.path.dirname(os.path.abspath(__file__))
|
|
40
|
+
module_md = os.path.join(_this_dir, "module.md")
|
|
41
|
+
|
|
42
|
+
# Calculate relative path for error messages
|
|
43
|
+
project_root = os.environ.get("KITE_PROJECT", "")
|
|
44
|
+
if project_root and _this_dir.startswith(project_root):
|
|
45
|
+
rel_path = os.path.relpath(_this_dir, project_root)
|
|
46
|
+
else:
|
|
47
|
+
rel_path = _this_dir
|
|
48
|
+
|
|
49
|
+
# Default values (will be overridden if valid config exists)
|
|
50
|
+
result = {
|
|
51
|
+
"name": "",
|
|
52
|
+
"preferred_port": 0,
|
|
53
|
+
"advertise_ip": "0.0.0.0"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Check if module.md exists
|
|
57
|
+
if not os.path.exists(module_md):
|
|
58
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
59
|
+
print(f" Path: {rel_path}/module.md")
|
|
60
|
+
print(f" Reason: File not found")
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
with open(module_md, encoding="utf-8") as f:
|
|
65
|
+
text = f.read()
|
|
66
|
+
|
|
67
|
+
# Extract YAML frontmatter (between --- markers)
|
|
68
|
+
import re
|
|
69
|
+
m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
|
|
70
|
+
if not m:
|
|
71
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
72
|
+
print(f" Path: {rel_path}/module.md")
|
|
73
|
+
print(f" Reason: Missing YAML frontmatter")
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
|
|
76
|
+
# Parse YAML frontmatter
|
|
77
|
+
try:
|
|
78
|
+
import yaml
|
|
79
|
+
fm = yaml.safe_load(m.group(1)) or {}
|
|
80
|
+
except ImportError:
|
|
81
|
+
print(f"[{rel_path}] ERROR: PyYAML not installed, cannot parse module.md")
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
85
|
+
print(f" Path: {rel_path}/module.md")
|
|
86
|
+
print(f" Reason: YAML parse error: {e}")
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
89
|
+
# Validate 'name' field (required)
|
|
90
|
+
if "name" not in fm:
|
|
91
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
92
|
+
print(f" Path: {rel_path}/module.md")
|
|
93
|
+
print(f" Reason: Missing 'name' field")
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
raw_name = str(fm["name"]).strip()
|
|
97
|
+
|
|
98
|
+
if not raw_name:
|
|
99
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
100
|
+
print(f" Path: {rel_path}/module.md")
|
|
101
|
+
print(f" Reason: Empty module name")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
# Validate name characters
|
|
105
|
+
sanitized = re.sub(r'[^a-zA-Z0-9_\-]', '', raw_name)
|
|
106
|
+
|
|
107
|
+
if sanitized != raw_name:
|
|
108
|
+
invalid_chars = ''.join(sorted(set(c for c in raw_name if c not in sanitized)))
|
|
109
|
+
print(f"[{rel_path}] ERROR: Invalid module configuration in module.md")
|
|
110
|
+
print(f" Path: {rel_path}/module.md")
|
|
111
|
+
print(f" Reason: Invalid characters in name '{raw_name}': {repr(invalid_chars)}")
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
|
|
114
|
+
result["name"] = sanitized
|
|
115
|
+
|
|
116
|
+
# Extract optional fields
|
|
117
|
+
if "preferred_port" in fm:
|
|
118
|
+
try:
|
|
119
|
+
result["preferred_port"] = int(fm["preferred_port"])
|
|
120
|
+
except (ValueError, TypeError):
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
if "advertise_ip" in fm:
|
|
124
|
+
result["advertise_ip"] = str(fm["advertise_ip"])
|
|
125
|
+
|
|
126
|
+
except SystemExit:
|
|
127
|
+
raise # Re-raise exit to prevent catching by outer except
|
|
128
|
+
except Exception as e:
|
|
129
|
+
print(f"[{rel_path}] ERROR: Failed to read module.md: {e}")
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
_module_config = _load_module_config()
|
|
135
|
+
MODULE_NAME = _module_config["name"]
|
|
30
136
|
|
|
31
137
|
|
|
32
138
|
class _SafeWriter:
|
|
@@ -251,27 +357,6 @@ def _fmt_elapsed(t0: float) -> str:
|
|
|
251
357
|
return f"{d:.0f}s"
|
|
252
358
|
|
|
253
359
|
|
|
254
|
-
def _read_module_md() -> dict:
|
|
255
|
-
"""Read preferred_port and advertise_ip from own module.md."""
|
|
256
|
-
md_path = os.path.join(_this_dir, "module.md")
|
|
257
|
-
result = {"preferred_port": 0, "advertise_ip": "0.0.0.0"}
|
|
258
|
-
try:
|
|
259
|
-
with open(md_path, "r", encoding="utf-8") as f:
|
|
260
|
-
text = f.read()
|
|
261
|
-
m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
|
|
262
|
-
if m:
|
|
263
|
-
try:
|
|
264
|
-
import yaml
|
|
265
|
-
fm = yaml.safe_load(m.group(1)) or {}
|
|
266
|
-
except ImportError:
|
|
267
|
-
fm = {}
|
|
268
|
-
result["preferred_port"] = int(fm.get("preferred_port", 0))
|
|
269
|
-
result["advertise_ip"] = fm.get("advertise_ip", "0.0.0.0")
|
|
270
|
-
except Exception:
|
|
271
|
-
pass
|
|
272
|
-
return result
|
|
273
|
-
|
|
274
|
-
|
|
275
360
|
def _bind_port(preferred: int, host: str, max_attempts: int = 10) -> int | None:
|
|
276
361
|
"""
|
|
277
362
|
Try to bind to preferred port, then port+1, port+2, ... up to max_attempts.
|
|
@@ -351,10 +436,9 @@ def main():
|
|
|
351
436
|
|
|
352
437
|
print(f"[web] Token received ({len(token)} chars), kernel port: {kernel_port} ({_fmt_elapsed(_t0)})")
|
|
353
438
|
|
|
354
|
-
#
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
port = _bind_port(md_cfg["preferred_port"], host)
|
|
439
|
+
# Use cached module config (already loaded at module level)
|
|
440
|
+
host = _module_config["advertise_ip"]
|
|
441
|
+
port = _bind_port(_module_config["preferred_port"], host)
|
|
358
442
|
|
|
359
443
|
# If port binding failed after 10 attempts, exit gracefully
|
|
360
444
|
if port is None:
|
|
@@ -385,6 +469,10 @@ def main():
|
|
|
385
469
|
_print_crash_summary(type(e), e.__traceback__)
|
|
386
470
|
sys.exit(1)
|
|
387
471
|
|
|
472
|
+
# Check if server requested exit with non-zero code
|
|
473
|
+
if server._exit_code != 0:
|
|
474
|
+
sys.exit(server._exit_code)
|
|
475
|
+
|
|
388
476
|
|
|
389
477
|
if __name__ == "__main__":
|
|
390
478
|
main()
|
|
@@ -1,24 +1,35 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: web
|
|
3
|
-
display_name: Web Management
|
|
4
|
-
version:
|
|
5
|
-
type: service
|
|
6
|
-
state: enabled
|
|
7
|
-
runtime: python
|
|
8
|
-
entry: entry.py
|
|
9
|
-
preferred_port: 18766
|
|
10
|
-
advertise_ip: 0.0.0.0
|
|
11
|
-
events:
|
|
12
|
-
|
|
13
|
-
subscriptions:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
---
|
|
2
|
+
name: web
|
|
3
|
+
display_name: Web Management
|
|
4
|
+
version: '1.0'
|
|
5
|
+
type: service
|
|
6
|
+
state: enabled
|
|
7
|
+
runtime: python
|
|
8
|
+
entry: entry.py
|
|
9
|
+
preferred_port: 18766
|
|
10
|
+
advertise_ip: 0.0.0.0
|
|
11
|
+
events:
|
|
12
|
+
- web.test
|
|
13
|
+
subscriptions:
|
|
14
|
+
- module.started
|
|
15
|
+
- module.stopped
|
|
16
|
+
- module.shutdown
|
|
17
|
+
|
|
18
|
+
# 业务配置列表
|
|
19
|
+
businesses:
|
|
20
|
+
- name: web_server
|
|
21
|
+
type: http_service
|
|
22
|
+
description: Web 管理界面和 API 服务
|
|
23
|
+
config_file: web_config.json5
|
|
24
|
+
|
|
25
|
+
- name: relay_service
|
|
26
|
+
type: kernel_relay
|
|
27
|
+
description: Kernel WebSocket 中转服务
|
|
28
|
+
config_file: relay_config.json5
|
|
29
|
+
---
|
|
30
|
+
# Web Management(Web 管理界面)
|
|
31
|
+
|
|
32
|
+
Web 管理界面模块,提供系统管理和监控的 Web UI。
|
|
33
|
+
|
|
34
|
+
- 管理界面 — 提供系统配置和状态监控的 Web UI
|
|
35
|
+
- 事件通知 — 通过 Event Hub 发布管理操作事件
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
配对码管理模块
|
|
3
|
+
|
|
4
|
+
负责配对码的生成、验证、使用和 frontend_token 的管理。
|
|
5
|
+
|
|
6
|
+
零共享代码依赖 - 此文件可以独立拷贝到其他模块使用。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import secrets
|
|
12
|
+
import string
|
|
13
|
+
import time
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PairingManager:
|
|
19
|
+
"""配对码管理器"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, pairing_file: str, code_length: int = 6, token_expiry: int = 2592000):
|
|
22
|
+
"""
|
|
23
|
+
初始化配对码管理器。
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
pairing_file: 配对码文件路径(JSONL 格式)
|
|
27
|
+
code_length: 配对码长度(默认 6)
|
|
28
|
+
token_expiry: frontend_token 有效期(秒,默认 30 天)
|
|
29
|
+
"""
|
|
30
|
+
self.pairing_file = pairing_file
|
|
31
|
+
self.code_length = code_length
|
|
32
|
+
self.token_expiry = token_expiry
|
|
33
|
+
|
|
34
|
+
# 临时配对码(未持久化)
|
|
35
|
+
self.pending_codes = {} # {code: {"role": "admin", "created_at": timestamp}}
|
|
36
|
+
|
|
37
|
+
# 确保文件存在
|
|
38
|
+
if not os.path.exists(pairing_file):
|
|
39
|
+
os.makedirs(os.path.dirname(pairing_file), exist_ok=True)
|
|
40
|
+
|
|
41
|
+
def generate_pairing_code(self, role: str = "admin") -> str:
|
|
42
|
+
"""
|
|
43
|
+
生成临时配对码(不持久化)。
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
role: 用户角色
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
配对码
|
|
50
|
+
"""
|
|
51
|
+
code = self._generate_code()
|
|
52
|
+
self.pending_codes[code] = {
|
|
53
|
+
"role": role,
|
|
54
|
+
"created_at": time.time()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# 清理过期的配对码(超过 5 分钟)
|
|
58
|
+
self._cleanup_expired_codes()
|
|
59
|
+
|
|
60
|
+
return code
|
|
61
|
+
|
|
62
|
+
def _cleanup_expired_codes(self):
|
|
63
|
+
"""清理过期的临时配对码"""
|
|
64
|
+
now = time.time()
|
|
65
|
+
expired = [
|
|
66
|
+
code for code, info in self.pending_codes.items()
|
|
67
|
+
if now - info["created_at"] > 300 # 5 分钟
|
|
68
|
+
]
|
|
69
|
+
for code in expired:
|
|
70
|
+
del self.pending_codes[code]
|
|
71
|
+
|
|
72
|
+
def _ensure_active_code(self):
|
|
73
|
+
"""确保有 active 状态的配对码,如果没有则生成(已废弃)"""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
def _print_pairing_code(self, code: str, is_new: bool):
|
|
77
|
+
"""在控制台绿色高亮显示配对码(已废弃,改为通过事件发送给 Launcher)"""
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def _create_initial_code(self):
|
|
81
|
+
"""创建初始配对码(已废弃,由 _ensure_active_code 替代)"""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def _generate_code(self) -> str:
|
|
85
|
+
"""生成随机配对码"""
|
|
86
|
+
chars = string.ascii_uppercase + string.digits
|
|
87
|
+
return ''.join(secrets.choice(chars) for _ in range(self.code_length))
|
|
88
|
+
|
|
89
|
+
def _generate_token(self) -> str:
|
|
90
|
+
"""生成 frontend_token"""
|
|
91
|
+
return "tok_" + secrets.token_urlsafe(32)
|
|
92
|
+
|
|
93
|
+
def _read_codes(self) -> list[dict]:
|
|
94
|
+
"""读取所有配对码记录"""
|
|
95
|
+
if not os.path.exists(self.pairing_file):
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
codes = []
|
|
99
|
+
with open(self.pairing_file, "r", encoding="utf-8") as f:
|
|
100
|
+
for line in f:
|
|
101
|
+
line = line.strip()
|
|
102
|
+
if line:
|
|
103
|
+
try:
|
|
104
|
+
codes.append(json.loads(line))
|
|
105
|
+
except json.JSONDecodeError:
|
|
106
|
+
pass
|
|
107
|
+
return codes
|
|
108
|
+
|
|
109
|
+
def _write_code(self, code_record: dict):
|
|
110
|
+
"""追加配对码记录"""
|
|
111
|
+
with open(self.pairing_file, "a", encoding="utf-8") as f:
|
|
112
|
+
f.write(json.dumps(code_record, ensure_ascii=False) + "\n")
|
|
113
|
+
|
|
114
|
+
def verify_code(self, code: str) -> Optional[dict]:
|
|
115
|
+
"""
|
|
116
|
+
验证配对码(包括临时配对码和持久化配对码)。
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
code: 配对码
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
如果有效,返回配对码信息(包含 role);否则返回 None
|
|
123
|
+
"""
|
|
124
|
+
# 先检查临时配对码
|
|
125
|
+
if code in self.pending_codes:
|
|
126
|
+
info = self.pending_codes[code]
|
|
127
|
+
# 检查是否过期(5 分钟)
|
|
128
|
+
if time.time() - info["created_at"] < 300:
|
|
129
|
+
return {"code": code, "role": info["role"], "status": "pending"}
|
|
130
|
+
|
|
131
|
+
# 再检查持久化配对码(向后兼容)
|
|
132
|
+
codes = self._read_codes()
|
|
133
|
+
for record in reversed(codes):
|
|
134
|
+
if record.get("code") == code and record.get("status") == "active":
|
|
135
|
+
return record
|
|
136
|
+
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def pair(self, code: str) -> Optional[dict]:
|
|
140
|
+
"""
|
|
141
|
+
使用配对码进行配对。
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
code: 配对码
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
如果成功,返回 {"token": "...", "role": "..."};否则返回 None
|
|
148
|
+
"""
|
|
149
|
+
# 验证配对码
|
|
150
|
+
code_info = self.verify_code(code)
|
|
151
|
+
if not code_info:
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
# 生成 frontend_token
|
|
155
|
+
token = self._generate_token()
|
|
156
|
+
role = code_info.get("role", "viewer")
|
|
157
|
+
|
|
158
|
+
# 持久化保存 token
|
|
159
|
+
used_record = {
|
|
160
|
+
"code": code,
|
|
161
|
+
"role": role,
|
|
162
|
+
"status": "used",
|
|
163
|
+
"token": token,
|
|
164
|
+
"paired_at": datetime.now(timezone.utc).isoformat(),
|
|
165
|
+
"expires_at": datetime.fromtimestamp(
|
|
166
|
+
time.time() + self.token_expiry, tz=timezone.utc
|
|
167
|
+
).isoformat()
|
|
168
|
+
}
|
|
169
|
+
self._write_code(used_record)
|
|
170
|
+
|
|
171
|
+
# 从临时配对码中删除
|
|
172
|
+
if code in self.pending_codes:
|
|
173
|
+
del self.pending_codes[code]
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
"token": token,
|
|
177
|
+
"role": role,
|
|
178
|
+
"expires_at": used_record["expires_at"]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
def verify_token(self, token: str) -> Optional[dict]:
|
|
182
|
+
"""
|
|
183
|
+
验证 frontend_token。
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
token: frontend_token
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
如果有效,返回 {"role": "...", "expires_at": "..."};否则返回 None
|
|
190
|
+
"""
|
|
191
|
+
codes = self._read_codes()
|
|
192
|
+
|
|
193
|
+
# 检查 token 是否被吊销
|
|
194
|
+
for record in reversed(codes):
|
|
195
|
+
if record.get("token") == token and record.get("status") == "revoked":
|
|
196
|
+
return None # Token 已被吊销
|
|
197
|
+
|
|
198
|
+
# 查找 token 对应的记录
|
|
199
|
+
for record in reversed(codes):
|
|
200
|
+
if record.get("token") == token and record.get("status") == "used":
|
|
201
|
+
# 检查是否过期
|
|
202
|
+
expires_at = record.get("expires_at")
|
|
203
|
+
if expires_at:
|
|
204
|
+
expire_time = datetime.fromisoformat(expires_at)
|
|
205
|
+
if datetime.now(timezone.utc) < expire_time:
|
|
206
|
+
return {
|
|
207
|
+
"role": record.get("role", "viewer"),
|
|
208
|
+
"expires_at": expires_at
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def renew_token(self, old_token: str) -> Optional[str]:
|
|
214
|
+
"""
|
|
215
|
+
续期 frontend_token。
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
old_token: 旧的 frontend_token
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
如果成功,返回新的 token;否则返回 None
|
|
222
|
+
"""
|
|
223
|
+
# 验证旧 token
|
|
224
|
+
token_info = self.verify_token(old_token)
|
|
225
|
+
if not token_info:
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
# 生成新 token
|
|
229
|
+
new_token = self._generate_token()
|
|
230
|
+
role = token_info["role"]
|
|
231
|
+
|
|
232
|
+
# 写入新 token 记录
|
|
233
|
+
new_record = {
|
|
234
|
+
"token": new_token,
|
|
235
|
+
"role": role,
|
|
236
|
+
"status": "used",
|
|
237
|
+
"renewed_from": old_token,
|
|
238
|
+
"paired_at": datetime.now(timezone.utc).isoformat(),
|
|
239
|
+
"expires_at": datetime.fromtimestamp(
|
|
240
|
+
time.time() + self.token_expiry, tz=timezone.utc
|
|
241
|
+
).isoformat()
|
|
242
|
+
}
|
|
243
|
+
self._write_code(new_record)
|
|
244
|
+
|
|
245
|
+
return new_token
|
|
246
|
+
|
|
247
|
+
def get_active_codes(self) -> list[dict]:
|
|
248
|
+
"""获取所有 active 状态的配对码"""
|
|
249
|
+
codes = self._read_codes()
|
|
250
|
+
return [c for c in codes if c.get("status") == "active"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{"code": "Z02ZRG", "role": "admin", "status": "active", "created_at": "2026-03-05T15:47:25.324158+00:00"}
|
|
2
|
+
{"code": "6LL9JO", "role": "admin", "status": "used", "token": "tok_bRHd1ci_m5ziNGhGrza-OVUFqiu5dukqVUnZP3DB5rM", "paired_at": "2026-03-05T16:15:24.338472+00:00", "expires_at": "2026-04-04T16:15:24.338483+00:00"}
|
|
3
|
+
{"code": "1KEKFO", "role": "admin", "status": "used", "token": "tok_mQRErFLfPTOd5iuMM0tR3HlsL-ZwRjaoqAOsrd3f7o0", "paired_at": "2026-03-05T16:19:32.214747+00:00", "expires_at": "2026-04-04T16:19:32.214758+00:00"}
|
|
4
|
+
{"token": "tok_bRHd1ci_m5ziNGhGrza-OVUFqiu5dukqVUnZP3DB5rM", "status": "revoked", "revoked_at": "2026-03-05T17:16:47.003028+00:00"}
|
|
5
|
+
{"token": "tok_mQRErFLfPTOd5iuMM0tR3HlsL-ZwRjaoqAOsrd3f7o0", "status": "revoked", "revoked_at": "2026-03-05T17:16:51.276194+00:00"}
|
|
6
|
+
{"code": "62DLAO", "role": "admin", "status": "used", "token": "tok_xUecpGG2v64ET3OIvLvmMZx73AhwMH3GvbuFVYeWThQ", "paired_at": "2026-03-05T17:19:27.498121+00:00", "expires_at": "2026-04-04T17:19:27.498131+00:00"}
|
|
7
|
+
{"token": "tok_xUecpGG2v64ET3OIvLvmMZx73AhwMH3GvbuFVYeWThQ", "status": "revoked", "revoked_at": "2026-03-05T17:19:52.246926+00:00"}
|
|
8
|
+
{"code": "B2KC73", "role": "admin", "status": "used", "token": "tok_mtdU9v_mqwtx5Vzq4NpWL7X8oN_te9G3mKYKbyJ3KJQ", "paired_at": "2026-03-05T17:20:02.715650+00:00", "expires_at": "2026-04-04T17:20:02.715660+00:00"}
|
|
9
|
+
{"token": "tok_mtdU9v_mqwtx5Vzq4NpWL7X8oN_te9G3mKYKbyJ3KJQ", "status": "revoked", "revoked_at": "2026-03-05T17:24:19.299991+00:00"}
|
|
10
|
+
{"code": "RR46UW", "role": "admin", "status": "used", "token": "tok_okmAev1nyxMTpZr5NeIYR5i0GOoQBiBsyu-7O5Zt-bY", "paired_at": "2026-03-05T17:24:42.072484+00:00", "expires_at": "2026-04-04T17:24:42.072494+00:00"}
|
|
11
|
+
{"token": "tok_okmAev1nyxMTpZr5NeIYR5i0GOoQBiBsyu-7O5Zt-bY", "status": "revoked", "revoked_at": "2026-03-05T17:24:46.132417+00:00"}
|
|
12
|
+
{"code": "HUBWSE", "role": "admin", "status": "used", "token": "tok_ycxZ6hbiHg1xmuTbUX7yivvy__YMVYIn6cKvtuDPrM0", "paired_at": "2026-03-05T17:37:23.438551+00:00", "expires_at": "2026-04-04T17:37:23.438560+00:00"}
|
|
13
|
+
{"token": "tok_ycxZ6hbiHg1xmuTbUX7yivvy__YMVYIn6cKvtuDPrM0", "status": "revoked", "revoked_at": "2026-03-05T17:37:43.801272+00:00"}
|
|
14
|
+
{"code": "15Q4QJ", "role": "admin", "status": "used", "token": "tok_byVOicuvkitvDTQRuh05XQpmtAdyUineSddRCi5MHbs", "paired_at": "2026-03-05T17:38:50.616922+00:00", "expires_at": "2026-04-04T17:38:50.616932+00:00"}
|
|
15
|
+
{"token": "tok_byVOicuvkitvDTQRuh05XQpmtAdyUineSddRCi5MHbs", "status": "revoked", "revoked_at": "2026-03-06T07:55:02.015491+00:00"}
|
|
16
|
+
{"code": "7N5Q43", "role": "admin", "status": "used", "token": "tok_J8zYOS5OKcHkEyOzIFRD_sHbq61ngWqyoxlVQrDdhyA", "paired_at": "2026-03-06T07:55:19.446442+00:00", "expires_at": "2026-04-05T07:55:19.446452+00:00"}
|