mirror of
https://github.com/7836246/cursor2api.git
synced 2026-05-07 22:27:15 +08:00
{ "message": "fix: stop renaming file_path to path implicitly, and remove compression tests" }
This commit is contained in:
@@ -25,10 +25,9 @@ const SMART_SINGLE_QUOTES = new Set([
|
||||
export function normalizeToolArguments(args: Record<string, unknown>): Record<string, unknown> {
|
||||
if (!args || typeof args !== 'object') return args;
|
||||
|
||||
if ('file_path' in args && !('path' in args)) {
|
||||
args.path = args.file_path;
|
||||
delete args.file_path;
|
||||
}
|
||||
// Removed legacy mapping that forcefully converted 'file_path' to 'path'.
|
||||
// Claude Code 2.1.71 tools like 'Read' legitimately require 'file_path' as per their schema,
|
||||
// and this legacy mapping causes infinite loop failures.
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
/**
|
||||
* 快速测试:上下文压缩 + tolerantParse 增强
|
||||
*/
|
||||
|
||||
// ==================== 1. tolerantParse 测试 ====================
|
||||
|
||||
// 内联一个简化版 tolerantParse 进行测试
|
||||
function tolerantParse(jsonStr: string): any {
|
||||
try { return JSON.parse(jsonStr); } catch {}
|
||||
|
||||
let inString = false, escaped = false, fixed = '';
|
||||
const bracketStack: string[] = [];
|
||||
for (let i = 0; i < jsonStr.length; i++) {
|
||||
const char = jsonStr[i];
|
||||
if (char === '\\' && !escaped) { escaped = true; fixed += char; }
|
||||
else if (char === '"' && !escaped) { inString = !inString; fixed += char; escaped = false; }
|
||||
else {
|
||||
if (inString) {
|
||||
if (char === '\n') fixed += '\\n';
|
||||
else if (char === '\r') fixed += '\\r';
|
||||
else if (char === '\t') fixed += '\\t';
|
||||
else fixed += char;
|
||||
} else {
|
||||
if (char === '{' || char === '[') bracketStack.push(char === '{' ? '}' : ']');
|
||||
else if (char === '}' || char === ']') { if (bracketStack.length > 0) bracketStack.pop(); }
|
||||
fixed += char;
|
||||
}
|
||||
escaped = false;
|
||||
}
|
||||
}
|
||||
if (inString) fixed += '"';
|
||||
while (bracketStack.length > 0) fixed += bracketStack.pop();
|
||||
fixed = fixed.replace(/,\s*([}\]])/g, '$1');
|
||||
|
||||
try { return JSON.parse(fixed); } catch (_e2) {
|
||||
const lastBrace = fixed.lastIndexOf('}');
|
||||
if (lastBrace > 0) { try { return JSON.parse(fixed.substring(0, lastBrace + 1)); } catch {} }
|
||||
|
||||
// 第四层:正则兜底
|
||||
try {
|
||||
const toolMatch = jsonStr.match(/"(?:tool|name)"\s*:\s*"([^"]+)"/);
|
||||
if (toolMatch) {
|
||||
const toolName = toolMatch[1];
|
||||
const paramsMatch = jsonStr.match(/"(?:parameters|arguments|input)"\s*:\s*(\{[\s\S]*)/);
|
||||
let params: Record<string, unknown> = {};
|
||||
if (paramsMatch) {
|
||||
const paramsStr = paramsMatch[1];
|
||||
let depth = 0, end = -1, pInString = false, pEscaped = false;
|
||||
for (let i = 0; i < paramsStr.length; i++) {
|
||||
const c = paramsStr[i];
|
||||
if (c === '\\' && !pEscaped) { pEscaped = true; continue; }
|
||||
if (c === '"' && !pEscaped) { pInString = !pInString; }
|
||||
if (!pInString) {
|
||||
if (c === '{') depth++;
|
||||
if (c === '}') { depth--; if (depth === 0) { end = i; break; } }
|
||||
}
|
||||
pEscaped = false;
|
||||
}
|
||||
if (end > 0) {
|
||||
const rawParams = paramsStr.substring(0, end + 1);
|
||||
try { params = JSON.parse(rawParams); } catch {
|
||||
const fieldRegex = /"([^"]+)"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
|
||||
let fm;
|
||||
while ((fm = fieldRegex.exec(rawParams)) !== null) {
|
||||
params[fm[1]] = fm[2].replace(/\\n/g, '\n').replace(/\\t/g, '\t');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { tool: toolName, parameters: params };
|
||||
}
|
||||
} catch {}
|
||||
throw _e2;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 2. compressMessage 测试 ====================
|
||||
const COMPRESS_CONTENT_MAX = 200;
|
||||
|
||||
function compressMessage(role: string, text: string): string {
|
||||
if (text.length <= COMPRESS_CONTENT_MAX) return text;
|
||||
if (role === 'user') {
|
||||
const actionMatch = text.match(/^Action output:\n([\s\S]*?)(?:\n\nBased on the output above|$)/);
|
||||
if (actionMatch) {
|
||||
const output = actionMatch[1];
|
||||
const firstLine = output.split('\n')[0]?.trim() || '';
|
||||
const lineCount = output.split('\n').length;
|
||||
return `Action output: [${output.length} chars, ${lineCount} lines] ${firstLine.substring(0, 80)}...`;
|
||||
}
|
||||
const errorMatch = text.match(/^The action encountered an error:\n([\s\S]*?)(?:\n\nBased on the output above|$)/);
|
||||
if (errorMatch) {
|
||||
return `Action error: ${errorMatch[1].substring(0, 150)}...`;
|
||||
}
|
||||
return text.substring(0, COMPRESS_CONTENT_MAX) + `... [${text.length} chars total]`;
|
||||
}
|
||||
if (role === 'assistant') {
|
||||
const toolBlocks = text.match(/```json action\s*\n([\s\S]*?)```/g);
|
||||
if (toolBlocks && toolBlocks.length > 0) {
|
||||
const summaries: string[] = [];
|
||||
for (const block of toolBlocks) {
|
||||
try {
|
||||
const jsonMatch = block.match(/```json action\s*\n([\s\S]*?)```/);
|
||||
if (jsonMatch) {
|
||||
const parsed = JSON.parse(jsonMatch[1]);
|
||||
const toolName = parsed.tool || parsed.name || 'unknown';
|
||||
const paramKeys = parsed.parameters ? Object.keys(parsed.parameters) : [];
|
||||
summaries.push(`[Called ${toolName}(${paramKeys.join(', ')})]`);
|
||||
}
|
||||
} catch { summaries.push('[Called action]'); }
|
||||
}
|
||||
const cleanText = text.replace(/```json action\s*\n[\s\S]*?```/g, '').trim();
|
||||
const briefText = cleanText.length > 100 ? cleanText.substring(0, 100) + '...' : cleanText;
|
||||
return (briefText ? briefText + '\n' : '') + summaries.join('\n');
|
||||
}
|
||||
return text.substring(0, COMPRESS_CONTENT_MAX) + `... [${text.length} chars]`;
|
||||
}
|
||||
return text.substring(0, COMPRESS_CONTENT_MAX) + '...';
|
||||
}
|
||||
|
||||
// ==================== 运行测试 ====================
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
function assert(name: string, condition: boolean, detail?: string) {
|
||||
if (condition) { passed++; console.log(` ✅ ${name}`); }
|
||||
else { failed++; console.log(` ❌ ${name}${detail ? ': ' + detail : ''}`); }
|
||||
}
|
||||
|
||||
console.log('\n=== tolerantParse 测试 ===');
|
||||
|
||||
// 正常 JSON
|
||||
const t1 = tolerantParse('{"tool":"read_file","parameters":{"file_path":"src/index.ts"}}');
|
||||
assert('正常 JSON', t1.tool === 'read_file' && t1.parameters.file_path === 'src/index.ts');
|
||||
|
||||
// 带裸换行符
|
||||
const t2 = tolerantParse('{"tool":"write_file","parameters":{"content":"line1\nline2"}}');
|
||||
assert('裸换行修复', t2.tool === 'write_file');
|
||||
|
||||
// 截断 JSON(未闭合)
|
||||
const t3 = tolerantParse('{"tool":"bash","parameters":{"command":"ls -la');
|
||||
assert('截断兜底', t3.tool === 'bash');
|
||||
|
||||
// 含未转义引号的代码内容(最重要的场景)
|
||||
const badJson = `{
|
||||
"tool": "write_file",
|
||||
"parameters": {
|
||||
"file_path": "test.ts",
|
||||
"content": "const x = "hello"; console.log(x);"
|
||||
}
|
||||
}`;
|
||||
const t4 = tolerantParse(badJson);
|
||||
assert('未转义引号 - 提取 tool 名', t4.tool === 'write_file');
|
||||
assert('未转义引号 - 提取参数', Object.keys(t4.parameters).length > 0, `keys=${JSON.stringify(Object.keys(t4.parameters))}`);
|
||||
|
||||
// 尾部逗号
|
||||
const t5 = tolerantParse('{"tool":"list_dir","parameters":{"path":"./",},}');
|
||||
assert('尾部逗号修复', t5.tool === 'list_dir');
|
||||
|
||||
console.log('\n=== compressMessage 测试 ===');
|
||||
|
||||
// 短消息不压缩
|
||||
assert('短消息保留', compressMessage('user', 'hello world') === 'hello world');
|
||||
|
||||
// 长工具结果压缩
|
||||
const longResult = 'Action output:\n' + 'x'.repeat(5000) + '\n\nBased on the output above, continue...';
|
||||
const c1 = compressMessage('user', longResult);
|
||||
assert('工具结果压缩', c1.length < 200, `压缩到 ${c1.length} chars`);
|
||||
assert('工具结果保留信息', c1.includes('5000 chars') && c1.includes('Action output'));
|
||||
|
||||
// 错误结果压缩
|
||||
const errorResult = 'The action encountered an error:\nPermission denied: cannot access /root/secret\n\nBased on the output above, continue...';
|
||||
const c2 = compressMessage('user', errorResult.padEnd(300, ' detail'));
|
||||
assert('错误结果压缩', c2.startsWith('Action error:'));
|
||||
|
||||
// 助手消息(工具调用)压缩
|
||||
const assistantMsg = `Let me check the file structure first.\n\n\`\`\`json action\n{"tool":"read_file","parameters":{"file_path":"src/index.ts"}}\n\`\`\`\n\nAnd then more text here to pad the message beyond the threshold limit. ${'x'.repeat(200)}`;
|
||||
const c3 = compressMessage('assistant', assistantMsg);
|
||||
assert('助手消息压缩保留工具名', c3.includes('[Called read_file(file_path)]'), c3);
|
||||
assert('助手消息压缩', c3.length < assistantMsg.length, `${c3.length} < ${assistantMsg.length}`);
|
||||
|
||||
// 普通长用户消息
|
||||
const longUser = 'Please help me with '.padEnd(500, 'this task ');
|
||||
const c4 = compressMessage('user', longUser);
|
||||
assert('普通用户消息截短', c4.length < 300 && c4.includes('chars total'));
|
||||
|
||||
console.log(`\n=== 结果: ${passed} 通过, ${failed} 失败 ===\n`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
@@ -1,221 +0,0 @@
|
||||
/**
|
||||
* 集成测试:模拟长对话,验证上下文压缩流程
|
||||
*
|
||||
* 测试场景:30+ 条消息的工具模式对话,验证:
|
||||
* 1. 压缩触发条件(消息数 > 30 或字符数 > 60000)
|
||||
* 2. few-shot 头部不被压缩
|
||||
* 3. 最近 6 条消息保持原文
|
||||
* 4. 中间老消息被压缩
|
||||
* 5. 消息数量不变(压缩不丢弃)
|
||||
* 6. 总字符数显著减少
|
||||
*/
|
||||
|
||||
// 模拟 CursorMessage 类型
|
||||
interface CursorMessage {
|
||||
parts: { type: string; text?: string }[];
|
||||
id: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
// 从 converter.ts 复制常量
|
||||
const FEWSHOT_COUNT = 2;
|
||||
const MAX_CURSOR_MESSAGES = 30;
|
||||
const MAX_CONTEXT_CHARS = 60000;
|
||||
const KEEP_RECENT_MESSAGES = 6;
|
||||
const COMPRESS_CONTENT_MAX = 200;
|
||||
|
||||
// 从 converter.ts 复制 compressMessage
|
||||
function compressMessage(role: string, text: string): string {
|
||||
if (text.length <= COMPRESS_CONTENT_MAX) return text;
|
||||
if (role === 'user') {
|
||||
const actionMatch = text.match(/^Action output:\n([\s\S]*?)(?:\n\nBased on the output above|$)/);
|
||||
if (actionMatch) {
|
||||
const output = actionMatch[1];
|
||||
const firstLine = output.split('\n')[0]?.trim() || '';
|
||||
const lineCount = output.split('\n').length;
|
||||
return `Action output: [${output.length} chars, ${lineCount} lines] ${firstLine.substring(0, 80)}...`;
|
||||
}
|
||||
const errorMatch = text.match(/^The action encountered an error:\n([\s\S]*?)(?:\n\nBased on the output above|$)/);
|
||||
if (errorMatch) {
|
||||
return `Action error: ${errorMatch[1].substring(0, 150)}...`;
|
||||
}
|
||||
return text.substring(0, COMPRESS_CONTENT_MAX) + `... [${text.length} chars total]`;
|
||||
}
|
||||
if (role === 'assistant') {
|
||||
const toolBlocks = text.match(/```json action\s*\n([\s\S]*?)```/g);
|
||||
if (toolBlocks && toolBlocks.length > 0) {
|
||||
const summaries: string[] = [];
|
||||
for (const block of toolBlocks) {
|
||||
try {
|
||||
const jsonMatch = block.match(/```json action\s*\n([\s\S]*?)```/);
|
||||
if (jsonMatch) {
|
||||
const parsed = JSON.parse(jsonMatch[1]);
|
||||
const toolName = parsed.tool || parsed.name || 'unknown';
|
||||
const paramKeys = parsed.parameters ? Object.keys(parsed.parameters) : [];
|
||||
summaries.push(`[Called ${toolName}(${paramKeys.join(', ')})]`);
|
||||
}
|
||||
} catch { summaries.push('[Called action]'); }
|
||||
}
|
||||
const cleanText = text.replace(/```json action\s*\n[\s\S]*?```/g, '').trim();
|
||||
const briefText = cleanText.length > 100 ? cleanText.substring(0, 100) + '...' : cleanText;
|
||||
return (briefText ? briefText + '\n' : '') + summaries.join('\n');
|
||||
}
|
||||
return text.substring(0, COMPRESS_CONTENT_MAX) + `... [${text.length} chars]`;
|
||||
}
|
||||
return text.substring(0, COMPRESS_CONTENT_MAX) + '...';
|
||||
}
|
||||
|
||||
// 模拟 converter.ts 中的压缩流程
|
||||
function applyCompression(messages: CursorMessage[], hasTools: boolean): { compressedCount: number; charsBefore: number; charsAfter: number } {
|
||||
let compressedCount = 0;
|
||||
const charsBefore = messages.reduce((s, m) => s + m.parts.reduce((a, p) => a + (p.text?.length ?? 0), 0), 0);
|
||||
|
||||
if (hasTools && messages.length > FEWSHOT_COUNT) {
|
||||
if (charsBefore > MAX_CONTEXT_CHARS || messages.length > MAX_CURSOR_MESSAGES) {
|
||||
const keepRecentCount = Math.min(KEEP_RECENT_MESSAGES, messages.length - FEWSHOT_COUNT);
|
||||
const compressBoundary = messages.length - keepRecentCount;
|
||||
|
||||
for (let i = FEWSHOT_COUNT; i < compressBoundary; i++) {
|
||||
const original = messages[i].parts.map(p => p.text ?? '').join('');
|
||||
const compressed = compressMessage(messages[i].role, original);
|
||||
if (compressed.length < original.length) {
|
||||
messages[i] = { ...messages[i], parts: [{ type: 'text', text: compressed }] };
|
||||
compressedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const charsAfter = messages.reduce((s, m) => s + m.parts.reduce((a, p) => a + (p.text?.length ?? 0), 0), 0);
|
||||
return { compressedCount, charsBefore, charsAfter };
|
||||
}
|
||||
|
||||
// ==================== 构造测试数据 ====================
|
||||
|
||||
function buildLongConversation(turnCount: number): CursorMessage[] {
|
||||
const messages: CursorMessage[] = [];
|
||||
|
||||
// few-shot 头部 (2 条)
|
||||
messages.push({
|
||||
parts: [{ type: 'text', text: 'You are a coding assistant. Use tools with ```json action``` format...' }],
|
||||
id: 'fs1', role: 'user'
|
||||
});
|
||||
messages.push({
|
||||
parts: [{ type: 'text', text: 'Understood. I\'ll use the structured format for actions.' }],
|
||||
id: 'fs2', role: 'assistant'
|
||||
});
|
||||
|
||||
// 模拟 N 轮工具交互
|
||||
for (let i = 0; i < turnCount; i++) {
|
||||
// 用户请求或工具结果
|
||||
if (i === 0) {
|
||||
messages.push({
|
||||
parts: [{ type: 'text', text: `Please read the file src/module${i}.ts and analyze its structure.\n\nRespond with the appropriate action using the structured format.` }],
|
||||
id: `u${i}`, role: 'user'
|
||||
});
|
||||
} else {
|
||||
// 工具结果:模拟真实大小的文件内容
|
||||
const fileContent = `import { something } from './utils';\n\nexport class Module${i} {\n` +
|
||||
Array.from({ length: 50 }, (_, j) => ` public method${j}(): void { /* implementation line ${j} */ }`).join('\n') +
|
||||
`\n}\n`;
|
||||
messages.push({
|
||||
parts: [{ type: 'text', text: `Action output:\n${fileContent}\n\nBased on the output above, continue with the next appropriate action using the structured format.` }],
|
||||
id: `u${i}`, role: 'user'
|
||||
});
|
||||
}
|
||||
|
||||
// 助手的工具调用
|
||||
messages.push({
|
||||
parts: [{
|
||||
type: 'text',
|
||||
text: `Let me examine the structure of module${i}.\n\n\`\`\`json action\n{"tool": "read_file", "parameters": {"file_path": "src/module${i}.ts"}}\n\`\`\``
|
||||
}],
|
||||
id: `a${i}`, role: 'assistant'
|
||||
});
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
// ==================== 运行测试 ====================
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
function assert(name: string, condition: boolean, detail?: string) {
|
||||
if (condition) { passed++; console.log(` ✅ ${name}`); }
|
||||
else { failed++; console.log(` ❌ ${name}${detail ? ': ' + detail : ''}`); }
|
||||
}
|
||||
|
||||
console.log('\n=== 场景 1:短对话(不触发压缩)===');
|
||||
{
|
||||
const msgs = buildLongConversation(3); // 2 few-shot + 6 实际 = 8 条
|
||||
const originalCount = msgs.length;
|
||||
const { compressedCount, charsBefore, charsAfter } = applyCompression(msgs, true);
|
||||
assert('短对话不压缩', compressedCount === 0, `compressed=${compressedCount}`);
|
||||
assert('消息数不变', msgs.length === originalCount);
|
||||
assert('字符数不变', charsBefore === charsAfter);
|
||||
console.log(` 📊 ${msgs.length} 条消息, ${charsBefore} chars`);
|
||||
}
|
||||
|
||||
console.log('\n=== 场景 2:长对话(触发按条数压缩)===');
|
||||
{
|
||||
const msgs = buildLongConversation(20); // 2 + 40 = 42 条消息
|
||||
const originalCount = msgs.length;
|
||||
const { compressedCount, charsBefore, charsAfter } = applyCompression(msgs, true);
|
||||
assert('压缩触发', compressedCount > 0, `compressed=${compressedCount}`);
|
||||
assert('消息数不变(压缩不丢弃)', msgs.length === originalCount, `${msgs.length} vs ${originalCount}`);
|
||||
assert('总字符减少', charsAfter < charsBefore, `${charsAfter} < ${charsBefore}`);
|
||||
assert('压缩率 >50%', charsAfter < charsBefore * 0.5, `ratio=${(charsAfter / charsBefore * 100).toFixed(1)}%`);
|
||||
console.log(` 📊 ${msgs.length} 条, ${charsBefore} → ${charsAfter} chars (${(100 - charsAfter / charsBefore * 100).toFixed(1)}% 减少)`);
|
||||
|
||||
// 验证 few-shot 头部不被压缩
|
||||
assert('few-shot[0] 保持原文', msgs[0].parts[0].text!.includes('You are a coding assistant'));
|
||||
assert('few-shot[1] 保持原文', msgs[1].parts[0].text!.includes('Understood'));
|
||||
|
||||
// 验证最近 6 条保持完整
|
||||
const lastSix = msgs.slice(-6);
|
||||
for (const m of lastSix) {
|
||||
const text = m.parts[0].text!;
|
||||
const isOriginal = text.includes('Action output:\n') || text.includes('```json action');
|
||||
assert(`最近消息保持原文 (role=${m.role}, ${text.length} chars)`, isOriginal || text.length <= COMPRESS_CONTENT_MAX);
|
||||
}
|
||||
|
||||
// 验证中间消息被压缩
|
||||
const midMsg = msgs[4]; // 第 5 条消息(应在压缩区)
|
||||
assert('中间消息已压缩', midMsg.parts[0].text!.length < 300, `len=${midMsg.parts[0].text!.length}`);
|
||||
}
|
||||
|
||||
console.log('\n=== 场景 3:非工具模式(不压缩)===');
|
||||
{
|
||||
const msgs = buildLongConversation(20);
|
||||
const { compressedCount } = applyCompression(msgs, false);
|
||||
assert('非工具模式不压缩', compressedCount === 0);
|
||||
}
|
||||
|
||||
console.log('\n=== 场景 4:大字符数但少消息(按字符数触发)===');
|
||||
{
|
||||
const msgs: CursorMessage[] = [
|
||||
{ parts: [{ type: 'text', text: 'few-shot user' }], id: 'fs1', role: 'user' },
|
||||
{ parts: [{ type: 'text', text: 'few-shot assistant' }], id: 'fs2', role: 'assistant' },
|
||||
];
|
||||
// 10 轮,但每条都很大
|
||||
for (let i = 0; i < 10; i++) {
|
||||
msgs.push({
|
||||
parts: [{ type: 'text', text: `Action output:\n${'x'.repeat(8000)}\n\nBased on the output above, continue with the next appropriate action using the structured format.` }],
|
||||
id: `u${i}`, role: 'user'
|
||||
});
|
||||
msgs.push({
|
||||
parts: [{ type: 'text', text: `Analysis done.\n\n\`\`\`json action\n{"tool": "write_file", "parameters": {"file_path": "out${i}.ts", "content": "${'y'.repeat(3000)}"}}\n\`\`\`` }],
|
||||
id: `a${i}`, role: 'assistant'
|
||||
});
|
||||
}
|
||||
const originalChars = msgs.reduce((s, m) => s + m.parts.reduce((a, p) => a + (p.text?.length ?? 0), 0), 0);
|
||||
assert('超过字符阈值', originalChars > MAX_CONTEXT_CHARS, `${originalChars} > ${MAX_CONTEXT_CHARS}`);
|
||||
|
||||
const { compressedCount, charsBefore, charsAfter } = applyCompression(msgs, true);
|
||||
assert('按字符数触发压缩', compressedCount > 0);
|
||||
assert('字符数大幅减少', charsAfter < charsBefore * 0.5, `${charsAfter} < ${charsBefore * 0.5}`);
|
||||
console.log(` 📊 ${msgs.length} 条, ${charsBefore} → ${charsAfter} chars (${(100 - charsAfter / charsBefore * 100).toFixed(1)}% 减少)`);
|
||||
}
|
||||
|
||||
console.log(`\n=== 总结果: ${passed} 通过, ${failed} 失败 ===\n`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
@@ -17,10 +17,7 @@ const SMART_SINGLE_QUOTES = new Set([
|
||||
|
||||
function normalizeToolArguments(args) {
|
||||
if (!args || typeof args !== 'object') return args;
|
||||
if ('file_path' in args && !('path' in args)) {
|
||||
args.path = args.file_path;
|
||||
delete args.file_path;
|
||||
}
|
||||
// Removed legacy file_path to path conversion
|
||||
return args;
|
||||
}
|
||||
|
||||
@@ -69,19 +66,19 @@ function assertEqual(a, b, msg) {
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
console.log('\n📦 [1] normalizeToolArguments — 字段名映射\n');
|
||||
|
||||
test('file_path → path 映射', () => {
|
||||
test('file_path不再隐式转为path', () => {
|
||||
const args = { file_path: 'src/index.ts', content: 'hello' };
|
||||
const result = normalizeToolArguments(args);
|
||||
assertEqual(result.path, 'src/index.ts');
|
||||
assert(!('file_path' in result), 'file_path 应被删除');
|
||||
assertEqual(result.content, 'hello', 'content 不应被修改');
|
||||
assertEqual(result.file_path, 'src/index.ts', '应保留原始 file_path');
|
||||
assert(!('path' in result), '不应自动生成 path');
|
||||
assertEqual(result.content, 'hello');
|
||||
});
|
||||
|
||||
test('已有 path 字段时不覆盖', () => {
|
||||
test('同时存在时保持不变', () => {
|
||||
const args = { file_path: 'old.ts', path: 'new.ts' };
|
||||
const result = normalizeToolArguments(args);
|
||||
assertEqual(result.path, 'new.ts', '应保留原始 path');
|
||||
assert('file_path' in result, 'file_path 应保留');
|
||||
assertEqual(result.path, 'new.ts');
|
||||
assert('file_path' in result);
|
||||
});
|
||||
|
||||
test('无 file_path 时不影响', () => {
|
||||
@@ -145,17 +142,17 @@ test('代码中的智能引号修复', () => {
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
console.log('\n📦 [3] fixToolCallArguments — 综合修复\n');
|
||||
|
||||
test('Read 工具: file_path → path', () => {
|
||||
test('Read 工具: file_path 保持 file_path', () => {
|
||||
const args = { file_path: 'src/main.ts' };
|
||||
const result = fixToolCallArguments('Read', args);
|
||||
assertEqual(result.path, 'src/main.ts');
|
||||
assert(!('file_path' in result));
|
||||
assertEqual(result.file_path, 'src/main.ts');
|
||||
assert(!('path' in result));
|
||||
});
|
||||
|
||||
test('Write 工具: file_path + content 完整修复', () => {
|
||||
test('Write 工具: file_path + content 保持不被截断', () => {
|
||||
const args = { file_path: 'test.ts', content: 'console.log("hello")' };
|
||||
const result = fixToolCallArguments('Write', args);
|
||||
assertEqual(result.path, 'test.ts');
|
||||
assertEqual(result.file_path, 'test.ts');
|
||||
assertEqual(result.content, 'console.log("hello")');
|
||||
});
|
||||
|
||||
@@ -217,7 +214,7 @@ function parseToolCallsWithFix(responseText) {
|
||||
return { toolCalls, cleanText: cleanText.trim() };
|
||||
}
|
||||
|
||||
test('解析含 file_path 的工具调用 → 自动修复为 path', () => {
|
||||
test('解析含 file_path 的工具调用 → 保持为 file_path', () => {
|
||||
const text = `I'll read the file now.
|
||||
|
||||
\`\`\`json action
|
||||
@@ -231,11 +228,11 @@ test('解析含 file_path 的工具调用 → 自动修复为 path', () => {
|
||||
const { toolCalls } = parseToolCallsWithFix(text);
|
||||
assertEqual(toolCalls.length, 1);
|
||||
assertEqual(toolCalls[0].name, 'Read');
|
||||
assertEqual(toolCalls[0].arguments.path, 'src/index.ts');
|
||||
assert(!('file_path' in toolCalls[0].arguments), 'file_path 应被删除');
|
||||
assertEqual(toolCalls[0].arguments.file_path, 'src/index.ts');
|
||||
assert(!('path' in toolCalls[0].arguments), '不应生成 path');
|
||||
});
|
||||
|
||||
test('多个工具调用全部修复', () => {
|
||||
test('多个工具调用不再强转', () => {
|
||||
const text = `\`\`\`json action
|
||||
{"tool":"Read","parameters":{"file_path":"a.ts"}}
|
||||
\`\`\`
|
||||
@@ -245,8 +242,8 @@ test('多个工具调用全部修复', () => {
|
||||
\`\`\``;
|
||||
const { toolCalls } = parseToolCallsWithFix(text);
|
||||
assertEqual(toolCalls.length, 2);
|
||||
assertEqual(toolCalls[0].arguments.path, 'a.ts');
|
||||
assertEqual(toolCalls[1].arguments.path, 'b.ts');
|
||||
assertEqual(toolCalls[0].arguments.file_path, 'a.ts');
|
||||
assertEqual(toolCalls[1].arguments.file_path, 'b.ts');
|
||||
assertEqual(toolCalls[1].arguments.content, 'hello');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user