feat: Introduce advanced TLS client with browser fingerprinting and new backend modules for API processing, authentication, mail, and ChatGPT registration.
This commit is contained in:
@@ -67,6 +67,18 @@ type TeamProcessState struct {
|
|||||||
|
|
||||||
var teamProcessState = &TeamProcessState{}
|
var teamProcessState = &TeamProcessState{}
|
||||||
|
|
||||||
|
// waitGroupWithTimeout 带超时的 WaitGroup 等待,超时返回 false
|
||||||
|
func waitGroupWithTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() { wg.Wait(); close(done) }()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return true
|
||||||
|
case <-time.After(timeout):
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// getProxyDisplay 获取代理显示名称(隐藏密码)
|
// getProxyDisplay 获取代理显示名称(隐藏密码)
|
||||||
func getProxyDisplay(proxy string) string {
|
func getProxyDisplay(proxy string) string {
|
||||||
if proxy == "" {
|
if proxy == "" {
|
||||||
@@ -648,8 +660,17 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
|
|||||||
memberLogPrefix := fmt.Sprintf("%s [Member %d]", logPrefix, memberIdx+1)
|
memberLogPrefix := fmt.Sprintf("%s [Member %d]", logPrefix, memberIdx+1)
|
||||||
memberStartTime := time.Now()
|
memberStartTime := time.Now()
|
||||||
|
|
||||||
// 获取入库信号量
|
// 获取入库信号量(3分钟超时)
|
||||||
s2aSem <- struct{}{}
|
select {
|
||||||
|
case s2aSem <- struct{}{}:
|
||||||
|
case <-time.After(3 * time.Minute):
|
||||||
|
logger.Warning(fmt.Sprintf("%s 入库信号量等待超时 (3分钟)", memberLogPrefix), memberEmail, "team")
|
||||||
|
atomic.AddInt32(&s2aFailCount, 1)
|
||||||
|
memberMu.Lock()
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("成员 %d 入库信号量超时", memberIdx+1))
|
||||||
|
memberMu.Unlock()
|
||||||
|
return false
|
||||||
|
}
|
||||||
defer func() { <-s2aSem }()
|
defer func() { <-s2aSem }()
|
||||||
|
|
||||||
// 从代理池获取随机代理(默认轮询使用代理池,无代理则直连)
|
// 从代理池获取随机代理(默认轮询使用代理池,无代理则直连)
|
||||||
@@ -764,23 +785,29 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
|
|||||||
currentEmail := email
|
currentEmail := email
|
||||||
currentPassword := password
|
currentPassword := password
|
||||||
if attempt > 0 {
|
if attempt > 0 {
|
||||||
// 重试时使用新邮箱
|
// 注册失败重试:保持原邮箱(邀请已发送),仅重新注册
|
||||||
currentEmail = mail.GenerateEmail()
|
logger.Warning(fmt.Sprintf("%s 注册重试 (第%d次), 保持邮箱: %s", memberLogPrefix, attempt+1, currentEmail), currentEmail, "team")
|
||||||
currentPassword = register.GeneratePassword()
|
|
||||||
logger.Warning(fmt.Sprintf("%s 重试 (第%d次), 新邮箱: %s", memberLogPrefix, attempt+1, currentEmail), currentEmail, "team")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送邀请
|
// 首次尝试时发送邀请,重试时跳过(邀请已发送到该邮箱)
|
||||||
if err := inviter.SendInvites([]string{currentEmail}); err != nil {
|
if attempt == 0 {
|
||||||
errStr := err.Error()
|
if err := inviter.SendInvites([]string{currentEmail}); err != nil {
|
||||||
logger.Error(fmt.Sprintf("%s 邀请失败: %v", memberLogPrefix, err), currentEmail, "team")
|
errStr := err.Error()
|
||||||
|
logger.Error(fmt.Sprintf("%s 邀请失败: %v", memberLogPrefix, err), currentEmail, "team")
|
||||||
|
|
||||||
// 检测 Team 已达邀请上限(401 或 maximum number of seats)
|
// 检测 Team 已达邀请上限(401 或 maximum number of seats)
|
||||||
if strings.Contains(errStr, "401") || strings.Contains(errStr, "maximum number of seats") {
|
if strings.Contains(errStr, "401") || strings.Contains(errStr, "maximum number of seats") {
|
||||||
markTeamExhausted()
|
markTeamExhausted()
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
// 邀请失败时换新邮箱重试
|
||||||
|
email = mail.GenerateEmail()
|
||||||
|
password = register.GeneratePassword()
|
||||||
|
currentEmail = email
|
||||||
|
currentPassword = password
|
||||||
|
logger.Warning(fmt.Sprintf("%s 邀请失败,换新邮箱: %s", memberLogPrefix, currentEmail), currentEmail, "team")
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 再次检查是否应该停止(邀请期间其他 goroutine 可能已标记)
|
// 再次检查是否应该停止(邀请期间其他 goroutine 可能已标记)
|
||||||
@@ -841,11 +868,15 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
|
|||||||
registerAndS2AMember(idx, email, password)
|
registerAndS2AMember(idx, email, password)
|
||||||
}(i)
|
}(i)
|
||||||
}
|
}
|
||||||
regWg.Wait()
|
if !waitGroupWithTimeout(®Wg, 8*time.Minute) {
|
||||||
|
logger.Warning(fmt.Sprintf("%s 注册阶段超时 (8分钟),继续处理已完成的成员", logPrefix), owner.Email, "team")
|
||||||
|
}
|
||||||
|
|
||||||
// 如果 Team 已满,等待已启动的入库完成
|
// 如果 Team 已满,等待已启动的入库完成
|
||||||
if isTeamExhausted() {
|
if isTeamExhausted() {
|
||||||
s2aWg.Wait()
|
if !waitGroupWithTimeout(&s2aWg, 5*time.Minute) {
|
||||||
|
logger.Warning(fmt.Sprintf("%s 入库等待超时 (5分钟)", logPrefix), owner.Email, "team")
|
||||||
|
}
|
||||||
result.AddedToS2A = int(atomic.LoadInt32(&s2aSuccessCount))
|
result.AddedToS2A = int(atomic.LoadInt32(&s2aSuccessCount))
|
||||||
result.Errors = append(result.Errors, "Team 邀请已满")
|
result.Errors = append(result.Errors, "Team 邀请已满")
|
||||||
result.DurationMs = time.Since(startTime).Milliseconds()
|
result.DurationMs = time.Since(startTime).Milliseconds()
|
||||||
@@ -874,7 +905,9 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 等待所有入库完成
|
// 等待所有入库完成
|
||||||
s2aWg.Wait()
|
if !waitGroupWithTimeout(&s2aWg, 10*time.Minute) {
|
||||||
|
logger.Warning(fmt.Sprintf("%s 入库阶段超时 (10分钟),继续统计结果", logPrefix), owner.Email, "team")
|
||||||
|
}
|
||||||
|
|
||||||
// 补救后再次检查 Team 是否已满
|
// 补救后再次检查 Team 是否已满
|
||||||
if isTeamExhausted() {
|
if isTeamExhausted() {
|
||||||
|
|||||||
@@ -296,8 +296,27 @@ func (c *CodexAPIAuth) GetSessionID() string {
|
|||||||
return c.sessionID
|
return c.sessionID
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObtainAuthorizationCode 获取授权码
|
// ObtainAuthorizationCode 获取授权码(全局 3 分钟超时)
|
||||||
func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
|
func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
|
||||||
|
type authResult struct {
|
||||||
|
code string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
resultCh := make(chan authResult, 1)
|
||||||
|
go func() {
|
||||||
|
code, err := c.obtainAuthorizationCodeInternal()
|
||||||
|
resultCh <- authResult{code, err}
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case r := <-resultCh:
|
||||||
|
return r.code, r.err
|
||||||
|
case <-time.After(3 * time.Minute):
|
||||||
|
return "", fmt.Errorf("授权超时 (3分钟)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obtainAuthorizationCodeInternal ObtainAuthorizationCode 的内部实现
|
||||||
|
func (c *CodexAPIAuth) obtainAuthorizationCodeInternal() (string, error) {
|
||||||
c.logStep(StepNavigate, "开始 Codex API 授权流程...")
|
c.logStep(StepNavigate, "开始 Codex API 授权流程...")
|
||||||
|
|
||||||
// 选择使用固定 URL 还是 S2A 生成的 URL
|
// 选择使用固定 URL 还是 S2A 生成的 URL
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func createTLSClient(c *TLSClient, fp BrowserFingerprint, proxyStr string) (*TLS
|
|||||||
jar := tls_client.NewCookieJar()
|
jar := tls_client.NewCookieJar()
|
||||||
|
|
||||||
options := []tls_client.HttpClientOption{
|
options := []tls_client.HttpClientOption{
|
||||||
tls_client.WithTimeoutSeconds(90),
|
tls_client.WithTimeoutSeconds(45),
|
||||||
tls_client.WithClientProfile(fp.TLSProfile),
|
tls_client.WithClientProfile(fp.TLSProfile),
|
||||||
tls_client.WithRandomTLSExtensionOrder(),
|
tls_client.WithRandomTLSExtensionOrder(),
|
||||||
tls_client.WithCookieJar(jar),
|
tls_client.WithCookieJar(jar),
|
||||||
@@ -131,7 +131,7 @@ func createAzureTLSClient(c *TLSClient, fp BrowserFingerprint, proxyStr string)
|
|||||||
|
|
||||||
session.Browser = browser
|
session.Browser = browser
|
||||||
session.GetClientHelloSpec = azuretls.GetBrowserClientHelloFunc(browser)
|
session.GetClientHelloSpec = azuretls.GetBrowserClientHelloFunc(browser)
|
||||||
session.SetTimeout(90 * time.Second)
|
session.SetTimeout(45 * time.Second)
|
||||||
|
|
||||||
if proxyStr != "" {
|
if proxyStr != "" {
|
||||||
normalized, err := proxyutil.Normalize(proxyStr)
|
normalized, err := proxyutil.Normalize(proxyStr)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package mail
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
@@ -355,7 +356,13 @@ func (m *Client) GetEmails(email string, size int) ([]EmailItem, error) {
|
|||||||
|
|
||||||
// WaitForCode 等待验证码邮件
|
// WaitForCode 等待验证码邮件
|
||||||
func (m *Client) WaitForCode(email string, timeout time.Duration) (string, error) {
|
func (m *Client) WaitForCode(email string, timeout time.Duration) (string, error) {
|
||||||
start := time.Now()
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
return m.WaitForCodeWithContext(ctx, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForCodeWithContext 等待验证码邮件(支持 context 取消)
|
||||||
|
func (m *Client) WaitForCodeWithContext(ctx context.Context, email string) (string, error) {
|
||||||
// 匹配6位数字验证码
|
// 匹配6位数字验证码
|
||||||
codeRegex := regexp.MustCompile(`\b(\d{6})\b`)
|
codeRegex := regexp.MustCompile(`\b(\d{6})\b`)
|
||||||
// 专门匹配 OpenAI 验证码邮件标题格式: "Your ChatGPT code is 016547" 或 "OpenAI - Verify your email"
|
// 专门匹配 OpenAI 验证码邮件标题格式: "Your ChatGPT code is 016547" 或 "OpenAI - Verify your email"
|
||||||
@@ -381,7 +388,12 @@ func (m *Client) WaitForCode(email string, timeout time.Duration) (string, error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for time.Since(start) < timeout {
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return "", fmt.Errorf("验证码获取超时")
|
||||||
|
default:
|
||||||
|
}
|
||||||
emails, err := m.GetEmails(email, 10)
|
emails, err := m.GetEmails(email, 10)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, mail := range emails {
|
for _, mail := range emails {
|
||||||
@@ -435,10 +447,12 @@ func (m *Client) WaitForCode(email string, timeout time.Duration) (string, error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
time.Sleep(1 * time.Second)
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return "", fmt.Errorf("验证码获取超时")
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("验证码获取超时")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WaitForInviteLink 等待邀请邮件并提取链接
|
// WaitForInviteLink 等待邀请邮件并提取链接
|
||||||
@@ -520,11 +534,22 @@ func GetLatestEmailID(email string) int {
|
|||||||
// GetEmailOTPAfterID 获取指定邮件ID之后的OTP验证码
|
// GetEmailOTPAfterID 获取指定邮件ID之后的OTP验证码
|
||||||
// 基于 get_code.go 的实现,只获取 afterEmailID 之后的新邮件中的验证码
|
// 基于 get_code.go 的实现,只获取 afterEmailID 之后的新邮件中的验证码
|
||||||
func GetEmailOTPAfterID(email string, afterEmailID int, timeout time.Duration) (string, error) {
|
func GetEmailOTPAfterID(email string, afterEmailID int, timeout time.Duration) (string, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
return GetEmailOTPAfterIDWithContext(ctx, email, afterEmailID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEmailOTPAfterIDWithContext 获取指定邮件ID之后的OTP验证码(支持 context 取消)
|
||||||
|
func GetEmailOTPAfterIDWithContext(ctx context.Context, email string, afterEmailID int) (string, error) {
|
||||||
client := NewClientForEmail(email)
|
client := NewClientForEmail(email)
|
||||||
start := time.Now()
|
|
||||||
codeRegex := regexp.MustCompile(`\b(\d{6})\b`)
|
codeRegex := regexp.MustCompile(`\b(\d{6})\b`)
|
||||||
|
|
||||||
for time.Since(start) < timeout {
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return "", fmt.Errorf("验证码获取超时 (afterEmailID=%d)", afterEmailID)
|
||||||
|
default:
|
||||||
|
}
|
||||||
emails, err := client.GetEmails(email, 5)
|
emails, err := client.GetEmails(email, 5)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, mail := range emails {
|
for _, mail := range emails {
|
||||||
@@ -554,8 +579,10 @@ func GetEmailOTPAfterID(email string, afterEmailID int, timeout time.Duration) (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
time.Sleep(1 * time.Second)
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return "", fmt.Errorf("验证码获取超时 (afterEmailID=%d)", afterEmailID)
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("验证码获取超时 (afterEmailID=%d)", afterEmailID)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -281,8 +281,27 @@ func Run(email, password, realName, birthdate, proxy string) (*ChatGPTReg, error
|
|||||||
return APIRegister(email, password, realName, birthdate, proxy, "[RegTest]")
|
return APIRegister(email, password, realName, birthdate, proxy, "[RegTest]")
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIRegister 使用 API 完成注册 (集成 403 重试机制)
|
// APIRegister 使用 API 完成注册 (集成 403 重试机制,全局 5 分钟超时)
|
||||||
func APIRegister(email, password, realName, birthdate, proxy string, logPrefix string) (*ChatGPTReg, error) {
|
func APIRegister(email, password, realName, birthdate, proxy string, logPrefix string) (*ChatGPTReg, error) {
|
||||||
|
type regResult struct {
|
||||||
|
reg *ChatGPTReg
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
resultCh := make(chan regResult, 1)
|
||||||
|
go func() {
|
||||||
|
reg, err := apiRegisterInternal(email, password, realName, birthdate, proxy, logPrefix)
|
||||||
|
resultCh <- regResult{reg, err}
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case r := <-resultCh:
|
||||||
|
return r.reg, r.err
|
||||||
|
case <-time.After(5 * time.Minute):
|
||||||
|
return nil, fmt.Errorf("注册超时 (5分钟)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiRegisterInternal APIRegister 的内部实现
|
||||||
|
func apiRegisterInternal(email, password, realName, birthdate, proxy string, logPrefix string) (*ChatGPTReg, error) {
|
||||||
var reg *ChatGPTReg
|
var reg *ChatGPTReg
|
||||||
var lastErr error
|
var lastErr error
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user