feat: Introduce a new monitoring and configuration dashboard with backend API for autopool management and S2A integration.

This commit is contained in:
2026-02-02 07:55:22 +08:00
parent 5a3b3aa8ef
commit 3d026b2010
8 changed files with 722 additions and 68 deletions

1
.gitignore vendored
View File

@@ -101,3 +101,4 @@ check_ban.py
backend/codex-pool.exe backend/codex-pool.exe
backend/codex-pool.exe backend/codex-pool.exe
.claude/settings.local.json .claude/settings.local.json
CodexAuth

View File

@@ -232,6 +232,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
"team_reg_proxy_test_status": getTeamRegProxyTestStatus(), "team_reg_proxy_test_status": getTeamRegProxyTestStatus(),
"team_reg_proxy_test_ip": getTeamRegProxyTestIP(), "team_reg_proxy_test_ip": getTeamRegProxyTestIP(),
"site_name": config.Global.SiteName, "site_name": config.Global.SiteName,
"auth_method": config.Global.AuthMethod,
"mail_services_count": len(config.Global.MailServices), "mail_services_count": len(config.Global.MailServices),
"mail_services": 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"` DefaultProxy *string `json:"default_proxy"`
TeamRegProxy *string `json:"team_reg_proxy"` TeamRegProxy *string `json:"team_reg_proxy"`
SiteName *string `json:"site_name"` SiteName *string `json:"site_name"`
AuthMethod *string `json:"auth_method"`
} }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
api.Error(w, http.StatusBadRequest, "请求格式错误") api.Error(w, http.StatusBadRequest, "请求格式错误")
@@ -287,6 +289,9 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
if req.SiteName != nil { if req.SiteName != nil {
config.Global.SiteName = *req.SiteName config.Global.SiteName = *req.SiteName
} }
if req.AuthMethod != nil {
config.Global.AuthMethod = *req.AuthMethod
}
// 保存到数据库 (实时生效) // 保存到数据库 (实时生效)
if err := config.Update(config.Global); err != nil { if err := config.Update(config.Global); err != nil {

View File

@@ -32,14 +32,14 @@ type TeamProcessRequest struct {
// Owner 账号列表 // Owner 账号列表
Owners []TeamOwner `json:"owners"` Owners []TeamOwner `json:"owners"`
// 配置 // 配置
MembersPerTeam int `json:"members_per_team"` // 每个 Team 的成员数 MembersPerTeam int `json:"members_per_team"` // 每个 Team 的成员数
ConcurrentTeams int `json:"concurrent_teams"` // 并发 Team 数量 ConcurrentTeams int `json:"concurrent_teams"` // 并发 Team 数量
ConcurrentS2A int `json:"concurrent_s2a"` // 入库并发数默认2 ConcurrentS2A int `json:"concurrent_s2a"` // 入库并发数默认2
BrowserType string `json:"browser_type"` // "chromedp" 或 "rod" BrowserType string `json:"browser_type"` // "chromedp" 或 "rod"
Headless bool `json:"headless"` // 是否无头模式 Headless bool `json:"headless"` // 是否无头模式
Proxy string `json:"proxy"` // 代理设置 Proxy string `json:"proxy"` // 代理设置
IncludeOwner bool `json:"include_owner"` // 母号也入库到 S2A IncludeOwner bool `json:"include_owner"` // 母号也入库到 S2A
ProcessCount int `json:"process_count"` // 处理数量0表示全部 ProcessCount int `json:"process_count"` // 处理数量0表示全部
} }
// TeamProcessResult 团队处理结果 // TeamProcessResult 团队处理结果
@@ -755,7 +755,11 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
// 根据配置选择浏览器自动化 // 根据配置选择浏览器自动化
var code string 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) code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, memberChild.Email, memberChild.Password, teamID, req.Headless, req.Proxy, authLogger)
} else { } else {
code, err = auth.CompleteWithChromedpLogged(s2aResp.Data.AuthURL, memberChild.Email, memberChild.Password, teamID, req.Headless, req.Proxy, authLogger) 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 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) code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy, authLogger)
} else { } else {
code, err = auth.CompleteWithChromedpLogged(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy, authLogger) code, err = auth.CompleteWithChromedpLogged(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy, authLogger)

View File

@@ -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,
"canSharefunction 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
}

View File

