Files
cursor2api/internal/token/pool.go
chinadoiphin 2ea992a98c fix: 改为每次请求生成新 token,避免 403 错误
- 移除 token 池轮询复用机制
- 每次请求独立生成 x-is-human token
- 避免 Cursor 检测到 token 重复使用
2025-12-18 11:13:19 +08:00

380 lines
8.9 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 token 提供 Token 池管理
package token
import (
"fmt"
"os"
"os/exec"
"strings"
"sync"
"sync/atomic"
"time"
"cursor2api/internal/config"
"cursor2api/internal/logger"
"github.com/enetx/g"
"github.com/enetx/surf"
)
var log = logger.Get().WithPrefix("TokenPool")
// Pool Token 池管理器
type Pool struct {
tokens map[string]*TokenEntry // name -> token
nameMap map[string]string // apiKey -> name (用于显示)
roundRobin []*TokenEntry // 轮询 token 池
rrIndex int32 // 轮询索引
client *surf.Client
cfg *config.Config
mu sync.RWMutex
envJS string
mainJS string
stopChan chan struct{}
nextID int32 // 用于生成 token 名称
hitCount int64 // 缓存命中次数
missCount int64 // 缓存未命中次数
poolSize int // 轮询池大小
}
// TokenEntry Token 条目
type TokenEntry struct {
Name string // token 名称,如 "Token-1", "Token-2"
Token string
CreatedAt time.Time
UseCount int64 // 使用次数
mu sync.Mutex
}
const (
tokenExpiry = 25 * time.Minute // token 有效期
refreshInterval = 20 * time.Minute // 刷新间隔提前5分钟刷新
)
var (
instance *Pool
once sync.Once
)
// GetPool 获取 Token 池单例
func GetPool() *Pool {
once.Do(func() {
cfg := config.Get()
poolSize := cfg.TokenPoolSize
if poolSize <= 0 {
poolSize = 3 // 默认 3 个 token 轮询
}
instance = &Pool{
tokens: make(map[string]*TokenEntry),
nameMap: make(map[string]string),
roundRobin: make([]*TokenEntry, 0, poolSize),
cfg: cfg,
poolSize: poolSize,
}
instance.init()
})
return instance
}
func (p *Pool) init() {
// 初始化 HTTP 客户端
p.client = surf.NewClient().Builder().Impersonate().Chrome().Build()
p.stopChan = make(chan struct{})
// 加载 JS 模板
envJS, err := os.ReadFile("jscode/env.js")
if err != nil {
log.Warn("failed to load env.js: %v", err)
}
p.envJS = string(envJS)
mainJS, err := os.ReadFile("jscode/main.js")
if err != nil {
log.Warn("failed to load main.js: %v", err)
}
p.mainJS = string(mainJS)
// 预生成轮询 token 池
log.Info("预热 %d 个 token...", p.poolSize)
for i := 0; i < p.poolSize; i++ {
tokenStr, err := p.generateToken()
if err != nil {
log.Error("预热 token %d 失败: %v", i+1, err)
continue
}
name := p.generateName()
entry := &TokenEntry{
Name: name,
Token: tokenStr,
CreatedAt: time.Now(),
}
p.roundRobin = append(p.roundRobin, entry)
log.Info("预热 %s 完成 (%d/%d)", name, i+1, p.poolSize)
}
// 启动后台刷新协程
go p.backgroundRefresh()
log.Info("Initialized (轮询池: %d)", len(p.roundRobin))
}
// preWarmToken 预热 token
func (p *Pool) preWarmToken(apiKey string) {
tokenStr, err := p.generateToken()
if err != nil {
log.Error("Pre-warm failed: %v", err)
return
}
name := p.generateName()
p.mu.Lock()
p.tokens[apiKey] = &TokenEntry{
Name: name,
Token: tokenStr,
CreatedAt: time.Now(),
}
p.nameMap[apiKey] = name
p.mu.Unlock()
log.Info("Pre-warmed %s for key: %s", name, truncateKey(apiKey))
}
// generateName 生成 token 名称
func (p *Pool) generateName() string {
id := atomic.AddInt32(&p.nextID, 1)
return fmt.Sprintf("Token-%d", id)
}
// backgroundRefresh 后台定时刷新所有 token
func (p *Pool) backgroundRefresh() {
ticker := time.NewTicker(refreshInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
p.refreshAllTokens()
case <-p.stopChan:
return
}
}
}
// refreshAllTokens 刷新轮询池中的所有 token
func (p *Pool) refreshAllTokens() {
p.mu.RLock()
poolLen := len(p.roundRobin)
p.mu.RUnlock()
for i := 0; i < poolLen; i++ {
p.refreshRoundRobinToken(i)
}
log.Info("后台刷新完成 (轮询池: %d)", poolLen)
}
// GetToken 获取 Token每次生成新 token
func (p *Pool) GetToken(apiKey string) (string, error) {
// 每次请求生成新 token避免被 Cursor 检测到重复使用
log.Debug("生成新 token...")
tokenStr, err := p.generateToken()
if err != nil {
log.Error("生成 token 失败: %v", err)
return "", err
}
atomic.AddInt64(&p.hitCount, 1)
log.Debug("新 token 生成成功")
return tokenStr, nil
}
// refreshRoundRobinToken 刷新轮询池中指定索引的 token
func (p *Pool) refreshRoundRobinToken(idx int) {
p.mu.RLock()
if idx >= len(p.roundRobin) {
p.mu.RUnlock()
return
}
entry := p.roundRobin[idx]
p.mu.RUnlock()
entry.mu.Lock()
defer entry.mu.Unlock()
// 双重检查
if time.Since(entry.CreatedAt) < tokenExpiry {
return
}
tokenStr, err := p.generateToken()
if err != nil {
log.Error("刷新 %s 失败: %v", entry.Name, err)
return
}
entry.Token = tokenStr
entry.CreatedAt = time.Now()
log.Info("刷新 %s 完成", entry.Name)
}
// refreshToken 刷新指定 API Key 的 Token
func (p *Pool) refreshToken(apiKey string) (string, error) {
p.mu.Lock()
entry, exists := p.tokens[apiKey]
if !exists {
name := p.generateName()
entry = &TokenEntry{Name: name}
p.tokens[apiKey] = entry
p.nameMap[apiKey] = name
}
p.mu.Unlock()
entry.mu.Lock()
defer entry.mu.Unlock()
// 双重检查
if time.Since(entry.CreatedAt) < tokenExpiry && entry.Token != "" {
return entry.Token, nil
}
tokenStr, err := p.generateToken()
if err != nil {
return "", err
}
entry.Token = tokenStr
entry.CreatedAt = time.Now()
log.Info("Created %s for key: %s (total: %d)", entry.Name, truncateKey(apiKey), p.Count())
return tokenStr, nil
}
// Count 返回 token 总数
func (p *Pool) Count() int {
p.mu.RLock()
defer p.mu.RUnlock()
return len(p.tokens)
}
// Stats 返回统计信息
func (p *Pool) Stats() (total int, hits, misses int64) {
p.mu.RLock()
total = len(p.tokens)
p.mu.RUnlock()
hits = atomic.LoadInt64(&p.hitCount)
misses = atomic.LoadInt64(&p.missCount)
return
}
// List 返回所有 token 信息
func (p *Pool) List() []map[string]any {
p.mu.RLock()
defer p.mu.RUnlock()
result := make([]map[string]any, 0, len(p.tokens))
for key, entry := range p.tokens {
result = append(result, map[string]any{
"name": entry.Name,
"key": truncateKey(key),
"uses": entry.UseCount,
"age": time.Since(entry.CreatedAt).Round(time.Second).String(),
"expires": (tokenExpiry - time.Since(entry.CreatedAt)).Round(time.Second).String(),
})
}
return result
}
// generateToken 使用 Node.js 生成 token
func (p *Pool) generateToken() (string, error) {
if p.cfg.ScriptURL == "" {
return "", fmt.Errorf("script_url not configured")
}
// 获取 Cursor 脚本
cursorJS, err := p.fetchCursorScript()
if err != nil {
return "", fmt.Errorf("fetch cursor script: %w", err)
}
// 构建 JS 代码
code := p.buildJSCode(cursorJS)
// 写入临时文件执行(避免 argument list too long
tmpFile, err := os.CreateTemp("", "cursor_token_*.js")
if err != nil {
return "", fmt.Errorf("create temp file: %w", err)
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
if _, err := tmpFile.WriteString(code); err != nil {
tmpFile.Close()
return "", fmt.Errorf("write temp file: %w", err)
}
tmpFile.Close()
// 使用 Node.js 执行临时文件
cmd := exec.Command("node", tmpPath)
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("node error: %s", string(exitErr.Stderr))
}
return "", fmt.Errorf("execute node: %w", err)
}
return strings.TrimSpace(string(output)), nil
}
// fetchCursorScript 获取 Cursor 验证脚本
func (p *Pool) fetchCursorScript() (string, error) {
headers := map[string]string{
"sec-ch-ua-arch": `"x86"`,
"sec-ch-ua-platform": `"Windows"`,
"sec-ch-ua": `"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"`,
"sec-ch-ua-bitness": `"64"`,
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform-version": `"19.0.0"`,
"sec-fetch-site": "same-origin",
"sec-fetch-mode": "no-cors",
"sec-fetch-dest": "script",
"referer": "https://cursor.com/",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
}
resp := p.client.Get(g.String(p.cfg.ScriptURL)).SetHeaders(headers).Do()
if resp.IsErr() {
return "", fmt.Errorf("fetch script: %w", resp.Err())
}
return string(resp.Ok().Body.String()), nil
}
// buildJSCode 构建 JavaScript 代码
func (p *Pool) buildJSCode(cursorJS string) string {
fp := p.cfg.Fingerprint
replacer := strings.NewReplacer(
"$$currentScriptSrc$$", p.cfg.ScriptURL,
"$$UNMASKED_VENDOR_WEBGL$$", fp.UnmaskedVendorWebGL,
"$$UNMASKED_RENDERER_WEBGL$$", fp.UnmaskedRendererWebGL,
"$$userAgent$$", fp.UserAgent,
"$$env_jscode$$", p.envJS,
"$$cursor_jscode$$", cursorJS,
)
return replacer.Replace(p.mainJS)
}
// Close 关闭 Token 池
func (p *Pool) Close() {
close(p.stopChan)
}
func truncateKey(s string) string {
if s == "default" {
return "default"
}
if len(s) <= 8 {
return s
}
return s[:8] + "..."
}