/** * cursor-client.ts - Cursor API 客户端 * * 职责: * 1. 发送请求到 https://cursor.com/api/chat(带 Chrome TLS 指纹模拟 headers) * 2. 流式解析 SSE 响应 * 3. 自动重试(最多 2 次) * * 注:x-is-human token 验证已被 Cursor 停用,直接发送空字符串即可。 */ import type { CursorChatRequest, CursorSSEEvent } from './types.js'; import { getConfig } from './config.js'; import { getProxyFetchOptions } from './proxy-agent.js'; const CURSOR_CHAT_API = 'https://cursor.com/api/chat'; // Chrome 浏览器请求头模拟 function getChromeHeaders(): Record { const config = getConfig(); return { 'Content-Type': 'application/json', 'sec-ch-ua-platform': '"Windows"', 'x-path': '/api/chat', 'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"', 'x-method': 'POST', 'sec-ch-ua-bitness': '"64"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-arch': '"x86"', 'sec-ch-ua-platform-version': '"19.0.0"', 'origin': 'https://cursor.com', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://cursor.com/', 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8', 'priority': 'u=1, i', 'user-agent': config.fingerprint.userAgent, 'x-is-human': '', // Cursor 不再校验此字段 'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15' }; } // ==================== API 请求 ==================== /** * 发送请求到 Cursor /api/chat 并以流式方式处理响应(带重试) */ export async function sendCursorRequest( req: CursorChatRequest, onChunk: (event: CursorSSEEvent) => void, ): Promise { const maxRetries = 2; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { await sendCursorRequestInner(req, onChunk); return; } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.error(`[Cursor] 请求失败 (${attempt}/${maxRetries}): ${msg}`); if (attempt < maxRetries) { console.log(`[Cursor] 2s 后重试...`); await new Promise(r => setTimeout(r, 2000)); } else { throw err; } } } } async function sendCursorRequestInner( req: CursorChatRequest, onChunk: (event: CursorSSEEvent) => void, ): Promise { const headers = getChromeHeaders(); console.log(`[Cursor] 发送请求: model=${req.model}, messages=${req.messages.length}`); const config = getConfig(); const controller = new AbortController(); // ★ 空闲超时(Idle Timeout):用读取活动检测替换固定总时长超时。 // 每次收到新数据时重置计时器,只有在指定时间内完全无数据到达时才中断。 // 这样长输出(如写长文章、大量工具调用)不会因总时长超限被误杀。 const IDLE_TIMEOUT_MS = config.timeout * 1000; // 复用 timeout 配置作为空闲超时阈值 let idleTimer: ReturnType | null = null; const resetIdleTimer = () => { if (idleTimer) clearTimeout(idleTimer); idleTimer = setTimeout(() => { console.warn(`[Cursor] 空闲超时(${config.timeout}s 无新数据),中止请求`); controller.abort(); }, IDLE_TIMEOUT_MS); }; // 启动初始计时(等待服务器开始响应) resetIdleTimer(); try { const resp = await fetch(CURSOR_CHAT_API, { method: 'POST', headers, body: JSON.stringify(req), signal: controller.signal, ...getProxyFetchOptions(), } as any); if (!resp.ok) { const body = await resp.text(); throw new Error(`Cursor API 错误: HTTP ${resp.status} - ${body}`); } if (!resp.body) { throw new Error('Cursor API 响应无 body'); } // 流式读取 SSE 响应 const reader = resp.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; // 每次收到数据就重置空闲计时器 resetIdleTimer(); buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.startsWith('data: ')) continue; const data = line.slice(6).trim(); if (!data) continue; try { const event: CursorSSEEvent = JSON.parse(data); onChunk(event); } catch { // 非 JSON 数据,忽略 } } } // 处理剩余 buffer if (buffer.startsWith('data: ')) { const data = buffer.slice(6).trim(); if (data) { try { const event: CursorSSEEvent = JSON.parse(data); onChunk(event); } catch { /* ignore */ } } } } finally { if (idleTimer) clearTimeout(idleTimer); } } /** * 发送非流式请求,收集完整响应 */ export async function sendCursorRequestFull(req: CursorChatRequest): Promise { let fullText = ''; await sendCursorRequest(req, (event) => { if (event.type === 'text-delta' && event.delta) { fullText += event.delta; } }); return fullText; }