mirror of
https://github.com/7836246/cursor2api.git
synced 2026-06-02 11:59:45 +08:00
问题:当模型 thinking 内容超出单次输出上限时,<thinking> 标签未闭合, 导致 thinking 内容被当作正文泄漏给客户端;续写请求中 assistantContext 含未闭合标签,模型不知道思考阶段已结束,继续输出 thinking 而非正文。 修复: 1. splitLeadingThinkingBlocks:未闭合时返回已积累的部分 thinkingContent 而非空字符串,供调用方正确提取 2. handler.ts / openai-handler.ts:流结束 flush 新增 !complete 分支, 提取截断的 thinkingContent,不将 thinking 内容 flush 为正文 3. 新增 closeUnclosedThinking:续写前补全缺失的 </thinking> 标签, 应用于所有 4 处续写 assistantContext 构建,让模型正确从正文续写 4. shouldAutoContinueTruncatedToolResponse:json action 块未闭合时 跳过 200-char 检查,修复 thinking 剥离后正文过短导致续写不触发的问题 测试:新增 unit-thinking-truncation.mjs(11个单元测试)、 e2e-thinking-truncation.mjs(3个实际 API 请求测试),全部通过
90 lines
2.6 KiB
JavaScript
90 lines
2.6 KiB
JavaScript
import { shouldAutoContinueTruncatedToolResponse } from '../dist/handler.js';
|
||
|
||
let passed = 0;
|
||
let failed = 0;
|
||
|
||
function test(name, fn) {
|
||
try {
|
||
fn();
|
||
console.log(` ✅ ${name}`);
|
||
passed++;
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
console.error(` ❌ ${name}`);
|
||
console.error(` ${message}`);
|
||
failed++;
|
||
}
|
||
}
|
||
|
||
function assertEqual(actual, expected, message) {
|
||
if (actual !== expected) {
|
||
throw new Error(message || `Expected ${expected}, got ${actual}`);
|
||
}
|
||
}
|
||
|
||
console.log('\n📦 handler 截断续写判定\n');
|
||
|
||
test('短参数工具调用可恢复时不再继续续写', () => {
|
||
const text = [
|
||
'我先读取配置文件。',
|
||
'',
|
||
'```json action',
|
||
'{',
|
||
' "tool": "Read",',
|
||
' "parameters": {',
|
||
' "file_path": "/app/config.yaml"',
|
||
' }',
|
||
].join('\n');
|
||
|
||
assertEqual(
|
||
shouldAutoContinueTruncatedToolResponse(text, true),
|
||
false,
|
||
'Read 这类短参数工具不应继续续写',
|
||
);
|
||
});
|
||
|
||
test('大参数写入工具仍然继续续写', () => {
|
||
const longContent = 'A'.repeat(4000);
|
||
const text = [
|
||
'```json action',
|
||
'{',
|
||
' "tool": "Write",',
|
||
' "parameters": {',
|
||
' "file_path": "/tmp/large.txt",',
|
||
` "content": "${longContent}`,
|
||
].join('\n');
|
||
|
||
assertEqual(
|
||
shouldAutoContinueTruncatedToolResponse(text, true),
|
||
true,
|
||
'Write 大内容仍应继续续写以补全参数',
|
||
);
|
||
});
|
||
|
||
test('普通代码块截断但文本过短(<200字)不续写', () => {
|
||
// 200-char 保护:非 json action 块截断时,过短的响应缺乏上下文,不触发续写
|
||
const text = '```ts\nexport const answer = {';
|
||
|
||
assertEqual(
|
||
shouldAutoContinueTruncatedToolResponse(text, true),
|
||
false,
|
||
'非 json action 块且文本 <200 chars 时不应续写',
|
||
);
|
||
});
|
||
|
||
test('json action 块未闭合且文本过短时仍触发续写(thinking 剥离后场景)', () => {
|
||
// 场景:thinking 剥离后 fullResponse 只剩 json action 块开头(很短)
|
||
// 200-char 保护不应阻止这种明确的工具调用截断
|
||
const text = '```json action\n{\n "tool": "Write",';
|
||
|
||
assertEqual(
|
||
shouldAutoContinueTruncatedToolResponse(text, true),
|
||
true,
|
||
'json action 块未闭合时即使文本 <200 chars 也应续写',
|
||
);
|
||
});
|
||
|
||
console.log(`\n结果: ${passed} 通过 / ${failed} 失败 / ${passed + failed} 总计\n`);
|
||
|
||
if (failed > 0) process.exit(1);
|