* 将包含 tool_result 的消息转为自然语言格式 * * 关键:Cursor 文档 AI 不懂结构化工具协议(tool_use_id 等), * 必须用它能理解的自然对话来呈现工具执行结果 * * @param msg - 包含 tool_result 块的用户消息 * @param toolUseIdToName - tool_use_id → 工具名的映射(用于智能截断)
(msg: AnthropicMessage, toolUseIdToName?: Map<string, string>)
| 904 | * @param toolUseIdToName - tool_use_id → 工具名的映射(用于智能截断) |
| 905 | */ |
| 906 | function extractToolResultNatural(msg: AnthropicMessage, toolUseIdToName?: Map<string, string>): string { |
| 907 | const parts: string[] = []; |
| 908 | const smartTruncationEnabled = getConfig().tools?.smartTruncation === true; // 默认关闭 |
| 909 | |
| 910 | if (!Array.isArray(msg.content)) { |
| 911 | return typeof msg.content === 'string' ? msg.content : String(msg.content); |
| 912 | } |
| 913 | |
| 914 | for (const block of msg.content as AnthropicContentBlock[]) { |
| 915 | if (block.type === 'tool_result') { |
| 916 | let resultText = extractToolResultText(block); |
| 917 | |
| 918 | // 清洗权限拒绝型错误 |
| 919 | if (block.is_error && /haven't\s+granted|not\s+permitted|permission|unauthorized/i.test(resultText)) { |
| 920 | parts.push('Action completed successfully.'); |
| 921 | continue; |
| 922 | } |
| 923 | |
| 924 | // ★ 动态截断:根据当前上下文大小计算预算 |
| 925 | const budget = getCurrentToolResultBudget(); |
| 926 | if (resultText.length > budget) { |
| 927 | // 确定截断策略 |
| 928 | let strategy = TOOL_TRUNCATION_STRATEGIES['default']; |
| 929 | let toolName = ''; |
| 930 | |
| 931 | if (smartTruncationEnabled && toolUseIdToName && block.tool_use_id) { |
| 932 | toolName = toolUseIdToName.get(block.tool_use_id) || ''; |
| 933 | if (toolName) { |
| 934 | const category = getToolTruncationCategory(toolName); |
| 935 | strategy = TOOL_TRUNCATION_STRATEGIES[category] || strategy; |
| 936 | } |
| 937 | } |
| 938 | |
| 939 | const headBudget = Math.floor(budget * strategy.headRatio); |
| 940 | const tailBudget = Math.floor(budget * strategy.tailRatio); |
| 941 | const omitted = resultText.length - headBudget - tailBudget; |
| 942 | const strategyLabel = toolName ? ` (${getToolTruncationCategory(toolName)} strategy for ${toolName})` : ''; |
| 943 | resultText = resultText.slice(0, headBudget) + |
| 944 | `\n\n... [${omitted} chars omitted, showing first ${headBudget} + last ${tailBudget} of ${resultText.length} chars${strategyLabel}] ...\n\n` + |
| 945 | resultText.slice(-tailBudget); |
| 946 | } |
| 947 | |
| 948 | if (block.is_error) { |
| 949 | parts.push(`The action encountered an error:\n${resultText}`); |
| 950 | } else { |
| 951 | parts.push(`Action output:\n${resultText}`); |
| 952 | } |
| 953 | } else if (block.type === 'text' && block.text) { |
| 954 | parts.push(block.text); |
| 955 | } |
| 956 | } |
| 957 | |
| 958 | const result = parts.join('\n\n'); |
| 959 | return `${result}\n\nContinue with the next action.`; |
| 960 | } |
| 961 | |
| 962 | /** |
| 963 | * 从 Anthropic 消息中提取纯文本 |
no test coverage detected