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:
54
backend/internal/api/http.go
Normal file
54
backend/internal/api/http.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"codex-pool/internal/config"
|
||||
)
|
||||
|
||||
// Result 统一 API 响应
|
||||
type Result struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// JSON 发送 JSON 响应
|
||||
func JSON(w http.ResponseWriter, code int, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
// Success 发送成功响应
|
||||
func Success(w http.ResponseWriter, data interface{}) {
|
||||
JSON(w, http.StatusOK, Result{Code: 0, Data: data})
|
||||
}
|
||||
|
||||
// Error 发送错误响应
|
||||
func Error(w http.ResponseWriter, httpCode int, message string) {
|
||||
JSON(w, httpCode, Result{Code: -1, Message: message})
|
||||
}
|
||||
|
||||
// CORS 跨域中间件
|
||||
func CORS(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := "*"
|
||||
if config.Global != nil && config.Global.CorsOrigin != "" {
|
||||
origin = config.Global.CorsOrigin
|
||||
}
|
||||
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Api-Key")
|
||||
w.Header().Set("Access-Control-Max-Age", "86400")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
423
backend/internal/api/team_process.go
Normal file
423
backend/internal/api/team_process.go
Normal file
@@ -0,0 +1,423 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"codex-pool/internal/auth"
|
||||
"codex-pool/internal/config"
|
||||
"codex-pool/internal/invite"
|
||||
"codex-pool/internal/logger"
|
||||
"codex-pool/internal/mail"
|
||||
"codex-pool/internal/register"
|
||||
)
|
||||
|
||||
// TeamProcessRequest 团队处理请求
|
||||
type TeamProcessRequest struct {
|
||||
// Owner 账号列表
|
||||
Owners []struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
} `json:"owners"`
|
||||
// 配置
|
||||
MembersPerTeam int `json:"members_per_team"` // 每个 Team 的成员数
|
||||
ConcurrentTeams int `json:"concurrent_teams"` // 并发 Team 数量
|
||||
BrowserType string `json:"browser_type"` // "chromedp" 或 "rod"
|
||||
Headless bool `json:"headless"` // 是否无头模式
|
||||
Proxy string `json:"proxy"` // 代理设置
|
||||
}
|
||||
|
||||
// TeamProcessResult 团队处理结果
|
||||
type TeamProcessResult struct {
|
||||
TeamIndex int `json:"team_index"`
|
||||
OwnerEmail string `json:"owner_email"`
|
||||
TeamID string `json:"team_id"`
|
||||
Registered int `json:"registered"`
|
||||
AddedToS2A int `json:"added_to_s2a"`
|
||||
MemberEmails []string `json:"member_emails"`
|
||||
Errors []string `json:"errors"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
}
|
||||
|
||||
// TeamProcessState 处理状态
|
||||
type TeamProcessState struct {
|
||||
Running bool `json:"running"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
TotalTeams int `json:"total_teams"`
|
||||
Completed int32 `json:"completed"`
|
||||
Results []TeamProcessResult `json:"results"`
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var teamProcessState = &TeamProcessState{}
|
||||
|
||||
// HandleTeamProcess POST /api/team/process - 启动 Team 批量处理
|
||||
func HandleTeamProcess(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
Error(w, http.StatusMethodNotAllowed, "仅支持 POST")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否正在运行
|
||||
if teamProcessState.Running {
|
||||
Error(w, http.StatusConflict, "已有任务正在运行")
|
||||
return
|
||||
}
|
||||
|
||||
var req TeamProcessRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, http.StatusBadRequest, "请求格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if len(req.Owners) == 0 {
|
||||
Error(w, http.StatusBadRequest, "请提供至少一个 Owner 账号")
|
||||
return
|
||||
}
|
||||
if req.MembersPerTeam <= 0 {
|
||||
req.MembersPerTeam = 4
|
||||
}
|
||||
if req.ConcurrentTeams <= 0 {
|
||||
req.ConcurrentTeams = len(req.Owners)
|
||||
}
|
||||
if req.ConcurrentTeams > len(req.Owners) {
|
||||
req.ConcurrentTeams = len(req.Owners)
|
||||
}
|
||||
if req.BrowserType == "" {
|
||||
req.BrowserType = "chromedp" // 默认使用 Chromedp
|
||||
}
|
||||
if req.Proxy == "" && config.Global != nil {
|
||||
req.Proxy = config.Global.GetProxy() // 使用新的代理获取方法
|
||||
}
|
||||
|
||||
// 初始化状态
|
||||
teamProcessState.Running = true
|
||||
teamProcessState.StartedAt = time.Now()
|
||||
teamProcessState.TotalTeams = len(req.Owners) // 所有 owners 都会处理
|
||||
teamProcessState.Completed = 0
|
||||
teamProcessState.Results = make([]TeamProcessResult, 0, len(req.Owners))
|
||||
|
||||
// 异步执行
|
||||
go runTeamProcess(req)
|
||||
|
||||
Success(w, map[string]interface{}{
|
||||
"message": "任务已启动",
|
||||
"total_teams": len(req.Owners),
|
||||
"concurrent_teams": req.ConcurrentTeams,
|
||||
"started_at": teamProcessState.StartedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleTeamProcessStatus GET /api/team/status - 获取处理状态
|
||||
func HandleTeamProcessStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "仅支持 GET")
|
||||
return
|
||||
}
|
||||
|
||||
teamProcessState.mu.Lock()
|
||||
defer teamProcessState.mu.Unlock()
|
||||
|
||||
Success(w, map[string]interface{}{
|
||||
"running": teamProcessState.Running,
|
||||
"started_at": teamProcessState.StartedAt,
|
||||
"total_teams": teamProcessState.TotalTeams,
|
||||
"completed": teamProcessState.Completed,
|
||||
"results": teamProcessState.Results,
|
||||
"elapsed_ms": time.Since(teamProcessState.StartedAt).Milliseconds(),
|
||||
})
|
||||
}
|
||||
|
||||
// HandleTeamProcessStop POST /api/team/stop - 停止处理
|
||||
func HandleTeamProcessStop(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
Error(w, http.StatusMethodNotAllowed, "仅支持 POST")
|
||||
return
|
||||
}
|
||||
|
||||
teamProcessState.Running = false
|
||||
Success(w, map[string]string{"message": "已发送停止信号"})
|
||||
}
|
||||
|
||||
// runTeamProcess 执行 Team 批量处理 - 使用工作池模式
|
||||
func runTeamProcess(req TeamProcessRequest) {
|
||||
defer func() {
|
||||
teamProcessState.Running = false
|
||||
}()
|
||||
|
||||
totalOwners := len(req.Owners)
|
||||
workerCount := req.ConcurrentTeams // 同时运行的 worker 数量
|
||||
if workerCount > totalOwners {
|
||||
workerCount = totalOwners
|
||||
}
|
||||
if workerCount <= 0 {
|
||||
workerCount = 2 // 默认 2 个并发
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("Starting Team process: %d owners, %d concurrent workers", totalOwners, workerCount), "", "team")
|
||||
|
||||
// 任务队列
|
||||
taskChan := make(chan int, totalOwners)
|
||||
resultChan := make(chan TeamProcessResult, totalOwners)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 启动 worker
|
||||
for w := 0; w < workerCount; w++ {
|
||||
wg.Add(1)
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
for idx := range taskChan {
|
||||
if !teamProcessState.Running {
|
||||
return
|
||||
}
|
||||
result := processSingleTeam(idx, req)
|
||||
resultChan <- result
|
||||
atomic.AddInt32(&teamProcessState.Completed, 1)
|
||||
}
|
||||
}(w)
|
||||
}
|
||||
|
||||
// 发送任务
|
||||
go func() {
|
||||
for i := 0; i < totalOwners; i++ {
|
||||
if !teamProcessState.Running {
|
||||
break
|
||||
}
|
||||
taskChan <- i
|
||||
}
|
||||
close(taskChan)
|
||||
}()
|
||||
|
||||
// 等待完成并收集结果
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
for result := range resultChan {
|
||||
teamProcessState.mu.Lock()
|
||||
teamProcessState.Results = append(teamProcessState.Results, result)
|
||||
teamProcessState.mu.Unlock()
|
||||
}
|
||||
|
||||
logger.Success(fmt.Sprintf("Team process complete: %d/%d teams processed", teamProcessState.Completed, totalOwners), "", "team")
|
||||
}
|
||||
|
||||
// processSingleTeam 处理单个 Team
|
||||
func processSingleTeam(idx int, req TeamProcessRequest) TeamProcessResult {
|
||||
startTime := time.Now()
|
||||
owner := req.Owners[idx]
|
||||
result := TeamProcessResult{
|
||||
TeamIndex: idx + 1,
|
||||
OwnerEmail: owner.Email,
|
||||
MemberEmails: make([]string, 0),
|
||||
Errors: make([]string, 0),
|
||||
}
|
||||
|
||||
logPrefix := fmt.Sprintf("[Team %d]", idx+1)
|
||||
logger.Info(fmt.Sprintf("%s Starting with owner: %s", logPrefix, owner.Email), owner.Email, "team")
|
||||
|
||||
// Step 1: 获取 Team ID
|
||||
inviter := invite.NewWithProxy(owner.Token, req.Proxy)
|
||||
teamID, err := inviter.GetAccountID()
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("获取 Team ID 失败: %v", err))
|
||||
result.DurationMs = time.Since(startTime).Milliseconds()
|
||||
logger.Error(fmt.Sprintf("%s Failed to get Team ID: %v", logPrefix, err), owner.Email, "team")
|
||||
return result
|
||||
}
|
||||
result.TeamID = teamID
|
||||
logger.Success(fmt.Sprintf("%s Team ID: %s", logPrefix, teamID), owner.Email, "team")
|
||||
|
||||
// Step 2: 生成成员邮箱并发送邀请
|
||||
type MemberAccount struct {
|
||||
Email string
|
||||
Password string
|
||||
Success bool
|
||||
}
|
||||
children := make([]MemberAccount, req.MembersPerTeam)
|
||||
for i := 0; i < req.MembersPerTeam; i++ {
|
||||
children[i].Email = mail.GenerateEmail()
|
||||
children[i].Password = register.GeneratePassword()
|
||||
logger.Info(fmt.Sprintf("%s [Member %d] Email: %s", logPrefix, i+1, children[i].Email), children[i].Email, "team")
|
||||
}
|
||||
|
||||
// 发送邀请
|
||||
inviteEmails := make([]string, req.MembersPerTeam)
|
||||
for i, c := range children {
|
||||
inviteEmails[i] = c.Email
|
||||
}
|
||||
if err := inviter.SendInvites(inviteEmails); err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("发送邀请失败: %v", err))
|
||||
result.DurationMs = time.Since(startTime).Milliseconds()
|
||||
return result
|
||||
}
|
||||
logger.Success(fmt.Sprintf("%s Sent %d invite(s)", logPrefix, len(inviteEmails)), owner.Email, "team")
|
||||
|
||||
// Step 3: 并发注册成员
|
||||
var memberWg sync.WaitGroup
|
||||
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()
|
||||
|
||||
// 重试逻辑
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
if !teamProcessState.Running {
|
||||
return
|
||||
}
|
||||
|
||||
if attempt > 0 {
|
||||
email = mail.GenerateEmail()
|
||||
password = register.GeneratePassword()
|
||||
logger.Info(fmt.Sprintf("%s [Member %d] Retry %d: %s", logPrefix, memberIdx+1, attempt, email), email, "team")
|
||||
|
||||
if err := inviter.SendInvites([]string{email}); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
_, err := registerWithTimeout(email, password, name, birthdate, req.Proxy)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "验证码") {
|
||||
continue
|
||||
}
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("Member %d: %v", memberIdx+1, err))
|
||||
return
|
||||
}
|
||||
|
||||
memberMutex.Lock()
|
||||
children[memberIdx].Email = email
|
||||
children[memberIdx].Password = password
|
||||
children[memberIdx].Success = true
|
||||
memberMutex.Unlock()
|
||||
|
||||
logger.Success(fmt.Sprintf("%s [Member %d] Registered", logPrefix, memberIdx+1), email, "team")
|
||||
return
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
memberWg.Wait()
|
||||
|
||||
// 统计注册成功数
|
||||
registeredChildren := make([]MemberAccount, 0)
|
||||
for _, c := range children {
|
||||
if c.Success {
|
||||
registeredChildren = append(registeredChildren, c)
|
||||
result.MemberEmails = append(result.MemberEmails, c.Email)
|
||||
result.Registered++
|
||||
}
|
||||
}
|
||||
logger.Info(fmt.Sprintf("%s Registered: %d/%d", logPrefix, result.Registered, req.MembersPerTeam), owner.Email, "team")
|
||||
|
||||
// Step 4: S2A 授权入库
|
||||
for i, child := range registeredChildren {
|
||||
if !teamProcessState.Running {
|
||||
break
|
||||
}
|
||||
|
||||
s2aResp, err := auth.GenerateS2AAuthURL(config.Global.S2AApiBase, config.Global.S2AAdminKey, config.Global.ProxyID)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("Member %d auth URL: %v", i+1, err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 根据配置选择浏览器自动化
|
||||
var code string
|
||||
if req.BrowserType == "rod" {
|
||||
code, err = auth.CompleteWithRod(s2aResp.Data.AuthURL, child.Email, child.Password, teamID, req.Headless, req.Proxy)
|
||||
} else {
|
||||
code, err = auth.CompleteWithChromedp(s2aResp.Data.AuthURL, child.Email, child.Password, teamID, req.Headless, req.Proxy)
|
||||
}
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("Member %d browser: %v", i+1, err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 提交到 S2A
|
||||
_, err = auth.SubmitS2AOAuth(
|
||||
config.Global.S2AApiBase,
|
||||
config.Global.S2AAdminKey,
|
||||
s2aResp.Data.SessionID,
|
||||
code,
|
||||
child.Email,
|
||||
config.Global.Concurrency,
|
||||
config.Global.Priority,
|
||||
config.Global.GroupIDs,
|
||||
config.Global.ProxyID,
|
||||
)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("Member %d S2A: %v", i+1, err))
|
||||
continue
|
||||
}
|
||||
|
||||
result.AddedToS2A++
|
||||
logger.Success(fmt.Sprintf("%s [Member %d] Added to S2A", logPrefix, i+1), child.Email, "team")
|
||||
}
|
||||
|
||||
result.DurationMs = time.Since(startTime).Milliseconds()
|
||||
logger.Success(fmt.Sprintf("%s Complete: %d registered, %d in S2A", logPrefix, result.Registered, result.AddedToS2A), owner.Email, "team")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// registerWithTimeout 带超时的注册
|
||||
func registerWithTimeout(email, password, name, birthdate, proxy string) (*register.ChatGPTReg, error) {
|
||||
reg, err := register.New(proxy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := reg.InitSession(); err != nil {
|
||||
return nil, fmt.Errorf("初始化失败: %v", err)
|
||||
}
|
||||
if err := reg.GetAuthorizeURL(email); err != nil {
|
||||
return nil, fmt.Errorf("获取授权URL失败: %v", err)
|
||||
}
|
||||
if err := reg.StartAuthorize(); err != nil {
|
||||
return nil, fmt.Errorf("启动授权失败: %v", err)
|
||||
}
|
||||
if err := reg.Register(email, password); err != nil {
|
||||
return nil, fmt.Errorf("注册失败: %v", err)
|
||||
}
|
||||
if err := reg.SendVerificationEmail(); err != nil {
|
||||
return nil, fmt.Errorf("发送邮件失败: %v", err)
|
||||
}
|
||||
|
||||
// 短超时获取验证码
|
||||
otpCode, err := mail.GetVerificationCode(email, 5*time.Second)
|
||||
if err != nil {
|
||||
otpCode, err = mail.GetVerificationCode(email, 15*time.Second)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("验证码获取超时")
|
||||
}
|
||||
}
|
||||
|
||||
if err := reg.ValidateOTP(otpCode); err != nil {
|
||||
return nil, fmt.Errorf("OTP验证失败: %v", err)
|
||||
}
|
||||
if err := reg.CreateAccount(name, birthdate); err != nil {
|
||||
return nil, fmt.Errorf("创建账户失败: %v", err)
|
||||
}
|
||||
|
||||
_ = reg.GetSessionToken()
|
||||
return reg, nil
|
||||
}
|
||||
Reference in New Issue
Block a user