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:
2026-01-30 07:40:35 +08:00
commit f4448bbef2
106 changed files with 19282 additions and 0 deletions

137
backend/README.md Normal file
View File

@@ -0,0 +1,137 @@
# Codex Pool Backend
Codex Pool 后端服务 - 标准 Go 项目结构
## 目录结构
```
backend/
├── cmd/
│ └── main.go # 程序入口
├── internal/
│ ├── api/
│ │ └── http.go # HTTP 工具、中间件
│ ├── auth/
│ │ ├── s2a.go # S2A 授权逻辑
│ │ ├── rod.go # Rod 浏览器自动化
│ │ └── chromedp.go # Chromedp 浏览器自动化
│ ├── client/
│ │ └── tls.go # TLS 指纹 HTTP 客户端
│ ├── config/
│ │ └── config.go # 配置类型和加载
│ ├── database/
│ │ └── sqlite.go # SQLite 操作
│ ├── invite/
│ │ └── team.go # Team 邀请功能
│ ├── logger/
│ │ └── logger.go # 日志系统
│ ├── mail/
│ │ └── service.go # 邮箱服务
│ └── register/
│ └── chatgpt.go # ChatGPT 注册功能
├── config.json # 配置文件
├── config.example.json # 配置示例
└── go.mod
```
## 快速启动
```bash
# 编译
go build -o codex-pool.exe ./cmd
# 运行
./codex-pool.exe
```
## 配置文件
创建 `config.json`
```json
{
"port": 8088,
"cors_origin": "*",
"s2a_api_base": "https://your-s2a-api.com",
"s2a_admin_key": "your-admin-key",
"default_proxy": "",
"accounts_path": "accounts.json",
"concurrency": 100,
"priority": 30,
"group_ids": [1, 2, 3],
"mail_services": [
{
"name": "主邮箱服务",
"api_base": "https://mail.example.com",
"api_token": "your-token",
"domain": "example.com"
}
]
}
```
## 包说明
| 包 | 说明 |
|---|------|
| `cmd` | 程序入口 |
| `internal/api` | HTTP 响应工具、CORS 中间件 |
| `internal/auth` | S2A 授权、浏览器自动化 |
| `internal/client` | TLS 指纹 HTTP 客户端 |
| `internal/config` | 配置类型、加载函数 |
| `internal/database` | SQLite 数据库操作 |
| `internal/invite` | Team 邀请功能 |
| `internal/logger` | 日志系统 |
| `internal/mail` | 邮箱服务 |
| `internal/register` | ChatGPT 注册功能 |
## API 接口
### 基础
- `GET /api/health` - 健康检查
- `GET /api/config` - 获取配置
### 日志
- `GET /api/logs` - 获取日志
- `POST /api/logs/clear` - 清空日志
### S2A 代理
- `GET /api/s2a/test` - 测试连接
### 邮箱服务
- `GET /api/mail/services` - 获取配置
- `POST /api/mail/services/test` - 测试连接
### Team Owner
- `GET /api/db/owners` - 获取列表
- `GET /api/db/owners/stats` - 获取统计
- `POST /api/db/owners/clear` - 清空
## 清理旧文件
如果已迁移到新结构,可以删除根目录的旧文件:
```powershell
# 删除旧的 .go 文件 (保留 go.mod, go.sum)
Remove-Item main.go, types.go, http.go, api_handlers.go, db_api.go, database.go, mail.go, codex-auth.go, browser-auth-rod.go, browser-auth-cdp.go, client.go, register.go, team-invite.go, log_stream.go, logger.go -ErrorAction SilentlyContinue
# 删除旧的 exe
Remove-Item codex-pool.exe -ErrorAction SilentlyContinue
```
## 开发
```bash
# 安装依赖
go mod tidy
# 运行
go run ./cmd
# 编译
go build -o codex-pool.exe ./cmd
```
## 许可
MIT License

349
backend/cmd/main.go Normal file
View 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,
})
}

View 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]
}

BIN
backend/codex-pool.exe Normal file

Binary file not shown.

40
backend/go.mod Normal file
View File

@@ -0,0 +1,40 @@
module codex-pool
go 1.24.1
require (
github.com/andybalholm/brotli v1.2.0
github.com/bogdanfinn/fhttp v0.6.7
github.com/bogdanfinn/tls-client v1.13.1
github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327
github.com/chromedp/chromedp v0.14.2
github.com/go-rod/rod v0.116.2
github.com/go-rod/stealth v0.4.9
)
require (
github.com/bdandy/go-errors v1.2.2 // indirect
github.com/bdandy/go-socks4 v1.2.3 // indirect
github.com/bogdanfinn/quic-go-utls v1.0.7-utls // indirect
github.com/bogdanfinn/utls v1.7.7-barnius // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/mattn/go-sqlite3 v1.14.33 // indirect
github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect
github.com/ysmood/fetchup v0.2.3 // indirect
github.com/ysmood/goob v0.4.0 // indirect
github.com/ysmood/got v0.40.0 // indirect
github.com/ysmood/gson v0.7.3 // indirect
github.com/ysmood/leakless v0.9.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

90
backend/go.sum Normal file
View File

@@ -0,0 +1,90 @@
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM=
github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic=
github.com/bdandy/go-socks4 v1.2.3/go.mod h1:98kiVFgpdogR8aIGLWLvjDVZ8XcKPsSI/ypGrO+bqHI=
github.com/bogdanfinn/fhttp v0.6.7 h1:yTDywa9INbRqePBE5gHhpxlMjvAQ0bdX77pvOTPJoPI=
github.com/bogdanfinn/fhttp v0.6.7/go.mod h1:A+EKDzMx2hb4IUbMx4TlkoHnaJEiLl8r/1Ss1Y+5e5M=
github.com/bogdanfinn/quic-go-utls v1.0.7-utls h1:opxU/wt2C6FcD3rkGSOwfpQgfGSFx9eAKYQrFwYBzuo=
github.com/bogdanfinn/quic-go-utls v1.0.7-utls/go.mod h1:bk8QMY2KypO8A6LzHJ7C4+bdB0ksLOd6NZt600wXYe8=
github.com/bogdanfinn/tls-client v1.13.1 h1:O2sfv8JK8R7nNz+Km675VOIajum4sMqOb/ys/4gXfPQ=
github.com/bogdanfinn/tls-client v1.13.1/go.mod h1:4ZnckBKYWaQD9wq55cpUr5/2i45cCBAG+2V3fge+yvQ=
github.com/bogdanfinn/utls v1.7.7-barnius h1:OuJ497cc7F3yKNVHRsYPQdGggmk5x6+V5ZlrCR7fOLU=
github.com/bogdanfinn/utls v1.7.7-barnius/go.mod h1:aAK1VZQlpKZClF1WEQeq6kyclbkPq4hz6xTbB5xSlmg=
github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 h1:UQ4AU+BGti3Sy/aLU8KVseYKNALcX9UXY6DfpwQ6J8E=
github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM=
github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs=
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/go-rod/rod v0.113.0/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw=
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
github.com/go-rod/stealth v0.4.9 h1:X2PmQk4DUF2wzw6GOsWjW/glb8K5ebnftbEvLh7MlZ4=
github.com/go-rod/stealth v0.4.9/go.mod h1:eAzyvw8c0iAd5nJJsSWeh0fQ5z94vCIfdi1hUmYDimc=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785 h1:J1//5K/6QF10cZ59zLcVNFGmBfiSrH8Cho/lNrViK9s=
github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc=
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
github.com/ysmood/gop v0.0.2/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg=
github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
github.com/ysmood/got v0.34.1/go.mod h1:yddyjq/PmAf08RMLSwDjPyCvHvYed+WjHnQxpH851LM=
github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q=
github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg=
github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY=
github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=
github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
github.com/ysmood/leakless v0.8.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View 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)
}
}

View 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
}

View File

