Files
codexautopool/backend/internal/api/auto_add.go

218 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
// 设置认证头(与代理保持一致)
adminKey := config.Global.S2AAdminKey
req.Header.Set("Authorization", "Bearer "+adminKey)
req.Header.Set("X-API-Key", adminKey)
req.Header.Set("X-Admin-Key", adminKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
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
}