700 lines
21 KiB
Go
700 lines
21 KiB
Go
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,
|
||
"canShare−function 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 获取授权码
|
||
func (c *CodexAPIAuth) ObtainAuthorizationCode() (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)
|
||
}
|