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,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()
|