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
}
// 计算需要多少个 Team每个 Team 产生 4 个账号)
teamsNeeded := (deficit + 3) / 4 // 向上取整
// 读取每 Team 成员数配置
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
owners, err := database.Instance.GetPendingOwners()
@@ -164,13 +172,14 @@ func checkAndAutoAdd() {
}
}
// 读取代理配置
// 读取代理配置 - 支持代理池模式
proxyURL := ""
replenishUseProxy := false
if val, _ := database.Instance.GetConfig("monitor_replenish_use_proxy"); val == "true" {
replenishUseProxy = true
}
if replenishUseProxy {
// 使用全局代理配置(支持 pool:random, pool:id:N 等格式)
proxyURL = config.Global.DefaultProxy
}
@@ -180,10 +189,27 @@ func checkAndAutoAdd() {
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{
Owners: reqOwners,
MembersPerTeam: 4,
ConcurrentTeams: 2,
MembersPerTeam: membersPerTeam,
ConcurrentTeams: concurrentTeams,
S2AConcurrency: s2aConcurrency,
IncludeOwner: false,
Headless: true,
BrowserType: browserType,
@@ -192,7 +218,13 @@ func checkAndAutoAdd() {
// 输出代理使用状态日志
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 {
logger.Info("自动补号: 未使用代理", "", "auto-add")
}
@@ -209,7 +241,8 @@ func checkAndAutoAdd() {
// 异步执行
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 当前账号数量

View File

@@ -11,14 +11,20 @@ import (
// MonitorSettings 监控设置
type MonitorSettings struct {
Target int `json:"target"`
AutoAdd bool `json:"auto_add"`
MinInterval int `json:"min_interval"`
CheckInterval int `json:"check_interval"` // 自动补号检查间隔(秒)
PollingEnabled bool `json:"polling_enabled"`
PollingInterval int `json:"polling_interval"`
ReplenishUseProxy bool `json:"replenish_use_proxy"` // 补号时使用代理
BrowserType string `json:"browser_type"` // 授权浏览器引擎: chromedp 或 rod
Target int `json:"target"`
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"`
CheckInterval int `json:"check_interval"` // 自动补号检查间隔(秒)
PollingEnabled bool `json:"polling_enabled"`
PollingInterval int `json:"polling_interval"`
ReplenishUseProxy bool `json:"replenish_use_proxy"` // 补号时使用代理
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 获取监控设置
@@ -34,14 +40,20 @@ func HandleGetMonitorSettings(w http.ResponseWriter, r *http.Request) {
}
settings := MonitorSettings{
Target: 50,
AutoAdd: false,
MinInterval: 300,
CheckInterval: 60,
PollingEnabled: false,
PollingInterval: 60,
ReplenishUseProxy: false,
BrowserType: "chromedp", // 默认使用 chromedp
Target: 50,
AutoAdd: false,
AutoRegister: false,
AutoRegConcurrency: 2,
AutoRegUseProxy: false,
MinInterval: 300,
CheckInterval: 60,
PollingEnabled: false,
PollingInterval: 60,
ReplenishUseProxy: false,
BrowserType: "chromedp", // 默认使用 chromedp
MembersPerTeam: 4, // 默认每 Team 4 个成员
ConcurrentTeams: 2, // 默认并发 2 个 Team
S2AConcurrency: 2, // 默认入库并发 2
}
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" {
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 v, err := strconv.Atoi(val); err == nil {
settings.MinInterval = v
@@ -76,6 +99,21 @@ func HandleGetMonitorSettings(w http.ResponseWriter, r *http.Request) {
if val, _ := database.Instance.GetConfig("monitor_browser_type"); 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)
}
@@ -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 {
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 {
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 {
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 {
errMsg := "保存监控设置部分失败: " + saveErrors[0]

View File

@@ -148,11 +148,11 @@ func HandleBanCheckSettings(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
settings := map[string]interface{}{
"enabled": false,
"interval": 1800, // 检查服务间隔(秒)
"check_hours": 24, // 多少小时后重新检查
"last_check": lastBanCheckTime,
"task_state": banCheckTaskState,
"enabled": false,
"interval": 1800, // 检查服务间隔(秒)
"check_hours": 24, // 多少小时后重新检查
"last_check": lastBanCheckTime,
"task_state": banCheckTaskState,
"service_running": banCheckRunning,
}
@@ -225,9 +225,9 @@ func HandleManualBanCheck(w http.ResponseWriter, r *http.Request) {
}
var req struct {
IDs []int64 `json:"ids"` // 指定检查的母号 ID为空则检查所有有效母号
Concurrency int `json:"concurrency"` // 并发数
ForceCheck bool `json:"force_check"` // 是否强制检查(忽略上次检查时间)
IDs []int64 `json:"ids"` // 指定检查的母号 ID为空则检查所有有效母号
Concurrency int `json:"concurrency"` // 并发数
ForceCheck bool `json:"force_check"` // 是否强制检查(忽略上次检查时间)
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
// 允许空 body
@@ -315,10 +315,10 @@ func runBanCheckTask(owners []database.TeamOwner, concurrency int) {
taskChan := make(chan database.TeamOwner, len(owners))
var wg sync.WaitGroup
// 获取代理配置
proxy := ""
// 获取代理配置模式
proxyStr := ""
if config.Global != nil {
proxy = config.Global.GetProxy()
proxyStr = config.Global.GetProxy()
}
// 启动 worker
@@ -327,7 +327,9 @@ func runBanCheckTask(owners []database.TeamOwner, concurrency int) {
go func() {
defer wg.Done()
for owner := range taskChan {
result := checkSingleOwnerBan(owner, proxy)
// 每次循环重新解析代理,支持轮询
resolvedProxy := database.Instance.ResolveProxy(proxyStr)
result := checkSingleOwnerBan(owner, resolvedProxy)
// 更新计数
atomic.AddInt32(&banCheckTaskState.Checked, 1)

View File

@@ -155,12 +155,15 @@ func HandleTeamProcess(w http.ResponseWriter, r *http.Request) {
req.Proxy = config.Global.GetProxy() // 使用新的代理获取方法
}
if req.Proxy != "" {
normalized, err := proxyutil.Normalize(req.Proxy)
if err != nil {
Error(w, http.StatusBadRequest, fmt.Sprintf("代理格式错误: %v", err))
return
// 如果是模式字符串pool:random, pool:id:N跳过 Normalize
if !strings.HasPrefix(req.Proxy, "pool:") && req.Proxy != "[RANDOM]" {
normalized, err := proxyutil.Normalize(req.Proxy)
if err != nil {
Error(w, http.StatusBadRequest, fmt.Sprintf("代理格式错误: %v", err))
return
}
req.Proxy = normalized
}
req.Proxy = normalized
}
// 初始化状态
@@ -466,7 +469,9 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
// Step 1: 获取 Team ID优先使用已存储的 account_id
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 != "" {
// 直接使用数据库中存储的 account_id
@@ -581,7 +586,7 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
// 入库单个成员的函数
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()
// 获取入库信号量
@@ -598,15 +603,21 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
logger.Warning(fmt.Sprintf("%s 入库重试 (第%d次)", memberLogPrefix, attempt+1), memberEmail, "team")
}
// 创建日志回调
// 创建日志回调 - 只显示关键步骤
authLogger := auth.NewAuthLogger(memberEmail, logPrefix, memberIdx+1, func(entry auth.AuthLogEntry) {
if entry.IsError {
logger.Error(fmt.Sprintf("%s %s", memberLogPrefix, entry.Message), memberEmail, "team")
} else {
// 只显示关键步骤:提交邮箱、验证密码、选择工作区、授权成功
switch entry.Step {
case auth.StepNavigate, auth.StepInputEmail, auth.StepInputPassword,
auth.StepComplete, auth.StepConsent, auth.StepSelectWorkspace:
logger.Info(fmt.Sprintf("%s %s", memberLogPrefix, entry.Message), memberEmail, "team")
case auth.StepInputEmail:
logger.Info(fmt.Sprintf("%s 提交邮箱: %s", memberLogPrefix, memberEmail), 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
if config.Global.AuthMethod == "api" {
proxyToUse := req.Proxy
if poolProxy, poolErr := database.Instance.GetRandomCodexProxy(); poolErr == nil && poolProxy != "" {
proxyToUse = poolProxy
logger.Info(fmt.Sprintf("%s 使用代理池: %s", memberLogPrefix, getProxyDisplay(poolProxy)), memberEmail, "team")
proxyToUse := database.Instance.ResolveProxy(req.Proxy)
if proxyToUse != req.Proxy && proxyToUse != "" {
logger.Info(fmt.Sprintf("%s 使用解析代理: %s", memberLogPrefix, getProxyDisplay(proxyToUse)), memberEmail, "team")
}
code, err = auth.CompleteWithCodexAPI(memberEmail, memberPassword, teamID, s2aResp.Data.AuthURL, s2aResp.Data.SessionID, proxyToUse, authLogger)
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 {
name := register.GenerateName()
birthdate := register.GenerateBirthdate()
memberLogPrefix := fmt.Sprintf("%s [成员 %d]", logPrefix, memberIdx+1)
memberLogPrefix := fmt.Sprintf("%s [Member %d]", logPrefix, memberIdx+1)
regStartTime := time.Now()
for attempt := 0; attempt < 2; attempt++ { // 最多尝试2次首次+1次重试
@@ -764,7 +774,7 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
}
email := mail.GenerateEmail()
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)
}(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",
config.Count, config.Concurrency, config.Proxy))
config.Count, config.Concurrency, resolvedProxy))
// 创建上下文用于取消
ctx, cancel := context.WithCancel(context.Background())
@@ -360,8 +366,8 @@ func runTeamRegProcess(config TeamRegConfig) {
fmt.Fprintf(stdin, "%d\n", config.Concurrency)
time.Sleep(200 * time.Millisecond)
addTeamRegLog(fmt.Sprintf("[输入] 代理地址: %s", config.Proxy))
fmt.Fprintf(stdin, "%s\n", config.Proxy)
addTeamRegLog(fmt.Sprintf("[输入] 代理地址: %s", resolvedProxy))
fmt.Fprintf(stdin, "%s\n", resolvedProxy)
// 等待进程完成
err = cmd.Wait()

View File

@@ -379,7 +379,8 @@ func fetchAccountID(token string) (string, error) {
proxy = cfg.GetProxy()
}
tlsClient, err := client.New(proxy)
resolvedProxy := database.Instance.ResolveProxy(proxy)
tlsClient, err := client.New(resolvedProxy)
if err != nil {
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") {
// 5. 验证密码
c.logStep(StepInputPassword, "验证密码...")
if !c.callSentinelReq("authorize_continue__auto") {
return "", fmt.Errorf("Sentinel 请求失败")
}
@@ -493,7 +492,6 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
verifyHeaders["OpenAI-Sentinel-Token"] = c.getSentinelHeader("password_verify")
passwordPayload := map[string]string{
"username": c.email,
"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))]))
return "", fmt.Errorf("密码验证失败: %d", resp.StatusCode)
}
c.logStep(StepInputPassword, "密码验证成功")
// 解析密码验证响应
json.Unmarshal(body, &data)
@@ -520,8 +517,6 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
c.logError(StepInputPassword, "账号需要邮箱验证,无法继续 Codex 授权流程")
return "", fmt.Errorf("账号需要邮箱验证,请使用浏览器模式或等待账号状态更新")
}
} else {
c.logStep(StepInputPassword, "跳过密码验证步骤 (服务器未要求)")
}
// 6. 选择工作区

View File

@@ -4,6 +4,8 @@ import (
"database/sql"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
@@ -941,10 +943,44 @@ func (d *DB) DeleteCodexProxy(id int64) error {
return err
}
// ClearCodexProxies 清空所有代理
func (d *DB) ClearCodexProxies() error {
_, err := d.db.Exec("DELETE FROM codex_auth_proxies")
return err
// GetCodexProxyByID 获取指定 ID 的代理地址
func (d *DB) GetCodexProxyByID(id int64) (string, error) {
var proxyURL string
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 获取代理统计