mirror of
https://github.com/7836246/cursor2api.git
synced 2026-06-20 10:22:16 +08:00
fix: 修复 SSE 流式事件格式错误、启用配置超时、修正工具调用完整性检测
1. handler.ts: 修复 content_block_delta 事件缺少 index 和 delta 包装层的严重 Bug - 当 AI 响应包含 ```json 但非工具调用时,文本增量会因格式错误而丢失 2. cursor-client.ts: 请求超时改用 config.timeout 配置值,不再硬编码 120s 3. converter.ts: 修复 isToolCallComplete() 始终返回 true 的逻辑错误 4. handler.ts: 移除未使用的 isToolCallComplete 导入
This commit is contained in:
@@ -238,10 +238,6 @@ function extractToolResultText(block: AnthropicContentBlock): string {
|
||||
|
||||
// ==================== 响应解析 ====================
|
||||
|
||||
/**
|
||||
* 从 AI 响应文本中解析工具调用
|
||||
* 匹配 ```json action ... ``` 块
|
||||
*/
|
||||
export function parseToolCalls(responseText: string): {
|
||||
toolCalls: ParsedToolCall[];
|
||||
cleanText: string;
|
||||
@@ -249,27 +245,29 @@ export function parseToolCalls(responseText: string): {
|
||||
const toolCalls: ParsedToolCall[] = [];
|
||||
let cleanText = responseText;
|
||||
|
||||
const regex = /```json\s+action[\s\S]*?\{([\s\S]*?)\}\s*```/g;
|
||||
|
||||
// 我们先把整块内容取出来
|
||||
const fullBlockRegex = /```json\s+action\s*([\s\S]*?)\s*```/g;
|
||||
const fullBlockRegex = /```json(?:\s+action)?\s*([\s\S]*?)\s*```/g;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = fullBlockRegex.exec(responseText)) !== null) {
|
||||
let isToolCall = false;
|
||||
try {
|
||||
const parsed = JSON.parse(match[1]);
|
||||
if (parsed.tool) {
|
||||
// check for tool or name
|
||||
if (parsed.tool || parsed.name) {
|
||||
toolCalls.push({
|
||||
name: parsed.tool,
|
||||
arguments: parsed.parameters || {}
|
||||
name: parsed.tool || parsed.name,
|
||||
arguments: parsed.parameters || parsed.arguments || parsed.input || {}
|
||||
});
|
||||
isToolCall = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Converter] Failed to parse JSON action block:', e);
|
||||
// Ignored, not a valid json tool call
|
||||
}
|
||||
|
||||
// 移除已解析的调用块
|
||||
cleanText = cleanText.replace(match[0], '');
|
||||
if (isToolCall) {
|
||||
// 移除已解析的调用块
|
||||
cleanText = cleanText.replace(match[0], '');
|
||||
}
|
||||
}
|
||||
|
||||
return { toolCalls, cleanText: cleanText.trim() };
|
||||
@@ -279,7 +277,7 @@ export function parseToolCalls(responseText: string): {
|
||||
* 检查文本是否包含工具调用
|
||||
*/
|
||||
export function hasToolCalls(text: string): boolean {
|
||||
return text.includes('```json action');
|
||||
return text.includes('```json');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,9 +285,10 @@ export function hasToolCalls(text: string): boolean {
|
||||
*/
|
||||
export function isToolCallComplete(text: string): boolean {
|
||||
const openCount = (text.match(/```json\s+action/g) || []).length;
|
||||
const closeCount = (text.match(/```(?!json\s+action)/g) || []).length;
|
||||
// 粗略估计:如果是 ``` 结尾的,通常是结束了。这里不做完全精确匹配
|
||||
return true;
|
||||
// Count closing ``` that are NOT part of opening ```json action
|
||||
const allBackticks = (text.match(/```/g) || []).length;
|
||||
const closeCount = allBackticks - openCount;
|
||||
return openCount > 0 && closeCount >= openCount;
|
||||
}
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
|
||||
@@ -252,9 +252,10 @@ async function sendCursorRequestInner(
|
||||
|
||||
console.log(`[Cursor] 发送请求: model=${req.model}, messages=${req.messages.length}`);
|
||||
|
||||
// 请求级超时
|
||||
// 请求级超时(使用配置值)
|
||||
const config = getConfig();
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 120_000); // 2分钟
|
||||
const timeout = setTimeout(() => controller.abort(), config.timeout * 1000);
|
||||
|
||||
try {
|
||||
const resp = await fetch(CURSOR_CHAT_API, {
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
AnthropicContentBlock,
|
||||
CursorSSEEvent,
|
||||
} from './types.js';
|
||||
import { convertToCursorRequest, parseToolCalls, hasToolCalls, isToolCallComplete } from './converter.js';
|
||||
import { convertToCursorRequest, parseToolCalls, hasToolCalls } from './converter.js';
|
||||
import { sendCursorRequest, sendCursorRequestFull } from './cursor-client.js';
|
||||
import { getConfig } from './config.js';
|
||||
|
||||
@@ -105,6 +105,7 @@ async function handleStream(res: Response, cursorReq: ReturnType<typeof convertT
|
||||
});
|
||||
|
||||
let fullResponse = '';
|
||||
let sentText = '';
|
||||
let blockIndex = 0;
|
||||
let textBlockStarted = false;
|
||||
|
||||
@@ -137,6 +138,7 @@ async function handleStream(res: Response, cursorReq: ReturnType<typeof convertT
|
||||
index: blockIndex,
|
||||
delta: { type: 'text_delta', text: event.delta },
|
||||
});
|
||||
sentText += event.delta;
|
||||
});
|
||||
|
||||
// 流完成后,处理完整响应
|
||||
@@ -148,10 +150,33 @@ async function handleStream(res: Response, cursorReq: ReturnType<typeof convertT
|
||||
if (toolCalls.length > 0) {
|
||||
stopReason = 'tool_use';
|
||||
|
||||
// 如果有工具调用前的文本,确保文本块已关闭
|
||||
// find match length of sentText in cleanText
|
||||
let matchLen = 0;
|
||||
for (let i = Math.min(cleanText.length, sentText.length); i >= 0; i--) {
|
||||
if (cleanText.startsWith(sentText.substring(0, i))) {
|
||||
matchLen = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const unsentCleanText = cleanText.substring(matchLen).trim();
|
||||
|
||||
// 如果解析后有干净的文本(工具调用之外的文本),发送文本块
|
||||
if (unsentCleanText) {
|
||||
if (!textBlockStarted) {
|
||||
writeSSE(res, 'content_block_start', {
|
||||
type: 'content_block_start', index: blockIndex,
|
||||
content_block: { type: 'text', text: '' },
|
||||
});
|
||||
textBlockStarted = true;
|
||||
}
|
||||
writeSSE(res, 'content_block_delta', {
|
||||
type: 'content_block_delta', index: blockIndex,
|
||||
delta: { type: 'text_delta', text: (sentText.endsWith('\n') ? '' : '\n') + unsentCleanText }
|
||||
});
|
||||
}
|
||||
|
||||
// 如果有文本块,确保完成结束
|
||||
if (textBlockStarted) {
|
||||
// 需要重新计算:流式发送的文本可能包含了工具调用标签之前的部分
|
||||
// 这里我们结束当前文本块
|
||||
writeSSE(res, 'content_block_stop', {
|
||||
type: 'content_block_stop', index: blockIndex,
|
||||
});
|
||||
@@ -159,13 +184,6 @@ async function handleStream(res: Response, cursorReq: ReturnType<typeof convertT
|
||||
textBlockStarted = false;
|
||||
}
|
||||
|
||||
// 如果解析后有干净的文本(工具调用之外的文本),发送文本块
|
||||
if (cleanText && !textBlockStarted) {
|
||||
// 文本可能已经通过流式增量发送了,这里的 cleanText 是去除工具调用后的剩余
|
||||
// 不需要重复发送
|
||||
}
|
||||
|
||||
// 发送工具调用块
|
||||
for (const tc of toolCalls) {
|
||||
const tcId = toolId();
|
||||
writeSSE(res, 'content_block_start', {
|
||||
@@ -186,6 +204,23 @@ async function handleStream(res: Response, cursorReq: ReturnType<typeof convertT
|
||||
});
|
||||
blockIndex++;
|
||||
}
|
||||
} else {
|
||||
// False alarm! The tool triggers were just normal text.
|
||||
// We must send the remaining unsent fullResponse.
|
||||
const unsentText = fullResponse.substring(sentText.length);
|
||||
if (unsentText) {
|
||||
if (!textBlockStarted) {
|
||||
writeSSE(res, 'content_block_start', {
|
||||
type: 'content_block_start', index: blockIndex,
|
||||
content_block: { type: 'text', text: '' },
|
||||
});
|
||||
textBlockStarted = true;
|
||||
}
|
||||
writeSSE(res, 'content_block_delta', {
|
||||
type: 'content_block_delta', index: blockIndex,
|
||||
delta: { type: 'text_delta', text: unsentText },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user