@@ -39,6 +39,9 @@ type Config struct {
DefaultProxy string `json:"default_proxy"` DefaultProxy string `json:"default_proxy"`
TeamRegProxy string `json:"team_reg_proxy"` // Team 注册使用的代理 TeamRegProxy string `json:"team_reg_proxy"` // Team 注册使用的代理
// 授权方式配置
AuthMethod string `json:"auth_method"` // "api" 或 "browser"
// 自动化配置 // 自动化配置
AutoPauseOnExpired bool `json:"auto_pause_on_expired"` AutoPauseOnExpired bool `json:"auto_pause_on_expired"`
AccountsPath string `json:"accounts_path"` AccountsPath string `json:"accounts_path"`
@@ -181,6 +184,11 @@ func InitFromDB() *Config {
if v, _ := configDB.GetConfig("site_name"); v != "" { if v, _ := configDB.GetConfig("site_name"); v != "" {
cfg.SiteName = v cfg.SiteName = v
} }
if v, _ := configDB.GetConfig("auth_method"); v != "" {
cfg.AuthMethod = v
} else {
cfg.AuthMethod = "browser" // 默认使用浏览器模式
}
Global = cfg Global = cfg
return cfg return cfg
@@ -222,6 +230,10 @@ func SaveToDB() error {
configDB.SetConfig("site_name", cfg.SiteName) configDB.SetConfig("site_name", cfg.SiteName)
} }
if cfg.AuthMethod != "" {
configDB.SetConfig("auth_method", cfg.AuthMethod)
}
return nil return nil
} }

View File

