mirror of
https://github.com/7836246/cursor2api.git
synced 2026-05-13 09:33:17 +08:00
🗜️ 智能压缩 - 长对话老消息压缩而非丢弃,保留因果链语义 - 工具结果压缩为摘要,助手消息保留工具名 - 压缩率 70-80%,解决 Cursor 上下文溢出问题 ⚠️ 截断检测 - 代码块/XML 未闭合时返回 stop_reason=max_tokens - Claude Code 自动继续,无需手动点击"继续" 🔧 tolerantParse - 新增正则兜底层,处理未转义双引号的 JSON - 解决 position 5384 等长参数解析崩溃 🛡️ 拒绝 fallback 优化 - 工具模式下返回极短引导文本
204 lines
7.4 KiB
TypeScript
204 lines
7.4 KiB
TypeScript
/**
|
||
* 端到端测试:向真实 Cursor2API 服务发送请求
|
||
*
|
||
* 测试场景:
|
||
* 1. 简单请求能正常返回
|
||
* 2. 带工具的多轮长对话触发压缩
|
||
* 3. 验证 stop_reason 正确
|
||
*/
|
||
|
||
const API_URL = 'http://localhost:3010/v1/messages';
|
||
|
||
interface TestResult {
|
||
name: string;
|
||
passed: boolean;
|
||
detail: string;
|
||
}
|
||
|
||
const results: TestResult[] = [];
|
||
|
||
function assert(name: string, condition: boolean, detail = '') {
|
||
results.push({ name, passed: condition, detail });
|
||
console.log(condition ? ` ✅ ${name}` : ` ❌ ${name}: ${detail}`);
|
||
}
|
||
|
||
// 构造一个模拟 Claude Code 的长对话请求(带很多轮工具交互历史)
|
||
function buildLongToolRequest(turnCount: number) {
|
||
const messages: any[] = [];
|
||
|
||
// 模拟多轮工具交互历史
|
||
for (let i = 0; i < turnCount; i++) {
|
||
if (i === 0) {
|
||
// 第一轮:用户发起请求
|
||
messages.push({
|
||
role: 'user',
|
||
content: 'Help me analyze the project structure. Read the main entry file first.'
|
||
});
|
||
} else {
|
||
// 工具结果
|
||
messages.push({
|
||
role: 'user',
|
||
content: [
|
||
{
|
||
type: 'tool_result',
|
||
tool_use_id: `tool_${i}`,
|
||
content: `File content of module${i}.ts:\n` +
|
||
`import { something } from './utils';\n\n` +
|
||
`export class Module${i} {\n` +
|
||
Array.from({length: 30}, (_, j) => ` method${j}() { return ${j}; }`).join('\n') +
|
||
`\n}\n`
|
||
}
|
||
]
|
||
});
|
||
}
|
||
|
||
// 助手的工具调用
|
||
messages.push({
|
||
role: 'assistant',
|
||
content: [
|
||
{ type: 'text', text: `Let me check module${i + 1}.` },
|
||
{
|
||
type: 'tool_use',
|
||
id: `tool_${i + 1}`,
|
||
name: 'Read',
|
||
input: { file_path: `src/module${i + 1}.ts` }
|
||
}
|
||
]
|
||
});
|
||
}
|
||
|
||
// 最后一轮工具结果
|
||
messages.push({
|
||
role: 'user',
|
||
content: [
|
||
{
|
||
type: 'tool_result',
|
||
tool_use_id: `tool_${turnCount}`,
|
||
content: 'File not found: src/module' + turnCount + '.ts'
|
||
}
|
||
]
|
||
});
|
||
|
||
return {
|
||
model: 'claude-sonnet-4-20250514',
|
||
max_tokens: 4096,
|
||
stream: false,
|
||
system: 'You are a helpful coding assistant.',
|
||
tools: [
|
||
{
|
||
name: 'Read',
|
||
description: 'Read a file from disk',
|
||
input_schema: {
|
||
type: 'object',
|
||
properties: {
|
||
file_path: { type: 'string', description: 'Path to the file' }
|
||
},
|
||
required: ['file_path']
|
||
}
|
||
},
|
||
{
|
||
name: 'Bash',
|
||
description: 'Execute a shell command',
|
||
input_schema: {
|
||
type: 'object',
|
||
properties: {
|
||
command: { type: 'string', description: 'The command to execute' }
|
||
},
|
||
required: ['command']
|
||
}
|
||
}
|
||
],
|
||
messages
|
||
};
|
||
}
|
||
|
||
async function runTests() {
|
||
console.log('\n=== 测试 1:基本请求 ===');
|
||
try {
|
||
const resp = await fetch(API_URL, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'x-api-key': 'test' },
|
||
body: JSON.stringify({
|
||
model: 'claude-sonnet-4-20250514',
|
||
max_tokens: 1024,
|
||
stream: false,
|
||
messages: [{ role: 'user', content: 'Say "hello" in one word.' }]
|
||
})
|
||
});
|
||
assert('服务器响应', resp.ok, `status=${resp.status}`);
|
||
const data = await resp.json();
|
||
assert('返回 message 类型', data.type === 'message', `type=${data.type}`);
|
||
assert('stop_reason 是 end_turn', data.stop_reason === 'end_turn', `stop_reason=${data.stop_reason}`);
|
||
assert('有 content', data.content?.length > 0, `content=${JSON.stringify(data.content)}`);
|
||
console.log(` 📝 响应: ${data.content?.[0]?.text?.substring(0, 100)}`);
|
||
} catch (e: any) {
|
||
assert('基本请求', false, e.message);
|
||
}
|
||
|
||
console.log('\n=== 测试 2:长对话工具请求(触发压缩)===');
|
||
try {
|
||
const longReq = buildLongToolRequest(18); // 18 轮 → 37 条消息
|
||
console.log(` 📊 发送 ${longReq.messages.length} 条消息...`);
|
||
|
||
const resp = await fetch(API_URL, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'x-api-key': 'test' },
|
||
body: JSON.stringify(longReq)
|
||
});
|
||
assert('长对话服务器响应', resp.ok, `status=${resp.status}`);
|
||
const data = await resp.json();
|
||
assert('长对话返回 message', data.type === 'message', `type=${data.type}`);
|
||
assert('长对话有 content', data.content?.length > 0);
|
||
|
||
// 检查 stop_reason
|
||
const validStops = ['end_turn', 'tool_use', 'max_tokens'];
|
||
assert('stop_reason 合法', validStops.includes(data.stop_reason), `stop_reason=${data.stop_reason}`);
|
||
|
||
console.log(` 📝 stop_reason: ${data.stop_reason}`);
|
||
console.log(` 📝 content blocks: ${data.content?.length}`);
|
||
if (data.content?.[0]?.text) {
|
||
console.log(` 📝 响应片段: ${data.content[0].text.substring(0, 150)}...`);
|
||
}
|
||
} catch (e: any) {
|
||
assert('长对话请求', false, e.message);
|
||
}
|
||
|
||
console.log('\n=== 测试 3:流式请求 ===');
|
||
try {
|
||
const resp = await fetch(API_URL, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'x-api-key': 'test' },
|
||
body: JSON.stringify({
|
||
model: 'claude-sonnet-4-20250514',
|
||
max_tokens: 1024,
|
||
stream: true,
|
||
messages: [{ role: 'user', content: 'Say "world" in one word.' }]
|
||
})
|
||
});
|
||
assert('流式响应 200', resp.ok, `status=${resp.status}`);
|
||
assert('Content-Type 是 SSE', resp.headers.get('content-type')?.includes('text/event-stream') ?? false);
|
||
|
||
const body = await resp.text();
|
||
const events = body.split('\n').filter(l => l.startsWith('event:'));
|
||
assert('有 SSE 事件', events.length > 0, `events=${events.length}`);
|
||
assert('包含 message_start', body.includes('message_start'));
|
||
assert('包含 message_stop', body.includes('message_stop'));
|
||
|
||
// 检查 stop_reason
|
||
const deltaMatch = body.match(/"stop_reason"\s*:\s*"([^"]+)"/);
|
||
if (deltaMatch) {
|
||
assert('流式 stop_reason 合法', ['end_turn', 'tool_use', 'max_tokens'].includes(deltaMatch[1]), `stop_reason=${deltaMatch[1]}`);
|
||
}
|
||
console.log(` 📝 SSE 事件数: ${events.length}`);
|
||
} catch (e: any) {
|
||
assert('流式请求', false, e.message);
|
||
}
|
||
|
||
// 总结
|
||
const passed = results.filter(r => r.passed).length;
|
||
const failed = results.filter(r => !r.passed).length;
|
||
console.log(`\n=== 端到端结果: ${passed} 通过, ${failed} 失败 ===\n`);
|
||
}
|
||
|
||
runTests().catch(console.error);
|