From d92c64f2c261fa1472b5b5935d3483c9d86e4364 Mon Sep 17 00:00:00 2001 From: kyx236 Date: Tue, 3 Feb 2026 08:49:40 +0800 Subject: [PATCH] feat: add mail service for managing email configurations, generating accounts, and fetching emails with verification code support. --- backend/internal/auth/codex_api.go | 56 ++++++++++++-- backend/internal/mail/service.go | 13 +++- frontend/src/components/layout/Header.tsx | 21 +++++- frontend/src/hooks/useTeamStatus.ts | 90 +++++++++++++++++++++++ 4 files changed, 172 insertions(+), 8 deletions(-) create mode 100644 frontend/src/hooks/useTeamStatus.ts diff --git a/backend/internal/auth/codex_api.go b/backend/internal/auth/codex_api.go index 8f53dce..ea39f19 100644 --- a/backend/internal/auth/codex_api.go +++ b/backend/internal/auth/codex_api.go @@ -16,6 +16,8 @@ import ( "strings" "time" + "codex-pool/internal/mail" + utls "github.com/refraction-networking/utls" "golang.org/x/net/http2" ) @@ -513,17 +515,61 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { } } - // 如果需要邮箱验证,这是新账号的问题 + // 如果需要邮箱验证,尝试获取验证码并完成验证 if nextPageType == "email_otp_verification" { - c.logError(StepInputPassword, "账号需要邮箱验证,无法继续 Codex 授权流程") - return "", fmt.Errorf("账号需要邮箱验证,请使用浏览器模式或等待账号状态更新") + c.logStep(StepInputPassword, "账号需要邮箱验证,正在获取验证码...") + + // 等待获取验证码 (最多60秒) + // 邮件标题格式: "Your ChatGPT code is 016547" + otpCode, err := mail.GetVerificationCode(c.email, 60*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) } + } else { + c.logStep(StepInputPassword, "跳过密码验证步骤 (服务器未要求)") } // 6. 选择工作区 c.logStep(StepSelectWorkspace, "选择工作区: %s", c.workspaceID) - if !c.callSentinelReq("password_verify__auto") { - return "", fmt.Errorf("Sentinel 请求失败") + // 根据前面的流程选择正确的 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 diff --git a/backend/internal/mail/service.go b/backend/internal/mail/service.go index bd229da..7346378 100644 --- a/backend/internal/mail/service.go +++ b/backend/internal/mail/service.go @@ -363,21 +363,30 @@ func (m *Client) WaitForCode(email string, timeout time.Duration) (string, error for _, mail := range emails { subject := strings.ToLower(mail.Subject) // 匹配多种可能的验证码邮件主题 + // 包括 "Your ChatGPT code is 016547" 格式 isCodeEmail := strings.Contains(subject, "code") || strings.Contains(subject, "verify") || strings.Contains(subject, "verification") || strings.Contains(subject, "openai") || - strings.Contains(subject, "confirm") + strings.Contains(subject, "confirm") || + strings.Contains(subject, "chatgpt") if !isCodeEmail { continue } + // 优先从标题中提取验证码 (如 "Your ChatGPT code is 016547") + matches := codeRegex.FindStringSubmatch(mail.Subject) + if len(matches) >= 2 { + return matches[1], nil + } + + // 如果标题中没有,从内容中提取 content := mail.Content if content == "" { content = mail.Text } - matches := codeRegex.FindStringSubmatch(content) + matches = codeRegex.FindStringSubmatch(content) if len(matches) >= 2 { return matches[1], nil } diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index 6750baa..8240403 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -1,5 +1,6 @@ -import { Menu, Moon, Sun } from 'lucide-react' +import { Menu, Moon, Sun, Database, UserPlus } from 'lucide-react' import { useState, useEffect } from 'react' +import { useTeamStatus } from '../../hooks/useTeamStatus' interface HeaderProps { onMenuClick: () => void @@ -8,6 +9,7 @@ interface HeaderProps { export default function Header({ onMenuClick, isConnected = false }: HeaderProps) { const [isDark, setIsDark] = useState(false) + const teamStatus = useTeamStatus() useEffect(() => { // Check for saved theme preference or system preference @@ -55,6 +57,23 @@ export default function Header({ onMenuClick, isConnected = false }: HeaderProps {/* Spacer */}
+ {/* Team Status Indicators */} + {teamStatus.isProcessing && ( +
+ + 入库中 + {teamStatus.processingTeams} +
+ )} + + {teamStatus.isRegistering && ( +
+ + 注册中 + {teamStatus.registeringTeams} +
+ )} + {/* Connection status */}
({ + processingTeams: 0, + processedTeams: 0, + isProcessing: false, + registeringTeams: 0, + isRegistering: false, + }) + + const fetchStatus = useCallback(async () => { + try { + // 并行获取两个状态 + const [processRes, regRes] = await Promise.all([ + fetch('/api/team/status').catch(() => null), + fetch('/api/team-reg/status').catch(() => null), + ]) + + let newStatus: TeamStatus = { + processingTeams: 0, + processedTeams: 0, + isProcessing: false, + registeringTeams: 0, + isRegistering: false, + } + + // 解析入库状态 + if (processRes?.ok) { + const data = await processRes.json() + if (data.code === 0 && data.data) { + const processData = data.data as TeamProcessStatus + newStatus.isProcessing = processData.running + newStatus.processingTeams = processData.running + ? processData.total_teams - processData.completed + : 0 + newStatus.processedTeams = processData.completed + } + } + + // 解析注册状态 + if (regRes?.ok) { + const regData = await regRes.json() as TeamRegStatus + newStatus.isRegistering = regData.running + newStatus.registeringTeams = regData.running && regData.config + ? regData.config.count + : 0 + } + + setStatus(newStatus) + } catch (error) { + console.error('Failed to fetch team status:', error) + } + }, []) + + useEffect(() => { + // 立即获取一次 + fetchStatus() + + // 定时轮询 + const interval = setInterval(fetchStatus, pollInterval) + + return () => clearInterval(interval) + }, [fetchStatus, pollInterval]) + + return status +}