mirror of
https://github.com/7836246/cursor2api.git
synced 2026-05-07 22:27:15 +08:00
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:
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
|
||||
@@ -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(' ╔══════════════════════════════════════╗');
|
||||
|
||||
@@ -90,11 +90,8 @@ export interface AppConfig {
|
||||
port: number;
|
||||
timeout: number;
|
||||
proxy?: string;
|
||||
scriptUrl: string;
|
||||
cursorModel: string;
|
||||
fingerprint: {
|
||||
unmaskedVendorWebGL: string;
|
||||
unmaskedRendererWebGL: string;
|
||||
userAgent: string;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user