@@ -51,6 +51,7 @@ import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
5151import {
5252 getReplyPayloadMetadata ,
5353 isReplyPayloadStatusNotice ,
54+ readPairingQrReplyChannelData ,
5455 type ReplyPayload ,
5556} from "../../auto-reply/reply-payload.js" ;
5657import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js" ;
@@ -84,6 +85,8 @@ import {
8485} from "../../media/local-roots.js" ;
8586import { parseInboundMediaUri } from "../../media/media-reference.js" ;
8687import type { PromptImageOrderEntry } from "../../media/prompt-image-order.js" ;
88+ import { renderQrPngDataUrl } from "../../media/qr-image.js" ;
89+ import { renderQrTerminal } from "../../media/qr-terminal.js" ;
8790import {
8891 deleteMediaBuffer ,
8992 MEDIA_MAX_BYTES ,
@@ -857,12 +860,35 @@ function buildTranscriptReplyText(payloads: ReplyPayload[]): string {
857860
858861function hasSensitiveMediaPayload ( payloads : ReplyPayload [ ] ) : boolean {
859862 return payloads . some (
860- ( payload ) => payload . sensitiveMedia === true && isMediaBearingPayload ( payload ) ,
863+ ( payload ) =>
864+ payload . sensitiveMedia === true &&
865+ ( isMediaBearingPayload ( payload ) || Boolean ( readPairingQrReplyChannelData ( payload ) ) ) ,
861866 ) ;
862867}
863868
864869type AssistantDisplayContentBlock = Record < string , unknown > ;
865870
871+ async function buildPairingQrAssistantContentBlock (
872+ payload : ReplyPayload ,
873+ ) : Promise < AssistantDisplayContentBlock | undefined > {
874+ const qr = readPairingQrReplyChannelData ( payload ) ;
875+ if ( ! qr ) {
876+ return undefined ;
877+ }
878+ const [ imageUrl , terminalText ] = await Promise . all ( [
879+ renderQrPngDataUrl ( qr . setupCode ) ,
880+ renderQrTerminal ( qr . setupCode , { small : true } ) ,
881+ ] ) ;
882+ return {
883+ type : "openclaw_pairing_qr" ,
884+ image_url : imageUrl ,
885+ terminalText,
886+ alt : "OpenClaw pairing QR code" ,
887+ expiresAtMs : qr . expiresAtMs ,
888+ sensitive : true ,
889+ } ;
890+ }
891+
866892function sanitizeAssistantDisplayText ( value ?: string | null ) : string | undefined {
867893 if ( ! value ) {
868894 return undefined ;
@@ -896,8 +922,10 @@ async function buildAssistantDisplayContentFromReplyPayloads(params: {
896922 payloads : ReplyPayload [ ] ;
897923 managedImageLocalRoots ?: Parameters < typeof createManagedOutgoingImageBlocks > [ 0 ] [ "localRoots" ] ;
898924 includeSensitiveMedia ?: boolean ;
925+ includeSensitiveDisplay ?: boolean ;
899926 onLocalAudioAccessDenied ?: ( message : string ) => void ;
900927 onManagedImagePrepareError ?: ( message : string ) => void ;
928+ onSensitiveDisplayPrepareError ?: ( message : string ) => void ;
901929} ) : Promise < AssistantDisplayContentBlock [ ] | undefined > {
902930 const rawTextPayloadCount = params . payloads . filter (
903931 ( payload ) =>
@@ -919,6 +947,16 @@ async function buildAssistantDisplayContentFromReplyPayloads(params: {
919947 } else if ( typeof payload . text === "string" && payload . text . trim ( ) . length > 0 ) {
920948 strippedTextPayloadCount += 1 ;
921949 }
950+ if ( params . includeSensitiveDisplay === true ) {
951+ try {
952+ const pairingQrBlock = await buildPairingQrAssistantContentBlock ( payload ) ;
953+ if ( pairingQrBlock ) {
954+ content . push ( pairingQrBlock ) ;
955+ }
956+ } catch ( err ) {
957+ params . onSensitiveDisplayPrepareError ?.( formatForLog ( err ) ) ;
958+ }
959+ }
922960 if ( params . includeSensitiveMedia === false && payload . sensitiveMedia === true ) {
923961 continue ;
924962 }
@@ -4642,6 +4680,7 @@ export const chatHandlers: GatewayRequestHandlers = {
46424680 payloads : finalPayloads ,
46434681 managedImageLocalRoots : mediaLocalRoots ,
46444682 includeSensitiveMedia : false ,
4683+ includeSensitiveDisplay : true ,
46454684 onLocalAudioAccessDenied : ( message ) => {
46464685 context . logGateway . warn (
46474686 `webchat audio embedding denied local path: ${ message } ` ,
@@ -4652,6 +4691,11 @@ export const chatHandlers: GatewayRequestHandlers = {
46524691 `webchat image embedding skipped attachment: ${ message } ` ,
46534692 ) ;
46544693 } ,
4694+ onSensitiveDisplayPrepareError : ( message ) => {
4695+ context . logGateway . warn (
4696+ `webchat sensitive display skipped attachment: ${ message } ` ,
4697+ ) ;
4698+ } ,
46554699 } ) ;
46564700 const mediaMessage = await buildWebchatAssistantMediaMessage ( finalPayloads , {
46574701 localRoots : mediaLocalRoots ,
0 commit comments