@@ -0,0 +1,194 @@
package auth
import (
"context"
"fmt"
"strings"
"time"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp"
)
// CompleteWithChromedp 使用 chromedp 完成 S2A OAuth 授权
func CompleteWithChromedp(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", headless),
chromedp.Flag("disable-gpu", true),
chromedp.Flag("no-sandbox", true),
chromedp.Flag("disable-dev-shm-usage", true),
chromedp.Flag("disable-blink-features", "AutomationControlled"),
chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"),
)
if proxy != "" {
opts = append(opts, chromedp.ProxyServer(proxy))
}
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
ctx, cancel = context.WithTimeout(ctx, 120*time.Second)
defer cancel()
var callbackURL string
chromedp.ListenTarget(ctx, func(ev interface{}) {
if req, ok := ev.(*network.EventRequestWillBeSent); ok {
url := req.Request.URL
if strings.Contains(url, "localhost") && strings.Contains(url, "code=") {
callbackURL = url
}
}
})
err := chromedp.Run(ctx,
network.Enable(),
chromedp.Navigate(authURL),
chromedp.WaitReady("body"),
)
if err != nil {
return "", fmt.Errorf("访问失败: %v", err)
}
time.Sleep(2 * time.Second)
if callbackURL != "" {
return ExtractCodeFromCallbackURL(callbackURL), nil
}
var currentURL string
_ = chromedp.Run(ctx, chromedp.Location(&currentURL))
if strings.Contains(currentURL, "code=") {
return ExtractCodeFromCallbackURL(currentURL), nil
}
time.Sleep(1 * time.Second)
emailSelectors := []string{
`input[name="email"]`,
`input[type="email"]`,
`input[name="username"]`,
}
var emailFilled bool
for _, sel := range emailSelectors {
err = chromedp.Run(ctx, chromedp.WaitVisible(sel, chromedp.ByQuery))
if err == nil {
err = chromedp.Run(ctx,
chromedp.Clear(sel, chromedp.ByQuery),
chromedp.SendKeys(sel, email, chromedp.ByQuery),
)
if err == nil {
emailFilled = true
break
}
}
}
if !emailFilled {
return "", fmt.Errorf("未找到邮箱输入框")
}
time.Sleep(300 * time.Millisecond)
buttonSelectors := []string{
`button[type="submit"]`,
`button[data-testid="login-button"]`,
`button.continue-btn`,
`input[type="submit"]`,
}
for _, sel := range buttonSelectors {
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
if err == nil {
break
}
}
time.Sleep(1500 * time.Millisecond)
if callbackURL != "" {
return ExtractCodeFromCallbackURL(callbackURL), nil
}
_ = chromedp.Run(ctx, chromedp.Location(&currentURL))
if strings.Contains(currentURL, "code=") {
return ExtractCodeFromCallbackURL(currentURL), nil
}
passwordSelectors := []string{
`input[name="current-password"]`,
`input[name="password"]`,
`input[type="password"]`,
}
var passwordFilled bool
for _, sel := range passwordSelectors {
err = chromedp.Run(ctx, chromedp.WaitVisible(sel, chromedp.ByQuery))
if err == nil {
err = chromedp.Run(ctx,
chromedp.Clear(sel, chromedp.ByQuery),
chromedp.SendKeys(sel, password, chromedp.ByQuery),
)
if err == nil {
passwordFilled = true
break
}
}
}
if !passwordFilled {
return "", fmt.Errorf("未找到密码输入框")
}
time.Sleep(300 * time.Millisecond)
for _, sel := range buttonSelectors {
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
if err == nil {
break
}
}
for i := 0; i < 30; i++ {
time.Sleep(500 * time.Millisecond)
if callbackURL != "" {
return ExtractCodeFromCallbackURL(callbackURL), nil
}
var url string
if err := chromedp.Run(ctx, chromedp.Location(&url)); err == nil {
if strings.Contains(url, "code=") {
return ExtractCodeFromCallbackURL(url), nil
}
if strings.Contains(url, "consent") {
for _, sel := range buttonSelectors {
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
if err == nil {
break
}
}
time.Sleep(1 * time.Second)
}
if strings.Contains(url, "authorize") && teamID != "" {
err = chromedp.Run(ctx,
chromedp.Click(fmt.Sprintf(`[data-workspace-id="%s"], [data-account-id="%s"]`, teamID, teamID), chromedp.ByQuery),
)
}
}
}
if callbackURL != "" {
return ExtractCodeFromCallbackURL(callbackURL), nil
}
return "", fmt.Errorf("授权超时")
}

View File

@@ -0,0 +1,167 @@
package auth
import (
"fmt"
"strings"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/proto"
"github.com/go-rod/stealth"
)
// RodAuth 使用 Rod + Stealth 完成 OAuth 授权
type RodAuth struct {
browser *rod.Browser
headless bool
proxy string
}
// NewRodAuth 创建 Rod 授权器
func NewRodAuth(headless bool, proxy string) (*RodAuth, error) {
l := launcher.New().
Headless(headless).
Set("disable-blink-features", "AutomationControlled").
Set("disable-dev-shm-usage").
Set("no-sandbox").
Set("disable-gpu").
Set("disable-extensions").
Set("disable-background-networking").
Set("disable-sync").
Set("disable-translate").
Set("metrics-recording-only").
Set("no-first-run")
if proxy != "" {
l = l.Proxy(proxy)
}
controlURL, err := l.Launch()
if err != nil {
return nil, fmt.Errorf("启动浏览器失败: %v", err)
}
browser := rod.New().ControlURL(controlURL)
if err := browser.Connect(); err != nil {
return nil, fmt.Errorf("连接浏览器失败: %v", err)
}
return &RodAuth{
browser: browser,
headless: headless,
proxy: proxy,
}, nil
}
// Close 关闭浏览器
func (r *RodAuth) Close() {
if r.browser != nil {
r.browser.Close()
}
}
// CompleteOAuth 完成 OAuth 授权
func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string, error) {
page, err := stealth.Page(r.browser)
if err != nil {
return "", fmt.Errorf("创建页面失败: %v", err)
}
defer page.Close()
page = page.Timeout(45 * time.Second)
if err := page.Navigate(authURL); err != nil {
return "", fmt.Errorf("访问授权URL失败: %v", err)
}
page.MustWaitDOMStable()
if code := r.checkForCode(page); code != "" {
return code, nil
}
emailInput, err := page.Timeout(5 * time.Second).Element("input[name='email'], input[type='email'], input[name='username']")
if err != nil {
return "", fmt.Errorf("未找到邮箱输入框")
}
emailInput.MustSelectAllText().MustInput(email)
time.Sleep(200 * time.Millisecond)
if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil {
btn.MustClick()
}
time.Sleep(1500 * time.Millisecond)
if code := r.checkForCode(page); code != "" {
return code, nil
}
passwordInput, err := page.Timeout(8 * time.Second).Element("input[type='password']")
if err != nil {
return "", fmt.Errorf("未找到密码输入框")
}
passwordInput.MustSelectAllText().MustInput(password)
time.Sleep(200 * time.Millisecond)
if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil {
btn.MustClick()
}
for i := 0; i < 66; i++ {
time.Sleep(300 * time.Millisecond)
if code := r.checkForCode(page); code != "" {
return code, nil
}
info, _ := page.Info()
currentURL := info.URL
if strings.Contains(currentURL, "consent") {
if btn, _ := page.Timeout(500 * time.Millisecond).Element("button[type='submit']"); btn != nil {
btn.Click(proto.InputMouseButtonLeft, 1)
}
}
if strings.Contains(currentURL, "authorize") && teamID != "" {
wsSelector := fmt.Sprintf("[data-workspace-id='%s'], [data-account-id='%s']", teamID, teamID)
if wsBtn, _ := page.Timeout(500 * time.Millisecond).Element(wsSelector); wsBtn != nil {
wsBtn.Click(proto.InputMouseButtonLeft, 1)
}
}
}
return "", fmt.Errorf("授权超时")
}
// checkForCode 检查 URL 中是否包含 code
func (r *RodAuth) checkForCode(page *rod.Page) string {
info, err := page.Info()
if err != nil {
return ""
}
if strings.Contains(info.URL, "code=") {
return ExtractCodeFromCallbackURL(info.URL)
}
return ""
}
// CompleteWithRod 使用 Rod + Stealth 完成 S2A 授权
func CompleteWithRod(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
auth, err := NewRodAuth(headless, proxy)
if err != nil {
return "", err
}
defer auth.Close()
return auth.CompleteOAuth(authURL, email, password, teamID)
}
// CompleteWithBrowser 使用 Rod 完成 S2A 授权 (别名)
func CompleteWithBrowser(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
return CompleteWithRod(authURL, email, password, teamID, headless, proxy)
}

View File

