UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

293 lines (292 loc) • 14.4 kB
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs"; import { t as FsSafeError } from "./fs-safe-CS1DrPTK.mjs"; import { o as mediaKindFromMime } from "./constants-GzjKtFvA.mjs"; import { g as resolveSessionAgentId } from "./agent-scope-DWB4x52T.mjs"; import { r as logVerbose } from "./globals-D2Pr1KIl.mjs"; import { T as setReplyPayloadMetadata, a as copyReplyPayloadMetadata, n as appendReplyMediaFailures, s as getReplyPayloadMetadata } from "./reply-payload-DBtQZ6PK.mjs"; import { t as normalizeMediaReferenceForComparison } from "./media-reference-comparison-D61D3Ct7.mjs"; import { t as collectReplyMediaEntries } from "./reply-media-entries-DlpWge7g.mjs"; import { t as basenameFromAnyPath } from "./file-name-CKnWacTs.mjs"; import { u as mimeTypeFromFilePath } from "./mime-CGDYrRzL.mjs"; import { a as resolveSendableOutboundReplyParts } from "./reply-payload-parts-8DJKWSDo.mjs"; import "./reply-payload-DPu0E58h.mjs"; import { t as isPassThroughRemoteMediaSource } from "./media-source-url-BdNs7xa2.mjs"; import { c as resolveAllowedManagedMediaPath, f as resolveSandboxedMediaSource, t as assertMediaNotDataUrl } from "./sandbox-paths-IwSGCnhL.mjs"; import { i as resolveSandboxPathMapping, o as toRelativeWorkspacePath, r as resolvePathFromInput } from "./path-policy-o1nxUmmt.mjs"; import { r as resolveInboundMediaReference } from "./media-reference-Dfnbn2bP.mjs"; import { n as LocalMediaAccessError, t as HostReadMediaTypeError } from "./local-media-access-AyWsieKK.mjs"; import { t as resolveAgentScopedOutboundMediaAccess } from "./read-capability-CIuo3_Ri.mjs"; import { r as resolveOutboundMediaMaxBytes } from "./configured-max-bytes-X3NTOxEj.mjs"; import { n as resolveOutboundAttachmentFromUrl } from "./outbound-attachment-B9kI1l4M.mjs"; import { t as ensureSandboxWorkspaceForSession } from "./context-VCSXF-7p.mjs"; import "./sandbox-BMYK2iq5.mjs"; import path from "node:path"; import { sanitizeUntrustedFileName } from "@openclaw/fs-safe/advanced"; //#region src/auto-reply/reply/reply-media-paths.ts const FILE_URL_RE = /^file:/i; const WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\/]/; const SCHEME_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:/; const HAS_FILE_EXT_RE = /\.\w{1,10}$/; const MAX_FAILURE_LABEL_LENGTH = 180; function resolveReplyMediaFailureLabel(media, index) { const trimmed = media.trim(); let source = trimmed; if (SCHEME_RE.test(trimmed)) try { source = new URL(trimmed).pathname; } catch {} const basename = basenameFromAnyPath(source).trim(); const fallback = `Attachment ${index + 1}`; return truncateUtf16Safe(sanitizeUntrustedFileName(basename, fallback) || fallback, MAX_FAILURE_LABEL_LENGTH); } function resolveReplyMediaFailureKind(media) { const kind = mediaKindFromMime(mimeTypeFromFilePath(media)); return kind === "image" || kind === "audio" || kind === "video" ? kind : "document"; } function resolveReplyMediaFailureCode(error) { let current = error; for (let depth = 0; current instanceof Error && depth < 4; depth += 1) { if (current instanceof LocalMediaAccessError && current.code === "not-found" || current instanceof FsSafeError && current.code === "not-found") return "file-not-found"; if (current instanceof HostReadMediaTypeError || current instanceof LocalMediaAccessError && current.code === "unsupported-media-type") return "unsupported-format"; current = current.cause; } return "delivery-failed"; } function createReplyMediaFailure(media, index, error) { const mimeType = mimeTypeFromFilePath(media); return { code: resolveReplyMediaFailureCode(error), kind: resolveReplyMediaFailureKind(media), label: resolveReplyMediaFailureLabel(media, index), ...mimeType ? { mimeType } : {} }; } function isLikelyLocalMediaSource(media) { return FILE_URL_RE.test(media) || media.startsWith("/") || media.startsWith("./") || media.startsWith("../") || media.startsWith("~") || WINDOWS_DRIVE_RE.test(media) || media.startsWith("\\\\") || !SCHEME_RE.test(media) && (media.includes("/") || media.includes("\\") || HAS_FILE_EXT_RE.test(media)); } function createReplyMediaSourcePreparer(params) { const agentId = params.agentId ?? (params.sessionKey ? resolveSessionAgentId({ sessionKey: params.sessionKey, config: params.cfg }) : void 0); const maxBytes = resolveOutboundMediaMaxBytes({ cfg: params.cfg, channel: params.messageProvider, accountId: params.accountId }); const explicitSandboxRoot = params.sandboxRoot?.trim(); let sandboxWorkspacePromise = explicitSandboxRoot ? Promise.resolve({ root: explicitSandboxRoot, containerWorkdir: params.sandboxContainerWorkdir, workspaceAccess: params.workspaceMediaAccess?.readFile ? "ro" : "none" }) : void 0; const persistedMediaBySource = /* @__PURE__ */ new Map(); const resolveSandboxWorkspace = async () => { if (!sandboxWorkspacePromise) sandboxWorkspacePromise = ensureSandboxWorkspaceForSession({ config: params.cfg, agentId, sessionKey: params.sessionKey, workspaceDir: params.workspaceDir }).then((sandbox) => sandbox ? { root: sandbox.workspaceDir, containerWorkdir: sandbox.containerWorkdir, workspaceAccess: sandbox.workspaceAccess ?? "none" } : void 0); return await sandboxWorkspacePromise; }; const resolveMediaAccessForSource = (media, sessionWorkspaceDir, workspaceDir) => resolveAgentScopedOutboundMediaAccess({ cfg: params.cfg, agentId, workspaceDir: workspaceDir ?? params.workspaceDir, sessionWorkspaceDir: sessionWorkspaceDir ?? params.sessionWorkspaceDir, workspaceOnly: params.workspaceOnly, allowHostWorkspace: params.allowHostWorkspace, mediaSources: [media], mediaAccess: params.mediaAccess, workspaceMediaAccess: params.workspaceMediaAccess, sessionKey: params.sessionKey, messageProvider: params.sessionKey ? void 0 : params.messageProvider, accountId: params.accountId, requesterSenderId: params.requesterSenderId, requesterSenderName: params.requesterSenderName, requesterSenderUsername: params.requesterSenderUsername, requesterSenderE164: params.requesterSenderE164, groupId: params.groupId, groupChannel: params.groupChannel, groupSpace: params.groupSpace }); const persistLocalReplyMedia = async (media, sessionWorkspaceDir, workspaceDir) => { if (!isLikelyLocalMediaSource(media)) return { path: media }; const managedMediaPath = await resolveAllowedManagedMediaPath(media); if (managedMediaPath) return { path: managedMediaPath, contentType: mimeTypeFromFilePath(managedMediaPath) }; const cached = persistedMediaBySource.get(media); if (cached) return await cached; const persistPromise = resolveOutboundAttachmentFromUrl(media, maxBytes, { mediaAccess: resolveMediaAccessForSource(media, sessionWorkspaceDir, workspaceDir) }).then((saved) => ({ ...saved, contentType: saved.contentType ?? mimeTypeFromFilePath(media) ?? "application/octet-stream" })).catch((err) => { persistedMediaBySource.delete(media); throw err; }); persistedMediaBySource.set(media, persistPromise); return await persistPromise; }; const prepareLocalReplyMedia = async (media, sessionWorkspaceDir, workspaceDir) => { const persisted = await persistLocalReplyMedia(media, sessionWorkspaceDir, workspaceDir); return { mediaUrl: persisted.path, trustedLocalMedia: true, fileName: path.basename(media), ...persisted.contentType ? { mimeType: persisted.contentType } : {} }; }; const resolveWorkspaceRelativeMedia = (media) => { const relativeWorkspacePath = toRelativeWorkspacePath(params.workspaceDir, media, { cwd: params.workspaceDir }); return resolvePathFromInput(relativeWorkspacePath, params.workspaceDir); }; const resolveAbsoluteWorkspaceMedia = (media) => { if (FILE_URL_RE.test(media) || !path.isAbsolute(media) && !WINDOWS_DRIVE_RE.test(media)) return; try { return resolveWorkspaceRelativeMedia(media); } catch { return; } }; const normalizeMediaSource = async (raw) => { const trimmed = raw.trim(); const source = /^media:\/\//i.test(trimmed) ? (await resolveInboundMediaReference(trimmed))?.physicalPath ?? trimmed : trimmed; const media = (params.workspaceMediaRoot ? resolveSandboxPathMapping([{ hostRoot: params.workspaceDir, containerRoot: params.workspaceMediaRoot }], source) : null)?.hostPath ?? source; if (!media) return { mediaUrl: media, trustedLocalMedia: false }; assertMediaNotDataUrl(media); if (isPassThroughRemoteMediaSource(media)) return { mediaUrl: media, trustedLocalMedia: false }; const sandboxWorkspace = await resolveSandboxWorkspace(); const workspaceMounted = !sandboxWorkspace || sandboxWorkspace.workspaceAccess !== "none"; const absoluteWorkspaceMedia = workspaceMounted ? resolveAbsoluteWorkspaceMedia(media) : void 0; if (absoluteWorkspaceMedia) return prepareLocalReplyMedia(absoluteWorkspaceMedia); const isRelativeLocalMedia = isLikelyLocalMediaSource(media) && !FILE_URL_RE.test(media) && !media.startsWith("~") && !path.isAbsolute(media) && !WINDOWS_DRIVE_RE.test(media); const useRemoteWorkspace = workspaceMounted && isRelativeLocalMedia && params.workspaceMediaRoot && params.workspaceMediaAccess?.readFile; if (sandboxWorkspace && !useRemoteWorkspace) { let sandboxResolvedMedia; try { sandboxResolvedMedia = await resolveSandboxedMediaSource({ media, sandboxRoot: sandboxWorkspace.root, containerWorkdir: sandboxWorkspace.containerWorkdir }); } catch (err) { if (FILE_URL_RE.test(media)) throw new Error("Host-local MEDIA file URLs are blocked in normal replies. Use a safe path or the message tool.", { cause: err }); throw err; } return prepareLocalReplyMedia(sandboxResolvedMedia, sandboxWorkspace.root, workspaceMounted ? void 0 : sandboxWorkspace.root); } if (isRelativeLocalMedia) return prepareLocalReplyMedia(resolveWorkspaceRelativeMedia(media)); if (!isLikelyLocalMediaSource(media)) return { mediaUrl: media, trustedLocalMedia: false }; if (FILE_URL_RE.test(media)) throw new Error("Host-local MEDIA file URLs are blocked in normal replies. Use a safe path or the message tool."); return prepareLocalReplyMedia(media); }; return async (sources) => { const prepared = []; for (const [index, source] of sources.entries()) try { prepared.push({ source, outcome: await normalizeMediaSource(source) }); } catch (error) { prepared.push({ source, outcome: { failure: createReplyMediaFailure(source, index, error) } }); logVerbose(`dropping blocked reply media ${source}: ${String(error)}`); } return prepared; }; } /** Applies already-prepared bytes after final answer selection, without reopening files. */ function applyPreparedReplyMedia(payload, prepared) { const mediaList = resolveSendableOutboundReplyParts(payload).mediaUrls; if (!mediaList.length || !prepared.length) return payload; const bySource = new Map(prepared.map(({ source, outcome }) => [source, outcome])); const normalizedMedia = []; const normalizedAttachments = []; const previousSourceUrls = getReplyPayloadMetadata(payload)?.replyMediaSourceUrls; const sourcesByReference = /* @__PURE__ */ new Map(); const seen = /* @__PURE__ */ new Set(); let hasTrustedLocalMedia = payload.trustedLocalMedia === true; const mediaFailures = []; const mediaEntries = collectReplyMediaEntries(payload, mediaList); for (const { url: media, attachment } of mediaEntries) { const normalized = bySource.get(media) ?? { mediaUrl: media, trustedLocalMedia: false }; if ("failure" in normalized) { mediaFailures.push(normalized.failure); continue; } if (!normalized.mediaUrl) continue; const normalizedKey = normalizeMediaReferenceForComparison(normalized.mediaUrl); const sourceKey = normalizeMediaReferenceForComparison(media); const sourceUrls = sourcesByReference.get(normalizedKey) ?? /* @__PURE__ */ new Set(); for (const source of previousSourceUrls?.get(sourceKey) ?? []) sourceUrls.add(source); if (normalized.mediaUrl !== media.trim()) sourceUrls.add(media.trim()); if (sourceUrls.size > 0) sourcesByReference.set(normalizedKey, sourceUrls); if (seen.has(normalized.mediaUrl)) continue; seen.add(normalized.mediaUrl); normalizedMedia.push(normalized.mediaUrl); hasTrustedLocalMedia ||= normalized.trustedLocalMedia; const existingAttachment = attachment ?? {}; const normalizedAttachment = { ...existingAttachment, ...normalized.fileName && !existingAttachment.name ? { name: normalized.fileName } : {}, ...normalized.mimeType && !existingAttachment.mimeType ? { mimeType: normalized.mimeType } : {}, ...normalized.trustedLocalMedia ? { trustedLocalMedia: true } : {} }; if (normalized.mediaUrl !== media) { for (const field of [ "path", "url", "mediaUrl", "filePath" ]) if (normalizedAttachment[field] !== void 0) normalizedAttachment[field] = normalized.mediaUrl; } normalizedAttachments.push(normalizedAttachment); } const replyMediaSourceUrls = sourcesByReference.size > 0 ? new Map([...sourcesByReference].map(([key, sources]) => [key, [...sources]])) : void 0; const text = appendReplyMediaFailures(payload.text, mediaFailures); const assistantMediaFailures = [...getReplyPayloadMetadata(payload)?.assistantMediaFailures ?? [], ...mediaFailures]; const normalized = copyReplyPayloadMetadata(payload, { ...payload, text, mediaUrl: normalizedMedia[0], mediaUrls: normalizedMedia.length > 0 ? normalizedMedia : void 0, attachments: normalizedAttachments.some((attachment) => Object.keys(attachment).length > 0) ? normalizedAttachments : void 0, ...normalizedMedia.length > 0 && hasTrustedLocalMedia ? { trustedLocalMedia: true } : {} }); return setReplyPayloadMetadata(normalized, { replyMediaSourceUrls, ...mediaFailures.length > 0 ? { assistantMediaFailures } : {} }); } function createReplyMediaPathNormalizer(params) { const prepare = createReplyMediaSourcePreparer(params); return async (payload) => applyPreparedReplyMedia(payload, await prepare(resolveSendableOutboundReplyParts(payload).mediaUrls)); } function createReplyMediaContext(params) { return { normalizePayload: params.mediaNormalizationOwner === "gateway" ? async (payload) => payload : createReplyMediaPathNormalizer(params) }; } //#endregion export { createReplyMediaSourcePreparer as i, createReplyMediaContext as n, createReplyMediaPathNormalizer as r, applyPreparedReplyMedia as t };