ai-chatbot/lib/openclaw-chat/media-attachments.ts

184 lines
4.6 KiB
TypeScript

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<string, string> = {
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<string>();
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");
}