@@ -0,0 +1,291 @@
package auth
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
const (
CodexClientID = "app_EMoamEEZ73f0CkXaXp7hrann"
CodexRedirectURI = "http://localhost:1455/auth/callback"
CodexScope = "openid profile email offline_access"
)
// CodexTokens Codex Token 结构
type CodexTokens struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
ExpiredAt string `json:"expired_at,omitempty"`
}
// S2AAuthURLRequest S2A 授权 URL 请求
type S2AAuthURLRequest struct {
ProxyID *int `json:"proxy_id,omitempty"`
}
// S2AAuthURLResponse S2A 授权 URL 响应
type S2AAuthURLResponse struct {
Code int `json:"code"`
Data struct {
AuthURL string `json:"auth_url"`
SessionID string `json:"session_id"`
} `json:"data"`
Message string `json:"message,omitempty"`
}
// S2ACreateFromOAuthRequest 提交 OAuth 入库请求
type S2ACreateFromOAuthRequest struct {
SessionID string `json:"session_id"`
Code string `json:"code"`
Name string `json:"name,omitempty"`
Concurrency int `json:"concurrency,omitempty"`
Priority int `json:"priority,omitempty"`
GroupIDs []int `json:"group_ids,omitempty"`
ProxyID *int `json:"proxy_id,omitempty"`
}
// S2ACreateFromOAuthResponse 入库响应
type S2ACreateFromOAuthResponse struct {
Code int `json:"code"`
Data struct {
ID int `json:"id"`
Name string `json:"name"`
Platform string `json:"platform"`
Type string `json:"type"`
Status string `json:"status"`
Concurrency int `json:"concurrency"`
Priority int `json:"priority"`
} `json:"data"`
Message string `json:"message,omitempty"`
}
// GenerateS2AAuthURL 从 S2A 生成 Codex 授权 URL
func GenerateS2AAuthURL(s2aAPIBase, s2aAdminKey string, proxyID *int) (*S2AAuthURLResponse, error) {
client := &http.Client{Timeout: 15 * time.Second}
apiURL := s2aAPIBase + "/api/v1/admin/openai/generate-auth-url"
payload := S2AAuthURLRequest{ProxyID: proxyID}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiURL, bytes.NewReader(body))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Api-Key", s2aAdminKey)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if len(respBody) > 0 && respBody[0] == '<' {
return nil, fmt.Errorf("服务器返回 HTML: %s", string(respBody)[:min(100, len(respBody))])
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody)[:min(200, len(respBody))])
}
var result S2AAuthURLResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("解析响应失败: %v, body: %s", err, string(respBody)[:min(100, len(respBody))])
}
if result.Code != 0 {
return nil, fmt.Errorf("S2A 错误: %s", result.Message)
}
return &result, nil
}
// SubmitS2AOAuth 提交 OAuth code 到 S2A 入库
func SubmitS2AOAuth(s2aAPIBase, s2aAdminKey, sessionID, code, name string, concurrency, priority int, groupIDs []int, proxyID *int) (*S2ACreateFromOAuthResponse, error) {
client := &http.Client{Timeout: 30 * time.Second}
apiURL := s2aAPIBase + "/api/v1/admin/openai/create-from-oauth"
payload := S2ACreateFromOAuthRequest{
SessionID: sessionID,
Code: code,
Name: name,
Concurrency: concurrency,
Priority: priority,
GroupIDs: groupIDs,
ProxyID: proxyID,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiURL, bytes.NewReader(body))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Api-Key", s2aAdminKey)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var result S2ACreateFromOAuthResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("S2A 入库失败: %s", result.Message)
}
return &result, nil
}
// VerifyS2AAccount 验证账号入库状态
func VerifyS2AAccount(s2aAPIBase, s2aAdminKey, email string) (bool, error) {
client := &http.Client{Timeout: 30 * time.Second}
apiURL := fmt.Sprintf("%s/api/v1/admin/accounts?page=1&page_size=20&search=%s&timezone=Asia/Shanghai", s2aAPIBase, url.QueryEscape(email))
req, _ := http.NewRequest("GET", apiURL, nil)
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Api-Key", s2aAdminKey)
resp, err := client.Do(req)
if err != nil {
return false, fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var result struct {
Code int `json:"code"`
Data struct {
Items []struct {
ID int `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
} `json:"items"`
Total int `json:"total"`
} `json:"data"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return false, fmt.Errorf("解析响应失败: %v", err)
}
if result.Code != 0 || result.Data.Total == 0 {
return false, nil
}
for _, item := range result.Data.Items {
if item.Status == "active" {
return true, nil
}
}
return false, nil
}
// ExtractCodeFromCallbackURL 从回调 URL 中提取 code
func ExtractCodeFromCallbackURL(callbackURL string) string {
parsedURL, err := url.Parse(callbackURL)
if err != nil {
return ""
}
return parsedURL.Query().Get("code")
}
// RefreshCodexToken 刷新 Codex token
func RefreshCodexToken(refreshToken string, proxyURL string) (*CodexTokens, error) {
client := &http.Client{Timeout: 30 * time.Second}
if proxyURL != "" {
proxyURLParsed, _ := url.Parse(proxyURL)
client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURLParsed)}
}
data := url.Values{
"client_id": {CodexClientID},
"grant_type": {"refresh_token"},
"refresh_token": {refreshToken},
"scope": {"openid profile email"},
}
req, _ := http.NewRequest("POST", "https://auth.openai.com/oauth/token", strings.NewReader(data.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("刷新 token 失败: %d, %s", resp.StatusCode, string(body)[:min(200, len(body))])
}
var tokens CodexTokens
if err := json.Unmarshal(body, &tokens); err != nil {
return nil, err
}
if tokens.ExpiresIn > 0 {
expiredAt := time.Now().Add(time.Duration(tokens.ExpiresIn) * time.Second)
tokens.ExpiredAt = expiredAt.Format(time.RFC3339)
}
return &tokens, nil
}
// ExtractWorkspaceFromCookie 从 cookie 提取 workspace_id
func ExtractWorkspaceFromCookie(cookieValue string) string {
parts := strings.Split(cookieValue, ".")
if len(parts) < 1 {
return ""
}
payload := parts[0]
if m := len(payload) % 4; m != 0 {
payload += strings.Repeat("=", 4-m)
}
decoded, err := base64.StdEncoding.DecodeString(payload)
if err != nil {
decoded, err = base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return ""
}
}
var data struct {
Workspaces []struct {
ID string `json:"id"`
} `json:"workspaces"`
}
if err := json.Unmarshal(decoded, &data); err != nil {
return ""
}
if len(data.Workspaces) > 0 {
return data.Workspaces[0].ID
}
return ""
}

View File

@@ -0,0 +1,240 @@
package client
import (
"bytes"
"compress/gzip"
"io"
"math/rand"
"net/http"
"net/url"
"strings"
"github.com/andybalholm/brotli"
http2 "github.com/bogdanfinn/fhttp"
tls_client "github.com/bogdanfinn/tls-client"
"github.com/bogdanfinn/tls-client/profiles"
)
// TLSClient 使用 tls-client 模拟浏览器指纹的 HTTP 客户端
type TLSClient struct {
client tls_client.HttpClient
userAgent string
chromeVer string
acceptLang string
}
// 语言偏好池
var languagePrefs = []string{
"en-US,en;q=0.9",
"en-GB,en;q=0.9,en-US;q=0.8",
"de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
}
// New 创建一个新的 TLS 客户端
func New(proxyStr string) (*TLSClient, error) {
jar := tls_client.NewCookieJar()
chromeVer := "133"
options := []tls_client.HttpClientOption{
tls_client.WithTimeoutSeconds(60),
tls_client.WithClientProfile(profiles.Chrome_133),
tls_client.WithRandomTLSExtensionOrder(),
tls_client.WithCookieJar(jar),
tls_client.WithInsecureSkipVerify(),
}
if proxyStr != "" {
options = append(options, tls_client.WithProxyUrl(proxyStr))
}
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)
if err != nil {
return nil, err
}
acceptLang := languagePrefs[rand.Intn(len(languagePrefs))]
userAgent := generateUserAgent(chromeVer)
return &TLSClient{
client: client,
userAgent: userAgent,
chromeVer: chromeVer,
acceptLang: acceptLang,
}, nil
}
// generateUserAgent 生成随机化的 User-Agent
func generateUserAgent(chromeVer string) string {
winVersions := []string{
"Windows NT 10.0; Win64; x64",
"Windows NT 10.0; WOW64",
}
winVer := winVersions[rand.Intn(len(winVersions))]
return "Mozilla/5.0 (" + winVer + ") AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + chromeVer + ".0.0.0 Safari/537.36"
}
// getDefaultHeaders 获取默认请求头
func (c *TLSClient) getDefaultHeaders() map[string]string {
secChUa := `"Chromium";v="` + c.chromeVer + `", "Not(A:Brand";v="99", "Google Chrome";v="` + c.chromeVer + `"`
return map[string]string{
"User-Agent": c.userAgent,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Language": c.acceptLang,
"Accept-Encoding": "gzip, deflate, br, zstd",
"Cache-Control": "max-age=0",
"Sec-Ch-Ua": secChUa,
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": `"Windows"`,
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
}
}
// Do 执行 HTTP 请求
func (c *TLSClient) Do(req *http.Request) (*http.Response, error) {
fhttpReq, err := http2.NewRequest(req.Method, req.URL.String(), req.Body)
if err != nil {
return nil, err
}
for key, value := range c.getDefaultHeaders() {
if req.Header.Get(key) == "" {
fhttpReq.Header.Set(key, value)
}
}
for key, values := range req.Header {
for _, value := range values {
fhttpReq.Header.Set(key, value)
}
}
resp, err := c.client.Do(fhttpReq)
if err != nil {
return nil, err
}
finalReq := req
if resp.Request != nil && resp.Request.URL != nil {
finalReq = &http.Request{
Method: resp.Request.Method,
URL: (*url.URL)(resp.Request.URL),
Header: http.Header(resp.Request.Header),
}
}
stdResp := &http.Response{
Status: resp.Status,
StatusCode: resp.StatusCode,
Proto: resp.Proto,
ProtoMajor: resp.ProtoMajor,
ProtoMinor: resp.ProtoMinor,
Header: http.Header(resp.Header),
Body: resp.Body,
ContentLength: resp.ContentLength,
TransferEncoding: resp.TransferEncoding,
Close: resp.Close,
Uncompressed: resp.Uncompressed,
Request: finalReq,
}
return stdResp, nil
}
// Get 执行 GET 请求
func (c *TLSClient) Get(urlStr string) (*http.Response, error) {
req, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
// Post 执行 POST 请求
func (c *TLSClient) Post(urlStr string, contentType string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequest("POST", urlStr, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
return c.Do(req)
}
// PostForm 执行 POST 表单请求
func (c *TLSClient) PostForm(urlStr string, data url.Values) (*http.Response, error) {
return c.Post(urlStr, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
}
// PostJSON 执行 POST JSON 请求
func (c *TLSClient) PostJSON(urlStr string, body io.Reader) (*http.Response, error) {
return c.Post(urlStr, "application/json", body)
}
// GetCookie 获取指定 URL 的 Cookie
func (c *TLSClient) GetCookie(urlStr string, name string) string {
u, err := url.Parse(urlStr)
if err != nil {
return ""
}
cookies := c.client.GetCookies(u)
for _, cookie := range cookies {
if cookie.Name == name {
return cookie.Value
}
}
return ""
}
// SetCookie 设置 Cookie
func (c *TLSClient) SetCookie(urlStr string, cookie *http.Cookie) {
u, err := url.Parse(urlStr)
if err != nil {
return
}
c.client.SetCookies(u, []*http2.Cookie{
{
Name: cookie.Name,
Value: cookie.Value,
Path: cookie.Path,
Domain: cookie.Domain,
},
})
}
// ReadBody 读取响应体并自动处理压缩
func ReadBody(resp *http.Response) ([]byte, error) {
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
switch resp.Header.Get("Content-Encoding") {
case "gzip":
gzReader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return data, nil
}
defer gzReader.Close()
return io.ReadAll(gzReader)
case "br":
return io.ReadAll(brotli.NewReader(bytes.NewReader(data)))
}
return data, nil
}
// ReadBodyString 读取响应体为字符串
func ReadBodyString(resp *http.Response) (string, error) {
body, err := ReadBody(resp)
if err != nil {
return "", err
}
return string(body), nil
}

View File

@@ -0,0 +1,264 @@
package config
import (
"encoding/json"
"os"
"strconv"
"strings"
"sync"
)
// MailServiceConfig 邮箱服务配置
type MailServiceConfig struct {
Name string `yaml:"name" json:"name"`
APIBase string `yaml:"api_base" json:"api_base"`
APIToken string `yaml:"api_token" json:"api_token"`
Domain string `yaml:"domain" json:"domain"`
EmailPath string `yaml:"email_path,omitempty" json:"email_path,omitempty"`
AddUserAPI string `yaml:"add_user_api,omitempty" json:"add_user_api,omitempty"`
}
// Config 应用配置 (实时从数据库读取)
type Config struct {
// 服务器配置 (启动时固定)
Port int `json:"port"`
CorsOrigin string `json:"cors_origin"`
// S2A 配置 (可实时更新)
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"`
ProxyID *int `json:"proxy_id"`
// 代理配置 (可实时更新)
ProxyEnabled bool `json:"proxy_enabled"`
DefaultProxy string `json:"default_proxy"`
// 自动化配置
AutoPauseOnExpired bool `json:"auto_pause_on_expired"`
AccountsPath string `json:"accounts_path"`
// 邮箱服务
MailServices []MailServiceConfig `json:"mail_services"`
}
// GetProxy 获取代理地址(如果启用)
func (c *Config) GetProxy() string {
if c.ProxyEnabled && c.DefaultProxy != "" {
return c.DefaultProxy
}
return ""
}
// Account 账号结构 (保持 JSON 格式用于账号文件)
type Account struct {
Email string `json:"email,omitempty"`
Account string `json:"account,omitempty"`
Password string `json:"password"`
Name string `json:"name,omitempty"`
AccessToken string `json:"access_token,omitempty"`
Token string `json:"token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
Pooled bool `json:"pooled,omitempty"`
PooledAt string `json:"pooled_at,omitempty"`
PoolID int `json:"pool_id,omitempty"`
Used bool `json:"used,omitempty"`
UsedAt string `json:"used_at,omitempty"`
}
// GetEmail 获取邮箱
func (a *Account) GetEmail() string {
if a.Email != "" {
return a.Email
}
return a.Account
}
// GetAccessToken 获取 Token
func (a *Account) GetAccessToken() string {
if a.AccessToken != "" {
return a.AccessToken
}
return a.Token
}
// PoolingConfig 入库任务配置
type PoolingConfig struct {
Concurrency int `json:"concurrency"`
SerialAuthorize bool `json:"serial_authorize"`
BrowserType string `json:"browser_type"`
Headless bool `json:"headless"`
Proxy string `json:"proxy"`
}
// 全局配置实例
var (
Global *Config
configMu sync.RWMutex
)
// ConfigDB 配置数据库接口
type ConfigDB interface {
GetConfig(key string) (string, error)
SetConfig(key, value string) error
GetAllConfig() (map[string]string, error)
}
var configDB ConfigDB
// SetConfigDB 设置配置数据库
func SetConfigDB(db ConfigDB) {
configDB = db
}
// InitFromDB 从数据库初始化配置
func InitFromDB() *Config {
configMu.Lock()
defer configMu.Unlock()
cfg := &Config{
Port: 8848,
CorsOrigin: "*",
Concurrency: 2,
Priority: 0,
}
if configDB == nil {
Global = cfg
return cfg
}
// 从数据库加载配置
if v, _ := configDB.GetConfig("s2a_api_base"); v != "" {
cfg.S2AApiBase = v
}
if v, _ := configDB.GetConfig("s2a_admin_key"); v != "" {
cfg.S2AAdminKey = v
}
if v, _ := configDB.GetConfig("concurrency"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
cfg.Concurrency = n
}
}
if v, _ := configDB.GetConfig("priority"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
cfg.Priority = n
}
}
if v, _ := configDB.GetConfig("group_ids"); v != "" {
var ids []int
for _, s := range strings.Split(v, ",") {
if n, err := strconv.Atoi(strings.TrimSpace(s)); err == nil {
ids = append(ids, n)
}
}
cfg.GroupIDs = ids
}
if v, _ := configDB.GetConfig("proxy_enabled"); v == "true" {
cfg.ProxyEnabled = true
}
if v, _ := configDB.GetConfig("default_proxy"); v != "" {
cfg.DefaultProxy = v
}
if v, _ := configDB.GetConfig("mail_services"); v != "" {
var services []MailServiceConfig
if err := json.Unmarshal([]byte(v), &services); err == nil {
cfg.MailServices = services
}
}
Global = cfg
return cfg
}
// SaveToDB 保存配置到数据库
func SaveToDB() error {
if configDB == nil || Global == nil {
return nil
}
configMu.RLock()
cfg := Global
configMu.RUnlock()
configDB.SetConfig("s2a_api_base", cfg.S2AApiBase)
configDB.SetConfig("s2a_admin_key", cfg.S2AAdminKey)
configDB.SetConfig("concurrency", strconv.Itoa(cfg.Concurrency))
configDB.SetConfig("priority", strconv.Itoa(cfg.Priority))
if len(cfg.GroupIDs) > 0 {
var ids []string
for _, id := range cfg.GroupIDs {
ids = append(ids, strconv.Itoa(id))
}
configDB.SetConfig("group_ids", strings.Join(ids, ","))
}
configDB.SetConfig("proxy_enabled", strconv.FormatBool(cfg.ProxyEnabled))
configDB.SetConfig("default_proxy", cfg.DefaultProxy)
if len(cfg.MailServices) > 0 {
data, _ := json.Marshal(cfg.MailServices)
configDB.SetConfig("mail_services", string(data))
}
return nil
}
// Update 更新配置 (实时生效)
func Update(cfg *Config) error {
configMu.Lock()
Global = cfg
configMu.Unlock()
return SaveToDB()
}
// Get 获取当前配置
func Get() *Config {
configMu.RLock()
defer configMu.RUnlock()
return Global
}
// FindPath 查找配置文件路径 (兼容)
func FindPath() string {
if envPath := os.Getenv("CONFIG_PATH"); envPath != "" {
return envPath
}
return "data/config.yaml"
}
// Load 加载配置 (兼容旧代码,现在直接从数据库加载)
func Load(path string) (*Config, error) {
return InitFromDB(), nil
}
// LoadAccounts 加载账号列表 (保持 JSON 格式)
func LoadAccounts(path string) ([]Account, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var accounts []Account
if err := json.Unmarshal(data, &accounts); err != nil {
return nil, err
}
return accounts, nil
}
// SaveAccounts 保存账号列表 (保持 JSON 格式)
func SaveAccounts(path string, accounts []Account) error {
data, err := json.MarshalIndent(accounts, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}

View File

@@ -0,0 +1,272 @@
package database
import (
"database/sql"
"fmt"
"time"
_ "github.com/mattn/go-sqlite3"
)
// TeamOwner 账号结构
type TeamOwner struct {
ID int64 `json:"id"`
Email string `json:"email"`
Password string `json:"password,omitempty"`
Token string `json:"token,omitempty"`
AccountID string `json:"account_id"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
// DB 数据库管理器
type DB struct {
db *sql.DB
}
// 全局数据库实例
var Instance *DB
// Init 初始化数据库
func Init(dbPath string) error {
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return fmt.Errorf("打开数据库失败: %w", err)
}
Instance = &DB{db: db}
if err := Instance.createTables(); err != nil {
return fmt.Errorf("创建表失败: %w", err)
}
fmt.Printf("[数据库] SQLite 已连接: %s\n", dbPath)
return nil
}
// createTables 创建表
func (d *DB) createTables() error {
_, err := d.db.Exec(`
CREATE TABLE IF NOT EXISTS team_owners (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
password TEXT,
token TEXT,
account_id TEXT NOT NULL,
status TEXT DEFAULT 'valid',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_team_owners_email ON team_owners(email);
CREATE INDEX IF NOT EXISTS idx_team_owners_status ON team_owners(status);
CREATE INDEX IF NOT EXISTS idx_team_owners_account_id ON team_owners(account_id);
-- 配置表 (key-value 形式)
CREATE TABLE IF NOT EXISTS app_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`)
return err
}
// GetConfig 获取配置值
func (d *DB) GetConfig(key string) (string, error) {
var value string
err := d.db.QueryRow("SELECT value FROM app_config WHERE key = ?", key).Scan(&value)
if err == sql.ErrNoRows {
return "", nil
}
return value, err
}
// SetConfig 设置配置值
func (d *DB) SetConfig(key, value string) error {
_, err := d.db.Exec(`
INSERT INTO app_config (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = CURRENT_TIMESTAMP
`, key, value, value)
return err
}
// GetAllConfig 获取所有配置
func (d *DB) GetAllConfig() (map[string]string, error) {
rows, err := d.db.Query("SELECT key, value FROM app_config")
if err != nil {
return nil, err
}
defer rows.Close()
result := make(map[string]string)
for rows.Next() {
var key, value string
if err := rows.Scan(&key, &value); err != nil {
continue
}
result[key] = value
}
return result, nil
}
// AddTeamOwner 添加 Team Owner
func (d *DB) AddTeamOwner(owner TeamOwner) (int64, error) {
result, err := d.db.Exec(`
INSERT OR REPLACE INTO team_owners (email, password, token, account_id, status, created_at)
VALUES (?, ?, ?, ?, 'valid', CURRENT_TIMESTAMP)
`, owner.Email, owner.Password, owner.Token, owner.AccountID)
if err != nil {
return 0, err
}
return result.LastInsertId()
}
// AddTeamOwners 批量添加
func (d *DB) AddTeamOwners(owners []TeamOwner) (int, error) {
tx, err := d.db.Begin()
if err != nil {
return 0, err
}
defer tx.Rollback()
stmt, err := tx.Prepare(`
INSERT OR REPLACE INTO team_owners (email, password, token, account_id, status, created_at)
VALUES (?, ?, ?, ?, 'valid', CURRENT_TIMESTAMP)
`)
if err != nil {
return 0, err
}
defer stmt.Close()
count := 0
for _, owner := range owners {
_, err := stmt.Exec(owner.Email, owner.Password, owner.Token, owner.AccountID)
if err != nil {
fmt.Printf("[数据库] 插入失败 %s: %v\n", owner.Email, err)
continue
}
count++
}
if err := tx.Commit(); err != nil {
return 0, err
}
return count, nil
}
// GetTeamOwners 获取列表
func (d *DB) GetTeamOwners(status string, limit, offset int) ([]TeamOwner, int, error) {
query := "SELECT id, email, password, token, account_id, status, created_at FROM team_owners WHERE 1=1"
countQuery := "SELECT COUNT(*) FROM team_owners WHERE 1=1"
args := []interface{}{}
if status != "" {
query += " AND status = ?"
countQuery += " AND status = ?"
args = append(args, status)
}
var total int
err := d.db.QueryRow(countQuery, args...).Scan(&total)
if err != nil {
return nil, 0, err
}
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
args = append(args, limit, offset)
rows, err := d.db.Query(query, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var owners []TeamOwner
for rows.Next() {
var owner TeamOwner
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt)
if err != nil {
continue
}
owners = append(owners, owner)
}
return owners, total, nil
}
// GetPendingOwners 获取待处理
func (d *DB) GetPendingOwners() ([]TeamOwner, error) {
rows, err := d.db.Query(`
SELECT id, email, password, token, account_id, status, created_at
FROM team_owners WHERE status = 'valid'
ORDER BY created_at ASC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var owners []TeamOwner
for rows.Next() {
var owner TeamOwner
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt)
if err != nil {
continue
}
owners = append(owners, owner)
}
return owners, nil
}
// UpdateOwnerStatus 更新状态
func (d *DB) UpdateOwnerStatus(id int64, status string) error {
_, err := d.db.Exec("UPDATE team_owners SET status = ? WHERE id = ?", status, id)
return err
}
// DeleteTeamOwner 删除
func (d *DB) DeleteTeamOwner(id int64) error {
_, err := d.db.Exec("DELETE FROM team_owners WHERE id = ?", id)
return err
}
// ClearTeamOwners 清空
func (d *DB) ClearTeamOwners() error {
_, err := d.db.Exec("DELETE FROM team_owners")
return err
}
// GetOwnerStats 获取统计
func (d *DB) GetOwnerStats() map[string]int {
stats := map[string]int{
"total": 0,
"valid": 0,
"registered": 0,
"pooled": 0,
}
var count int
if err := d.db.QueryRow("SELECT COUNT(*) FROM team_owners").Scan(&count); err == nil {
stats["total"] = count
}
if err := d.db.QueryRow("SELECT COUNT(*) FROM team_owners WHERE status = 'valid'").Scan(&count); err == nil {
stats["valid"] = count
}
if err := d.db.QueryRow("SELECT COUNT(*) FROM team_owners WHERE status = 'registered'").Scan(&count); err == nil {
stats["registered"] = count
}
if err := d.db.QueryRow("SELECT COUNT(*) FROM team_owners WHERE status = 'pooled'").Scan(&count); err == nil {
stats["pooled"] = count
}
return stats
}
// Close 关闭数据库
func (d *DB) Close() error {
if d.db != nil {
return d.db.Close()
}
return nil
}

View File

@@ -0,0 +1,190 @@
package invite
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"codex-pool/internal/client"
)
// DefaultProxy 默认代理
const DefaultProxy = "http://127.0.0.1:7890"
// TeamInviter Team 邀请器
type TeamInviter struct {
client *client.TLSClient
accessToken string
accountID string
}
// InviteRequest 邀请请求
type InviteRequest struct {
EmailAddresses []string `json:"email_addresses"`
Role string `json:"role"`
ResendEmails bool `json:"resend_emails"`
}
// AccountCheckResponse 账号检查响应
type AccountCheckResponse struct {
Accounts map[string]struct {
Account struct {
PlanType string `json:"plan_type"`
} `json:"account"`
} `json:"accounts"`
}
// New 创建邀请器 (使用默认代理)
func New(accessToken string) *TeamInviter {
c, _ := client.New(DefaultProxy)
return &TeamInviter{
client: c,
accessToken: accessToken,
}
}
// NewWithProxy 创建邀请器 (指定代理)
func NewWithProxy(accessToken, proxy string) *TeamInviter {
c, _ := client.New(proxy)
return &TeamInviter{
client: c,
accessToken: accessToken,
}
}
// GetAccountID 获取 Team 的 account_id (workspace_id)
func (t *TeamInviter) GetAccountID() (string, error) {
req, _ := http.NewRequest("GET", "https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27", nil)
req.Header.Set("Authorization", "Bearer "+t.accessToken)
req.Header.Set("Accept", "application/json")
resp, err := t.client.Do(req)
if err != nil {
return "", fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)[:min(200, len(body))])
}
var result AccountCheckResponse
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("解析失败: %v", err)
}
// 查找 team plan 的 account_id
for accountID, info := range result.Accounts {
if accountID != "default" && info.Account.PlanType == "team" {
t.accountID = accountID
return accountID, nil
}
}
// 如果没找到 team返回第一个非 default 的
for accountID := range result.Accounts {
if accountID != "default" {
t.accountID = accountID
return accountID, nil
}
}
return "", fmt.Errorf("未找到 account_id")
}
// SendInvites 发送邀请
func (t *TeamInviter) SendInvites(emails []string) error {
if t.accountID == "" {
return fmt.Errorf("未设置 account_id请先调用 GetAccountID()")
}
url := fmt.Sprintf("https://chatgpt.com/backend-api/accounts/%s/invites", t.accountID)
payload := InviteRequest{
EmailAddresses: emails,
Role: "standard-user",
ResendEmails: true,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+t.accessToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Chatgpt-Account-Id", t.accountID)
resp, err := t.client.Do(req)
if err != nil {
return fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 && resp.StatusCode != 201 {
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody)[:min(200, len(respBody))])
}
return nil
}
// GetPendingInvites 获取待处理的邀请列表
func (t *TeamInviter) GetPendingInvites() ([]string, error) {
if t.accountID == "" {
return nil, fmt.Errorf("未设置 account_id")
}
url := fmt.Sprintf("https://chatgpt.com/backend-api/accounts/%s/invites?offset=0&limit=100&query=", t.accountID)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+t.accessToken)
resp, err := t.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result struct {
Invites []struct {
Email string `json:"email"`
} `json:"invites"`
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
var emails []string
for _, inv := range result.Invites {
emails = append(emails, inv.Email)
}
return emails, nil
}
// AcceptInvite 接受邀请 (使用被邀请账号的 token)
func AcceptInvite(inviteLink string, accessToken string) error {
c, _ := client.New(DefaultProxy)
req, _ := http.NewRequest("GET", inviteLink, nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := c.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 && resp.StatusCode != 302 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)[:min(100, len(body))])
}
return nil
}

View File

@@ -0,0 +1,140 @@
package logger
import (
"fmt"
"sync"
"time"
)
// LogEntry 日志条目
type LogEntry struct {
Timestamp time.Time `json:"timestamp"`
Level string `json:"level"`
Message string `json:"message"`
Email string `json:"email,omitempty"`
Module string `json:"module,omitempty"`
}
// 日志存储
var (
logs = make([]LogEntry, 0, 1000)
logsMu sync.RWMutex
listeners = make(map[string]chan LogEntry)
listMu sync.RWMutex
)
// AddListener 添加日志监听器
func AddListener(id string) chan LogEntry {
listMu.Lock()
defer listMu.Unlock()
ch := make(chan LogEntry, 100)
listeners[id] = ch
return ch
}
// RemoveListener 移除日志监听器
func RemoveListener(id string) {
listMu.Lock()
defer listMu.Unlock()
if ch, ok := listeners[id]; ok {
close(ch)
delete(listeners, id)
}
}
// broadcast 广播日志
func broadcast(entry LogEntry) {
listMu.RLock()
defer listMu.RUnlock()
for _, ch := range listeners {
select {
case ch <- entry:
default:
}
}
}
// log 记录日志
func log(level, message, email, module string) {
entry := LogEntry{
Timestamp: time.Now(),
Level: level,
Message: message,
Email: email,
Module: module,
}
logsMu.Lock()
if len(logs) >= 1000 {
logs = logs[100:]
}
logs = append(logs, entry)
logsMu.Unlock()
broadcast(entry)
// 打印到控制台
prefix := ""
switch level {
case "info":
prefix = "[INFO]"
case "success":
prefix = "[SUCCESS]"
case "error":
prefix = "[ERROR]"
case "warning":
prefix = "[WARN]"
}
if email != "" {
fmt.Printf("%s [%s] %s - %s\n", prefix, module, email, message)
} else {
fmt.Printf("%s [%s] %s\n", prefix, module, message)
}
}
// Info 记录信息日志
func Info(message, email, module string) {
log("info", message, email, module)
}
// Success 记录成功日志
func Success(message, email, module string) {
log("success", message, email, module)
}
// Error 记录错误日志
func Error(message, email, module string) {
log("error", message, email, module)
}
// Warning 记录警告日志
func Warning(message, email, module string) {
log("warning", message, email, module)
}
// GetLogs 获取日志
func GetLogs(limit int) []LogEntry {
logsMu.RLock()
defer logsMu.RUnlock()
if limit <= 0 || limit > len(logs) {
limit = len(logs)
}
start := len(logs) - limit
if start < 0 {
start = 0
}
result := make([]LogEntry, limit)
copy(result, logs[start:])
return result
}
// ClearLogs 清空日志
func ClearLogs() {
logsMu.Lock()
defer logsMu.Unlock()
logs = make([]LogEntry, 0, 1000)
}

View File

@@ -0,0 +1,455 @@
package mail
import (
"bytes"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"regexp"
"strings"
"sync"
"time"
"codex-pool/internal/config"
)
// 默认邮箱配置
var defaultMailServices = []config.MailServiceConfig{
{
Name: "esyteam",
APIBase: "https://mail.esyteam.edu.kg",
APIToken: "005d6f3e-5312-4c37-8125-e1f71243e1ba",
Domain: "esyteam.edu.kg",
EmailPath: "/api/public/emailList",
AddUserAPI: "/api/public/addUser",
},
}
// 全局变量
var (
currentMailServices []config.MailServiceConfig
mailServicesMutex sync.RWMutex
currentServiceIndex int
)
func init() {
currentMailServices = defaultMailServices
}
// Init 初始化邮箱服务配置
func Init(services []config.MailServiceConfig) {
mailServicesMutex.Lock()
defer mailServicesMutex.Unlock()
if len(services) > 0 {
for i := range services {
if services[i].EmailPath == "" {
services[i].EmailPath = "/api/public/emailList"
}
if services[i].AddUserAPI == "" {
services[i].AddUserAPI = "/api/public/addUser"
}
if services[i].Name == "" {
services[i].Name = fmt.Sprintf("mail-service-%d", i+1)
}
}
currentMailServices = services
fmt.Printf("[邮箱] 已加载 %d 个邮箱服务配置:\n", len(services))
for _, s := range services {
fmt.Printf(" - %s (%s) @ %s\n", s.Name, s.Domain, s.APIBase)
}
} else {
currentMailServices = defaultMailServices
fmt.Println("[邮箱] 使用默认邮箱服务配置")
}
currentServiceIndex = 0
}
// GetServices 获取当前邮箱服务配置
func GetServices() []config.MailServiceConfig {
mailServicesMutex.RLock()
defer mailServicesMutex.RUnlock()
return currentMailServices
}
// GetNextService 轮询获取下一个邮箱服务
func GetNextService() config.MailServiceConfig {
mailServicesMutex.Lock()
defer mailServicesMutex.Unlock()
if len(currentMailServices) == 0 {
return defaultMailServices[0]
}
service := currentMailServices[currentServiceIndex]
currentServiceIndex = (currentServiceIndex + 1) % len(currentMailServices)
return service
}
// GetRandomService 随机获取一个邮箱服务
func GetRandomService() config.MailServiceConfig {
mailServicesMutex.RLock()
defer mailServicesMutex.RUnlock()
if len(currentMailServices) == 0 {
return defaultMailServices[0]
}
return currentMailServices[rand.Intn(len(currentMailServices))]
}
// GetServiceByDomain 根据域名获取对应的邮箱服务
func GetServiceByDomain(domain string) *config.MailServiceConfig {
mailServicesMutex.RLock()
defer mailServicesMutex.RUnlock()
for _, s := range currentMailServices {
if s.Domain == domain || strings.HasSuffix(domain, "."+s.Domain) {
return &s
}
}
return nil
}
// ==================== 邮件结构 ====================
// EmailListRequest 邮件列表请求
type EmailListRequest struct {
ToEmail string `json:"toEmail"`
TimeSort string `json:"timeSort"`
Size int `json:"size"`
}
// EmailListResponse 邮件列表响应
type EmailListResponse struct {
Code int `json:"code"`
Data []EmailItem `json:"data"`
}
// EmailItem 邮件项
type EmailItem struct {
Content string `json:"content"`
Text string `json:"text"`
Subject string `json:"subject"`
}
// AddUserRequest 创建用户请求
type AddUserRequest struct {
List []AddUserItem `json:"list"`
}
// AddUserItem 用户项
type AddUserItem struct {
Email string `json:"email"`
Password string `json:"password"`
}
// AddUserResponse 创建用户响应
type AddUserResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
// ==================== 邮箱生成 ====================
// GenerateEmail 生成随机邮箱并在邮件系统中创建
func GenerateEmail() string {
return GenerateEmailWithService(GetNextService())
}
// GenerateEmailWithService 使用指定服务生成随机邮箱
func GenerateEmailWithService(service config.MailServiceConfig) string {
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 10)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
email := string(b) + "@" + service.Domain
if err := CreateMailboxWithService(email, service); err != nil {
fmt.Printf(" [!] 创建邮箱失败 (%s): %v (继续尝试)\n", service.Name, err)
}
return email
}
// CreateMailbox 在邮件系统中创建邮箱
func CreateMailbox(email string) error {
parts := strings.Split(email, "@")
if len(parts) != 2 {
return fmt.Errorf("无效的邮箱地址: %s", email)
}
domain := parts[1]
service := GetServiceByDomain(domain)
if service == nil {
services := GetServices()
if len(services) > 0 {
service = &services[0]
} else {
return fmt.Errorf("没有可用的邮箱服务")
}
}
return CreateMailboxWithService(email, *service)
}
// CreateMailboxWithService 使用指定服务在邮件系统中创建邮箱
func CreateMailboxWithService(email string, service config.MailServiceConfig) error {
client := &http.Client{Timeout: 10 * time.Second}
parts := strings.Split(email, "@")
if len(parts) == 2 {
domain := parts[1]
if strings.HasSuffix(domain, "."+service.Domain) {
email = parts[0] + "@" + service.Domain
}
}
payload := AddUserRequest{
List: []AddUserItem{
{Email: email, Password: GeneratePassword()},
},
}
jsonData, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", service.APIBase+service.AddUserAPI, bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Authorization", service.APIToken)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var result AddUserResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
if result.Code != 200 {
if strings.Contains(result.Message, "exist") {
return nil
}
return fmt.Errorf("API 错误: %s", result.Message)
}
return nil
}
// GeneratePassword 生成随机密码
func GeneratePassword() string {
const (
upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lower = "abcdefghijklmnopqrstuvwxyz"
digits = "0123456789"
special = "#$%@!"
)
password := make([]byte, 12)
password[0] = upper[rand.Intn(len(upper))]
password[1] = lower[rand.Intn(len(lower))]
password[10] = digits[rand.Intn(len(digits))]
password[11] = special[rand.Intn(len(special))]
charset := upper + lower
for i := 2; i < 10; i++ {
password[i] = charset[rand.Intn(len(charset))]
}
return string(password)
}
// ==================== 邮件客户端 ====================
// Client 邮件客户端
type Client struct {
client *http.Client
service *config.MailServiceConfig
}
// NewClient 创建邮件客户端
func NewClient() *Client {
services := GetServices()
var service *config.MailServiceConfig
if len(services) > 0 {
service = &services[0]
} else {
service = &defaultMailServices[0]
}
return &Client{
client: &http.Client{Timeout: 10 * time.Second},
service: service,
}
}
// NewClientWithService 创建指定服务的邮件客户端
func NewClientWithService(service config.MailServiceConfig) *Client {
return &Client{
client: &http.Client{Timeout: 10 * time.Second},
service: &service,
}
}
// NewClientForEmail 根据邮箱地址创建对应的邮件客户端
func NewClientForEmail(email string) *Client {
parts := strings.Split(email, "@")
if len(parts) == 2 {
if service := GetServiceByDomain(parts[1]); service != nil {
return NewClientWithService(*service)
}
}
return NewClient()
}
// GetEmails 获取邮件列表
func (m *Client) GetEmails(email string, size int) ([]EmailItem, error) {
service := m.service
parts := strings.Split(email, "@")
if len(parts) == 2 {
if s := GetServiceByDomain(parts[1]); s != nil {
service = s
}
}
url := service.APIBase + service.EmailPath
payload := EmailListRequest{
ToEmail: email,
TimeSort: "desc",
Size: size,
}
jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
req.Header.Set("Authorization", service.APIToken)
req.Header.Set("Content-Type", "application/json")
resp, err := m.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
}
var result EmailListResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
if result.Code != 200 {
return nil, fmt.Errorf("API 错误: %d", result.Code)
}
return result.Data, nil
}
// WaitForCode 等待验证码邮件
func (m *Client) WaitForCode(email string, timeout time.Duration) (string, error) {
start := time.Now()
codeRegex := regexp.MustCompile(`\b(\d{6})\b`)
for time.Since(start) < timeout {
emails, err := m.GetEmails(email, 10)
if err == nil {
for _, mail := range emails {
subject := strings.ToLower(mail.Subject)
// 匹配多种可能的验证码邮件主题
isCodeEmail := strings.Contains(subject, "code") ||
strings.Contains(subject, "verify") ||
strings.Contains(subject, "verification") ||
strings.Contains(subject, "openai") ||
strings.Contains(subject, "confirm")
if !isCodeEmail {
continue
}
content := mail.Content
if content == "" {
content = mail.Text
}
matches := codeRegex.FindStringSubmatch(content)
if len(matches) >= 2 {
return matches[1], nil
}
}
}
time.Sleep(2 * time.Second)
}
return "", fmt.Errorf("验证码获取超时")
}
// WaitForInviteLink 等待邀请邮件并提取链接
func (m *Client) WaitForInviteLink(email string, timeout time.Duration) (string, error) {
start := time.Now()
for time.Since(start) < timeout {
emails, err := m.GetEmails(email, 10)
if err == nil {
for _, mail := range emails {
content := mail.Content
if content == "" {
content = mail.Text
}
if strings.Contains(mail.Subject, "invite") ||
strings.Contains(mail.Subject, "Team") ||
strings.Contains(mail.Subject, "ChatGPT") ||
strings.Contains(content, "invite") {
link := extractInviteLink(content)
if link != "" {
return link, nil
}
}
}
}
time.Sleep(2 * time.Second)
}
return "", fmt.Errorf("等待邀请邮件超时")
}
// extractInviteLink 从邮件内容提取邀请链接
func extractInviteLink(content string) string {
patterns := []string{
`https://chatgpt\.com/invite/[^\s"'<>]+`,
`https://chat\.openai\.com/invite/[^\s"'<>]+`,
`https://chatgpt\.com/[^\s"'<>]*accept[^\s"'<>]*`,
}
for _, pattern := range patterns {
re := regexp.MustCompile(pattern)
match := re.FindString(content)
if match != "" {
match = strings.ReplaceAll(match, "&amp;", "&")
return match
}
}
return ""
}
// ==================== 便捷函数 ====================
// WaitForInviteEmail 等待邀请邮件
func WaitForInviteEmail(email string, timeout time.Duration) (string, error) {
client := NewClientForEmail(email)
return client.WaitForInviteLink(email, timeout)
}
// GetVerificationCode 获取验证码
func GetVerificationCode(email string, timeout time.Duration) (string, error) {
client := NewClientForEmail(email)
return client.WaitForCode(email, timeout)
}

View File

@@ -0,0 +1,415 @@
package register
import (
"bytes"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"net/url"
"strings"
"time"
"codex-pool/internal/client"
"codex-pool/internal/mail"
)
// ChatGPTReg ChatGPT 注册器
type ChatGPTReg struct {
Proxy string
Client *client.TLSClient
AuthSessionLoggingID string
OAIDid string
CSRFToken string
AuthorizeURL string
AccessToken string
}
// Result 注册结果
type Result struct {
Email string `json:"email"`
Password string `json:"password"`
Name string `json:"name"`
AccessToken string `json:"access_token"`
}
// New 创建注册器
func New(proxy string) (*ChatGPTReg, error) {
c, err := client.New(proxy)
if err != nil {
return nil, err
}
return &ChatGPTReg{
Proxy: proxy,
Client: c,
AuthSessionLoggingID: GenerateUUID(),
}, nil
}
// InitSession 初始化会话
func (r *ChatGPTReg) InitSession() error {
resp, err := r.Client.Get("https://chatgpt.com")
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("初始化失败,状态码: %d", resp.StatusCode)
}
r.OAIDid = r.Client.GetCookie("https://chatgpt.com", "oai-did")
csrfCookie := r.Client.GetCookie("https://chatgpt.com", "__Host-next-auth.csrf-token")
if csrfCookie != "" {
decoded, err := url.QueryUnescape(csrfCookie)
if err == nil {
parts := strings.Split(decoded, "|")
if len(parts) > 0 {
r.CSRFToken = parts[0]
}
}
}
if r.CSRFToken == "" {
return fmt.Errorf("无法获取 CSRF token")
}
loginURL := fmt.Sprintf("https://chatgpt.com/auth/login?openaicom-did=%s", r.OAIDid)
loginResp, err := r.Client.Get(loginURL)
if err != nil {
return err
}
defer loginResp.Body.Close()
return nil
}
// GetAuthorizeURL 获取授权 URL
func (r *ChatGPTReg) GetAuthorizeURL(email string) error {
loginURL := fmt.Sprintf(
"https://chatgpt.com/api/auth/signin/openai?prompt=login&ext-oai-did=%s&auth_session_logging_id=%s&screen_hint=login_or_signup&login_hint=%s",
r.OAIDid,
r.AuthSessionLoggingID,
url.QueryEscape(email),
)
data := url.Values{}
data.Set("callbackUrl", "https://chatgpt.com/")
data.Set("csrfToken", r.CSRFToken)
data.Set("json", "true")
req, err := http.NewRequest("POST", loginURL, strings.NewReader(data.Encode()))
if err != nil {
return err
}
req.Header.Set("Origin", "https://chatgpt.com")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := r.Client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
if authURL, ok := result["url"].(string); ok && strings.Contains(authURL, "auth.openai.com") {
r.AuthorizeURL = authURL
return nil
}
return fmt.Errorf("无法获取授权 URL")
}
// StartAuthorize 开始授权流程
func (r *ChatGPTReg) StartAuthorize() error {
resp, err := r.Client.Get(r.AuthorizeURL)
if err != nil {
return err
}
defer resp.Body.Close()
finalURL := resp.Request.URL.String()
if strings.Contains(finalURL, "create-account") || strings.Contains(finalURL, "log-in") {
return nil
}
return fmt.Errorf("授权流程启动失败")
}
// Register 注册账户
func (r *ChatGPTReg) Register(email, password string) error {
payload := map[string]string{
"password": password,
"username": email,
}
jsonData, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", "https://auth.openai.com/api/accounts/user/register", bytes.NewReader(jsonData))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Origin", "https://auth.openai.com")
resp, err := r.Client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := client.ReadBodyString(resp)
return fmt.Errorf("注册失败,状态码: %d, 响应: %s", resp.StatusCode, truncateStr(body, 200))
}
return nil
}
// SendVerificationEmail 发送验证邮件
func (r *ChatGPTReg) SendVerificationEmail() error {
resp, err := r.Client.Get("https://auth.openai.com/api/accounts/email-otp/send")
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("发送验证邮件失败,状态码: %d", resp.StatusCode)
}
return nil
}
// ValidateOTP 验证 OTP
func (r *ChatGPTReg) ValidateOTP(code string) error {
payload := map[string]string{"code": code}
jsonData, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", "https://auth.openai.com/api/accounts/email-otp/validate", bytes.NewReader(jsonData))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Origin", "https://auth.openai.com")
resp, err := r.Client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("OTP 验证失败,状态码: %d", resp.StatusCode)
}
return nil
}
// CreateAccount 创建账户
func (r *ChatGPTReg) CreateAccount(name, birthdate string) error {
payload := map[string]string{
"name": name,
"birthdate": birthdate,
}
jsonData, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", "https://auth.openai.com/api/accounts/create_account", bytes.NewReader(jsonData))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Origin", "https://auth.openai.com")
resp, err := r.Client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("创建账户失败,状态码: %d", resp.StatusCode)
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err == nil {
if continueURL, ok := result["continue_url"].(string); ok && continueURL != "" {
contResp, err := r.Client.Get(continueURL)
if err == nil {
contResp.Body.Close()
}
}
}
return nil
}
// GetSessionToken 获取 access token
func (r *ChatGPTReg) GetSessionToken() error {
resp, err := r.Client.Get("https://chatgpt.com/api/auth/session")
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("获取 session 失败,状态码: %d", resp.StatusCode)
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
if token, ok := result["accessToken"].(string); ok {
r.AccessToken = token
return nil
}
return fmt.Errorf("响应中没有 accessToken")
}
// Run 完整的注册流程
func Run(email, password, name, birthdate, proxy string) (*ChatGPTReg, error) {
return RunWithRetry(email, password, name, birthdate, proxy, 3)
}
// RunWithRetry 带重试的注册流程
// 当验证码获取超过5秒就换新邮箱重新注册
func RunWithRetry(email, password, name, birthdate, proxy string, maxRetries int) (*ChatGPTReg, error) {
for attempt := 0; attempt < maxRetries; attempt++ {
if attempt > 0 {
// 重试时生成新邮箱
email = mail.GenerateEmail()
password = GeneratePassword()
fmt.Printf(" [Retry %d] New email: %s\n", attempt, email)
}
reg, err := runOnce(email, password, name, birthdate, proxy)
if err == nil {
return reg, nil
}
// 如果不是验证码超时错误,直接返回
if !strings.Contains(err.Error(), "验证码获取超时") {
return nil, err
}
fmt.Printf(" [!] OTP timeout, retrying with new email...\n")
}
return nil, fmt.Errorf("注册失败: 已重试 %d 次", maxRetries)
}
// runOnce 执行一次注册流程(使用短超时获取验证码)
func runOnce(email, password, name, birthdate, proxy string) (*ChatGPTReg, error) {
reg, err := 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)
}
// 先用5秒超时尝试获取验证码
otpCode, err := mail.GetVerificationCode(email, 5*time.Second)
if err != nil {
// 5秒内没获取到再等120秒总共等待更多时间
otpCode, err = mail.GetVerificationCode(email, 120*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)
}
// 获取 Token
_ = reg.GetSessionToken()
return reg, nil
}
// ==================== 工具函数 ====================
// GenerateName 生成随机姓名
func GenerateName() string {
firstNames := []string{"James", "John", "Robert", "Michael", "David", "William", "Richard", "Joseph", "Thomas", "Charles"}
lastNames := []string{"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"}
return firstNames[rand.Intn(len(firstNames))] + " " + lastNames[rand.Intn(len(lastNames))]
}
// GenerateUUID 生成 UUID
func GenerateUUID() string {
b := make([]byte, 16)
rand.Read(b)
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}
// GenerateBirthdate 生成随机生日
func GenerateBirthdate() string {
year := 2000 + rand.Intn(5)
month := 1 + rand.Intn(12)
day := 1 + rand.Intn(28)
return fmt.Sprintf("%d-%02d-%02d", year, month, day)
}
// GeneratePassword 生成随机密码
func GeneratePassword() string {
const (
upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lower = "abcdefghijklmnopqrstuvwxyz"
digits = "0123456789"
special = "!@#$%"
)
b := make([]byte, 13)
for i := 0; i < 2; i++ {
b[i] = upper[rand.Intn(len(upper))]
}
for i := 2; i < 10; i++ {
b[i] = lower[rand.Intn(len(lower))]
}
for i := 10; i < 12; i++ {
b[i] = digits[rand.Intn(len(digits))]
}
b[12] = special[rand.Intn(len(special))]
return string(b)
}
func truncateStr(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}

View File

@@ -0,0 +1,18 @@
//go:build !embed
// +build !embed
package web
import (
"net/http"
)
// GetFileSystem 返回 nil开发模式不嵌入前端
func GetFileSystem() http.FileSystem {
return nil
}
// IsEmbedded 返回前端是否已嵌入
func IsEmbedded() bool {
return false
}

View File

@@ -0,0 +1,27 @@
//go:build embed
// +build embed
package web
import (
"embed"
"io/fs"
"net/http"
)
//go:embed dist/*
var distFS embed.FS
// GetFileSystem 返回嵌入的前端文件系统
func GetFileSystem() http.FileSystem {
sub, err := fs.Sub(distFS, "dist")
if err != nil {
panic(err)
}
return http.FS(sub)
}
// IsEmbedded 返回前端是否已嵌入
func IsEmbedded() bool {
return true
}