From 3d026b201006097e0c65b0699fe58277289e1fb7 Mon Sep 17 00:00:00 2001
From: kyx236
Date: Mon, 2 Feb 2026 07:55:22 +0800
Subject: [PATCH] feat: Introduce a new monitoring and configuration dashboard
with backend API for autopool management and S2A integration.
---
.gitignore | 1 +
backend/cmd/main.go | 5 +
backend/internal/api/team_process.go | 28 +-
backend/internal/auth/codex_api.go | 483 +++++++++++++++++++++++++++
backend/internal/config/config.go | 12 +
frontend/src/pages/Config.tsx | 123 ++++++-
frontend/src/pages/Monitor.tsx | 65 ++--
frontend/src/pages/Upload.tsx | 73 ++--
8 files changed, 722 insertions(+), 68 deletions(-)
create mode 100644 backend/internal/auth/codex_api.go
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})
+
使用浏览器自动化进行授权
+
+ >
+ )}