From 98ac10987c27f6dbedf60da3e56e99fb3aab8f3f Mon Sep 17 00:00:00 2001 From: kyx236 Date: Fri, 6 Feb 2026 18:49:55 +0800 Subject: [PATCH] feat: Introduce advanced TLS client with browser fingerprinting and new backend modules for API processing, authentication, mail, and ChatGPT registration. --- backend/internal/api/team_process.go | 69 ++++++++++++++++++++-------- backend/internal/auth/codex_api.go | 21 ++++++++- backend/internal/client/tls.go | 4 +- backend/internal/mail/service.go | 47 +++++++++++++++---- backend/internal/register/chatgpt.go | 21 ++++++++- 5 files changed, 130 insertions(+), 32 deletions(-) diff --git a/backend/internal/api/team_process.go b/backend/internal/api/team_process.go index 23a7c81..eb877ca 100644 --- a/backend/internal/api/team_process.go +++ b/backend/internal/api/team_process.go @@ -67,6 +67,18 @@ type TeamProcessState struct { var teamProcessState = &TeamProcessState{} +// waitGroupWithTimeout 带超时的 WaitGroup 等待,超时返回 false +func waitGroupWithTimeout(wg *sync.WaitGroup, timeout time.Duration) bool { + done := make(chan struct{}) + go func() { wg.Wait(); close(done) }() + select { + case <-done: + return true + case <-time.After(timeout): + return false + } +} + // getProxyDisplay 获取代理显示名称(隐藏密码) func getProxyDisplay(proxy string) string { if proxy == "" { @@ -648,8 +660,17 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul memberLogPrefix := fmt.Sprintf("%s [Member %d]", logPrefix, memberIdx+1) memberStartTime := time.Now() - // 获取入库信号量 - s2aSem <- struct{}{} + // 获取入库信号量(3分钟超时) + select { + case s2aSem <- struct{}{}: + case <-time.After(3 * time.Minute): + logger.Warning(fmt.Sprintf("%s 入库信号量等待超时 (3分钟)", memberLogPrefix), memberEmail, "team") + atomic.AddInt32(&s2aFailCount, 1) + memberMu.Lock() + result.Errors = append(result.Errors, fmt.Sprintf("成员 %d 入库信号量超时", memberIdx+1)) + memberMu.Unlock() + return false + } defer func() { <-s2aSem }() // 从代理池获取随机代理(默认轮询使用代理池,无代理则直连) @@ -764,23 +785,29 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul currentEmail := email currentPassword := password if attempt > 0 { - // 重试时使用新邮箱 - currentEmail = mail.GenerateEmail() - currentPassword = register.GeneratePassword() - logger.Warning(fmt.Sprintf("%s 重试 (第%d次), 新邮箱: %s", memberLogPrefix, attempt+1, currentEmail), currentEmail, "team") + // 注册失败重试:保持原邮箱(邀请已发送),仅重新注册 + logger.Warning(fmt.Sprintf("%s 注册重试 (第%d次), 保持邮箱: %s", memberLogPrefix, attempt+1, currentEmail), currentEmail, "team") } - // 发送邀请 - if err := inviter.SendInvites([]string{currentEmail}); err != nil { - errStr := err.Error() - logger.Error(fmt.Sprintf("%s 邀请失败: %v", memberLogPrefix, err), currentEmail, "team") + // 首次尝试时发送邀请,重试时跳过(邀请已发送到该邮箱) + if attempt == 0 { + if err := inviter.SendInvites([]string{currentEmail}); err != nil { + errStr := err.Error() + logger.Error(fmt.Sprintf("%s 邀请失败: %v", memberLogPrefix, err), currentEmail, "team") - // 检测 Team 已达邀请上限(401 或 maximum number of seats) - if strings.Contains(errStr, "401") || strings.Contains(errStr, "maximum number of seats") { - markTeamExhausted() - return false + // 检测 Team 已达邀请上限(401 或 maximum number of seats) + if strings.Contains(errStr, "401") || strings.Contains(errStr, "maximum number of seats") { + markTeamExhausted() + return false + } + // 邀请失败时换新邮箱重试 + email = mail.GenerateEmail() + password = register.GeneratePassword() + currentEmail = email + currentPassword = password + logger.Warning(fmt.Sprintf("%s 邀请失败,换新邮箱: %s", memberLogPrefix, currentEmail), currentEmail, "team") + continue } - continue } // 再次检查是否应该停止(邀请期间其他 goroutine 可能已标记) @@ -841,11 +868,15 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul registerAndS2AMember(idx, email, password) }(i) } - regWg.Wait() + if !waitGroupWithTimeout(®Wg, 8*time.Minute) { + logger.Warning(fmt.Sprintf("%s 注册阶段超时 (8分钟),继续处理已完成的成员", logPrefix), owner.Email, "team") + } // 如果 Team 已满,等待已启动的入库完成 if isTeamExhausted() { - s2aWg.Wait() + if !waitGroupWithTimeout(&s2aWg, 5*time.Minute) { + logger.Warning(fmt.Sprintf("%s 入库等待超时 (5分钟)", logPrefix), owner.Email, "team") + } result.AddedToS2A = int(atomic.LoadInt32(&s2aSuccessCount)) result.Errors = append(result.Errors, "Team 邀请已满") result.DurationMs = time.Since(startTime).Milliseconds() @@ -874,7 +905,9 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul } // 等待所有入库完成 - s2aWg.Wait() + if !waitGroupWithTimeout(&s2aWg, 10*time.Minute) { + logger.Warning(fmt.Sprintf("%s 入库阶段超时 (10分钟),继续统计结果", logPrefix), owner.Email, "team") + } // 补救后再次检查 Team 是否已满 if isTeamExhausted() { diff --git a/backend/internal/auth/codex_api.go b/backend/internal/auth/codex_api.go index de3893d..88cfb3a 100644 --- a/backend/internal/auth/codex_api.go +++ b/backend/internal/auth/codex_api.go @@ -296,8 +296,27 @@ func (c *CodexAPIAuth) GetSessionID() string { return c.sessionID } -// ObtainAuthorizationCode 获取授权码 +// 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 diff --git a/backend/internal/client/tls.go b/backend/internal/client/tls.go index 3087540..545e7e9 100644 --- a/backend/internal/client/tls.go +++ b/backend/internal/client/tls.go @@ -94,7 +94,7 @@ func createTLSClient(c *TLSClient, fp BrowserFingerprint, proxyStr string) (*TLS jar := tls_client.NewCookieJar() options := []tls_client.HttpClientOption{ - tls_client.WithTimeoutSeconds(90), + tls_client.WithTimeoutSeconds(45), tls_client.WithClientProfile(fp.TLSProfile), tls_client.WithRandomTLSExtensionOrder(), tls_client.WithCookieJar(jar), @@ -131,7 +131,7 @@ func createAzureTLSClient(c *TLSClient, fp BrowserFingerprint, proxyStr string) session.Browser = browser session.GetClientHelloSpec = azuretls.GetBrowserClientHelloFunc(browser) - session.SetTimeout(90 * time.Second) + session.SetTimeout(45 * time.Second) if proxyStr != "" { normalized, err := proxyutil.Normalize(proxyStr) diff --git a/backend/internal/mail/service.go b/backend/internal/mail/service.go index c4288f5..e58635f 100644 --- a/backend/internal/mail/service.go +++ b/backend/internal/mail/service.go @@ -2,6 +2,7 @@ package mail import ( "bytes" + "context" "encoding/json" "fmt" "math/rand" @@ -355,7 +356,13 @@ func (m *Client) GetEmails(email string, size int) ([]EmailItem, error) { // WaitForCode 等待验证码邮件 func (m *Client) WaitForCode(email string, timeout time.Duration) (string, error) { - start := time.Now() + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + return m.WaitForCodeWithContext(ctx, email) +} + +// WaitForCodeWithContext 等待验证码邮件(支持 context 取消) +func (m *Client) WaitForCodeWithContext(ctx context.Context, email string) (string, error) { // 匹配6位数字验证码 codeRegex := regexp.MustCompile(`\b(\d{6})\b`) // 专门匹配 OpenAI 验证码邮件标题格式: "Your ChatGPT code is 016547" 或 "OpenAI - Verify your email" @@ -381,7 +388,12 @@ func (m *Client) WaitForCode(email string, timeout time.Duration) (string, error } } - for time.Since(start) < timeout { + for { + select { + case <-ctx.Done(): + return "", fmt.Errorf("验证码获取超时") + default: + } emails, err := m.GetEmails(email, 10) if err == nil { for _, mail := range emails { @@ -435,10 +447,12 @@ func (m *Client) WaitForCode(email string, timeout time.Duration) (string, error } } } - time.Sleep(1 * time.Second) + select { + case <-ctx.Done(): + return "", fmt.Errorf("验证码获取超时") + case <-time.After(1 * time.Second): + } } - - return "", fmt.Errorf("验证码获取超时") } // WaitForInviteLink 等待邀请邮件并提取链接 @@ -520,11 +534,22 @@ func GetLatestEmailID(email string) int { // GetEmailOTPAfterID 获取指定邮件ID之后的OTP验证码 // 基于 get_code.go 的实现,只获取 afterEmailID 之后的新邮件中的验证码 func GetEmailOTPAfterID(email string, afterEmailID int, timeout time.Duration) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + return GetEmailOTPAfterIDWithContext(ctx, email, afterEmailID) +} + +// GetEmailOTPAfterIDWithContext 获取指定邮件ID之后的OTP验证码(支持 context 取消) +func GetEmailOTPAfterIDWithContext(ctx context.Context, email string, afterEmailID int) (string, error) { client := NewClientForEmail(email) - start := time.Now() codeRegex := regexp.MustCompile(`\b(\d{6})\b`) - for time.Since(start) < timeout { + for { + select { + case <-ctx.Done(): + return "", fmt.Errorf("验证码获取超时 (afterEmailID=%d)", afterEmailID) + default: + } emails, err := client.GetEmails(email, 5) if err == nil { for _, mail := range emails { @@ -554,8 +579,10 @@ func GetEmailOTPAfterID(email string, afterEmailID int, timeout time.Duration) ( } } } - time.Sleep(1 * time.Second) + select { + case <-ctx.Done(): + return "", fmt.Errorf("验证码获取超时 (afterEmailID=%d)", afterEmailID) + case <-time.After(1 * time.Second): + } } - - return "", fmt.Errorf("验证码获取超时 (afterEmailID=%d)", afterEmailID) } diff --git a/backend/internal/register/chatgpt.go b/backend/internal/register/chatgpt.go index 6beaf55..58c2edb 100644 --- a/backend/internal/register/chatgpt.go +++ b/backend/internal/register/chatgpt.go @@ -281,8 +281,27 @@ func Run(email, password, realName, birthdate, proxy string) (*ChatGPTReg, error return APIRegister(email, password, realName, birthdate, proxy, "[RegTest]") } -// APIRegister 使用 API 完成注册 (集成 403 重试机制) +// APIRegister 使用 API 完成注册 (集成 403 重试机制,全局 5 分钟超时) func APIRegister(email, password, realName, birthdate, proxy string, logPrefix string) (*ChatGPTReg, error) { + type regResult struct { + reg *ChatGPTReg + err error + } + resultCh := make(chan regResult, 1) + go func() { + reg, err := apiRegisterInternal(email, password, realName, birthdate, proxy, logPrefix) + resultCh <- regResult{reg, err} + }() + select { + case r := <-resultCh: + return r.reg, r.err + case <-time.After(5 * time.Minute): + return nil, fmt.Errorf("注册超时 (5分钟)") + } +} + +// apiRegisterInternal APIRegister 的内部实现 +func apiRegisterInternal(email, password, realName, birthdate, proxy string, logPrefix string) (*ChatGPTReg, error) { var reg *ChatGPTReg var lastErr error