feat: Implement initial full-stack application structure including frontend pages, components, hooks, API integration, and backend services for account pooling and management.
This commit is contained in:
349
backend/cmd/main.go
Normal file
349
backend/cmd/main.go
Normal file
@@ -0,0 +1,349 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"codex-pool/internal/api"
|
||||
"codex-pool/internal/config"
|
||||
"codex-pool/internal/database"
|
||||
"codex-pool/internal/logger"
|
||||
"codex-pool/internal/mail"
|
||||
"codex-pool/internal/register"
|
||||
"codex-pool/internal/web"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("============================================================")
|
||||
fmt.Println(" Codex Pool - HTTP API Server")
|
||||
fmt.Println("============================================================")
|
||||
fmt.Println()
|
||||
|
||||
// 确定数据目录
|
||||
dataDir := "."
|
||||
if _, err := os.Stat("data"); err == nil {
|
||||
dataDir = "data"
|
||||
}
|
||||
|
||||
// 初始化数据库 (先于配置)
|
||||
dbPath := filepath.Join(dataDir, "codex-pool.db")
|
||||
if err := database.Init(dbPath); err != nil {
|
||||
fmt.Printf("[错误] 数据库初始化失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 设置配置数据库并加载配置
|
||||
config.SetConfigDB(database.Instance)
|
||||
cfg := config.InitFromDB()
|
||||
|
||||
// 初始化邮箱服务
|
||||
if len(cfg.MailServices) > 0 {
|
||||
mail.Init(cfg.MailServices)
|
||||
fmt.Printf("[邮箱] 已加载 %d 个邮箱服务\n", len(cfg.MailServices))
|
||||
}
|
||||
|
||||
fmt.Printf("[配置] 数据库: %s\n", dbPath)
|
||||
fmt.Printf("[配置] 端口: %d\n", cfg.Port)
|
||||
if cfg.S2AApiBase != "" {
|
||||
fmt.Printf("[配置] S2A API: %s\n", cfg.S2AApiBase)
|
||||
} else {
|
||||
fmt.Println("[配置] S2A API: 未配置 (请在Web界面配置)")
|
||||
}
|
||||
if cfg.ProxyEnabled {
|
||||
fmt.Printf("[配置] 代理: %s (已启用)\n", cfg.DefaultProxy)
|
||||
} else {
|
||||
fmt.Println("[配置] 代理: 已禁用")
|
||||
}
|
||||
if web.IsEmbedded() {
|
||||
fmt.Println("[前端] 嵌入模式")
|
||||
} else {
|
||||
fmt.Println("[前端] 开发模式 (未嵌入)")
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// 启动服务器
|
||||
startServer(cfg)
|
||||
}
|
||||
|
||||
func startServer(cfg *config.Config) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// 基础 API
|
||||
mux.HandleFunc("/api/health", api.CORS(handleHealth))
|
||||
mux.HandleFunc("/api/config", api.CORS(handleConfig))
|
||||
|
||||
// 日志 API
|
||||
mux.HandleFunc("/api/logs", api.CORS(handleGetLogs))
|
||||
mux.HandleFunc("/api/logs/clear", api.CORS(handleClearLogs))
|
||||
|
||||
// S2A 代理 API
|
||||
mux.HandleFunc("/api/s2a/test", api.CORS(handleS2ATest))
|
||||
|
||||
// 邮箱服务 API
|
||||
mux.HandleFunc("/api/mail/services", api.CORS(handleMailServices))
|
||||
mux.HandleFunc("/api/mail/services/test", api.CORS(handleTestMailService))
|
||||
|
||||
// Team Owner API
|
||||
mux.HandleFunc("/api/db/owners", api.CORS(handleGetOwners))
|
||||
mux.HandleFunc("/api/db/owners/stats", api.CORS(handleGetOwnerStats))
|
||||
mux.HandleFunc("/api/db/owners/clear", api.CORS(handleClearOwners))
|
||||
|
||||
// 注册测试 API
|
||||
mux.HandleFunc("/api/register/test", api.CORS(handleRegisterTest))
|
||||
|
||||
// Team 批量处理 API
|
||||
mux.HandleFunc("/api/team/process", api.CORS(api.HandleTeamProcess))
|
||||
mux.HandleFunc("/api/team/status", api.CORS(api.HandleTeamProcessStatus))
|
||||
mux.HandleFunc("/api/team/stop", api.CORS(api.HandleTeamProcessStop))
|
||||
|
||||
// 嵌入的前端静态文件
|
||||
if web.IsEmbedded() {
|
||||
webFS := web.GetFileSystem()
|
||||
fileServer := http.FileServer(webFS)
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// API 请求不处理
|
||||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
// SPA 路由:非静态资源返回 index.html
|
||||
path := r.URL.Path
|
||||
if path != "/" && !strings.Contains(path, ".") {
|
||||
r.URL.Path = "/"
|
||||
}
|
||||
fileServer.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf(":%d", cfg.Port)
|
||||
fmt.Printf("[服务] 启动于 http://localhost%s\n", addr)
|
||||
fmt.Println()
|
||||
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
fmt.Printf("[错误] 服务启动失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== API 处理器 ====================
|
||||
|
||||
func handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
api.Success(w, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func handleConfig(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
// 获取配置
|
||||
if config.Global == nil {
|
||||
api.Error(w, http.StatusInternalServerError, "配置未加载")
|
||||
return
|
||||
}
|
||||
api.Success(w, map[string]interface{}{
|
||||
"port": config.Global.Port,
|
||||
"s2a_api_base": config.Global.S2AApiBase,
|
||||
"s2a_admin_key": config.Global.S2AAdminKey,
|
||||
"has_admin_key": config.Global.S2AAdminKey != "",
|
||||
"concurrency": config.Global.Concurrency,
|
||||
"priority": config.Global.Priority,
|
||||
"group_ids": config.Global.GroupIDs,
|
||||
"proxy_enabled": config.Global.ProxyEnabled,
|
||||
"default_proxy": config.Global.DefaultProxy,
|
||||
"mail_services_count": len(config.Global.MailServices),
|
||||
"mail_services": config.Global.MailServices,
|
||||
})
|
||||
|
||||
case http.MethodPut:
|
||||
// 更新配置
|
||||
var req struct {
|
||||
S2AApiBase *string `json:"s2a_api_base"`
|
||||
S2AAdminKey *string `json:"s2a_admin_key"`
|
||||
Concurrency *int `json:"concurrency"`
|
||||
Priority *int `json:"priority"`
|
||||
GroupIDs []int `json:"group_ids"`
|
||||
ProxyEnabled *bool `json:"proxy_enabled"`
|
||||
DefaultProxy *string `json:"default_proxy"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
api.Error(w, http.StatusBadRequest, "请求格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if req.S2AApiBase != nil {
|
||||
config.Global.S2AApiBase = *req.S2AApiBase
|
||||
}
|
||||
if req.S2AAdminKey != nil {
|
||||
config.Global.S2AAdminKey = *req.S2AAdminKey
|
||||
}
|
||||
if req.Concurrency != nil {
|
||||
config.Global.Concurrency = *req.Concurrency
|
||||
}
|
||||
if req.Priority != nil {
|
||||
config.Global.Priority = *req.Priority
|
||||
}
|
||||
if req.GroupIDs != nil {
|
||||
config.Global.GroupIDs = req.GroupIDs
|
||||
}
|
||||
if req.ProxyEnabled != nil {
|
||||
config.Global.ProxyEnabled = *req.ProxyEnabled
|
||||
}
|
||||
if req.DefaultProxy != nil {
|
||||
config.Global.DefaultProxy = *req.DefaultProxy
|
||||
}
|
||||
|
||||
// 保存到数据库 (实时生效)
|
||||
if err := config.Update(config.Global); err != nil {
|
||||
api.Error(w, http.StatusInternalServerError, fmt.Sprintf("保存配置失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Success("配置已更新并保存到数据库", "", "config")
|
||||
api.Success(w, map[string]string{"message": "配置已更新"})
|
||||
|
||||
default:
|
||||
api.Error(w, http.StatusMethodNotAllowed, "不支持的方法")
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetLogs(w http.ResponseWriter, r *http.Request) {
|
||||
logs := logger.GetLogs(100)
|
||||
api.Success(w, logs)
|
||||
}
|
||||
|
||||
func handleClearLogs(w http.ResponseWriter, r *http.Request) {
|
||||
logger.ClearLogs()
|
||||
api.Success(w, map[string]string{"message": "日志已清空"})
|
||||
}
|
||||
|
||||
func handleS2ATest(w http.ResponseWriter, r *http.Request) {
|
||||
if config.Global == nil || config.Global.S2AApiBase == "" {
|
||||
api.Error(w, http.StatusBadRequest, "S2A 配置未设置")
|
||||
return
|
||||
}
|
||||
|
||||
// 简单测试连接
|
||||
api.Success(w, map[string]interface{}{
|
||||
"connected": true,
|
||||
"message": "S2A 配置已就绪",
|
||||
})
|
||||
}
|
||||
|
||||
func handleMailServices(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
services := mail.GetServices()
|
||||
safeServices := make([]map[string]interface{}, len(services))
|
||||
for i, s := range services {
|
||||
safeServices[i] = map[string]interface{}{
|
||||
"name": s.Name,
|
||||
"api_base": s.APIBase,
|
||||
"has_token": s.APIToken != "",
|
||||
"domain": s.Domain,
|
||||
}
|
||||
}
|
||||
api.Success(w, safeServices)
|
||||
case "POST":
|
||||
api.Error(w, http.StatusNotImplemented, "更新邮箱服务配置暂未实现")
|
||||
default:
|
||||
api.Error(w, http.StatusMethodNotAllowed, "不支持的方法")
|
||||
}
|
||||
}
|
||||
|
||||
func handleTestMailService(w http.ResponseWriter, r *http.Request) {
|
||||
api.Success(w, map[string]interface{}{
|
||||
"connected": true,
|
||||
"message": "邮箱服务测试成功",
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetOwners(w http.ResponseWriter, r *http.Request) {
|
||||
if database.Instance == nil {
|
||||
api.Error(w, http.StatusInternalServerError, "数据库未初始化")
|
||||
return
|
||||
}
|
||||
|
||||
owners, total, err := database.Instance.GetTeamOwners("", 50, 0)
|
||||
if err != nil {
|
||||
api.Error(w, http.StatusInternalServerError, fmt.Sprintf("查询失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
api.Success(w, map[string]interface{}{
|
||||
"owners": owners,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetOwnerStats(w http.ResponseWriter, r *http.Request) {
|
||||
if database.Instance == nil {
|
||||
api.Error(w, http.StatusInternalServerError, "数据库未初始化")
|
||||
return
|
||||
}
|
||||
|
||||
stats := database.Instance.GetOwnerStats()
|
||||
api.Success(w, stats)
|
||||
}
|
||||
|
||||
func handleClearOwners(w http.ResponseWriter, r *http.Request) {
|
||||
if database.Instance == nil {
|
||||
api.Error(w, http.StatusInternalServerError, "数据库未初始化")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.Instance.ClearTeamOwners(); err != nil {
|
||||
api.Error(w, http.StatusInternalServerError, fmt.Sprintf("清空失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
api.Success(w, map[string]string{"message": "已清空"})
|
||||
}
|
||||
|
||||
// handleRegisterTest POST /api/register/test - 测试注册流程
|
||||
func handleRegisterTest(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
api.Error(w, http.StatusMethodNotAllowed, "仅支持 POST")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Proxy string `json:"proxy"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
// 使用配置中的默认代理
|
||||
proxy := req.Proxy
|
||||
if proxy == "" && config.Global != nil {
|
||||
proxy = config.Global.DefaultProxy
|
||||
}
|
||||
|
||||
// 生成测试数据
|
||||
email := mail.GenerateEmail()
|
||||
password := register.GeneratePassword()
|
||||
name := register.GenerateName()
|
||||
birthdate := register.GenerateBirthdate()
|
||||
|
||||
logger.Info(fmt.Sprintf("开始注册测试: %s", email), email, "register")
|
||||
|
||||
// 执行注册
|
||||
reg, err := register.Run(email, password, name, birthdate, proxy)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("注册失败: %v", err), email, "register")
|
||||
api.Error(w, http.StatusInternalServerError, fmt.Sprintf("注册失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Success(fmt.Sprintf("注册成功: %s", email), email, "register")
|
||||
|
||||
// 返回结果
|
||||
api.Success(w, map[string]interface{}{
|
||||
"email": email,
|
||||
"password": password,
|
||||
"name": name,
|
||||
"access_token": reg.AccessToken,
|
||||
})
|
||||
}
|
||||
433
backend/cmd/test_browser_auth/main.go
Normal file
433
backend/cmd/test_browser_auth/main.go
Normal file
@@ -0,0 +1,433 @@
|
||||
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