From 61712cf4fbcfb4772aed13254d379dbac6c3b477 Mon Sep 17 00:00:00 2001 From: kyx236 Date: Wed, 4 Feb 2026 09:43:22 +0800 Subject: [PATCH] feat: Implement browserless Codex API authentication with PoW solving and a new mail service, and update gitignore to exclude `get_code.go`. --- .gitignore | 1 + backend/internal/auth/codex_api.go | 53 ++++++++++++++++++++++++++-- backend/internal/mail/service.go | 55 ++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index aaa6b27..a3122cf 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,4 @@ backend/codex-pool.exe .claude/settings.local.json CodexAuth +get_code.go diff --git a/backend/internal/auth/codex_api.go b/backend/internal/auth/codex_api.go index ea39f19..9b47ac0 100644 --- a/backend/internal/auth/codex_api.go +++ b/backend/internal/auth/codex_api.go @@ -482,6 +482,10 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { 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 请求失败") @@ -519,9 +523,9 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { if nextPageType == "email_otp_verification" { c.logStep(StepInputPassword, "账号需要邮箱验证,正在获取验证码...") - // 等待获取验证码 (最多60秒) - // 邮件标题格式: "Your ChatGPT code is 016547" - otpCode, err := mail.GetVerificationCode(c.email, 60*time.Second) + // 使用邮件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) @@ -558,6 +562,20 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { // 重新解析响应,检查下一步 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, "跳过密码验证步骤 (服务器未要求)") } @@ -656,6 +674,35 @@ func (c *CodexAPIAuth) ExchangeCodeForTokens(code, codeVerifier string) (*CodexT 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 { diff --git a/backend/internal/mail/service.go b/backend/internal/mail/service.go index e32ab03..c4288f5 100644 --- a/backend/internal/mail/service.go +++ b/backend/internal/mail/service.go @@ -129,6 +129,7 @@ type EmailListResponse struct { // EmailItem 邮件项 type EmailItem struct { + EmailID int `json:"emailId"` Content string `json:"content"` Text string `json:"text"` Subject string `json:"subject"` @@ -504,3 +505,57 @@ func GetVerificationCode(email string, timeout time.Duration) (string, error) { client := NewClientForEmail(email) return client.WaitForCode(email, timeout) } + +// GetLatestEmailID 获取邮箱最新邮件的ID +// 基于 get_code.go 的实现,用于在发送验证码请求前记录最新邮件ID +func GetLatestEmailID(email string) int { + client := NewClientForEmail(email) + emails, err := client.GetEmails(email, 1) + if err != nil || len(emails) == 0 { + return 0 + } + return emails[0].EmailID +} + +// GetEmailOTPAfterID 获取指定邮件ID之后的OTP验证码 +// 基于 get_code.go 的实现,只获取 afterEmailID 之后的新邮件中的验证码 +func GetEmailOTPAfterID(email string, afterEmailID int, timeout time.Duration) (string, error) { + client := NewClientForEmail(email) + start := time.Now() + codeRegex := regexp.MustCompile(`\b(\d{6})\b`) + + for time.Since(start) < timeout { + emails, err := client.GetEmails(email, 5) + if err == nil { + for _, mail := range emails { + // 只处理 afterEmailID 之后的新邮件 + if mail.EmailID <= afterEmailID { + continue + } + + subject := strings.ToLower(mail.Subject) + // 匹配 OpenAI/ChatGPT 验证码邮件 + if !strings.Contains(subject, "code") { + continue + } + + // 从标题提取验证码 + if matches := codeRegex.FindStringSubmatch(mail.Subject); len(matches) >= 2 { + return matches[1], nil + } + + // 从内容提取验证码 + content := mail.Content + if content == "" { + content = mail.Text + } + if matches := codeRegex.FindStringSubmatch(content); len(matches) >= 2 { + return matches[1], nil + } + } + } + time.Sleep(1 * time.Second) + } + + return "", fmt.Errorf("验证码获取超时 (afterEmailID=%d)", afterEmailID) +}