From 0e16861b2c550913bc6d8d1ae6664d2090dbb892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=B5=B7?= <7836246@qq.com> Date: Mon, 16 Mar 2026 11:43:16 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=BF=9E=E6=8E=A5=E4=BF=9D=E6=B4=BB=20+?= =?UTF-8?q?=20=E7=BB=AD=E5=86=99=E6=94=B6=E6=95=9B=E4=BC=98=E5=8C=96=20+?= =?UTF-8?q?=20=E6=8B=92=E7=BB=9D=E6=A8=A1=E5=BC=8F=E6=89=A9=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 流式路径:每 15s 发送 SSE 注释 keepalive,防止 504 网关超时 - 非流式路径:chunked 空白字符保活,JSON.parse 天然兼容 - MAX_AUTO_CONTINUE 6→3,新增最小进展阈值(<100 chars 停止) - 新增连续小增量检测(2 次 <500 chars 停止挤牙膏) - 追加 15 条 REFUSAL_PATTERNS(EN scope/expertise + CN 中文拒绝) --- src/handler.ts | 96 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/src/handler.ts b/src/handler.ts index 95e255a..be56065 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -115,6 +115,23 @@ const REFUSAL_PATTERNS = [ /语言偏好请求/, /提供.*具体场景/, /即报错/, + // EN: scope/expertise wordings (2026-03 batch) + /(?:outside|beyond)\s+(?:the\s+)?scope\s+of\s+what/i, + /not\s+(?:within|in)\s+(?:my|the)\s+scope/i, + /this\s+assistant\s+is\s+(?:focused|scoped)/i, + /(?:only|just)\s+(?:able|here)\s+to\s+(?:answer|help)/i, + /I\s+(?:can\s+)?only\s+help\s+with\s+(?:questions|issues)\s+(?:related|about)/i, + /(?:here|designed)\s+to\s+help\s+(?:with\s+)?(?:questions\s+)?about\s+Cursor/i, + /not\s+(?:something|a\s+topic)\s+(?:related|specific)\s+to\s+(?:Cursor|coding)/i, + /outside\s+(?:my|the|your)\s+area\s+of\s+(?:expertise|scope)/i, + /(?:can[.']?t|cannot|unable\s+to)\s+help\s+with\s+(?:this|that)\s+(?:request|question|topic)/i, + /scoped\s+to\s+(?:answering|helping)/i, + // CN: Chinese refusal wordings — Cursor 中文界面下的拒绝 (2026-03 batch) + /只能回答.*(?:Cursor|编辑器).*(?:相关|有关)/, + /专[注门].*(?:回答|帮助|解答).*(?:Cursor|编辑器)/, + /有什么.*(?:Cursor|编辑器).*(?:问题|可以)/, + /无法提供.*(?:推荐|建议|帮助)/, + /(?:功能使用|账户|故障排除|账号|订阅|套餐|计费).*(?:等|问题)/, ]; export function isRefusal(text: string): boolean { @@ -594,6 +611,15 @@ async function handleStream(res: Response, cursorReq: CursorChatRequest, body: A }, }); + // ★ 流式保活:在缓冲/续写期间每 15s 发送 SSE 注释,防止网关判定连接空闲超时 (504) + const keepaliveInterval = setInterval(() => { + try { + res.write(': keepalive\n\n'); + // @ts-expect-error flush exists on ServerResponse when compression is used + if (typeof res.flush === 'function') res.flush(); + } catch { /* connection already closed, ignore */ } + }, 15000); + let fullResponse = ''; let sentText = ''; let blockIndex = 0; @@ -677,8 +703,9 @@ async function handleStream(res: Response, cursorReq: CursorChatRequest, body: A // 流完成后,处理完整响应 // ★ 内部截断续写:如果模型输出过长被截断(常见于写大文件),Proxy 内部分段续写,然后拼接成完整响应 // 这样可以确保工具调用(如 Write)不会横跨两次 API 响应而退化为纯文本 - const MAX_AUTO_CONTINUE = 6; + const MAX_AUTO_CONTINUE = 3; let continueCount = 0; + let consecutiveSmallAdds = 0; // 连续小增量计数 // 保存原始请求的消息快照(不含续写追加的消息) const originalMessages = [...activeCursorReq.messages]; @@ -745,6 +772,23 @@ Continue EXACTLY from where you stopped. DO NOT repeat any content already gener console.log(`[Handler] ⚠️ 续写内容全部为重复,停止续写`); break; } + + // ★ 最小进展检测:去重后新增内容过少(<100 chars),模型几乎已完成 + if (deduped.trim().length < 100) { + console.log(`[Handler] ⚠️ 续写新增内容过少 (${deduped.trim().length} chars < 100),停止续写`); + break; + } + + // ★ 连续小增量检测:连续2次增量 < 500 chars,说明模型已经在挤牙膏 + if (deduped.trim().length < 500) { + consecutiveSmallAdds++; + if (consecutiveSmallAdds >= 2) { + console.log(`[Handler] ⚠️ 连续 ${consecutiveSmallAdds} 次小增量续写,停止续写`); + break; + } + } else { + consecutiveSmallAdds = 0; + } } let stopReason = (hasTools && isTruncated(fullResponse)) ? 'max_tokens' : 'end_turn'; @@ -939,6 +983,9 @@ Continue EXACTLY from where you stopped. DO NOT repeat any content already gener writeSSE(res, 'error', { type: 'error', error: { type: 'api_error', message }, }); + } finally { + // ★ 清除保活定时器 + clearInterval(keepaliveInterval); } res.end(); @@ -947,6 +994,18 @@ Continue EXACTLY from where you stopped. DO NOT repeat any content already gener // ==================== 非流式处理 ==================== async function handleNonStream(res: Response, cursorReq: CursorChatRequest, body: AnthropicRequest): Promise { + // ★ 非流式保活:手动设置 chunked 响应,在缓冲期间每 15s 发送空白字符保活 + // JSON.parse 会忽略前导空白,所以客户端解析不受影响 + res.writeHead(200, { 'Content-Type': 'application/json' }); + const keepaliveInterval = setInterval(() => { + try { + res.write(' '); + // @ts-expect-error flush exists on ServerResponse when compression is used + if (typeof res.flush === 'function') res.flush(); + } catch { /* connection already closed, ignore */ } + }, 15000); + + try { let fullText = await sendCursorRequestFull(cursorReq); const hasTools = (body.tools?.length ?? 0) > 0; let activeCursorReq = cursorReq; @@ -1004,8 +1063,9 @@ async function handleNonStream(res: Response, cursorReq: CursorChatRequest, body // ★ 内部截断续写(与流式路径对齐) // Claude CLI 使用非流式模式时,写大文件最容易被截断 // 在 proxy 内部完成续写,确保工具调用参数完整 - const MAX_AUTO_CONTINUE = 6; + const MAX_AUTO_CONTINUE = 3; let continueCount = 0; + let consecutiveSmallAdds = 0; // 连续小增量计数 const originalMessages = [...activeCursorReq.messages]; while (hasTools && isTruncated(fullText) && continueCount < MAX_AUTO_CONTINUE) { @@ -1061,6 +1121,23 @@ Continue EXACTLY from where you stopped. DO NOT repeat any content already gener console.log(`[Handler] ⚠️ 非流式续写内容全部为重复,停止续写`); break; } + + // ★ 最小进展检测:去重后新增内容过少(<100 chars),模型几乎已完成 + if (deduped.trim().length < 100) { + console.log(`[Handler] ⚠️ 非流式续写新增内容过少 (${deduped.trim().length} chars < 100),停止续写`); + break; + } + + // ★ 连续小增量检测:连续2次增量 < 500 chars,说明模型已经在挤牙膏 + if (deduped.trim().length < 500) { + consecutiveSmallAdds++; + if (consecutiveSmallAdds >= 2) { + console.log(`[Handler] ⚠️ 非流式连续 ${consecutiveSmallAdds} 次小增量续写,停止续写`); + break; + } + } else { + consecutiveSmallAdds = 0; + } } const contentBlocks: AnthropicContentBlock[] = []; @@ -1166,7 +1243,20 @@ Continue EXACTLY from where you stopped. DO NOT repeat any content already gener }, }; - res.json(response); + clearInterval(keepaliveInterval); + res.end(JSON.stringify(response)); + + } catch (err: unknown) { + clearInterval(keepaliveInterval); + const message = err instanceof Error ? err.message : String(err); + console.error(`[Handler] 非流式请求处理失败:`, message); + try { + res.end(JSON.stringify({ + type: 'error', + error: { type: 'api_error', message }, + })); + } catch { /* response already ended */ } + } } // ==================== SSE 工具函数 ====================