refactor: remove dead x-is-human token generation system

Cursor no longer validates the x-is-human header, so the entire token
generation pipeline was dead code producing only error logs.

Removed:
- loadScripts(), fetchCursorScript(), generateToken(), replenishPool(),
  getXIsHumanToken() and token pool management from cursor-client.ts
- scriptUrl config field and SCRIPT_URL env var support
- unmaskedVendorWebGL/unmaskedRendererWebGL fingerprint fields
- jscode/ script loading (env.js, main.js templates)
- script_url and WebGL fields from config.yaml

Kept:
- Chrome TLS fingerprint headers (user-agent, sec-ch-ua, etc.)
- x-is-human header sent as empty string (Cursor accepts it)
This commit is contained in:
小海
2026-03-05 17:20:51 +08:00
parent 03a3484067
commit 184cabf5cc
5 changed files with 9 additions and 210 deletions

View File

@@ -9,16 +9,9 @@ timeout: 120
# 代理设置(可选)
# proxy: "http://127.0.0.1:7890"
# Cursor 验证脚本 URL用于生成 x-is-human token
# 访问 https://cursor.com/cn/docs打开 DevTools 网络面板,
# 找到类似 https://cursor.com/xxx/xxx/c.js?... 的请求
script_url: "https://cursor.com/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/a-4-a/c.js?i=0&v=3&h=cursor.com"
# Cursor 使用的模型
cursor_model: "anthropic/claude-sonnet-4.6"
# 浏览器指纹配置
fingerprint:
unmasked_vendor_webgl: "Google Inc. (Intel)"
unmasked_renderer_webgl: "ANGLE (Intel, Intel(R) UHD Graphics (0x00009BA4) Direct3D11 vs_5_0 ps_5_0, D3D11)"
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"

View File

