@@ -11,6 +11,13 @@ import { BotsChatCloudClient } from "./ws-client.js";
1111import crypto from "crypto" ;
1212import { encryptText , encryptBytes , decryptText , decryptBytes , toBase64 , fromBase64 } from "./e2e-crypto.js" ;
1313
14+ /** Subset of OpenClaw's ReplyPayload that the deliver callback receives. */
15+ type DeliverPayload = {
16+ text ?: string ;
17+ mediaUrl ?: string ;
18+ mediaUrls ?: string [ ] ;
19+ } ;
20+
1421// ---------------------------------------------------------------------------
1522// A2UI message-tool hints — injected via agentPrompt.messageToolHints so
1623// the agent knows it can output interactive UI components. These strings
@@ -638,44 +645,69 @@ async function handleCloudMessage(
638645 // Create a reply dispatcher that sends responses back through the cloud WSS
639646 // NOTE: reuses `client` from line ~424 (same block scope, same value)
640647 console . log ( `[botschat] client for accountId=${ ctx . accountId } : connected=${ client ?. connected } ` ) ;
641- const deliver = async ( payload : { text ?: string ; mediaUrl ?: string } ) => {
642- console . log ( `[botschat][deliver] called, connected=${ client ?. connected } , hasKey=${ ! ! client ?. e2eKey } , textLen=${ ( payload . text || "" ) . length } ` ) ;
648+ const deliver = async ( payload : DeliverPayload ) => {
649+ const mediaList = payload . mediaUrls ?. length
650+ ? payload . mediaUrls
651+ : payload . mediaUrl
652+ ? [ payload . mediaUrl ]
653+ : [ ] ;
654+ console . log ( `[botschat][deliver] called, connected=${ client ?. connected } , hasKey=${ ! ! client ?. e2eKey } , textLen=${ ( payload . text || "" ) . length } , mediaCount=${ mediaList . length } ` ) ;
643655 if ( ! client ?. connected ) { console . log ( "[botschat][deliver] SKIP - not connected" ) ; return ; }
644- const messageId = crypto . randomUUID ( ) ;
645- let text = payload . text ?? "" ;
646- let caption = payload . text ?? "" ;
647- let encrypted = false ;
648656
649- if ( client . e2eKey && text ) {
650- try {
651- const ct = await encryptText ( client . e2eKey , text , messageId ) ;
652- text = toBase64 ( ct ) ;
653- caption = text ;
654- encrypted = true ;
655- console . log ( `[botschat][deliver] encrypted OK: msgId=${ messageId } , ctLen=${ text . length } , encrypted=${ encrypted } ` ) ;
656- } catch ( err ) {
657- console . error ( "[botschat][deliver] E2E encrypt failed:" , err ) ;
658- }
659- } else {
660- console . log ( `[botschat][deliver] no encryption: hasKey=${ ! ! client . e2eKey } , textLen=${ text . length } ` ) ;
661- }
657+ if ( mediaList . length > 0 ) {
658+ let first = true ;
659+ for ( const mediaUrl of mediaList ) {
660+ const messageId = crypto . randomUUID ( ) ;
661+ const rawCaption = first ? ( payload . text ?? "" ) : "" ;
662+ first = false ;
663+ let caption = rawCaption ;
664+ let encrypted = false ;
665+
666+ if ( client . e2eKey && caption ) {
667+ try {
668+ const ct = await encryptText ( client . e2eKey , caption , messageId ) ;
669+ caption = toBase64 ( ct ) ;
670+ encrypted = true ;
671+ } catch ( err ) {
672+ console . error ( "[botschat][deliver] E2E encrypt caption failed:" , err ) ;
673+ }
674+ }
662675
663- const notifyPreviewText = ( encrypted && client . notifyPreview && payload . text )
664- ? ( payload . text . length > 100 ? payload . text . slice ( 0 , 100 ) + "…" : payload . text )
665- : undefined ;
666- console . log ( `[botschat][deliver] sending: type=${ payload . mediaUrl ? " agent.media" : "agent.text" } , encrypted=${ encrypted } , messageId=${ messageId } , notifyPreview= ${ ! ! notifyPreviewText } ` ) ;
667- if ( payload . mediaUrl ) {
668- client . send ( {
669- type : "agent.media" ,
670- sessionKey : msg . sessionKey ,
671- mediaUrl : payload . mediaUrl ,
672- caption : encrypted ? caption : payload . text ,
673- threadId ,
674- messageId ,
675- encrypted ,
676- ... ( notifyPreviewText ? { notifyPreview : notifyPreviewText } : { } ) ,
677- } ) ;
676+ const notifyPreviewText = ( encrypted && client . notifyPreview && rawCaption )
677+ ? ( rawCaption . length > 100 ? rawCaption . slice ( 0 , 100 ) + "…" : rawCaption )
678+ : undefined ;
679+ console . log ( `[botschat][deliver] sending: type=agent.media, encrypted=${ encrypted } , messageId=${ messageId } ` ) ;
680+ client . send ( {
681+ type : "agent.media" ,
682+ sessionKey : msg . sessionKey ,
683+ mediaUrl ,
684+ caption : caption || undefined ,
685+ threadId ,
686+ messageId ,
687+ encrypted ,
688+ ... ( notifyPreviewText ? { notifyPreview : notifyPreviewText } : { } ) ,
689+ } ) ;
690+ }
678691 } else if ( payload . text ) {
692+ const messageId = crypto . randomUUID ( ) ;
693+ let text = payload . text ;
694+ let encrypted = false ;
695+
696+ if ( client . e2eKey && text ) {
697+ try {
698+ const ct = await encryptText ( client . e2eKey , text , messageId ) ;
699+ text = toBase64 ( ct ) ;
700+ encrypted = true ;
701+ console . log ( `[botschat][deliver] encrypted OK: msgId=${ messageId } , ctLen=${ text . length } ` ) ;
702+ } catch ( err ) {
703+ console . error ( "[botschat][deliver] E2E encrypt failed:" , err ) ;
704+ }
705+ }
706+
707+ const notifyPreviewText = ( encrypted && client . notifyPreview && payload . text )
708+ ? ( payload . text . length > 100 ? payload . text . slice ( 0 , 100 ) + "…" : payload . text )
709+ : undefined ;
710+ console . log ( `[botschat][deliver] sending: type=agent.text, encrypted=${ encrypted } , messageId=${ messageId } ` ) ;
679711 client . send ( {
680712 type : "agent.text" ,
681713 sessionKey : msg . sessionKey ,
@@ -686,9 +718,6 @@ async function handleCloudMessage(
686718 ...( notifyPreviewText ? { notifyPreview : notifyPreviewText } : { } ) ,
687719 } ) ;
688720 // Detect model-change confirmations and emit model.changed
689- // Handles both formats:
690- // "Model set to provider/model." (no parentheses)
691- // "Model set to Friendly Name (provider/model)." (with parentheses)
692721 const modelMatch = payload . text . match (
693722 / M o d e l (?: s e t t o | r e s e t t o d e f a u l t ) \b .* ?( [ a - z A - Z 0 - 9 _ - ] + (?: \. [ a - z A - Z 0 - 9 _ - ] + ) * \/ [ a - z A - Z 0 - 9 _ - ] + (?: \. [ a - z A - Z 0 - 9 _ - ] + ) * ) / ,
694723 ) ;
@@ -731,9 +760,7 @@ async function handleCloudMessage(
731760 const { dispatcher, replyOptions, markDispatchIdle } =
732761 runtime . channel . reply . createReplyDispatcherWithTyping ( {
733762 deliver : async ( payload : unknown ) => {
734- // The payload from the dispatcher is a ReplyPayload
735- const p = payload as { text ?: string ; mediaUrl ?: string } ;
736- await deliver ( p ) ;
763+ await deliver ( payload as DeliverPayload ) ;
737764 } ,
738765 onTypingStart : ( ) => { } ,
739766 onTypingStop : ( ) => { } ,
@@ -1280,11 +1307,18 @@ async function handleTaskRun(
12801307 } , THROTTLE_MS ) ;
12811308 } ;
12821309
1283- const deliver = async ( payload : { text ?: string ; mediaUrl ?: string } ) => {
1284- if ( payload . text ) {
1285- completedParts . push ( payload . text ) ;
1310+ const deliver = async ( payload : DeliverPayload ) => {
1311+ const mediaList = payload . mediaUrls ?. length
1312+ ? payload . mediaUrls
1313+ : payload . mediaUrl
1314+ ? [ payload . mediaUrl ]
1315+ : [ ] ;
1316+ const parts : string [ ] = [ ] ;
1317+ if ( payload . text ) parts . push ( payload . text ) ;
1318+ for ( const url of mediaList ) parts . push ( `` ) ;
1319+ if ( parts . length > 0 ) {
1320+ completedParts . push ( parts . join ( "\n" ) ) ;
12861321 currentStreamText = "" ;
1287- // Flush immediately on completed message
12881322 if ( sendTimer ) { clearTimeout ( sendTimer ) ; sendTimer = null ; }
12891323 sendOutput ( ) ;
12901324 }
@@ -1300,8 +1334,7 @@ async function handleTaskRun(
13001334 const { dispatcher, replyOptions, markDispatchIdle } =
13011335 runtime . channel . reply . createReplyDispatcherWithTyping ( {
13021336 deliver : async ( payload : unknown ) => {
1303- const p = payload as { text ?: string ; mediaUrl ?: string } ;
1304- await deliver ( p ) ;
1337+ await deliver ( payload as DeliverPayload ) ;
13051338 } ,
13061339 onTypingStart : ( ) => { } ,
13071340 onTypingStop : ( ) => { } ,
0 commit comments