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 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-07 04:36:17 -08:00
parent 5076e16cc7
commit 8f7b633041
1 changed files with 38 additions and 13 deletions

View File

@ -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 blockClaude 访 URL
*/
private buildAttachmentBlocks(attachments: FileAttachment[]): any[] {
private async buildAttachmentBlocks(attachments: FileAttachment[]): Promise<any[]> {
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<string> {
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<ClaudeMessage[]> {
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 {