feat: Implement batch team owner processing with dedicated upload, configuration, and monitoring pages and backend services.

This commit is contained in:
2026-01-30 18:59:03 +08:00
parent 165c6d69b9
commit 6f18740215
7 changed files with 348 additions and 46 deletions

View File

@@ -0,0 +1,209 @@
package api
import (
"fmt"
"net/http"
"strconv"
"sync"
"time"
"codex-pool/internal/config"
"codex-pool/internal/database"
"codex-pool/internal/logger"
)
var (
autoAddRunning bool
autoAddMu sync.Mutex
autoAddStopChan chan struct{}
lastAutoAddTime time.Time
)
// StartAutoAddService 启动自动补号服务
func StartAutoAddService() {
autoAddMu.Lock()
if autoAddRunning {
autoAddMu.Unlock()
return
}
autoAddRunning = true
autoAddStopChan = make(chan struct{})
autoAddMu.Unlock()
logger.Info("自动补号服务已启动", "", "auto-add")
go func() {
ticker := time.NewTicker(60 * time.Second) // 每分钟检查一次
defer ticker.Stop()
for {
select {
case <-autoAddStopChan:
logger.Info("自动补号服务已停止", "", "auto-add")
return
case <-ticker.C:
checkAndAutoAdd()
}
}
}()
}
// StopAutoAddService 停止自动补号服务
func StopAutoAddService() {
autoAddMu.Lock()
defer autoAddMu.Unlock()
if autoAddRunning && autoAddStopChan != nil {
close(autoAddStopChan)
autoAddRunning = false
}
}
// checkAndAutoAdd 检查并自动补号
func checkAndAutoAdd() {
if database.Instance == nil {
return
}
// 读取配置
autoAddEnabled := false
if val, _ := database.Instance.GetConfig("monitor_auto_add"); val == "true" {
autoAddEnabled = true
}
if !autoAddEnabled {
return
}
// 检查最小间隔
minInterval := 300
if val, _ := database.Instance.GetConfig("monitor_min_interval"); val != "" {
if v, err := strconv.Atoi(val); err == nil {
minInterval = v
}
}
if time.Since(lastAutoAddTime).Seconds() < float64(minInterval) {
return
}
// 检查是否有任务在运行
if teamProcessState.Running {
logger.Info("已有任务在运行,跳过自动补号", "", "auto-add")
return
}
// 获取目标数量
target := 50
if val, _ := database.Instance.GetConfig("monitor_target"); val != "" {
if v, err := strconv.Atoi(val); err == nil {
target = v
}
}
// 获取当前 S2A 账号数量
current, err := getS2AAccountCount()
if err != nil {
logger.Error(fmt.Sprintf("获取 S2A 账号数量失败: %v", err), "", "auto-add")
return
}
deficit := target - current
if deficit <= 0 {
return
}
// 计算需要多少个 Team每个 Team 产生 4 个账号)
teamsNeeded := (deficit + 3) / 4 // 向上取整
// 获取可用的 Owner
owners, err := database.Instance.GetPendingOwners()
if err != nil {
logger.Error(fmt.Sprintf("获取可用 Owner 失败: %v", err), "", "auto-add")
return
}
if len(owners) == 0 {
logger.Warning("没有可用的 Owner 账号,无法自动补号", "", "auto-add")
return
}
// 限制使用的 Owner 数量
actualTeams := teamsNeeded
if actualTeams > len(owners) {
logger.Warning(fmt.Sprintf("可用 Owner 不足: 需要 %d 个, 仅有 %d 个可用", teamsNeeded, len(owners)), "", "auto-add")
actualTeams = len(owners)
}
logger.Info(fmt.Sprintf("自动补号: 当前 %d, 目标 %d, 缺少 %d, 需要 %d 个 Team, 将处理 %d 个",
current, target, deficit, teamsNeeded, actualTeams), "", "auto-add")
// 构建请求 - 直接使用 database.TeamOwner 转换
reqOwners := make([]TeamOwner, actualTeams)
for i := 0; i < actualTeams; i++ {
reqOwners[i] = TeamOwner{
Email: owners[i].Email,
Password: owners[i].Password,
Token: owners[i].Token,
AccountID: owners[i].AccountID,
}
}
req := TeamProcessRequest{
Owners: reqOwners,
MembersPerTeam: 4,
ConcurrentTeams: 2,
IncludeOwner: false,
Headless: true,
BrowserType: "rod", // 默认使用 rod
Proxy: "", // 不使用代理
}
// 初始化状态
teamProcessState.Running = true
teamProcessState.StartedAt = time.Now()
teamProcessState.TotalTeams = len(reqOwners)
teamProcessState.Completed = 0
teamProcessState.Results = make([]TeamProcessResult, 0, len(reqOwners))
lastAutoAddTime = time.Now()
// 异步执行
go runTeamProcess(req)
logger.Success(fmt.Sprintf("自动补号任务已启动: %d 个 Team", actualTeams), "", "auto-add")
}
// getS2AAccountCount 获取 S2A 当前账号数量
func getS2AAccountCount() (int, error) {
if config.Global == nil || config.Global.S2AApiBase == "" {
return 0, fmt.Errorf("S2A 配置未设置")
}
url := config.Global.S2AApiBase + "/api/v1/admin/dashboard/stats"
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return 0, err
}
req.Header.Set("Authorization", "Bearer "+config.Global.S2AAdminKey)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("S2A 返回状态: %d", resp.StatusCode)
}
var result struct {
NormalAccounts int `json:"normal_accounts"`
}
if err := decodeJSON(resp.Body, &result); err != nil {
return 0, err
}
return result.NormalAccounts, nil
}