/** * Stealth Proxy - 通过无头浏览器绕过 Vercel Bot Protection * * 架构: * 客户端 → cursor2api → stealth-proxy → (Chrome浏览器上下文) → cursor.com/api/chat * * 原理: * 1. 启动时用 stealth 浏览器访问 cursor.com,通过 JS Challenge 获取 _vcrcs cookie * 2. 在同一浏览器上下文内通过 page.evaluate(fetch) 代理 API 请求 * 3. 定时刷新 challenge(_vcrcs 有效期 3600s,每 50 分钟刷新) * 4. 支持 SSE 流式响应透传 */ const express = require('express'); const crypto = require('crypto'); const PORT = parseInt(process.env.PORT || '3011'); const CHALLENGE_URL = process.env.CHALLENGE_URL || 'https://cursor.com/cn/docs'; const REFRESH_INTERVAL = parseInt(process.env.REFRESH_INTERVAL || '3000000'); // 50 分钟 const CHALLENGE_WAIT = parseInt(process.env.CHALLENGE_WAIT || '55000'); // challenge 最长等待时间 let browser, context, challengePage, workerPage; let ready = false; let startTime = Date.now(); let challengeCount = 0; let requestCount = 0; const pendingRequests = new Map(); // ==================== 浏览器管理 ==================== const fs = require('fs'); // 自动检测系统 Chrome 路径(优先使用,指纹更真实) function findSystemChrome() { const paths = [ // macOS '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', // Linux '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser', // Windows 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', ]; for (const p of paths) { if (fs.existsSync(p)) return p; } return null; } async function loadStealth() { const { chromium } = require('playwright-extra'); const stealth = require('puppeteer-extra-plugin-stealth'); chromium.use(stealth()); return chromium; } async function initBrowser() { const chromium = await loadStealth(); const chromePath = findSystemChrome(); const launchOptions = { headless: true, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', ], }; if (chromePath) { launchOptions.executablePath = chromePath; console.log(`[Stealth] Using system Chrome: ${chromePath}`); } else { console.log('[Stealth] System Chrome not found, using Playwright Chromium'); } console.log('[Stealth] Launching browser...'); browser = await chromium.launch(launchOptions); context = await browser.newContext({ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36', locale: 'zh-CN', viewport: { width: 1920, height: 1080 }, }); // ---- Challenge 页面:获取 _vcrcs ---- challengePage = await context.newPage(); console.log(`[Stealth] Passing Vercel challenge: ${CHALLENGE_URL}`); await challengePage.goto(CHALLENGE_URL, { waitUntil: 'domcontentloaded', timeout: 60000, }); // 模拟人类行为,帮助通过 bot 检测 await simulateHumanBehavior(challengePage); // 等待 cookie(challenge JS 会异步设置 _vcrcs) const ok = await waitForCookie(); if (!ok) { // 重试:刷新页面 + 再次模拟行为 console.log('[Stealth] First attempt failed, retrying challenge...'); await challengePage.reload({ waitUntil: 'domcontentloaded', timeout: 60000 }); await simulateHumanBehavior(challengePage); const retryOk = await waitForCookie(); if (!retryOk) { throw new Error('Failed to obtain _vcrcs cookie after retry'); } } challengeCount++; // ---- Worker 页面:代理 API 请求 ---- // cookie 已在 challenge 页面获取,worker 页面只需加载到 domcontentloaded 即可 workerPage = await context.newPage(); await workerPage.goto(CHALLENGE_URL, { waitUntil: 'domcontentloaded', timeout: 60000, }); // 注册流式回调(Node.js 侧接收浏览器内 fetch 的数据块) await workerPage.exposeFunction( '__proxyCallback', (requestId, type, data) => { const pending = pendingRequests.get(requestId); if (!pending) return; switch (type) { case 'headers': { const { status, contentType } = JSON.parse(data); const headers = { 'Cache-Control': 'no-cache', Connection: 'keep-alive', }; if (contentType) headers['Content-Type'] = contentType; pending.res.writeHead(status, headers); break; } case 'chunk': pending.res.write(data); break; case 'end': pending.res.end(); pending.resolve(); break; case 'error': if (!pending.res.headersSent) { pending.res.writeHead(502, { 'Content-Type': 'application/json', }); } pending.res.end( JSON.stringify({ error: { message: data } }), ); pending.resolve(); break; } }, ); ready = true; console.log('[Stealth] Ready! Accepting proxy requests.'); } async function waitForCookie(maxWait) { maxWait = maxWait || CHALLENGE_WAIT; const start = Date.now(); while (Date.now() - start < maxWait) { const cookies = await context.cookies(); const vcrcs = cookies.find((c) => c.name === '_vcrcs'); if (vcrcs) { console.log( '[Stealth] _vcrcs obtained:', vcrcs.value.substring(0, 40) + '...', ); return true; } await new Promise((r) => setTimeout(r, 2000)); } console.error('[Stealth] Failed to obtain _vcrcs within timeout'); return false; } // 模拟人类行为:鼠标移动、点击、滚动,帮助通过 Vercel bot 检测 async function simulateHumanBehavior(page) { try { // 随机延迟 await new Promise(r => setTimeout(r, 500 + Math.random() * 1000)); // 模拟鼠标移动轨迹(多个随机点) const points = Array.from({ length: 5 }, () => ({ x: 100 + Math.random() * 600, y: 100 + Math.random() * 400, })); for (const p of points) { await page.mouse.move(p.x, p.y, { steps: 5 + Math.floor(Math.random() * 10) }); await new Promise(r => setTimeout(r, 100 + Math.random() * 300)); } // 模拟滚动 await page.mouse.wheel(0, 100 + Math.random() * 200); await new Promise(r => setTimeout(r, 300 + Math.random() * 500)); await page.mouse.wheel(0, -(50 + Math.random() * 100)); // 模拟点击页面空白区域 await page.mouse.click(300 + Math.random() * 400, 300 + Math.random() * 200); await new Promise(r => setTimeout(r, 200 + Math.random() * 500)); console.log('[Stealth] Human behavior simulation done'); } catch (e) { // 模拟行为失败不影响主流程 console.log('[Stealth] Human simulation skipped:', e.message); } } async function refreshChallenge() { console.log('[Stealth] Refreshing challenge...'); try { await challengePage.goto(CHALLENGE_URL, { waitUntil: 'networkidle', timeout: 30000, }); const ok = await waitForCookie(); if (ok) { challengeCount++; console.log( `[Stealth] Challenge refreshed (total: ${challengeCount})`, ); } else { console.error('[Stealth] Challenge refresh failed - cookie not obtained'); } } catch (e) { console.error('[Stealth] Challenge refresh error:', e.message); } } async function restartBrowser() { console.log('[Stealth] Restarting browser...'); ready = false; try { if (browser) await browser.close().catch(() => {}); } catch (_) {} browser = null; context = null; challengePage = null; workerPage = null; await initBrowser(); } // ==================== HTTP 服务 ==================== const app = express(); app.use(express.json({ limit: '10mb' })); // 健康检查 app.get('/health', async (_req, res) => { let cookie = null; if (context) { const cookies = await context.cookies().catch(() => []); const vcrcs = cookies.find((c) => c.name === '_vcrcs'); if (vcrcs) cookie = vcrcs.value.substring(0, 40) + '...'; } res.json({ status: ready ? 'ok' : 'initializing', uptime: Math.floor((Date.now() - startTime) / 1000), challengeCount, requestCount, cookie, }); }); // 代理请求 app.post('/proxy/chat', async (req, res) => { if (!ready) { res.status(503).json({ error: { message: 'Stealth proxy not ready, please wait' }, }); return; } const requestId = crypto.randomUUID(); requestCount++; // 客户端断开时清理 let aborted = false; req.on('close', () => { aborted = true; }); const promise = new Promise((resolve) => { pendingRequests.set(requestId, { res, resolve }); }); // 在浏览器上下文内发起 fetch 并流式回传 workerPage .evaluate( async ({ body, requestId }) => { try { const r = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); await window.__proxyCallback( requestId, 'headers', JSON.stringify({ status: r.status, contentType: r.headers.get('content-type'), }), ); if (!r.body) { const text = await r.text(); if (text) await window.__proxyCallback( requestId, 'chunk', text, ); await window.__proxyCallback(requestId, 'end', ''); return; } const reader = r.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); if (chunk) await window.__proxyCallback( requestId, 'chunk', chunk, ); } await window.__proxyCallback(requestId, 'end', ''); } catch (e) { await window.__proxyCallback( requestId, 'error', e.message || 'Browser fetch failed', ); } }, { body: req.body, requestId }, ) .catch((err) => { const pending = pendingRequests.get(requestId); if (pending && !pending.res.headersSent) { pending.res.writeHead(502, { 'Content-Type': 'application/json', }); pending.res.end( JSON.stringify({ error: { message: 'Browser evaluate failed: ' + err.message, }, }), ); pending.resolve(); } }); await promise; pendingRequests.delete(requestId); }); // ==================== 启动 ==================== const MAX_INIT_RETRIES = parseInt(process.env.MAX_INIT_RETRIES || '5'); (async () => { // 自动重试启动:网络不稳定时多试几次 for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt++) { try { console.log(`[Stealth] Initialization attempt ${attempt}/${MAX_INIT_RETRIES}...`); await initBrowser(); break; // 成功,跳出重试循环 } catch (e) { console.error(`[Stealth] Attempt ${attempt} failed:`, e.message); // 清理失败的浏览器实例 if (browser) await browser.close().catch(() => {}); browser = null; context = null; challengePage = null; workerPage = null; if (attempt >= MAX_INIT_RETRIES) { console.error(`[Stealth] All ${MAX_INIT_RETRIES} attempts failed, exiting.`); process.exit(1); } const delay = attempt * 5; console.log(`[Stealth] Retrying in ${delay}s...`); await new Promise(r => setTimeout(r, delay * 1000)); } } app.listen(PORT, '0.0.0.0', () => { console.log(`[Stealth] Proxy listening on port ${PORT}`); }); // 定时刷新 challenge setInterval(refreshChallenge, REFRESH_INTERVAL); // 浏览器崩溃恢复 browser.on('disconnected', () => { console.error('[Stealth] Browser disconnected! Restarting...'); ready = false; setTimeout(restartBrowser, 3000); }); })(); // 优雅退出 const shutdown = async () => { console.log('[Stealth] Shutting down...'); ready = false; if (browser) await browser.close().catch(() => {}); process.exit(0); }; process.on('SIGTERM', shutdown); process.on('SIGINT', shutdown);