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,588 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 鉴权Token管理脚本
5
+ 管理评测服务的鉴权Token,包括Token获取、缓存、检查和刷新
6
+
7
+ 支持两种登录模式:
8
+ 1. 手动模式(OOB):用户手动复制授权码
9
+ 2. 自动模式(Callback):本地启动回调服务器,自动接收授权码
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import os
15
+ import sys
16
+ import webbrowser
17
+ from datetime import datetime, timedelta
18
+ from typing import Optional, Dict, Any, Tuple
19
+
20
+ from utils import (
21
+ result,
22
+ DEFAULT_AUTH_CONFIG,
23
+ DEFAULT_AUTH_CACHE,
24
+ OOB_REDIRECT,
25
+ DEFAULT_CALLBACK_HOST,
26
+ DEFAULT_CALLBACK_PORT,
27
+ DEFAULT_CALLBACK_TIMEOUT,
28
+ is_expired,
29
+ NetworkError,
30
+ )
31
+ from files import (
32
+ load_json,
33
+ save_json,
34
+ load_config_yaml,
35
+ )
36
+ from clients import (
37
+ generate_pkce_pair,
38
+ generate_state_token,
39
+ OAuthCallbackServer,
40
+ AuthClient,
41
+ )
42
+
43
+
44
+ # ============================================================================
45
+ # 辅助函数
46
+ # ============================================================================
47
+
48
+ def _try_open_browser(url: str) -> bool:
49
+ """
50
+ 尝试打开浏览器
51
+
52
+ Args:
53
+ url: 要打开的URL
54
+
55
+ Returns:
56
+ 是否成功打开浏览器
57
+ """
58
+ try:
59
+ return webbrowser.open(url)
60
+ except Exception:
61
+ return False
62
+
63
+
64
+ def _save_state_token(output_path: str, state_token: str,
65
+ client_id: str,
66
+ pkce_verifier: Optional[str] = None):
67
+ """
68
+ 保存 state token 到临时文件
69
+
70
+ Args:
71
+ output_path: Token缓存文件路径(用于推导state文件路径)
72
+ state_token: 状态标识
73
+ client_id: 客户端标识
74
+ pkce_verifier: PKCE verifier(可选)
75
+ """
76
+ state_path = output_path.replace("auth.json", "state.json")
77
+ state_data = {
78
+ "state_token": state_token,
79
+ "client_id": client_id
80
+ }
81
+ if pkce_verifier:
82
+ state_data["pkce_verifier"] = pkce_verifier
83
+ save_json(state_path, state_data)
84
+
85
+
86
+ # ============================================================================
87
+ # Token 检查
88
+ # ============================================================================
89
+
90
+ def check_token(output_path: str) -> Dict[str, Any]:
91
+ """检查本地Token是否有效"""
92
+ load_result = load_json(output_path)
93
+
94
+ if not load_result.get("success"):
95
+ return result("check", "not_found", "Token缓存文件不存在")
96
+
97
+ auth_cache = load_result.get("data", {})
98
+
99
+ access_token = auth_cache.get("access_token")
100
+ if not access_token:
101
+ return result("check", "invalid", "Token不存在")
102
+
103
+ expires_at = auth_cache.get("expires_at")
104
+ if expires_at and is_expired(expires_at):
105
+ return result("check", "invalid", "Token已过期")
106
+
107
+ return result("check", "valid", "Token有效", {
108
+ "access_token": access_token,
109
+ "expires_at": expires_at
110
+ })
111
+
112
+
113
+ # ============================================================================
114
+ # 登录URL请求
115
+ # ============================================================================
116
+
117
+ def _request_login_url(client: AuthClient, auth_init_url: str,
118
+ state_token: str, redirect_uri: str,
119
+ client_id: str,
120
+ pkce_challenge: Optional[str] = None) -> Tuple[str, Optional[str]]:
121
+ """
122
+ 请求登录URL
123
+
124
+ Args:
125
+ client: AuthClient 实例
126
+ auth_init_url: 认证初始化 URL
127
+ state_token: 状态标识
128
+ redirect_uri: 回调地址
129
+ client_id: 客户端标识
130
+ pkce_challenge: PKCE challenge(可选)
131
+
132
+ Returns:
133
+ (login_url, error_message) - 成功时error_message为None
134
+ """
135
+ try:
136
+ payload = {
137
+ "state_token": state_token,
138
+ "redirect_uri": redirect_uri,
139
+ "client_id": client_id
140
+ }
141
+ # 如果启用 PKCE,添加 challenge
142
+ if pkce_challenge:
143
+ payload["code_challenge"] = pkce_challenge
144
+ payload["code_challenge_method"] = "S256"
145
+
146
+ resp = client.request_full_url("POST", auth_init_url, json=payload)
147
+ login_url = resp.get("login_url") if isinstance(resp, dict) else None
148
+ if not login_url:
149
+ return None, "未获取到登录地址"
150
+ return login_url, None
151
+ except NetworkError as e:
152
+ return None, f"登录初始化失败: {e}"
153
+
154
+
155
+ # ============================================================================
156
+ # Token 换取
157
+ # ============================================================================
158
+
159
+ def exchange_token(client: AuthClient, token_url: str, code: str, state_token: str,
160
+ output_path: str = DEFAULT_AUTH_CACHE,
161
+ client_id: str = None,
162
+ pkce_verifier: Optional[str] = None) -> Dict[str, Any]:
163
+ """
164
+ 使用授权码换取Token
165
+
166
+ Args:
167
+ client: AuthClient 实例
168
+ token_url: Token换取 URL
169
+ code: 授权码
170
+ state_token: 状态标识
171
+ output_path: Token缓存文件路径
172
+ client_id: 客户端标识
173
+ pkce_verifier: PKCE verifier(可选)
174
+ """
175
+ # 验证state_token并获取client_id
176
+ state_path = output_path.replace('auth.json', 'state.json')
177
+ state_result = load_json(state_path)
178
+ cached_state = state_result.get("data") if state_result.get("success") else None
179
+ if cached_state and cached_state.get("state_token") != state_token:
180
+ return result("token", "error", "状态标识不匹配,可能存在CSRF攻击", success=False)
181
+
182
+ # 从 state 文件获取 client_id,否则使用传入的值
183
+ if not client_id:
184
+ client_id = cached_state.get("client_id") if cached_state else None
185
+
186
+ # 构建请求
187
+ payload = {
188
+ "grant_type": "authorization_code",
189
+ "code": code,
190
+ "state": state_token,
191
+ "client_id": client_id
192
+ }
193
+ # 如果启用 PKCE,添加 verifier
194
+ if pkce_verifier:
195
+ payload["code_verifier"] = pkce_verifier
196
+
197
+ # 请求Token
198
+ try:
199
+ resp = client.request_full_url("POST", token_url, json=payload)
200
+
201
+ token_data = resp.get("data", resp) if isinstance(resp, dict) else resp
202
+ access_token = token_data.get("access_token")
203
+ if not access_token:
204
+ return result("token", "error", "未获取到access_token", success=False)
205
+
206
+ # 计算过期时间
207
+ expires_in = token_data.get("expires_in", 7200)
208
+ now = datetime.now().astimezone()
209
+ expires_at = now + timedelta(seconds=expires_in)
210
+
211
+ # 保存Token
212
+ auth_cache = {
213
+ "access_token": access_token,
214
+ "expires_in": expires_in,
215
+ "created_at": now.isoformat(),
216
+ "expires_at": expires_at.isoformat()
217
+ }
218
+ save_json(output_path, auth_cache)
219
+
220
+ # 清理state缓存
221
+ if os.path.exists(state_path):
222
+ os.remove(state_path)
223
+
224
+ return result("token", "success", "Token获取成功", {
225
+ "access_token": access_token,
226
+ "expires_in": expires_in,
227
+ "created_at": auth_cache["created_at"],
228
+ "expires_at": auth_cache["expires_at"]
229
+ })
230
+ except NetworkError as e:
231
+ return result("token", "error", f"Token获取失败: {e}", success=False)
232
+
233
+
234
+ # ============================================================================
235
+ # 环境检测
236
+ # ============================================================================
237
+
238
+ def detect_browser_environment() -> Dict[str, Any]:
239
+ """
240
+ 检测环境是否支持自动打开浏览器
241
+
242
+ Returns:
243
+ - can_auto: 是否支持自动模式
244
+ - reason: 判断原因
245
+ """
246
+ import platform
247
+ system = platform.system()
248
+
249
+ # Windows/macOS 默认有图形界面
250
+ if system in ("Windows", "Darwin"):
251
+ return {"can_auto": True, "reason": f"{system}系统默认支持浏览器"}
252
+
253
+ # Linux 检测显示环境
254
+ if system == "Linux" and not (os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")):
255
+ return {"can_auto": False, "reason": "Linux环境无显示服务,检测为服务器终端"}
256
+
257
+ # 尝试检测浏览器
258
+ try:
259
+ if webbrowser.get():
260
+ return {"can_auto": True, "reason": "检测到可用浏览器"}
261
+ except Exception:
262
+ pass
263
+
264
+ return {"can_auto": False, "reason": "无法检测到可用浏览器"}
265
+
266
+
267
+ # ============================================================================
268
+ # 回调模式登录
269
+ # ============================================================================
270
+
271
+ def auto_login(
272
+ client: AuthClient,
273
+ config: Dict,
274
+ output_path: str = DEFAULT_AUTH_CACHE,
275
+ port: int = DEFAULT_CALLBACK_PORT,
276
+ timeout: int = DEFAULT_CALLBACK_TIMEOUT,
277
+ use_pkce: bool = True
278
+ ) -> Dict[str, Any]:
279
+ """
280
+ 自动登录流程(回调模式)
281
+
282
+ 1. 启动本地回调服务器
283
+ 2. 请求登录URL
284
+ 3. 打开浏览器
285
+ 4. 等待回调接收授权码
286
+ 5. 换取Token并保存
287
+
288
+ Args:
289
+ client: AuthClient 实例
290
+ config: 配置字典
291
+ output_path: Token缓存文件路径
292
+ port: 回调服务器端口
293
+ timeout: 超时时间(秒)
294
+ use_pkce: 是否启用PKCE
295
+
296
+ Returns:
297
+ result 字典
298
+ """
299
+ # 回调模式使用 callback_client_id
300
+ client_id = config.get("callback_client_id") or config.get("client_id")
301
+
302
+ # 生成 state token
303
+ state_token = generate_state_token()
304
+
305
+ # 生成 PKCE pair(可选)
306
+ pkce_verifier = None
307
+ pkce_challenge = None
308
+ if use_pkce:
309
+ pkce_verifier, pkce_challenge = generate_pkce_pair()
310
+
311
+ # 启动回调服务器
312
+ callback_server = OAuthCallbackServer(
313
+ expected_state=state_token,
314
+ host=DEFAULT_CALLBACK_HOST,
315
+ port=port,
316
+ timeout=timeout
317
+ )
318
+ start_result = callback_server.start()
319
+ if not start_result.get("success"):
320
+ return start_result
321
+
322
+ redirect_uri = callback_server.redirect_uri
323
+
324
+ try:
325
+ # 请求登录URL
326
+ login_url, error = _request_login_url(
327
+ client, config["auth_init_url"], state_token, redirect_uri, client_id, pkce_challenge
328
+ )
329
+ if error:
330
+ # 如果是 redirect_uri 不被接受的错误,返回特定状态以便上层回退到 OOB 模式
331
+ if "未获取到登录地址" in error:
332
+ return result("login", "redirect_uri_rejected", error, success=False)
333
+ return result("login", "error", error, success=False)
334
+
335
+ # 保存 state(用于后续验证)
336
+ _save_state_token(output_path, state_token, client_id, pkce_verifier)
337
+
338
+ # 打开浏览器
339
+ browser_opened = _try_open_browser(login_url)
340
+
341
+ if not browser_opened:
342
+ # 浏览器未打开,返回特定状态以便上层回退到手动模式
343
+ return result("login", "browser_failed", "无法打开浏览器", success=False)
344
+
345
+ # 等待回调
346
+ callback_result = callback_server.wait_for_callback()
347
+
348
+ if not callback_result.get("success"):
349
+ return callback_result
350
+
351
+ # 获取授权码
352
+ code = callback_result.get("data", {}).get("code")
353
+
354
+ # 换取 Token
355
+ token_result = exchange_token(
356
+ client, config["token_url"], code, state_token, output_path, client_id, pkce_verifier
357
+ )
358
+
359
+ return token_result
360
+
361
+ finally:
362
+ callback_server.stop()
363
+
364
+
365
+ # ============================================================================
366
+ # 手动登录
367
+ # ============================================================================
368
+
369
+ def manual_login(
370
+ client: AuthClient,
371
+ config: Dict,
372
+ output_path: str = DEFAULT_AUTH_CACHE
373
+ ) -> Dict[str, Any]:
374
+ """
375
+ 手动登录流程(OOB模式)
376
+
377
+ 返回登录链接,用户自行访问完成授权,适合服务器终端等无图形界面环境。
378
+
379
+ Args:
380
+ client: AuthClient 实例
381
+ config: 配置字典
382
+ output_path: Token缓存文件路径
383
+
384
+ Returns:
385
+ result 字典,包含 login_url 和 state_token
386
+ """
387
+ # 手动模式使用 oob_client_id
388
+ client_id = config.get("oob_client_id") or config.get("client_id")
389
+
390
+ # 生成state并请求登录URL
391
+ state_token = generate_state_token()
392
+ login_url, error = _request_login_url(client, config["auth_init_url"], state_token, OOB_REDIRECT, client_id)
393
+ if error:
394
+ return result("login", "error", error, success=False)
395
+
396
+ # 保存state
397
+ _save_state_token(output_path, state_token, client_id)
398
+
399
+ return result("login", "manual_url", "请访问登录链接完成授权", {
400
+ "login_url": login_url,
401
+ "state_token": state_token,
402
+ "mode": "oob"
403
+ }, success=True)
404
+
405
+
406
+ # ============================================================================
407
+ # 智能登录入口
408
+ # ============================================================================
409
+
410
+ def login(
411
+ config_path: str,
412
+ output_path: str = DEFAULT_AUTH_CACHE,
413
+ force_mode: Optional[str] = None,
414
+ use_callback: bool = True,
415
+ callback_port: int = DEFAULT_CALLBACK_PORT
416
+ ) -> Dict[str, Any]:
417
+ """
418
+ 智能登录流程:自动选择模式并完成登录
419
+
420
+ 流程:
421
+ 1. 检测环境是否支持浏览器
422
+ 2. 若支持且 use_callback=True,使用回调模式(自动完成)
423
+ 3. 否则使用 OOB 模式(需手动输入授权码)
424
+
425
+ Args:
426
+ config_path: 配置文件路径
427
+ output_path: Token缓存文件路径
428
+ force_mode: 强制指定模式,"auto"或"manual"
429
+ use_callback: 是否优先使用回调模式
430
+ callback_port: 回调服务器端口
431
+
432
+ Returns:
433
+ 统一 result() 格式的字典
434
+ """
435
+ # 加载配置
436
+ config_result = load_config_yaml(config_path)
437
+ config = config_result.get("data", {})
438
+
439
+ # 创建 AuthClient 实例(使用完整 URL 请求,无需 base_url)
440
+ client = AuthClient()
441
+
442
+ # 环境检测
443
+ env_info = detect_browser_environment()
444
+ can_auto = env_info["can_auto"]
445
+
446
+ # 根据 force_mode 覆盖
447
+ if force_mode == "auto":
448
+ can_auto = True
449
+ elif force_mode == "manual":
450
+ can_auto = False
451
+
452
+ # 选择登录模式
453
+ if can_auto and use_callback:
454
+ # 回调模式:自动完成整个流程
455
+ callback_result = auto_login(
456
+ client=client,
457
+ config=config,
458
+ output_path=output_path,
459
+ port=callback_port
460
+ )
461
+
462
+ # 如果回调模式失败,自动回退到手动模式
463
+ if not callback_result.get("success"):
464
+ status = callback_result.get("status")
465
+ if status in ("redirect_uri_rejected", "browser_failed"):
466
+ return manual_login(
467
+ client=client,
468
+ config=config,
469
+ output_path=output_path
470
+ )
471
+
472
+ return callback_result
473
+ else:
474
+ # 手动模式:返回登录URL等待手动输入
475
+ return manual_login(
476
+ client=client,
477
+ config=config,
478
+ output_path=output_path
479
+ )
480
+
481
+
482
+ # ============================================================================
483
+ # CLI 入口
484
+ # ============================================================================
485
+
486
+ def main():
487
+ parser = argparse.ArgumentParser(
488
+ description="评测服务鉴权Token管理脚本",
489
+ formatter_class=argparse.RawDescriptionHelpFormatter,
490
+ epilog="""
491
+ 示例:
492
+ # 智能登录(自动选择最佳模式)
493
+ python eval_auth.py login
494
+
495
+ # 强制使用自动模式(回调)
496
+ python eval_auth.py login --mode auto
497
+
498
+ # 强制使用手动模式
499
+ python eval_auth.py login --mode manual
500
+
501
+ # 指定回调端口
502
+ python eval_auth.py login --mode auto --port 8080
503
+
504
+ # 手动输入授权码换取Token
505
+ python eval_auth.py token --code <code> --state_token <state>
506
+
507
+ # 检查Token有效性
508
+ python eval_auth.py check
509
+
510
+ # 检测浏览器环境
511
+ python eval_auth.py detect
512
+ """
513
+ )
514
+ subparsers = parser.add_subparsers(dest="command",
515
+ help="可用命令")
516
+
517
+ # detect 子命令
518
+ detect_parser = subparsers.add_parser("detect", help="检测浏览器环境")
519
+ detect_parser.add_argument("--output", default=DEFAULT_AUTH_CACHE,
520
+ help="Token缓存文件路径")
521
+
522
+ # login 子命令
523
+ login_parser = subparsers.add_parser("login", help="智能登录授权")
524
+ login_parser.add_argument("--config", default=DEFAULT_AUTH_CONFIG,
525
+ help="鉴权配置文件路径")
526
+ login_parser.add_argument("--output", default=DEFAULT_AUTH_CACHE,
527
+ help="Token缓存文件路径")
528
+ login_parser.add_argument("--mode", choices=["auto", "manual"],
529
+ default=None, help="登录模式:auto(自动) 或 manual(手动),默认自动选择")
530
+ login_parser.add_argument("--port", type=int, default=DEFAULT_CALLBACK_PORT,
531
+ help=f"回调模式端口(默认 {DEFAULT_CALLBACK_PORT})")
532
+
533
+ # token 子命令
534
+ token_parser = subparsers.add_parser("token", help="授权码换取Token")
535
+ token_parser.add_argument("--code", required=True, help="授权码")
536
+ token_parser.add_argument("--state_token", required=True, help="状态标识")
537
+ token_parser.add_argument("--config", default=DEFAULT_AUTH_CONFIG,
538
+ help="鉴权配置文件路径")
539
+ token_parser.add_argument("--output", default=DEFAULT_AUTH_CACHE,
540
+ help="Token缓存文件路径")
541
+
542
+ # check 子命令
543
+ check_parser = subparsers.add_parser("check", help="检查Token有效性")
544
+ check_parser.add_argument("--output", default=DEFAULT_AUTH_CACHE,
545
+ help="Token缓存文件路径")
546
+
547
+ args = parser.parse_args()
548
+
549
+ # Python 3.6 兼容:手动检查子命令
550
+ if args.command is None:
551
+ parser.error("请指定子命令: check, detect, login, token")
552
+
553
+ # 分发到对应处理函数
554
+ if args.command == "check":
555
+ result_data = check_token(args.output)
556
+ elif args.command == "detect":
557
+ result_data = result("detect", "success", "环境检测完成",
558
+ detect_browser_environment())
559
+ elif args.command == "login":
560
+ # 根据 mode 确定登录方式
561
+ force_mode = args.mode
562
+ use_callback = (args.mode != "manual") # 非 manual 模式都尝试回调
563
+
564
+ result_data = login(
565
+ config_path=args.config,
566
+ output_path=args.output,
567
+ force_mode=force_mode,
568
+ use_callback=use_callback,
569
+ callback_port=args.port
570
+ )
571
+ elif args.command == "token":
572
+ # 加载配置
573
+ config_result = load_config_yaml(args.config)
574
+ config = config_result.get("data", {})
575
+
576
+ # 创建 AuthClient
577
+ client = AuthClient()
578
+
579
+ result_data = exchange_token(
580
+ client, config["token_url"], args.code, args.state_token, args.output
581
+ )
582
+
583
+ print(json.dumps(result_data, indent=2, ensure_ascii=False))
584
+ sys.exit(0 if result_data.get("success") else 1)
585
+
586
+
587
+ if __name__ == "__main__":
588
+ main()