From 54ccd4a100b68625b83ff4e0ee16a91297b0e02d Mon Sep 17 00:00:00 2001 From: kyx236 Date: Mon, 2 Feb 2026 09:58:29 +0800 Subject: [PATCH] feat: Add a new Go backend module for API-based authentication, featuring TLS fingerprinting and Proof-of-Work solving. --- .gitignore | 3 +- backend/go.mod | 1 + backend/internal/auth/codex_api.go | 476 ++++++++++++++++++----------- 3 files changed, 293 insertions(+), 187 deletions(-) diff --git a/.gitignore b/.gitignore index e897727..0074cfc 100644 --- a/.gitignore +++ b/.gitignore @@ -101,4 +101,5 @@ check_ban.py backend/codex-pool.exe backend/codex-pool.exe .claude/settings.local.json -CodexAuth \ No newline at end of file +CodexAuth +get_code.go diff --git a/backend/go.mod b/backend/go.mod index ddbf267..c00c59d 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -13,6 +13,7 @@ require ( github.com/go-rod/rod v0.116.2 github.com/go-rod/stealth v0.4.9 github.com/mattn/go-sqlite3 v1.14.33 + github.com/refraction-networking/utls v1.6.7 ) require ( diff --git a/backend/internal/auth/codex_api.go b/backend/internal/auth/codex_api.go index 235384a..c98ae7a 100644 --- a/backend/internal/auth/codex_api.go +++ b/backend/internal/auth/codex_api.go @@ -2,24 +2,30 @@ package auth import ( "bytes" + "context" "crypto/sha256" + "crypto/tls" "encoding/base64" "encoding/json" "fmt" + "io" "math/rand" + "net" "net/http" + "net/http/cookiejar" "net/url" "strings" "time" - "codex-pool/internal/client" + utls "github.com/refraction-networking/utls" + "golang.org/x/net/http2" ) // 常量 CodexClientID, CodexRedirectURI, CodexScope 已在 s2a.go 中定义 -// CodexAPIAuth 纯 API 授权 (无浏览器) +// CodexAPIAuth 纯 API 授权 (无浏览器) - 基于 get_code.go 的实现 type CodexAPIAuth struct { - client *client.TLSClient + client *http.Client email string password string workspaceID string @@ -28,24 +34,126 @@ type CodexAPIAuth struct { sentinelToken string solvedPow string userAgent string + proxyURL string logger *AuthLogger } +// utlsRoundTripper - 模拟 Chrome TLS 指纹 +type utlsRoundTripper struct { + proxyURL *url.URL +} + +func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + var conn net.Conn + var err error + + targetHost := req.URL.Host + if !strings.Contains(targetHost, ":") { + if req.URL.Scheme == "https" { + targetHost += ":443" + } else { + targetHost += ":80" + } + } + + // 通过代理连接 + if rt.proxyURL != nil { + proxyHost := rt.proxyURL.Host + conn, err = net.DialTimeout("tcp", proxyHost, 30*time.Second) + if err != nil { + return nil, fmt.Errorf("proxy dial error: %w", err) + } + + // 发送 CONNECT 请求 + connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", targetHost, targetHost) + _, err = conn.Write([]byte(connectReq)) + if err != nil { + conn.Close() + return nil, fmt.Errorf("proxy connect write error: %w", err) + } + + // 读取代理响应 + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil { + conn.Close() + return nil, fmt.Errorf("proxy connect read error: %w", err) + } + response := string(buf[:n]) + if !strings.Contains(response, "200") { + conn.Close() + return nil, fmt.Errorf("proxy connect failed: %s", response) + } + } else { + conn, err = net.DialTimeout("tcp", targetHost, 30*time.Second) + if err != nil { + return nil, fmt.Errorf("dial error: %w", err) + } + } + + // 使用 uTLS 进行 TLS 握手,模拟 Chrome + tlsConn := utls.UClient(conn, &utls.Config{ + ServerName: req.URL.Hostname(), + InsecureSkipVerify: true, + }, utls.HelloChrome_Auto) + + err = tlsConn.Handshake() + if err != nil { + conn.Close() + return nil, fmt.Errorf("tls handshake error: %w", err) + } + + // 使用 HTTP/2 或 HTTP/1.1 + alpn := tlsConn.ConnectionState().NegotiatedProtocol + if alpn == "h2" { + // HTTP/2 + t2 := &http2.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { + return tlsConn, nil + }, + } + return t2.RoundTrip(req) + } + + // HTTP/1.1 + t1 := &http.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return tlsConn, nil + }, + } + return t1.RoundTrip(req) +} + // NewCodexAPIAuth 创建 CodexAuth 实例 func NewCodexAPIAuth(email, password, workspaceID, proxy string, logger *AuthLogger) (*CodexAPIAuth, error) { - tlsClient, err := client.New(proxy) - if err != nil { - return nil, fmt.Errorf("创建 TLS 客户端失败: %v", err) + var proxyURL *url.URL + if proxy != "" { + var err error + proxyURL, err = url.Parse(proxy) + if err != nil { + return nil, fmt.Errorf("解析代理地址失败: %v", err) + } + } + + jar, _ := cookiejar.New(nil) + client := &http.Client{ + Transport: &utlsRoundTripper{proxyURL: proxyURL}, + Jar: jar, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse // 禁用自动重定向,手动跟随 + }, + Timeout: 30 * time.Second, } return &CodexAPIAuth{ - client: tlsClient, + client: client, email: email, password: password, workspaceID: workspaceID, deviceID: generateUUID(), sid: generateUUID(), - userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", + proxyURL: proxy, logger: logger, }, nil } @@ -54,9 +162,8 @@ func NewCodexAPIAuth(email, password, workspaceID, proxy string, logger *AuthLog func generateUUID() string { b := make([]byte, 16) rand.Read(b) - b[6] = (b[6] & 0x0f) | 0x40 - b[8] = (b[8] & 0x3f) | 0x80 - return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) + return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", + b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } // generateCodeVerifier 生成 PKCE code_verifier @@ -96,14 +203,19 @@ func fnv1a32(data []byte) uint32 { // getParseTime 生成 JS Date().toString() 格式的时间字符串 func getParseTime() string { - now := time.Now() - return now.Format("Mon Jan 02 2006 15:04:05") + " GMT+0800 (中国标准时间)" + 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{}{ - 2500 + rand.Intn(1000), + rand.Intn(1000) + 2500, getParseTime(), 4294967296, 0, @@ -114,13 +226,13 @@ func (c *CodexAPIAuth) getConfig() []interface{} { "zh-CN", 0, "canShare−function canShare() { [native code] }", - fmt.Sprintf("_reactListening%d", 1000000+rand.Intn(9000000)), + fmt.Sprintf("_reactListening%d", rand.Intn(9000000)+1000000), "onhashchange", - float64(time.Now().UnixNano()/1e6) / 1000.0, + float64(time.Now().UnixNano()) / 1e6, c.sid, "", 24, - time.Now().UnixMilli() - int64(10000+rand.Intn(40000)), + time.Now().UnixMilli() - int64(rand.Intn(40000)+10000), } } @@ -135,7 +247,8 @@ func (c *CodexAPIAuth) solvePow(seed, difficulty string, cfg []interface{}, maxI jsonStr, _ := json.Marshal(cfg) encoded := base64.StdEncoding.EncodeToString(jsonStr) - h := fnv1a32(append(seedBytes, []byte(encoded)...)) + combined := append(seedBytes, []byte(encoded)...) + h := fnv1a32(combined) hexHash := fmt.Sprintf("%08x", h) if hexHash[:len(difficulty)] <= difficulty { @@ -156,75 +269,100 @@ func (c *CodexAPIAuth) getRequirementsToken() string { return "gAAAAAC" + encoded + "~S" } +// doRequest 执行 HTTP 请求 +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") + req.Header.Set("sec-ch-ua", `"Not(A:Brand";v="8", "Chromium";v="110", "Google Chrome";v="110"`) + req.Header.Set("sec-ch-ua-mobile", "?0") + req.Header.Set("sec-ch-ua-platform", `"Windows"`) + 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.client.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) error { +func (c *CodexAPIAuth) callSentinelReq(flow string) bool { initToken := c.getRequirementsToken() - payload := map[string]interface{}{ + payload := map[string]string{ "p": initToken, "id": c.deviceID, "flow": flow, } - body, _ := json.Marshal(payload) - req, _ := http.NewRequest("POST", "https://sentinel.openai.com/backend-api/sentinel/req", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "*/*") - req.Header.Set("Origin", "https://auth.openai.com") - req.Header.Set("Sec-Fetch-Dest", "empty") - req.Header.Set("Sec-Fetch-Mode", "cors") - req.Header.Set("Sec-Fetch-Site", "cross-site") - - resp, err := c.client.Do(req) - if err != nil { - return fmt.Errorf("sentinel 请求失败: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return fmt.Errorf("sentinel 状态码: %d", resp.StatusCode) + 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 } - respBody, _ := client.ReadBody(resp) - var result struct { - Token string `json:"token"` - ProofOfWork struct { - Required bool `json:"required"` - Seed string `json:"seed"` - Difficulty string `json:"difficulty"` - } `json:"proofofwork"` + var data map[string]interface{} + json.Unmarshal(body, &data) + + if token, ok := data["token"].(string); ok { + c.sentinelToken = token } - if err := json.Unmarshal(respBody, &result); err != nil { - return fmt.Errorf("解析 sentinel 响应失败: %v", err) - } - - c.sentinelToken = result.Token - - if result.ProofOfWork.Required { - cfg := c.getConfig() - solved := c.solvePow(result.ProofOfWork.Seed, result.ProofOfWork.Difficulty, cfg, 5000000) - if solved == "" { - return fmt.Errorf("PoW 解决失败") + 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) + 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.solvedPow = initToken } - c.solvedPow = "gAAAAAB" + solved } else { c.solvedPow = initToken } - return nil + return true } // getSentinelHeader 构建 sentinel header func (c *CodexAPIAuth) getSentinelHeader(flow string) string { - obj := map[string]interface{}{ + sentinelObj := map[string]string{ "p": c.solvedPow, "id": c.deviceID, "flow": flow, } if c.sentinelToken != "" { - obj["c"] = c.sentinelToken + sentinelObj["c"] = c.sentinelToken } - header, _ := json.Marshal(obj) + header, _ := json.Marshal(sentinelObj) return string(header) } @@ -242,17 +380,6 @@ func (c *CodexAPIAuth) logError(step AuthStep, format string, args ...interface{ } } -// setAPIHeaders 设置 API 请求的通用头 (模拟 XHR 请求而非页面导航) -func (c *CodexAPIAuth) setAPIHeaders(req *http.Request, referer string) { - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "*/*") - req.Header.Set("Origin", "https://auth.openai.com") - req.Header.Set("Referer", referer) - req.Header.Set("Sec-Fetch-Dest", "empty") - req.Header.Set("Sec-Fetch-Mode", "cors") - req.Header.Set("Sec-Fetch-Site", "same-origin") -} - // ObtainAuthorizationCode 获取授权码 func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { c.logStep(StepNavigate, "开始 Codex API 授权流程...") @@ -278,158 +405,134 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { authURL := "https://auth.openai.com/oauth/authorize?" + params.Encode() - // 3. 访问授权页面 + headers := map[string]string{ + "Origin": "https://auth.openai.com", + "Referer": "https://auth.openai.com/log-in", + "Content-Type": "application/json", + } + + // 3. 访问授权页面并手动跟随重定向 c.logStep(StepNavigate, "访问授权页面...") - req, _ := http.NewRequest("GET", authURL, nil) - req.Header.Set("Referer", "https://auth.openai.com/log-in") - resp, err := c.client.Do(req) + resp, _, err := c.doRequest("GET", authURL, nil, headers) if err != nil { c.logError(StepNavigate, "访问授权页失败: %v", err) return "", fmt.Errorf("访问授权页失败: %v", err) } - defer resp.Body.Close() - client.ReadBody(resp) // 消耗响应体 - referer := resp.Request.URL.String() + + // 手动跟随重定向 + 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 // 4. 提交邮箱 c.logStep(StepInputEmail, "提交邮箱: %s", c.email) - if err := c.callSentinelReq("login_web_init"); err != nil { - c.logError(StepInputEmail, "Sentinel 请求失败: %v", err) - return "", err + 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, }, } - emailBody, _ := json.Marshal(emailPayload) - req, _ = http.NewRequest("POST", "https://auth.openai.com/api/accounts/authorize/continue", bytes.NewReader(emailBody)) - c.setAPIHeaders(req, referer) - req.Header.Set("OpenAI-Sentinel-Token", c.getSentinelHeader("authorize_continue")) - - resp, err = c.client.Do(req) - if err != nil { - c.logError(StepInputEmail, "提交邮箱失败: %v", err) - return "", fmt.Errorf("提交邮箱失败: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - body, _ := client.ReadBody(resp) - c.logError(StepInputEmail, "提交邮箱失败: %d - %s", resp.StatusCode, string(body)) + 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) } - // 解析响应,检查是否需要密码 - emailResp, _ := client.ReadBody(resp) - var emailResult map[string]interface{} - json.Unmarshal(emailResp, &emailResult) + var data map[string]interface{} + json.Unmarshal(body, &data) - // 5. 验证密码 - c.logStep(StepInputPassword, "验证密码...") - if err := c.callSentinelReq("authorize_continue__auto"); err != nil { - c.logError(StepInputPassword, "Sentinel 请求失败: %v", err) - return "", err + // 检查是否需要密码验证 + pageType := "" + if page, ok := data["page"].(map[string]interface{}); ok { + if pt, ok := page["type"].(string); ok { + pageType = pt + } } - pwdPayload := map[string]string{ - "username": c.email, - "password": c.password, - } - pwdBody, _ := json.Marshal(pwdPayload) + if pageType == "password" || strings.Contains(string(body), "password") { + // 5. 验证密码 + c.logStep(StepInputPassword, "验证密码...") + if !c.callSentinelReq("authorize_continue__auto") { + return "", fmt.Errorf("Sentinel 请求失败") + } - req, _ = http.NewRequest("POST", "https://auth.openai.com/api/accounts/password/verify", bytes.NewReader(pwdBody)) - c.setAPIHeaders(req, referer) - req.Header.Set("OpenAI-Sentinel-Token", c.getSentinelHeader("password_verify")) + verifyHeaders := make(map[string]string) + for k, v := range headers { + verifyHeaders[k] = v + } + verifyHeaders["OpenAI-Sentinel-Token"] = c.getSentinelHeader("password_verify") - resp, err = c.client.Do(req) - if err != nil { - c.logError(StepInputPassword, "验证密码失败: %v", err) - return "", fmt.Errorf("验证密码失败: %v", err) - } - defer resp.Body.Close() + passwordPayload := map[string]string{ + "username": c.email, + "password": c.password, + } - if resp.StatusCode != 200 { - body, _ := client.ReadBody(resp) - c.logError(StepInputPassword, "密码验证失败: %d - %s", resp.StatusCode, string(body)) - return "", fmt.Errorf("密码验证失败: %d", resp.StatusCode) + 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) + } } // 6. 选择工作区 c.logStep(StepSelectWorkspace, "选择工作区: %s", c.workspaceID) - if err := c.callSentinelReq("password_verify__auto"); err != nil { - c.logError(StepSelectWorkspace, "Sentinel 请求失败: %v", err) - return "", err + if !c.callSentinelReq("password_verify__auto") { + return "", fmt.Errorf("Sentinel 请求失败") } - wsPayload := map[string]string{ + workspacePayload := map[string]string{ "workspace_id": c.workspaceID, } - wsBody, _ := json.Marshal(wsPayload) - req, _ = http.NewRequest("POST", "https://auth.openai.com/api/accounts/workspace/select", bytes.NewReader(wsBody)) - c.setAPIHeaders(req, referer) - - resp, err = c.client.Do(req) - if err != nil { - c.logError(StepSelectWorkspace, "选择工作区失败: %v", err) - return "", fmt.Errorf("选择工作区失败: %v", err) - } - defer resp.Body.Close() - - // 调试: 打印响应头信息 - c.logStep(StepSelectWorkspace, "响应状态: %d, Content-Length: %s, Content-Encoding: %s", - resp.StatusCode, resp.Header.Get("Content-Length"), resp.Header.Get("Content-Encoding")) - - if resp.StatusCode != 200 { - body, _ := client.ReadBody(resp) - c.logError(StepSelectWorkspace, "选择工作区失败: %d - %s", resp.StatusCode, string(body)) + resp, body, err = c.doRequest("POST", "https://auth.openai.com/api/accounts/workspace/select", workspacePayload, headers) + if err != nil || resp.StatusCode != 200 { + c.logError(StepSelectWorkspace, "选择工作区失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))])) return "", fmt.Errorf("选择工作区失败: %d", resp.StatusCode) } - // 解析 continue_url - wsResp, _ := client.ReadBody(resp) - c.logStep(StepSelectWorkspace, "工作区响应: %s", string(wsResp)) - - var wsResult struct { - ContinueURL string `json:"continue_url"` - Page struct { - Type string `json:"type"` - } `json:"page"` - Error string `json:"error"` - Message string `json:"message"` - } - if err := json.Unmarshal(wsResp, &wsResult); err != nil { - c.logError(StepSelectWorkspace, "解析响应失败: %v, 原始: %s", err, string(wsResp)) - return "", fmt.Errorf("解析响应失败: %v", err) - } - - if wsResult.ContinueURL == "" { - c.logError(StepSelectWorkspace, "未获取到 continue_url, page=%s, error=%s, msg=%s", - wsResult.Page.Type, wsResult.Error, wsResult.Message) + 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, "跟随重定向...") - continueURL := wsResult.ContinueURL - for i := 0; i < 10; i++ { - req, _ = http.NewRequest("GET", continueURL, nil) - resp, err = c.client.Do(req) + resp, _, err = c.doRequest("GET", continueURL, nil, headers) if err != nil { break } - location := resp.Header.Get("Location") - resp.Body.Close() - - if resp.StatusCode >= 301 && resp.StatusCode <= 308 { + 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, "授权成功,获取到授权码") @@ -456,24 +559,18 @@ func (c *CodexAPIAuth) ExchangeCodeForTokens(code, codeVerifier string) (*CodexT "redirect_uri": CodexRedirectURI, } - body, _ := json.Marshal(payload) - req, _ := http.NewRequest("POST", "https://auth.openai.com/oauth/token", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - - resp, err := c.client.Do(req) + 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) } - defer resp.Body.Close() if resp.StatusCode != 200 { - respBody, _ := client.ReadBody(resp) - return nil, fmt.Errorf("token 交换失败: %d - %s", resp.StatusCode, string(respBody)) + return nil, fmt.Errorf("token 交换失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))])) } - respBody, _ := client.ReadBody(resp) var tokens CodexTokens - if err := json.Unmarshal(respBody, &tokens); err != nil { + if err := json.Unmarshal(body, &tokens); err != nil { return nil, fmt.Errorf("解析 token 失败: %v", err) } @@ -485,7 +582,14 @@ func (c *CodexAPIAuth) ExchangeCodeForTokens(code, codeVerifier string) (*CodexT return &tokens, nil } -// CodexTokens 结构体已在 s2a.go 中定义 +// min 返回较小值 +func min(a, b int) int { + if a < b { + return a + } + return b +} + // CompleteWithCodexAPI 使用纯 API 方式完成授权 func CompleteWithCodexAPI(email, password, workspaceID, proxy string, logger *AuthLogger) (string, error) { if logger != nil {