Files
cursor2api/test/unit-handler-truncation.mjs
huangzhenting f317dc04b0 fix: 修复 thinking 截断时内容泄漏到正文的问题
问题:当模型 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 请求测试),全部通过
2026-03-22 14:10:58 +08:00

90 lines
2.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);