@@ -13,7 +13,9 @@ import {
Globe, Globe,
Wifi, Wifi,
WifiOff, WifiOff,
HelpCircle HelpCircle,
Zap,
Monitor
} from 'lucide-react' } from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common' import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common'
import { useConfig } from '../hooks/useConfig' import { useConfig } from '../hooks/useConfig'
@@ -33,6 +35,8 @@ export default function Config() {
const [teamRegProxyStatus, setTeamRegProxyStatus] = useState<'unknown' | 'success' | 'error'>('unknown') const [teamRegProxyStatus, setTeamRegProxyStatus] = useState<'unknown' | 'success' | 'error'>('unknown')
const [proxyOriginIP, setProxyOriginIP] = useState('') const [proxyOriginIP, setProxyOriginIP] = useState('')
const [teamRegProxyIP, setTeamRegProxyIP] = useState('') const [teamRegProxyIP, setTeamRegProxyIP] = useState('')
const [authMethod, setAuthMethod] = useState<'api' | 'browser'>('browser')
const [savingAuthMethod, setSavingAuthMethod] = useState(false)
const { toasts, toast, removeToast } = useToast() const { toasts, toast, removeToast } = useToast()
// 加载站点名称和代理配置 // 加载站点名称和代理配置
@@ -61,6 +65,10 @@ export default function Config() {
if (data.data.team_reg_proxy_test_ip) { if (data.data.team_reg_proxy_test_ip) {
setTeamRegProxyIP(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) { } catch (error) {
console.error('Failed to fetch config:', 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 = [ const configItems = [
{ {
to: '/config/s2a', to: '/config/s2a',
@@ -311,7 +346,91 @@ export default function Config() {
</p> </p>
</div> </div>
{/* 代理地址配置 */} {/* 授权方式配置 */}
<div className="pt-4 border-t border-slate-200 dark:border-slate-700">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">
</label>
<div className="grid grid-cols-2 gap-3">
{/* API 模式 - CodexAuth */}
<button
onClick={() => handleSaveAuthMethod('api')}
disabled={savingAuthMethod}
className={`relative flex flex-col items-center gap-2 p-4 rounded-xl border-2 transition-all duration-200 ${authMethod === 'api'
? 'border-blue-500 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/30 dark:to-indigo-900/30 shadow-sm'
: 'border-slate-200 dark:border-slate-700 hover:border-blue-300 dark:hover:border-blue-800 hover:bg-slate-50 dark:hover:bg-slate-800/50'
}`}
>
{authMethod === 'api' && (
<div className="absolute top-2 right-2">
<CheckCircle className="h-4 w-4 text-blue-500" />
</div>
)}
<div className={`p-3 rounded-lg ${authMethod === 'api'
? 'bg-blue-500 text-white'
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400'
}`}>
<Zap className="h-6 w-6" />
</div>
<div className="text-center">
<div className={`font-semibold ${authMethod === 'api'
? 'text-blue-700 dark:text-blue-300'
: 'text-slate-700 dark:text-slate-300'
}`}>
CodexAuth API
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-1">
API
</div>
</div>
</button>
{/* 浏览器模式 */}
<button
onClick={() => handleSaveAuthMethod('browser')}
disabled={savingAuthMethod}
className={`relative flex flex-col items-center gap-2 p-4 rounded-xl border-2 transition-all duration-200 ${authMethod === 'browser'
? 'border-emerald-500 bg-gradient-to-br from-emerald-50 to-green-50 dark:from-emerald-900/30 dark:to-green-900/30 shadow-sm'
: 'border-slate-200 dark:border-slate-700 hover:border-emerald-300 dark:hover:border-emerald-800 hover:bg-slate-50 dark:hover:bg-slate-800/50'
}`}
>
{authMethod === 'browser' && (
<div className="absolute top-2 right-2">
<CheckCircle className="h-4 w-4 text-emerald-500" />
</div>
)}
<div className={`p-3 rounded-lg ${authMethod === 'browser'
? 'bg-emerald-500 text-white'
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400'
}`}>
<Monitor className="h-6 w-6" />
</div>
<div className="text-center">
<div className={`font-semibold ${authMethod === 'browser'
? 'text-emerald-700 dark:text-emerald-300'
: 'text-slate-700 dark:text-slate-300'
}`}>
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-1">
Chromedp/Rod
</div>
</div>
</button>
</div>
{authMethod === 'api' && (
<div className="mt-3 p-3 rounded-lg bg-gradient-to-r from-blue-500 to-indigo-500 text-white shadow-sm">
<div className="flex items-center gap-2">
<Zap className="h-4 w-4" />
<span className="font-medium">CodexAuth API </span>
</div>
<p className="text-xs text-blue-100 mt-1">
使 API
</p>
</div>
)}
</div>
<div className="pt-4 border-t border-slate-200 dark:border-slate-700"> <div className="pt-4 border-t border-slate-200 dark:border-slate-700">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300"> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300">

View File

@@ -13,6 +13,7 @@ import {
CheckCircle, CheckCircle,
Clock, Clock,
Save, Save,
Monitor as MonitorIcon,
} from 'lucide-react' } from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button, Input, Switch } from '../components/common' import { Card, CardHeader, CardTitle, CardContent, Button, Input, Switch } from '../components/common'
import LiveLogViewer from '../components/LiveLogViewer' import LiveLogViewer from '../components/LiveLogViewer'
@@ -70,6 +71,7 @@ export default function Monitor() {
const [replenishUseProxy, setReplenishUseProxy] = useState(false) // 补号时使用代理 const [replenishUseProxy, setReplenishUseProxy] = useState(false) // 补号时使用代理
const [globalProxy, setGlobalProxy] = useState('') // 全局代理地址(只读显示) const [globalProxy, setGlobalProxy] = useState('') // 全局代理地址(只读显示)
const [browserType, setBrowserType] = useState<'chromedp' | 'rod'>('chromedp') // 授权浏览器引擎 const [browserType, setBrowserType] = useState<'chromedp' | 'rod'>('chromedp') // 授权浏览器引擎
const [authMethod, setAuthMethod] = useState<'api' | 'browser'>('browser') // 授权方式
// 倒计时状态 // 倒计时状态
const [countdown, setCountdown] = useState(60) const [countdown, setCountdown] = useState(60)
@@ -264,6 +266,10 @@ export default function Monitor() {
const configJson = await configRes.json() const configJson = await configRes.json()
if (configJson.code === 0 && configJson.data) { if (configJson.code === 0 && configJson.data) {
setGlobalProxy(configJson.data.default_proxy || '') setGlobalProxy(configJson.data.default_proxy || '')
// 加载授权方式配置
if (configJson.data.auth_method) {
setAuthMethod(configJson.data.auth_method === 'api' ? 'api' : 'browser')
}
} }
} }
@@ -543,37 +549,36 @@ export default function Monitor() {
} }
/> />
</div> </div>
{/* 浏览器选择器 */} {/* 授权方式状态显示 */}
<div className="space-y-2"> <div className={`p-4 rounded-lg border ${authMethod === 'api'
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300"> ? 'bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border-blue-200 dark:border-blue-800'
: 'bg-gradient-to-r from-emerald-50 to-green-50 dark:from-emerald-900/20 dark:to-green-900/20 border-emerald-200 dark:border-emerald-800'
</label> }`}>
<div className="flex rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700"> <div className="flex items-center gap-3">
<button {authMethod === 'api' ? (
type="button" <>
onClick={() => setBrowserType('chromedp')} <div className="p-2 rounded-lg bg-blue-500 text-white">
disabled={!autoAdd} <Zap className="h-5 w-5" />
className={`flex-1 px-4 py-2.5 text-sm font-medium transition-all duration-200 ${browserType === 'chromedp' </div>
? 'bg-blue-600 text-white' <div>
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700' <div className="font-semibold text-blue-700 dark:text-blue-300">CodexAuth API </div>
} ${!autoAdd ? 'opacity-50 cursor-not-allowed' : ''}`} <div className="text-xs text-blue-600/70 dark:text-blue-400/70"> API </div>
> </div>
Chromedp </>
</button> ) : (
<button <>
type="button" <div className="p-2 rounded-lg bg-emerald-500 text-white">
onClick={() => setBrowserType('rod')} <MonitorIcon className="h-5 w-5" />
disabled={!autoAdd} </div>
className={`flex-1 px-4 py-2.5 text-sm font-medium transition-all duration-200 border-l border-slate-200 dark:border-slate-700 ${browserType === 'rod' <div>
? 'bg-blue-600 text-white' <div className="font-semibold text-emerald-700 dark:text-emerald-300"> ({browserType})</div>
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700' <div className="text-xs text-emerald-600/70 dark:text-emerald-400/70">使</div>
} ${!autoAdd ? 'opacity-50 cursor-not-allowed' : ''}`} </div>
> </>
Rod )}
</button>
</div> </div>
<p className="text-xs text-slate-500 dark:text-slate-400"> <p className="mt-2 text-xs text-slate-500 dark:text-slate-400">
Chromedp Rod
</p> </p>
</div> </div>
<Input <Input

View File

@@ -10,6 +10,8 @@ import {
Activity, Activity,
CheckCircle, CheckCircle,
RefreshCw, RefreshCw,
Zap,
Monitor,
} from 'lucide-react' } from 'lucide-react'
import { FileDropzone } from '../components/upload' import { FileDropzone } from '../components/upload'
import LogStream from '../components/upload/LogStream' import LogStream from '../components/upload/LogStream'
@@ -70,14 +72,31 @@ export default function Upload() {
const [membersPerTeam, setMembersPerTeam] = useState(4) const [membersPerTeam, setMembersPerTeam] = useState(4)
const [concurrentTeams, setConcurrentTeams] = useState(2) const [concurrentTeams, setConcurrentTeams] = useState(2)
const [concurrentS2A, setConcurrentS2A] = useState(2) // 入库并发数 const [concurrentS2A, setConcurrentS2A] = useState(2) // 入库并发数
const [browserType, setBrowserType] = useState<'chromedp' | 'rod'>('chromedp') const [browserType] = useState<'chromedp' | 'rod'>('chromedp') // 浏览器引擎类型(只读)
const [useProxy, setUseProxy] = useState(false) // 是否使用全局代理 const [useProxy, setUseProxy] = useState(false) // 是否使用全局代理
const [includeOwner, setIncludeOwner] = useState(false) // 母号也入库 const [includeOwner, setIncludeOwner] = useState(false) // 母号也入库
const [processCount, setProcessCount] = useState(0) // 处理数量0表示全部 const [processCount, setProcessCount] = useState(0) // 处理数量0表示全部
const [authMethod, setAuthMethod] = useState<'api' | 'browser'>('browser') // 授权方式
// 获取全局代理地址 // 获取全局代理地址
const globalProxy = config.proxy?.default || '' 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 const hasConfig = config.s2a.apiBase && config.s2a.adminKey
// Load stats // Load stats
@@ -456,31 +475,33 @@ export default function Upload() {
/> />
</div> </div>
<div> {/* 授权方式状态显示 */}
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"> <div className={`p-3 rounded-lg border ${authMethod === 'api'
? 'bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border-blue-200 dark:border-blue-800'
</label> : 'bg-gradient-to-r from-emerald-50 to-green-50 dark:from-emerald-900/20 dark:to-green-900/20 border-emerald-200 dark:border-emerald-800'
<div className="flex gap-2"> }`}>
<button <div className="flex items-center gap-2">
onClick={() => setBrowserType('chromedp')} {authMethod === 'api' ? (
disabled={isRunning} <>
className={`flex-1 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${browserType === 'chromedp' <div className="p-1.5 rounded bg-blue-500 text-white">
? 'bg-blue-500 text-white' <Zap className="h-4 w-4" />
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200' </div>
}`} <div>
> <div className="font-medium text-blue-700 dark:text-blue-300">CodexAuth API </div>
Chromedp () <div className="text-xs text-blue-600/70 dark:text-blue-400/70"> API </div>
</button> </div>
<button </>
onClick={() => setBrowserType('rod')} ) : (
disabled={isRunning} <>
className={`flex-1 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${browserType === 'rod' <div className="p-1.5 rounded bg-emerald-500 text-white">
? 'bg-blue-500 text-white' <Monitor className="h-4 w-4" />
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200' </div>
}`} <div>
> <div className="font-medium text-emerald-700 dark:text-emerald-300"> ({browserType})</div>
Rod <div className="text-xs text-emerald-600/70 dark:text-emerald-400/70">使</div>
</button> </div>
</>
)}
</div> </div>
</div> </div>