diff --git a/.gitignore b/.gitignore index dfb69a4..e897727 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,4 @@ check_ban.py backend/codex-pool.exe backend/codex-pool.exe .claude/settings.local.json +CodexAuth \ No newline at end of file diff --git a/backend/cmd/main.go b/backend/cmd/main.go index f348eb4..a497f82 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -232,6 +232,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) { "team_reg_proxy_test_status": getTeamRegProxyTestStatus(), "team_reg_proxy_test_ip": getTeamRegProxyTestIP(), "site_name": config.Global.SiteName, + "auth_method": config.Global.AuthMethod, "mail_services_count": len(config.Global.MailServices), "mail_services": config.Global.MailServices, }) @@ -248,6 +249,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) { DefaultProxy *string `json:"default_proxy"` TeamRegProxy *string `json:"team_reg_proxy"` SiteName *string `json:"site_name"` + AuthMethod *string `json:"auth_method"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { api.Error(w, http.StatusBadRequest, "请求格式错误") @@ -287,6 +289,9 @@ func handleConfig(w http.ResponseWriter, r *http.Request) { if req.SiteName != nil { config.Global.SiteName = *req.SiteName } + if req.AuthMethod != nil { + config.Global.AuthMethod = *req.AuthMethod + } // 保存到数据库 (实时生效) if err := config.Update(config.Global); err != nil { diff --git a/backend/internal/api/team_process.go b/backend/internal/api/team_process.go index 3d730ef..a45b937 100644 --- a/backend/internal/api/team_process.go +++ b/backend/internal/api/team_process.go @@ -32,14 +32,14 @@ type TeamProcessRequest struct { // Owner 账号列表 Owners []TeamOwner `json:"owners"` // 配置 - MembersPerTeam int `json:"members_per_team"` // 每个 Team 的成员数 - ConcurrentTeams int `json:"concurrent_teams"` // 并发 Team 数量 - ConcurrentS2A int `json:"concurrent_s2a"` // 入库并发数(默认2) - BrowserType string `json:"browser_type"` // "chromedp" 或 "rod" - Headless bool `json:"headless"` // 是否无头模式 - Proxy string `json:"proxy"` // 代理设置 - IncludeOwner bool `json:"include_owner"` // 母号也入库到 S2A - ProcessCount int `json:"process_count"` // 处理数量,0表示全部 + MembersPerTeam int `json:"members_per_team"` // 每个 Team 的成员数 + ConcurrentTeams int `json:"concurrent_teams"` // 并发 Team 数量 + ConcurrentS2A int `json:"concurrent_s2a"` // 入库并发数(默认2) + BrowserType string `json:"browser_type"` // "chromedp" 或 "rod" + Headless bool `json:"headless"` // 是否无头模式 + Proxy string `json:"proxy"` // 代理设置 + IncludeOwner bool `json:"include_owner"` // 母号也入库到 S2A + ProcessCount int `json:"process_count"` // 处理数量,0表示全部 } // TeamProcessResult 团队处理结果 @@ -755,7 +755,11 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul // 根据配置选择浏览器自动化 var code string - if req.BrowserType == "rod" { + // 根据全局配置决定授权方式 + if config.Global.AuthMethod == "api" { + // 使用纯 API 模式(CodexAuth) + code, err = auth.CompleteWithCodexAPI(memberChild.Email, memberChild.Password, teamID, req.Proxy, authLogger) + } else if req.BrowserType == "rod" { code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, memberChild.Email, memberChild.Password, teamID, req.Headless, req.Proxy, authLogger) } else { code, err = auth.CompleteWithChromedpLogged(s2aResp.Data.AuthURL, memberChild.Email, memberChild.Password, teamID, req.Headless, req.Proxy, authLogger) @@ -851,7 +855,11 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul } var code string - if req.BrowserType == "rod" { + // 根据全局配置决定授权方式 + if config.Global.AuthMethod == "api" { + // 使用纯 API 模式(CodexAuth) + code, err = auth.CompleteWithCodexAPI(owner.Email, owner.Password, teamID, req.Proxy, authLogger) + } else if req.BrowserType == "rod" { code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy, authLogger) } else { code, err = auth.CompleteWithChromedpLogged(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy, authLogger) diff --git a/backend/internal/auth/codex_api.go b/backend/internal/auth/codex_api.go new file mode 100644 index 0000000..ecf250c --- /dev/null +++ b/backend/internal/auth/codex_api.go @@ -0,0 +1,483 @@ +package auth + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "math/rand" + "net/http" + "net/url" + "strings" + "time" + + "codex-pool/internal/client" +) + +// 常量 CodexClientID, CodexRedirectURI, CodexScope 已在 s2a.go 中定义 + +// CodexAPIAuth 纯 API 授权 (无浏览器) +type CodexAPIAuth struct { + client *client.TLSClient + email string + password string + workspaceID string + deviceID string + sid string + sentinelToken string + solvedPow string + userAgent string + logger *AuthLogger +} + +// NewCodexAPIAuth 创建 CodexAuth 实例 +func NewCodexAPIAuth(email, password, workspaceID, proxy string, logger *AuthLogger) (*CodexAPIAuth, error) { + tlsClient, err := client.New(proxy) + if err != nil { + return nil, fmt.Errorf("创建 TLS 客户端失败: %v", err) + } + + return &CodexAPIAuth{ + client: tlsClient, + email: email, + password: password, + workspaceID: workspaceID, + deviceID: generateUUID(), + sid: generateUUID(), + userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + logger: logger, + }, nil +} + +// generateUUID 生成 UUID v4 +func generateUUID() string { + b := make([]byte, 16) + rand.Read(b) + b[6] = (b[6] & 0x0f) | 0x40 + b[8] = (b[8] & 0x3f) | 0x80 + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) +} + +// generateCodeVerifier 生成 PKCE code_verifier +func generateCodeVerifier() string { + b := make([]byte, 64) + rand.Read(b) + return base64.RawURLEncoding.EncodeToString(b) +} + +// generateCodeChallenge 生成 PKCE code_challenge (S256) +func generateCodeChallenge(verifier string) string { + hash := sha256.Sum256([]byte(verifier)) + return base64.RawURLEncoding.EncodeToString(hash[:]) +} + +// generateState 生成 state +func generateState() string { + b := make([]byte, 16) + rand.Read(b) + return base64.RawURLEncoding.EncodeToString(b) +} + +// fnv1a32 FNV-1a 32-bit hash +func fnv1a32(data []byte) uint32 { + h := uint32(2166136261) + for _, b := range data { + h ^= uint32(b) + h *= 16777619 + } + h ^= (h >> 16) + h *= 2246822507 + h ^= (h >> 13) + h *= 3266489909 + h ^= (h >> 16) + return h +} + +// getParseTime 生成 JS Date().toString() 格式的时间字符串 +func getParseTime() string { + now := time.Now() + return now.Format("Mon Jan 02 2006 15:04:05") + " GMT+0800 (中国标准时间)" +} + +// getConfig 生成 PoW 配置数组 +func (c *CodexAPIAuth) getConfig() []interface{} { + return []interface{}{ + 2500 + rand.Intn(1000), + getParseTime(), + 4294967296, + 0, + c.userAgent, + "chrome-extension://pgojnojmmhpofjgdmaebadhbocahppod/assets/aW5qZWN0X2hhc2g/aW5qZ", + nil, + "zh-CN", + "zh-CN", + 0, + "canShare−function canShare() { [native code] }", + fmt.Sprintf("_reactListening%d", 1000000+rand.Intn(9000000)), + "onhashchange", + float64(time.Now().UnixNano()/1e6) / 1000.0, + c.sid, + "", + 24, + time.Now().UnixMilli() - int64(10000+rand.Intn(40000)), + } +} + +// solvePow 解决 PoW 挑战 +func (c *CodexAPIAuth) solvePow(seed, difficulty string, cfg []interface{}, maxIterations int) string { + seedBytes := []byte(seed) + + for i := 0; i < maxIterations; i++ { + cfg[3] = i + cfg[9] = 0 + + jsonStr, _ := json.Marshal(cfg) + encoded := base64.StdEncoding.EncodeToString(jsonStr) + + h := fnv1a32(append(seedBytes, []byte(encoded)...)) + hexHash := fmt.Sprintf("%08x", h) + + if hexHash[:len(difficulty)] <= difficulty { + return encoded + "~S" + } + } + + return "" +} + +// getRequirementsToken 生成初始 token +func (c *CodexAPIAuth) getRequirementsToken() string { + cfg := c.getConfig() + cfg[3] = 0 + cfg[9] = 0 + jsonStr, _ := json.Marshal(cfg) + encoded := base64.StdEncoding.EncodeToString(jsonStr) + return "gAAAAAC" + encoded + "~S" +} + +// callSentinelReq 调用 Sentinel 获取 token +func (c *CodexAPIAuth) callSentinelReq(flow string) error { + initToken := c.getRequirementsToken() + payload := map[string]interface{}{ + "p": initToken, + "id": c.deviceID, + "flow": flow, + } + + body, _ := json.Marshal(payload) + req, _ := http.NewRequest("POST", "https://sentinel.openai.com/backend-api/sentinel/req", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + resp, err := c.client.Do(req) + if err != nil { + return fmt.Errorf("sentinel 请求失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("sentinel 状态码: %d", resp.StatusCode) + } + + respBody, _ := client.ReadBody(resp) + var result struct { + Token string `json:"token"` + ProofOfWork struct { + Required bool `json:"required"` + Seed string `json:"seed"` + Difficulty string `json:"difficulty"` + } `json:"proofofwork"` + } + + if err := json.Unmarshal(respBody, &result); err != nil { + return fmt.Errorf("解析 sentinel 响应失败: %v", err) + } + + c.sentinelToken = result.Token + + if result.ProofOfWork.Required { + cfg := c.getConfig() + solved := c.solvePow(result.ProofOfWork.Seed, result.ProofOfWork.Difficulty, cfg, 5000000) + if solved == "" { + return fmt.Errorf("PoW 解决失败") + } + c.solvedPow = "gAAAAAB" + solved + } else { + c.solvedPow = initToken + } + + return nil +} + +// getSentinelHeader 构建 sentinel header +func (c *CodexAPIAuth) getSentinelHeader(flow string) string { + obj := map[string]interface{}{ + "p": c.solvedPow, + "id": c.deviceID, + "flow": flow, + } + if c.sentinelToken != "" { + obj["c"] = c.sentinelToken + } + header, _ := json.Marshal(obj) + return string(header) +} + +// logStep 记录日志 +func (c *CodexAPIAuth) logStep(step AuthStep, format string, args ...interface{}) { + if c.logger != nil { + c.logger.LogStep(step, format, args...) + } +} + +// logError 记录错误 +func (c *CodexAPIAuth) logError(step AuthStep, format string, args ...interface{}) { + if c.logger != nil { + c.logger.LogError(step, format, args...) + } +} + +// ObtainAuthorizationCode 获取授权码 +func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { + c.logStep(StepNavigate, "开始 Codex API 授权流程...") + + // 1. 生成 PKCE 参数 + codeVerifier := generateCodeVerifier() + codeChallenge := generateCodeChallenge(codeVerifier) + state := generateState() + + // 2. 构建授权 URL + params := url.Values{ + "client_id": {CodexClientID}, + "scope": {CodexScope}, + "response_type": {"code"}, + "redirect_uri": {CodexRedirectURI}, + "code_challenge": {codeChallenge}, + "code_challenge_method": {"S256"}, + "state": {state}, + "id_token_add_organizations": {"true"}, + "codex_cli_simplified_flow": {"true"}, + "originator": {"codex_cli_rs"}, + } + + authURL := "https://auth.openai.com/oauth/authorize?" + params.Encode() + + // 3. 访问授权页面 + c.logStep(StepNavigate, "访问授权页面...") + req, _ := http.NewRequest("GET", authURL, nil) + req.Header.Set("Referer", "https://auth.openai.com/log-in") + resp, err := c.client.Do(req) + if err != nil { + c.logError(StepNavigate, "访问授权页失败: %v", err) + return "", fmt.Errorf("访问授权页失败: %v", err) + } + defer resp.Body.Close() + client.ReadBody(resp) // 消耗响应体 + referer := resp.Request.URL.String() + + // 4. 提交邮箱 + c.logStep(StepInputEmail, "提交邮箱: %s", c.email) + if err := c.callSentinelReq("login_web_init"); err != nil { + c.logError(StepInputEmail, "Sentinel 请求失败: %v", err) + return "", err + } + + emailPayload := map[string]interface{}{ + "username": map[string]string{ + "kind": "email", + "value": c.email, + }, + } + emailBody, _ := json.Marshal(emailPayload) + + req, _ = http.NewRequest("POST", "https://auth.openai.com/api/accounts/authorize/continue", bytes.NewReader(emailBody)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Origin", "https://auth.openai.com") + req.Header.Set("Referer", referer) + req.Header.Set("OpenAI-Sentinel-Token", c.getSentinelHeader("authorize_continue")) + + resp, err = c.client.Do(req) + if err != nil { + c.logError(StepInputEmail, "提交邮箱失败: %v", err) + return "", fmt.Errorf("提交邮箱失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + body, _ := client.ReadBody(resp) + c.logError(StepInputEmail, "提交邮箱失败: %d - %s", resp.StatusCode, string(body)) + return "", fmt.Errorf("提交邮箱失败: %d", resp.StatusCode) + } + + // 解析响应,检查是否需要密码 + emailResp, _ := client.ReadBody(resp) + var emailResult map[string]interface{} + json.Unmarshal(emailResp, &emailResult) + + // 5. 验证密码 + c.logStep(StepInputPassword, "验证密码...") + if err := c.callSentinelReq("authorize_continue__auto"); err != nil { + c.logError(StepInputPassword, "Sentinel 请求失败: %v", err) + return "", err + } + + pwdPayload := map[string]string{ + "username": c.email, + "password": c.password, + } + pwdBody, _ := json.Marshal(pwdPayload) + + req, _ = http.NewRequest("POST", "https://auth.openai.com/api/accounts/password/verify", bytes.NewReader(pwdBody)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Origin", "https://auth.openai.com") + req.Header.Set("Referer", referer) + req.Header.Set("OpenAI-Sentinel-Token", c.getSentinelHeader("password_verify")) + + resp, err = c.client.Do(req) + if err != nil { + c.logError(StepInputPassword, "验证密码失败: %v", err) + return "", fmt.Errorf("验证密码失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + body, _ := client.ReadBody(resp) + c.logError(StepInputPassword, "密码验证失败: %d - %s", resp.StatusCode, string(body)) + return "", fmt.Errorf("密码验证失败: %d", resp.StatusCode) + } + + // 6. 选择工作区 + c.logStep(StepSelectWorkspace, "选择工作区: %s", c.workspaceID) + if err := c.callSentinelReq("password_verify__auto"); err != nil { + c.logError(StepSelectWorkspace, "Sentinel 请求失败: %v", err) + return "", err + } + + wsPayload := map[string]string{ + "workspace_id": c.workspaceID, + } + wsBody, _ := json.Marshal(wsPayload) + + req, _ = http.NewRequest("POST", "https://auth.openai.com/api/accounts/workspace/select", bytes.NewReader(wsBody)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Origin", "https://auth.openai.com") + req.Header.Set("Referer", referer) + + resp, err = c.client.Do(req) + if err != nil { + c.logError(StepSelectWorkspace, "选择工作区失败: %v", err) + return "", fmt.Errorf("选择工作区失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + body, _ := client.ReadBody(resp) + c.logError(StepSelectWorkspace, "选择工作区失败: %d - %s", resp.StatusCode, string(body)) + return "", fmt.Errorf("选择工作区失败: %d", resp.StatusCode) + } + + // 解析 continue_url + wsResp, _ := client.ReadBody(resp) + var wsResult struct { + ContinueURL string `json:"continue_url"` + } + if err := json.Unmarshal(wsResp, &wsResult); err != nil || wsResult.ContinueURL == "" { + c.logError(StepSelectWorkspace, "未获取到 continue_url") + return "", fmt.Errorf("未获取到 continue_url") + } + + // 7. 跟随重定向获取授权码 + c.logStep(StepWaitCallback, "跟随重定向...") + continueURL := wsResult.ContinueURL + + for i := 0; i < 10; i++ { + req, _ = http.NewRequest("GET", continueURL, nil) + resp, err = c.client.Do(req) + if err != nil { + break + } + + location := resp.Header.Get("Location") + resp.Body.Close() + + if resp.StatusCode >= 301 && resp.StatusCode <= 308 { + if strings.Contains(location, "localhost:1455") { + // 提取授权码 + code := ExtractCodeFromCallbackURL(location) + if code != "" { + c.logStep(StepComplete, "授权成功,获取到授权码") + return code, nil + } + } + continueURL = location + } else { + break + } + } + + c.logError(StepWaitCallback, "未能获取授权码") + return "", fmt.Errorf("未能获取授权码") +} + +// ExchangeCodeForTokens 用授权码换取 tokens +func (c *CodexAPIAuth) ExchangeCodeForTokens(code, codeVerifier string) (*CodexTokens, error) { + payload := map[string]string{ + "grant_type": "authorization_code", + "client_id": CodexClientID, + "code_verifier": codeVerifier, + "code": code, + "redirect_uri": CodexRedirectURI, + } + + body, _ := json.Marshal(payload) + req, _ := http.NewRequest("POST", "https://auth.openai.com/oauth/token", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("token 交换失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + respBody, _ := client.ReadBody(resp) + return nil, fmt.Errorf("token 交换失败: %d - %s", resp.StatusCode, string(respBody)) + } + + respBody, _ := client.ReadBody(resp) + var tokens CodexTokens + if err := json.Unmarshal(respBody, &tokens); err != nil { + return nil, fmt.Errorf("解析 token 失败: %v", err) + } + + if tokens.ExpiresIn > 0 { + expiredAt := time.Now().Add(time.Duration(tokens.ExpiresIn) * time.Second) + tokens.ExpiredAt = expiredAt.Format(time.RFC3339) + } + + return &tokens, nil +} + +// CodexTokens 结构体已在 s2a.go 中定义 +// CompleteWithCodexAPI 使用纯 API 方式完成授权 +func CompleteWithCodexAPI(email, password, workspaceID, proxy string, logger *AuthLogger) (string, error) { + if logger != nil { + logger.LogStep(StepBrowserStart, "使用 CodexAuth API 模式...") + } + + auth, err := NewCodexAPIAuth(email, password, workspaceID, proxy, logger) + if err != nil { + if logger != nil { + logger.LogError(StepBrowserStart, "创建 CodexAuth 失败: %v", err) + } + return "", err + } + + code, err := auth.ObtainAuthorizationCode() + if err != nil { + return "", err + } + + return code, nil +} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 93c8fd7..f1d597b 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -39,6 +39,9 @@ type Config struct { DefaultProxy string `json:"default_proxy"` TeamRegProxy string `json:"team_reg_proxy"` // Team 注册使用的代理 + // 授权方式配置 + AuthMethod string `json:"auth_method"` // "api" 或 "browser" + // 自动化配置 AutoPauseOnExpired bool `json:"auto_pause_on_expired"` AccountsPath string `json:"accounts_path"` @@ -181,6 +184,11 @@ func InitFromDB() *Config { if v, _ := configDB.GetConfig("site_name"); v != "" { cfg.SiteName = v } + if v, _ := configDB.GetConfig("auth_method"); v != "" { + cfg.AuthMethod = v + } else { + cfg.AuthMethod = "browser" // 默认使用浏览器模式 + } Global = cfg return cfg @@ -222,6 +230,10 @@ func SaveToDB() error { configDB.SetConfig("site_name", cfg.SiteName) } + if cfg.AuthMethod != "" { + configDB.SetConfig("auth_method", cfg.AuthMethod) + } + return nil } diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index 926aeec..59dfd14 100644 --- a/frontend/src/pages/Config.tsx +++ b/frontend/src/pages/Config.tsx @@ -13,7 +13,9 @@ import { Globe, Wifi, WifiOff, - HelpCircle + HelpCircle, + Zap, + Monitor } from 'lucide-react' import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common' import { useConfig } from '../hooks/useConfig' @@ -33,6 +35,8 @@ export default function Config() { const [teamRegProxyStatus, setTeamRegProxyStatus] = useState<'unknown' | 'success' | 'error'>('unknown') const [proxyOriginIP, setProxyOriginIP] = useState('') const [teamRegProxyIP, setTeamRegProxyIP] = useState('') + const [authMethod, setAuthMethod] = useState<'api' | 'browser'>('browser') + const [savingAuthMethod, setSavingAuthMethod] = useState(false) const { toasts, toast, removeToast } = useToast() // 加载站点名称和代理配置 @@ -61,6 +65,10 @@ export default function Config() { if (data.data.team_reg_proxy_test_ip) { setTeamRegProxyIP(data.data.team_reg_proxy_test_ip) } + // 加载授权方式 + if (data.data.auth_method) { + setAuthMethod(data.data.auth_method === 'api' ? 'api' : 'browser') + } } } catch (error) { console.error('Failed to fetch config:', error) @@ -204,6 +212,33 @@ export default function Config() { } } + // 保存授权方式 + const handleSaveAuthMethod = async (method: 'api' | 'browser') => { + setSavingAuthMethod(true) + const previousMethod = authMethod + setAuthMethod(method) // 立即更新 UI + try { + const res = await fetch('/api/config', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ auth_method: method }), + }) + const data = await res.json() + if (data.code === 0) { + toast.success(`授权方式已切换为 ${method === 'api' ? 'CodexAuth API' : '浏览器模拟'}`) + refreshConfig() + } else { + setAuthMethod(previousMethod) // 回滚 + toast.error(data.message || '保存失败') + } + } catch { + setAuthMethod(previousMethod) // 回滚 + toast.error('网络错误') + } finally { + setSavingAuthMethod(false) + } + } + const configItems = [ { to: '/config/s2a', @@ -311,7 +346,91 @@ export default function Config() {

- {/* 代理地址配置 */} + {/* 授权方式配置 */} +
+ +
+ {/* API 模式 - CodexAuth */} + + + {/* 浏览器模式 */} + +
+ {authMethod === 'api' && ( +
+
+ + CodexAuth API 模式已启用 +
+

+ 使用纯 API 调用进行授权,无需启动浏览器,速度更快、更稳定 +

+
+ )} +
+
- {/* 浏览器选择器 */} -
- -
- - + {/* 授权方式状态显示 */} +
+
+ {authMethod === 'api' ? ( + <> +
+ +
+
+
CodexAuth API 模式
+
纯 API 调用,无需浏览器,速度更快、更稳定
+
+ + ) : ( + <> +
+ +
+
+
浏览器模拟模式 ({browserType})
+
使用浏览器自动化进行授权,兼容性更好
+
+ + )}
-

- 选择用于自动授权的浏览器引擎,Chromedp 为默认高效选项,Rod 为备选 +

+ 要切换授权方式,请在“基础配置”页面中修改

('chromedp') + const [browserType] = useState<'chromedp' | 'rod'>('chromedp') // 浏览器引擎类型(只读) const [useProxy, setUseProxy] = useState(false) // 是否使用全局代理 const [includeOwner, setIncludeOwner] = useState(false) // 母号也入库 const [processCount, setProcessCount] = useState(0) // 处理数量,0表示全部 + const [authMethod, setAuthMethod] = useState<'api' | 'browser'>('browser') // 授权方式 // 获取全局代理地址 const globalProxy = config.proxy?.default || '' + // 加载全局配置中的授权方式 + useEffect(() => { + const fetchAuthMethod = async () => { + try { + const res = await fetch('/api/config') + const data = await res.json() + if (data.code === 0 && data.data?.auth_method) { + setAuthMethod(data.data.auth_method === 'api' ? 'api' : 'browser') + } + } catch (error) { + console.error('Failed to fetch auth method:', error) + } + } + fetchAuthMethod() + }, []) + const hasConfig = config.s2a.apiBase && config.s2a.adminKey // Load stats @@ -456,31 +475,33 @@ export default function Upload() { />
-
- -
- - + {/* 授权方式状态显示 */} +
+
+ {authMethod === 'api' ? ( + <> +
+ +
+
+
CodexAuth API 模式
+
纯 API 调用,无需浏览器,速度更快
+
+ + ) : ( + <> +
+ +
+
+
浏览器模拟模式 ({browserType})
+
使用浏览器自动化进行授权
+
+ + )}