UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

550 lines (549 loc) • 27.1 kB
import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-1VuiLaaR.mjs"; import { u as openLocalFileSafely } from "./fs-safe-BBzelG8F.mjs"; import { d as resolveAgentWorkspaceDir } from "./agent-scope-config-BEk6-ThS.mjs"; import "./agent-scope-bJOokm5r.mjs"; import { D as setReplyPayloadMetadata, c as getReplyPayloadMetadata, o as copyReplyPayloadMetadata } from "./reply-payload-BEgsL2xJ.mjs"; import { t as normalizeMediaReferenceForComparison } from "./media-reference-comparison-D61D3Ct7.mjs"; import { t as collectReplyMediaEntries } from "./reply-media-entries-DF6UkRyL.mjs"; import { n as isCloudWorkerPlacementState } from "./session-placement-state-BsPYZ8PU.mjs"; import { o as isAudioFileName, u as mimeTypeFromFilePath } from "./mime-C3j3dfQb.mjs"; import { n as estimateBase64DecodedBytes } from "./base64-CU53EX1R.mjs"; import { i as parseInlineDirectives, l as stripInlineDirectiveTagsForDelivery, s as sanitizeReplyDirectiveId } from "./directive-tags-DLtws5Pc.mjs"; import { i as resolveSessionPermissionCoreToolPolicy } from "./session-permission-exec-mode-w9RzCNiZ.mjs"; import { i as withChannelReadAuthority, t as captureChannelReadAuthority } from "./channel-read-authority-DJtFPb9x.mjs"; import { n as createStructuredOutboundPayloadPlan } from "./payloads-k3x9706z.mjs"; import { a as resolveSendableOutboundReplyParts } from "./reply-payload-parts-XnYlwTNV.mjs"; import "./reply-payload-eiK8mKuN.mjs"; import { t as isPassThroughRemoteMediaSource } from "./media-source-url-BdNs7xa2.mjs"; import { n as resolveEffectiveToolFsWorkspaceOnly } from "./tool-fs-policy-D543Bcua.mjs"; import { n as getAgentScopedMediaLocalRoots, t as appendLocalMediaParentRoots } from "./local-roots-Bd0Cpppp.mjs"; import { s as getMediaDir } from "./store-7jnF8fA5.mjs"; import { n as LocalMediaAccessError, r as assertLocalMediaAllowed } from "./local-media-access-BBod-__x.mjs"; import { n as resolveAgentScopedOutboundMediaAccess } from "./read-capability-BZ3Bz1b_.mjs"; import { t as WEBCHAT_LOCAL_MEDIA_MAX_BYTES } from "./configured-max-bytes-C0WewC1G.mjs"; import { t as resolveSessionWorkerPlacementContext } from "./session-worker-placement-context-CvBn6ZZM.mjs"; import { r as isSuppressedControlReplyText } from "./control-reply-text-CdlY-Sik.mjs"; import { r as loadGatewaySessionEntry } from "./session-utils-store-qn-zT-Fk.mjs"; import { t as resolveSessionWorkspaceRoots } from "./session-workspace-roots-C9j-5mH-.mjs"; import "./session-utils-BLqrCe75.mjs"; import { n as buildAssistantReplyContentFromInputs, r as combineNonStreamingReplyParts, u as sanitizeAssistantDisplayText } from "./chat-assistant-content-CZwd1b0s.mjs"; import path from "node:path"; import { assertNoWindowsNetworkPath, safeFileURLToPath } from "@openclaw/fs-safe/advanced"; //#region src/gateway/server-methods/chat-send-command-replies.ts function readChatSendReplyPayload(input) { return input.kind === "raw" ? input.payload : input.plan.payload; } function buildTranscriptReplyTextFromInputs(inputs) { const chunks = inputs.map((input) => { const payload = readChatSendReplyPayload(input); if (payload.isReasoning === true) return ""; const parts = input.kind === "prepared" ? input.plan.parts : resolveSendableOutboundReplyParts(payload); const lines = []; const parsedText = input.kind === "raw" && payload.text?.includes("[[") ? parseInlineDirectives(payload.text) : void 0; const replyToId = sanitizeReplyDirectiveId(payload.replyToId) ?? sanitizeReplyDirectiveId(parsedText?.replyToExplicitId); if (replyToId) lines.push(`[[reply_to:${replyToId}]]`); else if (payload.replyToCurrent || parsedText?.replyToCurrent) lines.push("[[reply_to_current]]"); const text = input.kind === "raw" && payload.text ? stripInlineDirectiveTagsForDelivery(payload.text).text : payload.text ?? ""; if (text.trim() && (input.kind === "prepared" || !isSuppressedControlReplyText(text))) lines.push(text); for (const mediaUrl of parts.mediaUrls) { if (payload.sensitiveMedia === true) continue; const trimmed = mediaUrl.trim(); if (trimmed) lines.push(`Attachment: ${trimmed}`); } if ((payload.audioAsVoice || parsedText?.audioAsVoice) && parts.mediaUrls.some((mediaUrl) => isAudioFileName(mediaUrl))) lines.push("[[audio_as_voice]]"); return lines.join("\n"); }).filter(Boolean); return combineNonStreamingReplyParts(chunks); } function replaceChatSendReplyPayload(input, payload) { return input.kind === "raw" ? [{ kind: "raw", payload }] : createStructuredOutboundPayloadPlan([payload]).map((plan) => ({ kind: "prepared", plan })); } function parseReplyInlineDirectives(payload) { return typeof payload.text === "string" && payload.text.includes("[[") ? parseInlineDirectives(payload.text) : void 0; } function replyMediaDedupeKeys(payload) { return resolveSendableOutboundReplyParts(payload).mediaUrls.map((mediaUrl) => normalizeMediaReferenceForComparison(mediaUrl)); } function canonicalizeReplyMedia(payload) { const { mediaUrls } = resolveSendableOutboundReplyParts(payload); return copyReplyPayloadMetadata(payload, { ...payload, mediaUrl: void 0, mediaUrls: mediaUrls.length > 0 ? mediaUrls : void 0 }); } function mergeDefinedReplySemantics(target, source) { const sourceInlineDirectives = parseReplyInlineDirectives(source); const sourceReplyToId = sanitizeReplyDirectiveId(source.replyToId) ?? sanitizeReplyDirectiveId(sourceInlineDirectives?.replyToExplicitId); const mergedMedia = mergeMediaReplySemantics(target, source, sourceInlineDirectives); return copyReplyPayloadMetadata(mergedMedia, { ...mergedMedia, ...source.presentation !== void 0 ? { presentation: source.presentation } : {}, ...source.delivery !== void 0 ? { delivery: source.delivery } : {}, ...source.interactive !== void 0 ? { interactive: source.interactive } : {}, ...sourceReplyToId !== void 0 ? { replyToId: sourceReplyToId } : {}, ...source.replyToTag === true || target.replyToTag === true ? { replyToTag: true } : {}, ...source.replyToCurrent === true || sourceInlineDirectives?.replyToCurrent === true || target.replyToCurrent === true ? { replyToCurrent: true } : {}, ...source.spokenText !== void 0 ? { spokenText: source.spokenText } : {}, ...source.ttsSupplement !== void 0 ? { ttsSupplement: source.ttsSupplement } : {}, ...source.isError === true || target.isError === true ? { isError: true } : {}, ...source.channelData !== void 0 ? { channelData: source.channelData } : {} }); } function mergeMediaReplySemantics(target, source, sourceInlineDirectives = parseReplyInlineDirectives(source)) { let attachments = target.attachments; if (source.attachments?.length) { const sourceAttachments = /* @__PURE__ */ new Map(); for (const { url, attachment } of collectReplyMediaEntries(source)) { const key = normalizeMediaReferenceForComparison(url); if (attachment && !sourceAttachments.has(key)) sourceAttachments.set(key, attachment); } attachments = collectReplyMediaEntries(target).map(({ url, attachment }) => { const sourceAttachment = sourceAttachments.get(normalizeMediaReferenceForComparison(url)); if (!sourceAttachment) return attachment ?? {}; const merged = Object.assign({}, attachment, sourceAttachment); for (const field of [ "path", "url", "mediaUrl", "filePath" ]) if (merged[field] !== void 0) merged[field] = url; return merged; }); } return copyReplyPayloadMetadata(target, { ...target, ...attachments ? { attachments } : {}, ...source.trustedLocalMedia === true || target.trustedLocalMedia === true ? { trustedLocalMedia: true } : {}, ...source.sensitiveMedia === true || target.sensitiveMedia === true ? { sensitiveMedia: true } : {}, ...source.audioAsVoice === true || sourceInlineDirectives?.audioAsVoice === true || target.audioAsVoice === true ? { audioAsVoice: true } : {} }); } function hasMergeableReplySemantics(payload) { const inlineDirectives = parseReplyInlineDirectives(payload); return Boolean(payload.trustedLocalMedia !== void 0 || payload.sensitiveMedia !== void 0 || payload.presentation || payload.delivery || payload.interactive || payload.replyToId || payload.replyToTag !== void 0 || payload.replyToCurrent !== void 0 || payload.audioAsVoice !== void 0 || inlineDirectives?.hasReplyTag || inlineDirectives?.hasAudioTag || payload.spokenText || payload.ttsSupplement || payload.isError !== void 0 || payload.channelData); } function hasUnmergedReplySemantics(payload) { return Boolean(payload.isReasoning || payload.isReasoningSnapshot || payload.isCompactionNotice || payload.isFallbackNotice || payload.isStatusNotice || payload.btw); } function mediaSetsMatch(leftMediaUrls, rightMediaUrls) { return leftMediaUrls.length === rightMediaUrls.length && leftMediaUrls.every((mediaUrl, index) => mediaUrl === rightMediaUrls[index]); } function replyDisplayText(payload) { return sanitizeAssistantDisplayText(payload.text) ?? ""; } /** Folds raw command replies while preserving each prepared reply's ownership. */ function selectChatSendFinalReplyInputs(params) { const { deliveredReplies, foldCommandBlocks, suppressReplies } = params; const finalInputs = deliveredReplies.filter((entry) => entry.kind === "final").map((entry) => entry.input); let commandBlockInputs = foldCommandBlocks ? deliveredReplies.filter((entry) => entry.kind === "block").map(({ input }) => input.kind === "raw" ? { kind: "raw", payload: canonicalizeReplyMedia(input.payload) } : input) : []; const sensitiveMediaDedupeKeys = new Set(finalInputs.flatMap((input) => { const payload = readChatSendReplyPayload(input); return payload.sensitiveMedia === true ? replyMediaDedupeKeys(payload).filter(Boolean) : []; })); if (sensitiveMediaDedupeKeys.size > 0) commandBlockInputs = commandBlockInputs.flatMap((input) => { const payload = readChatSendReplyPayload(input); if (!replyMediaDedupeKeys(payload).some((key) => sensitiveMediaDedupeKeys.has(key))) return [input]; const sensitivePayload = { ...payload, sensitiveMedia: true }; return replaceChatSendReplyPayload(input, copyReplyPayloadMetadata(payload, sensitivePayload)); }); const finalInputsForDelivery = foldCommandBlocks ? finalInputs.flatMap((input) => { if (input.kind === "prepared") return [input]; const payload = input.payload; const { mediaUrls: finalMediaUrls } = resolveSendableOutboundReplyParts(payload); const finalMediaKeys = replyMediaDedupeKeys(payload); const finalDisplayText = replyDisplayText(payload); const matchingMediaBlock = finalMediaUrls.length > 0 ? commandBlockInputs.find((candidate) => candidate.kind === "raw" && mediaSetsMatch(replyMediaDedupeKeys(candidate.payload), finalMediaKeys)) : void 0; const duplicateBlock = finalDisplayText ? commandBlockInputs.find((candidate) => candidate.kind === "raw" && replyDisplayText(candidate.payload) === finalDisplayText && (finalMediaUrls.length === 0 || mediaSetsMatch(replyMediaDedupeKeys(candidate.payload), finalMediaKeys))) : matchingMediaBlock; if (duplicateBlock?.kind === "raw") duplicateBlock.payload = mergeDefinedReplySemantics(duplicateBlock.payload, payload); else if (matchingMediaBlock?.kind === "raw") matchingMediaBlock.payload = mergeMediaReplySemantics(matchingMediaBlock.payload, payload); const remainingFinalMediaUrls = matchingMediaBlock ? [] : finalMediaUrls; if (remainingFinalMediaUrls.length === 0 && (duplicateBlock && !hasUnmergedReplySemantics(payload) || !duplicateBlock && !finalDisplayText && !hasMergeableReplySemantics(payload) && !hasUnmergedReplySemantics(payload))) return []; return [{ kind: "raw", payload: copyReplyPayloadMetadata(payload, { ...payload, mediaUrl: void 0, mediaUrls: remainingFinalMediaUrls.length > 0 ? remainingFinalMediaUrls : void 0 }) }]; }) : finalInputs; if (suppressReplies) return []; return [...commandBlockInputs, ...finalInputsForDelivery]; } //#endregion //#region src/gateway/server-methods/chat-webchat-media.ts /** Cap local audio files exposed through assistant media. */ const MAX_WEBCHAT_AUDIO_BYTES = 15728640; const MAX_WEBCHAT_IMAGE_DATA_URL_CHARS = 2e6; const MAX_WEBCHAT_IMAGE_DATA_BYTES = 15e5; const ALLOWED_WEBCHAT_DATA_IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([ "image/apng", "image/avif", "image/bmp", "image/gif", "image/jpeg", "image/png", "image/webp" ]); /** Map `mediaUrl` strings to an absolute filesystem path for local embedding (plain paths or `file:` URLs). */ function resolveLocalMediaPathForEmbedding(raw) { const trimmed = raw.trim(); if (!trimmed || /^(?:data|https?):/i.test(trimmed)) return null; try { if (/^file:/iu.test(trimmed)) { const p = safeFileURLToPath(trimmed); return path.isAbsolute(p) ? p : null; } if (!path.isAbsolute(trimmed)) return null; assertNoWindowsNetworkPath(trimmed, "Local media path"); return trimmed; } catch { return null; } } async function readLocalAudioContentBlockForEmbedding(payload, raw, options) { if (payload.trustedLocalMedia !== true) return null; const resolved = resolveLocalMediaPathForEmbedding(raw); if (!resolved || !isAudioFileName(resolved)) return null; let opened; try { options?.assertCurrent?.(); await assertLocalMediaAllowed(resolved, options?.localRoots); options?.assertCurrent?.(); opened = await openLocalFileSafely({ filePath: resolved }); await assertLocalMediaAllowed(opened.realPath, options?.localRoots); options?.assertCurrent?.(); if (opened.stat.size > MAX_WEBCHAT_AUDIO_BYTES) return null; return { path: opened.realPath, block: { type: "attachment", attachment: { url: opened.realPath, kind: "audio", label: path.basename(opened.realPath), mimeType: mimeTypeFromFilePath(opened.realPath) ?? "audio/mpeg", ...payload.audioAsVoice === true ? { isVoiceNote: true } : {} } } }; } catch (err) { if (err instanceof LocalMediaAccessError) options?.onLocalAudioAccessDenied?.(err); return null; } finally { await opened?.handle.close().catch(() => {}); } } function resolveEmbeddableImageUrl(url) { const trimmed = url.trim(); if (!trimmed || trimmed.length > MAX_WEBCHAT_IMAGE_DATA_URL_CHARS) return null; const commaIndex = trimmed.indexOf(","); if (commaIndex < 0) return null; const metadata = trimmed.slice(0, commaIndex); const match = /^data:(image\/[a-z0-9.+-]+);base64$/i.exec(metadata); const base64Data = trimmed.slice(commaIndex + 1); if (!match || !base64Data || /[^A-Za-z0-9+/=\t\n\v\f\r ]/u.test(base64Data)) return null; const mediaType = normalizeLowercaseStringOrEmpty(match[1]); if (!ALLOWED_WEBCHAT_DATA_IMAGE_MEDIA_TYPES.has(mediaType)) return null; if (estimateBase64DecodedBytes(base64Data) > MAX_WEBCHAT_IMAGE_DATA_BYTES) return null; return trimmed; } function resolveReplyDirectivePrefix(payload) { const replyToId = sanitizeReplyDirectiveId(payload.replyToId); if (replyToId) return `[[reply_to:${replyToId}]]`; if (payload.replyToCurrent) return "[[reply_to_current]]"; return ""; } function mediaReplyText(hasAudio, hasImage) { return hasAudio && hasImage ? "Media reply" : hasAudio ? "Audio reply" : "Image reply"; } async function buildWebchatAssistantMessageFromReplyPayloads(payloads, options) { const content = []; const transcriptTextParts = []; const payloadTexts = []; const seenAudio = /* @__PURE__ */ new Set(); const seenImages = /* @__PURE__ */ new Set(); for (const [payloadIndex, payload] of payloads.entries()) { if (payload.isReasoning === true) continue; const visibleText = payload.text?.trim(); const text = visibleText && !isSuppressedControlReplyText(visibleText) ? visibleText : void 0; const replyDirectivePrefix = resolveReplyDirectivePrefix(payload); let payloadHasAudio = false; let payloadHasImage = false; const payloadMediaBlocks = []; const parts = resolveSendableOutboundReplyParts(payload); for (const raw of parts.mediaUrls) { const url = raw.trim(); if (!url) continue; const audio = await readLocalAudioContentBlockForEmbedding(payload, url, options); if (audio && !seenAudio.has(audio.path)) { seenAudio.add(audio.path); payloadMediaBlocks.push(audio.block); payloadHasAudio = true; continue; } const imageUrl = resolveEmbeddableImageUrl(url); if (!imageUrl || seenImages.has(imageUrl)) continue; seenImages.add(imageUrl); payloadMediaBlocks.push({ type: "input_image", image_url: imageUrl }); payloadHasImage = true; } const syntheticText = payloadMediaBlocks.length > 0 && (!text || replyDirectivePrefix) && transcriptTextParts.length === 0 ? mediaReplyText(payloadHasAudio, payloadHasImage) : void 0; const fullText = replyDirectivePrefix + (text ?? syntheticText ?? ""); if (fullText) { transcriptTextParts.push(fullText); payloadTexts[payloadIndex] = fullText; content.push({ type: "text", text: fullText }); } content.push(...payloadMediaBlocks); } if (seenAudio.size === 0 && seenImages.size === 0) return null; const transcriptText = transcriptTextParts.join("\n\n").trim() || mediaReplyText(seenAudio.size > 0, seenImages.size > 0); if (transcriptTextParts.length === 0) content.unshift({ type: "text", text: transcriptText }); return { content, transcriptText, payloadTexts }; } //#endregion //#region src/gateway/server-methods/chat-reply-media.ts function resolveRequesterPolicyContext(requester) { return { requesterSenderId: requester?.SenderId, requesterSenderName: requester?.SenderName, requesterSenderUsername: requester?.SenderUsername, requesterSenderE164: requester?.SenderE164, groupChannel: requester?.GroupChannel, groupSpace: requester?.GroupSpace, messageProvider: requester?.Surface ?? requester?.Provider }; } /** The policy facts retained while preparing one reply's files. */ function webchatReplyMediaAuthority(scope, placement) { const entry = scope.sessionEntry; const workspace = resolveWebchatReplyWorkspace(scope, placement); return { workspace, key: JSON.stringify([ entry?.sessionId, entry?.lifecycleRevision, entry?.permissionMode, entry?.execNode, entry?.repositoryWorkspaceId, workspace.remote, workspace.workspaceDir, resolveWebchatReplyWorkspaceOnly(scope) ]) }; } /** Bind reads to the source session; reread its owner after every awaited file operation. */ function captureWebchatReplyMediaScope(params) { const readEntry = () => loadGatewaySessionEntry(params.sessionKey, params.sessionLoadOptions).entry; const readPlacement = (entry) => entry?.sessionId && !entry.execNode && !entry.repositoryWorkspaceId ? resolveSessionWorkerPlacementContext().workerSessionPlacementService?.getMany([entry.sessionId]).get(entry.sessionId) : void 0; const sessionEntry = readEntry(); const scope = { ...params, sessionEntry: sessionEntry ? { ...sessionEntry } : void 0 }; const expected = webchatReplyMediaAuthority(scope, readPlacement(sessionEntry)); return { ...scope, workspace: expected.workspace, assertCurrent: () => { params.assertCurrent?.(); const current = readEntry(); if (webchatReplyMediaAuthority({ ...scope, sessionEntry: current }, readPlacement(current)).key !== expected.key) throw new Error("Session media access changed before attachment delivery."); } }; } /** Keep normalization and grouped content preparation inside one media custody scope. */ async function withPreparedWebchatReplyMedia(params, prepare) { const scope = params.scope; const sourcePayloads = params.inputs.map(readChatSendReplyPayload); const hasMedia = sourcePayloads.some((payload) => resolveSendableOutboundReplyParts(payload).mediaUrls.length > 0); return await withChannelReadAuthority(scope.assertCurrent, async () => { const payloads = await normalizeWebchatReplyMediaPathsForDisplay({ ...scope, payloads: sourcePayloads }); const inputsByIndex = params.inputs.map((input, index) => { const payload = payloads[index]; return payload ? replaceChatSendReplyPayload(input, payload) : []; }); const localRoots = hasMedia ? getWebchatReplyMediaLocalRoots({ ...scope, storePath: params.storePath }) : []; return prepare({ payloads, inputsByIndex, buildContent: async (inputs) => { const mediaMessage = await buildWebchatAssistantMessageFromReplyPayloads(inputs.map(readChatSendReplyPayload), { localRoots, assertCurrent: captureChannelReadAuthority(), onLocalAudioAccessDenied: params.onLocalAudioAccessDenied }); return { ...await buildAssistantReplyContentFromInputs({ sessionKey: params.transcriptTarget?.sessionKey ?? scope.sessionKey, agentId: params.transcriptTarget?.agentId ?? scope.agentId, inputs, transcriptMediaMessage: mediaMessage, managedMediaLocalRoots: localRoots, assertCurrent: scope.assertCurrent, abortSignal: params.abortSignal, includeSensitiveMedia: params.includeSensitiveMedia, includeSensitiveDisplay: params.includeSensitiveDisplay, onManagedMediaPrepareError: params.onManagedMediaPrepareError, onSensitiveDisplayPrepareError: params.onSensitiveDisplayPrepareError }), mediaMessage }; } }); }, hasMedia ? params.abortSignal : void 0); } async function prepareWebchatReplyMediaForDisplay(params) { return withPreparedWebchatReplyMedia(params, async ({ payloads, inputsByIndex, buildContent }) => { const inputs = inputsByIndex.flat(); return { ...await buildContent(inputs), inputs, payloads }; }); } function resolveWebchatReplyWorkspace(params, placement) { const entry = params.sessionEntry; const remote = Boolean(entry?.execNode || entry?.repositoryWorkspaceId || isCloudWorkerPlacementState(placement?.state)); return { remote, workspaceDir: !remote && entry ? entry.sessionRoot ?? resolveSessionWorkspaceRoots(params.cfg, params.agentId, entry).root : resolveAgentWorkspaceDir(params.cfg, params.agentId) }; } function resolveWebchatReplyWorkspaceOnly(params) { const mode = params.sessionEntry?.permissionMode; return mode ? resolveSessionPermissionCoreToolPolicy({ mode }).workspaceOnly : resolveEffectiveToolFsWorkspaceOnly(params); } /** Trusted audio bypasses staging, but its reader must use the same session workspace. */ function getWebchatReplyMediaLocalRoots(params) { const { remote, workspaceDir } = params.workspace; if (remote) return [getMediaDir()]; const workspaceRoots = getAgentScopedMediaLocalRoots(params.cfg, params.agentId, workspaceDir); const roots = resolveWebchatReplyWorkspaceOnly(params) ? workspaceRoots : [.../* @__PURE__ */ new Set([...getAgentScopedMediaLocalRoots(params.cfg, params.agentId), ...workspaceRoots])]; return resolveAgentScopedOutboundMediaAccess({ cfg: params.cfg, agentId: params.agentId, sessionKey: params.sessionKey, accountId: params.accountId, workspaceDir, workspaceOnly: resolveWebchatReplyWorkspaceOnly(params), mediaAccess: { localRoots: appendLocalMediaParentRoots(roots, params.storePath ? [params.storePath] : void 0) }, ...resolveRequesterPolicyContext(params.requesterContext) }).localRoots ?? []; } function shouldPreserveDisplayMediaUrl(payload, mediaUrl) { return mediaUrl.trim().toLowerCase().startsWith("data:") || isAudioFileName(mediaUrl) && (isPassThroughRemoteMediaSource(mediaUrl) || payload.trustedLocalMedia === true); } /** Normalize reply media paths for webchat display without leaking sensitive media. */ async function normalizeWebchatReplyMediaPathsForDisplay(params) { return await withChannelReadAuthority(params.assertCurrent, async () => { if (params.payloads.every((payload) => payload.sensitiveMedia === true || resolveSendableOutboundReplyParts(payload).mediaUrls.every((url) => shouldPreserveDisplayMediaUrl(payload, url)))) return params.payloads; const { remote, workspaceDir } = params.workspace; if (!workspaceDir) return params.payloads; const assertCurrent = captureChannelReadAuthority(); const { createReplyMediaPathNormalizer } = await import("./reply-media-paths-DooFGbtd.mjs"); assertCurrent?.(); const workspaceOnly = resolveWebchatReplyWorkspaceOnly(params); const normalizeMediaPaths = createReplyMediaPathNormalizer({ cfg: params.cfg, sessionKey: params.sessionKey, agentId: params.agentId, workspaceDir, sessionWorkspaceDir: workspaceOnly && !remote ? workspaceDir : void 0, workspaceOnly, allowHostWorkspace: !remote, accountId: params.accountId, ...resolveRequesterPolicyContext(params.requesterContext), localMediaMaxBytes: WEBCHAT_LOCAL_MEDIA_MAX_BYTES }, params.readSource); const normalized = []; for (const payload of params.payloads) { if (payload.sensitiveMedia === true) { normalized.push(payload); continue; } const mediaUrls = resolveSendableOutboundReplyParts(payload).mediaUrls; if (!mediaUrls.some((mediaUrl) => shouldPreserveDisplayMediaUrl(payload, mediaUrl))) { normalized.push(await normalizeMediaPaths(payload)); continue; } if (!mediaUrls.some((mediaUrl) => !shouldPreserveDisplayMediaUrl(payload, mediaUrl))) { normalized.push(payload); continue; } const mergedMediaUrls = []; const mergedAttachments = []; const previousMediaFailures = getReplyPayloadMetadata(payload)?.assistantMediaFailures ?? []; const mediaFailures = [...previousMediaFailures]; let text = payload.text; for (const { url: mediaUrl, attachment } of collectReplyMediaEntries(payload, mediaUrls)) { if (shouldPreserveDisplayMediaUrl(payload, mediaUrl)) { mergedMediaUrls.push(mediaUrl); mergedAttachments.push(attachment ?? {}); continue; } const normalizedPayload = await normalizeMediaPaths(copyReplyPayloadMetadata(payload, { ...payload, text, mediaUrl, mediaUrls: [mediaUrl], attachments: attachment ? [attachment] : void 0 })); const normalizedMediaUrls = resolveSendableOutboundReplyParts(normalizedPayload).mediaUrls; mediaFailures.push(...(getReplyPayloadMetadata(normalizedPayload)?.assistantMediaFailures ?? []).slice(previousMediaFailures.length)); text = normalizedPayload.text; if (normalizedMediaUrls.length === 0) continue; mergedMediaUrls.push(...normalizedMediaUrls); mergedAttachments.push(...normalizedPayload.attachments ?? normalizedMediaUrls.map(() => ({}))); } const merged = copyReplyPayloadMetadata(payload, { ...payload, text, mediaUrl: mergedMediaUrls[0], mediaUrls: mergedMediaUrls, attachments: mergedAttachments }); normalized.push(mediaFailures.length > 0 ? setReplyPayloadMetadata(merged, { assistantMediaFailures: mediaFailures }) : merged); } return normalized; }); } //#endregion export { webchatReplyMediaAuthority as a, readChatSendReplyPayload as c, prepareWebchatReplyMediaForDisplay as i, replaceChatSendReplyPayload as l, getWebchatReplyMediaLocalRoots as n, withPreparedWebchatReplyMedia as o, normalizeWebchatReplyMediaPathsForDisplay as r, buildTranscriptReplyTextFromInputs as s, captureWebchatReplyMediaScope as t, selectChatSendFinalReplyInputs as u };