diff --git a/packages/channels/base/src/ChannelBase.ts b/packages/channels/base/src/ChannelBase.ts index db958f52e..e10e805ce 100644 --- a/packages/channels/base/src/ChannelBase.ts +++ b/packages/channels/base/src/ChannelBase.ts @@ -238,6 +238,28 @@ export abstract class ChannelBase { promptText = `[Replying to: "${envelope.referencedText}"]\n\n${promptText}`; } + // Resolve attachments: extract image for bridge, append file paths to text + let imageBase64 = envelope.imageBase64; + let imageMimeType = envelope.imageMimeType; + if (envelope.attachments?.length) { + const filePaths: string[] = []; + for (const att of envelope.attachments) { + if (att.type === 'image' && att.data && !imageBase64) { + imageBase64 = att.data; + imageMimeType = att.mimeType; + } else if (att.filePath) { + const label = att.type === 'file' ? 'file' : att.type; + const name = att.fileName ? ` "${att.fileName}"` : ''; + filePaths.push( + `User sent a ${label}${name}. It has been saved to: ${att.filePath}`, + ); + } + } + if (filePaths.length > 0) { + promptText = promptText + '\n\n' + filePaths.join('\n'); + } + } + // Prepend channel instructions on first message of a session if (this.config.instructions && !this.instructedSessions.has(sessionId)) { promptText = `${this.config.instructions}\n\n${promptText}`; @@ -269,8 +291,8 @@ export abstract class ChannelBase { try { const response = await this.bridge.prompt(sessionId, promptText, { - imageBase64: envelope.imageBase64, - imageMimeType: envelope.imageMimeType, + imageBase64, + imageMimeType, }); if (response) { diff --git a/packages/channels/base/src/index.ts b/packages/channels/base/src/index.ts index 111ba2d51..532216478 100644 --- a/packages/channels/base/src/index.ts +++ b/packages/channels/base/src/index.ts @@ -16,6 +16,7 @@ export { SenderGate } from './SenderGate.js'; export type { SenderCheckResult } from './SenderGate.js'; export { SessionRouter } from './SessionRouter.js'; export type { + Attachment, BlockStreamingChunkConfig, BlockStreamingCoalesceConfig, ChannelConfig, diff --git a/packages/channels/base/src/types.ts b/packages/channels/base/src/types.ts index d2424e05a..0a1f91636 100644 --- a/packages/channels/base/src/types.ts +++ b/packages/channels/base/src/types.ts @@ -45,6 +45,19 @@ export interface ChannelConfig { blockStreamingCoalesce?: BlockStreamingCoalesceConfig; } +export interface Attachment { + /** Content category. */ + type: 'image' | 'file' | 'audio' | 'video'; + /** Base64-encoded data (for images or small files). */ + data?: string; + /** Absolute path to a local file (for large files saved to disk). */ + filePath?: string; + /** MIME type (e.g. "image/jpeg", "application/pdf"). */ + mimeType: string; + /** Original file name from the platform. */ + fileName?: string; +} + export interface Envelope { channelName: string; senderId: string; @@ -63,6 +76,8 @@ export interface Envelope { imageBase64?: string; /** MIME type for the image (e.g. "image/jpeg", "image/png"). */ imageMimeType?: string; + /** Structured attachments (images, files, audio, video). */ + attachments?: Attachment[]; } export interface SessionTarget { diff --git a/packages/channels/dingtalk/src/DingtalkAdapter.ts b/packages/channels/dingtalk/src/DingtalkAdapter.ts index 4aa468c38..45e40c219 100644 --- a/packages/channels/dingtalk/src/DingtalkAdapter.ts +++ b/packages/channels/dingtalk/src/DingtalkAdapter.ts @@ -415,10 +415,17 @@ export class DingtalkChannel extends ChannelBase { if (!media) return; if (mediaType === 'image') { - envelope.imageBase64 = media.buffer.toString('base64'); - envelope.imageMimeType = media.mimeType.startsWith('image/') + const mimeType = media.mimeType.startsWith('image/') ? media.mimeType : 'image/jpeg'; + envelope.attachments = [ + ...(envelope.attachments || []), + { + type: 'image', + data: media.buffer.toString('base64'), + mimeType, + }, + ]; } else { // Save non-image files to temp dir so the agent can read them const dir = join(tmpdir(), 'channel-files'); @@ -427,15 +434,24 @@ export class DingtalkChannel extends ChannelBase { const filePath = join(dir, safeName); writeFileSync(filePath, media.buffer); - const prefix = - envelope.text && - envelope.text !== `(file: ${fileName || 'file'})` && - envelope.text !== '(audio)' && - envelope.text !== '(video)' - ? envelope.text + '\n\n' - : ''; - envelope.text = - prefix + `User sent a ${mediaType}. It has been saved to: ${filePath}`; + // Clean up placeholder text like "(audio)", "(video)", "(file: name)" + if ( + envelope.text === `(file: ${fileName || 'file'})` || + envelope.text === '(audio)' || + envelope.text === '(video)' + ) { + envelope.text = ''; + } + + envelope.attachments = [ + ...(envelope.attachments || []), + { + type: mediaType, + filePath, + mimeType: media.mimeType, + fileName: safeName, + }, + ]; } } diff --git a/packages/channels/telegram/src/TelegramAdapter.ts b/packages/channels/telegram/src/TelegramAdapter.ts index 3445f3986..3912455a5 100644 --- a/packages/channels/telegram/src/TelegramAdapter.ts +++ b/packages/channels/telegram/src/TelegramAdapter.ts @@ -123,9 +123,15 @@ export class TelegramChannel extends ChannelBase { const filePath = join(dir, fileName); writeFileSync(filePath, buf); - envelope.text = - (msg.caption ? msg.caption + '\n\n' : '') + - `User sent a file. It has been saved to: ${filePath}`; + envelope.text = msg.caption || ''; + envelope.attachments = [ + { + type: 'file', + filePath, + mimeType: doc.mime_type || 'application/octet-stream', + fileName, + }, + ]; } catch (err) { process.stderr.write( `[Telegram:${this.name}] Failed to download document: ${err instanceof Error ? err.message : err}\n`, diff --git a/packages/channels/weixin/src/WeixinAdapter.ts b/packages/channels/weixin/src/WeixinAdapter.ts index 3944916bc..cad61e64d 100644 --- a/packages/channels/weixin/src/WeixinAdapter.ts +++ b/packages/channels/weixin/src/WeixinAdapter.ts @@ -135,7 +135,14 @@ export class WeixinChannel extends ChannelBase { if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); const filePath = join(dir, file.fileName); writeFileSync(filePath, fileData); - envelope.text = `User sent a file. It has been saved to: ${filePath}`; + envelope.attachments = [ + { + type: 'file', + filePath, + mimeType: 'application/octet-stream', + fileName: file.fileName, + }, + ]; } catch (err) { process.stderr.write( `[Weixin:${this.name}] Failed to download file: ${err instanceof Error ? err.message : err}\n`,