package auth import ( "context" "fmt" "strings" "time" "codex-pool/internal/proxyutil" "github.com/chromedp/cdproto/cdp" "github.com/chromedp/cdproto/fetch" "github.com/chromedp/cdproto/network" "github.com/chromedp/cdproto/page" "github.com/chromedp/chromedp" ) // CompleteWithChromedp 使用 chromedp 完成 S2A OAuth 授权 func CompleteWithChromedp(authURL, email, password, teamID string, headless bool, proxy string) (string, error) { return CompleteWithChromedpLogged(authURL, email, password, teamID, headless, proxy, nil) } // CompleteWithChromedpLogged 使用 chromedp 完成 S2A OAuth 授权(带日志回调) func CompleteWithChromedpLogged(authURL, email, password, teamID string, headless bool, proxy string, logger *AuthLogger) (string, error) { // 日志辅助函数 logStep := func(step AuthStep, format string, args ...interface{}) { if logger != nil { logger.LogStep(step, format, args...) } } logError := func(step AuthStep, format string, args ...interface{}) { if logger != nil { logger.LogError(step, format, args...) } } // 获取随机浏览器配置 profile := GetRandomBrowserProfile() var proxyServer string var proxyUser string var proxyPass string if proxy != "" { info, err := proxyutil.Parse(proxy) if err != nil { return "", fmt.Errorf("代理格式错误: %v", err) } if info.Server != nil { proxyServer = info.Server.String() } proxyUser = info.Username proxyPass = info.Password } opts := append(chromedp.DefaultExecAllocatorOptions[:], chromedp.Flag("headless", headless), chromedp.Flag("disable-gpu", true), chromedp.Flag("no-sandbox", true), chromedp.Flag("disable-dev-shm-usage", true), chromedp.Flag("disable-blink-features", "AutomationControlled"), chromedp.Flag("disable-automation", true), chromedp.Flag("disable-extensions", true), chromedp.Flag("disable-infobars", true), chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"), // 使用随机 User-Agent chromedp.UserAgent(profile.UserAgent), // 使用随机窗口大小 chromedp.WindowSize(profile.Width, profile.Height), // 随机语言 chromedp.Flag("accept-lang", profile.AcceptLang), ) if proxyServer != "" { opts = append(opts, chromedp.ProxyServer(proxyServer)) } allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) defer cancel() ctx, cancel := chromedp.NewContext(allocCtx) defer cancel() // 设置合理的超时时间 30 秒 ctx, cancel = context.WithTimeout(ctx, 30*time.Second) defer cancel() var callbackURL string // 只在代理需要认证时才启用 Fetch 域 if proxyServer != "" && proxyUser != "" { chromedp.ListenTarget(ctx, func(ev interface{}) { switch ev := ev.(type) { case *fetch.EventRequestPaused: // Fetch domain pauses requests; we must continue them to avoid stalling navigation. reqID := ev.RequestID go func() { _ = fetch.ContinueRequest(reqID).Do(ctx) }() case *fetch.EventAuthRequired: reqID := ev.RequestID source := fetch.AuthChallengeSourceServer if ev.AuthChallenge != nil { source = ev.AuthChallenge.Source } go func() { resp := &fetch.AuthChallengeResponse{Response: fetch.AuthChallengeResponseResponseDefault} if source == fetch.AuthChallengeSourceProxy { if proxyUser != "" { resp.Response = fetch.AuthChallengeResponseResponseProvideCredentials resp.Username = proxyUser resp.Password = proxyPass } else { // Fail fast if the proxy requires auth but user didn't provide credentials. resp.Response = fetch.AuthChallengeResponseResponseCancelAuth } } _ = fetch.ContinueWithAuth(reqID, resp).Do(ctx) }() } }) } // 监听回调 URL chromedp.ListenTarget(ctx, func(ev interface{}) { if ev, ok := ev.(*network.EventRequestWillBeSent); ok { url := ev.Request.URL if strings.Contains(url, "localhost") && strings.Contains(url, "code=") { callbackURL = url } } }) // 获取反检测脚本 antiDetectionJS := GetAntiDetectionJS(profile) // 构建运行任务 tasks := []chromedp.Action{ network.Enable(), // 在每个新文档加载时注入反检测脚本 chromedp.ActionFunc(func(ctx context.Context) error { _, err := page.AddScriptToEvaluateOnNewDocument(antiDetectionJS).Do(ctx) return err }), chromedp.Navigate(authURL), chromedp.WaitReady("body"), } // 只在代理需要认证时才启用 Fetch 域 if proxyServer != "" && proxyUser != "" { tasks = append([]chromedp.Action{fetch.Enable().WithHandleAuthRequests(true)}, tasks...) } err := chromedp.Run(ctx, tasks...) if err != nil { logError(StepNavigate, "访问授权页失败: %v", err) return "", fmt.Errorf("访问失败: %v", err) } time.Sleep(2 * time.Second) if callbackURL != "" { logStep(StepComplete, "授权成功(快速通道)") return ExtractCodeFromCallbackURL(callbackURL), nil } var currentURL string _ = chromedp.Run(ctx, chromedp.Location(¤tURL)) logStep(StepNavigate, "页面加载完成 | URL: %s", currentURL) 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"]`, } // 创建带短超时的上下文用于查找元素(10秒) findCtx, findCancel := context.WithTimeout(ctx, 10*time.Second) var emailFilled bool for _, sel := range emailSelectors { err = chromedp.Run(findCtx, chromedp.WaitVisible(sel, chromedp.ByQuery)) if err == nil { err = chromedp.Run(ctx, chromedp.Clear(sel, chromedp.ByQuery), chromedp.SendKeys(sel, email, chromedp.ByQuery), ) if err == nil { emailFilled = true logStep(StepInputEmail, "邮箱已填写 | 选择器: %s", sel) break } } } findCancel() if !emailFilled { _ = chromedp.Run(ctx, chromedp.Location(¤tURL)) logError(StepInputEmail, "未找到邮箱输入框 | URL: %s", currentURL) return "", fmt.Errorf("未找到邮箱输入框") } time.Sleep(300 * time.Millisecond) buttonSelectors := []string{ `button[type="submit"]`, `div._ctas_1alro_13 button`, `button[data-testid="login-button"]`, `button.continue-btn`, `input[type="submit"]`, `button[name="action"]`, } for _, sel := range buttonSelectors { err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery)) if err == nil { break } } // 等待页面跳转(等待 URL 变化或密码框出现,最多 10 秒) passwordSelectors := []string{ `input[name="current-password"]`, `input[autocomplete="current-password"]`, `input[type="password"]`, `input[name="password"]`, `input[id="password"]`, } var passwordFound bool for i := 0; i < 20; i++ { time.Sleep(500 * time.Millisecond) if callbackURL != "" { logStep(StepComplete, "授权成功") return ExtractCodeFromCallbackURL(callbackURL), nil } _ = chromedp.Run(ctx, chromedp.Location(¤tURL)) if strings.Contains(currentURL, "code=") { logStep(StepComplete, "授权成功") return ExtractCodeFromCallbackURL(currentURL), nil } // 检查是否有密码输入框(页面已跳转) for _, sel := range passwordSelectors { var nodes []*cdp.Node if err := chromedp.Run(ctx, chromedp.Nodes(sel, &nodes, chromedp.ByQuery)); err == nil && len(nodes) > 0 { passwordFound = true break } } if passwordFound { break } // 如果 URL 已经变化(包含 password),也跳出 if strings.Contains(currentURL, "password") { break } } logStep(StepInputPassword, "查找密码框 | URL: %s", currentURL) // 使用短超时查找密码框(10秒) findCtx2, findCancel2 := context.WithTimeout(ctx, 10*time.Second) var passwordFilled bool for _, sel := range passwordSelectors { err = chromedp.Run(findCtx2, chromedp.WaitVisible(sel, chromedp.ByQuery)) if err == nil { err = chromedp.Run(ctx, chromedp.Clear(sel, chromedp.ByQuery), chromedp.SendKeys(sel, password, chromedp.ByQuery), ) if err == nil { passwordFilled = true logStep(StepInputPassword, "密码已填写 | 选择器: %s", sel) break } } } findCancel2() if !passwordFilled { _ = chromedp.Run(ctx, chromedp.Location(¤tURL)) logError(StepInputPassword, "未找到密码输入框 | URL: %s", currentURL) return "", fmt.Errorf("未找到密码输入框") } time.Sleep(300 * time.Millisecond) logStep(StepSubmitPassword, "正在登录...") for _, sel := range buttonSelectors { err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery)) if err == nil { break } } // 等待授权回调(最多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(StepComplete, "授权成功") return ExtractCodeFromCallbackURL(url), nil } if strings.Contains(url, "consent") { logStep(StepConsent, "处理授权同意... | URL: %s", url) // 同意页面的确认按钮(第二个按钮) consentSelectors := []string{ `div._ctas_1alro_13 div:nth-child(2) button`, `button[type="submit"]`, `div._ctas_1alro_13 button`, } for _, sel := range consentSelectors { err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery)) if err == nil { break } } time.Sleep(1 * time.Second) } if strings.Contains(url, "authorize") && teamID != "" { logStep(StepSelectWorkspace, "选择工作区...") err = chromedp.Run(ctx, chromedp.Click(fmt.Sprintf(`[data-workspace-id="%s"], [data-account-id="%s"]`, teamID, teamID), chromedp.ByQuery), ) } } } if callbackURL != "" { logStep(StepComplete, "授权成功") return ExtractCodeFromCallbackURL(callbackURL), nil } logError(StepWaitCallback, "授权超时") return "", fmt.Errorf("授权超时") }