794 lines
24 KiB
Go
794 lines
24 KiB
Go
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)
|
|
}
|