feat: Add automated ChatGPT account registration with backend API, TLS client, and fingerprinting, alongside new frontend pages for configuration, monitoring, and upload.
This commit is contained in:
172
backend/internal/api/codex_proxy.go
Normal file
172
backend/internal/api/codex_proxy.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"codex-pool/internal/database"
|
||||
)
|
||||
|
||||
// HandleCodexProxies 处理代理池请求
|
||||
func HandleCodexProxies(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
listCodexProxies(w, r)
|
||||
case http.MethodPost:
|
||||
addCodexProxy(w, r)
|
||||
case http.MethodDelete:
|
||||
deleteCodexProxy(w, r)
|
||||
case http.MethodPut:
|
||||
toggleCodexProxy(w, r)
|
||||
default:
|
||||
Error(w, http.StatusMethodNotAllowed, "方法不允许")
|
||||
}
|
||||
}
|
||||
|
||||
// listCodexProxies 获取代理列表
|
||||
func listCodexProxies(w http.ResponseWriter, r *http.Request) {
|
||||
proxies, err := database.Instance.GetCodexProxies()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "获取代理列表失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
stats := database.Instance.GetCodexProxyStats()
|
||||
|
||||
Success(w, map[string]interface{}{
|
||||
"proxies": proxies,
|
||||
"stats": stats,
|
||||
})
|
||||
}
|
||||
|
||||
// AddProxyRequest 添加代理请求
|
||||
type AddProxyRequest struct {
|
||||
ProxyURL string `json:"proxy_url"`
|
||||
Description string `json:"description"`
|
||||
Proxies []string `json:"proxies"` // 批量添加
|
||||
}
|
||||
|
||||
// addCodexProxy 添加代理
|
||||
func addCodexProxy(w http.ResponseWriter, r *http.Request) {
|
||||
var req AddProxyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, http.StatusBadRequest, "请求参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 批量添加
|
||||
if len(req.Proxies) > 0 {
|
||||
// 过滤空行和格式化
|
||||
var validProxies []string
|
||||
for _, p := range req.Proxies {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
// 如果没有协议前缀,自动添加 http://
|
||||
if !strings.HasPrefix(p, "http://") && !strings.HasPrefix(p, "https://") && !strings.HasPrefix(p, "socks5://") {
|
||||
p = "http://" + p
|
||||
}
|
||||
validProxies = append(validProxies, p)
|
||||
}
|
||||
}
|
||||
|
||||
if len(validProxies) == 0 {
|
||||
Error(w, http.StatusBadRequest, "没有有效的代理地址")
|
||||
return
|
||||
}
|
||||
|
||||
count, err := database.Instance.AddCodexProxies(validProxies)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "批量添加代理失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, map[string]interface{}{
|
||||
"added": count,
|
||||
"total": len(validProxies),
|
||||
"message": "批量添加成功",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 单个添加
|
||||
if req.ProxyURL == "" {
|
||||
Error(w, http.StatusBadRequest, "代理地址不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
proxyURL := strings.TrimSpace(req.ProxyURL)
|
||||
if !strings.HasPrefix(proxyURL, "http://") && !strings.HasPrefix(proxyURL, "https://") && !strings.HasPrefix(proxyURL, "socks5://") {
|
||||
proxyURL = "http://" + proxyURL
|
||||
}
|
||||
|
||||
id, err := database.Instance.AddCodexProxy(proxyURL, req.Description)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
Error(w, http.StatusConflict, "代理地址已存在")
|
||||
return
|
||||
}
|
||||
Error(w, http.StatusInternalServerError, "添加代理失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, map[string]interface{}{
|
||||
"id": id,
|
||||
"message": "添加成功",
|
||||
})
|
||||
}
|
||||
|
||||
// deleteCodexProxy 删除代理
|
||||
func deleteCodexProxy(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.URL.Query().Get("id")
|
||||
if idStr == "" {
|
||||
// 如果没有 id 参数,检查是否要清空所有
|
||||
if r.URL.Query().Get("all") == "true" {
|
||||
err := database.Instance.ClearCodexProxies()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "清空代理失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
Success(w, map[string]string{"message": "已清空所有代理"})
|
||||
return
|
||||
}
|
||||
Error(w, http.StatusBadRequest, "缺少代理 ID")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, "无效的代理 ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.Instance.DeleteCodexProxy(id); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "删除代理失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, map[string]string{"message": "删除成功"})
|
||||
}
|
||||
|
||||
// toggleCodexProxy 切换代理启用状态
|
||||
func toggleCodexProxy(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.URL.Query().Get("id")
|
||||
if idStr == "" {
|
||||
Error(w, http.StatusBadRequest, "缺少代理 ID")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, "无效的代理 ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.Instance.ToggleCodexProxy(id); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "切换代理状态失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, map[string]string{"message": "状态已切换"})
|
||||
}
|
||||
@@ -66,6 +66,25 @@ type TeamProcessState struct {
|
||||
|
||||
var teamProcessState = &TeamProcessState{}
|
||||
|
||||
// getProxyDisplay 获取代理显示名称(隐藏密码)
|
||||
func getProxyDisplay(proxy string) string {
|
||||
if proxy == "" {
|
||||
return "无代理"
|
||||
}
|
||||
// 尝试解析 URL,只返回 host 部分
|
||||
if strings.Contains(proxy, "@") {
|
||||
parts := strings.Split(proxy, "@")
|
||||
if len(parts) >= 2 {
|
||||
return parts[len(parts)-1] // 返回 @ 后面的 host:port 部分
|
||||
}
|
||||
}
|
||||
// 去掉协议前缀
|
||||
proxy = strings.TrimPrefix(proxy, "http://")
|
||||
proxy = strings.TrimPrefix(proxy, "https://")
|
||||
proxy = strings.TrimPrefix(proxy, "socks5://")
|
||||
return proxy
|
||||
}
|
||||
|
||||
// HandleTeamProcess POST /api/team/process - 启动 Team 批量处理
|
||||
func HandleTeamProcess(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
@@ -593,7 +612,7 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
|
||||
}
|
||||
|
||||
// 注册
|
||||
_, err := registerWithTimeout(currentEmail, currentPassword, name, birthdate, req.Proxy)
|
||||
_, err := register.APIRegister(currentEmail, currentPassword, name, birthdate, req.Proxy, memberLogPrefix)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("%s [注册失败] %v", memberLogPrefix, err), currentEmail, "team")
|
||||
continue
|
||||
@@ -753,15 +772,23 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
|
||||
continue
|
||||
}
|
||||
|
||||
// 根据配置选择浏览器自动化
|
||||
// 根据配置选择授权方式
|
||||
var code string
|
||||
// 根据全局配置决定授权方式
|
||||
if config.Global.AuthMethod == "api" {
|
||||
// 使用纯 API 模式(CodexAuth)- 使用 S2A 生成的授权 URL
|
||||
code, err = auth.CompleteWithCodexAPI(memberChild.Email, memberChild.Password, teamID, s2aResp.Data.AuthURL, s2aResp.Data.SessionID, req.Proxy, authLogger)
|
||||
} else if req.BrowserType == "rod" {
|
||||
code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, memberChild.Email, memberChild.Password, teamID, req.Headless, req.Proxy, authLogger)
|
||||
// 从代理池随机选择代理
|
||||
proxyToUse := req.Proxy
|
||||
if poolProxy, poolErr := database.Instance.GetRandomCodexProxy(); poolErr == nil && poolProxy != "" {
|
||||
proxyToUse = poolProxy
|
||||
logger.Info(fmt.Sprintf("%s 使用代理池: %s", memberLogPrefix, getProxyDisplay(poolProxy)), memberChild.Email, "team")
|
||||
}
|
||||
code, err = auth.CompleteWithCodexAPI(memberChild.Email, memberChild.Password, teamID, s2aResp.Data.AuthURL, s2aResp.Data.SessionID, proxyToUse, authLogger)
|
||||
// 更新代理统计
|
||||
if proxyToUse != req.Proxy && proxyToUse != "" {
|
||||
database.Instance.UpdateCodexProxyStats(proxyToUse, err == nil)
|
||||
}
|
||||
} else {
|
||||
// 使用 Chromedp 浏览器自动化
|
||||
code, err = auth.CompleteWithChromedpLogged(s2aResp.Data.AuthURL, memberChild.Email, memberChild.Password, teamID, req.Headless, req.Proxy, authLogger)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -858,10 +885,19 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
|
||||
// 根据全局配置决定授权方式
|
||||
if config.Global.AuthMethod == "api" {
|
||||
// 使用纯 API 模式(CodexAuth)- 使用 S2A 生成的授权 URL
|
||||
code, err = auth.CompleteWithCodexAPI(owner.Email, owner.Password, teamID, s2aResp.Data.AuthURL, s2aResp.Data.SessionID, req.Proxy, authLogger)
|
||||
} else if req.BrowserType == "rod" {
|
||||
code, err = auth.CompleteWithRodLogged(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy, authLogger)
|
||||
// 从代理池随机选择代理
|
||||
proxyToUse := req.Proxy
|
||||
if poolProxy, poolErr := database.Instance.GetRandomCodexProxy(); poolErr == nil && poolProxy != "" {
|
||||
proxyToUse = poolProxy
|
||||
logger.Info(fmt.Sprintf("%s 使用代理池: %s", ownerLogPrefix, getProxyDisplay(poolProxy)), owner.Email, "team")
|
||||
}
|
||||
code, err = auth.CompleteWithCodexAPI(owner.Email, owner.Password, teamID, s2aResp.Data.AuthURL, s2aResp.Data.SessionID, proxyToUse, authLogger)
|
||||
// 更新代理统计
|
||||
if proxyToUse != req.Proxy && proxyToUse != "" {
|
||||
database.Instance.UpdateCodexProxyStats(proxyToUse, err == nil)
|
||||
}
|
||||
} else {
|
||||
// 使用 Chromedp 浏览器自动化
|
||||
code, err = auth.CompleteWithChromedpLogged(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy, authLogger)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -909,81 +945,3 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// registerWithTimeout 带超时的注册(遇到 403 会换指纹重试)
|
||||
func registerWithTimeout(email, password, name, birthdate, proxy string) (*register.ChatGPTReg, error) {
|
||||
const maxInitRetries = 3
|
||||
var reg *register.ChatGPTReg
|
||||
var initErr error
|
||||
|
||||
// 初始化阶段:遇到 403 换指纹重试
|
||||
for attempt := 0; attempt < maxInitRetries; attempt++ {
|
||||
var err error
|
||||
reg, err = register.New(proxy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := reg.InitSession(); err != nil {
|
||||
initErr = err
|
||||
// 检查是否是 403 错误,换指纹重试
|
||||
if strings.Contains(err.Error(), "403") {
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("初始化失败: %v", err)
|
||||
}
|
||||
|
||||
if err := reg.GetAuthorizeURL(email); err != nil {
|
||||
// 403 也可能在这里出现
|
||||
if strings.Contains(err.Error(), "403") {
|
||||
initErr = err
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("获取授权URL失败: %v", err)
|
||||
}
|
||||
|
||||
if err := reg.StartAuthorize(); err != nil {
|
||||
if strings.Contains(err.Error(), "403") {
|
||||
initErr = err
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("启动授权失败: %v", err)
|
||||
}
|
||||
|
||||
// 初始化成功,跳出重试循环
|
||||
initErr = nil
|
||||
break
|
||||
}
|
||||
|
||||
if initErr != nil {
|
||||
return nil, fmt.Errorf("初始化失败(重试%d次): %v", maxInitRetries, initErr)
|
||||
}
|
||||
|
||||
if err := reg.Register(email, password); err != nil {
|
||||
return nil, fmt.Errorf("注册失败: %v", err)
|
||||
}
|
||||
|
||||
if err := reg.SendVerificationEmail(); err != nil {
|
||||
return nil, fmt.Errorf("发送邮件失败: %v", err)
|
||||
}
|
||||
|
||||
// 短超时获取验证码
|
||||
otpCode, err := mail.GetVerificationCode(email, 5*time.Second)
|
||||
if err != nil {
|
||||
otpCode, err = mail.GetVerificationCode(email, 15*time.Second)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("验证码获取超时")
|
||||
}
|
||||
}
|
||||
|
||||
if err := reg.ValidateOTP(otpCode); err != nil {
|
||||
return nil, fmt.Errorf("OTP验证失败: %v", err)
|
||||
}
|
||||
|
||||
if err := reg.CreateAccount(name, birthdate); err != nil {
|
||||
return nil, fmt.Errorf("创建账户失败: %v", err)
|
||||
}
|
||||
|
||||
_ = reg.GetSessionToken()
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user