fix: support Claw AVIF and TIFF uploads
This commit is contained in:
parent
50f34e7a0f
commit
06a78a289a
@ -14,6 +14,9 @@ const ALLOWED_UPLOAD_IMAGE_TYPES = new Set([
|
|||||||
"image/jpg",
|
"image/jpg",
|
||||||
"image/png",
|
"image/png",
|
||||||
"image/webp",
|
"image/webp",
|
||||||
|
"image/avif",
|
||||||
|
"image/tiff",
|
||||||
|
"image/x-tiff",
|
||||||
"image/svg+xml",
|
"image/svg+xml",
|
||||||
"image/heic",
|
"image/heic",
|
||||||
"image/heif",
|
"image/heif",
|
||||||
@ -25,6 +28,11 @@ const ALLOWED_ATLASCLOUD_VIDEO_TYPES = new Set([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const HEIC_UPLOAD_TYPES = new Set(["image/heic", "image/heif"]);
|
const HEIC_UPLOAD_TYPES = new Set(["image/heic", "image/heif"]);
|
||||||
|
const SHARP_COMPATIBILITY_UPLOAD_TYPES = new Set([
|
||||||
|
"image/avif",
|
||||||
|
"image/tiff",
|
||||||
|
"image/x-tiff",
|
||||||
|
]);
|
||||||
|
|
||||||
function normalizeUploadContentType(contentType: string, filename: string) {
|
function normalizeUploadContentType(contentType: string, filename: string) {
|
||||||
const normalized = contentType.trim().toLowerCase();
|
const normalized = contentType.trim().toLowerCase();
|
||||||
@ -40,8 +48,16 @@ function createUnsupportedImageTypeMessage(contentType: string, filename: string
|
|||||||
if (contentType === "image/heic" || contentType === "image/heif" || extension === "heic" || extension === "heif") {
|
if (contentType === "image/heic" || contentType === "image/heif" || extension === "heic" || extension === "heif") {
|
||||||
return "HEIC/HEIF 图片转码失败,请在手机相册中导出为 JPG/PNG 后再上传。";
|
return "HEIC/HEIF 图片转码失败,请在手机相册中导出为 JPG/PNG 后再上传。";
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
SHARP_COMPATIBILITY_UPLOAD_TYPES.has(contentType) ||
|
||||||
|
extension === "avif" ||
|
||||||
|
extension === "tif" ||
|
||||||
|
extension === "tiff"
|
||||||
|
) {
|
||||||
|
return "AVIF/TIFF 图片转码失败,请导出为 JPG/PNG 后再上传。";
|
||||||
|
}
|
||||||
|
|
||||||
return "File type should be JPEG, PNG, WebP or SVG";
|
return "图片格式不支持,请上传 JPG、PNG、WebP、HEIC、AVIF 或 TIFF 图片。";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isHeicUpload(contentType: string, filename: string) {
|
function isHeicUpload(contentType: string, filename: string) {
|
||||||
@ -53,10 +69,20 @@ function isHeicUpload(contentType: string, filename: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSharpCompatibilityUpload(contentType: string, filename: string) {
|
||||||
|
const extension = filename.toLowerCase().split(".").pop() || "";
|
||||||
|
return (
|
||||||
|
SHARP_COMPATIBILITY_UPLOAD_TYPES.has(contentType) ||
|
||||||
|
extension === "avif" ||
|
||||||
|
extension === "tif" ||
|
||||||
|
extension === "tiff"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function toJpegFilename(filename: string) {
|
function toJpegFilename(filename: string) {
|
||||||
const trimmed = filename.trim() || `upload-${Date.now()}`;
|
const trimmed = filename.trim() || `upload-${Date.now()}`;
|
||||||
if (/\.(heic|heif)$/i.test(trimmed)) {
|
if (/\.(avif|heic|heif|tif|tiff)$/i.test(trimmed)) {
|
||||||
return trimmed.replace(/\.(heic|heif)$/i, ".jpg");
|
return trimmed.replace(/\.(avif|heic|heif|tif|tiff)$/i, ".jpg");
|
||||||
}
|
}
|
||||||
if (/\.[a-z0-9]+$/i.test(trimmed)) {
|
if (/\.[a-z0-9]+$/i.test(trimmed)) {
|
||||||
return trimmed.replace(/\.[a-z0-9]+$/i, ".jpg");
|
return trimmed.replace(/\.[a-z0-9]+$/i, ".jpg");
|
||||||
@ -76,6 +102,21 @@ async function convertHeicToJpeg(buffer: Buffer): Promise<Buffer> {
|
|||||||
}
|
}
|
||||||
return Buffer.from(output);
|
return Buffer.from(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function convertSharpCompatibilityImageToJpeg(
|
||||||
|
buffer: Buffer
|
||||||
|
): Promise<Buffer> {
|
||||||
|
const sharp = (await import("sharp")).default;
|
||||||
|
return sharp(buffer, { animated: false, failOn: "none" })
|
||||||
|
.rotate()
|
||||||
|
.flatten({ background: "#ffffff" })
|
||||||
|
.jpeg({
|
||||||
|
quality: 92,
|
||||||
|
mozjpeg: true,
|
||||||
|
progressive: true,
|
||||||
|
})
|
||||||
|
.toBuffer();
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 使用Sharp智能优化图片(保持原格式)
|
* 使用Sharp智能优化图片(保持原格式)
|
||||||
* 策略:避免双重压缩,只在需要时优化
|
* 策略:避免双重压缩,只在需要时优化
|
||||||
@ -288,6 +329,38 @@ export async function POST(request: Request) {
|
|||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if (isSharpCompatibilityUpload(contentType, filename)) {
|
||||||
|
try {
|
||||||
|
const convertedBuffer =
|
||||||
|
await convertSharpCompatibilityImageToJpeg(uploadBuffer);
|
||||||
|
logger.info("[Upload API] Compatibility image converted to JPEG", {
|
||||||
|
clientUploadId,
|
||||||
|
originalContentType: contentType,
|
||||||
|
originalSize: uploadBuffer.length,
|
||||||
|
convertedSize: convertedBuffer.length,
|
||||||
|
});
|
||||||
|
uploadBuffer = convertedBuffer;
|
||||||
|
filename = toJpegFilename(filename);
|
||||||
|
convertedFrom = contentType;
|
||||||
|
contentType = "image/jpeg";
|
||||||
|
} catch (conversionError) {
|
||||||
|
logger.warn("[Upload API] Compatibility image conversion failed", {
|
||||||
|
clientUploadId,
|
||||||
|
contentType,
|
||||||
|
size: uploadBuffer.length,
|
||||||
|
error:
|
||||||
|
conversionError instanceof Error
|
||||||
|
? conversionError.message
|
||||||
|
: String(conversionError),
|
||||||
|
});
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: createUnsupportedImageTypeMessage(contentType, filename),
|
||||||
|
clientUploadId,
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uploadPurpose === "agent-workflow-preview" && contentType !== "image/svg+xml") {
|
if (uploadPurpose === "agent-workflow-preview" && contentType !== "image/svg+xml") {
|
||||||
|
|||||||
@ -415,6 +415,19 @@ function isClawdChatMessage(message: unknown) {
|
|||||||
const CLAWD_TABLE_ACCEPT =
|
const CLAWD_TABLE_ACCEPT =
|
||||||
".xlsx,.csv,.tsv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv,text/tab-separated-values";
|
".xlsx,.csv,.tsv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv,text/tab-separated-values";
|
||||||
const CLAWD_TABLE_ALLOWED_EXTENSIONS = new Set(["xlsx", "csv", "tsv"]);
|
const CLAWD_TABLE_ALLOWED_EXTENSIONS = new Set(["xlsx", "csv", "tsv"]);
|
||||||
|
const CLAWD_IMAGE_ACCEPT =
|
||||||
|
".jpg,.jpeg,.png,.webp,.heic,.heif,.avif,.tif,.tiff,image/jpeg,image/png,image/webp,image/heic,image/heif,image/avif,image/tiff";
|
||||||
|
const CLAWD_IMAGE_ALLOWED_EXTENSIONS = new Set([
|
||||||
|
"jpg",
|
||||||
|
"jpeg",
|
||||||
|
"png",
|
||||||
|
"webp",
|
||||||
|
"heic",
|
||||||
|
"heif",
|
||||||
|
"avif",
|
||||||
|
"tif",
|
||||||
|
"tiff",
|
||||||
|
]);
|
||||||
const CLAWD_TABLE_MAX_FILES = 3;
|
const CLAWD_TABLE_MAX_FILES = 3;
|
||||||
const CLAWD_TABLE_MAX_BYTES = 10 * 1024 * 1024;
|
const CLAWD_TABLE_MAX_BYTES = 10 * 1024 * 1024;
|
||||||
const CLAWD_PROCESSING_HINT =
|
const CLAWD_PROCESSING_HINT =
|
||||||
@ -444,6 +457,13 @@ function isClawdTableFile(file: File) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isClawdImageFile(file: File) {
|
||||||
|
return (
|
||||||
|
file.type.startsWith("image/") ||
|
||||||
|
CLAWD_IMAGE_ALLOWED_EXTENSIONS.has(getClawdTableFileExtension(file.name))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeClawdComposerText(value: string) {
|
function normalizeClawdComposerText(value: string) {
|
||||||
return String(value || "")
|
return String(value || "")
|
||||||
.replace(/\u200b/g, " ")
|
.replace(/\u200b/g, " ")
|
||||||
@ -11906,7 +11926,7 @@ export function CanvasChatPanel({
|
|||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
} | null> => {
|
} | null> => {
|
||||||
if (!file.type.startsWith("image/")) {
|
if (!isClawdImageFile(file)) {
|
||||||
toast.error("请选择图片文件");
|
toast.error("请选择图片文件");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -11936,7 +11956,15 @@ export function CanvasChatPanel({
|
|||||||
}
|
}
|
||||||
: undefined);
|
: undefined);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("上传失败");
|
const payload = (await response.json().catch(() => null)) as {
|
||||||
|
error?: string;
|
||||||
|
details?: string;
|
||||||
|
} | null;
|
||||||
|
throw new Error(
|
||||||
|
payload?.error?.trim() ||
|
||||||
|
payload?.details?.trim() ||
|
||||||
|
"图片上传失败"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@ -11954,7 +11982,11 @@ export function CanvasChatPanel({
|
|||||||
assetId,
|
assetId,
|
||||||
};
|
};
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error("图片上传失败");
|
toast.error(
|
||||||
|
_error instanceof Error && _error.message
|
||||||
|
? _error.message
|
||||||
|
: "图片上传失败"
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, [clearComposerUploadProgress, updateComposerUploadProgress]);
|
}, [clearComposerUploadProgress, updateComposerUploadProgress]);
|
||||||
@ -12289,11 +12321,9 @@ export function CanvasChatPanel({
|
|||||||
|
|
||||||
if (isClawdMode) {
|
if (isClawdMode) {
|
||||||
const tableFiles = selectedFiles.filter((file) => isClawdTableFile(file));
|
const tableFiles = selectedFiles.filter((file) => isClawdTableFile(file));
|
||||||
const imageFiles = selectedFiles.filter((file) =>
|
const imageFiles = selectedFiles.filter((file) => isClawdImageFile(file));
|
||||||
file.type.startsWith("image/")
|
|
||||||
);
|
|
||||||
const unsupportedFiles = selectedFiles.filter(
|
const unsupportedFiles = selectedFiles.filter(
|
||||||
(file) => !isClawdTableFile(file) && !file.type.startsWith("image/")
|
(file) => !isClawdTableFile(file) && !isClawdImageFile(file)
|
||||||
);
|
);
|
||||||
|
|
||||||
unsupportedFiles.forEach((file) => {
|
unsupportedFiles.forEach((file) => {
|
||||||
@ -16095,7 +16125,7 @@ export function CanvasChatPanel({
|
|||||||
type="file"
|
type="file"
|
||||||
accept={
|
accept={
|
||||||
isClawdMode
|
isClawdMode
|
||||||
? `image/*,${CLAWD_TABLE_ACCEPT}`
|
? `${CLAWD_IMAGE_ACCEPT},${CLAWD_TABLE_ACCEPT}`
|
||||||
: supportsAtlasVideoReference
|
: supportsAtlasVideoReference
|
||||||
? "image/*,video/mp4,video/quicktime,video/webm"
|
? "image/*,video/mp4,video/quicktime,video/webm"
|
||||||
: "image/*"
|
: "image/*"
|
||||||
|
|||||||
@ -109,6 +109,11 @@ export function getContentType(filename: string): string {
|
|||||||
png: "image/png",
|
png: "image/png",
|
||||||
gif: "image/gif",
|
gif: "image/gif",
|
||||||
webp: "image/webp",
|
webp: "image/webp",
|
||||||
|
avif: "image/avif",
|
||||||
|
tif: "image/tiff",
|
||||||
|
tiff: "image/tiff",
|
||||||
|
heic: "image/heic",
|
||||||
|
heif: "image/heif",
|
||||||
svg: "image/svg+xml",
|
svg: "image/svg+xml",
|
||||||
psd: "image/vnd.adobe.photoshop",
|
psd: "image/vnd.adobe.photoshop",
|
||||||
psb: "application/octet-stream",
|
psb: "application/octet-stream",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user