Initial sanitized code sync
This commit is contained in:
793
internal/task/runner.go
Normal file
793
internal/task/runner.go
Normal file
@@ -0,0 +1,793 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"gpt-plus/config"
|
||||
"gpt-plus/internal/db"
|
||||
"gpt-plus/pkg/auth"
|
||||
"gpt-plus/pkg/captcha"
|
||||
"gpt-plus/pkg/chatgpt"
|
||||
"gpt-plus/pkg/httpclient"
|
||||
"gpt-plus/pkg/provider/card"
|
||||
"gpt-plus/pkg/provider/email"
|
||||
"gpt-plus/pkg/proxy"
|
||||
"gpt-plus/pkg/storage"
|
||||
"gpt-plus/pkg/stripe"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TaskRunner struct {
|
||||
taskID string
|
||||
gormDB *gorm.DB
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
stopping atomic.Bool
|
||||
running atomic.Bool
|
||||
}
|
||||
|
||||
func NewTaskRunner(taskID string, d *gorm.DB) (*TaskRunner, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &TaskRunner{
|
||||
taskID: taskID,
|
||||
gormDB: d,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *TaskRunner) IsRunning() bool {
|
||||
return r.running.Load()
|
||||
}
|
||||
|
||||
func (r *TaskRunner) GracefulStop() {
|
||||
r.stopping.Store(true)
|
||||
r.gormDB.Model(&db.Task{}).Where("id = ?", r.taskID).Update("status", StatusStopping)
|
||||
log.Printf("[task-runner] %s: graceful stop requested", r.taskID)
|
||||
}
|
||||
|
||||
func (r *TaskRunner) ForceStop() {
|
||||
r.stopping.Store(true)
|
||||
r.cancel()
|
||||
log.Printf("[task-runner] %s: force stop (context cancelled)", r.taskID)
|
||||
}
|
||||
|
||||
func (r *TaskRunner) Run() {
|
||||
r.running.Store(true)
|
||||
defer r.running.Store(false)
|
||||
|
||||
var t db.Task
|
||||
if err := r.gormDB.First(&t, "id = ?", r.taskID).Error; err != nil {
|
||||
log.Printf("[task-runner] %s: load task failed: %v", r.taskID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Load config from DB
|
||||
cfg, err := config.LoadFromDB(r.gormDB)
|
||||
if err != nil {
|
||||
log.Printf("[task-runner] %s: load config failed: %v", r.taskID, err)
|
||||
r.failTask(&t, "配置加载失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Override task-specific settings based on task type
|
||||
switch t.Type {
|
||||
case TaskTypePlus:
|
||||
cfg.Team.Enabled = false
|
||||
case TaskTypeTeam:
|
||||
cfg.Team.Enabled = true
|
||||
case TaskTypeBoth:
|
||||
cfg.Team.Enabled = true
|
||||
}
|
||||
|
||||
// Mark task as running
|
||||
now := time.Now()
|
||||
t.Status = StatusRunning
|
||||
t.StartedAt = &now
|
||||
r.gormDB.Save(&t)
|
||||
|
||||
// Wire locale
|
||||
if cfg.Account.Locale != "" {
|
||||
lang := cfg.Account.Locale
|
||||
acceptLang := lang + ",en;q=0.9"
|
||||
if lang == "en-US" {
|
||||
acceptLang = "en-US,en;q=0.9"
|
||||
}
|
||||
auth.SetLocale(lang, acceptLang)
|
||||
chatgpt.SetDefaultLanguage(lang)
|
||||
}
|
||||
|
||||
// Initialize components
|
||||
proxyHasSession := strings.Contains(cfg.Proxy.URL, "{SESSION}")
|
||||
b2proxyEnabled := cfg.Proxy.B2Proxy.Enabled
|
||||
var cardCountry string
|
||||
if b2proxyEnabled {
|
||||
cardCountry = cfg.Card.ExpectedCountry()
|
||||
}
|
||||
|
||||
var client *httpclient.Client
|
||||
if !b2proxyEnabled && !proxyHasSession {
|
||||
client, err = httpclient.NewClient(cfg.Proxy.URL)
|
||||
if err != nil {
|
||||
r.failTask(&t, "创建 HTTP 客户端失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Email provider
|
||||
emailProv := email.NewMailGateway(
|
||||
cfg.Email.MailGateway.BaseURL,
|
||||
cfg.Email.MailGateway.APIKey,
|
||||
cfg.Email.MailGateway.Provider,
|
||||
)
|
||||
|
||||
// Card provider — always use DB mode
|
||||
cardProv := card.NewDBCardProvider(card.DBCardProviderConfig{
|
||||
DB: r.gormDB,
|
||||
DefaultName: cfg.Card.API.DefaultName,
|
||||
DefaultCountry: cfg.Card.API.DefaultCountry,
|
||||
DefaultCurrency: cfg.Card.API.DefaultCurrency,
|
||||
DefaultAddress: cfg.Card.API.DefaultAddress,
|
||||
DefaultCity: cfg.Card.API.DefaultCity,
|
||||
DefaultState: cfg.Card.API.DefaultState,
|
||||
DefaultPostalCode: cfg.Card.API.DefaultPostalCode,
|
||||
APIBaseURL: cfg.Card.API.BaseURL,
|
||||
})
|
||||
|
||||
// Captcha solver
|
||||
var solver *captcha.Solver
|
||||
if cfg.Captcha.APIKey != "" {
|
||||
captchaProxy := cfg.Captcha.Proxy
|
||||
if captchaProxy == "" {
|
||||
captchaProxy = cfg.Proxy.URL
|
||||
}
|
||||
solver = captcha.NewSolver(cfg.Captcha.APIKey, captchaProxy)
|
||||
}
|
||||
|
||||
// Browser fingerprint pool
|
||||
var fpPool *stripe.BrowserFingerprintPool
|
||||
if cfg.Stripe.FingerprintDir != "" {
|
||||
fpPool, _ = stripe.NewBrowserFingerprintPool(cfg.Stripe.FingerprintDir, "en")
|
||||
}
|
||||
|
||||
// Stripe constants
|
||||
stripe.SetFallbackConstants(cfg.Stripe.BuildHash, cfg.Stripe.TagVersion)
|
||||
stripe.FetchStripeConstants()
|
||||
|
||||
log.Printf("[task-runner] %s: starting batch of %d", r.taskID, t.TotalCount)
|
||||
|
||||
// Main loop
|
||||
for i := 1; i <= t.TotalCount; i++ {
|
||||
if r.stopping.Load() {
|
||||
break
|
||||
}
|
||||
if r.ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
|
||||
log.Printf("[task-runner] %s: iteration %d/%d", r.taskID, i, t.TotalCount)
|
||||
|
||||
// Per-iteration client (B2Proxy / session template)
|
||||
iterClient := client
|
||||
if b2proxyEnabled {
|
||||
r.logStep(i, "", "正在连接 B2Proxy...")
|
||||
iterClient = r.getB2ProxyClient(cfg, cardCountry, i, solver)
|
||||
if iterClient == nil {
|
||||
r.logResult(&t, i, "", LogStatusFailed, "", "B2Proxy 连接失败", 0)
|
||||
continue
|
||||
}
|
||||
r.logStep(i, "", "B2Proxy 连接成功")
|
||||
} else if proxyHasSession {
|
||||
r.logStep(i, "", "正在连接代理...")
|
||||
iterClient = r.getSessionClient(cfg, i, solver)
|
||||
if iterClient == nil {
|
||||
r.logResult(&t, i, "", LogStatusFailed, "", "代理连接失败", 0)
|
||||
continue
|
||||
}
|
||||
r.logStep(i, "", "代理连接成功")
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
result := r.runOnce(r.ctx, i, t.Type, iterClient, emailProv, cardProv, solver, cfg, fpPool)
|
||||
dur := int(time.Since(start).Seconds())
|
||||
|
||||
status := LogStatusSuccess
|
||||
errMsg := ""
|
||||
if result.Error != nil {
|
||||
status = LogStatusFailed
|
||||
errMsg = result.Error.Error()
|
||||
}
|
||||
|
||||
r.logResult(&t, i, result.Email, status, result.Plan, errMsg, dur)
|
||||
|
||||
if i < t.TotalCount {
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize task status
|
||||
stopped := time.Now()
|
||||
t.StoppedAt = &stopped
|
||||
if r.stopping.Load() || r.ctx.Err() != nil {
|
||||
t.Status = StatusStopped
|
||||
} else {
|
||||
t.Status = StatusCompleted
|
||||
}
|
||||
r.gormDB.Save(&t)
|
||||
log.Printf("[task-runner] %s: finished (status=%s, success=%d, fail=%d)", r.taskID, t.Status, t.SuccessCount, t.FailCount)
|
||||
}
|
||||
|
||||
type iterResult struct {
|
||||
Email string
|
||||
Plan string
|
||||
Error error
|
||||
}
|
||||
|
||||
func (r *TaskRunner) runOnce(ctx context.Context, index int, taskType string, client *httpclient.Client,
|
||||
emailProv email.EmailProvider, cardProv card.CardProvider, solver *captcha.Solver,
|
||||
cfg *config.Config, fpPool *stripe.BrowserFingerprintPool) iterResult {
|
||||
|
||||
result := iterResult{Plan: "failed"}
|
||||
|
||||
password := generatePassword(cfg.Account.PasswordLength)
|
||||
|
||||
// Step: Register
|
||||
r.logStep(index, "", "正在注册账号...")
|
||||
regResult, err := auth.Register(ctx, client, emailProv, password)
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("注册失败: %w", err)
|
||||
return result
|
||||
}
|
||||
result.Email = regResult.Email
|
||||
r.logStep(index, regResult.Email, "注册成功: "+regResult.Email)
|
||||
|
||||
r.saveEmailRecord(regResult.Email, "in_use", "owner")
|
||||
|
||||
// Step: Login
|
||||
sentinel := auth.NewSentinelGeneratorWithDeviceID(client, regResult.DeviceID)
|
||||
var loginResult *auth.LoginResult
|
||||
|
||||
if regResult.Tokens != nil {
|
||||
loginResult = regResult.Tokens
|
||||
r.logStep(index, regResult.Email, "登录成功 (注册时已获取令牌)")
|
||||
} else {
|
||||
r.logStep(index, regResult.Email, "正在登录...")
|
||||
loginResult, err = auth.Login(ctx, client, regResult.Email, password,
|
||||
regResult.DeviceID, sentinel, regResult.MailboxID, emailProv)
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("登录失败: %w", err)
|
||||
r.updateEmailRecord(regResult.Email, "used_failed")
|
||||
return result
|
||||
}
|
||||
r.logStep(index, regResult.Email, "登录成功")
|
||||
}
|
||||
|
||||
session := chatgpt.NewSession(client, loginResult.AccessToken, loginResult.RefreshToken,
|
||||
loginResult.IDToken, regResult.DeviceID, loginResult.ChatGPTAccountID, loginResult.ChatGPTUserID)
|
||||
|
||||
// Step: Activate Plus
|
||||
r.logStep(index, regResult.Email, "正在开通 Plus...")
|
||||
var browserFP *stripe.BrowserFingerprint
|
||||
if fpPool != nil {
|
||||
browserFP = fpPool.Get()
|
||||
}
|
||||
statusFn := func(f string, a ...interface{}) {
|
||||
msg := fmt.Sprintf(f, a...)
|
||||
log.Printf("[task-runner] %s #%d: %s", r.taskID, index, msg)
|
||||
r.logStep(index, regResult.Email, msg)
|
||||
}
|
||||
plusResult, plusErr := chatgpt.ActivatePlus(ctx, session, sentinel, cardProv, cfg.Stripe, regResult.Email, solver, statusFn, browserFP)
|
||||
|
||||
plusSkipped := false
|
||||
if plusErr != nil {
|
||||
plusSkipped = true
|
||||
if errors.Is(plusErr, chatgpt.ErrPlusNotEligible) || errors.Is(plusErr, chatgpt.ErrCaptchaRequired) ||
|
||||
errors.Is(plusErr, stripe.ErrCardDeclined) || strings.Contains(plusErr.Error(), "challenge") {
|
||||
r.logStep(index, regResult.Email, "Plus 跳过: "+plusErr.Error())
|
||||
log.Printf("[task-runner] %s #%d: Plus skipped: %v", r.taskID, index, plusErr)
|
||||
} else {
|
||||
r.logStep(index, regResult.Email, "Plus 失败: "+plusErr.Error())
|
||||
log.Printf("[task-runner] %s #%d: Plus failed: %v", r.taskID, index, plusErr)
|
||||
}
|
||||
}
|
||||
|
||||
buildAccountResult := func(fp *stripe.Fingerprint) *chatgpt.AccountResult {
|
||||
return &chatgpt.AccountResult{
|
||||
Email: regResult.Email, Password: password,
|
||||
AccessToken: session.AccessToken, RefreshToken: session.RefreshToken,
|
||||
IDToken: session.IDToken, ChatGPTAccountID: session.AccountID,
|
||||
ChatGPTUserID: session.UserID, GUID: fp.GUID, MUID: fp.MUID, SID: fp.SID,
|
||||
Proxy: cfg.Proxy.URL, CreatedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
var fingerprint *stripe.Fingerprint
|
||||
if !plusSkipped {
|
||||
r.logStep(index, regResult.Email, "Plus 开通成功: "+plusResult.PlanType)
|
||||
result.Plan = plusResult.PlanType
|
||||
fingerprint = &stripe.Fingerprint{GUID: plusResult.GUID, MUID: plusResult.MUID, SID: plusResult.SID}
|
||||
|
||||
session.RefreshSession()
|
||||
if session.AccountID == "" {
|
||||
if acctInfo, acctErr := chatgpt.CheckAccount(session.Client, session.AccessToken, session.DeviceID); acctErr == nil {
|
||||
session.AccountID = acctInfo.AccountID
|
||||
}
|
||||
}
|
||||
|
||||
r.logStep(index, regResult.Email, "正在获取 Plus OAuth 授权...")
|
||||
plusCodexTokens, plusCodexErr := auth.ObtainCodexTokens(ctx, client, regResult.DeviceID, "")
|
||||
if plusCodexErr == nil {
|
||||
session.AccessToken = plusCodexTokens.AccessToken
|
||||
session.RefreshToken = plusCodexTokens.RefreshToken
|
||||
if plusCodexTokens.IDToken != "" {
|
||||
session.IDToken = plusCodexTokens.IDToken
|
||||
}
|
||||
if plusCodexTokens.ChatGPTAccountID != "" {
|
||||
session.AccountID = plusCodexTokens.ChatGPTAccountID
|
||||
}
|
||||
r.logStep(index, regResult.Email, "Plus OAuth 授权成功")
|
||||
} else {
|
||||
r.logStep(index, regResult.Email, "Plus OAuth 授权失败 (非致命)")
|
||||
}
|
||||
|
||||
plusAccount := buildAccountResult(fingerprint)
|
||||
plusAccount.PlanType = plusResult.PlanType
|
||||
plusAccount.StripeSessionID = plusResult.StripeSessionID
|
||||
storage.SavePlusAuthFile(cfg.Output.Dir, plusAccount)
|
||||
|
||||
r.saveAccountToDB(plusAccount, "plus", nil, r.taskID)
|
||||
r.updateEmailRecord(regResult.Email, "used")
|
||||
r.logStep(index, regResult.Email, "Plus 账号已保存")
|
||||
} else {
|
||||
sc := stripe.FetchStripeConstants()
|
||||
if browserFP != nil {
|
||||
fingerprint, _ = stripe.GetFingerprint(session.Client, chatgpt.GetDefaultUA(), "chatgpt.com", sc.TagVersion, browserFP)
|
||||
} else {
|
||||
fingerprint, _ = stripe.GetFingerprintAuto(ctx, session.Client, chatgpt.GetDefaultUA(), "chatgpt.com", sc.TagVersion)
|
||||
}
|
||||
if fingerprint == nil {
|
||||
fingerprint = &stripe.Fingerprint{}
|
||||
}
|
||||
}
|
||||
|
||||
// Team activation
|
||||
var teamResult *chatgpt.TeamResult
|
||||
var teamErr error
|
||||
if cfg.Team.Enabled {
|
||||
r.logStep(index, regResult.Email, "正在开通 Team...")
|
||||
teamStatusFn := func(f string, a ...interface{}) {
|
||||
msg := fmt.Sprintf(f, a...)
|
||||
log.Printf("[task-runner] %s #%d: %s", r.taskID, index, msg)
|
||||
r.logStep(index, regResult.Email, msg)
|
||||
}
|
||||
teamResult, teamErr = chatgpt.ActivateTeam(ctx, session, sentinel, cardProv,
|
||||
cfg.Stripe, cfg.Team, fingerprint, regResult.Email, solver,
|
||||
emailProv, regResult.MailboxID, teamStatusFn)
|
||||
if teamErr != nil {
|
||||
r.logStep(index, regResult.Email, "Team 失败: "+teamErr.Error())
|
||||
log.Printf("[task-runner] %s #%d: Team failed: %v", r.taskID, index, teamErr)
|
||||
}
|
||||
if teamResult != nil {
|
||||
r.logStep(index, regResult.Email, "Team 开通成功: "+teamResult.TeamAccountID)
|
||||
if result.Plan == "failed" {
|
||||
result.Plan = "team"
|
||||
}
|
||||
savedAccessToken := session.AccessToken
|
||||
r.logStep(index, regResult.Email, "正在获取 Team OAuth 授权...")
|
||||
codexTokens, codexErr := auth.ObtainCodexTokens(ctx, client, regResult.DeviceID, teamResult.TeamAccountID)
|
||||
|
||||
var wsAccessToken string
|
||||
if codexErr != nil {
|
||||
wsAccessToken, _ = chatgpt.GetWorkspaceAccessToken(client, teamResult.TeamAccountID)
|
||||
if wsAccessToken == "" {
|
||||
wsAccessToken = savedAccessToken
|
||||
}
|
||||
} else {
|
||||
wsAccessToken = codexTokens.AccessToken
|
||||
}
|
||||
|
||||
teamAccount := buildAccountResult(fingerprint)
|
||||
teamAccount.AccessToken = wsAccessToken
|
||||
teamAccount.ChatGPTAccountID = teamResult.TeamAccountID
|
||||
teamAccount.TeamAccountID = teamResult.TeamAccountID
|
||||
teamAccount.WorkspaceToken = wsAccessToken
|
||||
if codexTokens != nil {
|
||||
teamAccount.RefreshToken = codexTokens.RefreshToken
|
||||
if codexTokens.IDToken != "" {
|
||||
teamAccount.IDToken = codexTokens.IDToken
|
||||
}
|
||||
}
|
||||
teamAccount.PlanType = "team"
|
||||
storage.SaveTeamAuthFile(cfg.Output.Dir, teamAccount)
|
||||
|
||||
parentID := r.saveAccountToDB(teamAccount, "team_owner", nil, r.taskID)
|
||||
r.updateEmailRecord(regResult.Email, "used")
|
||||
r.logStep(index, regResult.Email, "Team 账号已保存")
|
||||
|
||||
if cfg.Team.InviteCount > 0 && teamResult.TeamAccountID != "" {
|
||||
r.logStep(index, regResult.Email, fmt.Sprintf("正在邀请 %d 个成员...", cfg.Team.InviteCount))
|
||||
r.inviteMembers(ctx, cfg, client, emailProv, session, regResult, teamResult, fingerprint, parentID)
|
||||
r.logStep(index, regResult.Email, "成员邀请完成")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if plusResult == nil && teamResult == nil && taskType == TaskTypePlus {
|
||||
result.Plan = "free"
|
||||
freeAccount := buildAccountResult(fingerprint)
|
||||
freeAccount.PlanType = "free"
|
||||
storage.SaveFreeAuthFile(cfg.Output.Dir, freeAccount)
|
||||
r.saveAccountToDB(freeAccount, "plus", nil, r.taskID)
|
||||
}
|
||||
|
||||
membershipState, verifyErr := r.verifyTaskMembership(ctx, taskType, session, teamAccountIDFromResult(teamResult), statusFn)
|
||||
if membershipState != nil {
|
||||
result.Plan = membershipState.resultPlanForTask(taskType)
|
||||
}
|
||||
if verifyErr != nil {
|
||||
reasons := []string{verifyErr.Error()}
|
||||
if plusErr != nil {
|
||||
reasons = append(reasons, "plus="+plusErr.Error())
|
||||
}
|
||||
if teamErr != nil {
|
||||
reasons = append(reasons, "team="+teamErr.Error())
|
||||
}
|
||||
result.Error = fmt.Errorf("最终会员校验失败: %s", strings.Join(reasons, "; "))
|
||||
r.updateEmailRecord(regResult.Email, "used_failed")
|
||||
return result
|
||||
}
|
||||
|
||||
r.updateEmailRecord(regResult.Email, "used")
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *TaskRunner) inviteMembers(ctx context.Context, cfg *config.Config,
|
||||
client *httpclient.Client, emailProv email.EmailProvider,
|
||||
session *chatgpt.Session, regResult *auth.RegisterResult,
|
||||
teamResult *chatgpt.TeamResult, fingerprint *stripe.Fingerprint, parentID *uint) {
|
||||
|
||||
invWsToken, invWsErr := chatgpt.GetWorkspaceAccessToken(client, teamResult.TeamAccountID)
|
||||
if invWsErr != nil {
|
||||
invWsToken = session.AccessToken
|
||||
}
|
||||
if invWsToken == "" {
|
||||
return
|
||||
}
|
||||
|
||||
for invIdx := 1; invIdx <= cfg.Team.InviteCount; invIdx++ {
|
||||
if r.stopping.Load() || r.ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
|
||||
r.logStep(0, regResult.Email, fmt.Sprintf("成员 %d/%d: 创建代理客户端...", invIdx, cfg.Team.InviteCount))
|
||||
|
||||
// Use B2Proxy for member too, not just cfg.Proxy.URL (which may be empty in B2Proxy mode)
|
||||
var memberClient *httpclient.Client
|
||||
var memberClientErr error
|
||||
if cfg.Proxy.B2Proxy.Enabled {
|
||||
cardCountry := cfg.Card.ExpectedCountry()
|
||||
for try := 1; try <= 3; try++ {
|
||||
proxyURL, fetchErr := proxy.FetchB2Proxy(cfg.Proxy.B2Proxy, cardCountry)
|
||||
if fetchErr != nil {
|
||||
continue
|
||||
}
|
||||
memberClient, memberClientErr = httpclient.NewClient(proxyURL)
|
||||
if memberClientErr == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(cfg.Proxy.URL, "{SESSION}") {
|
||||
memberProxyURL := strings.ReplaceAll(cfg.Proxy.URL, "{SESSION}", fmt.Sprintf("inv%s%d", randomSessionID(), invIdx))
|
||||
memberClient, memberClientErr = httpclient.NewClient(memberProxyURL)
|
||||
} else {
|
||||
memberClient, memberClientErr = httpclient.NewClient(cfg.Proxy.URL)
|
||||
}
|
||||
_ = memberClientErr
|
||||
if memberClient == nil {
|
||||
r.logStep(0, regResult.Email, fmt.Sprintf("成员 %d: 代理连接失败,跳过", invIdx))
|
||||
continue
|
||||
}
|
||||
|
||||
r.logStep(0, regResult.Email, fmt.Sprintf("成员 %d: 注册中...", invIdx))
|
||||
memberPassword := auth.GenerateRandomPassword(cfg.Account.PasswordLength)
|
||||
memberReg, err := auth.Register(ctx, memberClient, emailProv, memberPassword)
|
||||
if err != nil {
|
||||
r.logStep(0, regResult.Email, fmt.Sprintf("成员 %d: 注册失败: %v", invIdx, err))
|
||||
continue
|
||||
}
|
||||
r.logStep(0, memberReg.Email, fmt.Sprintf("成员 %d: 注册成功: %s", invIdx, memberReg.Email))
|
||||
|
||||
r.saveEmailRecord(memberReg.Email, "in_use", "member")
|
||||
|
||||
r.logStep(0, memberReg.Email, fmt.Sprintf("成员 %d: 发送邀请...", invIdx))
|
||||
if err := chatgpt.InviteToTeam(client, invWsToken, teamResult.TeamAccountID, session.DeviceID, memberReg.Email); err != nil {
|
||||
r.logStep(0, memberReg.Email, fmt.Sprintf("成员 %d: 邀请失败: %v", invIdx, err))
|
||||
r.updateEmailRecord(memberReg.Email, "used_failed")
|
||||
continue
|
||||
}
|
||||
r.logStep(0, memberReg.Email, fmt.Sprintf("成员 %d: 邀请成功,等待传播...", invIdx))
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
var memberLoginResult *auth.LoginResult
|
||||
if memberReg.Tokens != nil {
|
||||
memberLoginResult = memberReg.Tokens
|
||||
r.logStep(0, memberReg.Email, fmt.Sprintf("成员 %d: 使用注册令牌", invIdx))
|
||||
} else {
|
||||
r.logStep(0, memberReg.Email, fmt.Sprintf("成员 %d: 登录中...", invIdx))
|
||||
memberSentinel := auth.NewSentinelGeneratorWithDeviceID(memberClient, memberReg.DeviceID)
|
||||
memberLoginResult, err = auth.Login(ctx, memberClient, memberReg.Email, memberPassword,
|
||||
memberReg.DeviceID, memberSentinel, memberReg.MailboxID, emailProv)
|
||||
if err != nil {
|
||||
r.logStep(0, memberReg.Email, fmt.Sprintf("成员 %d: 登录失败: %v", invIdx, err))
|
||||
r.updateEmailRecord(memberReg.Email, "used_failed")
|
||||
continue
|
||||
}
|
||||
r.logStep(0, memberReg.Email, fmt.Sprintf("成员 %d: 登录成功", invIdx))
|
||||
}
|
||||
|
||||
var memberWsToken, memberRefreshToken, memberIDToken, memberAccountID string
|
||||
memberWsToken = memberLoginResult.AccessToken
|
||||
memberRefreshToken = memberLoginResult.RefreshToken
|
||||
memberIDToken = memberLoginResult.IDToken
|
||||
memberAccountID = memberLoginResult.ChatGPTAccountID
|
||||
|
||||
r.logStep(0, memberReg.Email, fmt.Sprintf("成员 %d: 获取 Team OAuth 授权...", invIdx))
|
||||
for codexTry := 1; codexTry <= 3; codexTry++ {
|
||||
memberCodex, err := auth.ObtainCodexTokens(ctx, memberClient, memberReg.DeviceID, teamResult.TeamAccountID)
|
||||
if err != nil {
|
||||
if codexTry < 3 {
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if memberCodex.ChatGPTAccountID == teamResult.TeamAccountID {
|
||||
memberWsToken = memberCodex.AccessToken
|
||||
memberRefreshToken = memberCodex.RefreshToken
|
||||
if memberCodex.IDToken != "" {
|
||||
memberIDToken = memberCodex.IDToken
|
||||
}
|
||||
memberAccountID = memberCodex.ChatGPTAccountID
|
||||
r.logStep(0, memberReg.Email, fmt.Sprintf("成员 %d: Team OAuth 成功", invIdx))
|
||||
break
|
||||
}
|
||||
if codexTry < 3 {
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
memberAuth := &chatgpt.AccountResult{
|
||||
Email: memberReg.Email, Password: memberPassword,
|
||||
AccessToken: memberWsToken, RefreshToken: memberRefreshToken,
|
||||
IDToken: memberIDToken, ChatGPTAccountID: memberAccountID,
|
||||
ChatGPTUserID: memberLoginResult.ChatGPTUserID,
|
||||
TeamAccountID: teamResult.TeamAccountID, WorkspaceToken: memberWsToken,
|
||||
PlanType: "team", Proxy: cfg.Proxy.URL,
|
||||
CreatedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
storage.SaveTeamAuthFile(cfg.Output.Dir, memberAuth)
|
||||
r.saveAccountToDB(memberAuth, "team_member", parentID, r.taskID)
|
||||
r.updateEmailRecord(memberReg.Email, "used_member")
|
||||
r.logStep(0, memberReg.Email, fmt.Sprintf("成员 %d: 已保存 (account=%s)", invIdx, memberAccountID))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TaskRunner) logStep(index int, email, message string) {
|
||||
r.gormDB.Create(&db.TaskLog{
|
||||
TaskID: r.taskID,
|
||||
Index: index,
|
||||
Email: email,
|
||||
Status: "step",
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *TaskRunner) logResult(t *db.Task, index int, email, status, plan, errMsg string, duration int) {
|
||||
message := status
|
||||
if errMsg != "" {
|
||||
message = errMsg
|
||||
} else if plan != "" {
|
||||
message = "membership verified: " + plan
|
||||
}
|
||||
logEntry := &db.TaskLog{
|
||||
TaskID: r.taskID,
|
||||
Index: index,
|
||||
Email: email,
|
||||
Status: status,
|
||||
Plan: plan,
|
||||
Error: errMsg,
|
||||
Duration: duration,
|
||||
Message: message,
|
||||
}
|
||||
r.gormDB.Create(logEntry)
|
||||
|
||||
t.DoneCount++
|
||||
if status == LogStatusSuccess {
|
||||
t.SuccessCount++
|
||||
} else {
|
||||
t.FailCount++
|
||||
}
|
||||
r.gormDB.Model(t).Updates(map[string]interface{}{
|
||||
"done_count": t.DoneCount,
|
||||
"success_count": t.SuccessCount,
|
||||
"fail_count": t.FailCount,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *TaskRunner) failTask(t *db.Task, errMsg string) {
|
||||
now := time.Now()
|
||||
t.Status = StatusStopped
|
||||
t.StoppedAt = &now
|
||||
r.gormDB.Save(t)
|
||||
log.Printf("[task-runner] %s: task failed: %s", r.taskID, errMsg)
|
||||
}
|
||||
|
||||
func (r *TaskRunner) saveEmailRecord(email, status, role string) {
|
||||
rec := &db.EmailRecord{
|
||||
Email: email,
|
||||
Status: status,
|
||||
UsedForRole: role,
|
||||
TaskID: r.taskID,
|
||||
}
|
||||
r.gormDB.Create(rec)
|
||||
}
|
||||
|
||||
func (r *TaskRunner) updateEmailRecord(email, status string) {
|
||||
r.gormDB.Model(&db.EmailRecord{}).Where("email = ?", email).Update("status", status)
|
||||
}
|
||||
|
||||
func teamAccountIDFromResult(result *chatgpt.TeamResult) string {
|
||||
if result == nil {
|
||||
return ""
|
||||
}
|
||||
return result.TeamAccountID
|
||||
}
|
||||
|
||||
func accountStatusForResult(result *chatgpt.AccountResult) string {
|
||||
if result == nil {
|
||||
return "active"
|
||||
}
|
||||
switch result.PlanType {
|
||||
case "free", "plus", "team":
|
||||
return result.PlanType
|
||||
default:
|
||||
return "active"
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TaskRunner) saveAccountToDB(result *chatgpt.AccountResult, plan string, parentID *uint, taskID string) *uint {
|
||||
// Check if account already exists (same email)
|
||||
var existing db.Account
|
||||
if r.gormDB.Where("email = ?", result.Email).First(&existing).Error == nil {
|
||||
// Update existing record (e.g. Plus account now also has Team)
|
||||
updates := map[string]interface{}{
|
||||
"plan": plan,
|
||||
"access_token": result.AccessToken,
|
||||
"refresh_token": result.RefreshToken,
|
||||
"id_token": result.IDToken,
|
||||
"account_id": result.ChatGPTAccountID,
|
||||
"user_id": result.ChatGPTUserID,
|
||||
"team_workspace_id": result.TeamAccountID,
|
||||
"workspace_token": result.WorkspaceToken,
|
||||
"status": accountStatusForResult(result),
|
||||
}
|
||||
if parentID != nil {
|
||||
updates["parent_id"] = *parentID
|
||||
}
|
||||
r.gormDB.Model(&existing).Updates(updates)
|
||||
return &existing.ID
|
||||
}
|
||||
|
||||
acct := &db.Account{
|
||||
TaskID: taskID,
|
||||
Email: result.Email,
|
||||
Password: result.Password,
|
||||
Plan: plan,
|
||||
ParentID: parentID,
|
||||
AccessToken: result.AccessToken,
|
||||
RefreshToken: result.RefreshToken,
|
||||
IDToken: result.IDToken,
|
||||
AccountID: result.ChatGPTAccountID,
|
||||
UserID: result.ChatGPTUserID,
|
||||
TeamWorkspaceID: result.TeamAccountID,
|
||||
WorkspaceToken: result.WorkspaceToken,
|
||||
Status: accountStatusForResult(result),
|
||||
}
|
||||
r.gormDB.Create(acct)
|
||||
return &acct.ID
|
||||
}
|
||||
|
||||
func (r *TaskRunner) getB2ProxyClient(cfg *config.Config, cardCountry string, index int, solver *captcha.Solver) *httpclient.Client {
|
||||
for try := 1; try <= 5; try++ {
|
||||
proxyURL, err := proxy.FetchB2Proxy(cfg.Proxy.B2Proxy, cardCountry)
|
||||
if err != nil {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
c, err := httpclient.NewClient(proxyURL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if testStripeConnectivity(c) {
|
||||
if solver != nil {
|
||||
solver.SetProxy(proxyURL)
|
||||
}
|
||||
return c
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *TaskRunner) getSessionClient(cfg *config.Config, index int, solver *captcha.Solver) *httpclient.Client {
|
||||
for try := 1; try <= 5; try++ {
|
||||
sessionID := randomSessionID()
|
||||
proxyURL := strings.ReplaceAll(cfg.Proxy.URL, "{SESSION}", sessionID)
|
||||
c, err := httpclient.NewClient(proxyURL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if testStripeConnectivity(c) {
|
||||
if solver != nil {
|
||||
solver.SetProxy(proxyURL)
|
||||
}
|
||||
return c
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func testStripeConnectivity(client *httpclient.Client) bool {
|
||||
req, err := http.NewRequest("GET", "https://m.stripe.com/6", nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
req2, _ := http.NewRequest("GET", "https://api.stripe.com", nil)
|
||||
req2.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
resp2, err := client.Do(req2)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
resp2.Body.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
func generatePassword(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
||||
if err != nil {
|
||||
b[i] = charset[i%len(charset)]
|
||||
continue
|
||||
}
|
||||
b[i] = charset[n.Int64()]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func randomSessionID() string {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
b := make([]byte, 16)
|
||||
for i := range b {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
|
||||
b[i] = chars[n.Int64()]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
Reference in New Issue
Block a user