package auth import ( "fmt" "os" "strings" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/proto" "github.com/go-rod/stealth" ) // RodAuth 使用 Rod + Stealth 完成 OAuth 授权 type RodAuth struct { browser *rod.Browser headless bool proxy string } // getChromiumPath 获取 Chromium 路径 func getChromiumPath() string { // 优先使用环境变量 if path := os.Getenv("CHROME_BIN"); path != "" { if _, err := os.Stat(path); err == nil { return path } } if path := os.Getenv("CHROME_PATH"); path != "" { if _, err := os.Stat(path); err == nil { return path } } // Alpine Linux 默认路径 paths := []string{ "/usr/bin/chromium-browser", "/usr/bin/chromium", "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", } for _, path := range paths { if _, err := os.Stat(path); err == nil { return path } } return "" // 让 Rod 自动下载 } // NewRodAuth 创建 Rod 授权器 func NewRodAuth(headless bool, proxy string) (*RodAuth, error) { l := launcher.New(). Headless(headless). Set("disable-blink-features", "AutomationControlled"). Set("disable-dev-shm-usage"). Set("no-sandbox"). Set("disable-gpu"). Set("disable-extensions"). Set("disable-background-networking"). Set("disable-sync"). Set("disable-translate"). Set("metrics-recording-only"). Set("no-first-run") // 使用系统 Chromium(如果存在) if chromiumPath := getChromiumPath(); chromiumPath != "" { l = l.Bin(chromiumPath) } if proxy != "" { l = l.Proxy(proxy) } controlURL, err := l.Launch() if err != nil { return nil, fmt.Errorf("启动浏览器失败: %v", err) } browser := rod.New().ControlURL(controlURL) if err := browser.Connect(); err != nil { return nil, fmt.Errorf("连接浏览器失败: %v", err) } return &RodAuth{ browser: browser, headless: headless, proxy: proxy, }, nil } // Close 关闭浏览器 func (r *RodAuth) Close() { if r.browser != nil { r.browser.Close() } } // CompleteOAuth 完成 OAuth 授权 func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string, error) { page, err := stealth.Page(r.browser) if err != nil { return "", fmt.Errorf("创建页面失败: %v", err) } defer page.Close() page = page.Timeout(45 * time.Second) if err := page.Navigate(authURL); err != nil { return "", fmt.Errorf("访问授权URL失败: %v", err) } page.MustWaitDOMStable() if code := r.checkForCode(page); code != "" { return code, nil } emailInput, err := page.Timeout(5 * time.Second).Element("input[name='email'], input[type='email'], input[name='username']") if err != nil { return "", fmt.Errorf("未找到邮箱输入框") } emailInput.MustSelectAllText().MustInput(email) time.Sleep(200 * time.Millisecond) if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil { btn.MustClick() } time.Sleep(1500 * time.Millisecond) if code := r.checkForCode(page); code != "" { return code, nil } passwordInput, err := page.Timeout(8 * time.Second).Element("input[type='password']") if err != nil { return "", fmt.Errorf("未找到密码输入框") } passwordInput.MustSelectAllText().MustInput(password) time.Sleep(200 * time.Millisecond) if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil { btn.MustClick() } for i := 0; i < 66; i++ { time.Sleep(300 * time.Millisecond) if code := r.checkForCode(page); code != "" { return code, nil } info, _ := page.Info() currentURL := info.URL if strings.Contains(currentURL, "consent") { if btn, _ := page.Timeout(500 * time.Millisecond).Element("button[type='submit']"); btn != nil { btn.Click(proto.InputMouseButtonLeft, 1) } } if strings.Contains(currentURL, "authorize") && teamID != "" { 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) } } } return "", fmt.Errorf("授权超时") } // checkForCode 检查 URL 中是否包含 code func (r *RodAuth) checkForCode(page *rod.Page) string { info, err := page.Info() if err != nil { return "" } if strings.Contains(info.URL, "code=") { return ExtractCodeFromCallbackURL(info.URL) } return "" } // CompleteWithRod 使用 Rod + Stealth 完成 S2A 授权 func CompleteWithRod(authURL, email, password, teamID string, headless bool, proxy string) (string, error) { auth, err := NewRodAuth(headless, proxy) if err != nil { return "", err } defer auth.Close() return auth.CompleteOAuth(authURL, email, password, teamID) } // CompleteWithBrowser 使用 Rod 完成 S2A 授权 (别名) func CompleteWithBrowser(authURL, email, password, teamID string, headless bool, proxy string) (string, error) { return CompleteWithRod(authURL, email, password, teamID, headless, proxy) }