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

392 lines
13 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 handler
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/xiaolajiaoyyds/regplatformm/internal/model"
"github.com/xiaolajiaoyyds/regplatformm/internal/service"
"gorm.io/gorm"
)
// geminiValidateMu 每用户验活并发锁,防止同一用户同时触发多个验活流
var geminiValidateMu sync.Map // key: userID (uint), value: *sync.Mutex
// GeminiValidateHandler Gemini Business 账号验活处理器
type GeminiValidateHandler struct {
db *gorm.DB
authSvc *service.AuthService
proxyPool *service.ProxyPool
}
// NewGeminiValidateHandler 创建 Gemini 验活处理器
func NewGeminiValidateHandler(db *gorm.DB, authSvc *service.AuthService, proxyPool *service.ProxyPool) *GeminiValidateHandler {
return &GeminiValidateHandler{db: db, authSvc: authSvc, proxyPool: proxyPool}
}
// ── Gemini Business 验活常量 ──────────────────────────────────────────────────
const (
// geminiBusinessBase Gemini Business 的基础 URL
geminiBusinessBase = "https://business.gemini.google"
)
// validateGeminiCredential 验证单个 Gemini Business 凭证
// 通过带 Cookie 请求 Gemini Business 页面,根据响应状态判断账号是否有效
// 返回 status: "valid" | "expired" | "invalid" | "error"
func validateGeminiCredential(ctx context.Context, proxyURL, cSes, cOses, csesidx, configID string) (string, error) {
jar, err := cookiejar.New(nil)
if err != nil {
return "error", fmt.Errorf("创建 CookieJar 失败: %w", err)
}
baseURL, _ := url.Parse(geminiBusinessBase)
// 设置 Gemini Business 会话 Cookies
cookies := []*http.Cookie{
{Name: "C_SES", Value: cSes, Domain: ".gemini.google", Path: "/", Secure: true, HttpOnly: true},
}
if cOses != "" {
cookies = append(cookies, &http.Cookie{
Name: "C_OSES", Value: cOses, Domain: ".gemini.google", Path: "/", Secure: true, HttpOnly: true,
})
}
jar.SetCookies(baseURL, cookies)
// 构建验证 URL
checkURL := geminiBusinessBase + "/"
if csesidx != "" {
checkURL += "?csesidx=" + url.QueryEscape(csesidx)
if configID != "" {
checkURL += "&cid=" + url.QueryEscape(configID)
}
}
// 不跟随重定向,手动检测 Location
// 构建 Transport(支持代理)
var transport http.RoundTripper
if proxyURL != "" {
proxyClient, proxyErr := service.BuildProxyHTTPClient(proxyURL, 15*time.Second)
if proxyErr == nil && proxyClient.Transport != nil {
transport = proxyClient.Transport
}
}
client := &http.Client{
Timeout: 15 * time.Second,
Jar: jar,
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // 不自动重定向
},
}
req, err := http.NewRequestWithContext(ctx, "GET", checkURL, nil)
if err != nil {
return "error", fmt.Errorf("构建验证请求失败: %w", err)
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
resp, err := client.Do(req)
if err != nil {
return "error", fmt.Errorf("验证请求网络错误: %w", err)
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, 4096))
if err != nil {
return "error", fmt.Errorf("读取验证响应失败: %w", err)
}
bodyStr := string(bodyBytes)
switch {
case resp.StatusCode == http.StatusTooManyRequests:
// 429 速率限制 → 返回 error 而非 invalid,避免误删有效账号
return "error", fmt.Errorf("Google 速率限制 HTTP 429")
case resp.StatusCode == http.StatusOK:
// 200 但可能是登录页面(检查页面内容)
if strings.Contains(bodyStr, "accounts.google.com/signin") ||
strings.Contains(bodyStr, "accounts.google.com/ServiceLogin") {
return "expired", nil
}
return "valid", nil
case resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusMovedPermanently ||
resp.StatusCode == http.StatusTemporaryRedirect:
// 重定向到登录页 → 会话过期
location := resp.Header.Get("Location")
if strings.Contains(location, "accounts.google.com") ||
strings.Contains(location, "auth.") ||
strings.Contains(location, "login") ||
strings.Contains(location, "signin") {
return "expired", nil
}
// 重定向到其他 Gemini 页面(可能正常跳转)
return "valid", nil
case resp.StatusCode == http.StatusForbidden:
// 403 → 账号被封禁或无权限
if strings.Contains(bodyStr, "suspended") || strings.Contains(bodyStr, "banned") {
return "suspended", nil
}
return "invalid", nil
case resp.StatusCode == http.StatusUnauthorized:
return "expired", nil
case resp.StatusCode >= 500:
return "error", fmt.Errorf("Gemini 服务端错误 HTTP %d", resp.StatusCode)
default:
return "invalid", fmt.Errorf("未知响应 HTTP %d: %s", resp.StatusCode, truncateStr(bodyStr, 120))
}
}
// SSEValidateGemini 验活 Gemini 账号 SSE 端点(GET /ws/gemini/validate?token=&action=archive|export_archive|validate&scope=active|archived)
func (h *GeminiValidateHandler) SSEValidateGemini(c *gin.Context) {
// 1. JWT 鉴权
token := c.Query("token")
userID, err := h.authSvc.VerifyJWT(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"detail": "未登录"})
return
}
// 2. 每用户并发锁——同一用户只能同时运行一个验活流
userMu, _ := geminiValidateMu.LoadOrStore(userID, &sync.Mutex{})
mu := userMu.(*sync.Mutex)
if !mu.TryLock() {
c.JSON(http.StatusConflict, gin.H{"detail": "已有验活任务进行中,请等待完成"})
return
}
defer mu.Unlock()
// 3. action 白名单校验
action := c.DefaultQuery("action", "archive")
if action != "validate" && action != "archive" && action != "export_archive" {
c.JSON(http.StatusBadRequest, gin.H{"detail": "无效的 action 参数"})
return
}
// 4. scope 参数
scope := c.DefaultQuery("scope", "active")
if scope != "active" && scope != "archived" {
c.JSON(http.StatusBadRequest, gin.H{"detail": "无效的 scope 参数"})
return
}
isArchived := scope == "archived"
// 5. 查询当前用户的 Gemini 账号(排除已禁用的)
var results []model.TaskResult
if err := h.db.Where("user_id = ? AND platform = ? AND is_archived = ? AND disabled = ?", userID, "gemini", isArchived, false).
Order("created_at ASC").
Find(&results).Error; err != nil {
log.Error().Err(err).Uint("user_id", userID).Msg("Gemini 验活查询账号失败")
c.JSON(http.StatusInternalServerError, gin.H{"detail": "查询账号失败"})
return
}
if len(results) == 0 {
c.JSON(http.StatusOK, gin.H{"detail": "没有 Gemini 账号需要验证"})
return
}
// 6. SSE headers
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
ctx := c.Request.Context()
total := len(results)
var validCount, invalidCount, errorCount int
var validResults []model.TaskResult
// 构建验活用代理 URL(每批在循环内轮换代理)
// 此处仅做初始化检查,实际代理在每批循环中获取
sseWrite(c, gin.H{"type": "log", "message": fmt.Sprintf("[*] 开始验证 %d 个 Gemini 账号...", total)})
sseWrite(c, gin.H{"type": "progress", "validated": 0, "total": total, "valid": 0, "invalid": 0})
c.Writer.Flush()
// 7. 分批并发验证(每批 5 个,批次间 500ms,每批轮换代理)
const batchSize = 5
for i := 0; i < total; i += batchSize {
select {
case <-ctx.Done():
return
default:
}
end := i + batchSize
if end > total {
end = total
}
batch := results[i:end]
// 每批轮换代理,避免所有请求走同一个出口
var batchProxyURL string
if p := h.proxyPool.GetNext(); p != nil {
batchProxyURL = p.HTTP
}
type batchResult struct {
idx int
result model.TaskResult
status string
}
var mu sync.Mutex
var batchResults []batchResult
var wg sync.WaitGroup
for j, r := range batch {
wg.Add(1)
go func(idx int, r model.TaskResult) {
defer wg.Done()
// 解析 credential_data 提取 c_ses/c_oses/csesidx/config_id
var cred map[string]interface{}
if err := json.Unmarshal(r.CredentialData, &cred); err != nil {
mu.Lock()
batchResults = append(batchResults, batchResult{idx: idx, result: r, status: "error"})
mu.Unlock()
return
}
cSes, _ := cred["c_ses"].(string)
cOses, _ := cred["c_oses"].(string)
csesidx, _ := cred["csesidx"].(string)
configID, _ := cred["config_id"].(string)
if cSes == "" {
mu.Lock()
batchResults = append(batchResults, batchResult{idx: idx, result: r, status: "invalid"})
mu.Unlock()
return
}
status, validateErr := validateGeminiCredential(ctx, batchProxyURL, cSes, cOses, csesidx, configID)
if validateErr != nil {
log.Warn().Err(validateErr).Str("email", r.Email).Str("status", status).Msg("Gemini 验活异常")
}
mu.Lock()
batchResults = append(batchResults, batchResult{idx: idx, result: r, status: status})
mu.Unlock()
}(j, r)
}
wg.Wait()
// 处理本批结果
now := time.Now()
for _, br := range batchResults {
switch br.status {
case "valid":
validCount++
validResults = append(validResults, br.result)
// 更新 last_validated_at
if err := h.db.Model(&model.TaskResult{}).Where("id = ? AND user_id = ?", br.result.ID, userID).
Update("last_validated_at", now).Error; err != nil {
log.Warn().Err(err).Uint("id", br.result.ID).Msg("更新 last_validated_at 失败")
}
sseWrite(c, gin.H{"type": "log", "message": fmt.Sprintf("[+] %s → 正常可用", br.result.Email)})
case "suspended":
invalidCount++
if err := h.db.Model(&model.TaskResult{}).Where("id = ? AND user_id = ?", br.result.ID, userID).
Updates(map[string]interface{}{"disabled": true, "disabled_reason": "已封禁", "disabled_at": now}).Error; err != nil {
log.Warn().Err(err).Uint("id", br.result.ID).Msg("禁用封禁账号失败")
}
sseWrite(c, gin.H{"type": "log", "message": fmt.Sprintf("[-] %s → 已封禁,已禁用", br.result.Email)})
case "expired":
invalidCount++
if err := h.db.Model(&model.TaskResult{}).Where("id = ? AND user_id = ?", br.result.ID, userID).
Updates(map[string]interface{}{"disabled": true, "disabled_reason": "Cookie 过期", "disabled_at": now}).Error; err != nil {
log.Warn().Err(err).Uint("id", br.result.ID).Msg("禁用过期账号失败")
}
sseWrite(c, gin.H{"type": "log", "message": fmt.Sprintf("[-] %s → Cookie 过期,已禁用", br.result.Email)})
case "invalid":
invalidCount++
if err := h.db.Model(&model.TaskResult{}).Where("id = ? AND user_id = ?", br.result.ID, userID).
Updates(map[string]interface{}{"disabled": true, "disabled_reason": "账号无效", "disabled_at": now}).Error; err != nil {
log.Warn().Err(err).Uint("id", br.result.ID).Msg("禁用无效账号失败")
}
sseWrite(c, gin.H{"type": "log", "message": fmt.Sprintf("[-] %s → 账号无效,已禁用", br.result.Email)})
case "error":
errorCount++
sseWrite(c, gin.H{"type": "log", "message": fmt.Sprintf("[!] %s → 检测异常(跳过),可能是 Google 临时故障", br.result.Email)})
}
}
validated := validCount + invalidCount + errorCount
sseWrite(c, gin.H{"type": "progress", "validated": validated, "total": total, "valid": validCount, "invalid": invalidCount})
c.Writer.Flush()
if end < total {
time.Sleep(500 * time.Millisecond)
}
}
// 8. 归档有效账号(仅 scope=active 且 action=archive/export_archive 时)
archivedCount := 0
if !isArchived && action != "validate" && len(validResults) > 0 {
var validIDs []uint
for _, r := range validResults {
validIDs = append(validIDs, r.ID)
}
if err := h.db.Model(&model.TaskResult{}).
Where("id IN ? AND user_id = ?", validIDs, userID).
Update("is_archived", true).Error; err != nil {
log.Error().Err(err).Msg("Gemini 验活归档更新失败")
} else {
archivedCount = len(validResults)
}
}
// 9. complete 事件
completeData := gin.H{
"type": "complete",
"total": total,
"valid": validCount,
"invalid": invalidCount,
"error": errorCount,
"archived": archivedCount,
}
if (action == "export_archive" || isArchived) && len(validResults) > 0 {
type exportItem struct {
ID uint `json:"id"`
Email string `json:"email"`
CredentialData json.RawMessage `json:"credential_data"`
}
creds := make([]exportItem, 0, len(validResults))
for _, r := range validResults {
creds = append(creds, exportItem{
ID: r.ID,
Email: r.Email,
CredentialData: json.RawMessage(r.CredentialData),
})
}
completeData["credentials"] = creds
}
logMsg := fmt.Sprintf(
"[+] 验证完成: 共 %d 个,正常 %d 个,失效 %d 个已禁用,异常跳过 %d 个",
total, validCount, invalidCount, errorCount,
)
if archivedCount > 0 {
logMsg += fmt.Sprintf(",已归档 %d 个", archivedCount)
}
sseWrite(c, gin.H{"type": "log", "message": logMsg})
sseWrite(c, completeData)
c.Writer.Flush()
}