refactor: extract canvas upload validation helpers

This commit is contained in:
Your Name 2026-05-27 10:45:26 +08:00
parent 9821832364
commit 593b7e3df0
3 changed files with 143 additions and 52 deletions

View File

@ -168,6 +168,12 @@ import {
fillDetailSlotAttachments,
getDetailSlotTargetIndexes,
} from "@/components/canvas/chat-panel/canvas-detail-slots";
import {
buildCanvasUploadedImageAttachment,
getCanvasUploadImageTitle,
normalizeUploadedAssetId,
validateCanvasImageUploadFile,
} from "@/components/canvas/chat-panel/canvas-upload-helpers";
import {
clampChatRequestText,
deriveSessionTitleFromMessages,
@ -4816,14 +4822,9 @@ export function CanvasChatPanel({
const file = event.target.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
toast.error("请上传图片文件");
event.currentTarget.value = "";
return;
}
if (file.size > 50 * 1024 * 1024) {
toast.error("图片大小不能超过 50MB");
const validation = validateCanvasImageUploadFile(file);
if (!validation.ok) {
toast.error(validation.message);
event.currentTarget.value = "";
return;
}
@ -4841,15 +4842,12 @@ export function CanvasChatPanel({
const data = await response.json();
const imageUrl = data.url as string;
const assetId =
typeof data.assetId === "string" && data.assetId.trim() ? data.assetId.trim() : undefined;
const assetId = normalizeUploadedAssetId(data.assetId);
updateAttachmentAtVideoSlot(slotIndex, {
url: imageUrl,
name: file.name,
contentType: file.type || "image/jpeg",
assetId,
});
updateAttachmentAtVideoSlot(
slotIndex,
buildCanvasUploadedImageAttachment({ file, imageUrl, assetId })
);
const dimensions = await getImageDimensions(imageUrl);
const nodeIndex = nodes.length + slotIndex;
@ -4863,8 +4861,8 @@ export function CanvasChatPanel({
data: {
src: imageUrl,
status: "completed",
title: file.name.replace(/\.[^.]+$/, ""),
prompt: file.name.replace(/\.[^.]+$/, ""),
title: getCanvasUploadImageTitle(file.name),
prompt: getCanvasUploadImageTitle(file.name),
metadata: {
originalWidth: dimensions.originalWidth,
originalHeight: dimensions.originalHeight,
@ -4888,14 +4886,9 @@ export function CanvasChatPanel({
const file = event.target.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
toast.error("请上传图片文件");
event.currentTarget.value = "";
return;
}
if (file.size > 50 * 1024 * 1024) {
toast.error("图片大小不能超过 50MB");
const validation = validateCanvasImageUploadFile(file);
if (!validation.ok) {
toast.error(validation.message);
event.currentTarget.value = "";
return;
}
@ -4913,15 +4906,12 @@ export function CanvasChatPanel({
const data = await response.json();
const imageUrl = data.url as string;
const assetId =
typeof data.assetId === "string" && data.assetId.trim() ? data.assetId.trim() : undefined;
const assetId = normalizeUploadedAssetId(data.assetId);
updateAttachmentAtAgentSlot(slotIndex, {
url: imageUrl,
name: file.name,
contentType: file.type || "image/jpeg",
assetId,
});
updateAttachmentAtAgentSlot(
slotIndex,
buildCanvasUploadedImageAttachment({ file, imageUrl, assetId })
);
const dimensions = await getImageDimensions(imageUrl);
const nodeIndex = nodes.length + slotIndex;
@ -4935,8 +4925,8 @@ export function CanvasChatPanel({
data: {
src: imageUrl,
status: "completed",
title: file.name.replace(/\.[^.]+$/, ""),
prompt: file.name.replace(/\.[^.]+$/, ""),
title: getCanvasUploadImageTitle(file.name),
prompt: getCanvasUploadImageTitle(file.name),
metadata: {
originalWidth: dimensions.originalWidth,
originalHeight: dimensions.originalHeight,
@ -5046,14 +5036,9 @@ export function CanvasChatPanel({
const file = event.target.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
toast.error("请上传图片文件");
event.currentTarget.value = "";
return;
}
if (file.size > 50 * 1024 * 1024) {
toast.error("图片大小不能超过 50MB");
const validation = validateCanvasImageUploadFile(file);
if (!validation.ok) {
toast.error(validation.message);
event.currentTarget.value = "";
return;
}
@ -5071,18 +5056,15 @@ export function CanvasChatPanel({
const data = await response.json();
const imageUrl = String(data.url || "").trim();
const assetId =
typeof data.assetId === "string" && data.assetId.trim() ? data.assetId.trim() : undefined;
const assetId = normalizeUploadedAssetId(data.assetId);
if (!imageUrl) {
throw new Error("未获取到图片地址");
}
updateDetailSlotAttachment(slotIndex, {
url: imageUrl,
name: file.name,
contentType: file.type || "image/jpeg",
assetId,
});
updateDetailSlotAttachment(
slotIndex,
buildCanvasUploadedImageAttachment({ file, imageUrl, assetId })
);
toast.success(`${DETAIL_SLOT_CONFIGS[slotIndex]?.label || "图片"}上传成功`);
} catch (_error) {
toast.error("图片上传失败");

View File

@ -0,0 +1,46 @@
import type { PickedObjectAttachment } from "@/components/canvas/chat-panel/canvas-chat-session";
export const CANVAS_IMAGE_UPLOAD_MAX_BYTES = 50 * 1024 * 1024;
export type CanvasUploadFileLike = {
name: string;
type: string;
size: number;
};
export function validateCanvasImageUploadFile(file?: CanvasUploadFileLike | null):
| { ok: true }
| { ok: false; message: string } {
if (!file) {
return { ok: false, message: "请选择图片文件" };
}
if (!file.type.startsWith("image/")) {
return { ok: false, message: "请上传图片文件" };
}
if (file.size > CANVAS_IMAGE_UPLOAD_MAX_BYTES) {
return { ok: false, message: "图片大小不能超过 50MB" };
}
return { ok: true };
}
export function getCanvasUploadImageTitle(fileName: string): string {
const title = String(fileName || "").replace(/\.[^.]+$/, "").trim();
return title || "图片";
}
export function buildCanvasUploadedImageAttachment(params: {
file: CanvasUploadFileLike;
imageUrl: string;
assetId?: string;
}): PickedObjectAttachment {
return {
url: params.imageUrl,
name: params.file.name,
contentType: params.file.type || "image/jpeg",
assetId: params.assetId,
};
}
export function normalizeUploadedAssetId(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}

View File

@ -38,6 +38,13 @@ import {
fillDetailSlotAttachments,
getDetailSlotTargetIndexes,
} from "@/components/canvas/chat-panel/canvas-detail-slots";
import {
buildCanvasUploadedImageAttachment,
CANVAS_IMAGE_UPLOAD_MAX_BYTES,
getCanvasUploadImageTitle,
normalizeUploadedAssetId,
validateCanvasImageUploadFile,
} from "@/components/canvas/chat-panel/canvas-upload-helpers";
import type { PickedObjectAttachment } from "@/components/canvas/chat-panel/canvas-chat-session";
import type { ChatMessage } from "@/lib/types";
@ -444,4 +451,60 @@ test.describe("canvas chat pure helpers", () => {
expect(filled.slots[4]).toBe(productB);
expect(filled.slots[5]).toBeNull();
});
test("validates and normalizes canvas image upload helper payloads", () => {
expect(validateCanvasImageUploadFile(null)).toEqual({
ok: false,
message: "请选择图片文件",
});
expect(
validateCanvasImageUploadFile({
name: "demo.txt",
type: "text/plain",
size: 10,
})
).toEqual({
ok: false,
message: "请上传图片文件",
});
expect(
validateCanvasImageUploadFile({
name: "huge.png",
type: "image/png",
size: CANVAS_IMAGE_UPLOAD_MAX_BYTES + 1,
})
).toEqual({
ok: false,
message: "图片大小不能超过 50MB",
});
expect(
validateCanvasImageUploadFile({
name: "valid.webp",
type: "image/webp",
size: CANVAS_IMAGE_UPLOAD_MAX_BYTES,
})
).toEqual({ ok: true });
expect(getCanvasUploadImageTitle(" product.hero.final.png ")).toBe("product.hero.final");
expect(getCanvasUploadImageTitle(".png")).toBe("图片");
expect(normalizeUploadedAssetId(" asset-123 ")).toBe("asset-123");
expect(normalizeUploadedAssetId(" ")).toBeUndefined();
expect(
buildCanvasUploadedImageAttachment({
file: {
name: "photo.png",
type: "",
size: 100,
},
imageUrl: "https://cdn.example.com/photo.png",
assetId: "asset-1",
})
).toEqual({
name: "photo.png",
url: "https://cdn.example.com/photo.png",
contentType: "image/jpeg",
assetId: "asset-1",
});
});
});