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.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/bin/astron-eval.mjs +111 -0
  4. package/package.json +24 -0
  5. package/skills/astron-eval/SKILL.md +60 -0
  6. package/skills/model-evaluation/SKILL.md +180 -0
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. package/skills/model-evaluation/assets/dimensions//345/207/206/347/241/256/346/200/247/347/273/264/345/272/246.json +20 -0
  13. 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
  14. package/skills/model-evaluation/assets/dimensions//345/210/233/346/226/260/346/200/247/347/273/264/345/272/246.json +20 -0
  15. 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
  16. package/skills/model-evaluation/assets/dimensions//345/256/214/346/225/264/346/200/247/347/273/264/345/272/246.json +20 -0
  17. 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
  18. package/skills/model-evaluation/assets/dimensions//345/277/240/350/257/232/345/272/246/347/273/264/345/272/246.json +20 -0
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. package/skills/model-evaluation/assets/dimensions//346/234/211/346/225/210/346/200/247/347/273/264/345/272/246.json +21 -0
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. package/skills/model-evaluation/assets/dimensions//347/233/270/345/205/263/346/200/247/347/273/264/345/272/246.json +21 -0
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. package/skills/model-evaluation/assets/eval-judge.json +11 -0
  42. package/skills/model-evaluation/assets/experts/business-process-automation.json +71 -0
  43. package/skills/model-evaluation/assets/experts/content-generation.json +75 -0
  44. package/skills/model-evaluation/assets/experts/content-match.json +37 -0
  45. package/skills/model-evaluation/assets/experts/information-analysis.json +87 -0
  46. package/skills/model-evaluation/assets/experts/marketing-digital-human.json +27 -0
  47. package/skills/model-evaluation/assets/experts/personalized-planning.json +87 -0
  48. package/skills/model-evaluation/assets/experts/text-translation.json +103 -0
  49. package/skills/model-evaluation/assets/experts/tourism-travel.json +119 -0
  50. package/skills/model-evaluation/assets/templates/custom-dimension.template.json +30 -0
  51. package/skills/model-evaluation/eval-build.md +281 -0
  52. package/skills/model-evaluation/eval-execute.md +196 -0
  53. package/skills/model-evaluation/eval-init.md +237 -0
  54. package/skills/model-evaluation/processes/dimension-process.md +207 -0
  55. package/skills/model-evaluation/processes/evalset-create-process.md +184 -0
  56. package/skills/model-evaluation/processes/evalset-parse-process.md +171 -0
  57. package/skills/model-evaluation/processes/evalset-supplement-process.md +136 -0
  58. package/skills/model-evaluation/processes/keypoint-process.md +148 -0
  59. package/skills/model-evaluation/processes/python-env-process.md +113 -0
  60. 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
  61. 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
  62. package/skills/model-evaluation/references//350/204/232/346/234/254/345/256/232/344/271/211.md +274 -0
  63. 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
  64. 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
  65. 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
  66. package/skills/model-evaluation/scripts/cfg/eval-auth.cfg +16 -0
  67. package/skills/model-evaluation/scripts/cfg/eval-server.cfg +1 -0
  68. package/skills/model-evaluation/scripts/clients/__init__.py +33 -0
  69. package/skills/model-evaluation/scripts/clients/api_client.py +97 -0
  70. package/skills/model-evaluation/scripts/clients/auth_client.py +96 -0
  71. package/skills/model-evaluation/scripts/clients/http_client.py +199 -0
  72. package/skills/model-evaluation/scripts/clients/oauth_callback.py +397 -0
  73. package/skills/model-evaluation/scripts/clients/token_manager.py +53 -0
  74. package/skills/model-evaluation/scripts/eval_auth.py +588 -0
  75. package/skills/model-evaluation/scripts/eval_dimension.py +240 -0
  76. package/skills/model-evaluation/scripts/eval_set.py +410 -0
  77. package/skills/model-evaluation/scripts/eval_task.py +324 -0
  78. package/skills/model-evaluation/scripts/files/__init__.py +38 -0
  79. package/skills/model-evaluation/scripts/files/file_utils.py +330 -0
  80. package/skills/model-evaluation/scripts/files/streaming.py +245 -0
  81. package/skills/model-evaluation/scripts/utils/__init__.py +128 -0
  82. package/skills/model-evaluation/scripts/utils/constants.py +101 -0
  83. package/skills/model-evaluation/scripts/utils/datetime_utils.py +60 -0
  84. package/skills/model-evaluation/scripts/utils/errors.py +244 -0
  85. package/skills/model-evaluation/scripts/utils/keypoint_prompts.py +73 -0
  86. package/skills/skill-driven-eval/SKILL.md +456 -0
  87. package/skills/skill-driven-eval/agents/grader.md +144 -0
  88. package/skills/skill-driven-eval/eval-viewer/__init__.py +1 -0
  89. package/skills/skill-driven-eval/eval-viewer/generate_report.py +485 -0
  90. package/skills/skill-driven-eval/eval-viewer/viewer.html +767 -0
  91. package/skills/skill-driven-eval/references/schemas.md +282 -0
  92. package/skills/skill-driven-eval/scripts/__init__.py +1 -0
  93. package/skills/skill-driven-eval/scripts/__main__.py +70 -0
  94. package/skills/skill-driven-eval/scripts/aggregate_results.py +681 -0
  95. package/skills/skill-driven-eval/scripts/extract_transcript.py +294 -0
  96. 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">&#10003;</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)