99 lines
3.0 KiB
TypeScript
99 lines
3.0 KiB
TypeScript
import path from "node:path";
|
|
import type {
|
|
AttachmentAnalysisState,
|
|
AttachmentKind,
|
|
MessageAttachment,
|
|
} from "@/lib/boss-data";
|
|
|
|
const LARGE_ATTACHMENT_THRESHOLD_BYTES = 20 * 1024 * 1024;
|
|
const MAX_ATTACHMENT_TEXT_EXCERPT_CHARS = 12_000;
|
|
|
|
function extensionOf(fileName: string) {
|
|
return path.extname(fileName).toLowerCase();
|
|
}
|
|
|
|
export function sanitizeFileName(fileName: string) {
|
|
const normalized = path
|
|
.basename(fileName || "attachment")
|
|
.normalize("NFKC")
|
|
.replace(/[\u0000-\u001f\u007f]/g, "")
|
|
.replace(/[<>:"|?*]+/g, "-")
|
|
.replace(/[\\/]+/g, "-")
|
|
.replace(/\s+/g, " ")
|
|
.trim()
|
|
.replace(/^\.+/, "");
|
|
return normalized || "attachment";
|
|
}
|
|
|
|
export function detectAttachmentKind(fileName: string, mimeType: string): AttachmentKind {
|
|
const normalizedMime = (mimeType || "").toLowerCase();
|
|
const ext = extensionOf(fileName);
|
|
|
|
if (normalizedMime.startsWith("image/")) return "image";
|
|
if (normalizedMime.startsWith("video/")) return "video";
|
|
if (normalizedMime === "application/pdf" || ext === ".pdf") return "pdf";
|
|
if (normalizedMime.startsWith("text/")) return "text";
|
|
if (
|
|
normalizedMime.includes("officedocument") ||
|
|
normalizedMime.includes("msword") ||
|
|
normalizedMime.includes("spreadsheet") ||
|
|
normalizedMime.includes("presentation") ||
|
|
[".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".odt", ".ods", ".odp"].includes(ext)
|
|
) {
|
|
return "office";
|
|
}
|
|
if (
|
|
[".txt", ".md", ".csv", ".log", ".json", ".yaml", ".yml", ".xml", ".html", ".htm"].includes(
|
|
ext,
|
|
)
|
|
) {
|
|
return "text";
|
|
}
|
|
return "binary";
|
|
}
|
|
|
|
export function resolveAttachmentAnalysisState(
|
|
kind: AttachmentKind,
|
|
fileSizeBytes: number,
|
|
): AttachmentAnalysisState {
|
|
if (fileSizeBytes > LARGE_ATTACHMENT_THRESHOLD_BYTES) {
|
|
return "ready_manual";
|
|
}
|
|
if (kind === "image" || kind === "pdf" || kind === "text") {
|
|
return "queued_auto";
|
|
}
|
|
return "ready_manual";
|
|
}
|
|
|
|
export function buildAttachmentDownloadHeaders(attachment: MessageAttachment) {
|
|
const safeName = sanitizeFileName(attachment.fileName);
|
|
const encodedName = encodeURIComponent(safeName);
|
|
return {
|
|
"Content-Type": attachment.mimeType || "application/octet-stream",
|
|
"Content-Disposition": `inline; filename="${safeName}"; filename*=UTF-8''${encodedName}`,
|
|
"Cache-Control": "private, no-store, max-age=0",
|
|
"X-Content-Type-Options": "nosniff",
|
|
};
|
|
}
|
|
|
|
export function canExtractAttachmentText(attachment: Pick<MessageAttachment, "attachmentKind" | "mimeType">) {
|
|
return (
|
|
attachment.attachmentKind === "text" ||
|
|
(attachment.mimeType || "").toLowerCase().startsWith("text/")
|
|
);
|
|
}
|
|
|
|
export function extractAttachmentTextExcerpt(buffer: Buffer | Uint8Array) {
|
|
const normalized = Buffer.from(buffer)
|
|
.toString("utf8")
|
|
.replace(/\u0000/g, "")
|
|
.trim();
|
|
if (!normalized) {
|
|
return "";
|
|
}
|
|
if (normalized.length <= MAX_ATTACHMENT_TEXT_EXCERPT_CHARS) {
|
|
return normalized;
|
|
}
|
|
return `${normalized.slice(0, MAX_ATTACHMENT_TEXT_EXCERPT_CHARS)}\n...[已截断]`;
|
|
}
|