astron-eval 0.0.1
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/LICENSE +21 -0
- package/README.md +119 -0
- package/bin/astron-eval.mjs +111 -0
- package/package.json +24 -0
- package/skills/astron-eval/SKILL.md +60 -0
- package/skills/model-evaluation/SKILL.md +180 -0
- package/skills/model-evaluation/assets/dimensions//345/206/205/345/256/271/347/233/270/345/205/263/346/200/247/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//345/206/205/345/256/271/347/262/276/347/241/256/347/273/264/345/272/246.json +19 -0
- package/skills/model-evaluation/assets/dimensions//345/207/206/347/241/256/346/200/247/347/273/264/345/272/246-/344/270/252/346/200/247/345/214/226/350/247/204/345/210/222.json +20 -0
- package/skills/model-evaluation/assets/dimensions//345/207/206/347/241/256/346/200/247/347/273/264/345/272/246-/344/277/241/346/201/257/345/210/206/346/236/220.json +20 -0
- package/skills/model-evaluation/assets/dimensions//345/207/206/347/241/256/346/200/247/347/273/264/345/272/246-/346/227/205/346/270/270/345/207/272/350/241/214.json +20 -0
- package/skills/model-evaluation/assets/dimensions//345/207/206/347/241/256/346/200/247/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//345/210/233/346/204/217/346/200/247-/345/220/270/345/274/225/346/200/247/347/273/264/345/272/246.json +21 -0
- package/skills/model-evaluation/assets/dimensions//345/210/233/346/226/260/346/200/247/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//345/256/214/346/225/264/346/200/247/347/273/264/345/272/246-/344/277/241/346/201/257/345/210/206/346/236/220.json +20 -0
- package/skills/model-evaluation/assets/dimensions//345/256/214/346/225/264/346/200/247/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//345/275/242/345/274/217/347/233/270/345/205/263/346/200/247/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//345/277/240/350/257/232/345/272/246/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//346/214/207/344/273/244/351/201/265/345/276/252/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//346/226/207/346/234/254/345/267/256/345/274/202/345/272/246-TER/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//346/234/211/346/225/210/346/200/247/347/273/264/345/272/246-/344/270/252/346/200/247/345/214/226/350/247/204/345/210/222.json +20 -0
- package/skills/model-evaluation/assets/dimensions//346/234/211/346/225/210/346/200/247/347/273/264/345/272/246-/344/277/241/346/201/257/345/210/206/346/236/220.json +20 -0
- package/skills/model-evaluation/assets/dimensions//346/234/211/346/225/210/346/200/247/347/273/264/345/272/246-/346/265/201/347/250/213/350/207/252/345/212/250/345/214/226.json +20 -0
- package/skills/model-evaluation/assets/dimensions//346/234/211/346/225/210/346/200/247/347/273/264/345/272/246.json +21 -0
- package/skills/model-evaluation/assets/dimensions//346/240/270/345/277/203/345/205/203/347/264/240/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//346/240/274/345/274/217/351/201/265/345/276/252/347/273/264/345/272/246.json +19 -0
- package/skills/model-evaluation/assets/dimensions//347/211/271/350/211/262/344/272/256/347/202/271/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//347/224/250/344/276/213/347/272/247/350/257/204/346/265/213/347/273/264/345/272/246/346/250/241/346/235/277.json +25 -0
- package/skills/model-evaluation/assets/dimensions//347/233/270/344/274/274/345/272/246-BERTScore/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//347/233/270/344/274/274/345/272/246-Cosine/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//347/233/270/344/274/274/345/272/246-ROUGE/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//347/233/270/345/205/263/346/200/247/347/273/264/345/272/246-/344/270/252/346/200/247/345/214/226/350/247/204/345/210/222.json +20 -0
- package/skills/model-evaluation/assets/dimensions//347/233/270/345/205/263/346/200/247/347/273/264/345/272/246.json +21 -0
- package/skills/model-evaluation/assets/dimensions//347/262/276/347/241/256/346/200/247-BLUE/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//347/262/276/347/241/256/346/200/247-COMET/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//351/200/273/350/276/221/345/220/210/347/220/206/346/200/247/347/273/264/345/272/246.json +20 -0
- package/skills/model-evaluation/assets/dimensions//351/200/273/350/276/221/350/277/236/350/264/257/346/200/247/347/273/264/345/272/246-/344/270/252/346/200/247/345/214/226/350/247/204/345/210/222.json +20 -0
- package/skills/model-evaluation/assets/dimensions//351/200/273/350/276/221/350/277/236/350/264/257/346/200/247/347/273/264/345/272/246-/344/277/241/346/201/257/345/210/206/346/236/220.json +20 -0
- package/skills/model-evaluation/assets/dimensions//351/200/273/350/276/221/350/277/236/350/264/257/346/200/247/347/273/264/345/272/246-/346/265/201/347/250/213/350/207/252/345/212/250/345/214/226.json +20 -0
- package/skills/model-evaluation/assets/dimensions//351/200/273/350/276/221/350/277/236/350/264/257/346/200/247/347/273/264/345/272/246.json +21 -0
- package/skills/model-evaluation/assets/eval-judge.json +11 -0
- package/skills/model-evaluation/assets/experts/business-process-automation.json +71 -0
- package/skills/model-evaluation/assets/experts/content-generation.json +75 -0
- package/skills/model-evaluation/assets/experts/content-match.json +37 -0
- package/skills/model-evaluation/assets/experts/information-analysis.json +87 -0
- package/skills/model-evaluation/assets/experts/marketing-digital-human.json +27 -0
- package/skills/model-evaluation/assets/experts/personalized-planning.json +87 -0
- package/skills/model-evaluation/assets/experts/text-translation.json +103 -0
- package/skills/model-evaluation/assets/experts/tourism-travel.json +119 -0
- package/skills/model-evaluation/assets/templates/custom-dimension.template.json +30 -0
- package/skills/model-evaluation/eval-build.md +281 -0
- package/skills/model-evaluation/eval-execute.md +196 -0
- package/skills/model-evaluation/eval-init.md +237 -0
- package/skills/model-evaluation/processes/dimension-process.md +207 -0
- package/skills/model-evaluation/processes/evalset-create-process.md +184 -0
- package/skills/model-evaluation/processes/evalset-parse-process.md +171 -0
- package/skills/model-evaluation/processes/evalset-supplement-process.md +136 -0
- package/skills/model-evaluation/processes/keypoint-process.md +148 -0
- package/skills/model-evaluation/processes/python-env-process.md +113 -0
- package/skills/model-evaluation/references//344/270/255/351/227/264/344/272/247/347/211/251/350/257/264/346/230/216.md +340 -0
- package/skills/model-evaluation/references//345/206/205/347/275/256/346/250/241/346/235/277/350/257/264/346/230/216.md +149 -0
- package/skills/model-evaluation/references//350/204/232/346/234/254/345/256/232/344/271/211.md +274 -0
- package/skills/model-evaluation/references//350/256/244/350/257/201/346/234/215/345/212/241/346/216/245/345/217/243/350/257/264/346/230/216.md +271 -0
- package/skills/model-evaluation/references//350/257/204/346/265/213/346/234/215/345/212/241/346/216/245/345/217/243/350/257/264/346/230/216.md +455 -0
- package/skills/model-evaluation/references//350/257/204/346/265/213/347/273/264/345/272/246/350/257/264/346/230/216.md +171 -0
- package/skills/model-evaluation/scripts/cfg/eval-auth.cfg +16 -0
- package/skills/model-evaluation/scripts/cfg/eval-server.cfg +1 -0
- package/skills/model-evaluation/scripts/clients/__init__.py +33 -0
- package/skills/model-evaluation/scripts/clients/api_client.py +97 -0
- package/skills/model-evaluation/scripts/clients/auth_client.py +96 -0
- package/skills/model-evaluation/scripts/clients/http_client.py +199 -0
- package/skills/model-evaluation/scripts/clients/oauth_callback.py +397 -0
- package/skills/model-evaluation/scripts/clients/token_manager.py +53 -0
- package/skills/model-evaluation/scripts/eval_auth.py +588 -0
- package/skills/model-evaluation/scripts/eval_dimension.py +240 -0
- package/skills/model-evaluation/scripts/eval_set.py +410 -0
- package/skills/model-evaluation/scripts/eval_task.py +324 -0
- package/skills/model-evaluation/scripts/files/__init__.py +38 -0
- package/skills/model-evaluation/scripts/files/file_utils.py +330 -0
- package/skills/model-evaluation/scripts/files/streaming.py +245 -0
- package/skills/model-evaluation/scripts/utils/__init__.py +128 -0
- package/skills/model-evaluation/scripts/utils/constants.py +101 -0
- package/skills/model-evaluation/scripts/utils/datetime_utils.py +60 -0
- package/skills/model-evaluation/scripts/utils/errors.py +244 -0
- package/skills/model-evaluation/scripts/utils/keypoint_prompts.py +73 -0
- package/skills/skill-driven-eval/SKILL.md +456 -0
- package/skills/skill-driven-eval/agents/grader.md +144 -0
- package/skills/skill-driven-eval/eval-viewer/__init__.py +1 -0
- package/skills/skill-driven-eval/eval-viewer/generate_report.py +485 -0
- package/skills/skill-driven-eval/eval-viewer/viewer.html +767 -0
- package/skills/skill-driven-eval/references/schemas.md +282 -0
- package/skills/skill-driven-eval/scripts/__init__.py +1 -0
- package/skills/skill-driven-eval/scripts/__main__.py +70 -0
- package/skills/skill-driven-eval/scripts/aggregate_results.py +681 -0
- package/skills/skill-driven-eval/scripts/extract_transcript.py +294 -0
- package/skills/skill-driven-eval/scripts/test_aggregate.py +244 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
OAuth2 本地回调服务器
|
|
4
|
+
启动临时 HTTP 服务器监听浏览器回调,接收授权码
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import hashlib
|
|
9
|
+
import secrets
|
|
10
|
+
import socket
|
|
11
|
+
import threading
|
|
12
|
+
import uuid
|
|
13
|
+
from typing import Dict, Optional, Tuple
|
|
14
|
+
from urllib.parse import parse_qs, urlparse
|
|
15
|
+
|
|
16
|
+
from utils.constants import (
|
|
17
|
+
DEFAULT_CALLBACK_HOST,
|
|
18
|
+
DEFAULT_CALLBACK_PORT,
|
|
19
|
+
DEFAULT_CALLBACK_PATH,
|
|
20
|
+
DEFAULT_CALLBACK_TIMEOUT,
|
|
21
|
+
)
|
|
22
|
+
from utils.errors import result
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# PKCE 工具函数
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
def generate_pkce_pair() -> Tuple[str, str]:
|
|
30
|
+
"""
|
|
31
|
+
生成 PKCE 的 verifier 和 challenge
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
(verifier, challenge) 元组
|
|
35
|
+
"""
|
|
36
|
+
# 生成 43-128 字符的随机 verifier
|
|
37
|
+
verifier = secrets.token_urlsafe(96)[:128]
|
|
38
|
+
|
|
39
|
+
# 计算 SHA256 的 challenge (S256 方法)
|
|
40
|
+
challenge_bytes = hashlib.sha256(verifier.encode()).digest()
|
|
41
|
+
# Base64 URL 安全编码,去掉 padding
|
|
42
|
+
challenge = base64.urlsafe_b64encode(challenge_bytes).decode().rstrip("=")
|
|
43
|
+
|
|
44
|
+
return verifier, challenge
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def generate_state_token() -> str:
|
|
48
|
+
"""
|
|
49
|
+
生成 state token (UUID 去掉连字符)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
32字符的 state token
|
|
53
|
+
"""
|
|
54
|
+
return uuid.uuid4().hex
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ============================================================================
|
|
58
|
+
# 回调结果
|
|
59
|
+
# ============================================================================
|
|
60
|
+
|
|
61
|
+
class CallbackResult:
|
|
62
|
+
"""回调结果数据类"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, code: str, state: str):
|
|
65
|
+
self.code = code
|
|
66
|
+
self.state = state
|
|
67
|
+
|
|
68
|
+
def __repr__(self) -> str:
|
|
69
|
+
return f"CallbackResult(code='***', state='{self.state[:8]}...')"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ============================================================================
|
|
73
|
+
# 本地回调服务器
|
|
74
|
+
# ============================================================================
|
|
75
|
+
|
|
76
|
+
class OAuthCallbackServer:
|
|
77
|
+
"""
|
|
78
|
+
OAuth2 本地回调服务器
|
|
79
|
+
|
|
80
|
+
启动临时 HTTP 服务器监听浏览器回调,接收授权码。
|
|
81
|
+
只绑定到 127.0.0.1,不绑定 0.0.0.0。
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
expected_state: str,
|
|
87
|
+
host: str = DEFAULT_CALLBACK_HOST,
|
|
88
|
+
port: int = 0,
|
|
89
|
+
callback_path: str = DEFAULT_CALLBACK_PATH,
|
|
90
|
+
timeout: int = DEFAULT_CALLBACK_TIMEOUT
|
|
91
|
+
):
|
|
92
|
+
"""
|
|
93
|
+
初始化回调服务器
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
expected_state: 预期的 state 值(用于校验)
|
|
97
|
+
host: 监听地址(默认 127.0.0.1)
|
|
98
|
+
port: 监听端口(0 表示自动选择)
|
|
99
|
+
callback_path: 回调路径(默认 /callback)
|
|
100
|
+
timeout: 超时时间(秒)
|
|
101
|
+
"""
|
|
102
|
+
self.expected_state = expected_state
|
|
103
|
+
self.host = host
|
|
104
|
+
self.port = port
|
|
105
|
+
self.callback_path = callback_path
|
|
106
|
+
self.timeout = timeout
|
|
107
|
+
|
|
108
|
+
self._socket: Optional[socket.socket] = None
|
|
109
|
+
self._actual_port: Optional[int] = None
|
|
110
|
+
self._result: Optional[CallbackResult] = None
|
|
111
|
+
self._error: Optional[str] = None
|
|
112
|
+
self._stop_event = threading.Event()
|
|
113
|
+
self._thread: Optional[threading.Thread] = None
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def actual_port(self) -> int:
|
|
117
|
+
"""实际监听的端口"""
|
|
118
|
+
return self._actual_port
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def redirect_uri(self) -> str:
|
|
122
|
+
"""回调 URI"""
|
|
123
|
+
return f"http://{self.host}:{self._actual_port}{self.callback_path}"
|
|
124
|
+
|
|
125
|
+
def start(self) -> Dict:
|
|
126
|
+
"""
|
|
127
|
+
启动回调服务器
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
result 字典,包含 redirect_uri
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
# 创建 socket
|
|
134
|
+
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
135
|
+
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
136
|
+
self._socket.bind((self.host, self.port))
|
|
137
|
+
self._socket.listen(1)
|
|
138
|
+
self._socket.settimeout(1.0) # 用于定期检查 stop 事件
|
|
139
|
+
|
|
140
|
+
self._actual_port = self._socket.getsockname()[1]
|
|
141
|
+
|
|
142
|
+
# 启动监听线程
|
|
143
|
+
self._thread = threading.Thread(target=self._listen, daemon=True)
|
|
144
|
+
self._thread.start()
|
|
145
|
+
|
|
146
|
+
return result(
|
|
147
|
+
"callback_server",
|
|
148
|
+
"started",
|
|
149
|
+
f"回调服务器已启动: {self.redirect_uri}",
|
|
150
|
+
data={"redirect_uri": self.redirect_uri, "port": self._actual_port},
|
|
151
|
+
success=True
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
except OSError as e:
|
|
155
|
+
return result(
|
|
156
|
+
"callback_server",
|
|
157
|
+
"error",
|
|
158
|
+
f"启动回调服务器失败: {e}",
|
|
159
|
+
success=False
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def _listen(self):
|
|
163
|
+
"""监听连接"""
|
|
164
|
+
while not self._stop_event.is_set():
|
|
165
|
+
try:
|
|
166
|
+
conn, addr = self._socket.accept()
|
|
167
|
+
conn.settimeout(5.0)
|
|
168
|
+
self._handle_connection(conn)
|
|
169
|
+
if self._result or self._error:
|
|
170
|
+
break
|
|
171
|
+
except socket.timeout:
|
|
172
|
+
continue
|
|
173
|
+
except OSError:
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
def _handle_connection(self, conn: socket.socket):
|
|
177
|
+
"""处理单个连接"""
|
|
178
|
+
try:
|
|
179
|
+
# 读取请求
|
|
180
|
+
request = b""
|
|
181
|
+
while b"\r\n\r\n" not in request:
|
|
182
|
+
chunk = conn.recv(1024)
|
|
183
|
+
if not chunk:
|
|
184
|
+
break
|
|
185
|
+
request += chunk
|
|
186
|
+
|
|
187
|
+
request_str = request.decode("utf-8", errors="ignore")
|
|
188
|
+
|
|
189
|
+
# 解析请求行
|
|
190
|
+
lines = request_str.split("\r\n")
|
|
191
|
+
if not lines:
|
|
192
|
+
self._send_response(conn, 400, "Bad Request")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
request_line = lines[0]
|
|
196
|
+
parts = request_line.split(" ")
|
|
197
|
+
if len(parts) < 2:
|
|
198
|
+
self._send_response(conn, 400, "Bad Request")
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
method, path = parts[0], parts[1]
|
|
202
|
+
|
|
203
|
+
# 只处理 GET 请求
|
|
204
|
+
if method != "GET":
|
|
205
|
+
self._send_response(conn, 405, "Method Not Allowed")
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# 解析路径
|
|
209
|
+
parsed = urlparse(path)
|
|
210
|
+
|
|
211
|
+
# 检查路径是否匹配
|
|
212
|
+
if parsed.path != self.callback_path:
|
|
213
|
+
self._send_response(conn, 404, "Not Found")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
# 解析查询参数
|
|
217
|
+
params = parse_qs(parsed.query)
|
|
218
|
+
code = params.get("code", [None])[0]
|
|
219
|
+
state = params.get("state", [None])[0]
|
|
220
|
+
|
|
221
|
+
# 验证参数
|
|
222
|
+
if not code:
|
|
223
|
+
self._send_response(conn, 400, "Missing authorization code")
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
if not state:
|
|
227
|
+
self._send_response(conn, 400, "Missing state parameter")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
if state != self.expected_state:
|
|
231
|
+
self._send_response(conn, 400, "State mismatch")
|
|
232
|
+
self._error = "State mismatch"
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
# 成功
|
|
236
|
+
self._result = CallbackResult(code=code, state=state)
|
|
237
|
+
self._send_success_response(conn)
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
self._error = str(e)
|
|
241
|
+
self._send_response(conn, 500, "Internal Server Error")
|
|
242
|
+
finally:
|
|
243
|
+
conn.close()
|
|
244
|
+
|
|
245
|
+
def _send_response(self, conn: socket.socket, status: int, message: str):
|
|
246
|
+
"""发送文本响应"""
|
|
247
|
+
body = f"{status} {message}".encode("utf-8")
|
|
248
|
+
response = (
|
|
249
|
+
f"HTTP/1.1 {status} {message}\r\n"
|
|
250
|
+
f"Content-Type: text/plain; charset=utf-8\r\n"
|
|
251
|
+
f"Content-Length: {len(body)}\r\n"
|
|
252
|
+
f"Connection: close\r\n"
|
|
253
|
+
f"\r\n"
|
|
254
|
+
).encode("utf-8") + body
|
|
255
|
+
try:
|
|
256
|
+
conn.sendall(response)
|
|
257
|
+
except Exception:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
def _send_success_response(self, conn: socket.socket):
|
|
261
|
+
"""发送成功 HTML 响应"""
|
|
262
|
+
html = """<!DOCTYPE html>
|
|
263
|
+
<html lang="zh-CN">
|
|
264
|
+
<head>
|
|
265
|
+
<meta charset="utf-8">
|
|
266
|
+
<title>登录成功</title>
|
|
267
|
+
<style>
|
|
268
|
+
body {
|
|
269
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
270
|
+
display: flex;
|
|
271
|
+
justify-content: center;
|
|
272
|
+
align-items: center;
|
|
273
|
+
height: 100vh;
|
|
274
|
+
margin: 0;
|
|
275
|
+
background: #f5f5f5;
|
|
276
|
+
}
|
|
277
|
+
.container {
|
|
278
|
+
text-align: center;
|
|
279
|
+
padding: 40px;
|
|
280
|
+
background: white;
|
|
281
|
+
border-radius: 8px;
|
|
282
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
283
|
+
}
|
|
284
|
+
.icon {
|
|
285
|
+
font-size: 48px;
|
|
286
|
+
color: #4CAF50;
|
|
287
|
+
margin-bottom: 20px;
|
|
288
|
+
}
|
|
289
|
+
h1 { color: #333; margin-bottom: 10px; }
|
|
290
|
+
p { color: #666; }
|
|
291
|
+
</style>
|
|
292
|
+
</head>
|
|
293
|
+
<body>
|
|
294
|
+
<div class="container">
|
|
295
|
+
<div class="icon">✓</div>
|
|
296
|
+
<h1>登录已回传到终端</h1>
|
|
297
|
+
<p>浏览器已经成功把授权结果回调到本地监听器。</p>
|
|
298
|
+
<p>你现在可以关闭这个页面,终端进程会继续完成 token 兑换。</p>
|
|
299
|
+
</div>
|
|
300
|
+
</body>
|
|
301
|
+
</html>"""
|
|
302
|
+
body = html.encode("utf-8")
|
|
303
|
+
response = (
|
|
304
|
+
"HTTP/1.1 200 OK\r\n"
|
|
305
|
+
"Content-Type: text/html; charset=utf-8\r\n"
|
|
306
|
+
f"Content-Length: {len(body)}\r\n"
|
|
307
|
+
"Cache-Control: no-store\r\n"
|
|
308
|
+
"Connection: close\r\n"
|
|
309
|
+
"\r\n"
|
|
310
|
+
).encode("utf-8") + body
|
|
311
|
+
try:
|
|
312
|
+
conn.sendall(response)
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
def wait_for_callback(self) -> Dict:
|
|
317
|
+
"""
|
|
318
|
+
等待浏览器回调
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
result 字典,包含 code 和 state
|
|
322
|
+
"""
|
|
323
|
+
if not self._thread:
|
|
324
|
+
return result(
|
|
325
|
+
"callback",
|
|
326
|
+
"error",
|
|
327
|
+
"回调服务器未启动",
|
|
328
|
+
success=False
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
self._thread.join(timeout=self.timeout)
|
|
332
|
+
|
|
333
|
+
if self._error:
|
|
334
|
+
return result(
|
|
335
|
+
"callback",
|
|
336
|
+
"error",
|
|
337
|
+
self._error,
|
|
338
|
+
success=False
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
if self._result:
|
|
342
|
+
return result(
|
|
343
|
+
"callback",
|
|
344
|
+
"success",
|
|
345
|
+
"收到授权码",
|
|
346
|
+
data={
|
|
347
|
+
"code": self._result.code,
|
|
348
|
+
"state": self._result.state
|
|
349
|
+
}
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
return result(
|
|
353
|
+
"callback",
|
|
354
|
+
"timeout",
|
|
355
|
+
f"等待回调超时 ({self.timeout}秒)",
|
|
356
|
+
success=False
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def stop(self):
|
|
360
|
+
"""停止回调服务器"""
|
|
361
|
+
self._stop_event.set()
|
|
362
|
+
if self._socket:
|
|
363
|
+
try:
|
|
364
|
+
self._socket.close()
|
|
365
|
+
except Exception:
|
|
366
|
+
pass
|
|
367
|
+
if self._thread and self._thread.is_alive():
|
|
368
|
+
self._thread.join(timeout=1.0)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# ============================================================================
|
|
372
|
+
# 便捷函数
|
|
373
|
+
# ============================================================================
|
|
374
|
+
|
|
375
|
+
def run_callback_server(
|
|
376
|
+
expected_state: str,
|
|
377
|
+
port: int = DEFAULT_CALLBACK_PORT,
|
|
378
|
+
timeout: int = DEFAULT_CALLBACK_TIMEOUT
|
|
379
|
+
) -> Tuple[Optional[OAuthCallbackServer], Dict]:
|
|
380
|
+
"""
|
|
381
|
+
启动回调服务器并返回服务器实例和启动结果
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
expected_state: 预期的 state 值
|
|
385
|
+
port: 监听端口(0 表示自动选择)
|
|
386
|
+
timeout: 超时时间
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
(server, result) 元组
|
|
390
|
+
"""
|
|
391
|
+
server = OAuthCallbackServer(
|
|
392
|
+
expected_state=expected_state,
|
|
393
|
+
port=port,
|
|
394
|
+
timeout=timeout
|
|
395
|
+
)
|
|
396
|
+
start_result = server.start()
|
|
397
|
+
return server, start_result
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Token 管理器模块
|
|
4
|
+
|
|
5
|
+
负责从 auth_file 读取 token,懒加载检查过期。
|
|
6
|
+
"""
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from utils.constants import ERR_REMOTE_AUTH_EXPIRED
|
|
10
|
+
from utils.errors import AuthExpiredError
|
|
11
|
+
from files.file_utils import load_json
|
|
12
|
+
from utils.datetime_utils import is_expired
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TokenManager:
|
|
16
|
+
"""
|
|
17
|
+
Token 管理器 - 懒加载检查 token 有效性
|
|
18
|
+
|
|
19
|
+
D-05: 负责从 auth_file 读取 token,懒加载检查过期
|
|
20
|
+
D-06: 请求时检查 token 有效性,无效时提示重新授权
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, auth_file: str):
|
|
24
|
+
self.auth_file = auth_file
|
|
25
|
+
self._token: Optional[str] = None
|
|
26
|
+
self._expires_at: Optional[str] = None
|
|
27
|
+
self._loaded = False
|
|
28
|
+
|
|
29
|
+
def get_token(self) -> str:
|
|
30
|
+
"""获取有效 token,懒加载并检查过期"""
|
|
31
|
+
if not self._loaded:
|
|
32
|
+
self._load_token()
|
|
33
|
+
if self._is_expired():
|
|
34
|
+
raise AuthExpiredError("Token 已过期,请重新授权")
|
|
35
|
+
return self._token
|
|
36
|
+
|
|
37
|
+
def _load_token(self):
|
|
38
|
+
"""从 auth_file 加载 token"""
|
|
39
|
+
result = load_json(self.auth_file)
|
|
40
|
+
if not result.get("success"):
|
|
41
|
+
raise FileNotFoundError(f"鉴权文件不存在: {self.auth_file}")
|
|
42
|
+
data = result.get("data", {})
|
|
43
|
+
self._token = data.get("access_token")
|
|
44
|
+
self._expires_at = data.get("expires_at")
|
|
45
|
+
self._loaded = True
|
|
46
|
+
if not self._token:
|
|
47
|
+
raise ValueError("鉴权文件中未找到 access_token")
|
|
48
|
+
|
|
49
|
+
def _is_expired(self) -> bool:
|
|
50
|
+
"""检查 token 是否过期"""
|
|
51
|
+
if not self._expires_at:
|
|
52
|
+
return True
|
|
53
|
+
return is_expired(self._expires_at)
|