mirror of
https://github.com/7836246/cursor2api.git
synced 2026-06-20 15:26:00 +08:00
fix: 连接保活 + 续写收敛优化 + 拒绝模式扩充
- 流式路径:每 15s 发送 SSE 注释 keepalive,防止 504 网关超时 - 非流式路径:chunked 空白字符保活,JSON.parse 天然兼容 - MAX_AUTO_CONTINUE 6→3,新增最小进展阈值(<100 chars 停止) - 新增连续小增量检测(2 次 <500 chars 停止挤牙膏) - 追加 15 条 REFUSAL_PATTERNS(EN scope/expertise + CN 中文拒绝)
This commit is contained in:
@@ -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<void> {
|
||||
// ★ 非流式保活:手动设置 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 工具函数 ====================
|
||||
|
||||
Reference in New Issue
Block a user