diff --git a/CHANGELOG.md b/CHANGELOG.md index e011889..180e312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## v2.5.5 (2026-03-12) + +### 🐛 修复长响应误判为拒绝 + +- **问题**:工具模式下,模型输出长文本(如 8654 字符的深度分析报告),正文中碰巧包含 `无法提供...信息`、`工具调用场景`、`即报错` 等拒绝检测关键词,导致整个响应被替换为无意义的引导文本 `"I understand the request..."`,进而 Claude Code 陷入死循环 +- **修复策略**: + - 截断响应(`stop_reason=max_tokens`)完全跳过拒绝检测 — 8654 字符的响应不可能是拒绝 + - 长响应(≥ 500 字符)仅检查**前 300 字符**是否包含拒绝模式 — 拒绝一定在开头 + - 短响应(< 500 字符)保持全文检测 — 真正的拒绝回复通常很短 +- 流式和非流式处理均已修复 + +### 🔇 减少 tolerantParse 日志噪音 + +- 模型输出中的普通 JSON 代码块(如含正则 `[\s\S]*?` 的代码示例)不再打印 `error` 级别日志 +- 仅当内容包含 `"tool"` / `"name"` 键(疑似工具调用)时才报 error,其余降为 `warn` 级别 + +--- ## v2.5.4 (2026-03-11) ### 🌐 内网代理支持 (Issue #17) diff --git a/package.json b/package.json index 7b953a8..d0c3086 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cursor2api", - "version": "2.5.4", + "version": "2.5.5", "description": "Proxy Cursor docs AI to Anthropic Messages API for Claude Code", "type": "module", "scripts": { diff --git a/src/converter.ts b/src/converter.ts index 03a6345..2c238f6 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -640,7 +640,13 @@ export function parseToolCalls(responseText: string): { blocksToRemove.push({ start: blockStart, end: closingPos + 3 }); } } catch (e) { - console.error('[Converter] tolerantParse 失败:', e); + // 仅当内容看起来像工具调用时才报 error,否则可能只是普通 JSON 代码块(代码示例等) + const looksLikeToolCall = /["'](?:tool|name)["']\s*:/.test(jsonContent); + if (looksLikeToolCall) { + console.error('[Converter] tolerantParse 失败(疑似工具调用):', e); + } else { + console.warn(`[Converter] 跳过非工具调用的 json 代码块 (${jsonContent.length} chars)`); + } } } else { // 没有闭合 ``` — 代码块被截断,尝试解析已有内容 diff --git a/src/handler.ts b/src/handler.ts index d5129fd..e1a4f55 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -728,7 +728,14 @@ Continue EXACTLY from where you stopped. DO NOT repeat any content already gener // We must send the remaining unsent fullResponse. let textToSend = fullResponse; - if (isRefusal(fullResponse)) { + // ★ 仅对短响应或开头明确匹配拒绝模式的响应进行压制 + // 长响应(如模型在写报告)中可能碰巧包含某个宽泛的拒绝关键词,不应被误判 + // 截断响应(stopReason=max_tokens)一定不是拒绝 + const isShortResponse = fullResponse.trim().length < 500; + const startsWithRefusal = isRefusal(fullResponse.substring(0, 300)); + const isActualRefusal = stopReason !== 'max_tokens' && (isShortResponse ? isRefusal(fullResponse) : startsWithRefusal); + + if (isActualRefusal) { console.log(`[Handler] Supressed complete refusal without tools: ${fullResponse.substring(0, 100)}...`); textToSend = 'I understand the request. Let me proceed with the appropriate action. Could you clarify what specific task you would like me to perform?'; } @@ -859,7 +866,11 @@ async function handleNonStream(res: Response, cursorReq: CursorChatRequest, body } } else { let textToSend = fullText; - if (isRefusal(fullText)) { + // ★ 同样仅对短响应或开头匹配的进行拒绝压制 + const isShort = fullText.trim().length < 500; + const startsRefusal = isRefusal(fullText.substring(0, 300)); + const isRealRefusal = stopReason !== 'max_tokens' && (isShort ? isRefusal(fullText) : startsRefusal); + if (isRealRefusal) { console.log(`[Handler] Supressed pure text refusal (non-stream): ${fullText.substring(0, 100)}...`); textToSend = 'Let me proceed with the task.'; }