diff --git a/config.yaml b/config.yaml index d1d0c4d..5ba4c06 100644 --- a/config.yaml +++ b/config.yaml @@ -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" diff --git a/src/config.ts b/src/config.ts index d1dfe59..e6d3a08 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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); diff --git a/src/cursor-client.ts b/src/cursor-client.ts index 5cb5f4a..d7f7632 100644 --- a/src/cursor-client.ts +++ b/src/cursor-client.ts @@ -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 { +function getChromeHeaders(): Record { const config = getConfig(); return { 'Content-Type': 'application/json', @@ -38,184 +35,10 @@ function getChromeHeaders(xIsHuman: string): Record { '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 { - 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 { - 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 { - 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 { - // 如果没有脚本,返回空(某些场景可能不需要 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 { - const token = await getXIsHumanToken(); - const headers = getChromeHeaders(token); + const headers = getChromeHeaders(); console.log(`[Cursor] 发送请求: model=${req.model}, messages=${req.messages.length}`); diff --git a/src/index.ts b/src/index.ts index cc99a24..eaaa7c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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(' ╔══════════════════════════════════════╗'); diff --git a/src/types.ts b/src/types.ts index bcf3ede..a01fb2e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -90,11 +90,8 @@ export interface AppConfig { port: number; timeout: number; proxy?: string; - scriptUrl: string; cursorModel: string; fingerprint: { - unmaskedVendorWebGL: string; - unmaskedRendererWebGL: string; userAgent: string; }; }