1+ import { randomUUID } from "node:crypto" ;
2+ import fs from "node:fs/promises" ;
3+ import path from "node:path" ;
14// Message-action runner normalizes tool params, resolves channel/target/media,
25// applies policies, and dispatches send/poll/plugin actions.
36import {
@@ -33,8 +36,11 @@ import {
3336 normalizeMessagePresentation ,
3437 type ReplyPayloadDelivery ,
3538} from "../../interactive/payload.js" ;
39+ import { canonicalizeBase64 } from "../../media/base64.js" ;
40+ import { basenameFromAnyPath } from "../../media/file-name.js" ;
3641import type { OutboundMediaAccess } from "../../media/load-options.js" ;
3742import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js" ;
43+ import { extensionForMime } from "../../media/mime.js" ;
3844import { resolveAgentScopedOutboundMediaAccess } from "../../media/read-capability.js" ;
3945import { hasPollCreationParams } from "../../poll-params.js" ;
4046import { resolvePollMaxSelections } from "../../polls.js" ;
@@ -49,6 +55,7 @@ import {
4955 type GatewayClientName ,
5056} from "../../utils/message-channel.js" ;
5157import { formatErrorMessage } from "../errors.js" ;
58+ import { resolvePreferredOpenClawTmpDir } from "../tmp-openclaw-dir.js" ;
5259import { throwIfAborted } from "./abort.js" ;
5360import { resolveOutboundChannelPlugin } from "./channel-resolution.js" ;
5461import {
@@ -467,6 +474,51 @@ type SendPayloadParts = {
467474 silent ?: boolean ;
468475} ;
469476
477+ async function materializeBufferBackedSendMedia ( params : {
478+ actionParams : Record < string , unknown > ;
479+ sandboxRoot ?: string ;
480+ } ) : Promise < void > {
481+ const existingMedia =
482+ readStringParam ( params . actionParams , "media" , { trim : false } ) ??
483+ readStringParam ( params . actionParams , "mediaUrl" , { trim : false } ) ??
484+ readStringParam ( params . actionParams , "path" , { trim : false } ) ??
485+ readStringParam ( params . actionParams , "filePath" , { trim : false } ) ??
486+ readStringParam ( params . actionParams , "fileUrl" , { trim : false } ) ;
487+ if ( existingMedia ) {
488+ return ;
489+ }
490+
491+ const rawBuffer = readStringParam ( params . actionParams , "buffer" , { trim : false } ) ;
492+ if ( ! rawBuffer ) {
493+ return ;
494+ }
495+ const canonicalBase64 = canonicalizeBase64 ( rawBuffer ) ;
496+ if ( ! canonicalBase64 ) {
497+ throw new Error ( "send buffer must be valid base64" ) ;
498+ }
499+
500+ const filenameHint =
501+ readStringParam ( params . actionParams , "filename" , { trim : false } ) ?? "attachment" ;
502+ const contentType =
503+ readStringParam ( params . actionParams , "contentType" ) ??
504+ readStringParam ( params . actionParams , "mimeType" ) ??
505+ undefined ;
506+ const baseName = basenameFromAnyPath ( filenameHint ) ?. trim ( ) || "attachment" ;
507+ const normalizedName = path . extname ( baseName )
508+ ? baseName
509+ : `${ baseName } .${ extensionForMime ( contentType ) ?? "bin" } ` ;
510+ const rootDir = params . sandboxRoot ?. trim ( ) || resolvePreferredOpenClawTmpDir ( ) ;
511+ const outputDir = path . join ( rootDir , "outbound-buffer-media" ) ;
512+ await fs . mkdir ( outputDir , { recursive : true } ) ;
513+ const outputPath = path . join ( outputDir , `${ randomUUID ( ) } -${ normalizedName } ` ) ;
514+ await fs . writeFile ( outputPath , Buffer . from ( canonicalBase64 , "base64" ) ) ;
515+ params . actionParams . buffer = canonicalBase64 ;
516+ params . actionParams . media = outputPath ;
517+ if ( ! readStringParam ( params . actionParams , "filename" , { trim : false } ) ) {
518+ params . actionParams . filename = normalizedName ;
519+ }
520+ }
521+
470522function updateSendPayloadPartsFromReplyPayload (
471523 parts : SendPayloadParts ,
472524 payload : ReplyPayload ,
@@ -934,6 +986,10 @@ async function buildSendPayloadParts(params: {
934986 }
935987 }
936988 }
989+ await materializeBufferBackedSendMedia ( {
990+ actionParams,
991+ sandboxRoot : input . sandboxRoot ,
992+ } ) ;
937993 const mediaHint =
938994 readStringParam ( actionParams , "media" , { trim : false } ) ??
939995 readStringParam ( actionParams , "mediaUrl" , { trim : false } ) ??
0 commit comments