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 {