258 lines
6.8 KiB
TypeScript
258 lines
6.8 KiB
TypeScript
export const OPENCLAW_CHAT_VIDEO_UPLOAD_PURPOSE = "openclaw-chat-video";
|
|
export const OPENCLAW_CHAT_FOLDER_UPLOAD_PURPOSE = "openclaw-chat-folder";
|
|
export const OPENCLAW_CHAT_VIDEO_MAX_FILES = 2;
|
|
export const OPENCLAW_CHAT_VIDEO_MAX_BYTES = 500 * 1024 * 1024;
|
|
export const OPENCLAW_CHAT_FOLDER_MAX_ARCHIVES = 5;
|
|
export const OPENCLAW_CHAT_FOLDER_MAX_FILES = 2000;
|
|
export const OPENCLAW_CHAT_FOLDER_MAX_TOTAL_BYTES = 300 * 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;
|
|
const ARCHIVE_EXTENSION_PATTERN = /\.zip(?:$|[?#])/i;
|
|
|
|
export type OpenClawChatMediaAttachment = {
|
|
type: "image" | "video" | "file";
|
|
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 isOpenClawChatArchiveAttachment(
|
|
url: string,
|
|
contentType?: string | null
|
|
) {
|
|
const normalizedContentType = normalizeContentType(contentType);
|
|
return (
|
|
normalizedContentType === "application/zip" ||
|
|
normalizedContentType === "application/x-zip-compressed" ||
|
|
ARCHIVE_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;
|
|
let archiveCount = 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);
|
|
const isArchive =
|
|
!isVideo &&
|
|
(requestedType === "file" ||
|
|
requestedType === "archive" ||
|
|
isOpenClawChatArchiveAttachment(url, rawContentType));
|
|
|
|
if (isVideo && videoCount >= OPENCLAW_CHAT_VIDEO_MAX_FILES) {
|
|
continue;
|
|
}
|
|
if (isArchive && archiveCount >= OPENCLAW_CHAT_FOLDER_MAX_ARCHIVES) {
|
|
continue;
|
|
}
|
|
if (result.length >= OPENCLAW_CHAT_MEDIA_MAX_ATTACHMENTS) {
|
|
break;
|
|
}
|
|
|
|
const type = isVideo ? "video" : isArchive ? "file" : "image";
|
|
const fallbackContentType = isVideo
|
|
? VIDEO_CONTENT_TYPE_BY_EXTENSION[getExtension(url)] || "video/mp4"
|
|
: isArchive
|
|
? "application/zip"
|
|
: "image/jpeg";
|
|
const contentType = isArchive
|
|
? rawContentType === "application/zip" ||
|
|
rawContentType === "application/x-zip-compressed"
|
|
? rawContentType
|
|
: fallbackContentType
|
|
: 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;
|
|
}
|
|
if (isArchive) {
|
|
archiveCount += 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");
|
|
}
|
|
|
|
export function buildOpenClawArchiveDisplayText(
|
|
attachments: OpenClawChatMediaAttachment[]
|
|
) {
|
|
const archives = attachments.filter(
|
|
(attachment) => attachment.type === "file"
|
|
);
|
|
if (archives.length === 0) {
|
|
return "";
|
|
}
|
|
|
|
const lines = archives.map(
|
|
(attachment, index) =>
|
|
`- ${attachment.name || `文件夹压缩包${index + 1}.zip`}`
|
|
);
|
|
return `已上传文件夹:\n${lines.join("\n")}`;
|
|
}
|
|
|
|
export function buildOpenClawArchiveReferenceText(
|
|
attachments: OpenClawChatMediaAttachment[]
|
|
) {
|
|
const archives = attachments.filter(
|
|
(attachment) => attachment.type === "file"
|
|
);
|
|
if (archives.length === 0) {
|
|
return "";
|
|
}
|
|
|
|
const lines = archives.map(
|
|
(attachment, index) =>
|
|
`- ${attachment.name || `文件夹压缩包${index + 1}.zip`}: ${attachment.url}`
|
|
);
|
|
return [
|
|
"用户上传了以下文件夹压缩包,压缩包保留原目录结构,请下载并解压后处理:",
|
|
lines.join("\n"),
|
|
].join("\n");
|
|
}
|