Files
xiaolajiaoyyds 6f71df4849 Initial open-source release
多平台账号批量注册系统,支持 OpenAI / Grok / Kiro / Gemini 平台的全自动注册。

Features:
- 用户名/密码注册登录(首个用户自动管理员)
- JWT 认证 + bcrypt 密码加密
- 积分系统与多用户管理
- HuggingFace Space 弹性 Worker 节点管理
- Cloudflare Worker 负载均衡
- 实时日志推送(SSE/WebSocket)
- 管理后台(用户管理、系统设置、代理池、卡池)

Tech Stack: Go 1.25 / Gin / GORM / PostgreSQL / Vue 3 / Vite / TailwindCSS
2026-03-22 03:44:42 +08:00

1171 lines
36 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/rs/zerolog/log"
"github.com/xiaolajiaoyyds/regplatformm/internal/model"
"github.com/xiaolajiaoyyds/regplatformm/internal/worker"
"gorm.io/gorm"
)
// taskKey 运行中任务的唯一标识
type taskKey struct {
UserID uint
Platform string
}
// RunningTask 运行中的任务
type RunningTask struct {
TaskID uint
UserID uint
Platform string
Target int
Threads int
SuccessCount atomic.Int64
FailCount atomic.Int64
ClaimedCount atomic.Int64 // 已认领的注册名额(防止并发超发)
Stopping atomic.Bool // 用户已请求停止(允许立即启动新任务)
Cancel context.CancelFunc
LogCh chan string
StatusCh chan struct{} // 状态变更通知(成功/失败计数变化时触发)
Done chan struct{}
StartedAt time.Time // 任务启动时间(用于耗时统计)
subscribers []chan string
subMu sync.RWMutex
logBuffer []string // 日志缓冲区(供刷新页面后回放)
logBufMu sync.RWMutex
}
// NotifyStatus 通知前端状态已变更(非阻塞)
func (rt *RunningTask) NotifyStatus() {
select {
case rt.StatusCh <- struct{}{}:
default: // 已有待处理的通知,跳过
}
}
// Subscribe 订阅日志
func (rt *RunningTask) Subscribe() <-chan string {
ch := make(chan string, 200)
rt.subMu.Lock()
rt.subscribers = append(rt.subscribers, ch)
rt.subMu.Unlock()
return ch
}
// Unsubscribe 取消订阅
func (rt *RunningTask) Unsubscribe(ch <-chan string) {
rt.subMu.Lock()
defer rt.subMu.Unlock()
for i, sub := range rt.subscribers {
if sub == ch {
rt.subscribers = append(rt.subscribers[:i], rt.subscribers[i+1:]...)
close(sub)
return
}
}
}
// IsDone 任务是否完成
func (rt *RunningTask) IsDone() bool {
select {
case <-rt.Done:
return true
default:
return false
}
}
// Log 写入日志
func (rt *RunningTask) Log(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
// 防止往已关闭的 LogCh 写入导致 panic
defer func() { recover() }()
select {
case rt.LogCh <- msg:
default:
// 通道满了丢弃
}
}
// isInternalBrowserLog 判断 [~] 前缀的浏览器日志是否包含敏感/内部信息
// 返回 true 表示该日志不应展示给用户(URL、Token、IP、内部选择器等)
func isInternalBrowserLog(msg string) bool {
// 完整 URL(泄漏内部服务地址)
if strings.Contains(msg, "https://") || strings.Contains(msg, "http://") {
return true
}
// Token / 密钥值
if strings.Contains(msg, "Token:") || strings.Contains(msg, "token=") ||
strings.Contains(msg, "xsrf") || strings.Contains(msg, "cookie") {
return true
}
// OIDC 客户端凭据 / 设备码
if strings.Contains(msg, "clientId") || strings.Contains(msg, "user_code=") ||
strings.Contains(msg, "客户端已注册") {
return true
}
// IP / 地理位置
if strings.Contains(msg, "IP 地理位置") || strings.Contains(msg, "tz=") {
return true
}
// 内部页面状态(含 URL 片段)
if strings.Contains(msg, "页面:") || strings.Contains(msg, "URL:") ||
strings.Contains(msg, "注册链接:") || strings.Contains(msg, "页面已自动跳转") {
return true
}
// 邮箱明文 + 页面地址
if strings.Contains(msg, "填写邮箱:") {
return true
}
// Playwright CSS 选择器细节
if strings.Contains(msg, "点击按钮 (") || strings.Contains(msg, "尝试点击按钮") {
return true
}
// OIDC 内部步骤
if strings.Contains(msg, "OIDC Step") || strings.Contains(msg, "OIDC:") {
return true
}
// Playwright 内部状态
if strings.Contains(msg, "networkidle") {
return true
}
return false
}
// shouldShowToUser 判断日志是否应推送到前端
// 只保留用户关心的结果/进度/状态信息,过滤掉注册流程内部细节
func shouldShowToUser(msg string) bool {
// 远程浏览器服务的流式日志(Kiro/Gemini 注册过程)
// 需脱敏:隐藏 token/URL/cookie/内部技术细节,只展示进度
if strings.Contains(msg, "[~]") {
return !isInternalBrowserLog(msg)
}
// 成功结果(只保留 [✓] 最终汇总行,过滤掉中间步骤的 "注册成功" / "远程注册成功")
if strings.Contains(msg, "[✓]") {
return true
}
// 分隔线
if strings.Contains(msg, "════") {
return true
}
// 任务汇总
if strings.Contains(msg, "任务完成") || strings.Contains(msg, "任务已加入") {
return true
}
// 统计信息(仅匹配任务汇总行,如 "成功: 5 失败: 3",不匹配 "注册失败: xxx")
if strings.Contains(msg, "成功:") && strings.Contains(msg, "失败:") {
return true
}
// 耗时/预计(不含宽泛的"等待",避免排队轮询日志刷屏)
if strings.Contains(msg, "耗时") || strings.Contains(msg, "预计") {
return true
}
// 排队状态(仅匹配明确的排队反馈,不匹配内部 pollMailCode 等待日志)
if strings.Contains(msg, "排队等待完成") || strings.Contains(msg, "排队中") {
return true
}
// 进度指示
if strings.Contains(msg, "第 ") || strings.Contains(msg, "进度") {
return true
}
// 用户操作反馈
if strings.Contains(msg, "手动停止") || strings.Contains(msg, "强制停止") {
return true
}
return false
}
// broadcastLogs 广播日志到所有订阅者,同时缓存到 logBuffer
// 连续相同消息自动去重,避免大量并发注册时前端 DOM 爆炸
func (rt *RunningTask) broadcastLogs() {
var (
lastRaw string // 上一条原始消息(不含时间戳)
dupCount int // 连续重复计数
)
// sendOne 发送单条日志到 buffer + 所有 subscriber
// 日志始终写入 logBuffer(后端调试用),仅用户关心的日志推送到前端
sendOne := func(line string) {
rt.logBufMu.Lock()
rt.logBuffer = append(rt.logBuffer, line)
if len(rt.logBuffer) > 2000 {
rt.logBuffer = rt.logBuffer[len(rt.logBuffer)-2000:]
}
rt.logBufMu.Unlock()
if !shouldShowToUser(line) {
return
}
rt.subMu.RLock()
for _, sub := range rt.subscribers {
select {
case sub <- line:
default:
}
}
rt.subMu.RUnlock()
}
// flushDup 输出累积的重复计数
flushDup := func() {
if dupCount > 0 {
sendOne(fmt.Sprintf("[%s] [~] ↑ 重复 ×%d", time.Now().Format("15:04:05"), dupCount))
dupCount = 0
}
}
for msg := range rt.LogCh {
// 连续相同消息去重
if msg == lastRaw {
dupCount++
// 每累积 100 条刷一次,避免长时间无输出
if dupCount%100 == 0 {
flushDup()
}
continue
}
flushDup()
lastRaw = msg
sendOne(fmt.Sprintf("[%s] %s", time.Now().Format("15:04:05"), msg))
}
flushDup()
}
// GetLogBuffer 获取已缓存的历史日志(刷新页面后回放用)
func (rt *RunningTask) GetLogBuffer() []string {
rt.logBufMu.RLock()
defer rt.logBufMu.RUnlock()
buf := make([]string, len(rt.logBuffer))
copy(buf, rt.logBuffer)
return buf
}
// TaskEngine 任务引擎
type TaskEngine struct {
db *gorm.DB
proxyPool *ProxyPool
cardPool *CardPool
creditSvc *CreditService
settingSvc *SettingService
running map[taskKey]*RunningTask
mu sync.RWMutex
activeCount atomic.Int32 // 全局活跃任务计数
platformCount map[string]*atomic.Int32 // 按平台活跃任务计数
scheduler *QueueScheduler // 队列调度器(启动后通过 SetScheduler 注入)
dispatcher *Dispatcher // 全局 Round-Robin 调度器(所有任务共享 Worker Pool)
workerCount atomic.Int32 // 当前运行中的 Pool Worker 数
targetWorkers atomic.Int32 // 目标 Pool Worker 数(从配置动态读取,热生效)
configCache map[string]*cachedConfig // 按平台缓存的 ScanConfig 结果
configCacheMu sync.RWMutex
}
// cachedConfig 平台扫描配置缓存
type cachedConfig struct {
config worker.Config
scannedAt time.Time
}
// NewTaskEngine 创建任务引擎
func NewTaskEngine(db *gorm.DB, proxyPool *ProxyPool, cardPool *CardPool, creditSvc *CreditService, settingSvc *SettingService) *TaskEngine {
// 注意:running/stopping 任务的清理和退款由 QueueScheduler.RecoverOnBoot() 统一处理
// 此处不再重复清理,避免抢在退款之前把状态改掉
// 初始化按平台并发计数器
pc := make(map[string]*atomic.Int32)
for name := range worker.Registry {
pc[name] = &atomic.Int32{}
}
e := &TaskEngine{
db: db,
proxyPool: proxyPool,
cardPool: cardPool,
creditSvc: creditSvc,
settingSvc: settingSvc,
running: make(map[taskKey]*RunningTask),
platformCount: pc,
dispatcher: NewDispatcher(),
configCache: make(map[string]*cachedConfig),
}
e.startWorkerPool()
go e.startConfigRefresher()
return e
}
// SetScheduler 注入队列调度器(解决循环依赖)
func (e *TaskEngine) SetScheduler(s *QueueScheduler) {
e.scheduler = s
}
// countActiveUsers 统计当前有多少个不同用户在运行任务
func (e *TaskEngine) countActiveUsers() int {
e.mu.RLock()
defer e.mu.RUnlock()
users := make(map[uint]struct{})
for _, rt := range e.running {
if !rt.IsDone() {
users[rt.UserID] = struct{}{}
}
}
return len(users)
}
// userHasRunningTask 检查该用户是否已有运行中任务(已占槽位)
func (e *TaskEngine) userHasRunningTask(userID uint) bool {
e.mu.RLock()
defer e.mu.RUnlock()
for _, rt := range e.running {
if rt.UserID == userID && !rt.IsDone() {
return true
}
}
return false
}
// countActiveUsersByPlatform 统计指定平台当前有多少个不同用户在运行任务
func (e *TaskEngine) countActiveUsersByPlatform(platform string) int {
e.mu.RLock()
defer e.mu.RUnlock()
users := make(map[uint]struct{})
for _, rt := range e.running {
if rt.Platform == platform && !rt.IsDone() {
users[rt.UserID] = struct{}{}
}
}
return len(users)
}
// computeMaxInflight 计算指定平台的单任务最大并发数(统一算法,供启动时 + RefreshLimits 复用)
// 自适应策略:单人 80% 容量,多人均分,上限不超过平台容量的 90%
func (e *TaskEngine) computeMaxInflight(platform string) int32 {
defaultCap := 50
if platform == "kiro" || platform == "gemini" {
defaultCap = 15
}
maxConcurrent := e.settingSvc.GetInt(platform+"_max_concurrent", defaultCap)
if maxConcurrent <= 0 {
maxConcurrent = defaultCap
}
var activeCount int64
e.db.Model(&model.Task{}).Where("platform = ? AND status = ?", platform, "running").Count(&activeCount)
var maxParallel int
if activeCount <= 1 {
maxParallel = maxConcurrent * 4 / 5
} else {
maxParallel = maxConcurrent / int(activeCount)
}
// 上限:不超过平台总容量的 90%(预留缓冲给突发/新任务进入)
parallelCap := maxConcurrent * 9 / 10
if parallelCap < 5 {
parallelCap = 5
}
if maxParallel > parallelCap {
maxParallel = parallelCap
}
if maxParallel < 3 {
maxParallel = 3
}
return int32(maxParallel)
}
// userHasRunningTaskOnPlatform 检查该用户在指定平台是否已有运行中任务
func (e *TaskEngine) userHasRunningTaskOnPlatform(userID uint, platform string) bool {
key := taskKey{userID, platform}
e.mu.RLock()
defer e.mu.RUnlock()
rt, ok := e.running[key]
return ok && !rt.IsDone()
}
// EstimateWaitTime 估算排队用户的预计等待时间(秒)
// 基于该平台最近已完成任务的平均耗时 + 当前运行中任务的剩余时间
func (e *TaskEngine) EstimateWaitTime(queuePosition int, platform string) int {
// 查询该平台最近 50 个已完成任务的平均耗时
var avgDuration float64
e.db.Model(&model.Task{}).
Where("platform = ? AND status IN ? AND started_at IS NOT NULL AND stopped_at IS NOT NULL", platform, []string{"completed", "stopped"}).
Order("stopped_at DESC").Limit(50).
Select("COALESCE(AVG(EXTRACT(EPOCH FROM (stopped_at - started_at))), 120)").
Scan(&avgDuration)
if avgDuration < 10 {
avgDuration = 120 // 兜底 2 分钟
}
// 找该平台当前运行中任务的最短剩余时间
e.mu.RLock()
minRemaining := avgDuration
for _, rt := range e.running {
if rt.IsDone() || rt.Platform != platform {
continue
}
elapsed := time.Since(rt.StartedAt).Seconds()
remaining := avgDuration - elapsed
if remaining < 0 {
remaining = 30 // 已超时的任务给 30 秒缓冲
}
if remaining < minRemaining {
minRemaining = remaining
}
}
e.mu.RUnlock()
// 预计等待 = 最短剩余时间 × ceil(排队位置 / 槽位数)
maxUsers, _ := strconv.Atoi(e.settingSvc.Get(platform+"_max_concurrent_users", "0"))
if maxUsers <= 0 {
maxUsers, _ = strconv.Atoi(e.settingSvc.Get("max_concurrent_users", "1"))
}
if maxUsers <= 0 {
maxUsers = 1
}
rounds := (queuePosition + maxUsers - 1) / maxUsers
return int(minRemaining) * rounds
}
// formatWaitTime 格式化等待秒数为可读字符串
func formatWaitTime(sec int) string {
if sec < 60 {
return fmt.Sprintf("约 %d 秒", sec)
}
min := sec / 60
s := sec % 60
if s > 0 {
return fmt.Sprintf("约 %d 分 %d 秒", min, s)
}
return fmt.Sprintf("约 %d 分钟", min)
}
// StartTask 启动任务
func (e *TaskEngine) StartTask(userID, taskID uint, platform string, target, threads int) (bool, string) {
key := taskKey{userID, platform}
w, ok := worker.Get(platform)
if !ok {
return false, fmt.Sprintf("不支持的平台: %s", platform)
}
// ── 按平台用户槽位并发控制 ──
maxUsers, _ := strconv.Atoi(e.settingSvc.Get(platform+"_max_concurrent_users", "0"))
if maxUsers <= 0 {
// 向后兼容:平台专属未设置时回退到全局设置
maxUsers, _ = strconv.Atoi(e.settingSvc.Get("max_concurrent_users", "0"))
}
if maxUsers > 0 && !e.userHasRunningTaskOnPlatform(userID, platform) {
activeUsers := e.countActiveUsersByPlatform(platform)
if activeUsers >= maxUsers {
e.db.Model(&model.Task{}).Where("id = ? AND status != ?", taskID, "queued").Update("status", "queued")
// 计算该平台排队位置和预计等待时间
var queuePos int64
e.db.Model(&model.Task{}).
Where("status = ? AND platform = ? AND created_at < (SELECT created_at FROM tasks WHERE id = ?)", "queued", platform, taskID).
Count(&queuePos)
queuePos++ // 自己也算一个位置
waitSec := e.EstimateWaitTime(int(queuePos), platform)
log.Info().Uint("task_id", taskID).Uint("user_id", userID).Str("platform", platform).
Int("active_users", activeUsers).Int("limit", maxUsers).Int64("queue_pos", queuePos).
Msg("平台用户槽位已满,任务加入队列")
return true, fmt.Sprintf("排队中,前方 %d 位用户,预计等待 %s", queuePos, formatWaitTime(waitSec))
}
}
// ── 按平台并发控制 ──
platformLimitKey := platform + "_max_concurrent"
platformLimit, _ := strconv.Atoi(e.settingSvc.Get(platformLimitKey, "0"))
if platformLimit <= 0 {
// 浏览器类平台(kiro/gemini)并发能力远低于 HTTP 类(openai/grok),兜底值需区分
switch platform {
case "kiro", "gemini":
platformLimit = 8
default:
platformLimit = 100
}
}
pc := e.getPlatformCount(platform)
newPlatformActive := pc.Add(1)
if int(newPlatformActive) > platformLimit {
pc.Add(-1)
e.db.Model(&model.Task{}).Where("id = ? AND status != ?", taskID, "queued").Update("status", "queued")
log.Info().Uint("task_id", taskID).Str("platform", platform).
Int("active", int(newPlatformActive-1)).Int("limit", platformLimit).
Msg("平台并发已满,任务加入队列")
return true, "已加入队列"
}
// ── 全局并发控制(安全上限)──
globalLimit, _ := strconv.Atoi(e.settingSvc.Get("max_concurrent_tasks", "100"))
if globalLimit <= 0 {
globalLimit = 100
}
newActive := e.activeCount.Add(1)
if int(newActive) > globalLimit {
e.activeCount.Add(-1)
pc.Add(-1)
e.db.Model(&model.Task{}).Where("id = ? AND status != ?", taskID, "queued").Update("status", "queued")
log.Info().Uint("task_id", taskID).Int("active", int(newActive-1)).Int("limit", globalLimit).
Msg("全局并发已满,任务加入队列")
return true, "已加入队列"
}
ctx, cancel := context.WithCancel(context.Background())
rt := &RunningTask{
TaskID: taskID,
UserID: userID,
Platform: platform,
Target: target,
Threads: threads,
Cancel: cancel,
LogCh: make(chan string, 500),
StatusCh: make(chan struct{}, 1),
Done: make(chan struct{}),
StartedAt: time.Now(),
}
// 原子检查并设置,防止 TOCTOU 竞态
e.mu.Lock()
if existing, ok := e.running[key]; ok && !existing.IsDone() {
e.mu.Unlock()
cancel() // 清理刚创建的 context
e.activeCount.Add(-1) // 回滚全局计数
pc.Add(-1) // 回滚平台计数
return false, "该平台已有运行中的任务"
}
e.running[key] = rt
e.mu.Unlock()
// 更新任务状态为 running
now := time.Now()
e.db.Model(&model.Task{}).Where("id = ?", taskID).Updates(map[string]interface{}{
"status": "running",
"started_at": &now,
})
// 启动日志广播
go rt.broadcastLogs()
log.Info().Uint("task_id", taskID).Str("platform", platform).Int("target", target).Msg("任务已启动")
// 异步执行重量级操作(ScanConfig + 调度 + 监控),避免阻塞调用方
go e.executeStartedTask(ctx, cancel, rt, key, w, pc, taskID, userID, platform, target)
return true, "任务已启动"
}
// executeStartedTask 执行已通过并发检查的任务(ScanConfig + 调度 + 监控)
// 在 goroutine 中运行,避免阻塞 StartTask 的调用方
func (e *TaskEngine) executeStartedTask(
ctx context.Context, cancel context.CancelFunc,
rt *RunningTask, key taskKey, w worker.Worker, pc *atomic.Int32,
taskID, userID uint, platform string, target int,
) {
// 获取共享基础配置(ScanConfig 只调一次,所有 Pool Worker 共享结果)
baseCfg := worker.Config{}
e.injectSettings(baseCfg)
// 优先使用缓存配置(后台每 5 分钟自动刷新)
scannedCfg, cached := e.getCachedConfig(platform)
if !cached {
rt.Log("[*] 正在获取页面配置(直连失败时会走浏览器过盾,约需 30-60 秒)...")
var scanErr error
scannedCfg, scanErr = w.ScanConfig(ctx, e.proxyPool.GetNext(), baseCfg)
if scanErr != nil {
rt.Log("[-] 配置获取失败: %s", scanErr)
cancel()
close(rt.Done)
close(rt.LogCh) // 停止 broadcastLogs goroutine
stopNow := time.Now()
e.db.Model(&model.Task{}).Where("id = ?", taskID).Updates(map[string]interface{}{
"status": "stopped",
"stopped_at": &stopNow,
})
// 退还所有预扣积分
var task model.Task
if dbErr := e.db.First(&task, taskID).Error; dbErr == nil && task.CreditsReserved > 0 {
e.creditSvc.RefundCredits(e.db, userID, taskID, task.CreditsReserved)
}
e.mu.Lock()
delete(e.running, key)
e.mu.Unlock()
e.creditSvc.CleanupTask(taskID)
e.activeCount.Add(-1)
pc.Add(-1)
if e.scheduler != nil {
e.scheduler.TryDispatch()
}
return
}
// 扫描成功,更新缓存供后续任务使用
e.updateConfigCache(platform, scannedCfg)
} else {
rt.Log("[*] 使用缓存配置(后台每 5 分钟自动刷新)")
}
// 合并扫描配置到基础配置
for k, v := range scannedCfg {
if baseCfg[k] == "" {
baseCfg[k] = v
}
}
// 将扫描到的动态配置回写数据库,避免 site_key / action_id 过期
for _, k := range []string{"site_key", "action_id"} {
if v := scannedCfg[k]; v != "" {
dbKey := platform + "_" + k
if setErr := e.settingSvc.Set(dbKey, v); setErr != nil {
rt.Log("[!] 配置持久化失败 (%s): %s", dbKey, setErr)
} else {
rt.Log("[*] 动态配置已更新: %s = %s", dbKey, v)
}
}
}
// 注入用户指定的代理(任务创建时保存在 task.proxy_url)
{
var task model.Task
if err := e.db.Select("proxy_url").First(&task, taskID).Error; err == nil && task.ProxyURL != "" {
baseCfg["user_proxy"] = task.ProxyURL
rt.Log("[*] 使用用户指定代理")
}
}
// 解析多节点 URL 配置(逗号分隔 → 轮询分配到各 Pool Worker)
multiNodeKeys := []string{"kiro_reg_url", "gemini_reg_url", "openai_reg_url", "grok_reg_url", "camoufox_reg_url", "turnstile_solver_url", "cf_bypass_solver_url"}
nodeURLs := make(map[string][]string)
for _, nk := range multiNodeKeys {
if val := baseCfg[nk]; val != "" && strings.Contains(val, ",") {
parts := strings.Split(val, ",")
trimmed := make([]string, 0, len(parts))
for _, p := range parts {
if t := strings.TrimSpace(p); t != "" {
trimmed = append(trimmed, t)
}
}
if len(trimmed) > 1 {
nodeURLs[nk] = trimmed
rt.Log("[*] %s → %d 个节点轮询", nk, len(trimmed))
}
}
}
// 计算单任务最大并发数(与 RefreshLimits 相同的自适应算法,避免前 10 秒使用过时值)
maxParallel := int(e.computeMaxInflight(platform))
// 构建调度条目并加入全局 Dispatcher(Pool Worker 会自动拾取并执行)
entry := &DispatchEntry{
Key: key,
RT: rt,
Worker: w,
Ctx: ctx,
Cancel: cancel,
Config: baseCfg,
NodeURLs: nodeURLs,
Platform: platform,
done: make(chan struct{}),
}
entry.maxInflight.Store(int32(maxParallel))
e.dispatcher.Add(entry)
// 新任务加入后立即刷新所有任务的 maxInflight(不等 10 秒定时器,公平分配即时生效)
e.dispatcher.RefreshLimits(func(p string) int32 {
return e.computeMaxInflight(p)
})
rt.Log("════════════════════════════════════")
rt.Log("[*] 配置扫描完成,任务已加入调度池(当前 %d 个 Pool Worker,单任务并发上限 %d)", e.workerCount.Load(), maxParallel)
rt.Log("════════════════════════════════════")
// 监控 goroutine:等待调度条目完成后清理资源
go func() {
<-entry.done
success := int(rt.SuccessCount.Load())
fail := int(rt.FailCount.Load())
elapsed := time.Since(rt.StartedAt).Round(time.Second)
rt.Log("════════════════════════════════════")
rt.Log("[✓] 任务完成 成功: %d 失败: %d 耗时: %s", success, fail, elapsed)
rt.Log("════════════════════════════════════")
// 从调度器移除(停止分配新名额给此任务)
e.dispatcher.Remove(key)
// 立即刷新剩余任务的 maxInflight(任务退出后释放的容量即时分配给剩余任务)
e.dispatcher.RefreshLimits(func(p string) int32 {
return e.computeMaxInflight(p)
})
// 更新任务状态(区分用户停止 vs 自然完成)
stopNow := time.Now()
finalStatus := "completed"
if ctx.Err() != nil {
// context 被 cancel,检查是否用户主动停止(非达标取消)
total := rt.SuccessCount.Load() + rt.FailCount.Load()
if total < int64(target) {
finalStatus = "stopped"
}
}
e.db.Model(&model.Task{}).Where("id = ?", taskID).Updates(map[string]interface{}{
"status": finalStatus,
"stopped_at": &stopNow,
})
// 退还未消费积分(必须在 close(rt.Done) 之前,确保前端收到 complete 时退款已入库)
// 使用 DB 中的 success_count 作为退款依据(内存计数器可能因 DB 写入失败而偏高)
var task model.Task
if err := e.db.First(&task, taskID).Error; err == nil {
refund := task.CreditsReserved - task.SuccessCount
if refund > 0 {
e.creditSvc.RefundCredits(e.db, userID, taskID, refund)
}
}
// 关闭 Done channel → 触发 SSE complete 事件(此时退款已到账)
close(rt.Done)
close(rt.LogCh)
// 清理运行中任务 + 退款锁内存
e.mu.Lock()
delete(e.running, key)
e.mu.Unlock()
e.creditSvc.CleanupTask(taskID)
// 释放全局并发槽位 + 平台槽位,并尝试调度下一个排队任务
e.activeCount.Add(-1)
pc.Add(-1)
if e.scheduler != nil {
e.scheduler.TryDispatch()
}
}()
}
// StopTask 停止任务
func (e *TaskEngine) StopTask(userID uint, platform string) bool {
key := taskKey{userID, platform}
e.mu.RLock()
rt, ok := e.running[key]
e.mu.RUnlock()
if !ok || rt.IsDone() {
return false
}
rt.Log("[!] 用户手动停止任务")
rt.Stopping.Store(true)
rt.Cancel()
e.dispatcher.ForceComplete(key) // 立即标记完成,不等 inflight worker
// 更新状态
e.db.Model(&model.Task{}).Where("id = ?", rt.TaskID).Update("status", "stopping")
return true
}
// GetStatus 获取运行中任务的状态
func (e *TaskEngine) GetStatus(userID uint, platform string) *RunningTask {
key := taskKey{userID, platform}
e.mu.RLock()
defer e.mu.RUnlock()
rt, ok := e.running[key]
if !ok || rt.IsDone() || rt.Stopping.Load() {
return nil
}
return rt
}
// FindRunningTaskByID 通过 taskID 查找运行中任务
func (e *TaskEngine) FindRunningTaskByID(taskID uint) *RunningTask {
e.mu.RLock()
defer e.mu.RUnlock()
for _, rt := range e.running {
if rt.TaskID == taskID {
return rt
}
}
return nil
}
// RunningTaskInfo 运行中任务的安全快照(不暴露 channel/cancel 等内部字段)
type RunningTaskInfo struct {
TaskID uint `json:"task_id"`
UserID uint `json:"user_id"`
Platform string `json:"platform"`
Target int `json:"target"`
Threads int `json:"threads"`
SuccessCount int64 `json:"success_count"`
FailCount int64 `json:"fail_count"`
StartedAt time.Time `json:"started_at"`
Stopping bool `json:"stopping"`
}
// ListAllRunning 返回所有运行中任务的快照(管理员用)
func (e *TaskEngine) ListAllRunning() []RunningTaskInfo {
e.mu.RLock()
defer e.mu.RUnlock()
result := make([]RunningTaskInfo, 0, len(e.running))
for _, rt := range e.running {
if rt.IsDone() {
continue
}
result = append(result, RunningTaskInfo{
TaskID: rt.TaskID,
UserID: rt.UserID,
Platform: rt.Platform,
Target: rt.Target,
Threads: rt.Threads,
SuccessCount: rt.SuccessCount.Load(),
FailCount: rt.FailCount.Load(),
StartedAt: rt.StartedAt,
Stopping: rt.Stopping.Load(),
})
}
return result
}
// AdminStopTask 管理员强制停止任务(通过 taskID 查找)
func (e *TaskEngine) AdminStopTask(taskID uint) bool {
e.mu.RLock()
var found *RunningTask
var foundKey taskKey
for k, rt := range e.running {
if rt.TaskID == taskID && !rt.IsDone() {
found = rt
foundKey = k
break
}
}
e.mu.RUnlock()
if found == nil {
return false
}
log.Info().Uint("task_id", taskID).Msg("管理员强制停止任务")
found.Log("[!] 管理员强制停止任务")
found.Stopping.Store(true)
found.Cancel()
e.dispatcher.ForceComplete(foundKey) // 立即标记完成,不等 inflight worker
e.db.Model(&model.Task{}).Where("id = ?", taskID).Update("status", "stopping")
return true
}
// getPlatformCount 获取平台并发计数器(惰性初始化)
func (e *TaskEngine) getPlatformCount(platform string) *atomic.Int32 {
e.mu.RLock()
if pc, ok := e.platformCount[platform]; ok {
e.mu.RUnlock()
return pc
}
e.mu.RUnlock()
e.mu.Lock()
defer e.mu.Unlock()
if pc, ok := e.platformCount[platform]; ok {
return pc
}
pc := &atomic.Int32{}
e.platformCount[platform] = pc
return pc
}
// saveResult 保存注册结果
func (e *TaskEngine) saveResult(taskID, userID uint, platform, email string, credential map[string]interface{}) {
credJSON, err := json.Marshal(credential)
if err != nil {
log.Error().Err(err).Uint("task_id", taskID).Msg("credential 序列化失败")
}
result := model.TaskResult{
TaskID: taskID,
UserID: userID,
Platform: platform,
Email: email,
CredentialData: credJSON,
}
// Grok 向后兼容:提取 auth_token(兼容旧数据 sso_token)
if platform == "grok" {
if token, ok := credential["auth_token"].(string); ok {
result.SSOToken = token
} else if token, ok := credential["sso_token"].(string); ok {
result.SSOToken = token
}
}
if err := e.db.Create(&result).Error; err != nil {
log.Error().Err(err).Uint("task_id", taskID).Str("email", email).Msg("保存注册结果失败")
}
}
// injectSettings 将系统设置注入到 Worker 配置中(不覆盖已有值)
func (e *TaskEngine) injectSettings(cfg worker.Config) {
// 需要注入的系统设置键列表
keys := []string{
"yydsmail_api_key",
"yydsmail_base_url",
"email_provider_priority",
"turnstile_solver_url",
"turnstile_solver_proxy",
"cf_bypass_solver_url",
"capsolver_key",
"yescaptcha_key",
"grok_action_id",
"grok_site_key",
"camoufox_reg_url",
"openai_reg_url",
"kiro_reg_url",
"grok_reg_url",
"grok_email_providers",
"openai_email_providers",
"kiro_email_providers",
"kiro_proxy",
"gemini_reg_url",
"gemini_email_providers",
"gemini_proxy",
"grok_proxy",
"openai_proxy",
"stripe_pk",
}
for _, k := range keys {
if cfg[k] == "" {
if v := e.settingSvc.Get(k, ""); v != "" {
cfg[k] = v
}
}
}
}
// startWorkerPool 启动 Worker Pool 并开启后台动态伸缩
// 每 10 秒检查 hf_instance_count × workers_per_instance,自动扩容/缩容
func (e *TaskEngine) startWorkerPool() {
poolSize := e.calcPoolSize()
e.targetWorkers.Store(int32(poolSize))
e.workerCount.Store(int32(poolSize))
for i := 0; i < poolSize; i++ {
go e.runPoolWorker()
}
go e.poolResizer()
log.Info().Int("pool_size", poolSize).Msg("Worker Pool 已启动")
}
// calcPoolSize 根据 HF 实例数 × 每实例并发数 计算 Pool 大小
func (e *TaskEngine) calcPoolSize() int {
instances, _ := strconv.Atoi(e.settingSvc.Get("hf_instance_count", "20"))
if instances <= 0 {
instances = 20
}
perInstance, _ := strconv.Atoi(e.settingSvc.Get("workers_per_instance", "10"))
if perInstance <= 0 {
perInstance = 10
}
return instances * perInstance
}
// poolResizer 后台动态伸缩 Worker Pool + 刷新任务并发上限
// 每 10 秒读取配置,扩容立即生效,缩容等 Worker 干完手头活后退出
// 同时刷新所有活跃任务的 maxInflight,使 SyncPoolSize 的动态参数热生效
func (e *TaskEngine) poolResizer() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
desired := int32(e.calcPoolSize())
e.targetWorkers.Store(desired)
current := e.workerCount.Load()
if desired > current {
// 扩容:立即补充差额 Worker
diff := desired - current
for i := int32(0); i < diff; i++ {
e.workerCount.Add(1)
go e.runPoolWorker()
}
log.Info().Int32("old", current).Int32("new", desired).Int32("spawned", diff).Msg("Worker Pool 扩容")
} else if desired < current {
// 缩容:多余 Worker 完成当前任务后自动退出
log.Info().Int32("current", current).Int32("target", desired).Msg("Worker Pool 缩容中")
e.dispatcher.Wake() // 唤醒空闲 Worker 让它们检查退出条件
}
// 动态刷新活跃任务的 maxInflight(复用 computeMaxInflight 统一算法)
e.dispatcher.RefreshLimits(func(p string) int32 {
return e.computeMaxInflight(p)
})
}
}
// runPoolWorker 单个 Pool Worker 的主循环
// 不断从 Dispatcher 获取下一个注册任务并执行,空闲时阻塞等待
func (e *TaskEngine) runPoolWorker() {
defer e.workerCount.Add(-1)
for {
// 缩容检查:超过目标数的 Worker 主动退出
if e.workerCount.Load() > e.targetWorkers.Load() {
return
}
job, ok := e.dispatcher.Next(e.proxyPool)
if !ok {
return // Dispatcher 已关闭(服务关停)
}
e.executeRegJob(job)
}
}
// executeRegJob 执行单次注册任务(含重试逻辑)
// 由 Pool Worker 调用,完成后通知 DispatchEntry 检查任务是否全部结束
func (e *TaskEngine) executeRegJob(job *RegJob) {
entry := job.Entry
rt := entry.RT
defer func() {
entry.inflight.Add(-1)
entry.checkCompletion()
e.dispatcher.Wake() // 释放并发槽位,唤醒等待中的 Worker
}()
const maxRetries = 3
// openai_team: 从卡池分配卡并注入配置
var cardID uint
if rt.Platform == "openai_team" && e.cardPool != nil {
card := e.cardPool.GetNext()
if card == nil {
rt.Log("[-] 卡池无可用卡,跳过")
rt.ClaimedCount.Add(-1)
rt.FailCount.Add(1)
rt.NotifyStatus()
e.db.Model(&model.Task{}).Where("id = ?", rt.TaskID).
Update("fail_count", gorm.Expr("fail_count + 1"))
e.dispatcher.Wake()
return
}
cardID = card.ID
job.Cfg["card_number"] = card.CardNumber
job.Cfg["exp_month"] = strconv.Itoa(card.ExpMonth)
job.Cfg["exp_year"] = strconv.Itoa(card.ExpYear)
job.Cfg["cvc"] = card.CVC
job.Cfg["billing_name"] = card.BillingName
job.Cfg["billing_email"] = card.BillingEmail
job.Cfg["billing_country"] = card.BillingCountry
job.Cfg["billing_city"] = card.BillingCity
job.Cfg["billing_line1"] = card.BillingLine1
job.Cfg["billing_zip"] = card.BillingZip
}
for retryN := 1; retryN <= maxRetries; retryN++ {
// 用户已停止 → 立即退出,不再尝试注册
if entry.Ctx.Err() != nil {
break
}
var succeeded, failed bool
entry.Worker.RegisterOne(entry.Ctx, worker.RegisterOpts{
Proxy: job.Proxy,
Config: job.Cfg,
LogCh: rt.LogCh,
OnSuccess: func(email string, credential map[string]interface{}) {
succeeded = true
newCount := rt.SuccessCount.Add(1)
rt.NotifyStatus()
// 实时推送成功账号到前端日志(序号+邮箱)
rt.Log("[✓] #%d %s", newCount, email)
e.saveResult(rt.TaskID, rt.UserID, rt.Platform, email, credential)
// 原子递增防止并发 worker 互相覆盖
e.db.Model(&model.Task{}).Where("id = ?", rt.TaskID).
Update("success_count", gorm.Expr("success_count + 1"))
// 卡池生命周期:标记卡已使用
if cardID > 0 && e.cardPool != nil {
e.cardPool.MarkUsed(cardID)
}
if newCount >= int64(rt.Target) {
entry.Cancel()
}
},
OnFail: func() {
failed = true
},
})
if succeeded || !failed {
break // 成功或 ctx 已取消,退出重试
}
if retryN < maxRetries {
// 中间重试不推送前端,静默重试
} else {
rt.Log("[-] 已重试 %d 次,计入失败", maxRetries)
rt.ClaimedCount.Add(-1) // 释放名额,让 Pool Worker 可以重新认领
// 卡池生命周期:标记卡失败
if cardID > 0 && e.cardPool != nil {
e.cardPool.MarkFailed(cardID)
}
newCount := rt.FailCount.Add(1)
rt.NotifyStatus()
// 原子递增防止并发 worker 互相覆盖
e.db.Model(&model.Task{}).Where("id = ?", rt.TaskID).
Update("fail_count", gorm.Expr("fail_count + 1"))
if newCount >= int64(rt.Target) {
entry.Cancel()
}
e.dispatcher.Wake() // 唤醒等待中的 Worker,重新认领释放的名额
}
}
}
// ─── ScanConfig 全局缓存 ───
// startConfigRefresher 后台定时刷新所有平台的扫描配置
// 首次延迟 10 秒(等服务就绪),之后每 5 分钟刷新一次
func (e *TaskEngine) startConfigRefresher() {
time.Sleep(10 * time.Second)
e.refreshAllConfigs()
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
e.refreshAllConfigs()
}
}
// refreshAllConfigs 刷新所有已注册平台的配置缓存
func (e *TaskEngine) refreshAllConfigs() {
for name, w := range worker.Registry {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
baseCfg := worker.Config{}
e.injectSettings(baseCfg)
scanned, err := w.ScanConfig(ctx, e.proxyPool.GetNext(), baseCfg)
cancel()
if err != nil {
log.Warn().Str("platform", name).Err(err).Msg("配置缓存刷新失败(保留旧缓存)")
continue
}
e.updateConfigCache(name, scanned)
log.Info().Str("platform", name).Msg("配置缓存已刷新")
}
}
// getCachedConfig 获取平台的缓存配置(10 分钟内有效)
func (e *TaskEngine) getCachedConfig(platform string) (worker.Config, bool) {
e.configCacheMu.RLock()
defer e.configCacheMu.RUnlock()
cc, ok := e.configCache[platform]
if !ok || time.Since(cc.scannedAt) > 10*time.Minute {
return nil, false
}
// 返回副本,避免并发修改
cp := make(worker.Config, len(cc.config))
for k, v := range cc.config {
cp[k] = v
}
return cp, true
}
// updateConfigCache 更新平台配置缓存
func (e *TaskEngine) updateConfigCache(platform string, cfg worker.Config) {
e.configCacheMu.Lock()
defer e.configCacheMu.Unlock()
e.configCache[platform] = &cachedConfig{
config: cfg,
scannedAt: time.Now(),
}
}