Spaces:
Sleeping
Sleeping
File size: 4,268 Bytes
c6dedd5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | /**
* tool-fixer.ts - 工具参数修复
*
* 移植自 claude-api-2-cursor 的 tool_use_fixer.py
* 修复 AI 模型输出的工具调用参数中常见的格式问题:
* 1. 字段名映射 (file_path → path)
* 2. 智能引号替换为普通引号
* 3. StrReplace/search_replace 工具的精确匹配修复
*/
import { readFileSync, existsSync } from 'fs';
const SMART_DOUBLE_QUOTES = new Set([
'\u00ab', '\u201c', '\u201d', '\u275e',
'\u201f', '\u201e', '\u275d', '\u00bb',
]);
const SMART_SINGLE_QUOTES = new Set([
'\u2018', '\u2019', '\u201a', '\u201b',
]);
/**
* 字段名映射:将常见的错误字段名修正为标准字段名
*/
export function normalizeToolArguments(args: Record<string, unknown>): Record<string, unknown> {
if (!args || typeof args !== 'object') return args;
// 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;
}
/**
* 将智能引号(中文引号等)替换为普通 ASCII 引号
*/
export function replaceSmartQuotes(text: string): string {
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 buildFuzzyPattern(text: string): string {
const parts: string[] = [];
for (const ch of text) {
if (SMART_DOUBLE_QUOTES.has(ch) || ch === '"') {
parts.push('["\u00ab\u201c\u201d\u275e\u201f\u201e\u275d\u00bb]');
} else if (SMART_SINGLE_QUOTES.has(ch) || ch === "'") {
parts.push("['\u2018\u2019\u201a\u201b]");
} else if (ch === ' ' || ch === '\t') {
parts.push('\\s+');
} else if (ch === '\\') {
parts.push('\\\\{1,2}');
} else {
parts.push(escapeRegExp(ch));
}
}
return parts.join('');
}
function escapeRegExp(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* 修复 StrReplace / search_replace 工具的 old_string 精确匹配问题
*
* 当 AI 输出的 old_string 包含智能引号或微小格式差异时,
* 尝试在实际文件中进行容错匹配,找到唯一匹配后替换为精确文本
*/
export function repairExactMatchToolArguments(
toolName: string,
args: Record<string, unknown>,
): Record<string, unknown> {
if (!args || typeof args !== 'object') return args;
const lowerName = (toolName || '').toLowerCase();
if (!lowerName.includes('str_replace') && !lowerName.includes('search_replace') && !lowerName.includes('strreplace')) {
return args;
}
const oldString = (args.old_string ?? args.old_str) as string | undefined;
if (!oldString) return args;
const filePath = (args.path ?? args.file_path) as string | undefined;
if (!filePath) return args;
try {
if (!existsSync(filePath)) return args;
const content = readFileSync(filePath, 'utf-8');
if (content.includes(oldString)) return args;
const pattern = buildFuzzyPattern(oldString);
const regex = new RegExp(pattern, 'g');
const matches = [...content.matchAll(regex)];
if (matches.length !== 1) return args;
const matchedText = matches[0][0];
if ('old_string' in args) args.old_string = matchedText;
else if ('old_str' in args) args.old_str = matchedText;
const newString = (args.new_string ?? args.new_str) as string | undefined;
if (newString) {
const fixed = replaceSmartQuotes(newString);
if ('new_string' in args) args.new_string = fixed;
else if ('new_str' in args) args.new_str = fixed;
}
} catch {
// best-effort: 文件读取失败不阻塞请求
}
return args;
}
/**
* 对解析出的工具调用应用全部修复
*/
export function fixToolCallArguments(
toolName: string,
args: Record<string, unknown>,
): Record<string, unknown> {
args = normalizeToolArguments(args);
args = repairExactMatchToolArguments(toolName, args);
return args;
}
|