feat: Introduce a new monitoring and configuration dashboard with backend API for autopool management and S2A integration.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
483
backend/internal/auth/codex_api.go
Normal file
483
backend/internal/auth/codex_api.go
Normal 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,
|
||||||
|
"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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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'
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700'
|
|
||||||
} ${!autoAdd ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
||||||
>
|
|
||||||
Chromedp
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setBrowserType('rod')}
|
|
||||||
disabled={!autoAdd}
|
|
||||||
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'
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700'
|
|
||||||
} ${!autoAdd ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
||||||
>
|
|
||||||
Rod
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
<div>
|
||||||
选择用于自动授权的浏览器引擎,Chromedp 为默认高效选项,Rod 为备选
|
<div className="font-semibold text-blue-700 dark:text-blue-300">CodexAuth API 模式</div>
|
||||||
|
<div className="text-xs text-blue-600/70 dark:text-blue-400/70">纯 API 调用,无需浏览器,速度更快、更稳定</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="p-2 rounded-lg bg-emerald-500 text-white">
|
||||||
|
<MonitorIcon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-emerald-700 dark:text-emerald-300">浏览器模拟模式 ({browserType})</div>
|
||||||
|
<div className="text-xs text-emerald-600/70 dark:text-emerald-400/70">使用浏览器自动化进行授权,兼容性更好</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
要切换授权方式,请在“基础配置”页面中修改
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -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 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'
|
||||||
|
: '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 items-center gap-2">
|
||||||
|
{authMethod === 'api' ? (
|
||||||
|
<>
|
||||||
|
<div className="p-1.5 rounded bg-blue-500 text-white">
|
||||||
|
<Zap className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
<div className="font-medium text-blue-700 dark:text-blue-300">CodexAuth API 模式</div>
|
||||||
浏览器引擎
|
<div className="text-xs text-blue-600/70 dark:text-blue-400/70">纯 API 调用,无需浏览器,速度更快</div>
|
||||||
</label>
|
</div>
|
||||||
<div className="flex gap-2">
|
</>
|
||||||
<button
|
) : (
|
||||||
onClick={() => setBrowserType('chromedp')}
|
<>
|
||||||
disabled={isRunning}
|
<div className="p-1.5 rounded bg-emerald-500 text-white">
|
||||||
className={`flex-1 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${browserType === 'chromedp'
|
<Monitor className="h-4 w-4" />
|
||||||
? 'bg-blue-500 text-white'
|
</div>
|
||||||
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200'
|
<div>
|
||||||
}`}
|
<div className="font-medium text-emerald-700 dark:text-emerald-300">浏览器模拟模式 ({browserType})</div>
|
||||||
>
|
<div className="text-xs text-emerald-600/70 dark:text-emerald-400/70">使用浏览器自动化进行授权</div>
|
||||||
Chromedp (推荐)
|
</div>
|
||||||
</button>
|
</>
|
||||||
<button
|
)}
|
||||||
onClick={() => setBrowserType('rod')}
|
|
||||||
disabled={isRunning}
|
|
||||||
className={`flex-1 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${browserType === 'rod'
|
|
||||||
? 'bg-blue-500 text-white'
|
|
||||||
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Rod
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user