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

270 lines
7.6 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 gptmail
import (
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
)
// Client GPTMail 邮箱服务客户端(支持多 key 均匀轮询 + 429 冷却跳过)
type Client struct {
baseURL string
keys []string // 多个 API Key
keyIdx atomic.Int64 // 轮询索引(每次请求 +1,均匀分配)
cooldowns map[int]time.Time // key 索引 → 冷却截止时间
cdMu sync.RWMutex // cooldowns 读写锁
httpClient *http.Client
mu sync.Mutex // 全局请求节流锁
lastCall time.Time // 上次请求时间
minDelay time.Duration // 最小请求间隔
}
// NewClient 创建 GPTMail 客户端,apiKey 支持逗号分隔多个 key 自动轮询
func NewClient(baseURL, apiKey string) *Client {
keys := make([]string, 0)
for _, k := range strings.Split(apiKey, ",") {
k = strings.TrimSpace(k)
if k != "" {
keys = append(keys, k)
}
}
if len(keys) == 0 {
keys = append(keys, "")
}
return &Client{
baseURL: strings.TrimRight(baseURL, "/"),
keys: keys,
cooldowns: make(map[int]time.Time),
httpClient: &http.Client{
Timeout: 15 * time.Second,
},
minDelay: 1 * time.Second,
}
}
// nextKey 获取下一个可用 key(均匀轮询 + 跳过冷却中的 key)
// 返回 key 字符串、对应索引、是否所有 key 都在冷却
func (c *Client) nextKey() (string, int, bool) {
n := len(c.keys)
base := int(c.keyIdx.Add(1) - 1) // 先取再加,每次请求递增
c.cdMu.RLock()
defer c.cdMu.RUnlock()
// 第一轮:找未冷却的 key
now := time.Now()
for i := 0; i < n; i++ {
idx := (base + i) % n
if until, ok := c.cooldowns[idx]; ok && now.Before(until) {
continue // 冷却中,跳过
}
return c.keys[idx], idx, false
}
// 所有 key 都在冷却 → 返回标记,让调用方快速失败
return "", -1, true
}
// markCooldown 标记某个 key 进入冷却期
func (c *Client) markCooldown(idx int, duration time.Duration) {
c.cdMu.Lock()
defer c.cdMu.Unlock()
c.cooldowns[idx] = time.Now().Add(duration)
}
// throttle 全局节流,确保请求间隔 >= minDelay
func (c *Client) throttle() {
c.mu.Lock()
defer c.mu.Unlock()
elapsed := time.Since(c.lastCall)
if elapsed < c.minDelay {
time.Sleep(c.minDelay - elapsed)
}
c.lastCall = time.Now()
}
// nextDailyReset 计算到下一个早 8 点的时长(GPTMail 每日额度重置时间)
func nextDailyReset() time.Duration {
now := time.Now()
reset := time.Date(now.Year(), now.Month(), now.Day(), 8, 0, 0, 0, now.Location())
if now.After(reset) {
reset = reset.AddDate(0, 0, 1) // 已过今天 8 点,算明天
}
return time.Until(reset)
}
// ErrAllKeysDead 所有 API Key 均不可用(401/403/429)
var ErrAllKeysDead = fmt.Errorf("GPTMail 所有 API Key 均不可用")
// doGet 执行 GET 请求(带节流 + 重试 + 401 快速失败 + 429 自动换 key)
// maxRetries 是网络重试次数;401/429 换 key 不消耗名额(额外 len(keys) 次预算)
func (c *Client) doGet(url string, maxRetries int) ([]byte, error) {
var lastErr error
totalBudget := maxRetries + len(c.keys)
for i := 0; i < totalBudget; i++ {
c.throttle()
key, keyIdx, allDead := c.nextKey()
if allDead {
// 所有 key 都在冷却(通常是全部 401),立即放弃不浪费时间
if lastErr == nil {
lastErr = ErrAllKeysDead
}
return nil, fmt.Errorf("GPTMail 请求失败 (所有 key 不可用): %w", lastErr)
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("X-API-Key", key)
resp, err := c.httpClient.Do(req)
if err != nil {
lastErr = err
time.Sleep(time.Duration(i+1) * 2 * time.Second)
continue
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == 401 || resp.StatusCode == 403 {
keyPreview := key
if len(keyPreview) > 8 {
keyPreview = keyPreview[:8]
}
lastErr = fmt.Errorf("GPTMail HTTP %d: key %s... 认证失败/额度耗尽", resp.StatusCode, keyPreview)
// 401 可能是 key 无效,也可能是当日额度用完,冷却 15 分钟
c.markCooldown(keyIdx, 15*time.Minute)
continue // 立即尝试下一个 key(不 sleep)
}
if resp.StatusCode == 429 {
keyPreview := key
if len(keyPreview) > 8 {
keyPreview = keyPreview[:8]
}
lastErr = fmt.Errorf("GPTMail 429 每日限额 (key: %s...)", keyPreview)
// 每日限额,冷却到明天早 8 点重置
c.markCooldown(keyIdx, nextDailyReset())
continue // 不 sleep,立即尝试下一个 key
}
if resp.StatusCode != 200 {
lastErr = fmt.Errorf("GPTMail HTTP %d: %s", resp.StatusCode, string(body))
continue
}
return body, nil
}
return nil, fmt.Errorf("GPTMail 请求失败 (重试 %d 次): %w", maxRetries, lastErr)
}
// GenerateEmail 创建临时邮箱,返回邮箱地址
func (c *Client) GenerateEmail() (string, error) {
body, err := c.doGet(c.baseURL+"/api/generate-email", 3)
if err != nil {
return "", err
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("解析邮箱响应失败: %w", err)
}
// 兼容两种格式:
// 旧格式:{"email": "xxx"}
// 新格式:{"success": true, "data": {"email": "xxx"}}
email, _ := result["email"].(string)
if email == "" {
if data, ok := result["data"].(map[string]interface{}); ok {
email, _ = data["email"].(string)
}
}
if email == "" {
return "", fmt.Errorf("邮箱响应中无 email 字段: %s", string(body))
}
return email, nil
}
// 验证码提取正则
var (
reSubjectCode = regexp.MustCompile(`^([A-Z0-9]+-[A-Z0-9]+)`)
reBodyCode = regexp.MustCompile(`\b([A-Z0-9]{6,8})\b`)
)
// FetchVerificationCode 轮询获取验证码(最多 maxAttempts 次,每次间隔 interval)
func (c *Client) FetchVerificationCode(email string, maxAttempts int, interval time.Duration) (string, error) {
url := fmt.Sprintf("%s/api/emails?email=%s", c.baseURL, email)
for i := 0; i < maxAttempts; i++ {
body, err := c.doGet(url, 1)
if err != nil {
time.Sleep(interval)
continue
}
// API 统一格式:{"success":true,"data":{"emails":[...],"count":N},"error":""}
var resp map[string]interface{}
if err := json.Unmarshal(body, &resp); err != nil {
time.Sleep(interval)
continue
}
// 提取 data.emails 数组
var emails []interface{}
if data, ok := resp["data"].(map[string]interface{}); ok {
if arr, ok := data["emails"].([]interface{}); ok {
emails = arr
}
}
for _, item := range emails {
em, ok := item.(map[string]interface{})
if !ok {
continue
}
// 策略 1: 从 subject 提取
if subject, ok := em["subject"].(string); ok {
if m := reSubjectCode.FindStringSubmatch(subject); len(m) > 1 {
code := strings.ReplaceAll(m[1], "-", "")
return code, nil
}
}
// 策略 2: 从 content/html_content 提取(API 字段名)
for _, field := range []string{"content", "html_content"} {
if content, ok := em[field].(string); ok && content != "" {
if m := reBodyCode.FindStringSubmatch(content); len(m) > 1 {
return m[1], nil
}
}
}
}
time.Sleep(interval)
}
return "", fmt.Errorf("获取验证码超时 (%d 次轮询)", maxAttempts)
}
// DeleteEmail 删除临时邮箱
func (c *Client) DeleteEmail(email string) error {
url := fmt.Sprintf("%s/api/emails/clear?email=%s", c.baseURL, email)
c.throttle()
key, _, allDead := c.nextKey()
if allDead {
return ErrAllKeysDead
}
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return err
}
req.Header.Set("X-API-Key", key)
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
resp.Body.Close()
return nil
}