import { resolvePathFromInput } from "../../agents/path-policy.js"; import { assertMediaNotDataUrl, resolveSandboxedMediaSource } from "../../agents/sandbox-paths.js"; import { ensureSandboxWorkspaceForSession } from "../../agents/sandbox.js"; import type { OpenClawConfig } from "../../config/config.js"; import { resolveSendableOutboundReplyParts } from "../../plugin-sdk/reply-payload.js"; import type { ReplyPayload } from "../types.js"; const HTTP_URL_RE = /^https?:\/\//i; 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}$/; function isLikelyLocalMediaSource(media: string): boolean { 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 getPayloadMediaList(payload: ReplyPayload): string[] { return resolveSendableOutboundReplyParts(payload).mediaUrls; } export function createReplyMediaPathNormalizer(params: { cfg: OpenClawConfig; sessionKey?: string; workspaceDir: string; }): (payload: ReplyPayload) => Promise { let sandboxRootPromise: Promise | undefined; const resolveSandboxRoot = async (): Promise => { if (!sandboxRootPromise) { sandboxRootPromise = ensureSandboxWorkspaceForSession({ config: params.cfg, sessionKey: params.sessionKey, workspaceDir: params.workspaceDir, }).then((sandbox) => sandbox?.workspaceDir); } return await sandboxRootPromise; }; const normalizeMediaSource = async (raw: string): Promise => { const media = raw.trim(); if (!media) { return media; } assertMediaNotDataUrl(media); if (HTTP_URL_RE.test(media)) { return media; } const sandboxRoot = await resolveSandboxRoot(); if (sandboxRoot) { return await resolveSandboxedMediaSource({ media, sandboxRoot, }); } if (!isLikelyLocalMediaSource(media)) { return media; } if (FILE_URL_RE.test(media)) { return media; } return resolvePathFromInput(media, params.workspaceDir); }; return async (payload) => { const mediaList = getPayloadMediaList(payload); if (mediaList.length === 0) { return payload; } const normalizedMedia: string[] = []; const seen = new Set(); for (const media of mediaList) { const normalized = await normalizeMediaSource(media); if (!normalized || seen.has(normalized)) { continue; } seen.add(normalized); normalizedMedia.push(normalized); } if (normalizedMedia.length === 0) { return { ...payload, mediaUrl: undefined, mediaUrls: undefined, }; } return { ...payload, mediaUrl: normalizedMedia[0], mediaUrls: normalizedMedia, }; }; }