| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| 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';
|
|
|
|
|
| function getChromeHeaders(): Record<string, string> {
|
| 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': '',
|
| 'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15'
|
| };
|
| }
|
|
|
|
|
|
|
| |
| |
|
|
| export async function sendCursorRequest(
|
| req: CursorChatRequest,
|
| onChunk: (event: CursorSSEEvent) => void,
|
| ): Promise<void> {
|
| 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<void> {
|
| const headers = getChromeHeaders();
|
|
|
| console.log(`[Cursor] 发送请求: model=${req.model}, messages=${req.messages.length}`);
|
|
|
| const config = getConfig();
|
| const controller = new AbortController();
|
|
|
|
|
|
|
|
|
| const IDLE_TIMEOUT_MS = config.timeout * 1000;
|
| let idleTimer: ReturnType<typeof setTimeout> | 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');
|
| }
|
|
|
|
|
| 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 {
|
|
|
| }
|
| }
|
| }
|
|
|
|
|
| if (buffer.startsWith('data: ')) {
|
| const data = buffer.slice(6).trim();
|
| if (data) {
|
| try {
|
| const event: CursorSSEEvent = JSON.parse(data);
|
| onChunk(event);
|
| } catch { }
|
| }
|
| }
|
| } finally {
|
| if (idleTimer) clearTimeout(idleTimer);
|
| }
|
| }
|
|
|
| |
| |
|
|
| export async function sendCursorRequestFull(req: CursorChatRequest): Promise<string> {
|
| let fullText = '';
|
| await sendCursorRequest(req, (event) => {
|
| if (event.type === 'text-delta' && event.delta) {
|
| fullText += event.delta;
|
| }
|
| });
|
| return fullText;
|
| }
|
|
|