From 4cd9f2b2b7b723d7e38aa9b56a3dcfae2a7b59f6 Mon Sep 17 00:00:00 2001 From: kyx236 Date: Mon, 2 Feb 2026 05:05:07 +0800 Subject: [PATCH] feat: Implement browser-based OAuth authentication using chromedp and rod, and add a team processing API. --- backend/internal/api/team_process.go | 92 +++++----------------------- backend/internal/auth/chromedp.go | 56 ++++++++++------- backend/internal/auth/rod.go | 40 ++++++------ 3 files changed, 66 insertions(+), 122 deletions(-) diff --git a/backend/internal/api/team_process.go b/backend/internal/api/team_process.go index e5f3598..92be974 100644 --- a/backend/internal/api/team_process.go +++ b/backend/internal/api/team_process.go @@ -575,10 +575,9 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul } // 发送邀请 - logger.Info(fmt.Sprintf("%s [发送邀请] %s", memberLogPrefix, currentEmail), currentEmail, "team") if err := inviter.SendInvites([]string{currentEmail}); err != nil { errStr := err.Error() - logger.Error(fmt.Sprintf("%s [邀请失败] %v", memberLogPrefix, err), currentEmail, "team") + 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") { @@ -587,18 +586,14 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul } continue } - logger.Info(fmt.Sprintf("%s [邀请成功]", memberLogPrefix), currentEmail, "team") // 再次检查是否应该停止(邀请期间其他 goroutine 可能已标记) if isTeamExhausted() { return false } - // 创建注册日志记录器 - regLogger := NewRegisterLogger(memberLogPrefix, currentEmail) - // 注册 - _, err := registerWithTimeoutLogged(currentEmail, currentPassword, name, birthdate, req.Proxy, regLogger) + _, err := registerWithTimeout(currentEmail, currentPassword, name, birthdate, req.Proxy) if err != nil { logger.Error(fmt.Sprintf("%s [注册失败] %v", memberLogPrefix, err), currentEmail, "team") continue @@ -736,25 +731,23 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul logger.Warning(fmt.Sprintf("%s 入库重试 (第%d次)", memberLogPrefix, attempt+1), memberChild.Email, "team") } - // 创建日志回调 + // 创建日志回调(只输出关键日志) authLogger := auth.NewAuthLogger(memberChild.Email, logPrefix, memberIdx+1, func(entry auth.AuthLogEntry) { - stepName := auth.StepName(entry.Step) + // 只输出错误和关键步骤 if entry.IsError { - logger.Error(fmt.Sprintf("%s [%s] %s (%.1fs)", memberLogPrefix, stepName, entry.Message, entry.Duration.Seconds()), memberChild.Email, "team") - } else { - logger.Info(fmt.Sprintf("%s [%s] %s", memberLogPrefix, stepName, entry.Message), memberChild.Email, "team") + logger.Error(fmt.Sprintf("%s %s", memberLogPrefix, entry.Message), memberChild.Email, "team") + } else if entry.Step == auth.StepComplete || entry.Step == auth.StepConsent || entry.Step == auth.StepSelectWorkspace { + logger.Info(fmt.Sprintf("%s %s", memberLogPrefix, entry.Message), memberChild.Email, "team") } }) // 获取授权 URL - logger.Info(fmt.Sprintf("%s 获取 S2A 授权 URL...", memberLogPrefix), memberChild.Email, "team") s2aResp, err := auth.GenerateS2AAuthURL(config.Global.S2AApiBase, config.Global.S2AAdminKey, config.Global.ProxyID) if err != nil { lastError = fmt.Sprintf("获取授权URL失败: %v", err) logger.Error(fmt.Sprintf("%s %s", memberLogPrefix, lastError), memberChild.Email, "team") continue } - logger.Info(fmt.Sprintf("%s 授权 URL 获取成功, SessionID: %s", memberLogPrefix, s2aResp.Data.SessionID[:8]+"..."), memberChild.Email, "team") // 根据配置选择浏览器自动化 var code string @@ -765,13 +758,11 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul } if err != nil { lastError = fmt.Sprintf("浏览器授权失败: %v", err) - logger.Error(fmt.Sprintf("%s %s (耗时: %.1fs)", memberLogPrefix, lastError, authLogger.TotalDuration().Seconds()), memberChild.Email, "team") + logger.Error(fmt.Sprintf("%s %s", memberLogPrefix, lastError), memberChild.Email, "team") continue } - logger.Info(fmt.Sprintf("%s 浏览器授权成功, 授权码: %s... (耗时: %.1fs)", memberLogPrefix, code[:8], authLogger.TotalDuration().Seconds()), memberChild.Email, "team") // 提交到 S2A - logger.Info(fmt.Sprintf("%s 正在提交到 S2A...", memberLogPrefix), memberChild.Email, "team") _, err = auth.SubmitS2AOAuth( config.Global.S2AApiBase, config.Global.S2AAdminKey, @@ -826,8 +817,7 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul if req.IncludeOwner && teamProcessState.Running { ownerLogPrefix := fmt.Sprintf("%s [母号 ]", logPrefix) ownerStartTime := time.Now() - logger.Info(fmt.Sprintf("%s ════════ 开始母号入库 ════════", logPrefix), owner.Email, "team") - logger.Info(fmt.Sprintf("%s 开始入库 | 邮箱: %s", ownerLogPrefix, owner.Email), owner.Email, "team") + logger.Info(fmt.Sprintf("%s 开始母号入库...", ownerLogPrefix), owner.Email, "team") var ownerSuccess bool var lastError string @@ -836,24 +826,21 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul logger.Warning(fmt.Sprintf("%s 入库重试 (第%d次)", ownerLogPrefix, attempt+1), owner.Email, "team") } - // 创建日志回调 + // 创建日志回调(只输出关键日志) authLogger := auth.NewAuthLogger(owner.Email, logPrefix, 0, func(entry auth.AuthLogEntry) { - stepName := auth.StepName(entry.Step) if entry.IsError { - logger.Error(fmt.Sprintf("%s [%s] %s (%.1fs)", ownerLogPrefix, stepName, entry.Message, entry.Duration.Seconds()), owner.Email, "team") - } else { - logger.Info(fmt.Sprintf("%s [%s] %s", ownerLogPrefix, stepName, entry.Message), owner.Email, "team") + logger.Error(fmt.Sprintf("%s %s", ownerLogPrefix, entry.Message), owner.Email, "team") + } else if entry.Step == auth.StepComplete || entry.Step == auth.StepConsent || entry.Step == auth.StepSelectWorkspace { + logger.Info(fmt.Sprintf("%s %s", ownerLogPrefix, entry.Message), owner.Email, "team") } }) - logger.Info(fmt.Sprintf("%s 获取 S2A 授权 URL...", ownerLogPrefix), owner.Email, "team") s2aResp, err := auth.GenerateS2AAuthURL(config.Global.S2AApiBase, config.Global.S2AAdminKey, config.Global.ProxyID) if err != nil { lastError = fmt.Sprintf("获取授权URL失败: %v", err) logger.Error(fmt.Sprintf("%s %s", ownerLogPrefix, lastError), owner.Email, "team") continue } - logger.Info(fmt.Sprintf("%s 授权 URL 获取成功, SessionID: %s", ownerLogPrefix, s2aResp.Data.SessionID[:8]+"..."), owner.Email, "team") var code string if req.BrowserType == "rod" { @@ -863,12 +850,11 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul } if err != nil { lastError = fmt.Sprintf("浏览器授权失败: %v", err) - logger.Error(fmt.Sprintf("%s %s (耗时: %.1fs)", ownerLogPrefix, lastError, authLogger.TotalDuration().Seconds()), owner.Email, "team") + logger.Error(fmt.Sprintf("%s %s", ownerLogPrefix, lastError), owner.Email, "team") continue } - logger.Info(fmt.Sprintf("%s 浏览器授权成功, 授权码: %s... (耗时: %.1fs)", ownerLogPrefix, code[:8], authLogger.TotalDuration().Seconds()), owner.Email, "team") - logger.Info(fmt.Sprintf("%s 正在提交到 S2A...", ownerLogPrefix), owner.Email, "team") + // 提交到 S2A _, err = auth.SubmitS2AOAuth( config.Global.S2AApiBase, config.Global.S2AAdminKey, @@ -896,7 +882,6 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul if !ownerSuccess { result.Errors = append(result.Errors, fmt.Sprintf("母号入库失败: %s", lastError)) } - logger.Info(fmt.Sprintf("%s ════════ 母号入库完成 ════════", logPrefix), owner.Email, "team") } result.DurationMs = time.Since(startTime).Milliseconds() @@ -909,97 +894,50 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul return result } -// RegisterLogger 注册日志记录器 -type RegisterLogger struct { - logPrefix string - email string - startTime time.Time -} - -// NewRegisterLogger 创建注册日志记录器 -func NewRegisterLogger(logPrefix, email string) *RegisterLogger { - return &RegisterLogger{ - logPrefix: logPrefix, - email: email, - startTime: time.Now(), - } -} - -// LogStep 记录步骤 -func (l *RegisterLogger) LogStep(step string) { - logger.Info(fmt.Sprintf("%s [%s]", l.logPrefix, step), l.email, "team") -} - -// LogStepDone 记录步骤完成 -func (l *RegisterLogger) LogStepDone(step string, duration time.Duration) { - logger.Info(fmt.Sprintf("%s [%s] 完成 (%.1fs)", l.logPrefix, step, duration.Seconds()), l.email, "team") -} - // registerWithTimeout 带超时的注册 func registerWithTimeout(email, password, name, birthdate, proxy string) (*register.ChatGPTReg, error) { - return registerWithTimeoutLogged(email, password, name, birthdate, proxy, nil) -} - -// registerWithTimeoutLogged 带超时和日志的注册 -func registerWithTimeoutLogged(email, password, name, birthdate, proxy string, regLogger *RegisterLogger) (*register.ChatGPTReg, error) { - logStep := func(step string) { - if regLogger != nil { - regLogger.LogStep(step) - } - } - reg, err := register.New(proxy) if err != nil { return nil, err } - logStep("初始化会话") if err := reg.InitSession(); err != nil { return nil, fmt.Errorf("初始化失败: %v", err) } - logStep("获取授权URL") if err := reg.GetAuthorizeURL(email); err != nil { return nil, fmt.Errorf("获取授权URL失败: %v", err) } - logStep("启动授权") if err := reg.StartAuthorize(); err != nil { return nil, fmt.Errorf("启动授权失败: %v", err) } - logStep("提交注册信息") if err := reg.Register(email, password); err != nil { return nil, fmt.Errorf("注册失败: %v", err) } - logStep("发送验证邮件") if err := reg.SendVerificationEmail(); err != nil { return nil, fmt.Errorf("发送邮件失败: %v", err) } // 短超时获取验证码 - logStep("等待验证码 (5s)") otpCode, err := mail.GetVerificationCode(email, 5*time.Second) if err != nil { - logStep("等待验证码 (15s)") otpCode, err = mail.GetVerificationCode(email, 15*time.Second) if err != nil { return nil, fmt.Errorf("验证码获取超时") } } - logStep("验证OTP") if err := reg.ValidateOTP(otpCode); err != nil { return nil, fmt.Errorf("OTP验证失败: %v", err) } - logStep("创建账户") if err := reg.CreateAccount(name, birthdate); err != nil { return nil, fmt.Errorf("创建账户失败: %v", err) } - logStep("获取会话令牌") _ = reg.GetSessionToken() return reg, nil } diff --git a/backend/internal/auth/chromedp.go b/backend/internal/auth/chromedp.go index 0150324..78aa9bf 100644 --- a/backend/internal/auth/chromedp.go +++ b/backend/internal/auth/chromedp.go @@ -33,7 +33,6 @@ func CompleteWithChromedpLogged(authURL, email, password, teamID string, headles } } - logStep(StepBrowserStart, "正在启动 Chromedp 浏览器...") // 获取随机浏览器配置 profile := GetRandomBrowserProfile() @@ -80,8 +79,8 @@ func CompleteWithChromedpLogged(authURL, email, password, teamID string, headles ctx, cancel := chromedp.NewContext(allocCtx) defer cancel() - // 增加超时时间到 180 秒 - ctx, cancel = context.WithTimeout(ctx, 180*time.Second) + // 设置合理的超时时间 30 秒 + ctx, cancel = context.WithTimeout(ctx, 30*time.Second) defer cancel() var callbackURL string @@ -150,17 +149,16 @@ func CompleteWithChromedpLogged(authURL, email, password, teamID string, headles tasks = append([]chromedp.Action{fetch.Enable().WithHandleAuthRequests(true)}, tasks...) } - logStep(StepNavigate, "正在访问授权页面...") err := chromedp.Run(ctx, tasks...) if err != nil { - logError(StepNavigate, "访问失败: %v", err) + logError(StepNavigate, "访问授权页失败: %v", err) return "", fmt.Errorf("访问失败: %v", err) } time.Sleep(2 * time.Second) if callbackURL != "" { - logStep(StepExtractCode, "已捕获授权码回调") + logStep(StepComplete, "授权成功(快速通道)") return ExtractCodeFromCallbackURL(callbackURL), nil } @@ -168,21 +166,27 @@ func CompleteWithChromedpLogged(authURL, email, password, teamID string, headles _ = chromedp.Run(ctx, chromedp.Location(¤tURL)) if strings.Contains(currentURL, "code=") { + logStep(StepComplete, "授权成功(快速通道)") return ExtractCodeFromCallbackURL(currentURL), nil } time.Sleep(1 * time.Second) + // 邮箱输入框选择器 emailSelectors := []string{ `input[name="email"]`, `input[type="email"]`, `input[name="username"]`, + `input[id="email"]`, + `input[autocomplete="email"]`, } - logStep(StepInputEmail, "正在查找邮箱输入框...") + // 创建带短超时的上下文用于查找元素(10秒) + findCtx, findCancel := context.WithTimeout(ctx, 10*time.Second) + var emailFilled bool for _, sel := range emailSelectors { - err = chromedp.Run(ctx, chromedp.WaitVisible(sel, chromedp.ByQuery)) + err = chromedp.Run(findCtx, chromedp.WaitVisible(sel, chromedp.ByQuery)) if err == nil { err = chromedp.Run(ctx, chromedp.Clear(sel, chromedp.ByQuery), @@ -190,11 +194,11 @@ func CompleteWithChromedpLogged(authURL, email, password, teamID string, headles ) if err == nil { emailFilled = true - logStep(StepInputEmail, "已输入邮箱") break } } } + findCancel() if !emailFilled { logError(StepInputEmail, "未找到邮箱输入框") @@ -208,9 +212,9 @@ func CompleteWithChromedpLogged(authURL, email, password, teamID string, headles `button[data-testid="login-button"]`, `button.continue-btn`, `input[type="submit"]`, + `button[name="action"]`, } - logStep(StepSubmitEmail, "正在提交邮箱...") for _, sel := range buttonSelectors { err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery)) if err == nil { @@ -221,26 +225,31 @@ func CompleteWithChromedpLogged(authURL, email, password, teamID string, headles time.Sleep(1500 * time.Millisecond) if callbackURL != "" { - logStep(StepExtractCode, "已捕获授权码回调") + logStep(StepComplete, "授权成功") return ExtractCodeFromCallbackURL(callbackURL), nil } _ = chromedp.Run(ctx, chromedp.Location(¤tURL)) if strings.Contains(currentURL, "code=") { - logStep(StepExtractCode, "已获取授权码") + logStep(StepComplete, "授权成功") return ExtractCodeFromCallbackURL(currentURL), nil } - logStep(StepInputPassword, "正在查找密码输入框...") + // 密码输入框选择器 passwordSelectors := []string{ - `input[name="current-password"]`, - `input[name="password"]`, `input[type="password"]`, + `input[name="password"]`, + `input[name="current-password"]`, + `input[id="password"]`, + `input[autocomplete="current-password"]`, } + // 使用短超时查找密码框(10秒) + findCtx2, findCancel2 := context.WithTimeout(ctx, 10*time.Second) + var passwordFilled bool for _, sel := range passwordSelectors { - err = chromedp.Run(ctx, chromedp.WaitVisible(sel, chromedp.ByQuery)) + err = chromedp.Run(findCtx2, chromedp.WaitVisible(sel, chromedp.ByQuery)) if err == nil { err = chromedp.Run(ctx, chromedp.Clear(sel, chromedp.ByQuery), @@ -248,11 +257,11 @@ func CompleteWithChromedpLogged(authURL, email, password, teamID string, headles ) if err == nil { passwordFilled = true - logStep(StepInputPassword, "已输入密码") break } } } + findCancel2() if !passwordFilled { logError(StepInputPassword, "未找到密码输入框") @@ -261,7 +270,7 @@ func CompleteWithChromedpLogged(authURL, email, password, teamID string, headles time.Sleep(300 * time.Millisecond) - logStep(StepSubmitPassword, "正在提交密码...") + logStep(StepSubmitPassword, "正在登录...") for _, sel := range buttonSelectors { err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery)) if err == nil { @@ -269,23 +278,24 @@ func CompleteWithChromedpLogged(authURL, email, password, teamID string, headles } } - logStep(StepWaitCallback, "等待授权回调...") + // 等待授权回调(最多15秒) for i := 0; i < 30; i++ { time.Sleep(500 * time.Millisecond) if callbackURL != "" { + logStep(StepComplete, "授权成功") return ExtractCodeFromCallbackURL(callbackURL), nil } var url string if err := chromedp.Run(ctx, chromedp.Location(&url)); err == nil { if strings.Contains(url, "code=") { - logStep(StepExtractCode, "已获取授权码") + logStep(StepComplete, "授权成功") return ExtractCodeFromCallbackURL(url), nil } if strings.Contains(url, "consent") { - logStep(StepConsent, "正在处理授权同意页面...") + logStep(StepConsent, "处理授权同意...") for _, sel := range buttonSelectors { err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery)) if err == nil { @@ -296,7 +306,7 @@ func CompleteWithChromedpLogged(authURL, email, password, teamID string, headles } if strings.Contains(url, "authorize") && teamID != "" { - logStep(StepSelectWorkspace, "正在选择工作区: %s", teamID) + logStep(StepSelectWorkspace, "选择工作区...") err = chromedp.Run(ctx, chromedp.Click(fmt.Sprintf(`[data-workspace-id="%s"], [data-account-id="%s"]`, teamID, teamID), chromedp.ByQuery), ) @@ -305,7 +315,7 @@ func CompleteWithChromedpLogged(authURL, email, password, teamID string, headles } if callbackURL != "" { - logStep(StepComplete, "授权完成") + logStep(StepComplete, "授权成功") return ExtractCodeFromCallbackURL(callbackURL), nil } diff --git a/backend/internal/auth/rod.go b/backend/internal/auth/rod.go index 5f869ff..bd88e73 100644 --- a/backend/internal/auth/rod.go +++ b/backend/internal/auth/rod.go @@ -208,67 +208,63 @@ func (r *RodAuth) CompleteOAuthLogged(authURL, email, password, teamID string, l ThisObj: nil, }) - // 增加超时时间到 90 秒 - page = page.Timeout(90 * time.Second) + // 设置合理的超时时间 60 秒 + page = page.Timeout(60 * time.Second) - logStep(StepNavigate, "正在访问授权页面...") if err := page.Navigate(authURL); err != nil { - logError(StepNavigate, "访问失败: %v", err) + logError(StepNavigate, "访问授权页失败: %v", err) return "", fmt.Errorf("访问授权URL失败: %v", err) } page.MustWaitDOMStable() if code := r.checkForCode(page); code != "" { - logStep(StepExtractCode, "已捕获授权码回调") + logStep(StepComplete, "授权成功(快速通道)") return code, nil } - logStep(StepInputEmail, "正在查找邮箱输入框...") - emailInput, err := page.Timeout(5 * time.Second).Element("input[name='email'], input[type='email'], input[name='username']") + // 使用10秒超时查找邮箱输入框 + emailInput, err := page.Timeout(10 * time.Second).Element("input[name='email'], input[type='email'], input[name='username'], input[id='email'], input[autocomplete='email']") if err != nil { logError(StepInputEmail, "未找到邮箱输入框") return "", fmt.Errorf("未找到邮箱输入框") } emailInput.MustSelectAllText().MustInput(email) - logStep(StepInputEmail, "已输入邮箱") time.Sleep(200 * time.Millisecond) - logStep(StepSubmitEmail, "正在提交邮箱...") - if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil { + if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit'], button[name='action']"); btn != nil { btn.MustClick() } time.Sleep(1500 * time.Millisecond) if code := r.checkForCode(page); code != "" { - logStep(StepExtractCode, "已获取授权码") + logStep(StepComplete, "授权成功") return code, nil } - logStep(StepInputPassword, "正在查找密码输入框...") - passwordInput, err := page.Timeout(8 * time.Second).Element("input[type='password']") + // 使用10秒超时查找密码输入框 + passwordInput, err := page.Timeout(10 * time.Second).Element("input[type='password'], input[name='password'], input[id='password']") if err != nil { logError(StepInputPassword, "未找到密码输入框") return "", fmt.Errorf("未找到密码输入框") } passwordInput.MustSelectAllText().MustInput(password) - logStep(StepInputPassword, "已输入密码") time.Sleep(200 * time.Millisecond) - logStep(StepSubmitPassword, "正在提交密码...") - if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil { + logStep(StepSubmitPassword, "正在登录...") + if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit'], button[name='action']"); btn != nil { btn.MustClick() } - logStep(StepWaitCallback, "等待授权回调...") - for i := 0; i < 66; i++ { - time.Sleep(300 * time.Millisecond) + // 等待授权回调(最多20秒) + for i := 0; i < 40; i++ { + time.Sleep(500 * time.Millisecond) if code := r.checkForCode(page); code != "" { - logStep(StepComplete, "授权完成") + logStep(StepComplete, "授权成功") return code, nil } @@ -276,14 +272,14 @@ func (r *RodAuth) CompleteOAuthLogged(authURL, email, password, teamID string, l currentURL := info.URL if strings.Contains(currentURL, "consent") { - logStep(StepConsent, "正在处理授权同意页面...") + logStep(StepConsent, "处理授权同意...") if btn, _ := page.Timeout(500 * time.Millisecond).Element("button[type='submit']"); btn != nil { btn.Click(proto.InputMouseButtonLeft, 1) } } if strings.Contains(currentURL, "authorize") && teamID != "" { - logStep(StepSelectWorkspace, "正在选择工作区: %s", teamID) + logStep(StepSelectWorkspace, "选择工作区...") wsSelector := fmt.Sprintf("[data-workspace-id='%s'], [data-account-id='%s']", teamID, teamID) if wsBtn, _ := page.Timeout(500 * time.Millisecond).Element(wsSelector); wsBtn != nil { wsBtn.Click(proto.InputMouseButtonLeft, 1)