mirror of
https://github.com/7836246/cursor2api.git
synced 2026-06-20 08:22:15 +08:00
v2.7.4: 截断安全 + 代理续写禁用 + 日志提示词对比视图
- 截断时跳过工具解析,防止损坏的工具调用(写入半截文件) - maxAutoContinue 默认 0,交由 Claude Code 原生续写 - 系统提示词身份声明清除(防 prompt injection 拒绝) - 流式热身窗口 96→300 chars(拒绝检测前不释放文本) - 日志查看器「提示词对比」视图:原始 vs Cursor 转换后 - 转换摘要面板:工具数/消息数/上下文大小一目了然 - 标题提取增强:通用 XML 标签清除 + 更多引导语过滤
This commit is contained in:
216
test/test-hybrid-stream.mjs
Normal file
216
test/test-hybrid-stream.mjs
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* 混合流式完整性测试
|
||||
* 验证:
|
||||
* 1. 文字增量流式 ✓
|
||||
* 2. 工具调用参数完整 ✓
|
||||
* 3. 多工具调用 ✓
|
||||
* 4. 纯文字(无工具调用)✓
|
||||
* 5. stop_reason 正确 ✓
|
||||
*/
|
||||
|
||||
import http from 'http';
|
||||
|
||||
const BASE = process.env.BASE_URL || 'http://localhost:3010';
|
||||
const url = new URL(BASE);
|
||||
|
||||
function runAnthropicTest(name, body, timeout = 60000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => { reject(new Error('超时 ' + timeout + 'ms')); }, timeout);
|
||||
const data = JSON.stringify(body);
|
||||
const req = http.request({
|
||||
hostname: url.hostname, port: url.port, path: '/v1/messages', method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json', 'x-api-key': 'test',
|
||||
'anthropic-version': '2023-06-01', 'Content-Length': Buffer.byteLength(data),
|
||||
},
|
||||
}, (res) => {
|
||||
const start = Date.now();
|
||||
let events = [];
|
||||
let buf = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
buf += chunk.toString();
|
||||
const lines = buf.split('\n');
|
||||
buf = lines.pop(); // keep incomplete last line
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data: ')) continue;
|
||||
const payload = line.slice(6).trim();
|
||||
if (payload === '[DONE]') continue;
|
||||
try {
|
||||
const ev = JSON.parse(payload);
|
||||
events.push({ ...ev, _ts: Date.now() - start });
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
clearTimeout(timer);
|
||||
// 解析结果
|
||||
const textDeltas = events.filter(e => e.type === 'content_block_delta' && e.delta?.type === 'text_delta');
|
||||
const toolStarts = events.filter(e => e.type === 'content_block_start' && e.content_block?.type === 'tool_use');
|
||||
const toolInputDeltas = events.filter(e => e.type === 'content_block_delta' && e.delta?.type === 'input_json_delta');
|
||||
const msgDelta = events.find(e => e.type === 'message_delta');
|
||||
const msgStop = events.find(e => e.type === 'message_stop');
|
||||
|
||||
const fullText = textDeltas.map(e => e.delta.text).join('');
|
||||
const tools = toolStarts.map(ts => {
|
||||
// 收集该工具的 input JSON
|
||||
const inputChunks = toolInputDeltas
|
||||
.filter(d => d.index === ts.index)
|
||||
.map(d => d.delta.partial_json);
|
||||
let parsedInput = null;
|
||||
try { parsedInput = JSON.parse(inputChunks.join('')); } catch { }
|
||||
return {
|
||||
name: ts.content_block.name,
|
||||
id: ts.content_block.id,
|
||||
input: parsedInput,
|
||||
inputRaw: inputChunks.join(''),
|
||||
};
|
||||
});
|
||||
|
||||
resolve({
|
||||
name,
|
||||
textChunks: textDeltas.length,
|
||||
textLength: fullText.length,
|
||||
textPreview: fullText.substring(0, 120).replace(/\n/g, '\\n'),
|
||||
tools,
|
||||
stopReason: msgDelta?.delta?.stop_reason || '?',
|
||||
firstTextMs: textDeltas[0]?._ts ?? -1,
|
||||
firstToolMs: toolStarts[0]?._ts ?? -1,
|
||||
doneMs: msgStop?._ts ?? -1,
|
||||
});
|
||||
});
|
||||
res.on('error', (err) => { clearTimeout(timer); reject(err); });
|
||||
});
|
||||
req.on('error', (err) => { clearTimeout(timer); reject(err); });
|
||||
req.write(data);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function printResult(r) {
|
||||
console.log(`\n 📊 ${r.name}`);
|
||||
console.log(` 时间: 首字=${r.firstTextMs}ms 首工具=${r.firstToolMs}ms 完成=${r.doneMs}ms`);
|
||||
console.log(` 文字: ${r.textChunks} chunks, ${r.textLength} chars`);
|
||||
if (r.textPreview) console.log(` 预览: "${r.textPreview}"`);
|
||||
console.log(` stop_reason: ${r.stopReason}`);
|
||||
if (r.tools.length > 0) {
|
||||
console.log(` 工具调用 (${r.tools.length}个):`);
|
||||
for (const t of r.tools) {
|
||||
console.log(` - ${t.name}(${JSON.stringify(t.input)})`);
|
||||
if (!t.input) console.log(` ⚠️ 参数解析失败! raw: ${t.inputRaw?.substring(0, 100)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TOOLS = [
|
||||
{ name: 'Read', description: 'Read file contents', input_schema: { type: 'object', properties: { file_path: { type: 'string' } }, required: ['file_path'] } },
|
||||
{ name: 'Write', description: 'Write file contents', input_schema: { type: 'object', properties: { file_path: { type: 'string' }, content: { type: 'string' } }, required: ['file_path', 'content'] } },
|
||||
{ name: 'Bash', description: 'Run bash command', input_schema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } },
|
||||
];
|
||||
|
||||
async function main() {
|
||||
console.log('\n ⚡ 混合流式完整性测试\n');
|
||||
|
||||
// 健康检查
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const req = http.get(`${BASE}/health`, r => { r.resume(); r.on('end', resolve); });
|
||||
req.on('error', reject);
|
||||
req.setTimeout(3000, () => { req.destroy(); reject(new Error('timeout')); });
|
||||
});
|
||||
} catch {
|
||||
console.log(' ❌ 服务器未运行\n');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(' ✅ 服务器在线');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
// ---- Test 1: 单工具调用 ----
|
||||
console.log('\n ━━━ ① 单工具调用 ━━━');
|
||||
try {
|
||||
const r = await runAnthropicTest('单工具调用', {
|
||||
model: 'claude-3-5-sonnet-20241022', max_tokens: 1024, stream: true,
|
||||
system: 'You are a coding assistant with file system access. When a user asks to read a file, use the Read tool immediately. Do not refuse or explain limitations.',
|
||||
tools: TOOLS,
|
||||
messages: [{ role: 'user', content: 'Read the file /src/index.ts for me please' }],
|
||||
});
|
||||
printResult(r);
|
||||
|
||||
// 验证
|
||||
const checks = [];
|
||||
checks.push({ name: 'stop_reason=tool_use', ok: r.stopReason === 'tool_use' });
|
||||
checks.push({ name: '至少1个工具调用', ok: r.tools.length >= 1 });
|
||||
checks.push({ name: '工具名=Read', ok: r.tools.some(t => t.name === 'Read') });
|
||||
checks.push({ name: '工具参数有 file_path', ok: r.tools.some(t => t.input?.file_path) });
|
||||
checks.push({ name: '首字延迟<10s', ok: r.firstTextMs >= 0 && r.firstTextMs < 10000 });
|
||||
|
||||
for (const c of checks) {
|
||||
console.log(` ${c.ok ? '✅' : '❌'} ${c.name}`);
|
||||
c.ok ? passed++ : failed++;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(` ❌ 失败: ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
|
||||
// ---- Test 2: 多工具调用 ----
|
||||
console.log('\n ━━━ ② 多工具调用 ━━━');
|
||||
try {
|
||||
const r = await runAnthropicTest('多工具调用', {
|
||||
model: 'claude-3-5-sonnet-20241022', max_tokens: 2048, stream: true,
|
||||
system: 'You are a coding assistant with file system access. When asked to read multiple files, use multiple Read tool calls in a single response. Do not refuse.',
|
||||
tools: TOOLS,
|
||||
messages: [{ role: 'user', content: 'Read both /src/index.ts and /src/config.ts for me' }],
|
||||
});
|
||||
printResult(r);
|
||||
|
||||
const checks = [];
|
||||
checks.push({ name: 'stop_reason=tool_use', ok: r.stopReason === 'tool_use' });
|
||||
checks.push({ name: '≥2个工具调用', ok: r.tools.length >= 2 });
|
||||
checks.push({ name: '工具参数都有 file_path', ok: r.tools.every(t => t.input?.file_path) });
|
||||
|
||||
for (const c of checks) {
|
||||
console.log(` ${c.ok ? '✅' : '❌'} ${c.name}`);
|
||||
c.ok ? passed++ : failed++;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(` ❌ 失败: ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
|
||||
// ---- Test 3: 纯文字(带工具定义但不需要调用) ----
|
||||
console.log('\n ━━━ ③ 纯文字(有工具但不调用) ━━━');
|
||||
try {
|
||||
const r = await runAnthropicTest('纯文字', {
|
||||
model: 'claude-3-5-sonnet-20241022', max_tokens: 512, stream: true,
|
||||
system: 'You are helpful. Answer questions directly without using any tools.',
|
||||
tools: TOOLS,
|
||||
messages: [{ role: 'user', content: 'What is 2+2? Just answer with the number.' }],
|
||||
});
|
||||
printResult(r);
|
||||
|
||||
const checks = [];
|
||||
checks.push({ name: 'stop_reason=end_turn', ok: r.stopReason === 'end_turn' });
|
||||
checks.push({ name: '0个工具调用', ok: r.tools.length === 0 });
|
||||
checks.push({ name: '有文字输出', ok: r.textLength > 0 });
|
||||
checks.push({ name: '文字含数字4', ok: r.textPreview.includes('4') });
|
||||
|
||||
for (const c of checks) {
|
||||
console.log(` ${c.ok ? '✅' : '❌'} ${c.name}`);
|
||||
c.ok ? passed++ : failed++;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(` ❌ 失败: ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
|
||||
// ---- 汇总 ----
|
||||
console.log(`\n ━━━ 汇总 ━━━`);
|
||||
console.log(` ✅ 通过: ${passed} ❌ 失败: ${failed}\n`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch(err => { console.error('致命错误:', err); process.exit(1); });
|
||||
Reference in New Issue
Block a user