

























@@ -142,6 +142,19 @@ function isImageMime(mime?: string): boolean {
142142return typeof mime === "string" && mime.startsWith("image/");
143143}
144144145+function isVideoMime(mime?: string): boolean {
146+return typeof mime === "string" && mime.startsWith("video/");
147+}
148+149+function isGenericMime(mime?: string): boolean {
150+return (
151+!mime ||
152+mime === "application/octet-stream" ||
153+mime === "binary/octet-stream" ||
154+mime === "application/unknown"
155+);
156+}
157+145158function isValidBase64(value: string): boolean {
146159if (value.length === 0 || value.length % 4 !== 0) {
147160return false;
@@ -307,6 +320,7 @@ export async function parseMessageWithAttachments(
307320const offloadedRefs: OffloadedRef[] = [];
308321let updatedMessage = message;
309322const shouldForceOffload = opts?.supportsImages === false;
323+let textOnlyImageOffloadCount = 0;
310324311325// Track IDs of files saved during this request for cleanup if a later
312326// attachment fails validation and the entire parse is aborted.
@@ -344,15 +358,54 @@ export async function parseMessageWithAttachments(
344358const providedMime = normalizeMime(mime);
345359const sniffedMime = normalizeMime(await sniffMimeFromBase64(b64));
346360347-if (sniffedMime && !isImageMime(sniffedMime)) {
361+if (sniffedMime && !isImageMime(sniffedMime) && isImageMime(providedMime)) {
348362log?.warn(`attachment ${label}: detected non-image (${sniffedMime}), dropping`);
349363continue;
350364}
351-if (!sniffedMime && !isImageMime(providedMime)) {
352-log?.warn(`attachment ${label}: unable to detect image mime type, dropping`);
365+366+const shouldHandleAsImage =
367+isImageMime(sniffedMime) || (isImageMime(providedMime) && !sniffedMime);
368+if (!shouldHandleAsImage) {
369+const finalMime = sniffedMime ?? providedMime ?? "application/octet-stream";
370+if (isVideoMime(finalMime)) {
371+log?.warn(`attachment ${label}: video attachments are not supported, dropping`);
372+continue;
373+}
374+375+const buffer = Buffer.from(b64, "base64");
376+verifyDecodedSize(buffer, sizeBytes, label);
377+378+try {
379+const rawResult = await saveMediaBuffer(buffer, finalMime, "inbound", maxBytes, label);
380+const savedMedia = assertSavedMedia(rawResult, label);
381+savedMediaIds.push(savedMedia.id);
382+383+const mediaRef = `media://inbound/${savedMedia.id}`;
384+updatedMessage += `\n[media attached: ${mediaRef}]`;
385+log?.info?.(`[Gateway] Saved file attachment. Saved: ${mediaRef}`);
386+offloadedRefs.push({
387+ mediaRef,
388+id: savedMedia.id,
389+path: savedMedia.path ?? "",
390+mimeType: finalMime,
391+ label,
392+});
393+imageOrder.push("offloaded");
394+} catch (err) {
395+const errorMessage = formatErrorMessage(err);
396+throw new MediaOffloadError(
397+`[Gateway Error] Failed to save intercepted media to disk: ${errorMessage}`,
398+{ cause: err },
399+);
400+}
353401continue;
354402}
355-if (sniffedMime && providedMime && sniffedMime !== providedMime) {
403+if (
404+sniffedMime &&
405+providedMime &&
406+!isGenericMime(providedMime) &&
407+sniffedMime !== providedMime
408+) {
356409log?.warn(
357410`attachment ${label}: mime mismatch (${providedMime} -> ${sniffedMime}), using sniffed`,
358411);
@@ -364,7 +417,7 @@ export async function parseMessageWithAttachments(
364417365418let isOffloaded = false;
366419367-if (shouldForceOffload && offloadedRefs.length >= TEXT_ONLY_OFFLOAD_LIMIT) {
420+if (shouldForceOffload && textOnlyImageOffloadCount >= TEXT_ONLY_OFFLOAD_LIMIT) {
368421log?.warn(
369422`attachment ${label}: dropping image because text-only offload limit ` +
370423`${TEXT_ONLY_OFFLOAD_LIMIT} was reached`,
@@ -437,6 +490,9 @@ export async function parseMessageWithAttachments(
437490 label,
438491});
439492imageOrder.push("offloaded");
493+if (shouldForceOffload) {
494+textOnlyImageOffloadCount++;
495+}
440496441497isOffloaded = true;
442498} catch (err) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。