From 8f7b633041662dcda83f845e18c08ec780aa99a3 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 7 Feb 2026 04:36:17 -0800 Subject: [PATCH] fix(agents): download text file content for Claude instead of passing URL Claude API cannot fetch arbitrary URLs. Text-based attachments (txt, csv, json, md) are now downloaded via their presigned MinIO URL and embedded directly as text blocks. PDF uses Claude's native document block. Added 50KB size limit with truncation for large text files. buildMessages() is now async to support text content fetching. Co-Authored-By: Claude Opus 4.6 --- .../coordinator/coordinator-agent.service.ts | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/services/conversation-service/src/infrastructure/agents/coordinator/coordinator-agent.service.ts b/packages/services/conversation-service/src/infrastructure/agents/coordinator/coordinator-agent.service.ts index 8661803..6a37a09 100644 --- a/packages/services/conversation-service/src/infrastructure/agents/coordinator/coordinator-agent.service.ts +++ b/packages/services/conversation-service/src/infrastructure/agents/coordinator/coordinator-agent.service.ts @@ -204,8 +204,8 @@ export class CoordinatorAgentService implements OnModuleInit { return; } - // 1. Build messages from conversation history - const messages = this.buildMessages(context, userContent, attachments); + // 1. Build messages from conversation history (async: 文本附件需下载内容) + const messages = await this.buildMessages(context, userContent, attachments); // 2. Build system prompt blocks (with cache control) const systemPrompt = this.buildSystemPromptBlocks(); @@ -328,13 +328,16 @@ export class CoordinatorAgentService implements OnModuleInit { // Message Building // ============================================================ + /** 文本类文件最大嵌入大小 (超过则截断) */ + private readonly MAX_TEXT_EMBED_SIZE = 50_000; + /** * 将附件转换为 Claude API content blocks * - image → { type: 'image', source: { type: 'url', url } } * - PDF → { type: 'document', source: { type: 'url', url }, title } - * - text/csv/json/md → 嵌入为带文件名提示的 text block + * - text/csv/json/md → 下载内容嵌入为 text block(Claude 无法自行访问 URL) */ - private buildAttachmentBlocks(attachments: FileAttachment[]): any[] { + private async buildAttachmentBlocks(attachments: FileAttachment[]): Promise { const blocks: any[] = []; for (const att of attachments) { @@ -353,17 +356,39 @@ export class CoordinatorAgentService implements OnModuleInit { title: att.originalName, }); } else if (this.isTextBasedMime(att.mimeType)) { - // 文本类文件:提示模型通过 URL 获取内容 - blocks.push({ - type: 'text', - text: `[附件: ${att.originalName} (${att.mimeType})] — 文件内容可通过URL获取: ${att.downloadUrl}`, - }); + // 文本类文件:下载内容并直接嵌入 + try { + const textContent = await this.fetchTextContent(att.downloadUrl); + blocks.push({ + type: 'text', + text: `--- 附件: ${att.originalName} ---\n${textContent}`, + }); + } catch (err) { + this.logger.warn(`Failed to fetch text content for ${att.originalName}: ${err}`); + blocks.push({ + type: 'text', + text: `[附件: ${att.originalName} — 内容加载失败]`, + }); + } } } return blocks; } + /** 通过预签名 URL 获取文本文件内容 */ + private async fetchTextContent(url: string): Promise { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const text = await response.text(); + if (text.length > this.MAX_TEXT_EMBED_SIZE) { + return text.substring(0, this.MAX_TEXT_EMBED_SIZE) + '\n...[内容过长,已截断]'; + } + return text; + } + /** 判断是否为可直接阅读的文本类 MIME */ private isTextBasedMime(mimeType: string): boolean { const textMimes = [ @@ -373,18 +398,18 @@ export class CoordinatorAgentService implements OnModuleInit { return textMimes.includes(mimeType) || mimeType.startsWith('text/'); } - private buildMessages( + private async buildMessages( context: LegacyConversationContext, userContent: string, attachments?: FileAttachment[], - ): ClaudeMessage[] { + ): Promise { const messages: ClaudeMessage[] = []; // Convert previous messages if (context.previousMessages) { for (const msg of context.previousMessages) { if (msg.attachments?.length) { - const contentBlocks = this.buildAttachmentBlocks(msg.attachments); + const contentBlocks = await this.buildAttachmentBlocks(msg.attachments); contentBlocks.push({ type: 'text', text: msg.content }); messages.push({ role: msg.role, content: contentBlocks }); } else { @@ -395,7 +420,7 @@ export class CoordinatorAgentService implements OnModuleInit { // Build current user message if (attachments?.length) { - const contentBlocks = this.buildAttachmentBlocks(attachments); + const contentBlocks = await this.buildAttachmentBlocks(attachments); contentBlocks.push({ type: 'text', text: userContent }); messages.push({ role: 'user', content: contentBlocks }); } else {