From ee9d08a7a45a1e6962148a1475b7c52438f45864 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sat, 9 May 2026 01:27:04 +0300 Subject: [PATCH] feat(attachments): route lead image delivery --- .../services/team/TeamProvisioningService.ts | 112 +++++++++--------- 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 6ebdfed5..0e0c441f 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -1,3 +1,8 @@ +import { + buildClaudeAttachmentDeliveryParts, + buildCodexNativeAttachmentDeliveryParts, + type CodexNativeImageArgPart, +} from '@features/agent-attachments/main'; import { resolveAnthropicFastMode, resolveAnthropicRuntimeSelection, @@ -65,7 +70,7 @@ import { stripCrossTeamPrefix, } from '@shared/constants/crossTeam'; import { getMemberColorByName } from '@shared/constants/memberColors'; -import { DEFAULT_TOOL_APPROVAL_SETTINGS } from '@shared/types/team'; +import { DEFAULT_TOOL_APPROVAL_SETTINGS, type AttachmentPayload } from '@shared/types/team'; import { resolveLanguageName } from '@shared/utils/agentLanguage'; import { resolveAnthropicLaunchModel } from '@shared/utils/anthropicLaunchModel'; import { getAnthropicDefaultTeamModel } from '@shared/utils/anthropicModelDefaults'; @@ -19615,58 +19620,15 @@ export class TeamProvisioningService { throw new Error(`Team "${run.teamName}" process stdin is not writable`); } - const contentBlocks: Record[] = [{ type: 'text', text: message }]; - if (attachments?.length) { - for (const att of attachments) { - if (att.mimeType === 'application/pdf') { - // PDF → document block with base64 source - contentBlocks.push({ - type: 'document', - source: { - type: 'base64', - media_type: 'application/pdf', - data: att.data, - }, - title: att.filename, - }); - } else if (att.mimeType === 'text/plain') { - // Text file → document block with text source (decode base64 → UTF-8) - const decoded = Buffer.from(att.data, 'base64').toString('utf-8'); - if (decoded.includes('\uFFFD')) { - // Non-UTF-8 file: fallback to base64 document to avoid garbled content - contentBlocks.push({ - type: 'document', - source: { - type: 'base64', - media_type: 'text/plain', - data: att.data, - }, - title: att.filename, - }); - } else { - contentBlocks.push({ - type: 'document', - source: { - type: 'text', - media_type: 'text/plain', - data: decoded, - }, - title: att.filename, - }); - } - } else { - // Image (default) → image block - contentBlocks.push({ - type: 'image', - source: { - type: 'base64', - media_type: att.mimeType, - data: att.data, - }, - }); - } - } - } + const attachmentPayloads = this.toLeadAttachmentPayloads(attachments); + const contentBlocks = + normalizeOptionalTeamProviderId(run.request.providerId) === 'codex' && + attachmentPayloads.length > 0 + ? await this.buildCodexLeadAttachmentContentBlocks(run, message, attachmentPayloads) + : (buildClaudeAttachmentDeliveryParts({ + text: message, + attachments: attachmentPayloads, + }).blocks as Record[]); const payload = JSON.stringify({ type: 'user', @@ -19685,6 +19647,50 @@ export class TeamProvisioningService { this.setLeadActivity(run, 'active'); } + private toLeadAttachmentPayloads( + attachments?: { data: string; mimeType: string; filename?: string }[] + ): AttachmentPayload[] { + return (attachments ?? []).map((attachment, index) => { + const filename = attachment.filename?.trim() || `attachment-${index + 1}`; + const bytes = Buffer.from(attachment.data, 'base64'); + return { + id: `lead_att_${index + 1}`, + filename, + mimeType: attachment.mimeType, + size: bytes.byteLength, + data: attachment.data, + }; + }); + } + + private async buildCodexLeadAttachmentContentBlocks( + run: ProvisioningRun, + message: string, + attachments: AttachmentPayload[] + ): Promise[]> { + const prepared = await buildCodexNativeAttachmentDeliveryParts({ + teamName: run.teamName, + messageId: `lead_${run.runId}_${Date.now()}`, + text: message, + attachments, + }); + return [ + { type: 'text', text: prepared.promptText }, + ...prepared.imageParts.map((part) => this.codexImagePartToContentBlock(part)), + ]; + } + + private codexImagePartToContentBlock(part: CodexNativeImageArgPart): Record { + return { + type: 'image', + source: { + type: 'file', + path: part.path, + media_type: part.mimeType, + }, + }; + } + /** * UNUSED (2026-03-23): teammates read their own inbox files directly via fs.watch, * so forwarding through the lead is unnecessary. Kept for reference — the prompt