| |
| |
| |
| |
| |
| |
|
|
| |
|
|
| const SMART_DOUBLE_QUOTES = new Set([ |
| '\u00ab', '\u201c', '\u201d', '\u275e', |
| '\u201f', '\u201e', '\u275d', '\u00bb', |
| ]); |
| const SMART_SINGLE_QUOTES = new Set([ |
| '\u2018', '\u2019', '\u201a', '\u201b', |
| ]); |
|
|
| function normalizeToolArguments(args) { |
| if (!args || typeof args !== 'object') return args; |
| |
| return args; |
| } |
|
|
| function replaceSmartQuotes(text) { |
| const chars = [...text]; |
| return chars.map(ch => { |
| if (SMART_DOUBLE_QUOTES.has(ch)) return '"'; |
| if (SMART_SINGLE_QUOTES.has(ch)) return "'"; |
| return ch; |
| }).join(''); |
| } |
|
|
| function fixToolCallArguments(toolName, args) { |
| args = normalizeToolArguments(args); |
| |
| return args; |
| } |
|
|
| |
| let passed = 0; |
| let failed = 0; |
|
|
| function test(name, fn) { |
| try { |
| fn(); |
| console.log(` โ
${name}`); |
| passed++; |
| } catch (e) { |
| console.error(` โ ${name}`); |
| console.error(` ${e.message}`); |
| failed++; |
| } |
| } |
|
|
| function assert(condition, msg) { |
| if (!condition) throw new Error(msg || 'Assertion failed'); |
| } |
|
|
| function assertEqual(a, b, msg) { |
| const as = JSON.stringify(a), bs = JSON.stringify(b); |
| if (as !== bs) throw new Error(msg || `Expected ${bs}, got ${as}`); |
| } |
|
|
| |
| |
| |
| console.log('\n๐ฆ [1] normalizeToolArguments โ ๅญๆฎตๅๆ ๅฐ\n'); |
|
|
| test('file_pathไธๅ้ๅผ่ฝฌไธบpath', () => { |
| const args = { file_path: 'src/index.ts', content: 'hello' }; |
| const result = normalizeToolArguments(args); |
| assertEqual(result.file_path, 'src/index.ts', 'ๅบไฟ็ๅๅง file_path'); |
| assert(!('path' in result), 'ไธๅบ่ชๅจ็ๆ path'); |
| assertEqual(result.content, 'hello'); |
| }); |
|
|
| test('ๅๆถๅญๅจๆถไฟๆไธๅ', () => { |
| const args = { file_path: 'old.ts', path: 'new.ts' }; |
| const result = normalizeToolArguments(args); |
| assertEqual(result.path, 'new.ts'); |
| assert('file_path' in result); |
| }); |
|
|
| test('ๆ file_path ๆถไธๅฝฑๅ', () => { |
| const args = { path: 'foo.ts', content: 'bar' }; |
| const result = normalizeToolArguments(args); |
| assertEqual(result.path, 'foo.ts'); |
| assertEqual(result.content, 'bar'); |
| }); |
|
|
| test('null/undefined ่พๅ
ฅๅฎๅ
จ', () => { |
| assertEqual(normalizeToolArguments(null), null); |
| assertEqual(normalizeToolArguments(undefined), undefined); |
| }); |
|
|
| test('็ฉบๅฏน่ฑก', () => { |
| const result = normalizeToolArguments({}); |
| assertEqual(result, {}); |
| }); |
|
|
| |
| |
| |
| console.log('\n๐ฆ [2] replaceSmartQuotes โ ๆบ่ฝๅผๅทๆฟๆข\n'); |
|
|
| test('ไธญๆๅๅผๅท โ ๆฎ้ๅๅผๅท', () => { |
| const input = '\u201cไฝ ๅฅฝ\u201d'; |
| assertEqual(replaceSmartQuotes(input), '"ไฝ ๅฅฝ"'); |
| }); |
|
|
| test('ไธญๆๅๅผๅท โ ๆฎ้ๅๅผๅท', () => { |
| const input = '\u2018hello\u2019'; |
| assertEqual(replaceSmartQuotes(input), "'hello'"); |
| }); |
|
|
| test('ๆททๅๅผๅทๆฟๆข', () => { |
| const input = '\u201cHello\u201d and \u2018World\u2019'; |
| assertEqual(replaceSmartQuotes(input), '"Hello" and \'World\''); |
| }); |
|
|
| test('ๆ ๆบ่ฝๅผๅทๆถๅๆ ท่ฟๅ', () => { |
| const input = '"normal" and \'single\''; |
| assertEqual(replaceSmartQuotes(input), input); |
| }); |
|
|
| test('็ฉบๅญ็ฌฆไธฒ', () => { |
| assertEqual(replaceSmartQuotes(''), ''); |
| }); |
|
|
| test('ๆณๆๅผๅท ยซ ยป', () => { |
| const input = '\u00abBonjour\u00bb'; |
| assertEqual(replaceSmartQuotes(input), '"Bonjour"'); |
| }); |
|
|
| test('ไปฃ็ ไธญ็ๆบ่ฝๅผๅทไฟฎๅค', () => { |
| const input = 'const name = \u201cClaude\u201d;'; |
| assertEqual(replaceSmartQuotes(input), 'const name = "Claude";'); |
| }); |
|
|
| |
| |
| |
| console.log('\n๐ฆ [3] fixToolCallArguments โ ็ปผๅไฟฎๅค\n'); |
|
|
| test('Read ๅทฅๅ
ท: file_path ไฟๆ file_path', () => { |
| const args = { file_path: 'src/main.ts' }; |
| const result = fixToolCallArguments('Read', args); |
| assertEqual(result.file_path, 'src/main.ts'); |
| assert(!('path' in result)); |
| }); |
|
|
| test('Write ๅทฅๅ
ท: file_path + content ไฟๆไธ่ขซๆชๆญ', () => { |
| const args = { file_path: 'test.ts', content: 'console.log("hello")' }; |
| const result = fixToolCallArguments('Write', args); |
| assertEqual(result.file_path, 'test.ts'); |
| assertEqual(result.content, 'console.log("hello")'); |
| }); |
|
|
| test('Bash ๅทฅๅ
ท: ๆ ๆ ๅฐ้่ฆ', () => { |
| const args = { command: 'ls -la' }; |
| const result = fixToolCallArguments('Bash', args); |
| assertEqual(result.command, 'ls -la'); |
| }); |
|
|
| test('้ๅฏน่ฑกๅๆฐๅฎๅ
จๅค็', () => { |
| assertEqual(fixToolCallArguments('Read', null), null); |
| assertEqual(fixToolCallArguments('Read', undefined), undefined); |
| }); |
|
|
| |
| |
| |
| console.log('\n๐ฆ [4] parseToolCalls + fixToolCallArguments ้ๆ\n'); |
|
|
| function tolerantParse(jsonStr) { |
| try { return JSON.parse(jsonStr); } catch { } |
| let inString = false, escaped = false, fixed = ''; |
| const bracketStack = []; |
| 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 { } } |
| throw _e2; |
| } |
| } |
|
|
| function parseToolCallsWithFix(responseText) { |
| const toolCalls = []; |
| let cleanText = responseText; |
| const fullBlockRegex = /```json(?:\s+action)?\s*([\s\S]*?)\s*```/g; |
| let match; |
| while ((match = fullBlockRegex.exec(responseText)) !== null) { |
| let isToolCall = false; |
| try { |
| const parsed = tolerantParse(match[1]); |
| if (parsed.tool || parsed.name) { |
| const name = parsed.tool || parsed.name; |
| let args = parsed.parameters || parsed.arguments || parsed.input || {}; |
| args = fixToolCallArguments(name, args); |
| toolCalls.push({ name, arguments: args }); |
| isToolCall = true; |
| } |
| } catch (e) { } |
| if (isToolCall) cleanText = cleanText.replace(match[0], ''); |
| } |
| return { toolCalls, cleanText: cleanText.trim() }; |
| } |
|
|
| test('่งฃๆๅซ file_path ็ๅทฅๅ
ท่ฐ็จ โ ไฟๆไธบ file_path', () => { |
| const text = `I'll read the file now. |
| |
| \`\`\`json action |
| { |
| "tool": "Read", |
| "parameters": { |
| "file_path": "src/index.ts" |
| } |
| } |
| \`\`\``; |
| const { toolCalls } = parseToolCallsWithFix(text); |
| assertEqual(toolCalls.length, 1); |
| assertEqual(toolCalls[0].name, 'Read'); |
| assertEqual(toolCalls[0].arguments.file_path, 'src/index.ts'); |
| assert(!('path' in toolCalls[0].arguments), 'ไธๅบ็ๆ path'); |
| }); |
|
|
| test('ๅคไธชๅทฅๅ
ท่ฐ็จไธๅๅผบ่ฝฌ', () => { |
| const text = `\`\`\`json action |
| {"tool":"Read","parameters":{"file_path":"a.ts"}} |
| \`\`\` |
| |
| \`\`\`json action |
| {"tool":"Write","parameters":{"file_path":"b.ts","content":"hello"}} |
| \`\`\``; |
| const { toolCalls } = parseToolCallsWithFix(text); |
| assertEqual(toolCalls.length, 2); |
| assertEqual(toolCalls[0].arguments.file_path, 'a.ts'); |
| assertEqual(toolCalls[1].arguments.file_path, 'b.ts'); |
| assertEqual(toolCalls[1].arguments.content, 'hello'); |
| }); |
|
|
| test('ๆ ้ไฟฎๅค็ๅทฅๅ
ท่ฐ็จไฟๆไธๅ', () => { |
| const text = `\`\`\`json action |
| {"tool":"Bash","parameters":{"command":"npm run build"}} |
| \`\`\``; |
| const { toolCalls } = parseToolCallsWithFix(text); |
| assertEqual(toolCalls.length, 1); |
| assertEqual(toolCalls[0].arguments.command, 'npm run build'); |
| }); |
|
|
| |
| |
| |
| console.log('\n' + 'โ'.repeat(55)); |
| console.log(` ็ปๆ: ${passed} ้่ฟ / ${failed} ๅคฑ่ดฅ / ${passed + failed} ๆป่ฎก`); |
| console.log('โ'.repeat(55) + '\n'); |
|
|
| if (failed > 0) process.exit(1); |
|
|