@@ -26,6 +26,10 @@ import {
2626import type { OpenClawConfig } from "../../config/types.openclaw.js" ;
2727import { resolveOutboundChannelPlugin } from "../../infra/outbound/channel-resolution.js" ;
2828import { resolveMessageChannelSelection } from "../../infra/outbound/channel-selection.js" ;
29+ import {
30+ hydrateAttachmentParamsForAction ,
31+ resolveAttachmentMediaPolicy ,
32+ } from "../../infra/outbound/message-action-params.js" ;
2933import {
3034 ensureOutboundSessionEntry ,
3135 resolveOutboundSessionRoute ,
@@ -507,25 +511,39 @@ export const sendHandlers: GatewayRequestHandlers = {
507511 }
508512
509513 try {
514+ const sessionKey = normalizeOptionalString ( request . sessionKey ) ?? undefined ;
515+ const agentId =
516+ normalizeOptionalString ( request . agentId ) ??
517+ ( sessionKey ? resolveSessionAgentId ( { sessionKey, config : cfg } ) : undefined ) ;
518+ const accountId = normalizeOptionalString ( request . accountId ) ?? undefined ;
519+ if ( request . action === "send" ) {
520+ await hydrateAttachmentParamsForAction ( {
521+ cfg,
522+ channel,
523+ accountId,
524+ args : request . params ,
525+ action : "send" ,
526+ mediaPolicy : resolveAttachmentMediaPolicy ( {
527+ mediaLocalRoots : getAgentScopedMediaLocalRoots ( cfg , agentId ) ,
528+ } ) ,
529+ } ) ;
530+ }
510531 const gatewayClientScopes = client ?. connect ?. scopes ?? [ ] ;
511532 const handled = await dispatchChannelMessageAction ( {
512533 channel,
513534 action : request . action as never ,
514535 cfg,
515536 params : request . params ,
516- accountId : normalizeOptionalString ( request . accountId ) ?? undefined ,
537+ accountId,
517538 requesterSenderId : normalizeOptionalString ( request . requesterSenderId ) ?? undefined ,
518539 senderIsOwner : gatewayClientScopes . includes ( ADMIN_SCOPE )
519540 ? request . senderIsOwner === true
520541 : false ,
521- sessionKey : normalizeOptionalString ( request . sessionKey ) ?? undefined ,
542+ sessionKey,
522543 sessionId : normalizeOptionalString ( request . sessionId ) ?? undefined ,
523544 inboundEventKind : request . inboundTurnKind ,
524- agentId : normalizeOptionalString ( request . agentId ) ?? undefined ,
525- mediaLocalRoots : getAgentScopedMediaLocalRoots (
526- cfg ,
527- normalizeOptionalString ( request . agentId ) ?? undefined ,
528- ) ,
545+ agentId,
546+ mediaLocalRoots : getAgentScopedMediaLocalRoots ( cfg , agentId ) ,
529547 toolContext : request . toolContext ,
530548 dryRun : false ,
531549 gatewayClientScopes,
@@ -539,10 +557,6 @@ export const sendHandlers: GatewayRequestHandlers = {
539557 return { ok : false , error, meta : { channel } } ;
540558 }
541559 const payload = extractToolPayload ( handled ) ;
542- const sessionKey = normalizeOptionalString ( request . sessionKey ) ?? undefined ;
543- const agentId =
544- normalizeOptionalString ( request . agentId ) ??
545- ( sessionKey ? resolveSessionAgentId ( { sessionKey, config : cfg } ) : undefined ) ;
546560 await scheduleDeliveredSourceReplyTranscriptMirror ( {
547561 context,
548562 mirror : {
@@ -583,6 +597,9 @@ export const sendHandlers: GatewayRequestHandlers = {
583597 message ?: string ;
584598 mediaUrl ?: string ;
585599 mediaUrls ?: string [ ] ;
600+ buffer ?: string ;
601+ filename ?: string ;
602+ contentType ?: string ;
586603 asVoice ?: boolean ;
587604 gifPlayback ?: boolean ;
588605 channel ?: string ;
@@ -615,7 +632,8 @@ export const sendHandlers: GatewayRequestHandlers = {
615632 . map ( ( entry ) => normalizeOptionalString ( entry ) )
616633 . filter ( ( entry ) : entry is string => Boolean ( entry ) )
617634 : undefined ;
618- if ( ! message && ! mediaUrl && ( mediaUrls ?. length ?? 0 ) === 0 ) {
635+ const buffer = readStringValue ( request . buffer ) ;
636+ if ( ! message && ! mediaUrl && ( mediaUrls ?. length ?? 0 ) === 0 && ! buffer ) {
619637 respond (
620638 false ,
621639 undefined ,
@@ -663,19 +681,6 @@ export const sendHandlers: GatewayRequestHandlers = {
663681 accountId,
664682 } ) ;
665683 const deliveryTarget = idLikeTarget ?. to ?? resolvedTarget . to ;
666- const outboundDeps = context . deps ? createOutboundSendDeps ( context . deps ) : undefined ;
667- const outboundPayloads = [
668- {
669- text : message ,
670- mediaUrl,
671- mediaUrls,
672- ...( request . asVoice === true ? { audioAsVoice : true } : { } ) ,
673- } ,
674- ] ;
675- const outboundPayloadPlan = createOutboundPayloadPlan ( outboundPayloads ) ;
676- const mirrorProjection = projectOutboundPayloadPlanForMirror ( outboundPayloadPlan ) ;
677- const mirrorText = mirrorProjection . text ;
678- const mirrorMediaUrls = mirrorProjection . mediaUrls ;
679684 // Preserve opaque, case-sensitive peer IDs (e.g. Matrix room ids) on an
680685 // explicit session key instead of raw-lowercasing it (openclaw#75670).
681686 // Non-enrolled channels still canonicalize to lowercase via the registry.
@@ -687,6 +692,42 @@ export const sendHandlers: GatewayRequestHandlers = {
687692 : undefined ;
688693 const defaultAgentId = resolveSessionAgentId ( { config : cfg } ) ;
689694 const effectiveAgentId = explicitAgentId ?? sessionAgentId ?? defaultAgentId ;
695+ const sendArgs : Record < string , unknown > = {
696+ mediaUrl,
697+ mediaUrls,
698+ buffer,
699+ filename : normalizeOptionalString ( request . filename ) ?? undefined ,
700+ contentType : normalizeOptionalString ( request . contentType ) ?? undefined ,
701+ } ;
702+ await hydrateAttachmentParamsForAction ( {
703+ cfg,
704+ channel,
705+ accountId,
706+ args : sendArgs ,
707+ action : "send" ,
708+ mediaPolicy : resolveAttachmentMediaPolicy ( {
709+ mediaLocalRoots : getAgentScopedMediaLocalRoots ( cfg , effectiveAgentId ) ,
710+ } ) ,
711+ } ) ;
712+ const hydratedMediaUrl = normalizeOptionalString ( sendArgs . mediaUrl ) ;
713+ const hydratedMediaUrls = Array . isArray ( sendArgs . mediaUrls )
714+ ? sendArgs . mediaUrls
715+ . map ( ( entry ) => normalizeOptionalString ( entry ) )
716+ . filter ( ( entry ) : entry is string => Boolean ( entry ) )
717+ : undefined ;
718+ const outboundDeps = context . deps ? createOutboundSendDeps ( context . deps ) : undefined ;
719+ const outboundPayloads = [
720+ {
721+ text : message ,
722+ mediaUrl : hydratedMediaUrl ,
723+ mediaUrls : hydratedMediaUrls ,
724+ ...( request . asVoice === true ? { audioAsVoice : true } : { } ) ,
725+ } ,
726+ ] ;
727+ const outboundPayloadPlan = createOutboundPayloadPlan ( outboundPayloads ) ;
728+ const mirrorProjection = projectOutboundPayloadPlanForMirror ( outboundPayloadPlan ) ;
729+ const mirrorText = mirrorProjection . text ;
730+ const mirrorMediaUrls = mirrorProjection . mediaUrls ;
690731 const derivedRoute = await resolveOutboundSessionRoute ( {
691732 cfg,
692733 channel,
0 commit comments