feat: Add a new cleaner page for managing and automatically cleaning error S2A accounts, supported by new backend services for logging, authentication, and client operations.
This commit is contained in:
@@ -332,7 +332,7 @@ func handleClearLogsByModule(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleQueryLogs GET /api/logs/query?module=cleaner&page=1&page_size=5
|
||||
// handleQueryLogs GET /api/logs/query?module=cleaner&page=1&page_size=5&level=success
|
||||
func handleQueryLogs(w http.ResponseWriter, r *http.Request) {
|
||||
module := r.URL.Query().Get("module")
|
||||
if module == "" {
|
||||
@@ -342,6 +342,8 @@ func handleQueryLogs(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
page := 1
|
||||
pageSize := 5
|
||||
level := r.URL.Query().Get("level") // 可选的日志级别过滤
|
||||
|
||||
if v := r.URL.Query().Get("page"); v != "" {
|
||||
if p, err := strconv.Atoi(v); err == nil && p > 0 {
|
||||
page = p
|
||||
@@ -353,7 +355,14 @@ func handleQueryLogs(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
entries, total := logger.GetLogsByModule(module, page, pageSize)
|
||||
// 如果指定了 level,使用带级别过滤的函数
|
||||
var entries []logger.LogEntry
|
||||
var total int
|
||||
if level != "" {
|
||||
entries, total = logger.GetLogsByModuleAndLevel(module, level, page, pageSize)
|
||||
} else {
|
||||
entries, total = logger.GetLogsByModule(module, page, pageSize)
|
||||
}
|
||||
totalPages := (total + pageSize - 1) / pageSize
|
||||
|
||||
api.Success(w, map[string]interface{}{
|
||||
|
||||
@@ -1,433 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"codex-pool/internal/auth"
|
||||
"codex-pool/internal/config"
|
||||
"codex-pool/internal/invite"
|
||||
"codex-pool/internal/mail"
|
||||
"codex-pool/internal/register"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
Account string `json:"account"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type MemberAccount struct {
|
||||
Email string
|
||||
Password string
|
||||
Success bool
|
||||
}
|
||||
|
||||
const (
|
||||
MembersPerTeam = 4 // 每个 team 注册的成员数
|
||||
NumTeams = 2 // 并发运行的 team 数量
|
||||
)
|
||||
|
||||
// ANSI 颜色码
|
||||
const (
|
||||
ColorReset = "\033[0m"
|
||||
ColorRed = "\033[31m"
|
||||
ColorGreen = "\033[32m"
|
||||
ColorYellow = "\033[33m"
|
||||
ColorBlue = "\033[34m"
|
||||
ColorMagenta = "\033[35m"
|
||||
ColorCyan = "\033[36m"
|
||||
ColorWhite = "\033[37m"
|
||||
ColorBold = "\033[1m"
|
||||
)
|
||||
|
||||
// Team 颜色
|
||||
var teamColors = []string{
|
||||
ColorCyan, // Team 1
|
||||
ColorMagenta, // Team 2
|
||||
ColorYellow, // Team 3
|
||||
ColorBlue, // Team 4
|
||||
}
|
||||
|
||||
// TeamLogger 带颜色的Team日志
|
||||
type TeamLogger struct {
|
||||
prefix string
|
||||
color string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewTeamLogger(teamIdx int) *TeamLogger {
|
||||
color := teamColors[teamIdx%len(teamColors)]
|
||||
return &TeamLogger{
|
||||
prefix: fmt.Sprintf("[Team %d]", teamIdx+1),
|
||||
color: color,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *TeamLogger) Log(format string, args ...interface{}) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s%s%s %s\n", l.color, l.prefix, ColorReset, msg)
|
||||
}
|
||||
|
||||
func (l *TeamLogger) Success(format string, args ...interface{}) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s%s%s %s✓%s %s\n", l.color, l.prefix, ColorReset, ColorGreen, ColorReset, msg)
|
||||
}
|
||||
|
||||
func (l *TeamLogger) Error(format string, args ...interface{}) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s%s%s %s✗%s %s\n", l.color, l.prefix, ColorReset, ColorRed, ColorReset, msg)
|
||||
}
|
||||
|
||||
func (l *TeamLogger) Info(format string, args ...interface{}) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s%s%s %s→%s %s\n", l.color, l.prefix, ColorReset, ColorYellow, ColorReset, msg)
|
||||
}
|
||||
|
||||
// Highlight 整行绿色高亮(用于重要成功信息)
|
||||
func (l *TeamLogger) Highlight(format string, args ...interface{}) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s%s %s✓ %s%s\n", ColorGreen, l.prefix, ColorBold, msg, ColorReset)
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Printf("%s%s=================================================================%s\n", ColorBold, ColorWhite, ColorReset)
|
||||
fmt.Printf("%s Multi-Team Concurrent Test (Chromedp)%s\n", ColorBold, ColorReset)
|
||||
fmt.Printf(" - %d Teams running concurrently\n", NumTeams)
|
||||
fmt.Printf(" - %d Members per team\n", MembersPerTeam)
|
||||
fmt.Printf("%s%s=================================================================%s\n", ColorBold, ColorWhite, ColorReset)
|
||||
fmt.Println()
|
||||
|
||||
// 加载配置
|
||||
configPath := config.FindPath()
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Printf("%s[Error]%s Failed to load config: %v\n", ColorRed, ColorReset, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 初始化邮箱服务
|
||||
if len(cfg.MailServices) > 0 {
|
||||
mail.Init(cfg.MailServices)
|
||||
}
|
||||
|
||||
// 加载账号
|
||||
accountsFile := "accounts-3-20260130-052841.json"
|
||||
data, err := os.ReadFile(accountsFile)
|
||||
if err != nil {
|
||||
fmt.Printf("%s[Error]%s Failed to read accounts file: %v\n", ColorRed, ColorReset, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var accounts []Account
|
||||
if err := json.Unmarshal(data, &accounts); err != nil {
|
||||
fmt.Printf("%s[Error]%s Failed to parse accounts file: %v\n", ColorRed, ColorReset, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(accounts) < NumTeams {
|
||||
fmt.Printf("%s[Error]%s Need at least %d owner accounts, got %d\n", ColorRed, ColorReset, NumTeams, len(accounts))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
proxy := cfg.DefaultProxy
|
||||
if proxy == "" {
|
||||
proxy = "http://127.0.0.1:7890"
|
||||
}
|
||||
fmt.Printf("[Proxy] %s\n", proxy)
|
||||
fmt.Println()
|
||||
|
||||
// 显示 Owner 列表
|
||||
fmt.Printf("%s========================================%s\n", ColorBold, ColorReset)
|
||||
fmt.Printf("%s[Owners]%s\n", ColorBold, ColorReset)
|
||||
fmt.Printf("%s========================================%s\n", ColorBold, ColorReset)
|
||||
for i := 0; i < NumTeams; i++ {
|
||||
color := teamColors[i%len(teamColors)]
|
||||
fmt.Printf(" %sTeam %d:%s %s\n", color, i+1, ColorReset, accounts[i].Account)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// 并发运行多个 Team
|
||||
var wg sync.WaitGroup
|
||||
var totalRegistered int32
|
||||
var totalS2A int32
|
||||
startTime := time.Now()
|
||||
|
||||
for teamIdx := 0; teamIdx < NumTeams; teamIdx++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
registered, s2a := runTeam(idx, accounts[idx], cfg, proxy)
|
||||
atomic.AddInt32(&totalRegistered, int32(registered))
|
||||
atomic.AddInt32(&totalS2A, int32(s2a))
|
||||
}(teamIdx)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
totalDuration := time.Since(startTime)
|
||||
|
||||
// 总结
|
||||
fmt.Println()
|
||||
fmt.Printf("%s%s=================================================================%s\n", ColorBold, ColorWhite, ColorReset)
|
||||
fmt.Printf("%s All Teams Complete%s\n", ColorBold, ColorReset)
|
||||
fmt.Printf("%s=================================================================%s\n", ColorBold, ColorReset)
|
||||
fmt.Printf(" Total Registered: %s%d/%d%s\n", ColorGreen, totalRegistered, NumTeams*MembersPerTeam, ColorReset)
|
||||
fmt.Printf(" Total Added to S2A: %s%d%s\n", ColorGreen, totalS2A, ColorReset)
|
||||
fmt.Printf(" Total Duration: %v\n", totalDuration)
|
||||
fmt.Printf("%s=================================================================%s\n", ColorBold, ColorReset)
|
||||
}
|
||||
|
||||
// runTeam 运行单个 Team 的流程
|
||||
func runTeam(teamIdx int, owner Account, cfg *config.Config, proxy string) (registered, s2a int) {
|
||||
log := NewTeamLogger(teamIdx)
|
||||
|
||||
log.Log("Starting with owner: %s", owner.Account)
|
||||
|
||||
// Step 1: 获取 Team ID
|
||||
log.Info("Fetching Team ID...")
|
||||
inviter := invite.NewWithProxy(owner.Token, proxy)
|
||||
teamID, err := inviter.GetAccountID()
|
||||
if err != nil {
|
||||
log.Error("Failed to get Team ID: %v", err)
|
||||
return 0, 0
|
||||
}
|
||||
log.Success("Team ID: %s", teamID)
|
||||
|
||||
// Step 2: 生成成员邮箱
|
||||
log.Info("Generating %d member emails...", MembersPerTeam)
|
||||
children := make([]MemberAccount, MembersPerTeam)
|
||||
for i := 0; i < MembersPerTeam; i++ {
|
||||
children[i].Email = mail.GenerateEmail()
|
||||
children[i].Password = register.GeneratePassword()
|
||||
log.Log("[Member %d] Email: %s", i+1, children[i].Email)
|
||||
}
|
||||
|
||||
// 批量发送邀请
|
||||
log.Info("Sending invites...")
|
||||
inviteEmails := make([]string, MembersPerTeam)
|
||||
for i, c := range children {
|
||||
inviteEmails[i] = c.Email
|
||||
}
|
||||
if err := inviter.SendInvites(inviteEmails); err != nil {
|
||||
log.Error("Failed to send invites: %v", err)
|
||||
return 0, 0
|
||||
}
|
||||
log.Success("Sent %d invite(s)", len(inviteEmails))
|
||||
|
||||
// Step 3: 并发注册成员
|
||||
log.Info("Starting member registration...")
|
||||
var memberWg sync.WaitGroup
|
||||
var successCount int32
|
||||
memberMutex := sync.Mutex{}
|
||||
|
||||
for i := range children {
|
||||
memberWg.Add(1)
|
||||
go func(memberIdx int) {
|
||||
defer memberWg.Done()
|
||||
|
||||
memberMutex.Lock()
|
||||
email := children[memberIdx].Email
|
||||
password := children[memberIdx].Password
|
||||
memberMutex.Unlock()
|
||||
|
||||
name := register.GenerateName()
|
||||
birthdate := register.GenerateBirthdate()
|
||||
|
||||
// 最多重试3次
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
if attempt > 0 {
|
||||
email = mail.GenerateEmail()
|
||||
password = register.GeneratePassword()
|
||||
log.Log("[Member %d] Retry %d - New email: %s", memberIdx+1, attempt, email)
|
||||
|
||||
// 发送新邀请
|
||||
if err := inviter.SendInvites([]string{email}); err != nil {
|
||||
log.Error("[Member %d] Failed to send retry invite: %v", memberIdx+1, err)
|
||||
continue
|
||||
}
|
||||
log.Success("[Member %d] Sent retry invite", memberIdx+1)
|
||||
}
|
||||
|
||||
// 详细注册流程
|
||||
if err := registerMemberDetailed(log, memberIdx+1, email, password, name, birthdate, proxy); err != nil {
|
||||
if strings.Contains(err.Error(), "验证码") {
|
||||
log.Error("[Member %d] OTP timeout, will retry...", memberIdx+1)
|
||||
continue
|
||||
}
|
||||
log.Error("[Member %d] Registration failed: %v", memberIdx+1, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 成功
|
||||
memberMutex.Lock()
|
||||
children[memberIdx].Email = email
|
||||
children[memberIdx].Password = password
|
||||
children[memberIdx].Success = true
|
||||
memberMutex.Unlock()
|
||||
|
||||
atomic.AddInt32(&successCount, 1)
|
||||
log.Success("[Member %d] Registration complete!", memberIdx+1)
|
||||
return
|
||||
}
|
||||
|
||||
log.Error("[Member %d] Failed after 3 retries", memberIdx+1)
|
||||
}(i)
|
||||
}
|
||||
|
||||
memberWg.Wait()
|
||||
registered = int(successCount)
|
||||
log.Success("Registration phase complete: %d/%d", registered, MembersPerTeam)
|
||||
|
||||
// 收集成功的成员
|
||||
var registeredChildren []MemberAccount
|
||||
for _, c := range children {
|
||||
if c.Success {
|
||||
registeredChildren = append(registeredChildren, c)
|
||||
}
|
||||
}
|
||||
|
||||
if len(registeredChildren) == 0 {
|
||||
log.Error("No members registered")
|
||||
return registered, 0
|
||||
}
|
||||
|
||||
// Step 4: 串行入库
|
||||
log.Info("Starting S2A authorization...")
|
||||
|
||||
for i, child := range registeredChildren {
|
||||
log.Log("[Member %d] Getting S2A auth URL...", i+1)
|
||||
s2aResp, err := auth.GenerateS2AAuthURL(cfg.S2AApiBase, cfg.S2AAdminKey, cfg.ProxyID)
|
||||
if err != nil {
|
||||
log.Error("[Member %d] Auth URL failed: %v", i+1, err)
|
||||
continue
|
||||
}
|
||||
log.Success("[Member %d] Got auth URL", i+1)
|
||||
|
||||
log.Log("[Member %d] Running browser automation (Chromedp)...", i+1)
|
||||
code, err := auth.CompleteWithChromedp(s2aResp.Data.AuthURL, child.Email, child.Password, teamID, true, proxy)
|
||||
if err != nil {
|
||||
log.Error("[Member %d] Browser auth failed: %v", i+1, err)
|
||||
continue
|
||||
}
|
||||
log.Success("[Member %d] Browser auth complete", i+1)
|
||||
|
||||
log.Log("[Member %d] Submitting to S2A...", i+1)
|
||||
result, err := auth.SubmitS2AOAuth(
|
||||
cfg.S2AApiBase,
|
||||
cfg.S2AAdminKey,
|
||||
s2aResp.Data.SessionID,
|
||||
code,
|
||||
child.Email,
|
||||
cfg.Concurrency,
|
||||
cfg.Priority,
|
||||
cfg.GroupIDs,
|
||||
cfg.ProxyID,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error("[Member %d] S2A submit failed: %v", i+1, err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Highlight("[Member %d] Added to S2A! ID=%d, Status=%s", i+1, result.Data.ID, result.Data.Status)
|
||||
s2a++
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
log.Success("Team complete: %d registered, %d in S2A", registered, s2a)
|
||||
return registered, s2a
|
||||
}
|
||||
|
||||
// registerMemberDetailed 详细的注册流程,带日志
|
||||
func registerMemberDetailed(log *TeamLogger, memberNum int, email, password, name, birthdate, proxy string) error {
|
||||
prefix := fmt.Sprintf("[Member %d]", memberNum)
|
||||
|
||||
log.Log("%s Creating TLS client...", prefix)
|
||||
reg, err := register.New(proxy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Log("%s Initializing session...", prefix)
|
||||
if err := reg.InitSession(); err != nil {
|
||||
return fmt.Errorf("初始化失败: %v", err)
|
||||
}
|
||||
log.Success("%s Session initialized", prefix)
|
||||
|
||||
log.Log("%s Getting authorize URL...", prefix)
|
||||
if err := reg.GetAuthorizeURL(email); err != nil {
|
||||
return fmt.Errorf("获取授权URL失败: %v", err)
|
||||
}
|
||||
log.Success("%s Got authorize URL", prefix)
|
||||
|
||||
log.Log("%s Starting authorize flow...", prefix)
|
||||
if err := reg.StartAuthorize(); err != nil {
|
||||
return fmt.Errorf("启动授权失败: %v", err)
|
||||
}
|
||||
log.Success("%s Authorize flow started", prefix)
|
||||
|
||||
log.Log("%s Registering account...", prefix)
|
||||
if err := reg.Register(email, password); err != nil {
|
||||
return fmt.Errorf("注册失败: %v", err)
|
||||
}
|
||||
log.Success("%s Account registered", prefix)
|
||||
|
||||
log.Log("%s Sending verification email...", prefix)
|
||||
if err := reg.SendVerificationEmail(); err != nil {
|
||||
return fmt.Errorf("发送邮件失败: %v", err)
|
||||
}
|
||||
log.Success("%s Verification email sent", prefix)
|
||||
|
||||
log.Log("%s Waiting for OTP code (5s timeout)...", prefix)
|
||||
otpCode, err := mail.GetVerificationCode(email, 5*time.Second)
|
||||
if err != nil {
|
||||
log.Log("%s OTP not received in 5s, waiting 15s more...", prefix)
|
||||
otpCode, err = mail.GetVerificationCode(email, 15*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("验证码获取超时")
|
||||
}
|
||||
}
|
||||
log.Success("%s Got OTP: %s", prefix, otpCode)
|
||||
|
||||
log.Log("%s Validating OTP...", prefix)
|
||||
if err := reg.ValidateOTP(otpCode); err != nil {
|
||||
return fmt.Errorf("OTP验证失败: %v", err)
|
||||
}
|
||||
log.Success("%s OTP validated", prefix)
|
||||
|
||||
log.Log("%s Creating account (name=%s, birthdate=%s)...", prefix, name, birthdate)
|
||||
if err := reg.CreateAccount(name, birthdate); err != nil {
|
||||
return fmt.Errorf("创建账户失败: %v", err)
|
||||
}
|
||||
log.Success("%s Account created", prefix)
|
||||
|
||||
log.Log("%s Getting session token...", prefix)
|
||||
_ = reg.GetSessionToken()
|
||||
if reg.AccessToken != "" {
|
||||
log.Success("%s Got access token: %s...", prefix, truncate(reg.AccessToken, 30))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen]
|
||||
}
|
||||
Reference in New Issue
Block a user