feat: Implement S2A, Rod, and Chromedp based authentication for external services, and introduce new frontend pages and backend APIs for monitoring, configuration, upload, and team processes.

This commit is contained in:
2026-02-01 04:58:12 +08:00
parent 842a4ab4b2
commit e867bc5cbd
10 changed files with 314 additions and 22 deletions

View File

@@ -6,12 +6,30 @@ import (
"strings"
"time"
"codex-pool/internal/proxyutil"
"github.com/chromedp/cdproto/fetch"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp"
)
// CompleteWithChromedp 使用 chromedp 完成 S2A OAuth 授权
func CompleteWithChromedp(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
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),
@@ -21,8 +39,8 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"),
)
if proxy != "" {
opts = append(opts, chromedp.ProxyServer(proxy))
if proxyServer != "" {
opts = append(opts, chromedp.ProxyServer(proxyServer))
}
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
@@ -37,8 +55,36 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
var callbackURL string
chromedp.ListenTarget(ctx, func(ev interface{}) {
if req, ok := ev.(*network.EventRequestWillBeSent); ok {
url := req.Request.URL
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)
}()
case *network.EventRequestWillBeSent:
url := ev.Request.URL
if strings.Contains(url, "localhost") && strings.Contains(url, "code=") {
callbackURL = url
}
@@ -46,6 +92,8 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
})
err := chromedp.Run(ctx,
// Handle proxy auth (407) in headless mode.
fetch.Enable().WithHandleAuthRequests(true),
network.Enable(),
chromedp.Navigate(authURL),
chromedp.WaitReady("body"),

View File

@@ -6,6 +6,8 @@ import (
"strings"
"time"
"codex-pool/internal/proxyutil"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/proto"
@@ -17,6 +19,8 @@ type RodAuth struct {
browser *rod.Browser
headless bool
proxy string
proxyUser string
proxyPass string
}
// getChromiumPath 获取 Chromium 路径
@@ -49,6 +53,24 @@ func getChromiumPath() string {
// NewRodAuth 创建 Rod 授权器
func NewRodAuth(headless bool, proxy string) (*RodAuth, error) {
var proxyServer string
var proxyUser string
var proxyPass string
if proxy != "" {
info, err := proxyutil.Parse(proxy)
if err != nil {
return nil, fmt.Errorf("代理格式错误: %v", err)
}
if info.URL != nil {
proxy = info.URL.String() // normalized
}
if info.Server != nil {
proxyServer = info.Server.String()
}
proxyUser = info.Username
proxyPass = info.Password
}
l := launcher.New().
Headless(headless).
Set("disable-blink-features", "AutomationControlled").
@@ -67,8 +89,8 @@ func NewRodAuth(headless bool, proxy string) (*RodAuth, error) {
l = l.Bin(chromiumPath)
}
if proxy != "" {
l = l.Proxy(proxy)
if proxyServer != "" {
l = l.Proxy(proxyServer)
}
controlURL, err := l.Launch()
@@ -85,6 +107,8 @@ func NewRodAuth(headless bool, proxy string) (*RodAuth, error) {
browser: browser,
headless: headless,
proxy: proxy,
proxyUser: proxyUser,
proxyPass: proxyPass,
}, nil
}
@@ -97,6 +121,39 @@ func (r *RodAuth) Close() {
// CompleteOAuth 完成 OAuth 授权
func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string, error) {
// Handle proxy auth (407) in headless mode.
// When Fetch domain is enabled without patterns, requests will be paused and must be continued.
if r.proxy != "" {
authBrowser, cancel := r.browser.WithCancel()
defer cancel()
restoreFetch := authBrowser.EnableDomain("", &proto.FetchEnable{HandleAuthRequests: true})
defer restoreFetch()
wait := authBrowser.EachEvent(
func(e *proto.FetchRequestPaused) {
_ = proto.FetchContinueRequest{RequestID: e.RequestID}.Call(authBrowser)
},
func(e *proto.FetchAuthRequired) {
resp := &proto.FetchAuthChallengeResponse{
Response: proto.FetchAuthChallengeResponseResponseDefault,
}
if e.AuthChallenge != nil && e.AuthChallenge.Source == proto.FetchAuthChallengeSourceProxy {
if r.proxyUser != "" {
resp.Response = proto.FetchAuthChallengeResponseResponseProvideCredentials
resp.Username = r.proxyUser
resp.Password = r.proxyPass
} else {
// Fail fast if the proxy requires auth but user didn't provide credentials.
resp.Response = proto.FetchAuthChallengeResponseResponseCancelAuth
}
}
_ = proto.FetchContinueWithAuth{RequestID: e.RequestID, AuthChallengeResponse: resp}.Call(authBrowser)
},
)
go wait()
}
page, err := stealth.Page(r.browser)
if err != nil {
return "", fmt.Errorf("创建页面失败: %v", err)

View File

@@ -10,6 +10,8 @@ import (
"net/url"
"strings"
"time"
"codex-pool/internal/proxyutil"
)
const (
@@ -214,8 +216,13 @@ func RefreshCodexToken(refreshToken string, proxyURL string) (*CodexTokens, erro
client := &http.Client{Timeout: 30 * time.Second}
if proxyURL != "" {
proxyURLParsed, _ := url.Parse(proxyURL)
client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURLParsed)}
info, err := proxyutil.Parse(proxyURL)
if err != nil {
return nil, fmt.Errorf("代理格式错误: %v", err)
}
if info.URL != nil {
client.Transport = &http.Transport{Proxy: http.ProxyURL(info.URL)}
}
}
data := url.Values{