export const OPENCLAW_CHAT_VIDEO_UPLOAD_PURPOSE = "openclaw-chat-video"; export const OPENCLAW_CHAT_VIDEO_MAX_FILES = 2; export const OPENCLAW_CHAT_VIDEO_MAX_BYTES = 500 * 1024 * 1024; export const OPENCLAW_CHAT_MEDIA_MAX_ATTACHMENTS = 16; export const OPENCLAW_CHAT_VIDEO_ACCEPT = "video/mp4,video/quicktime,video/webm,video/x-m4v,video/x-matroska,.mp4,.mov,.webm,.m4v,.mkv"; const VIDEO_CONTENT_TYPES = new Set([ "video/mp4", "video/quicktime", "video/webm", "video/x-m4v", "video/x-matroska", ]); const VIDEO_CONTENT_TYPE_BY_EXTENSION: Record = { m4v: "video/x-m4v", mkv: "video/x-matroska", mov: "video/quicktime", mp4: "video/mp4", webm: "video/webm", }; const VIDEO_EXTENSION_PATTERN = /\.(mp4|mov|webm|m4v|mkv)(?:$|[?#])/i; export type OpenClawChatMediaAttachment = { type: "image" | "video"; url: string; name?: string; content_type?: string; }; type OpenClawChatAttachmentLike = { type?: unknown; url?: unknown; name?: unknown; contentType?: unknown; content_type?: unknown; }; type OpenClawChatVideoFileLike = { name: string; type: string; }; function normalizeContentType(value: unknown) { return String(value || "") .split(";", 1)[0] .trim() .toLowerCase(); } function getExtension(value: string) { const cleanValue = String(value || "").split(/[?#]/, 1)[0]; return cleanValue.split(".").pop()?.trim().toLowerCase() || ""; } function isHttpUrl(value: string) { try { const parsed = new URL(value); return parsed.protocol === "http:" || parsed.protocol === "https:"; } catch { return false; } } export function getOpenClawChatVideoContentType( file: OpenClawChatVideoFileLike ) { const contentType = normalizeContentType(file.type); if (VIDEO_CONTENT_TYPES.has(contentType)) { return contentType; } return VIDEO_CONTENT_TYPE_BY_EXTENSION[getExtension(file.name)] || ""; } export function isOpenClawChatVideoFile(file: OpenClawChatVideoFileLike) { return Boolean(getOpenClawChatVideoContentType(file)); } export function isOpenClawChatVideoAttachment( url: string, contentType?: string | null ) { return ( normalizeContentType(contentType).startsWith("video/") || VIDEO_EXTENSION_PATTERN.test(String(url || "")) ); } export function normalizeOpenClawChatMediaAttachments( value: unknown ): OpenClawChatMediaAttachment[] { if (!Array.isArray(value)) { return []; } const result: OpenClawChatMediaAttachment[] = []; const seenUrls = new Set(); let videoCount = 0; for (const item of value) { if (!item || typeof item !== "object") { continue; } const raw = item as OpenClawChatAttachmentLike; const url = String(raw.url || "").trim(); if (!url || !isHttpUrl(url) || seenUrls.has(url)) { continue; } const requestedType = String(raw.type || "").trim().toLowerCase(); const rawContentType = normalizeContentType( raw.content_type || raw.contentType ); const isVideo = requestedType === "video" || isOpenClawChatVideoAttachment(url, rawContentType); if (isVideo && videoCount >= OPENCLAW_CHAT_VIDEO_MAX_FILES) { continue; } if (result.length >= OPENCLAW_CHAT_MEDIA_MAX_ATTACHMENTS) { break; } const type = isVideo ? "video" : "image"; const fallbackContentType = isVideo ? VIDEO_CONTENT_TYPE_BY_EXTENSION[getExtension(url)] || "video/mp4" : "image/jpeg"; const contentType = rawContentType.startsWith(`${type}/`) ? rawContentType : fallbackContentType; const name = String(raw.name || "") .trim() .slice(0, 200); result.push({ type, url, ...(name ? { name } : {}), content_type: contentType, }); seenUrls.add(url); if (isVideo) { videoCount += 1; } } return result; } export function buildOpenClawVideoDisplayText( attachments: OpenClawChatMediaAttachment[] ) { const videos = attachments.filter((attachment) => attachment.type === "video"); if (videos.length === 0) { return ""; } const lines = videos.map( (attachment, index) => `- ${attachment.name || `视频${index + 1}`}` ); return `已上传视频:\n${lines.join("\n")}`; } export function buildOpenClawVideoReferenceText( attachments: OpenClawChatMediaAttachment[] ) { const videos = attachments.filter((attachment) => attachment.type === "video"); if (videos.length === 0) { return ""; } const lines = videos.map( (attachment, index) => `- ${attachment.name || `视频${index + 1}`}: ${attachment.url}` ); return [ "用户上传了以下视频,请调用视频理解能力并按视频文件地址处理:", lines.join("\n"), ].join("\n"); }