feat: Introduce core application structure, configuration, monitoring, and team management features.

This commit is contained in:
2026-02-03 06:45:54 +08:00
parent 637753ddaa
commit b20399a00a
18 changed files with 961 additions and 631 deletions

View File

@@ -128,8 +128,16 @@ func checkAndAutoAdd() {
return return
} }
// 计算需要多少个 Team每个 Team 产生 4 个账号) // 读取每 Team 成员数配置
teamsNeeded := (deficit + 3) / 4 // 向上取整 membersPerTeam := 4
if val, _ := database.Instance.GetConfig("monitor_members_per_team"); val != "" {
if v, err := strconv.Atoi(val); err == nil && v >= 1 && v <= 10 {
membersPerTeam = v
}
}
// 计算需要多少个 Team
teamsNeeded := (deficit + membersPerTeam - 1) / membersPerTeam // 向上取整
// 获取可用的 Owner // 获取可用的 Owner
owners, err := database.Instance.GetPendingOwners() owners, err := database.Instance.GetPendingOwners()
@@ -164,13 +172,14 @@ func checkAndAutoAdd() {
} }
} }
// 读取代理配置 // 读取代理配置 - 支持代理池模式
proxyURL := "" proxyURL := ""
replenishUseProxy := false replenishUseProxy := false
if val, _ := database.Instance.GetConfig("monitor_replenish_use_proxy"); val == "true" { if val, _ := database.Instance.GetConfig("monitor_replenish_use_proxy"); val == "true" {
replenishUseProxy = true replenishUseProxy = true
} }
if replenishUseProxy { if replenishUseProxy {
// 使用全局代理配置(支持 pool:random, pool:id:N 等格式)
proxyURL = config.Global.DefaultProxy proxyURL = config.Global.DefaultProxy
} }
@@ -180,10 +189,27 @@ func checkAndAutoAdd() {
browserType = val browserType = val
} }
// 读取并发 Team 数配置
concurrentTeams := 2
if val, _ := database.Instance.GetConfig("monitor_concurrent_teams"); val != "" {
if v, err := strconv.Atoi(val); err == nil && v >= 1 && v <= 10 {
concurrentTeams = v
}
}
// 读取入库并发数配置
s2aConcurrency := 2
if val, _ := database.Instance.GetConfig("monitor_s2a_concurrency"); val != "" {
if v, err := strconv.Atoi(val); err == nil && v >= 1 && v <= 4 {
s2aConcurrency = v
}
}
req := TeamProcessRequest{ req := TeamProcessRequest{
Owners: reqOwners, Owners: reqOwners,
MembersPerTeam: 4, MembersPerTeam: membersPerTeam,
ConcurrentTeams: 2, ConcurrentTeams: concurrentTeams,
S2AConcurrency: s2aConcurrency,
IncludeOwner: false, IncludeOwner: false,
Headless: true, Headless: true,
BrowserType: browserType, BrowserType: browserType,
@@ -192,7 +218,13 @@ func checkAndAutoAdd() {
// 输出代理使用状态日志 // 输出代理使用状态日志
if proxyURL != "" { if proxyURL != "" {
logger.Info(fmt.Sprintf("自动补号: 使用代理 %s", proxyURL), "", "auto-add") displayProxy := proxyURL
if proxyURL == "pool:random" {
displayProxy = "代理池轮询模式"
} else if len(proxyURL) > 8 && proxyURL[:8] == "pool:id:" {
displayProxy = fmt.Sprintf("代理池固定项 (ID: %s)", proxyURL[8:])
}
logger.Info(fmt.Sprintf("自动补号: 使用代理 %s", displayProxy), "", "auto-add")
} else { } else {
logger.Info("自动补号: 未使用代理", "", "auto-add") logger.Info("自动补号: 未使用代理", "", "auto-add")
} }
@@ -209,7 +241,8 @@ func checkAndAutoAdd() {
// 异步执行 // 异步执行
go runTeamProcess(req) go runTeamProcess(req)
logger.Success(fmt.Sprintf("自动补号任务已启动: %d 个 Team (浏览器: %s)", actualTeams, browserType), "", "auto-add") logger.Success(fmt.Sprintf("自动补号任务已启动: %d 个 Team, 每 Team %d 成员, 并发 %d (浏览器: %s)",
actualTeams, membersPerTeam, concurrentTeams, browserType), "", "auto-add")
} }
// getS2AAccountCount 获取 S2A 当前账号数量 // getS2AAccountCount 获取 S2A 当前账号数量

View File

@@ -13,12 +13,18 @@ import (
type MonitorSettings struct { type MonitorSettings struct {
Target int `json:"target"` Target int `json:"target"`
AutoAdd bool `json:"auto_add"` AutoAdd bool `json:"auto_add"`
AutoRegister bool `json:"auto_register"` // 母号不足时自动注册
AutoRegConcurrency int `json:"auto_reg_concurrency"` // 自动注册并发数
AutoRegUseProxy bool `json:"auto_reg_use_proxy"` // 自动注册时使用代理
MinInterval int `json:"min_interval"` MinInterval int `json:"min_interval"`
CheckInterval int `json:"check_interval"` // 自动补号检查间隔(秒) CheckInterval int `json:"check_interval"` // 自动补号检查间隔(秒)
PollingEnabled bool `json:"polling_enabled"` PollingEnabled bool `json:"polling_enabled"`
PollingInterval int `json:"polling_interval"` PollingInterval int `json:"polling_interval"`
ReplenishUseProxy bool `json:"replenish_use_proxy"` // 补号时使用代理 ReplenishUseProxy bool `json:"replenish_use_proxy"` // 补号时使用代理
BrowserType string `json:"browser_type"` // 授权浏览器引擎: chromedp 或 rod BrowserType string `json:"browser_type"` // 授权浏览器引擎: chromedp 或 rod
MembersPerTeam int `json:"members_per_team"` // 每 Team 成员数
ConcurrentTeams int `json:"concurrent_teams"` // 并发 Team 数
S2AConcurrency int `json:"s2a_concurrency"` // 入库并发数
} }
// HandleGetMonitorSettings 获取监控设置 // HandleGetMonitorSettings 获取监控设置
@@ -36,12 +42,18 @@ func HandleGetMonitorSettings(w http.ResponseWriter, r *http.Request) {
settings := MonitorSettings{ settings := MonitorSettings{
Target: 50, Target: 50,
AutoAdd: false, AutoAdd: false,
AutoRegister: false,
AutoRegConcurrency: 2,
AutoRegUseProxy: false,
MinInterval: 300, MinInterval: 300,
CheckInterval: 60, CheckInterval: 60,
PollingEnabled: false, PollingEnabled: false,
PollingInterval: 60, PollingInterval: 60,
ReplenishUseProxy: false, ReplenishUseProxy: false,
BrowserType: "chromedp", // 默认使用 chromedp BrowserType: "chromedp", // 默认使用 chromedp
MembersPerTeam: 4, // 默认每 Team 4 个成员
ConcurrentTeams: 2, // 默认并发 2 个 Team
S2AConcurrency: 2, // 默认入库并发 2
} }
if val, _ := database.Instance.GetConfig("monitor_target"); val != "" { if val, _ := database.Instance.GetConfig("monitor_target"); val != "" {
@@ -52,6 +64,17 @@ func HandleGetMonitorSettings(w http.ResponseWriter, r *http.Request) {
if val, _ := database.Instance.GetConfig("monitor_auto_add"); val == "true" { if val, _ := database.Instance.GetConfig("monitor_auto_add"); val == "true" {
settings.AutoAdd = true settings.AutoAdd = true
} }
if val, _ := database.Instance.GetConfig("monitor_auto_register"); val == "true" {
settings.AutoRegister = true
}
if val, _ := database.Instance.GetConfig("monitor_auto_reg_concurrency"); val != "" {
if v, err := strconv.Atoi(val); err == nil {
settings.AutoRegConcurrency = v
}
}
if val, _ := database.Instance.GetConfig("monitor_auto_reg_use_proxy"); val == "true" {
settings.AutoRegUseProxy = true
}
if val, _ := database.Instance.GetConfig("monitor_min_interval"); val != "" { if val, _ := database.Instance.GetConfig("monitor_min_interval"); val != "" {
if v, err := strconv.Atoi(val); err == nil { if v, err := strconv.Atoi(val); err == nil {
settings.MinInterval = v settings.MinInterval = v
@@ -76,6 +99,21 @@ func HandleGetMonitorSettings(w http.ResponseWriter, r *http.Request) {
if val, _ := database.Instance.GetConfig("monitor_browser_type"); val != "" { if val, _ := database.Instance.GetConfig("monitor_browser_type"); val != "" {
settings.BrowserType = val settings.BrowserType = val
} }
if val, _ := database.Instance.GetConfig("monitor_members_per_team"); val != "" {
if v, err := strconv.Atoi(val); err == nil {
settings.MembersPerTeam = v
}
}
if val, _ := database.Instance.GetConfig("monitor_concurrent_teams"); val != "" {
if v, err := strconv.Atoi(val); err == nil {
settings.ConcurrentTeams = v
}
}
if val, _ := database.Instance.GetConfig("monitor_s2a_concurrency"); val != "" {
if v, err := strconv.Atoi(val); err == nil {
settings.S2AConcurrency = v
}
}
Success(w, settings) Success(w, settings)
} }
@@ -113,6 +151,22 @@ func HandleSaveMonitorSettings(w http.ResponseWriter, r *http.Request) {
if err := database.Instance.SetConfig("monitor_auto_add", strconv.FormatBool(settings.AutoAdd)); err != nil { if err := database.Instance.SetConfig("monitor_auto_add", strconv.FormatBool(settings.AutoAdd)); err != nil {
saveErrors = append(saveErrors, "auto_add: "+err.Error()) saveErrors = append(saveErrors, "auto_add: "+err.Error())
} }
if err := database.Instance.SetConfig("monitor_auto_register", strconv.FormatBool(settings.AutoRegister)); err != nil {
saveErrors = append(saveErrors, "auto_register: "+err.Error())
}
// 自动注册并发数 (1-10)
autoRegConcurrency := settings.AutoRegConcurrency
if autoRegConcurrency < 1 {
autoRegConcurrency = 1
} else if autoRegConcurrency > 10 {
autoRegConcurrency = 10
}
if err := database.Instance.SetConfig("monitor_auto_reg_concurrency", strconv.Itoa(autoRegConcurrency)); err != nil {
saveErrors = append(saveErrors, "auto_reg_concurrency: "+err.Error())
}
if err := database.Instance.SetConfig("monitor_auto_reg_use_proxy", strconv.FormatBool(settings.AutoRegUseProxy)); err != nil {
saveErrors = append(saveErrors, "auto_reg_use_proxy: "+err.Error())
}
if err := database.Instance.SetConfig("monitor_min_interval", strconv.Itoa(settings.MinInterval)); err != nil { if err := database.Instance.SetConfig("monitor_min_interval", strconv.Itoa(settings.MinInterval)); err != nil {
saveErrors = append(saveErrors, "min_interval: "+err.Error()) saveErrors = append(saveErrors, "min_interval: "+err.Error())
} }
@@ -141,6 +195,36 @@ func HandleSaveMonitorSettings(w http.ResponseWriter, r *http.Request) {
if err := database.Instance.SetConfig("monitor_browser_type", browserType); err != nil { if err := database.Instance.SetConfig("monitor_browser_type", browserType); err != nil {
saveErrors = append(saveErrors, "browser_type: "+err.Error()) saveErrors = append(saveErrors, "browser_type: "+err.Error())
} }
// 每 Team 成员数 (1-10)
membersPerTeam := settings.MembersPerTeam
if membersPerTeam < 1 {
membersPerTeam = 1
} else if membersPerTeam > 10 {
membersPerTeam = 10
}
if err := database.Instance.SetConfig("monitor_members_per_team", strconv.Itoa(membersPerTeam)); err != nil {
saveErrors = append(saveErrors, "members_per_team: "+err.Error())
}
// 并发 Team 数 (1-10)
concurrentTeams := settings.ConcurrentTeams
if concurrentTeams < 1 {
concurrentTeams = 1
} else if concurrentTeams > 10 {
concurrentTeams = 10
}
if err := database.Instance.SetConfig("monitor_concurrent_teams", strconv.Itoa(concurrentTeams)); err != nil {
saveErrors = append(saveErrors, "concurrent_teams: "+err.Error())
}
// 入库并发数 (1-4)
s2aConcurrency := settings.S2AConcurrency
if s2aConcurrency < 1 {
s2aConcurrency = 1
} else if s2aConcurrency > 4 {
s2aConcurrency = 4
}
if err := database.Instance.SetConfig("monitor_s2a_concurrency", strconv.Itoa(s2aConcurrency)); err != nil {
saveErrors = append(saveErrors, "s2a_concurrency: "+err.Error())
}
if len(saveErrors) > 0 { if len(saveErrors) > 0 {
errMsg := "保存监控设置部分失败: " + saveErrors[0] errMsg := "保存监控设置部分失败: " + saveErrors[0]

View File

@@ -315,10 +315,10 @@ func runBanCheckTask(owners []database.TeamOwner, concurrency int) {
taskChan := make(chan database.TeamOwner, len(owners)) taskChan := make(chan database.TeamOwner, len(owners))
var wg sync.WaitGroup var wg sync.WaitGroup
// 获取代理配置 // 获取代理配置模式
proxy := "" proxyStr := ""
if config.Global != nil { if config.Global != nil {
proxy = config.Global.GetProxy() proxyStr = config.Global.GetProxy()
} }
// 启动 worker // 启动 worker
@@ -327,7 +327,9 @@ func runBanCheckTask(owners []database.TeamOwner, concurrency int) {
go func() { go func() {
defer wg.Done() defer wg.Done()
for owner := range taskChan { for owner := range taskChan {
result := checkSingleOwnerBan(owner, proxy) // 每次循环重新解析代理,支持轮询
resolvedProxy := database.Instance.ResolveProxy(proxyStr)
result := checkSingleOwnerBan(owner, resolvedProxy)
// 更新计数 // 更新计数
atomic.AddInt32(&banCheckTaskState.Checked, 1) atomic.AddInt32(&banCheckTaskState.Checked, 1)

View File

@@ -155,6 +155,8 @@ func HandleTeamProcess(w http.ResponseWriter, r *http.Request) {
req.Proxy = config.Global.GetProxy() // 使用新的代理获取方法 req.Proxy = config.Global.GetProxy() // 使用新的代理获取方法
} }
if req.Proxy != "" { if req.Proxy != "" {
// 如果是模式字符串pool:random, pool:id:N跳过 Normalize
if !strings.HasPrefix(req.Proxy, "pool:") && req.Proxy != "[RANDOM]" {
normalized, err := proxyutil.Normalize(req.Proxy) normalized, err := proxyutil.Normalize(req.Proxy)
if err != nil { if err != nil {
Error(w, http.StatusBadRequest, fmt.Sprintf("代理格式错误: %v", err)) Error(w, http.StatusBadRequest, fmt.Sprintf("代理格式错误: %v", err))
@@ -162,6 +164,7 @@ func HandleTeamProcess(w http.ResponseWriter, r *http.Request) {
} }
req.Proxy = normalized req.Proxy = normalized
} }
}
// 初始化状态 // 初始化状态
teamProcessState.Running = true teamProcessState.Running = true
@@ -466,7 +469,9 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
// Step 1: 获取 Team ID优先使用已存储的 account_id // Step 1: 获取 Team ID优先使用已存储的 account_id
var teamID string var teamID string
inviter := invite.NewWithProxy(owner.Token, req.Proxy) // Resolve proxy for inviter
resolvedProxy := database.Instance.ResolveProxy(req.Proxy)
inviter := invite.NewWithProxy(owner.Token, resolvedProxy)
if owner.AccountID != "" { if owner.AccountID != "" {
// 直接使用数据库中存储的 account_id // 直接使用数据库中存储的 account_id
@@ -581,7 +586,7 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
// 入库单个成员的函数 // 入库单个成员的函数
doS2A := func(memberIdx int, memberEmail, memberPassword string) bool { doS2A := func(memberIdx int, memberEmail, memberPassword string) bool {
memberLogPrefix := fmt.Sprintf("%s [成员 %d]", logPrefix, memberIdx+1) memberLogPrefix := fmt.Sprintf("%s [Member %d]", logPrefix, memberIdx+1)
memberStartTime := time.Now() memberStartTime := time.Now()
// 获取入库信号量 // 获取入库信号量
@@ -598,15 +603,21 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
logger.Warning(fmt.Sprintf("%s 入库重试 (第%d次)", memberLogPrefix, attempt+1), memberEmail, "team") logger.Warning(fmt.Sprintf("%s 入库重试 (第%d次)", memberLogPrefix, attempt+1), memberEmail, "team")
} }
// 创建日志回调 // 创建日志回调 - 只显示关键步骤
authLogger := auth.NewAuthLogger(memberEmail, logPrefix, memberIdx+1, func(entry auth.AuthLogEntry) { authLogger := auth.NewAuthLogger(memberEmail, logPrefix, memberIdx+1, func(entry auth.AuthLogEntry) {
if entry.IsError { if entry.IsError {
logger.Error(fmt.Sprintf("%s %s", memberLogPrefix, entry.Message), memberEmail, "team") logger.Error(fmt.Sprintf("%s %s", memberLogPrefix, entry.Message), memberEmail, "team")
} else { } else {
// 只显示关键步骤:提交邮箱、验证密码、选择工作区、授权成功
switch entry.Step { switch entry.Step {
case auth.StepNavigate, auth.StepInputEmail, auth.StepInputPassword, case auth.StepInputEmail:
auth.StepComplete, auth.StepConsent, auth.StepSelectWorkspace: logger.Info(fmt.Sprintf("%s 提交邮箱: %s", memberLogPrefix, memberEmail), memberEmail, "team")
logger.Info(fmt.Sprintf("%s %s", memberLogPrefix, entry.Message), memberEmail, "team") case auth.StepInputPassword:
logger.Info(fmt.Sprintf("%s 验证密码...", memberLogPrefix), memberEmail, "team")
case auth.StepSelectWorkspace:
logger.Info(fmt.Sprintf("%s 选择工作区: %s", memberLogPrefix, teamID), memberEmail, "team")
case auth.StepComplete:
logger.Info(fmt.Sprintf("%s 授权成功,获取到授权码", memberLogPrefix), memberEmail, "team")
} }
} }
}) })
@@ -622,10 +633,9 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
// 根据配置选择授权方式 // 根据配置选择授权方式
var code string var code string
if config.Global.AuthMethod == "api" { if config.Global.AuthMethod == "api" {
proxyToUse := req.Proxy proxyToUse := database.Instance.ResolveProxy(req.Proxy)
if poolProxy, poolErr := database.Instance.GetRandomCodexProxy(); poolErr == nil && poolProxy != "" { if proxyToUse != req.Proxy && proxyToUse != "" {
proxyToUse = poolProxy logger.Info(fmt.Sprintf("%s 使用解析代理: %s", memberLogPrefix, getProxyDisplay(proxyToUse)), memberEmail, "team")
logger.Info(fmt.Sprintf("%s 使用代理池: %s", memberLogPrefix, getProxyDisplay(poolProxy)), memberEmail, "team")
} }
code, err = auth.CompleteWithCodexAPI(memberEmail, memberPassword, teamID, s2aResp.Data.AuthURL, s2aResp.Data.SessionID, proxyToUse, authLogger) code, err = auth.CompleteWithCodexAPI(memberEmail, memberPassword, teamID, s2aResp.Data.AuthURL, s2aResp.Data.SessionID, proxyToUse, authLogger)
if proxyToUse != req.Proxy && proxyToUse != "" { if proxyToUse != req.Proxy && proxyToUse != "" {
@@ -681,7 +691,7 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
registerAndS2AMember := func(memberIdx int, email, password string) bool { registerAndS2AMember := func(memberIdx int, email, password string) bool {
name := register.GenerateName() name := register.GenerateName()
birthdate := register.GenerateBirthdate() birthdate := register.GenerateBirthdate()
memberLogPrefix := fmt.Sprintf("%s [成员 %d]", logPrefix, memberIdx+1) memberLogPrefix := fmt.Sprintf("%s [Member %d]", logPrefix, memberIdx+1)
regStartTime := time.Now() regStartTime := time.Now()
for attempt := 0; attempt < 2; attempt++ { // 最多尝试2次首次+1次重试 for attempt := 0; attempt < 2; attempt++ { // 最多尝试2次首次+1次重试
@@ -764,7 +774,7 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
} }
email := mail.GenerateEmail() email := mail.GenerateEmail()
password := register.GeneratePassword() password := register.GeneratePassword()
logger.Info(fmt.Sprintf("%s [成员 %d] 邮箱: %s | 密码: %s", logPrefix, idx+1, email, password), email, "team") logger.Info(fmt.Sprintf("%s [Member %d] Email: %s | Password: %s", logPrefix, idx+1, email, password), email, "team")
registerAndS2AMember(idx, email, password) registerAndS2AMember(idx, email, password)
}(i) }(i)
} }

View File

@@ -297,8 +297,14 @@ func runTeamRegProcess(config TeamRegConfig) {
} }
} }
// 解析代理
resolvedProxy := database.Instance.ResolveProxy(config.Proxy)
if config.Proxy != resolvedProxy && config.Proxy != "" && resolvedProxy != "" {
addTeamRegLog(fmt.Sprintf("[系统] 代理模式: %s -> %s", config.Proxy, resolvedProxy))
}
addTeamRegLog(fmt.Sprintf("[系统] 配置: 数量=%d, 并发=%d, 代理=%s", addTeamRegLog(fmt.Sprintf("[系统] 配置: 数量=%d, 并发=%d, 代理=%s",
config.Count, config.Concurrency, config.Proxy)) config.Count, config.Concurrency, resolvedProxy))
// 创建上下文用于取消 // 创建上下文用于取消
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
@@ -360,8 +366,8 @@ func runTeamRegProcess(config TeamRegConfig) {
fmt.Fprintf(stdin, "%d\n", config.Concurrency) fmt.Fprintf(stdin, "%d\n", config.Concurrency)
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)
addTeamRegLog(fmt.Sprintf("[输入] 代理地址: %s", config.Proxy)) addTeamRegLog(fmt.Sprintf("[输入] 代理地址: %s", resolvedProxy))
fmt.Fprintf(stdin, "%s\n", config.Proxy) fmt.Fprintf(stdin, "%s\n", resolvedProxy)
// 等待进程完成 // 等待进程完成
err = cmd.Wait() err = cmd.Wait()

View File

@@ -379,7 +379,8 @@ func fetchAccountID(token string) (string, error) {
proxy = cfg.GetProxy() proxy = cfg.GetProxy()
} }
tlsClient, err := client.New(proxy) resolvedProxy := database.Instance.ResolveProxy(proxy)
tlsClient, err := client.New(resolvedProxy)
if err != nil { if err != nil {
return "", fmt.Errorf("创建 TLS 客户端失败: %v", err) return "", fmt.Errorf("创建 TLS 客户端失败: %v", err)
} }

View File

@@ -477,11 +477,10 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
} }
} }
c.logStep(StepInputPassword, "邮箱提交响应 pageType=%s, 包含password=%v", pageType, strings.Contains(string(body), "password")) c.logStep(StepInputPassword, "验证密码...")
if pageType == "password" || strings.Contains(string(body), "password") { if pageType == "password" || strings.Contains(string(body), "password") {
// 5. 验证密码 // 5. 验证密码
c.logStep(StepInputPassword, "验证密码...")
if !c.callSentinelReq("authorize_continue__auto") { if !c.callSentinelReq("authorize_continue__auto") {
return "", fmt.Errorf("Sentinel 请求失败") return "", fmt.Errorf("Sentinel 请求失败")
} }
@@ -493,7 +492,6 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
verifyHeaders["OpenAI-Sentinel-Token"] = c.getSentinelHeader("password_verify") verifyHeaders["OpenAI-Sentinel-Token"] = c.getSentinelHeader("password_verify")
passwordPayload := map[string]string{ passwordPayload := map[string]string{
"username": c.email,
"password": c.password, "password": c.password,
} }
@@ -502,7 +500,6 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
c.logError(StepInputPassword, "密码验证失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))])) c.logError(StepInputPassword, "密码验证失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))]))
return "", fmt.Errorf("密码验证失败: %d", resp.StatusCode) return "", fmt.Errorf("密码验证失败: %d", resp.StatusCode)
} }
c.logStep(StepInputPassword, "密码验证成功")
// 解析密码验证响应 // 解析密码验证响应
json.Unmarshal(body, &data) json.Unmarshal(body, &data)
@@ -520,8 +517,6 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
c.logError(StepInputPassword, "账号需要邮箱验证,无法继续 Codex 授权流程") c.logError(StepInputPassword, "账号需要邮箱验证,无法继续 Codex 授权流程")
return "", fmt.Errorf("账号需要邮箱验证,请使用浏览器模式或等待账号状态更新") return "", fmt.Errorf("账号需要邮箱验证,请使用浏览器模式或等待账号状态更新")
} }
} else {
c.logStep(StepInputPassword, "跳过密码验证步骤 (服务器未要求)")
} }
// 6. 选择工作区 // 6. 选择工作区

View File

@@ -4,6 +4,8 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv"
"strings"
"time" "time"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
@@ -941,10 +943,44 @@ func (d *DB) DeleteCodexProxy(id int64) error {
return err return err
} }
// ClearCodexProxies 清空所有代理 // GetCodexProxyByID 获取指定 ID 的代理地址
func (d *DB) ClearCodexProxies() error { func (d *DB) GetCodexProxyByID(id int64) (string, error) {
_, err := d.db.Exec("DELETE FROM codex_auth_proxies") var proxyURL string
return err err := d.db.QueryRow("SELECT proxy_url FROM codex_auth_proxies WHERE id = ?", id).Scan(&proxyURL)
if err == sql.ErrNoRows {
return "", nil
}
return proxyURL, err
}
// ResolveProxy 解析代理字符串(支持 pool:random, pool:id:N, 或直接 URL
func (d *DB) ResolveProxy(proxyStr string) string {
if proxyStr == "" {
return ""
}
// 兼容旧的 [RANDOM] 格式
if proxyStr == "pool:random" || proxyStr == "[RANDOM]" {
p, _ := d.GetRandomCodexProxy()
return p
}
// 处理 pool:id:N 格式
if strings.HasPrefix(proxyStr, "pool:id:") {
idStr := strings.TrimPrefix(proxyStr, "pool:id:")
id, err := strconv.ParseInt(idStr, 10, 64)
if err == nil {
p, _ := d.GetCodexProxyByID(id)
return p
}
}
// 否则视为字面 URL如果没协议头加一个保持现有行为
if !strings.HasPrefix(proxyStr, "http://") && !strings.HasPrefix(proxyStr, "https://") && !strings.HasPrefix(proxyStr, "socks5://") {
return "http://" + proxyStr
}
return proxyStr
} }
// GetCodexProxyStats 获取代理统计 // GetCodexProxyStats 获取代理统计

View File

@@ -1,7 +1,7 @@
import { Routes, Route } from 'react-router-dom' import { Routes, Route } from 'react-router-dom'
import { ConfigProvider, RecordsProvider } from './context' import { ConfigProvider, RecordsProvider } from './context'
import { Layout } from './components/layout' import { Layout } from './components/layout'
import { Dashboard, Upload, Records, Accounts, Config, S2AConfig, EmailConfig, Monitor, Cleaner, TeamReg, CodexProxyConfig } from './pages' import { Dashboard, Upload, Records, Accounts, Config, S2AConfig, EmailConfig, Monitor, Cleaner, TeamReg, CodexProxyConfig, S2AStats } from './pages'
function App() { function App() {
return ( return (
@@ -20,6 +20,7 @@ function App() {
<Route path="config/s2a" element={<S2AConfig />} /> <Route path="config/s2a" element={<S2AConfig />} />
<Route path="config/email" element={<EmailConfig />} /> <Route path="config/email" element={<EmailConfig />} />
<Route path="config/codex-proxy" element={<CodexProxyConfig />} /> <Route path="config/codex-proxy" element={<CodexProxyConfig />} />
<Route path="s2a-stats" element={<S2AStats />} />
</Route> </Route>
</Routes> </Routes>
</RecordsProvider> </RecordsProvider>

View File

@@ -211,13 +211,13 @@ export default function LiveLogViewer({
> >
<span className="text-slate-500 flex-shrink-0 font-medium">{log.timestamp}</span> <span className="text-slate-500 flex-shrink-0 font-medium">{log.timestamp}</span>
<span className={`flex-shrink-0 uppercase font-bold w-20 text-center rounded-[4px] px-1 ${levelColors[log.level] || 'text-slate-400'}`}> <span className={`flex-shrink-0 uppercase font-bold w-20 text-center rounded-[4px] px-1 ${levelColors[log.level] || 'text-slate-400'}`}>
{log.level} {log.level === 'success' ? '✓' : log.level}
</span> </span>
<span className="text-slate-400 flex-shrink-0 opacity-80">[{log.module}]</span> <span className="text-slate-400 flex-shrink-0 opacity-80">[{log.module}]</span>
{log.email && ( {log.email && (
<span className="text-purple-400 flex-shrink-0 truncate max-w-[180px] font-medium">{log.email}</span> <span className="text-purple-400 flex-shrink-0 truncate max-w-[180px] font-medium">{log.email}</span>
)} )}
<span className="text-slate-200 flex-1 break-all">{log.message}</span> <span className={`flex-1 break-all ${log.level === 'success' ? 'text-green-400 font-bold' : 'text-slate-200'}`}>{log.message}</span>
</div> </div>
)) ))
)} )}

View File

@@ -1,5 +1,5 @@
import { type SelectHTMLAttributes, forwardRef } from 'react' import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react'
import { ChevronDown } from 'lucide-react' import { ChevronDown, Check } from 'lucide-react'
export interface SelectOption { export interface SelectOption {
value: string | number value: string | number
@@ -7,62 +7,125 @@ export interface SelectOption {
disabled?: boolean disabled?: boolean
} }
export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'children'> { export interface SelectProps {
label?: string label?: string
error?: string error?: string
hint?: string hint?: string
options: SelectOption[] options: SelectOption[]
placeholder?: string placeholder?: string
value?: string | number
onChange?: (e: { target: { value: string | number } }) => void
className?: string
disabled?: boolean
id?: string
} }
const Select = forwardRef<HTMLSelectElement, SelectProps>( const Select = forwardRef<any, SelectProps>(
({ className = '', label, error, hint, options, placeholder, id, ...props }, ref) => { ({ className = '', label, error, hint, options, placeholder, value, onChange, disabled, id }, ref) => {
const [isOpen, setIsOpen] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const selectId = id || label?.toLowerCase().replace(/\s+/g, '-') const selectId = id || label?.toLowerCase().replace(/\s+/g, '-')
// Expose some functionality if needed
useImperativeHandle(ref, () => ({
focus: () => containerRef.current?.focus(),
}))
// Handle click outside to close
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const selectedOption = options.find((opt) => opt.value === value)
const handleSelect = (option: SelectOption) => {
if (option.disabled || disabled) return
if (onChange) {
onChange({ target: { value: option.value } })
}
setIsOpen(false)
}
return ( return (
<div className="w-full"> <div className={`w-full ${className}`} ref={containerRef}>
{label && ( {label && (
<label <label
htmlFor={selectId} htmlFor={selectId}
className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5"
> >
{label} {label}
</label> </label>
)} )}
<div className="relative"> <div className="relative">
<select <button
ref={ref} type="button"
id={selectId} id={selectId}
className={`w-full px-3 py-2 text-sm rounded-lg border transition-colors appearance-none disabled={disabled}
bg-white dark:bg-slate-800 onClick={() => !disabled && setIsOpen(!isOpen)}
className={`
relative w-full flex items-center justify-between
px-3 py-2.5 text-sm rounded-xl border transition-all duration-200
bg-white dark:bg-slate-800/50
text-slate-900 dark:text-slate-100 text-slate-900 dark:text-slate-100
${ ${isOpen
error ? 'border-blue-500 ring-2 ring-blue-500/20 shadow-sm'
? 'border-red-500 focus:border-red-500 focus:ring-red-500' : error
: 'border-slate-300 dark:border-slate-600 focus:border-blue-500 focus:ring-blue-500' ? 'border-red-500'
: 'border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600'
} }
focus:outline-none focus:ring-2 focus:ring-offset-0 ${disabled ? 'opacity-50 cursor-not-allowed bg-slate-50 dark:bg-slate-900/50' : 'cursor-pointer'}
disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none
pr-10 `}
${className}`}
{...props}
> >
{placeholder && ( <span className={`block truncate ${!selectedOption ? 'text-slate-400' : ''}`}>
<option value="" disabled> {selectedOption ? selectedOption.label : placeholder || '请选择...'}
{placeholder} </span>
</option> <span className="flex items-center pointer-events-none transition-transform duration-200" style={{ transform: isOpen ? 'rotate(180deg)' : 'rotate(0)' }}>
)} <ChevronDown className={`h-4 w-4 ${isOpen ? 'text-blue-500' : 'text-slate-400'}`} />
{options.map((option) => ( </span>
<option key={option.value} value={option.value} disabled={option.disabled}> </button>
{option.label}
</option> {isOpen && (
))} <div className="absolute z-50 w-full mt-2 py-1 overflow-auto text-base bg-white dark:bg-slate-800 rounded-xl shadow-xl max-h-60 ring-1 ring-black/5 dark:ring-white/10 focus:outline-none sm:text-sm animate-in fade-in zoom-in-95 duration-100">
</select> {options.length === 0 ? (
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400 pointer-events-none" /> <div className="px-4 py-2 text-slate-500 dark:text-slate-400 italic">
</div> </div>
{error && <p className="mt-1 text-sm text-red-500">{error}</p>} ) : (
options.map((option) => (
<div
key={option.value}
onClick={() => handleSelect(option)}
className={`
relative cursor-pointer select-none py-2.5 pl-10 pr-4 transition-colors
${option.value === value
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-medium'
: 'text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700/50'
}
${option.disabled ? 'opacity-50 cursor-not-allowed grayscale' : ''}
`}
>
<span className="block truncate">{option.label}</span>
{option.value === value && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-blue-600 dark:text-blue-400">
<Check className="h-4 w-4" />
</span>
)}
</div>
))
)}
</div>
)}
</div>
{error && <p className="mt-1.5 text-xs font-medium text-red-500 ml-1">{error}</p>}
{hint && !error && ( {hint && !error && (
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">{hint}</p> <p className="mt-1.5 text-xs text-slate-500 dark:text-slate-400 ml-1">{hint}</p>
)} )}
</div> </div>
) )

View File

@@ -16,6 +16,7 @@ import {
Trash2, Trash2,
UserPlus, UserPlus,
Globe, Globe,
BarChart3,
} from 'lucide-react' } from 'lucide-react'
interface SidebarProps { interface SidebarProps {
@@ -36,6 +37,7 @@ const navItems: NavItem[] = [
{ to: '/records', icon: History, label: '加号记录' }, { to: '/records', icon: History, label: '加号记录' },
{ to: '/accounts', icon: Users, label: '号池账号' }, { to: '/accounts', icon: Users, label: '号池账号' },
{ to: '/monitor', icon: Activity, label: '号池监控' }, { to: '/monitor', icon: Activity, label: '号池监控' },
{ to: '/s2a-stats', icon: BarChart3, label: 'S2A 统计' },
{ to: '/cleaner', icon: Trash2, label: '定期清理' }, { to: '/cleaner', icon: Trash2, label: '定期清理' },
{ to: '/team-reg', icon: UserPlus, label: 'Team 注册' }, { to: '/team-reg', icon: UserPlus, label: 'Team 注册' },
{ {
@@ -46,7 +48,7 @@ const navItems: NavItem[] = [
{ to: '/config', icon: Cog, label: '配置概览' }, { to: '/config', icon: Cog, label: '配置概览' },
{ to: '/config/s2a', icon: Server, label: 'S2A 配置' }, { to: '/config/s2a', icon: Server, label: 'S2A 配置' },
{ to: '/config/email', icon: Mail, label: '邮箱配置' }, { to: '/config/email', icon: Mail, label: '邮箱配置' },
{ to: '/config/codex-proxy', icon: Globe, label: 'CodexAuth代理池' }, { to: '/config/codex-proxy', icon: Globe, label: '代理配置' },
] ]
}, },
] ]
@@ -133,7 +135,7 @@ export default function Sidebar({ isOpen, onClose }: SidebarProps) {
{/* Sidebar */} {/* Sidebar */}
<aside <aside
className={`fixed inset-y-0 left-0 z-50 w-64 bg-white/80 dark:bg-slate-900/90 backdrop-blur-xl border-r border-slate-200 dark:border-slate-800 transform transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:z-auto ${isOpen ? 'translate-x-0' : '-translate-x-full' className={`fixed inset-y-0 left-0 z-50 w-64 bg-white/80 dark:bg-slate-900/90 backdrop-blur-xl border-r border-slate-200 dark:border-slate-800 transform transition-transform duration-300 ease-in-out lg:translate-x-0 lg:sticky lg:top-0 lg:h-screen lg:z-auto ${isOpen ? 'translate-x-0' : '-translate-x-full'
}`} }`}
> >
{/* Mobile close button */} {/* Mobile close button */}

View File

@@ -1,10 +1,11 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { import {
Globe, Plus, Trash2, ToggleLeft, ToggleRight, Globe, Plus, Trash2, ToggleLeft, ToggleRight,
Loader2, Save, RefreshCcw, CheckCircle, XCircle, Loader2, Save, RefreshCw, CheckCircle, XCircle,
AlertTriangle, Clock, MapPin, Play, PlayCircle AlertTriangle, Clock, MapPin, Play, PlayCircle,
Settings, Zap
} from 'lucide-react' } from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button, Input, useToast } from '../components/common' import { Card, CardHeader, CardTitle, CardContent, Button, Input, Select, useToast } from '../components/common'
interface CodexProxy { interface CodexProxy {
id: number id: number
@@ -34,6 +35,13 @@ export default function CodexProxyConfig() {
const [testingAll, setTestingAll] = useState(false) const [testingAll, setTestingAll] = useState(false)
const toast = useToast() const toast = useToast()
// 全局与注册代理配置
const [globalProxyMode, setGlobalProxyMode] = useState<'manual' | 'pool:random' | 'pool:id'>('manual')
const [globalProxyValue, setGlobalProxyValue] = useState('')
const [regProxyMode, setRegProxyMode] = useState<'manual' | 'pool:random' | 'pool:id'>('manual')
const [regProxyValue, setRegProxyValue] = useState('')
const [savingConfig, setSavingConfig] = useState(false)
// 单个添加 // 单个添加
const [newProxyUrl, setNewProxyUrl] = useState('') const [newProxyUrl, setNewProxyUrl] = useState('')
const [newDescription, setNewDescription] = useState('') const [newDescription, setNewDescription] = useState('')
@@ -59,9 +67,63 @@ export default function CodexProxyConfig() {
} }
}, []) }, [])
const fetchConfig = useCallback(async () => {
try {
const res = await fetch('/api/config')
const data = await res.json()
if (data.code === 0 && data.data) {
const parse = (val: string) => {
if (!val) return { mode: 'manual' as const, value: '' }
if (val === 'pool:random' || val === '[RANDOM]') return { mode: 'pool:random' as const, value: '' }
if (val.startsWith('pool:id:')) return { mode: 'pool:id' as const, value: val.replace('pool:id:', '') }
return { mode: 'manual' as const, value: val }
}
const g = parse(data.data.default_proxy)
setGlobalProxyMode(g.mode)
setGlobalProxyValue(g.value)
const r = parse(data.data.team_reg_proxy)
setRegProxyMode(r.mode)
setRegProxyValue(r.value)
}
} catch (e) {
console.error('获取配置失败:', e)
}
}, [])
useEffect(() => { useEffect(() => {
fetchProxies() fetchProxies()
}, [fetchProxies]) fetchConfig()
}, [fetchProxies, fetchConfig])
// 保存代理用途配置
const handleSaveUsageConfig = async () => {
setSavingConfig(true)
try {
const stringify = (mode: string, val: string) => {
if (mode === 'pool:random') return 'pool:random'
if (mode === 'pool:id') return `pool:id:${val}`
return val
}
const res = await fetch('/api/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
default_proxy: stringify(globalProxyMode, globalProxyValue),
team_reg_proxy: stringify(regProxyMode, regProxyValue),
}),
})
const data = await res.json()
if (data.code === 0) {
toast.success('用途配置已保存')
} else {
toast.error(data.message || '保存失败')
}
} catch {
toast.error('网络错误')
} finally {
setSavingConfig(false)
}
}
// 添加代理 // 添加代理
const handleAddProxy = async () => { const handleAddProxy = async () => {
@@ -354,6 +416,7 @@ export default function CodexProxyConfig() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
@@ -369,7 +432,7 @@ export default function CodexProxyConfig() {
<Button <Button
variant="outline" variant="outline"
onClick={fetchProxies} onClick={fetchProxies}
icon={<RefreshCcw className="h-4 w-4" />} icon={<RefreshCw className="h-4 w-4" />}
> >
</Button> </Button>
@@ -609,6 +672,160 @@ export default function CodexProxyConfig() {
</CardContent> </CardContent>
</Card> </Card>
{/* Usage Configuration */}
<Card className="border-blue-100 dark:border-blue-900 shadow-sm">
<CardHeader className="bg-slate-50/80 dark:bg-slate-800/80 border-b border-slate-100 dark:border-slate-700 rounded-t-xl">
<CardTitle className="flex items-center gap-2 text-slate-800 dark:text-slate-100 font-bold">
<Settings className="h-5 w-5 text-blue-500" />
</CardTitle>
<Button
size="sm"
onClick={handleSaveUsageConfig}
disabled={savingConfig}
className="rounded-full px-5"
icon={savingConfig ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
>
</Button>
</CardHeader>
<CardContent className="p-0">
<div className="flex flex-col divide-y divide-slate-100 dark:divide-slate-700">
{/* 全局默认代理 */}
<div className="p-8 space-y-6">
<div className="space-y-1.5 border-l-4 border-blue-500 pl-4">
<h3 className="text-base font-bold text-slate-900 dark:text-slate-100 flex items-center gap-2">
<Globe className="h-5 w-5 text-blue-500" />
</h3>
<p className="text-xs text-slate-500 dark:text-slate-400">
</p>
</div>
<div className="space-y-4">
<Select
label="使用模式"
value={globalProxyMode}
onChange={(e: any) => setGlobalProxyMode(e.target.value as any)}
options={[
{ label: '手动输入模式', value: 'manual' },
{ label: '池随机轮询 (Random Rotation)', value: 'pool:random' },
{ label: '池固定项使用 (Specified ID)', value: 'pool:id' },
]}
className="w-full"
/>
<div className="space-y-1.5">
{globalProxyMode === 'manual' && (
<Input
label="手动输入代理地址"
value={globalProxyValue}
onChange={(e) => setGlobalProxyValue(e.target.value)}
placeholder="http://host:port 或 user:pass@host:port"
className="w-full"
/>
)}
{globalProxyMode === 'pool:random' && (
<div className="space-y-1.5">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300"></label>
<div className="h-[46px] px-4 rounded-xl bg-blue-50/30 dark:bg-blue-900/10 border border-blue-100/50 dark:border-blue-800/30 flex items-center justify-between text-blue-600 dark:text-blue-400 shadow-sm border-dashed">
<div className="flex items-center gap-2 text-sm font-bold">
<Zap className="h-4 w-4 fill-current" />
</div>
<span className="text-[10px] px-2 py-0.5 rounded-full bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 font-bold uppercase tracking-wider">Active</span>
</div>
</div>
)}
{globalProxyMode === 'pool:id' && (
<Select
label="从池中选择特定项"
value={globalProxyValue}
onChange={(e: any) => setGlobalProxyValue(e.target.value)}
options={proxies.filter(p => p.is_enabled).map(p => ({
label: `${p.description || '未命名'} - ${formatProxyDisplay(p.proxy_url)}`,
value: p.id.toString()
}))}
placeholder="请在下方下拉菜单中选择一个代理"
className="w-full"
/>
)}
</div>
</div>
</div>
{/* 注册专用代理 */}
<div className="p-8 space-y-6">
<div className="space-y-1.5 border-l-4 border-purple-500 pl-4">
<h3 className="text-base font-bold text-slate-900 dark:text-slate-100 flex items-center gap-2">
<PlayCircle className="h-5 w-5 text-purple-500" />
</h3>
<p className="text-xs text-slate-500 dark:text-slate-400">
(Team-Reg)
</p>
</div>
<div className="space-y-4">
<Select
label="使用模式"
value={regProxyMode}
onChange={(e: any) => setRegProxyMode(e.target.value as any)}
options={[
{ label: '手动输入模式', value: 'manual' },
{ label: '池随机轮询 (Random Rotation)', value: 'pool:random' },
{ label: '池固定项使用 (Specified ID)', value: 'pool:id' },
]}
className="w-full"
/>
<div className="space-y-1.5">
{regProxyMode === 'manual' && (
<Input
label="手动输入代理地址"
value={regProxyValue}
onChange={(e) => setRegProxyValue(e.target.value)}
placeholder="http://host:port 或 user:pass@host:port"
className="w-full"
/>
)}
{regProxyMode === 'pool:random' && (
<div className="space-y-1.5">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300"></label>
<div className="h-[46px] px-4 rounded-xl bg-purple-50/30 dark:bg-purple-900/10 border border-purple-100/50 dark:border-purple-800/30 flex items-center justify-between text-purple-600 dark:text-purple-400 shadow-sm border-dashed">
<div className="flex items-center gap-2 text-sm font-bold">
<Zap className="h-4 w-4 fill-current" />
</div>
<span className="text-[10px] px-2 py-0.5 rounded-full bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300 font-bold uppercase tracking-wider">Active</span>
</div>
</div>
)}
{regProxyMode === 'pool:id' && (
<Select
label="从池中选择特定项"
value={regProxyValue}
onChange={(e: any) => setRegProxyValue(e.target.value)}
options={proxies.filter(p => p.is_enabled).map(p => ({
label: `${p.description || '未命名'} - ${formatProxyDisplay(p.proxy_url)}`,
value: p.id.toString()
}))}
placeholder="请在下方下拉菜单中选择一个代理"
className="w-full"
/>
)}
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Info */} {/* Info */}
<Card> <Card>
<CardContent> <CardContent>

View File

@@ -11,11 +11,9 @@ import {
Save, Save,
Loader2, Loader2,
Globe, Globe,
Wifi,
WifiOff,
HelpCircle,
Zap, Zap,
Monitor Monitor,
Network
} 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'
@@ -24,19 +22,10 @@ import { useToast, ToastContainer } from '../components/Toast'
export default function Config() { export default function Config() {
const { config, isConnected, refreshConfig } = useConfig() const { config, isConnected, refreshConfig } = useConfig()
const [siteName, setSiteName] = useState('') const [siteName, setSiteName] = useState('')
const [defaultProxy, setDefaultProxy] = useState('')
const [teamRegProxy, setTeamRegProxy] = useState('')
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [savingProxy, setSavingProxy] = useState(false)
const [savingTeamRegProxy, setSavingTeamRegProxy] = useState(false)
const [testingProxy, setTestingProxy] = useState(false)
const [testingTeamRegProxy, setTestingTeamRegProxy] = useState(false)
const [proxyStatus, setProxyStatus] = useState<'unknown' | 'success' | 'error'>('unknown')
const [teamRegProxyStatus, setTeamRegProxyStatus] = useState<'unknown' | 'success' | 'error'>('unknown')
const [proxyOriginIP, setProxyOriginIP] = useState('')
const [teamRegProxyIP, setTeamRegProxyIP] = useState('')
const [authMethod, setAuthMethod] = useState<'api' | 'browser'>('browser') const [authMethod, setAuthMethod] = useState<'api' | 'browser'>('browser')
const [savingAuthMethod, setSavingAuthMethod] = useState(false) const [savingAuthMethod, setSavingAuthMethod] = useState(false)
const [proxyPoolCount, setProxyPoolCount] = useState<number>(0)
const { toasts, toast, removeToast } = useToast() const { toasts, toast, removeToast } = useToast()
// 加载站点名称和代理配置 // 加载站点名称和代理配置
@@ -47,24 +36,6 @@ export default function Config() {
const data = await res.json() const data = await res.json()
if (data.code === 0 && data.data) { if (data.code === 0 && data.data) {
setSiteName(data.data.site_name || 'Codex Pool') setSiteName(data.data.site_name || 'Codex Pool')
setDefaultProxy(data.data.default_proxy || '')
setTeamRegProxy(data.data.team_reg_proxy || '')
// 恢复全局代理测试状态
const testStatus = data.data.proxy_test_status
if (testStatus === 'success' || testStatus === 'error') {
setProxyStatus(testStatus)
}
if (data.data.proxy_test_ip) {
setProxyOriginIP(data.data.proxy_test_ip)
}
// 恢复注册代理测试状态
const teamRegTestStatus = data.data.team_reg_proxy_test_status
if (teamRegTestStatus === 'success' || teamRegTestStatus === 'error') {
setTeamRegProxyStatus(teamRegTestStatus)
}
if (data.data.team_reg_proxy_test_ip) {
setTeamRegProxyIP(data.data.team_reg_proxy_test_ip)
}
// 加载授权方式 // 加载授权方式
if (data.data.auth_method) { if (data.data.auth_method) {
setAuthMethod(data.data.auth_method === 'api' ? 'api' : 'browser') setAuthMethod(data.data.auth_method === 'api' ? 'api' : 'browser')
@@ -74,7 +45,21 @@ export default function Config() {
console.error('Failed to fetch config:', error) console.error('Failed to fetch config:', error)
} }
} }
const fetchProxyPoolCount = async () => {
try {
const res = await fetch('/api/codex-proxy')
const data = await res.json()
if (data.code === 0 && data.data) {
setProxyPoolCount(data.data.stats?.total || 0)
}
} catch (error) {
console.error('Failed to fetch proxy pool count:', error)
}
}
fetchConfig() fetchConfig()
fetchProxyPoolCount()
}, []) }, [])
// 保存站点名称 // 保存站点名称
@@ -100,117 +85,6 @@ export default function Config() {
} }
} }
// 保存代理地址
const handleSaveProxy = async () => {
setSavingProxy(true)
try {
const res = await fetch('/api/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ default_proxy: defaultProxy }),
})
const data = await res.json()
if (data.code === 0) {
toast.success('代理地址已保存')
setProxyStatus('unknown') // 保存后重置状态
setProxyOriginIP('')
refreshConfig()
} else {
toast.error(data.message || '保存失败')
}
} catch {
toast.error('网络错误')
} finally {
setSavingProxy(false)
}
}
// 测试代理连接
const handleTestProxy = async () => {
if (!defaultProxy.trim()) {
toast.error('请先输入代理地址')
return
}
setTestingProxy(true)
setProxyStatus('unknown')
setProxyOriginIP('')
try {
const res = await fetch('/api/proxy/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ proxy_url: defaultProxy, proxy_type: 'default' }),
})
const data = await res.json()
if (data.code === 0 && data.data?.connected) {
setProxyStatus('success')
setProxyOriginIP(data.data.origin_ip || '')
toast.success(`代理连接成功${data.data.origin_ip ? `, 出口IP: ${data.data.origin_ip}` : ''}`)
} else {
setProxyStatus('error')
toast.error(data.message || '代理连接失败')
}
} catch (e) {
setProxyStatus('error')
toast.error(e instanceof Error ? e.message : '网络错误')
} finally {
setTestingProxy(false)
}
}
// 保存注册代理地址
const handleSaveTeamRegProxy = async () => {
setSavingTeamRegProxy(true)
try {
const res = await fetch('/api/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ team_reg_proxy: teamRegProxy }),
})
const data = await res.json()
if (data.code === 0) {
toast.success('注册代理地址已保存')
refreshConfig()
} else {
toast.error(data.message || '保存失败')
}
} catch {
toast.error('网络错误')
} finally {
setSavingTeamRegProxy(false)
}
}
// 测试注册代理连接
const handleTestTeamRegProxy = async () => {
if (!teamRegProxy.trim()) {
toast.error('请先输入注册代理地址')
return
}
setTestingTeamRegProxy(true)
setTeamRegProxyStatus('unknown')
setTeamRegProxyIP('')
try {
const res = await fetch('/api/proxy/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ proxy_url: teamRegProxy, proxy_type: 'team_reg' }),
})
const data = await res.json()
if (data.code === 0 && data.data?.connected) {
setTeamRegProxyStatus('success')
setTeamRegProxyIP(data.data.origin_ip || '')
toast.success(`注册代理连接成功${data.data.origin_ip ? `, 出口IP: ${data.data.origin_ip}` : ''}`)
} else {
setTeamRegProxyStatus('error')
toast.error(data.message || '注册代理连接失败')
}
} catch (e) {
setTeamRegProxyStatus('error')
toast.error(e instanceof Error ? e.message : '网络错误')
} finally {
setTestingTeamRegProxy(false)
}
}
// 保存授权方式 // 保存授权方式
const handleSaveAuthMethod = async (method: 'api' | 'browser') => { const handleSaveAuthMethod = async (method: 'api' | 'browser') => {
@@ -258,6 +132,15 @@ export default function Config() {
statusIcon: (config.email?.services?.length ?? 0) > 0 ? CheckCircle : XCircle, statusIcon: (config.email?.services?.length ?? 0) > 0 ? CheckCircle : XCircle,
statusColor: (config.email?.services?.length ?? 0) > 0 ? 'text-green-600 dark:text-green-400' : 'text-orange-500', statusColor: (config.email?.services?.length ?? 0) > 0 ? 'text-green-600 dark:text-green-400' : 'text-orange-500',
}, },
{
to: '/config/codex-proxy',
icon: Network,
title: '代理池配置',
description: '管理代理池,用于注册和验证',
status: proxyPoolCount > 0 ? `${proxyPoolCount} 个代理` : '未配置',
statusIcon: proxyPoolCount > 0 ? CheckCircle : XCircle,
statusColor: proxyPoolCount > 0 ? 'text-green-600 dark:text-green-400' : 'text-orange-500',
},
] ]
return ( return (
@@ -285,33 +168,7 @@ export default function Config() {
</Button> </Button>
</div> </div>
{/* Config Cards */} {/* 基础配置 - 移到顶部 */}
<div className="grid gap-4">
{configItems.map((item) => (
<Link key={item.to} to={item.to} className="block group">
<Card className="transition-all duration-200 hover:shadow-lg hover:border-blue-300 dark:hover:border-blue-600">
<CardContent className="flex items-center gap-4 py-5">
<div className="p-3 rounded-xl bg-blue-50 dark:bg-blue-900/30 group-hover:bg-blue-100 dark:group-hover:bg-blue-900/50 transition-colors">
<item.icon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-slate-900 dark:text-slate-100">{item.title}</h3>
<p className="text-sm text-slate-500 dark:text-slate-400">{item.description}</p>
</div>
<div className="flex items-center gap-2">
<item.statusIcon className={`h-4 w-4 ${item.statusColor}`} />
<span className={`text-sm font-medium ${item.statusColor}`}>
{item.status}
</span>
</div>
<ChevronRight className="h-5 w-5 text-slate-400 group-hover:text-blue-500 transition-colors" />
</CardContent>
</Card>
</Link>
))}
</div>
{/* Site Settings */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
@@ -338,7 +195,7 @@ export default function Config() {
icon={saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />} icon={saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
className="shrink-0" className="shrink-0"
> >
{saving ? '保存中...' : '保存名称'} {saving ? '保存中...' : '保存'}
</Button> </Button>
</div> </div>
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400"> <p className="mt-1 text-xs text-slate-500 dark:text-slate-400">
@@ -351,71 +208,71 @@ export default function Config() {
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-3"> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">
</label> </label>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-4">
{/* API 模式 - CodexAuth */} {/* API 模式 - CodexAuth */}
<button <button
onClick={() => handleSaveAuthMethod('api')} onClick={() => handleSaveAuthMethod('api')}
disabled={savingAuthMethod} disabled={savingAuthMethod}
className={`relative flex flex-col items-center gap-2 p-4 rounded-xl border-2 transition-all duration-200 ${authMethod === 'api' className={`relative flex items-center gap-3 p-3 rounded-xl border-2 transition-all duration-200 text-left ${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-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' : '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={`shrink-0 p-2 rounded-lg ${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-blue-500 text-white'
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400'
}`}> }`}>
<Zap className="h-6 w-6" /> <Zap className="h-5 w-5" />
</div> </div>
<div className="text-center"> <div className="flex-1 min-w-0">
<div className={`font-semibold ${authMethod === 'api' <div className={`font-semibold text-sm ${authMethod === 'api'
? 'text-blue-700 dark:text-blue-300' ? 'text-blue-700 dark:text-blue-300'
: 'text-slate-700 dark:text-slate-300' : 'text-slate-700 dark:text-slate-300'
}`}> }`}>
CodexAuth API CodexAuth API
</div> </div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-1"> <div className="text-[11px] text-slate-500 dark:text-slate-400 mt-0.5 truncate">
API API
</div> </div>
</div> </div>
{authMethod === 'api' && (
<div className="absolute top-1.5 right-1.5">
<CheckCircle className="h-3.5 w-3.5 text-blue-500" />
</div>
)}
</button> </button>
{/* 浏览器模式 */} {/* 浏览器模式 */}
<button <button
onClick={() => handleSaveAuthMethod('browser')} onClick={() => handleSaveAuthMethod('browser')}
disabled={savingAuthMethod} disabled={savingAuthMethod}
className={`relative flex flex-col items-center gap-2 p-4 rounded-xl border-2 transition-all duration-200 ${authMethod === 'browser' className={`relative flex items-center gap-3 p-3 rounded-xl border-2 transition-all duration-200 text-left ${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-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' : '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={`shrink-0 p-2 rounded-lg ${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-emerald-500 text-white'
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400'
}`}> }`}>
<Monitor className="h-6 w-6" /> <Monitor className="h-5 w-5" />
</div> </div>
<div className="text-center"> <div className="flex-1 min-w-0">
<div className={`font-semibold ${authMethod === 'browser' <div className={`font-semibold text-sm ${authMethod === 'browser'
? 'text-emerald-700 dark:text-emerald-300' ? 'text-emerald-700 dark:text-emerald-300'
: 'text-slate-700 dark:text-slate-300' : 'text-slate-700 dark:text-slate-300'
}`}> }`}>
</div> </div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-1"> <div className="text-[11px] text-slate-500 dark:text-slate-400 mt-0.5 truncate">
Chromedp Chromedp
</div> </div>
</div> </div>
{authMethod === 'browser' && (
<div className="absolute top-1.5 right-1.5">
<CheckCircle className="h-3.5 w-3.5 text-emerald-500" />
</div>
)}
</button> </button>
</div> </div>
{authMethod === 'api' && ( {authMethod === 'api' && (
@@ -430,140 +287,35 @@ export default function Config() {
</div> </div>
)} )}
</div> </div>
<div className="pt-4 border-t border-slate-200 dark:border-slate-700">
<div className="flex items-center gap-2 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">
</label>
{/* 代理状态徽章 */}
{proxyStatus === 'success' && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
<Wifi className="h-3 w-3" />
</span>
)}
{proxyStatus === 'error' && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400">
<WifiOff className="h-3 w-3" />
</span>
)}
{proxyStatus === 'unknown' && defaultProxy && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-400">
<HelpCircle className="h-3 w-3" />
</span>
)}
</div>
<div className="flex gap-3">
<Input
value={defaultProxy}
onChange={(e) => {
setDefaultProxy(e.target.value)
setProxyStatus('unknown')
}}
placeholder="http://127.0.0.1:7890 或 1.2.3.4:5678:user:pass"
className="flex-1"
/>
<Button
size="sm"
onClick={handleSaveProxy}
disabled={savingProxy}
icon={savingProxy ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
className="shrink-0"
>
{savingProxy ? '保存中...' : '保存代理'}
</Button>
<Button
size="sm"
variant="outline"
onClick={handleTestProxy}
disabled={testingProxy || !defaultProxy.trim()}
icon={testingProxy ? <Loader2 className="h-4 w-4 animate-spin" /> : <Wifi className="h-4 w-4" />}
className="shrink-0"
>
{testingProxy ? '测试中...' : '测试连接'}
</Button>
</div>
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400">
http://host:port、http://user:pass@host:port、host:port:user:pass默认按 http 解析)
{proxyOriginIP && (
<span className="ml-2 text-green-600 dark:text-green-400">
IP: {proxyOriginIP}
</span>
)}
</p>
</div>
{/* 注册代理地址配置 */}
<div className="pt-4 border-t border-slate-200 dark:border-slate-700">
<div className="flex items-center gap-2 mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">
</label>
{/* 代理状态徽章 */}
{teamRegProxyStatus === 'success' && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
<Wifi className="h-3 w-3" />
</span>
)}
{teamRegProxyStatus === 'error' && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400">
<WifiOff className="h-3 w-3" />
</span>
)}
{teamRegProxyStatus === 'unknown' && teamRegProxy && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-400">
<HelpCircle className="h-3 w-3" />
</span>
)}
</div>
<div className="flex gap-3">
<Input
value={teamRegProxy}
onChange={(e) => {
setTeamRegProxy(e.target.value)
setTeamRegProxyStatus('unknown')
}}
placeholder="http://user:pass@host:port"
className="flex-1"
/>
<Button
size="sm"
onClick={handleSaveTeamRegProxy}
disabled={savingTeamRegProxy}
icon={savingTeamRegProxy ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
className="shrink-0"
>
{savingTeamRegProxy ? '保存中...' : '保存'}
</Button>
<Button
size="sm"
variant="outline"
onClick={handleTestTeamRegProxy}
disabled={testingTeamRegProxy || !teamRegProxy.trim()}
icon={testingTeamRegProxy ? <Loader2 className="h-4 w-4 animate-spin" /> : <Wifi className="h-4 w-4" />}
className="shrink-0"
>
{testingTeamRegProxy ? '测试中...' : '测试连接'}
</Button>
</div>
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400">
Team 使使
{teamRegProxyIP && (
<span className="ml-2 text-green-600 dark:text-green-400">
IP: {teamRegProxyIP}
</span>
)}
</p>
</div>
</CardContent> </CardContent>
</Card> </Card>
{/* Config Cards - S2A、邮箱、代理池配置 */}
<div className="grid gap-4">
{configItems.map((item) => (
<Link key={item.to} to={item.to} className="block group">
<Card className="transition-all duration-200 hover:shadow-lg hover:border-blue-300 dark:hover:border-blue-600">
<CardContent className="flex items-center gap-4 py-5">
<div className="p-3 rounded-xl bg-blue-50 dark:bg-blue-900/30 group-hover:bg-blue-100 dark:group-hover:bg-blue-900/50 transition-colors">
<item.icon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-slate-900 dark:text-slate-100">{item.title}</h3>
<p className="text-sm text-slate-500 dark:text-slate-400">{item.description}</p>
</div>
<div className="flex items-center gap-2">
<item.statusIcon className={`h-4 w-4 ${item.statusColor}`} />
<span className={`text-sm font-medium ${item.statusColor}`}>
{item.status}
</span>
</div>
<ChevronRight className="h-5 w-5 text-slate-400 group-hover:text-blue-500 transition-colors" />
</CardContent>
</Card>
</Link>
))}
</div>
{/* Info */} {/* Info */}
<Card> <Card>
<CardContent> <CardContent>

View File

@@ -5,19 +5,15 @@ import {
RefreshCw, RefreshCw,
Play, Play,
Pause, Pause,
Shield,
TrendingUp, TrendingUp,
TrendingDown, TrendingDown,
Zap, Zap,
AlertTriangle, AlertTriangle,
CheckCircle,
Clock, Clock,
Save, Save,
Monitor as MonitorIcon, 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 type { DashboardStats } from '../types'
interface PoolStatus { interface PoolStatus {
target: number target: number
@@ -31,15 +27,6 @@ interface PoolStatus {
polling_interval: number polling_interval: number
} }
interface HealthCheckResult {
account_id: number
email: string
status: string
checked_at: string
error?: string
auto_paused?: boolean
}
interface AutoAddLog { interface AutoAddLog {
timestamp: string timestamp: string
target: number target: number
@@ -52,18 +39,17 @@ interface AutoAddLog {
} }
export default function Monitor() { export default function Monitor() {
const [stats, setStats] = useState<DashboardStats | null>(null)
const [poolStatus, setPoolStatus] = useState<PoolStatus | null>(null) const [poolStatus, setPoolStatus] = useState<PoolStatus | null>(null)
const [healthResults, setHealthResults] = useState<HealthCheckResult[]>([])
const [autoAddLogs, setAutoAddLogs] = useState<AutoAddLog[]>([]) const [autoAddLogs, setAutoAddLogs] = useState<AutoAddLog[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
const [checkingHealth, setCheckingHealth] = useState(false)
const [autoPauseEnabled, setAutoPauseEnabled] = useState(false)
// 配置表单状态 - 从后端 SQLite 加载 // 配置表单状态 - 从后端 SQLite 加载
const [targetInput, setTargetInput] = useState(50) const [targetInput, setTargetInput] = useState(50)
const [autoAdd, setAutoAdd] = useState(false) const [autoAdd, setAutoAdd] = useState(false)
const [autoRegister, setAutoRegister] = useState(false) // 母号不足时自动注册
const [autoRegConcurrency, setAutoRegConcurrency] = useState(2) // 自动注册并发数
const [autoRegUseProxy, setAutoRegUseProxy] = useState(false) // 自动注册时使用代理
const [minInterval, setMinInterval] = useState(300) const [minInterval, setMinInterval] = useState(300)
const [checkInterval, setCheckInterval] = useState(60) const [checkInterval, setCheckInterval] = useState(60)
const [pollingEnabled, setPollingEnabled] = useState(false) const [pollingEnabled, setPollingEnabled] = useState(false)
@@ -72,6 +58,11 @@ export default function Monitor() {
const [globalProxy, setGlobalProxy] = useState('') // 全局代理地址(只读显示) const [globalProxy, setGlobalProxy] = useState('') // 全局代理地址(只读显示)
const [authMethod, setAuthMethod] = useState<'api' | 'browser'>('browser') // 授权方式 const [authMethod, setAuthMethod] = useState<'api' | 'browser'>('browser') // 授权方式
// 自动补号配置
const [membersPerTeam, setMembersPerTeam] = useState(4)
const [concurrentTeams, setConcurrentTeams] = useState(2)
const [s2aConcurrency, setS2aConcurrency] = useState(2)
// 倒计时状态 // 倒计时状态
const [countdown, setCountdown] = useState(60) const [countdown, setCountdown] = useState(60)
@@ -100,7 +91,6 @@ export default function Monitor() {
try { try {
const data = await requestS2A('/dashboard/stats') const data = await requestS2A('/dashboard/stats')
if (data) { if (data) {
setStats(data)
// 更新 poolStatus // 更新 poolStatus
setPoolStatus({ setPoolStatus({
current: data.normal_accounts || 0, current: data.normal_accounts || 0,
@@ -129,11 +119,17 @@ export default function Monitor() {
body: JSON.stringify({ body: JSON.stringify({
target: targetInput, target: targetInput,
auto_add: autoAdd, auto_add: autoAdd,
auto_register: autoRegister,
auto_reg_concurrency: autoRegConcurrency,
auto_reg_use_proxy: autoRegUseProxy,
min_interval: minInterval, min_interval: minInterval,
check_interval: checkInterval, check_interval: checkInterval,
polling_enabled: pollingEnabled, polling_enabled: pollingEnabled,
polling_interval: pollingInterval, polling_interval: pollingInterval,
replenish_use_proxy: replenishUseProxy, replenish_use_proxy: replenishUseProxy,
members_per_team: membersPerTeam,
concurrent_teams: concurrentTeams,
s2a_concurrency: s2aConcurrency,
}), }),
}) })
const data = await res.json() const data = await res.json()
@@ -175,10 +171,14 @@ export default function Monitor() {
body: JSON.stringify({ body: JSON.stringify({
target: targetInput, target: targetInput,
auto_add: autoAdd, auto_add: autoAdd,
auto_register: autoRegister,
min_interval: minInterval, min_interval: minInterval,
check_interval: checkInterval, check_interval: checkInterval,
polling_enabled: newPollingEnabled, polling_enabled: newPollingEnabled,
polling_interval: pollingInterval, polling_interval: pollingInterval,
members_per_team: membersPerTeam,
concurrent_teams: concurrentTeams,
s2a_concurrency: s2aConcurrency,
}), }),
}) })
} catch (e) { } catch (e) {
@@ -202,10 +202,14 @@ export default function Monitor() {
body: JSON.stringify({ body: JSON.stringify({
target: targetInput, target: targetInput,
auto_add: autoAdd, auto_add: autoAdd,
auto_register: autoRegister,
min_interval: minInterval, min_interval: minInterval,
check_interval: checkInterval, check_interval: checkInterval,
polling_enabled: pollingEnabled, polling_enabled: pollingEnabled,
polling_interval: pollingInterval, polling_interval: pollingInterval,
members_per_team: membersPerTeam,
concurrent_teams: concurrentTeams,
s2a_concurrency: s2aConcurrency,
}), }),
}) })
const data = await res.json() const data = await res.json()
@@ -225,30 +229,6 @@ export default function Monitor() {
setLoading(false) setLoading(false)
} }
// 健康检查 - S2A 没有此接口,显示提示
const handleHealthCheck = async (_autoPause: boolean = false) => {
setCheckingHealth(true)
// S2A 没有健康检查 API使用 dashboard/stats 模拟
try {
const data = await requestS2A('/dashboard/stats')
if (data) {
// 模拟健康检查结果
setHealthResults([
{
account_id: 0,
email: '统计摘要',
status: 'info',
checked_at: new Date().toISOString(),
error: `正常: ${data.normal_accounts || 0}, 错误: ${data.error_accounts || 0}, 限流: ${data.ratelimit_accounts || 0}`,
}
])
}
} catch (e) {
console.error('健康检查失败:', e)
}
setCheckingHealth(false)
}
// 获取自动补号日志 - S2A 没有此接口 // 获取自动补号日志 - S2A 没有此接口
const fetchAutoAddLogs = async () => { const fetchAutoAddLogs = async () => {
// S2A 没有自动补号日志 API留空 // S2A 没有自动补号日志 API留空
@@ -279,6 +259,9 @@ export default function Monitor() {
const s = json.data const s = json.data
const target = s.target || 50 const target = s.target || 50
const autoAddVal = s.auto_add || false const autoAddVal = s.auto_add || false
const autoRegisterVal = s.auto_register || false
const autoRegConcurrencyVal = s.auto_reg_concurrency || 2
const autoRegUseProxyVal = s.auto_reg_use_proxy || false
const minIntervalVal = s.min_interval || 300 const minIntervalVal = s.min_interval || 300
const checkIntervalVal = s.check_interval || 60 const checkIntervalVal = s.check_interval || 60
const pollingEnabledVal = s.polling_enabled || false const pollingEnabledVal = s.polling_enabled || false
@@ -287,6 +270,9 @@ export default function Monitor() {
setTargetInput(target) setTargetInput(target)
setAutoAdd(autoAddVal) setAutoAdd(autoAddVal)
setAutoRegister(autoRegisterVal)
setAutoRegConcurrency(autoRegConcurrencyVal)
setAutoRegUseProxy(autoRegUseProxyVal)
setMinInterval(minIntervalVal) setMinInterval(minIntervalVal)
setCheckInterval(checkIntervalVal) setCheckInterval(checkIntervalVal)
setPollingEnabled(pollingEnabledVal) setPollingEnabled(pollingEnabledVal)
@@ -295,6 +281,11 @@ export default function Monitor() {
savedPollingIntervalRef.current = interval savedPollingIntervalRef.current = interval
setCountdown(interval) setCountdown(interval)
// 加载自动补号配置
setMembersPerTeam(s.members_per_team || 4)
setConcurrentTeams(s.concurrent_teams || 2)
setS2aConcurrency(s.s2a_concurrency || 2)
// 返回加载的配置用于后续刷新 // 返回加载的配置用于后续刷新
return { target, autoAdd: autoAddVal, minInterval: minIntervalVal, checkInterval: checkIntervalVal, pollingEnabled: pollingEnabledVal, pollingInterval: interval, replenishUseProxy: replenishUseProxyVal } return { target, autoAdd: autoAddVal, minInterval: minIntervalVal, checkInterval: checkIntervalVal, pollingEnabled: pollingEnabledVal, pollingInterval: interval, replenishUseProxy: replenishUseProxyVal }
} }
@@ -316,7 +307,6 @@ export default function Monitor() {
const data = await requestS2A('/dashboard/stats') const data = await requestS2A('/dashboard/stats')
if (data) { if (data) {
const target = settings?.target || 50 const target = settings?.target || 50
setStats(data)
setPoolStatus({ setPoolStatus({
current: data.normal_accounts || 0, current: data.normal_accounts || 0,
target: target, target: target,
@@ -360,16 +350,6 @@ export default function Monitor() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [pollingEnabled]) }, [pollingEnabled])
// 计算健康状态
const healthySummary = healthResults.reduce(
(acc, r) => {
if (r.status === 'active' && !r.error) acc.healthy++
else acc.unhealthy++
return acc
},
{ healthy: 0, unhealthy: 0 }
)
const deficit = poolStatus ? Math.max(0, poolStatus.target - poolStatus.current) : 0 const deficit = poolStatus ? Math.max(0, poolStatus.target - poolStatus.current) : 0
const healthPercent = poolStatus && poolStatus.target > 0 const healthPercent = poolStatus && poolStatus.target > 0
? Math.min(100, (poolStatus.current / poolStatus.target) * 100) ? Math.min(100, (poolStatus.current / poolStatus.target) * 100)
@@ -532,6 +512,15 @@ export default function Monitor() {
description="开启后,当号池不足时自动补充账号" description="开启后,当号池不足时自动补充账号"
/> />
</div> </div>
<div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700">
<Switch
checked={autoRegister}
onChange={setAutoRegister}
disabled={!autoAdd}
label="母号不足时自动注册"
description="开启后,当可用母号不足时自动注册新的 ChatGPT 账号(使用 Team 注册功能)"
/>
</div>
<div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700"> <div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700">
<Switch <Switch
checked={replenishUseProxy} checked={replenishUseProxy}
@@ -577,26 +566,6 @@ export default function Monitor() {
</p> </p>
</div> </div>
<Input
label="最小间隔 (秒)"
type="number"
min={60}
max={3600}
value={minInterval}
onChange={(e) => setMinInterval(Number(e.target.value))}
hint="两次自动补号的最小间隔"
disabled={!autoAdd}
/>
<Input
label="检查间隔 (秒)"
type="number"
min={10}
max={300}
value={checkInterval}
onChange={(e) => setCheckInterval(Number(e.target.value))}
hint="自动补号检查频率 (10-300秒)"
disabled={!autoAdd}
/>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<Button onClick={handleSetTarget} loading={loading} className="w-full"> <Button onClick={handleSetTarget} loading={loading} className="w-full">
@@ -648,6 +617,156 @@ export default function Monitor() {
<div className={`status-dot ${pollingEnabled ? 'online' : 'offline'}`} /> <div className={`status-dot ${pollingEnabled ? 'online' : 'offline'}`} />
</div> </div>
</div> </div>
{/* 自动补号配置 */}
<div className="pt-4 border-t border-slate-200 dark:border-slate-700">
<p className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-3 flex items-center gap-2">
<Zap className="h-4 w-4 text-blue-500" />
()
</p>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">
Team
</label>
<input
type="number"
min={1}
max={10}
value={membersPerTeam}
onChange={(e) => setMembersPerTeam(Number(e.target.value) || 4)}
className="w-full px-3 py-2 text-sm rounded-lg border transition-colors
bg-white dark:bg-slate-800
text-slate-900 dark:text-slate-100
border-slate-300 dark:border-slate-600
focus:border-blue-500 focus:ring-blue-500
focus:outline-none focus:ring-2"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">
Team
</label>
<input
type="number"
min={1}
max={10}
value={concurrentTeams}
onChange={(e) => setConcurrentTeams(Number(e.target.value) || 2)}
className="w-full px-3 py-2 text-sm rounded-lg border transition-colors
bg-white dark:bg-slate-800
text-slate-900 dark:text-slate-100
border-slate-300 dark:border-slate-600
focus:border-blue-500 focus:ring-blue-500
focus:outline-none focus:ring-2"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">
</label>
<input
type="number"
min={1}
max={4}
value={s2aConcurrency}
onChange={(e) => setS2aConcurrency(Number(e.target.value) || 2)}
className="w-full px-3 py-2 text-sm rounded-lg border transition-colors
bg-white dark:bg-slate-800
text-slate-900 dark:text-slate-100
border-slate-300 dark:border-slate-600
focus:border-blue-500 focus:ring-blue-500
focus:outline-none focus:ring-2"
/>
</div>
</div>
<p className="mt-2 text-xs text-slate-500 dark:text-slate-400">
使 1-4
</p>
<div className="grid grid-cols-2 gap-3 pt-3 mt-3 border-t border-dashed border-slate-200 dark:border-slate-700/50">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">
()
</label>
<input
type="number"
min={60}
max={3600}
value={minInterval}
onChange={(e) => setMinInterval(Number(e.target.value))}
disabled={!autoAdd}
className="w-full px-3 py-2 text-sm rounded-lg border transition-colors
bg-white dark:bg-slate-800
text-slate-900 dark:text-slate-100
border-slate-300 dark:border-slate-600
focus:border-blue-500 focus:ring-blue-500
focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">
()
</label>
<input
type="number"
min={10}
max={300}
value={checkInterval}
onChange={(e) => setCheckInterval(Number(e.target.value))}
disabled={!autoAdd}
className="w-full px-3 py-2 text-sm rounded-lg border transition-colors
bg-white dark:bg-slate-800
text-slate-900 dark:text-slate-100
border-slate-300 dark:border-slate-600
focus:border-blue-500 focus:ring-blue-500
focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed"
/>
</div>
</div>
{/* 自动注册配置 - 仅在启用自动注册时显示 */}
{autoRegister && autoAdd && (
<div className="mt-3 pt-3 border-t border-dashed border-purple-300 dark:border-purple-700/50">
<p className="text-xs font-medium text-purple-600 dark:text-purple-400 mb-2 flex items-center gap-1">
<Zap className="h-3 w-3" />
</p>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">
</label>
<input
type="number"
min={1}
max={10}
value={autoRegConcurrency}
onChange={(e) => setAutoRegConcurrency(Number(e.target.value) || 2)}
className="w-full px-3 py-2 text-sm rounded-lg border transition-colors
bg-white dark:bg-slate-800
text-slate-900 dark:text-slate-100
border-slate-300 dark:border-slate-600
focus:border-purple-500 focus:ring-purple-500
focus:outline-none focus:ring-2"
/>
</div>
<div className="flex items-end pb-2">
<label className="flex items-center gap-2 text-sm text-slate-600 dark:text-slate-400 cursor-pointer">
<input
type="checkbox"
checked={autoRegUseProxy}
onChange={(e) => setAutoRegUseProxy(e.target.checked)}
disabled={!globalProxy}
className="h-4 w-4 rounded border-slate-300 text-purple-600 focus:ring-purple-500 disabled:opacity-50"
/>
<span>使</span>
</label>
</div>
</div>
</div>
)}
</div>
</div> </div>
<div className="flex gap-3 mt-4"> <div className="flex gap-3 mt-4">
<Button <Button
@@ -673,117 +792,6 @@ export default function Monitor() {
</Card> </Card>
</div> </div>
{/* 健康检查 */}
<Card className="glass-card">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5 text-purple-500" />
</CardTitle>
<div className="flex items-center gap-2">
<label className="flex items-center gap-2 text-sm text-slate-600 dark:text-slate-400">
<input
type="checkbox"
checked={autoPauseEnabled}
onChange={(e) => setAutoPauseEnabled(e.target.checked)}
className="h-4 w-4 rounded border-slate-300 text-purple-600 focus:ring-purple-500"
/>
</label>
<Button
variant="outline"
size="sm"
onClick={() => handleHealthCheck(autoPauseEnabled)}
disabled={checkingHealth}
loading={checkingHealth}
icon={<Shield className="h-4 w-4" />}
>
{checkingHealth ? '检查中...' : '开始检查'}
</Button>
</div>
</CardHeader>
<CardContent>
{healthResults.length > 0 ? (
<>
{/* 统计 */}
<div className="flex gap-4 mb-4">
<div className="flex items-center gap-2 text-green-600">
<CheckCircle className="h-4 w-4" />
<span>: {healthySummary.healthy}</span>
</div>
<div className="flex items-center gap-2 text-red-500">
<AlertTriangle className="h-4 w-4" />
<span>: {healthySummary.unhealthy}</span>
</div>
</div>
{/* 结果列表 */}
<div className="max-h-64 overflow-y-auto space-y-2">
{healthResults.map((result) => (
<div
key={result.account_id}
className={`flex items-center justify-between p-3 rounded-lg ${result.error
? 'bg-red-50 dark:bg-red-900/20'
: 'bg-green-50 dark:bg-green-900/20'
}`}
>
<div>
<p className="font-medium text-slate-900 dark:text-slate-100">
{result.email}
</p>
<p className="text-xs text-slate-500">ID: {result.account_id}</p>
</div>
<div className="text-right">
<p className={`text-sm font-medium ${result.error ? 'text-red-500' : 'text-green-600'
}`}>
{result.status}
</p>
{result.error && (
<p className="text-xs text-red-400">{result.error}</p>
)}
</div>
</div>
))}
</div>
</>
) : (
<div className="text-center py-8 text-slate-500">
<Shield className="h-12 w-12 mx-auto mb-3 opacity-30" />
<p>"开始检查"</p>
</div>
)}
</CardContent>
</Card>
{/* S2A 实时统计 */}
{stats && (
<Card>
<CardHeader>
<CardTitle>S2A </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 rounded-lg bg-blue-50 dark:bg-blue-900/20">
<p className="text-2xl font-bold text-blue-600">{stats.total_accounts}</p>
<p className="text-sm text-slate-500"></p>
</div>
<div className="text-center p-4 rounded-lg bg-green-50 dark:bg-green-900/20">
<p className="text-2xl font-bold text-green-600">{stats.normal_accounts}</p>
<p className="text-sm text-slate-500"></p>
</div>
<div className="text-center p-4 rounded-lg bg-red-50 dark:bg-red-900/20">
<p className="text-2xl font-bold text-red-500">{stats.error_accounts}</p>
<p className="text-sm text-slate-500"></p>
</div>
<div className="text-center p-4 rounded-lg bg-orange-50 dark:bg-orange-900/20">
<p className="text-2xl font-bold text-orange-500">{stats.ratelimit_accounts}</p>
<p className="text-sm text-slate-500"></p>
</div>
</div>
</CardContent>
</Card>
)}
{/* 自动补号日志 */} {/* 自动补号日志 */}
{autoAddLogs.length > 0 && ( {autoAddLogs.length > 0 && (
<Card> <Card>
@@ -830,8 +838,6 @@ export default function Monitor() {
</CardContent> </CardContent>
</Card> </Card>
)} )}
{/* 实时日志 */}
<LiveLogViewer className="mt-6" />
</div> </div>
) )
} }

View File

@@ -0,0 +1,105 @@
import { useState, useEffect, useCallback } from 'react'
import { RefreshCw, BarChart3 } from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button } from '../components/common'
import LiveLogViewer from '../components/LiveLogViewer'
import type { DashboardStats } from '../types'
export default function S2AStats() {
const [stats, setStats] = useState<DashboardStats | null>(null)
const [refreshing, setRefreshing] = useState(false)
const proxyBase = '/api/s2a/proxy'
const requestS2A = async (path: string, options: RequestInit = {}) => {
const res = await fetch(`${proxyBase}${path}`, options)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const json = await res.json()
if (json && typeof json === 'object' && 'code' in json) {
if (json.code !== 0) throw new Error(json.message || 'API error')
return json.data
}
return json
}
const refreshStats = useCallback(async () => {
setRefreshing(true)
try {
const data = await requestS2A('/dashboard/stats')
if (data) {
setStats(data)
}
} catch (e) {
console.error('刷新统计失败:', e)
}
setRefreshing(false)
}, [])
useEffect(() => {
refreshStats()
}, [refreshStats])
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">S2A </h1>
<p className="text-sm text-slate-500 dark:text-slate-400">
S2A
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={refreshStats}
disabled={refreshing}
icon={<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />}
>
</Button>
</div>
</div>
{/* S2A 实时统计 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5 text-blue-500" />
S2A
</CardTitle>
</CardHeader>
<CardContent>
{stats ? (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 rounded-lg bg-blue-50 dark:bg-blue-900/20">
<p className="text-2xl font-bold text-blue-600">{stats.total_accounts}</p>
<p className="text-sm text-slate-500"></p>
</div>
<div className="text-center p-4 rounded-lg bg-green-50 dark:bg-green-900/20">
<p className="text-2xl font-bold text-green-600">{stats.normal_accounts}</p>
<p className="text-sm text-slate-500"></p>
</div>
<div className="text-center p-4 rounded-lg bg-red-50 dark:bg-red-900/20">
<p className="text-2xl font-bold text-red-500">{stats.error_accounts}</p>
<p className="text-sm text-slate-500"></p>
</div>
<div className="text-center p-4 rounded-lg bg-orange-50 dark:bg-orange-900/20">
<p className="text-2xl font-bold text-orange-500">{stats.ratelimit_accounts}</p>
<p className="text-sm text-slate-500"></p>
</div>
</div>
) : (
<div className="text-center py-8 text-slate-500">
<BarChart3 className="h-12 w-12 mx-auto mb-3 opacity-30" />
<p>...</p>
</div>
)}
</CardContent>
</Card>
{/* 实时日志 */}
<LiveLogViewer />
</div>
)
}

View File

@@ -12,7 +12,9 @@ import {
RefreshCw, RefreshCw,
FileJson, FileJson,
Users, Users,
Zap,
} from 'lucide-react' } from 'lucide-react'
import { Link } from 'react-router-dom'
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common' import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common'
interface TeamRegStatus { interface TeamRegStatus {
@@ -330,18 +332,24 @@ export default function TeamReg() {
{useProxy && proxy && ( {useProxy && proxy && (
<div className="ml-7 p-2 rounded bg-slate-100 dark:bg-slate-700"> <div className="ml-7 p-2 rounded bg-slate-100 dark:bg-slate-700">
<p className="text-sm text-slate-700 dark:text-slate-300 font-mono break-all"> <p className="text-sm text-slate-700 dark:text-slate-300 font-mono break-all flex items-center gap-2">
{proxy} {proxy === 'pool:random' || proxy === '[RANDOM]' ? (
<><Zap className="h-3 w-3 text-purple-500" /> </>
) : proxy.startsWith('pool:id:') ? (
<><CheckCircle className="h-3 w-3 text-blue-500" /> (ID: {proxy.replace('pool:id:', '')})</>
) : (
proxy
)}
</p> </p>
<p className="text-xs text-slate-500 mt-1"> <p className="text-xs text-slate-500 mt-1">
<a href="/config" className="text-blue-500 hover:underline"></a> <Link to="/config/codex-proxy" className="text-blue-500 hover:underline"></Link>
</p> </p>
</div> </div>
)} )}
{useProxy && !proxy && ( {useProxy && !proxy && (
<p className="ml-7 text-xs text-orange-500"> <p className="ml-7 text-xs text-orange-500">
<a href="/config" className="text-blue-500 hover:underline"></a> <Link to="/config/codex-proxy" className="text-blue-500 hover:underline"></Link>
</p> </p>
)} )}
</div> </div>
@@ -511,13 +519,23 @@ export default function TeamReg() {
// 日志行组件 - 根据内容着色 // 日志行组件 - 根据内容着色
function LogLine({ log }: { log: string }) { function LogLine({ log }: { log: string }) {
let colorClass = 'text-slate-300' let colorClass = 'text-slate-300'
let fontClass = ''
if (log.includes('[OK]') || log.includes('成功')) { // 成功日志 - 绿色加粗
if (log.includes('✓') || log.includes('入库成功') || log.includes('注册成功')) {
colorClass = 'text-green-400' colorClass = 'text-green-400'
} else if (log.includes('[!]') || log.includes('重试') || log.includes('[错误]')) { fontClass = 'font-bold'
} else if (log.includes('[OK]') || log.includes('成功') || log.includes('完成') || log.includes('结果已保存')) {
colorClass = 'text-emerald-400'
fontClass = 'font-medium'
} else if (log.includes('[!]') || log.includes('重试') || log.includes('[错误]') || log.includes('失败')) {
colorClass = 'text-orange-400' colorClass = 'text-orange-400'
} else if (log.includes('[系统]') || log.includes('[输入]')) { } else if (log.includes('[系统]') || log.includes('[输入]')) {
colorClass = 'text-blue-400' colorClass = 'text-blue-400'
} else if (log.includes('════════')) {
// 分隔线 - 高亮显示
colorClass = 'text-yellow-400'
fontClass = 'font-bold'
} else if (log.includes('[W1]')) { } else if (log.includes('[W1]')) {
colorClass = 'text-cyan-400' colorClass = 'text-cyan-400'
} else if (log.includes('[W2]')) { } else if (log.includes('[W2]')) {
@@ -526,12 +544,10 @@ function LogLine({ log }: { log: string }) {
colorClass = 'text-yellow-400' colorClass = 'text-yellow-400'
} else if (log.includes('[W4]')) { } else if (log.includes('[W4]')) {
colorClass = 'text-pink-400' colorClass = 'text-pink-400'
} else if (log.includes('完成') || log.includes('结果已保存')) {
colorClass = 'text-emerald-400 font-medium'
} }
return ( return (
<div className={`${colorClass} leading-relaxed whitespace-pre-wrap break-all`}> <div className={`${colorClass} ${fontClass} leading-relaxed whitespace-pre-wrap break-all`}>
{log} {log}
</div> </div>
) )

View File

@@ -9,5 +9,6 @@ export { default as Monitor } from './Monitor'
export { default as Cleaner } from './Cleaner' export { default as Cleaner } from './Cleaner'
export { default as TeamReg } from './TeamReg' export { default as TeamReg } from './TeamReg'
export { default as CodexProxyConfig } from './CodexProxyConfig' export { default as CodexProxyConfig } from './CodexProxyConfig'
export { default as S2AStats } from './S2AStats'