UNPKG

route-claudecode

Version:

Advanced routing and transformation system for Claude Code outputs to multiple AI providers

596 lines 22.1 kB
"use strict"; /** * 统一工具调用检测器 * 对所有OpenAI兼容输入执行监测,确保滑动窗口检测覆盖所有样本 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.UnifiedToolCallDetector = exports.SlidingWindowDetector = void 0; const logging_1 = require("../logging"); /** * 滑动窗口工具调用检测器 */ class SlidingWindowDetector { windowConfig; logger; constructor(config, port) { this.windowConfig = { windowSize: 1000, overlapSize: 200, minDetectionLength: 10, ...config }; this.logger = (0, logging_1.getLogger)(port); } /** * 对文本进行滑动窗口检测 */ detectInText(text, context) { if (!text || text.length < this.windowConfig.minDetectionLength) { return { hasToolCalls: false, extractedToolCalls: [], cleanedText: [text], detectionMethod: 'text-too-short', confidence: 1.0 }; } const windows = this.createSlidingWindows(text); const allDetections = []; const processedRanges = []; for (const window of windows) { const windowDetection = this.detectInWindow(window.content, window.start, context); if (windowDetection.hasToolCalls) { // 合并检测结果,避免重复 for (const toolCall of windowDetection.extractedToolCalls) { const adjustedStart = window.start + toolCall.textRange.start; const adjustedEnd = window.start + toolCall.textRange.end; // 检查是否与已处理的范围重叠 const isOverlapping = processedRanges.some(range => (adjustedStart >= range.start && adjustedStart <= range.end) || (adjustedEnd >= range.start && adjustedEnd <= range.end)); if (!isOverlapping) { allDetections.push({ ...toolCall, textRange: { start: adjustedStart, end: adjustedEnd } }); processedRanges.push({ start: adjustedStart, end: adjustedEnd }); } } } } // 提取清理后的文本 const cleanedTextParts = this.extractCleanedText(text, processedRanges); return { hasToolCalls: allDetections.length > 0, extractedToolCalls: allDetections, cleanedText: cleanedTextParts, detectionMethod: 'sliding-window', confidence: this.calculateConfidence(allDetections, text.length) }; } /** * 创建滑动窗口 */ createSlidingWindows(text) { const windows = []; const step = this.windowConfig.windowSize - this.windowConfig.overlapSize; for (let i = 0; i < text.length; i += step) { const end = Math.min(i + this.windowConfig.windowSize, text.length); const content = text.slice(i, end); windows.push({ content, start: i, end }); if (end >= text.length) break; } return windows; } /** * 在单个窗口中检测工具调用 */ detectInWindow(windowContent, windowStart, context) { const detections = []; // 检测模式1: JSON格式的tool_use const jsonToolCalls = this.detectJSONToolCalls(windowContent, windowStart); detections.push(...jsonToolCalls); // 检测模式2: "Tool call: FunctionName({...})" 格式 const textToolCalls = this.detectTextToolCalls(windowContent, windowStart); detections.push(...textToolCalls); // 检测模式3: 直接函数调用格式 "FunctionName({...})" const directToolCalls = this.detectDirectToolCalls(windowContent, windowStart); detections.push(...directToolCalls); // 检测模式4: 特定模型的格式 const modelSpecificCalls = this.detectModelSpecificFormats(windowContent, windowStart, context); detections.push(...modelSpecificCalls); return { hasToolCalls: detections.length > 0, extractedToolCalls: detections, cleanedText: [], detectionMethod: 'window-detection', confidence: detections.length > 0 ? 0.9 : 0.0 }; } /** * 检测JSON格式的工具调用 */ detectJSONToolCalls(text, offset) { const detections = []; const patterns = [ /\{\s*"type"\s*:\s*"tool_use"\s*,[\s\S]*?\}/g, /\{\s*"id"\s*:\s*"toolu_[^"]+"\s*,[\s\S]*?\}/g, /\{\s*"name"\s*:\s*"[^"]+"\s*,\s*"input"\s*:\s*\{[\s\S]*?\}\s*\}/g ]; for (const pattern of patterns) { let match; while ((match = pattern.exec(text)) !== null) { try { const jsonStr = match[0]; const parsed = JSON.parse(jsonStr); if (this.isValidToolCall(parsed)) { detections.push({ type: 'tool_use', id: parsed.id || `toolu_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`, name: parsed.name, input: parsed.input, textRange: { start: offset + match.index, end: offset + match.index + match[0].length }, detectionMethod: 'json-pattern', originalText: jsonStr }); } } catch (error) { // JSON解析失败,继续下一个匹配 } } } return detections; } /** * 检测文本格式的工具调用 */ detectTextToolCalls(text, offset) { const detections = []; const pattern = /Tool\s+call:\s*(\w+)\s*\((\{[^}]*(?:\{[^}]*\}[^}]*)*\})\)/gi; let match; while ((match = pattern.exec(text)) !== null) { try { const functionName = match[1]; const argsStr = match[2]; // 🔧 改进JSON解析:处理控制字符 const args = this.parseToolCallJSON(argsStr, functionName); if (args !== null) { detections.push({ type: 'tool_use', id: `toolu_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`, name: functionName, input: args, textRange: { start: offset + match.index, end: offset + match.index + match[0].length }, detectionMethod: 'text-pattern', originalText: match[0] }); } } catch (error) { // 参数解析失败,继续下一个匹配 console.warn(`Tool call JSON parsing failed for ${match[1]}: ${error instanceof Error ? error.message : String(error)}`); } } return detections; } /** * 检测直接函数调用格式 */ detectDirectToolCalls(text, offset) { const detections = []; const pattern = /\b(\w+)\s*\(\s*(\{[^}]*(?:\{[^}]*\}[^}]*)*\})\s*\)/g; let match; while ((match = pattern.exec(text)) !== null) { try { const functionName = match[1]; const argsStr = match[2]; // 排除常见的非工具调用函数 if (this.isLikelyToolFunction(functionName)) { const args = JSON.parse(argsStr); detections.push({ type: 'tool_use', id: `toolu_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`, name: functionName, input: args, textRange: { start: offset + match.index, end: offset + match.index + match[0].length }, detectionMethod: 'direct-pattern', originalText: match[0] }); } } catch (error) { // 参数解析失败,继续下一个匹配 } } return detections; } /** * 检测特定模型的格式 */ detectModelSpecificFormats(text, offset, context) { const detections = []; const modelLower = context.model.toLowerCase(); // GLM/ZhiPu模型特定格式 if (modelLower.includes('glm') || modelLower.includes('zhipu')) { const glmDetections = this.detectGLMFormat(text, offset); detections.push(...glmDetections); } // Qwen模型特定格式 if (modelLower.includes('qwen')) { const qwenDetections = this.detectQwenFormat(text, offset); detections.push(...qwenDetections); } // DeepSeek模型特定格式 if (modelLower.includes('deepseek')) { const deepseekDetections = this.detectDeepSeekFormat(text, offset); detections.push(...deepseekDetections); } return detections; } /** * GLM模型格式检测 */ detectGLMFormat(text, offset) { // GLM可能使用特殊的工具调用格式 // 这里可以添加GLM特定的检测逻辑 return []; } /** * Qwen模型格式检测 */ detectQwenFormat(text, offset) { const detections = []; // Qwen可能返回 {"name": "FunctionName", "arguments": "{...}"} 格式 const pattern = /\{\s*"name"\s*:\s*"([^"]+)"\s*,\s*"arguments"\s*:\s*"([^"]+)"\s*\}/g; let match; while ((match = pattern.exec(text)) !== null) { try { const functionName = match[1]; const argsStr = match[2].replace(/\\"/g, '"'); // 处理转义引号 const args = JSON.parse(argsStr); detections.push({ type: 'tool_use', id: `toolu_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`, name: functionName, input: args, textRange: { start: offset + match.index, end: offset + match.index + match[0].length }, detectionMethod: 'qwen-pattern', originalText: match[0] }); } catch (error) { // 解析失败,继续下一个匹配 } } return detections; } /** * DeepSeek模型格式检测 */ detectDeepSeekFormat(text, offset) { // DeepSeek特定格式检测逻辑 return []; } /** * 验证是否是有效的工具调用 */ isValidToolCall(obj) { return (obj && typeof obj === 'object' && (obj.type === 'tool_use' || (obj.name && obj.input !== undefined)) && typeof obj.name === 'string' && obj.input !== undefined); } /** * 判断函数名是否可能是工具函数 */ isLikelyToolFunction(functionName) { // 排除常见的非工具调用函数 const excludePatterns = [ /^(console|log|print|debug|info|warn|error)$/i, /^(get|set|is|has|can|should|will)$/i, /^(if|for|while|switch|case|try|catch|finally)$/i, /^(function|class|const|let|var|return)$/i ]; return !excludePatterns.some(pattern => pattern.test(functionName)); } /** * 提取清理后的文本 */ extractCleanedText(originalText, processedRanges) { if (processedRanges.length === 0) { return [originalText]; } const textParts = []; let lastEnd = 0; // 按起始位置排序 processedRanges.sort((a, b) => a.start - b.start); for (const range of processedRanges) { // 添加工具调用之前的文本 if (range.start > lastEnd) { const beforeText = originalText.slice(lastEnd, range.start).trim(); if (beforeText) { textParts.push(beforeText); } } lastEnd = Math.max(lastEnd, range.end); } // 添加最后剩余的文本 if (lastEnd < originalText.length) { const remainingText = originalText.slice(lastEnd).trim(); if (remainingText) { textParts.push(remainingText); } } return textParts.length > 0 ? textParts : ['']; } /** * 计算检测置信度 */ calculateConfidence(detections, textLength) { if (detections.length === 0) return 0.0; let totalConfidence = 0; for (const detection of detections) { switch (detection.detectionMethod) { case 'json-pattern': totalConfidence += 0.95; break; case 'text-pattern': totalConfidence += 0.90; break; case 'direct-pattern': totalConfidence += 0.75; break; case 'qwen-pattern': totalConfidence += 0.85; break; default: totalConfidence += 0.70; } } return Math.min(totalConfidence / detections.length, 1.0); } /** * 🔧 改进的工具调用JSON解析 * 处理大文本中的控制字符和转义问题 */ parseToolCallJSON(jsonStr, functionName) { try { // 方法1: 直接解析 return JSON.parse(jsonStr); } catch (error1) { try { // 方法2: 清理控制字符后解析 const cleanedJson = this.cleanJSONString(jsonStr); return JSON.parse(cleanedJson); } catch (error2) { // 记录详细错误信息 this.logger.warn(`JSON parsing failed for ${functionName}:`, { originalError: error1.message, cleanedError: error2.message, jsonPreview: jsonStr.substring(0, 100) + '...' }); } } return null; } /** * 清理JSON字符串中的控制字符 */ cleanJSONString(jsonStr) { return jsonStr // 处理换行符 - 这是主要问题 .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t') // 处理其他控制字符 .replace(/[\x00-\x1F\x7F]/g, (match) => { const code = match.charCodeAt(0); return `\\u${code.toString(16).padStart(4, '0')}`; }); } } exports.SlidingWindowDetector = SlidingWindowDetector; /** * 统一工具调用检测器 */ class UnifiedToolCallDetector { slidingWindowDetector; logger; constructor(port) { this.slidingWindowDetector = new SlidingWindowDetector({ windowSize: 1000, overlapSize: 200, minDetectionLength: 10 }, port); this.logger = (0, logging_1.getLogger)(port); } /** * 检测并处理工具调用 * 对所有OpenAI兼容输入都执行监测,避免准入条件太严格 */ async detectAndProcess(data, context) { if (!data || typeof data !== 'object') { return data; } const startTime = Date.now(); try { // 🎯 宽松准入策略:对所有有内容的响应都进行检测 if (this.hasTextContent(data)) { const processedData = await this.processTextContent(data, context); const duration = Date.now() - startTime; this.logger.debug('Tool call detection completed', { duration: `${duration}ms`, hasChanges: processedData !== data, originalBlocks: this.countContentBlocks(data), processedBlocks: this.countContentBlocks(processedData) }, context.requestId, 'tool-call-detection'); return processedData; } return data; } catch (error) { const duration = Date.now() - startTime; this.logger.error('Tool call detection failed', { error: error instanceof Error ? error.message : String(error), duration: `${duration}ms` }, context.requestId, 'tool-call-detection'); return data; } } /** * 检查是否有文本内容 */ hasTextContent(data) { if (!data.content || !Array.isArray(data.content)) { return false; } return data.content.some((block) => block && block.type === 'text' && block.text && typeof block.text === 'string'); } /** * 处理文本内容 */ async processTextContent(data, context) { if (!data.content || !Array.isArray(data.content)) { return data; } const processedContent = []; let hasChanges = false; for (const block of data.content) { if (block.type === 'text' && block.text) { // 对每个文本块进行滑动窗口检测 const detection = this.slidingWindowDetector.detectInText(block.text, context); if (detection.hasToolCalls) { hasChanges = true; // 添加清理后的文本块(如果有) for (const cleanText of detection.cleanedText) { if (cleanText.trim()) { processedContent.push({ type: 'text', text: cleanText }); } } // 添加提取的工具调用 for (const toolCall of detection.extractedToolCalls) { processedContent.push({ type: 'tool_use', id: toolCall.id, name: toolCall.name, input: toolCall.input }); } this.logger.debug('Extracted tool calls from text', { originalTextLength: block.text.length, toolCallsExtracted: detection.extractedToolCalls.length, detectionMethod: detection.detectionMethod, confidence: detection.confidence }, context.requestId, 'tool-call-extraction'); } else { // 没有工具调用,保持原样 processedContent.push(block); } } else { // 非文本块,保持原样 processedContent.push(block); } } if (hasChanges) { return { ...data, content: processedContent }; } return data; } /** * 计算内容块数量 */ countContentBlocks(data) { if (!data?.content || !Array.isArray(data.content)) { return 0; } return data.content.length; } /** * 🔧 改进的工具调用JSON解析 * 处理大文本中的控制字符和转义问题 */ parseToolCallJSON(jsonStr, functionName) { try { // 方法1: 直接解析 return JSON.parse(jsonStr); } catch (error1) { try { // 方法2: 清理控制字符后解析 const cleanedJson = this.cleanJSONString(jsonStr); return JSON.parse(cleanedJson); } catch (error2) { // 记录详细错误信息 console.warn(`JSON parsing failed for ${functionName}:`, { originalError: error1.message, cleanedError: error2.message, jsonPreview: jsonStr.substring(0, 100) + '...' }); } } return null; } /** * 清理JSON字符串中的控制字符 */ cleanJSONString(jsonStr) { return jsonStr // 处理换行符 - 这是主要问题 .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t') // 处理其他控制字符 .replace(/[\x00-\x1F\x7F]/g, (match) => { const code = match.charCodeAt(0); return `\\u${code.toString(16).padStart(4, '0')}`; }); } /** * 获取检测统计信息 */ getStats() { return { windowConfig: this.slidingWindowDetector['windowConfig'], detectionMethods: [ 'json-pattern', 'text-pattern', 'direct-pattern', 'qwen-pattern', 'glm-pattern', 'deepseek-pattern' ] }; } } exports.UnifiedToolCallDetector = UnifiedToolCallDetector; //# sourceMappingURL=unified-tool-call-detector.js.map