Files
codexautopool/backend/internal/auth/codex_api.go

719 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package auth
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"strings"
"time"
"codex-pool/internal/client"
"codex-pool/internal/mail"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
// FixedAuthURL 固定的授权 URL用于测试绕过 S2A
const FixedAuthURL = "https://auth.openai.com/oauth/authorize?client_id=app_EMoamEEZ73f0CkXaXp7hrann&code_challenge=fEepJO0_NJiqP-_FC_HLH-aqZsq68JeFtNvYY6q3qbQ&code_challenge_method=S256&codex_cli_simplified_flow=true&id_token_add_organizations=true&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&response_type=code&scope=openid+profile+email+offline_access&state=da3ec35b7368c91193d27c90e3ecd0c4fa45bebd430bcc6b5c236461d2742e93"
// UseFixedAuthURL 是否使用固定的授权 URL设为 true 可绕过 S2A 进行测试)
var UseFixedAuthURL = false
// 常量 CodexClientID, CodexRedirectURI, CodexScope 已在 s2a.go 中定义
// CodexAPIAuth 纯 API 授权 (无浏览器) - 基于 get_code.go 的实现
type CodexAPIAuth struct {
tlsClient *client.TLSClient
email string
password string
workspaceID string
authURL string // S2A 生成的授权 URL
sessionID string // S2A 会话 ID
deviceID string
sid string
sentinelToken string
solvedPow string
userAgent string
secChUa string
secChPlatform string
proxyURL string
logger *AuthLogger
}
// NewCodexAPIAuth 创建 CodexAuth 实例(使用随机 TLS 指纹)
// authURL 和 sessionID 由 S2A 生成
func NewCodexAPIAuth(email, password, workspaceID, authURL, sessionID, proxy string, logger *AuthLogger) (*CodexAPIAuth, error) {
tlsClient, err := client.New(proxy)
if err != nil {
return nil, fmt.Errorf("创建 TLS 客户端失败: %v", err)
}
fpInfo := tlsClient.GetFingerprintInfo()
ua, secChUa, secChPlatform := tlsClient.GetHeadersInfo()
if logger != nil {
logger.LogStep(StepBrowserStart, "指纹: %s", fpInfo)
}
return &CodexAPIAuth{
tlsClient: tlsClient,
email: email,
password: password,
workspaceID: workspaceID,
authURL: authURL,
sessionID: sessionID,
deviceID: generateUUID(),
sid: generateUUID(),
userAgent: ua,
secChUa: secChUa,
secChPlatform: secChPlatform,
proxyURL: proxy,
logger: logger,
}, nil
}
// generateUUID 生成 UUID v4
func generateUUID() string {
b := make([]byte, 16)
rand.Read(b)
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}
// fnv1a32 FNV-1a 32-bit hash
func fnv1a32(data []byte) uint32 {
h := uint32(2166136261)
for _, b := range data {
h ^= uint32(b)
h *= 16777619
}
h ^= (h >> 16)
h *= 2246822507
h ^= (h >> 13)
h *= 3266489909
h ^= (h >> 16)
return h
}
// getParseTime 生成 JS Date().toString() 格式的时间字符串
func getParseTime() string {
loc, _ := time.LoadLocation("Asia/Shanghai")
now := time.Now().In(loc)
days := []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}
months := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
return fmt.Sprintf("%s %s %02d %d %02d:%02d:%02d GMT+0800 (中国标准时间)",
days[now.Weekday()], months[now.Month()-1], now.Day(), now.Year(),
now.Hour(), now.Minute(), now.Second())
}
// getConfig 生成 PoW 配置数组
func (c *CodexAPIAuth) getConfig() []interface{} {
return []interface{}{
rand.Intn(1000) + 2500,
getParseTime(),
4294967296,
0,
c.userAgent,
"chrome-extension://pgojnojmmhpofjgdmaebadhbocahppod/assets/aW5qZWN0X2hhc2g/aW5qZ",
nil,
"zh-CN",
"zh-CN",
0,
"canSharefunction canShare() { [native code] }",
fmt.Sprintf("_reactListening%d", rand.Intn(9000000)+1000000),
"onhashchange",
float64(time.Now().UnixNano()) / 1e6,
c.sid,
"",
24,
time.Now().UnixMilli() - int64(rand.Intn(40000)+10000),
}
}
// solvePow 解决 PoW 挑战
func (c *CodexAPIAuth) solvePow(seed, difficulty string, cfg []interface{}, maxIterations int) string {
startTime := time.Now()
seedBytes := []byte(seed)
for i := 0; i < maxIterations; i++ {
cfg[3] = i
cfg[9] = 0
jsonStr, _ := json.Marshal(cfg)
encoded := base64.StdEncoding.EncodeToString(jsonStr)
combined := append(seedBytes, []byte(encoded)...)
h := fnv1a32(combined)
hexHash := fmt.Sprintf("%08x", h)
if hexHash[:len(difficulty)] <= difficulty {
elapsed := time.Since(startTime).Seconds()
c.logStep(StepNavigate, "[PoW] Solved in %.2fs (iteration %d, difficulty=%s)", elapsed, i, difficulty)
return encoded + "~S"
}
}
return ""
}
// getRequirementsToken 生成初始 token
func (c *CodexAPIAuth) getRequirementsToken() string {
cfg := c.getConfig()
cfg[3] = 0
cfg[9] = 0
jsonStr, _ := json.Marshal(cfg)
encoded := base64.StdEncoding.EncodeToString(jsonStr)
return "gAAAAAC" + encoded + "~S"
}
// doRequest 执行 HTTP 请求(通过 TLSClient 随机指纹)
func (c *CodexAPIAuth) doRequest(method, urlStr string, body interface{}, headers map[string]string) (*http.Response, []byte, error) {
var bodyReader io.Reader
if body != nil {
jsonBody, _ := json.Marshal(body)
bodyReader = bytes.NewReader(jsonBody)
}
req, err := http.NewRequest(method, urlStr, bodyReader)
if err != nil {
return nil, nil, err
}
// 设置默认头(使用指纹动态生成的值)
req.Header.Set("User-Agent", c.userAgent)
req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
if c.secChUa != "" {
req.Header.Set("sec-ch-ua", c.secChUa)
}
req.Header.Set("sec-ch-ua-mobile", "?0")
if c.secChPlatform != "" {
req.Header.Set("sec-ch-ua-platform", c.secChPlatform)
}
req.Header.Set("sec-fetch-dest", "empty")
req.Header.Set("sec-fetch-mode", "cors")
req.Header.Set("sec-fetch-site", "same-origin")
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := c.tlsClient.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
return resp, respBody, nil
}
// callSentinelReq 调用 Sentinel 获取 token
func (c *CodexAPIAuth) callSentinelReq(flow string) bool {
initToken := c.getRequirementsToken()
payload := map[string]string{
"p": initToken,
"id": c.deviceID,
"flow": flow,
}
headers := map[string]string{"Content-Type": "application/json"}
resp, body, err := c.doRequest("POST", "https://sentinel.openai.com/backend-api/sentinel/req", payload, headers)
if err != nil || resp.StatusCode != 200 {
c.logError(StepNavigate, "Sentinel 请求失败: %v (status: %d)", err, resp.StatusCode)
return false
}
var data map[string]interface{}
json.Unmarshal(body, &data)
if token, ok := data["token"].(string); ok {
c.sentinelToken = token
}
if powReq, ok := data["proofofwork"].(map[string]interface{}); ok {
if required, ok := powReq["required"].(bool); ok && required {
seed, _ := powReq["seed"].(string)
difficulty, _ := powReq["difficulty"].(string)
c.logStep(StepNavigate, "[Sentinel] flow=%s, PoW required, difficulty=%s", flow, difficulty)
config := c.getConfig()
solved := c.solvePow(seed, difficulty, config, 5000000)
if solved != "" {
c.solvedPow = "gAAAAAB" + solved
} else {
c.logError(StepNavigate, "PoW 求解失败")
return false
}
} else {
c.logStep(StepNavigate, "[Sentinel] flow=%s, PoW not required", flow)
c.solvedPow = initToken
}
} else {
c.logStep(StepNavigate, "[Sentinel] flow=%s, no PoW in response", flow)
c.solvedPow = initToken
}
return true
}
// getSentinelHeader 构建 sentinel header
func (c *CodexAPIAuth) getSentinelHeader(flow string) string {
sentinelObj := map[string]string{
"p": c.solvedPow,
"id": c.deviceID,
"flow": flow,
}
if c.sentinelToken != "" {
sentinelObj["c"] = c.sentinelToken
}
header, _ := json.Marshal(sentinelObj)
return string(header)
}
// logStep 记录日志
func (c *CodexAPIAuth) logStep(step AuthStep, format string, args ...interface{}) {
if c.logger != nil {
c.logger.LogStep(step, format, args...)
}
}
// logError 记录错误
func (c *CodexAPIAuth) logError(step AuthStep, format string, args ...interface{}) {
if c.logger != nil {
c.logger.LogError(step, format, args...)
}
}
// GetSessionID 获取 S2A 会话 ID用于后续入库
func (c *CodexAPIAuth) GetSessionID() string {
return c.sessionID
}
// ObtainAuthorizationCode 获取授权码(全局 3 分钟超时)
func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
type authResult struct {
code string
err error
}
resultCh := make(chan authResult, 1)
go func() {
code, err := c.obtainAuthorizationCodeInternal()
resultCh <- authResult{code, err}
}()
select {
case r := <-resultCh:
return r.code, r.err
case <-time.After(3 * time.Minute):
return "", fmt.Errorf("授权超时 (3分钟)")
}
}
// obtainAuthorizationCodeInternal ObtainAuthorizationCode 的内部实现
func (c *CodexAPIAuth) obtainAuthorizationCodeInternal() (string, error) {
c.logStep(StepNavigate, "开始 Codex API 授权流程...")
// 选择使用固定 URL 还是 S2A 生成的 URL
authURL := c.authURL
if UseFixedAuthURL {
authURL = FixedAuthURL
c.logStep(StepNavigate, "使用固定授权 URL测试模式")
} else if authURL == "" {
return "", fmt.Errorf("authURL 未设置,请先通过 S2A 生成授权 URL")
}
headers := map[string]string{
"Origin": "https://auth.openai.com",
"Referer": "https://auth.openai.com/log-in",
"Content-Type": "application/json",
}
// 访问授权页面并手动跟随重定向
c.logStep(StepNavigate, "访问授权页面...")
resp, _, err := c.doRequest("GET", authURL, nil, headers)
if err != nil {
c.logError(StepNavigate, "访问授权页失败: %v", err)
return "", fmt.Errorf("访问授权页失败: %v", err)
}
// 手动跟随重定向
currentURL := authURL
for resp.StatusCode >= 300 && resp.StatusCode < 400 {
location := resp.Header.Get("Location")
if location == "" {
break
}
if !strings.HasPrefix(location, "http") {
parsedURL, _ := url.Parse(currentURL)
location = parsedURL.Scheme + "://" + parsedURL.Host + location
}
currentURL = location
resp, _, err = c.doRequest("GET", currentURL, nil, headers)
if err != nil {
break
}
}
headers["Referer"] = currentURL
// 提交邮箱
c.logStep(StepInputEmail, "提交邮箱: %s", c.email)
if !c.callSentinelReq("login_web_init") {
return "", fmt.Errorf("Sentinel 请求失败")
}
authHeaders := make(map[string]string)
for k, v := range headers {
authHeaders[k] = v
}
authHeaders["OpenAI-Sentinel-Token"] = c.getSentinelHeader("authorize_continue")
emailPayload := map[string]interface{}{
"username": map[string]string{
"kind": "email",
"value": c.email,
},
}
resp, body, err := c.doRequest("POST", "https://auth.openai.com/api/accounts/authorize/continue", emailPayload, authHeaders)
if err != nil || resp.StatusCode != 200 {
c.logError(StepInputEmail, "提交邮箱失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))]))
return "", fmt.Errorf("提交邮箱失败: %d", resp.StatusCode)
}
var data map[string]interface{}
json.Unmarshal(body, &data)
// 检查是否需要密码验证
pageType := ""
if page, ok := data["page"].(map[string]interface{}); ok {
if pt, ok := page["type"].(string); ok {
pageType = pt
}
}
c.logStep(StepInputPassword, "验证密码...")
if pageType == "password" || strings.Contains(string(body), "password") {
// 在密码验证前记录最新邮件ID (基于 get_code.go 的增强逻辑)
latestEmailID := mail.GetLatestEmailID(c.email)
c.logStep(StepInputPassword, "记录当前最新邮件ID: %d", latestEmailID)
// 5. 验证密码
if !c.callSentinelReq("authorize_continue__auto") {
return "", fmt.Errorf("Sentinel 请求失败")
}
verifyHeaders := make(map[string]string)
for k, v := range headers {
verifyHeaders[k] = v
}
verifyHeaders["OpenAI-Sentinel-Token"] = c.getSentinelHeader("password_verify")
passwordPayload := map[string]string{
"password": c.password,
}
resp, body, err = c.doRequest("POST", "https://auth.openai.com/api/accounts/password/verify", passwordPayload, verifyHeaders)
if err != nil || resp.StatusCode != 200 {
c.logError(StepInputPassword, "密码验证失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))]))
return "", fmt.Errorf("密码验证失败: %d", resp.StatusCode)
}
c.logStep(StepInputPassword, "密码验证成功")
// 解析密码验证响应
json.Unmarshal(body, &data)
// 检查下一步是什么
nextPageType := ""
if page, ok := data["page"].(map[string]interface{}); ok {
if pt, ok := page["type"].(string); ok {
nextPageType = pt
}
}
// 如果需要邮箱验证,尝试获取验证码并完成验证
if nextPageType == "email_otp_verification" {
c.logStep(StepInputPassword, "账号需要邮箱验证,正在获取验证码...")
// 使用邮件ID过滤获取新的OTP验证码 (最多30秒)
// 基于 get_code.go 的增强逻辑使用邮件ID过滤避免获取到旧邮件的验证码
otpCode, err := mail.GetEmailOTPAfterID(c.email, latestEmailID, 30*time.Second)
if err != nil {
c.logError(StepInputPassword, "获取验证码失败: %v", err)
return "", fmt.Errorf("获取验证码失败: %v", err)
}
c.logStep(StepInputPassword, "获取到验证码: %s", otpCode)
// 提交验证码到 /api/accounts/email-otp/validate
// 先获取 Sentinel token (可能需要 PoW)
if !c.callSentinelReq("email_otp_verification__auto") {
// 如果失败,尝试 password_verify__auto
if !c.callSentinelReq("password_verify__auto") {
return "", fmt.Errorf("Sentinel 请求失败")
}
}
verifyOtpHeaders := make(map[string]string)
for k, v := range headers {
verifyOtpHeaders[k] = v
}
verifyOtpHeaders["OpenAI-Sentinel-Token"] = c.getSentinelHeader("email_otp_validate")
// 请求体格式: {"code":"016547"}
otpPayload := map[string]string{
"code": otpCode,
}
resp, body, err = c.doRequest("POST", "https://auth.openai.com/api/accounts/email-otp/validate", otpPayload, verifyOtpHeaders)
if err != nil || resp.StatusCode != 200 {
c.logError(StepInputPassword, "验证码验证失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))]))
return "", fmt.Errorf("验证码验证失败: %d", resp.StatusCode)
}
c.logStep(StepInputPassword, "邮箱验证成功")
// 重新解析响应,检查下一步
json.Unmarshal(body, &data)
}
// 基于 get_code.go 的增强逻辑:检查是否有 continue_url
// 如果有,直接跟随重定向获取授权码,跳过工作区选择
if cu, ok := data["continue_url"].(string); ok && cu != "" && !strings.Contains(cu, "email-verification") {
if strings.Contains(cu, "consent") {
// 访问 consent 页面
c.logStep(StepSelectWorkspace, "处理 consent 页面...")
c.doRequest("GET", cu, nil, headers)
} else {
// 直接跟随重定向获取授权码
c.logStep(StepWaitCallback, "跟随 continue_url 重定向...")
return c.followRedirectsForCode(cu, headers)
}
}
} else {
c.logStep(StepInputPassword, "跳过密码验证步骤 (服务器未要求)")
}
// 6. 选择工作区
c.logStep(StepSelectWorkspace, "选择工作区: %s", c.workspaceID)
// 根据前面的流程选择正确的 Sentinel 请求
if !c.callSentinelReq("email_otp_validate__auto") {
// 如果 email_otp_validate__auto 失败,尝试 password_verify__auto
if !c.callSentinelReq("password_verify__auto") {
return "", fmt.Errorf("Sentinel 请求失败")
}
}
// 选择工作区时带上 Sentinel Header
workspaceHeaders := make(map[string]string)
for k, v := range headers {
workspaceHeaders[k] = v
}
workspaceHeaders["OpenAI-Sentinel-Token"] = c.getSentinelHeader("workspace_select")
workspacePayload := map[string]string{
"workspace_id": c.workspaceID,
}
// 添加 500 错误重试机制 - 最多重试 3 次
var lastErr error
for retry := 0; retry < 3; retry++ {
if retry > 0 {
c.logStep(StepSelectWorkspace, "第 %d 次重试选择工作区...", retry+1)
time.Sleep(time.Duration(2+retry) * time.Second) // 递增延迟: 2s, 3s, 4s
// 重新获取 Sentinel token
if !c.callSentinelReq("password_verify__auto") {
c.callSentinelReq("email_otp_validate__auto")
}
workspaceHeaders["OpenAI-Sentinel-Token"] = c.getSentinelHeader("workspace_select")
}
resp, body, err = c.doRequest("POST", "https://auth.openai.com/api/accounts/workspace/select", workspacePayload, workspaceHeaders)
if err != nil {
lastErr = fmt.Errorf("请求失败: %v", err)
continue
}
// 成功
if resp.StatusCode == 200 {
json.Unmarshal(body, &data)
continueURL, ok := data["continue_url"].(string)
if !ok || continueURL == "" {
c.logError(StepSelectWorkspace, "未获取到 continue_url, 响应: %s", string(body[:min(500, len(body))]))
return "", fmt.Errorf("未获取到 continue_url")
}
// 7. 跟随重定向获取授权码
c.logStep(StepWaitCallback, "跟随重定向...")
for i := 0; i < 10; i++ {
resp, _, err = c.doRequest("GET", continueURL, nil, headers)
if err != nil {
break
}
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
location := resp.Header.Get("Location")
if strings.Contains(location, "localhost:1455") {
code := ExtractCodeFromCallbackURL(location)
if code != "" {
c.logStep(StepComplete, "授权成功,获取到授权码")
return code, nil
}
}
continueURL = location
} else {
break
}
}
c.logError(StepWaitCallback, "未能获取授权码")
return "", fmt.Errorf("未能获取授权码")
}
// 5xx 服务器错误,可重试
if resp.StatusCode >= 500 && resp.StatusCode < 600 {
c.logStep(StepSelectWorkspace, "服务器错误 %d将重试...", resp.StatusCode)
lastErr = fmt.Errorf("服务器错误: %d", resp.StatusCode)
continue
}
// 其他错误,不重试
c.logError(StepSelectWorkspace, "选择工作区失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))]))
return "", fmt.Errorf("选择工作区失败: %d", resp.StatusCode)
}
// 重试耗尽
c.logError(StepSelectWorkspace, "选择工作区失败,重试已耗尽: %v", lastErr)
return "", fmt.Errorf("选择工作区失败 (重试已耗尽): %v", lastErr)
}
// ExchangeCodeForTokens 用授权码换取 tokens
func (c *CodexAPIAuth) ExchangeCodeForTokens(code, codeVerifier string) (*CodexTokens, error) {
payload := map[string]string{
"grant_type": "authorization_code",
"client_id": CodexClientID,
"code_verifier": codeVerifier,
"code": code,
"redirect_uri": CodexRedirectURI,
}
headers := map[string]string{"Content-Type": "application/json"}
resp, body, err := c.doRequest("POST", "https://auth.openai.com/oauth/token", payload, headers)
if err != nil {
return nil, fmt.Errorf("token 交换失败: %v", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("token 交换失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))]))
}
var tokens CodexTokens
if err := json.Unmarshal(body, &tokens); err != nil {
return nil, fmt.Errorf("解析 token 失败: %v", err)
}
if tokens.ExpiresIn > 0 {
expiredAt := time.Now().Add(time.Duration(tokens.ExpiresIn) * time.Second)
tokens.ExpiredAt = expiredAt.Format(time.RFC3339)
}
return &tokens, nil
}
// followRedirectsForCode 跟随重定向获取授权码
// 基于 get_code.go 的实现,用于复用重定向逻辑
func (c *CodexAPIAuth) followRedirectsForCode(continueURL string, headers map[string]string) (string, error) {
c.logStep(StepWaitCallback, "跟随重定向获取授权码...")
for i := 0; i < 10; i++ {
resp, _, err := c.doRequest("GET", continueURL, nil, headers)
if err != nil {
break
}
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
location := resp.Header.Get("Location")
if strings.Contains(location, "localhost:1455") {
code := ExtractCodeFromCallbackURL(location)
if code != "" {
c.logStep(StepComplete, "授权成功,获取到授权码")
return code, nil
}
}
continueURL = location
} else {
break
}
}
c.logError(StepWaitCallback, "未能获取授权码")
return "", fmt.Errorf("未能获取授权码")
}
// min 返回较小值
func min(a, b int) int {
if a < b {
return a
}
return b
}
// CompleteWithCodexAPI 使用纯 API 方式完成授权(带 403 重试换指纹机制)
// authURL 和 sessionID 由 S2A 生成
func CompleteWithCodexAPI(email, password, workspaceID, authURL, sessionID, proxy string, logger *AuthLogger) (string, error) {
if logger != nil {
if proxy != "" {
logger.LogStep(StepBrowserStart, "使用 CodexAuth API 模式 (代理: %s)", proxy)
} else {
logger.LogStep(StepBrowserStart, "使用 CodexAuth API 模式 (无代理)")
}
}
// 403 重试机制 - 最多重试 3 次,每次换新指纹
var lastErr error
for retry := 0; retry < 3; retry++ {
auth, err := NewCodexAPIAuth(email, password, workspaceID, authURL, sessionID, proxy, logger)
if err != nil {
lastErr = err
if logger != nil {
logger.LogError(StepBrowserStart, "创建 CodexAuth 失败: %v", err)
}
if retry < 2 {
if logger != nil {
logger.LogStep(StepBrowserStart, "重试 %d/3换新指纹...", retry+1)
}
continue
}
return "", fmt.Errorf("创建 CodexAuth 失败 (已重试3次): %v", err)
}
code, err := auth.ObtainAuthorizationCode()
if err != nil {
auth.tlsClient.Close()
// 检查是否为 403 错误
if strings.Contains(err.Error(), "403") {
lastErr = err
if retry < 2 {
if logger != nil {
logger.LogStep(StepBrowserStart, "遇到 403重试 %d/3换新指纹...", retry+1)
}
time.Sleep(time.Duration(1+retry) * time.Second) // 递增延迟
continue
}
return "", fmt.Errorf("授权失败: %v (403 已重试3次)", err)
}
// 非 403 错误,不重试
return "", err
}
auth.tlsClient.Close()
return code, nil
}
return "", fmt.Errorf("授权失败 (已重试3次): %v", lastErr)
}