@@ -11,11 +11,8 @@ export function getConfig(): AppConfig {
config = {
port: 3010,
timeout: 120,
scriptUrl: '',
cursorModel: 'anthropic/claude-sonnet-4.6',
fingerprint: {
unmaskedVendorWebGL: 'Google Inc. (Intel)',
unmaskedRendererWebGL: 'ANGLE (Intel, Intel(R) UHD Graphics (0x00009BA4) Direct3D11 vs_5_0 ps_5_0, D3D11)',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
},
};
@@ -28,11 +25,8 @@ export function getConfig(): AppConfig {
if (yaml.port) config.port = yaml.port;
if (yaml.timeout) config.timeout = yaml.timeout;
if (yaml.proxy) config.proxy = yaml.proxy;
if (yaml.script_url) config.scriptUrl = yaml.script_url;
if (yaml.cursor_model) config.cursorModel = yaml.cursor_model;
if (yaml.fingerprint) {
if (yaml.fingerprint.unmasked_vendor_webgl) config.fingerprint.unmaskedVendorWebGL = yaml.fingerprint.unmasked_vendor_webgl;
if (yaml.fingerprint.unmasked_renderer_webgl) config.fingerprint.unmaskedRendererWebGL = yaml.fingerprint.unmasked_renderer_webgl;
if (yaml.fingerprint.user_agent) config.fingerprint.userAgent = yaml.fingerprint.user_agent;
}
} catch (e) {
@@ -44,15 +38,12 @@ export function getConfig(): AppConfig {
if (process.env.PORT) config.port = parseInt(process.env.PORT);
if (process.env.TIMEOUT) config.timeout = parseInt(process.env.TIMEOUT);
if (process.env.PROXY) config.proxy = process.env.PROXY;
if (process.env.SCRIPT_URL) config.scriptUrl = process.env.SCRIPT_URL;
if (process.env.CURSOR_MODEL) config.cursorModel = process.env.CURSOR_MODEL;
// 从 base64 FP 环境变量解析指纹
if (process.env.FP) {
try {
const fp = JSON.parse(Buffer.from(process.env.FP, 'base64').toString());
if (fp.UNMASKED_VENDOR_WEBGL) config.fingerprint.unmaskedVendorWebGL = fp.UNMASKED_VENDOR_WEBGL;
if (fp.UNMASKED_RENDERER_WEBGL) config.fingerprint.unmaskedRendererWebGL = fp.UNMASKED_RENDERER_WEBGL;
if (fp.userAgent) config.fingerprint.userAgent = fp.userAgent;
} catch (e) {
console.warn('[Config] 解析 FP 环境变量失败:', e);

View File

@@ -2,23 +2,20 @@
* cursor-client.ts - Cursor API 客户端
*
* 职责:
* 1. 发送请求到 https://cursor.com/api/chat带 TLS 指纹模拟 headers
* 2. 生成 x-is-human token在 Node.js 进程内执行 Cursor 验证脚本)
* 3. 管理 token 生命周期25分钟有效期提前刷新
* 1. 发送请求到 https://cursor.com/api/chat Chrome TLS 指纹模拟 headers
* 2. 流式解析 SSE 响应
* 3. 自动重试(最多 2 次
*
* 注x-is-human token 验证已被 Cursor 停用,直接发送空字符串即可。
*/
import { readFileSync, existsSync, writeFileSync, unlinkSync } from 'fs';
import { execSync } from 'child_process';
import { tmpdir } from 'os';
import { join } from 'path';
import { randomUUID } from 'crypto';
import type { CursorChatRequest, CursorSSEEvent } from './types.js';
import { getConfig } from './config.js';
const CURSOR_CHAT_API = 'https://cursor.com/api/chat';
// Chrome 浏览器请求头模拟
function getChromeHeaders(xIsHuman: string): Record<string, string> {
function getChromeHeaders(): Record<string, string> {
const config = getConfig();
return {
'Content-Type': 'application/json',
@@ -38,184 +35,10 @@ function getChromeHeaders(xIsHuman: string): Record<string, string> {
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
'priority': 'u=1, i',
'user-agent': config.fingerprint.userAgent,
'x-is-human': xIsHuman,
'x-is-human': '', // Cursor 不再校验此字段
};
}
// ==================== Token 管理 ====================
let envJS: string = '';
let mainJS: string = '';
interface TokenEntry {
value: string;
createdAt: number;
}
const tokenPool: TokenEntry[] = [];
const MAX_POOL_SIZE = 5;
let isGenerating = false;
const TOKEN_EXPIRY_MS = 25 * 60 * 1000; // 25 分钟
const TOKEN_REFRESH_MS = 20 * 60 * 1000; // 20 分钟时刷新
/**
* 加载 JS 脚本模板
*/
export function loadScripts(): void {
if (existsSync('jscode/env.js')) {
envJS = readFileSync('jscode/env.js', 'utf-8');
console.log('[Token] 已加载 env.js');
} else {
console.warn('[Token] ⚠ jscode/env.js 不存在token 生成将失败');
}
if (existsSync('jscode/main.js')) {
mainJS = readFileSync('jscode/main.js', 'utf-8');
console.log('[Token] 已加载 main.js');
} else {
console.warn('[Token] ⚠ jscode/main.js 不存在token 生成将失败');
}
}
/**
* 获取验证脚本(从 Cursor CDN
*/
async function fetchCursorScript(): Promise<string> {
const config = getConfig();
if (!config.scriptUrl) throw new Error('script_url 未配置');
const resp = await fetch(config.scriptUrl, {
headers: {
'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',
'user-agent': config.fingerprint.userAgent,
},
});
if (!resp.ok) throw new Error(`获取验证脚本失败: HTTP ${resp.status}`);
return resp.text();
}
/**
* 生成 x-is-human token
* 在 Node.js 进程内执行 Cursor 的验证 JS 脚本
*/
async function generateToken(): Promise<string> {
const config = getConfig();
if (!envJS || !mainJS) {
throw new Error('JS 脚本未加载,请确保 jscode/env.js 和 jscode/main.js 存在');
}
// 获取 Cursor 验证脚本
const cursorJS = await fetchCursorScript();
// 构建完整 JS 代码
const code = mainJS
.replace('$$currentScriptSrc$$', config.scriptUrl)
.replace('$$UNMASKED_VENDOR_WEBGL$$', config.fingerprint.unmaskedVendorWebGL)
.replace('$$UNMASKED_RENDERER_WEBGL$$', config.fingerprint.unmaskedRendererWebGL)
.replace('$$userAgent$$', config.fingerprint.userAgent)
.replace('$$env_jscode$$', envJS)
.replace('$$cursor_jscode$$', cursorJS);
// 写入临时文件并用 Node.js 子进程执行
// 使用子进程而非 vm 模块,因为验证脚本可能依赖全局对象
const tmpPath = join(tmpdir(), `cursor_token_${randomUUID()}.js`);
writeFileSync(tmpPath, code, 'utf-8');
try {
const output = execSync(`node "${tmpPath}"`, {
timeout: 30000,
encoding: 'utf-8',
});
return output.trim();
} finally {
try { unlinkSync(tmpPath); } catch { /* ignore */ }
}
}
/**
* 异步补充 Token 池
*/
async function replenishPool(): Promise<void> {
if (isGenerating) return;
isGenerating = true;
try {
console.log(`[Token] 补充 token 池 (当前有效: ${tokenPool.length}/${MAX_POOL_SIZE})...`);
const token = await generateToken();
tokenPool.push({ value: token, createdAt: Date.now() });
console.log(`[Token] ✓ 成功添加到资源池 (当前大小: ${tokenPool.length})`);
} catch (e) {
console.error('[Token] ✗ 补充池失败:', e);
} finally {
isGenerating = false;
}
}
/**
* 获取有效的 x-is-human token从池中随机抽取以分散风控特征
*/
export async function getXIsHumanToken(): Promise<string> {
// 如果没有脚本,返回空(某些场景可能不需要 token
if (!envJS || !mainJS) {
console.warn('[Token] JS 脚本未加载,跳过 token 生成');
return '';
}
const now = Date.now();
// 1. 清理过期 Token (>=25分钟)
for (let i = tokenPool.length - 1; i >= 0; i--) {
if (now - tokenPool[i].createdAt >= TOKEN_EXPIRY_MS) {
tokenPool.splice(i, 1);
}
}
// 2. 筛选仍然新鲜的 Token (<20分钟)
const freshTokens = tokenPool.filter(t => (now - t.createdAt) < TOKEN_REFRESH_MS);
// 3. 如果池子没满,且当前没有在生成,则异步触发补充机制
if (freshTokens.length < MAX_POOL_SIZE && !isGenerating) {
// 后台异步生成,不阻塞请求
replenishPool().catch(console.error);
}
// 4. 如果有新鲜 token从中随机挑选一个分散风控特征
if (freshTokens.length > 0) {
const randomIndex = Math.floor(Math.random() * freshTokens.length);
return freshTokens[randomIndex].value;
}
// 5. 如果没有新鲜的,但有即将过期的 (20-25分钟内),作为过渡先使用最近生成的一个
if (tokenPool.length > 0) {
const lastValid = [...tokenPool].sort((a, b) => b.createdAt - a.createdAt)[0];
console.warn('[Token] 暂无新鲜 token使用临近过期的就近 token 作为过渡');
return lastValid.value;
}
// 6. 连过期的都没有,只能同步阻塞等待生成一个
try {
console.log('[Token] 资源池为空,同步等待生成新 token...');
const token = await generateToken();
tokenPool.push({ value: token, createdAt: Date.now() });
console.log('[Token] ✓ 同步生成成功');
return token;
} catch (e) {
console.error('[Token] ✗ 同步生成失败:', e);
return '';
}
}
// ==================== API 请求 ====================
/**
@@ -234,7 +57,7 @@ export async function sendCursorRequest(
const msg = err instanceof Error ? err.message : String(err);
console.error(`[Cursor] 请求失败 (${attempt}/${maxRetries}): ${msg}`);
if (attempt < maxRetries) {
console.log(`[Cursor] ${2}s 后重试...`);
console.log(`[Cursor] 2s 后重试...`);
await new Promise(r => setTimeout(r, 2000));
} else {
throw err;
@@ -247,8 +70,7 @@ async function sendCursorRequestInner(
req: CursorChatRequest,
onChunk: (event: CursorSSEEvent) => void,
): Promise<void> {
const token = await getXIsHumanToken();
const headers = getChromeHeaders(token);
const headers = getChromeHeaders();
console.log(`[Cursor] 发送请求: model=${req.model}, messages=${req.messages.length}`);

View File

@@ -8,7 +8,6 @@
import 'dotenv/config';
import express from 'express';
import { getConfig } from './config.js';
import { loadScripts } from './cursor-client.js';
import { handleMessages, listModels, countTokens } from './handler.js';
import { handleOpenAIChatCompletions } from './openai-handler.js';
@@ -73,9 +72,6 @@ app.get('/', (_req, res) => {
// ==================== 启动 ====================
// 加载 JS 脚本
loadScripts();
app.listen(config.port, () => {
console.log('');
console.log(' ╔══════════════════════════════════════╗');

View File

@@ -90,11 +90,8 @@ export interface AppConfig {
port: number;
timeout: number;
proxy?: string;
scriptUrl: string;
cursorModel: string;
fingerprint: {
unmaskedVendorWebGL: string;
unmaskedRendererWebGL: string;
userAgent: string;
};
}