fix: 修复长响应误判为拒绝 + 减少 tolerantParse 日志噪音 (v2.5.5)

- 工具模式下长文本(8654 chars)正文碰巧含拒绝关键词被误判,导致 Claude Code 死循环
- 截断响应(max_tokens)跳过拒绝检测;长响应仅检查前300字符
- tolerantParse 对非工具调用的 JSON 代码块降为 warn 级别日志
This commit is contained in:
小海
2026-03-12 10:05:31 +08:00
parent 13098fb84d
commit 3dceac115c
4 changed files with 38 additions and 4 deletions

View File

@@ -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)

View File

@@ -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": {

View File

@@ -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 {
// 没有闭合 ``` — 代码块被截断,尝试解析已有内容

View File

@@ -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.';
}