401 lines
12 KiB
TypeScript
401 lines
12 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_FILE_UPLOAD_PURPOSE = "openclaw-chat-file";
|
||
export const OPENCLAW_CHAT_FILE_MAX_BYTES = 2 * 1024 * 1024 * 1024;
|
||
export const OPENCLAW_CHAT_VIDEO_MAX_FILES = 2;
|
||
export const OPENCLAW_CHAT_VIDEO_MAX_BYTES = OPENCLAW_CHAT_FILE_MAX_BYTES;
|
||
export const OPENCLAW_CHAT_FILE_MAX_FILES = 10;
|
||
export const OPENCLAW_CHAT_FOLDER_MAX_ARCHIVES = 5;
|
||
export const OPENCLAW_CHAT_FOLDER_MAX_FILES = 2000;
|
||
export const OPENCLAW_CHAT_FOLDER_MAX_TOTAL_BYTES = OPENCLAW_CHAT_FILE_MAX_BYTES;
|
||
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";
|
||
export const OPENCLAW_CHAT_FILE_ACCEPT =
|
||
".zip,.rar,.7z,.tar,.gz,.tgz,.pdf,.txt,.md,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.csv,.tsv,.json,.xml,.mp3,.wav,.m4a,.aac,.flac,.ogg,.psd,.ai,.dxf,.dwg,.step,.stp,application/zip,application/x-zip-compressed,application/vnd.rar,application/x-rar-compressed,application/x-7z-compressed,application/x-tar,application/gzip,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/plain,text/csv,text/tab-separated-values,audio/mpeg,audio/wav,audio/mp4,audio/aac,audio/flac,audio/ogg";
|
||
|
||
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;
|
||
const OPENCLAW_FILE_EXTENSION_PATTERN =
|
||
/\.(zip|rar|7z|tar|gz|tgz|pdf|txt|md|docx?|pptx?|xlsx?|csv|tsv|json|xml|mp3|wav|m4a|aac|flac|ogg|psd|ai|dxf|dwg|step|stp|folder\.json)(?:$|[?#])/i;
|
||
const BLOCKED_INLINE_FILE_EXTENSION_PATTERN =
|
||
/\.(html?|xhtml|svg|js|mjs|cjs|exe|dll|msi|bat|cmd|com|scr|ps1|sh|jar|apk|dmg|pkg|deb|rpm)(?:$|[?#])/i;
|
||
const BLOCKED_INLINE_FILE_CONTENT_TYPES = new Set([
|
||
"application/java-archive",
|
||
"application/javascript",
|
||
"application/vnd.android.package-archive",
|
||
"application/x-apple-diskimage",
|
||
"application/x-dosexec",
|
||
"application/x-msdownload",
|
||
"application/x-msi",
|
||
"application/x-sh",
|
||
"application/xhtml+xml",
|
||
"image/svg+xml",
|
||
"text/html",
|
||
"text/javascript",
|
||
]);
|
||
|
||
const FILE_CONTENT_TYPE_BY_EXTENSION: Record<string, string> = {
|
||
"7z": "application/x-7z-compressed",
|
||
csv: "text/csv",
|
||
gz: "application/gzip",
|
||
rar: "application/vnd.rar",
|
||
tar: "application/x-tar",
|
||
tgz: "application/gzip",
|
||
tsv: "text/tab-separated-values",
|
||
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||
zip: "application/zip",
|
||
};
|
||
|
||
const GENERIC_FILE_CONTENT_TYPES = new Set([
|
||
"application/gzip",
|
||
"application/octet-stream",
|
||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||
"application/vnd.rar",
|
||
"application/vnd.yedir.folder-manifest+json",
|
||
"application/x-7z-compressed",
|
||
"application/x-rar-compressed",
|
||
"application/x-tar",
|
||
"application/x-zip-compressed",
|
||
"application/zip",
|
||
"text/csv",
|
||
"text/tab-separated-values",
|
||
]);
|
||
|
||
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 getOpenClawChatFileContentType(file: {
|
||
name: string;
|
||
type?: string | null;
|
||
}) {
|
||
const normalized = normalizeContentType(file.type);
|
||
if (
|
||
normalized.startsWith("image/") ||
|
||
normalized.startsWith("video/") ||
|
||
BLOCKED_INLINE_FILE_CONTENT_TYPES.has(normalized) ||
|
||
BLOCKED_INLINE_FILE_EXTENSION_PATTERN.test(file.name)
|
||
) {
|
||
return "";
|
||
}
|
||
if (
|
||
normalized &&
|
||
normalized !== "application/octet-stream" &&
|
||
!BLOCKED_INLINE_FILE_CONTENT_TYPES.has(normalized)
|
||
) {
|
||
return normalized;
|
||
}
|
||
if (file.name.toLowerCase().endsWith(".folder.json")) {
|
||
return "application/vnd.yedir.folder-manifest+json";
|
||
}
|
||
return (
|
||
FILE_CONTENT_TYPE_BY_EXTENSION[getExtension(file.name)] ||
|
||
(OPENCLAW_FILE_EXTENSION_PATTERN.test(file.name)
|
||
? "application/octet-stream"
|
||
: "")
|
||
);
|
||
}
|
||
|
||
export function isOpenClawChatFile(file: {
|
||
name: string;
|
||
type?: string | null;
|
||
}) {
|
||
return Boolean(getOpenClawChatFileContentType(file));
|
||
}
|
||
|
||
export function isOpenClawChatFolderFileAllowed(file: {
|
||
name: string;
|
||
type?: string | null;
|
||
}) {
|
||
const normalized = normalizeContentType(file.type);
|
||
return !(
|
||
BLOCKED_INLINE_FILE_CONTENT_TYPES.has(normalized) ||
|
||
BLOCKED_INLINE_FILE_EXTENSION_PATTERN.test(file.name)
|
||
);
|
||
}
|
||
|
||
export function isOpenClawChatFileAttachment(
|
||
url: string,
|
||
contentType?: string | null
|
||
) {
|
||
const normalizedContentType = normalizeContentType(contentType);
|
||
return (
|
||
GENERIC_FILE_CONTENT_TYPES.has(normalizedContentType) ||
|
||
(Boolean(normalizedContentType) &&
|
||
!normalizedContentType.startsWith("image/") &&
|
||
!normalizedContentType.startsWith("video/") &&
|
||
!BLOCKED_INLINE_FILE_CONTENT_TYPES.has(normalizedContentType)) ||
|
||
OPENCLAW_FILE_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 isFile =
|
||
!isVideo &&
|
||
(requestedType === "file" ||
|
||
requestedType === "archive" ||
|
||
isOpenClawChatFileAttachment(url, rawContentType));
|
||
|
||
if (isVideo && videoCount >= OPENCLAW_CHAT_VIDEO_MAX_FILES) {
|
||
continue;
|
||
}
|
||
if (isFile && archiveCount >= OPENCLAW_CHAT_FILE_MAX_FILES) {
|
||
continue;
|
||
}
|
||
if (result.length >= OPENCLAW_CHAT_MEDIA_MAX_ATTACHMENTS) {
|
||
break;
|
||
}
|
||
|
||
const name = String(raw.name || "")
|
||
.trim()
|
||
.slice(0, 200);
|
||
const type = isVideo ? "video" : isFile ? "file" : "image";
|
||
const fallbackContentType = isVideo
|
||
? VIDEO_CONTENT_TYPE_BY_EXTENSION[getExtension(url)] || "video/mp4"
|
||
: isFile
|
||
? getOpenClawChatFileContentType({ name: name || url, type: rawContentType }) ||
|
||
"application/octet-stream"
|
||
: "image/jpeg";
|
||
const contentType = isFile
|
||
? rawContentType || fallbackContentType
|
||
: rawContentType.startsWith(`${type}/`)
|
||
? rawContentType
|
||
: fallbackContentType;
|
||
result.push({
|
||
type,
|
||
url,
|
||
...(name ? { name } : {}),
|
||
content_type: contentType,
|
||
});
|
||
seenUrls.add(url);
|
||
if (isVideo) {
|
||
videoCount += 1;
|
||
}
|
||
if (isFile) {
|
||
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 buildOpenClawFileDisplayText(
|
||
attachments: OpenClawChatMediaAttachment[]
|
||
) {
|
||
const files = attachments.filter((attachment) => attachment.type === "file");
|
||
if (files.length === 0) {
|
||
return "";
|
||
}
|
||
|
||
const lines = files.map(
|
||
(attachment, index) => `- ${attachment.name || `文件附件${index + 1}`}`
|
||
);
|
||
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");
|
||
}
|
||
|
||
export function buildOpenClawFileReferenceText(
|
||
attachments: OpenClawChatMediaAttachment[]
|
||
) {
|
||
const files = attachments.filter((attachment) => attachment.type === "file");
|
||
if (files.length === 0) {
|
||
return "";
|
||
}
|
||
|
||
const lines = files.map(
|
||
(attachment, index) =>
|
||
`- ${attachment.name || `文件附件${index + 1}`}: ${attachment.url} (${attachment.content_type || "application/octet-stream"})`
|
||
);
|
||
return [
|
||
"用户上传了以下文件。表格请下载后读取,压缩包请下载并解压;application/vnd.yedir.folder-manifest+json 是保留目录结构和各文件下载地址的文件夹清单:",
|
||
lines.join("\n"),
|
||
].join("\n");
|
||
}
|