From 593b7e3df0c275e00d7b209c91fe6e5791dde221 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 27 May 2026 10:45:26 +0800 Subject: [PATCH] refactor: extract canvas upload validation helpers --- components/canvas/CanvasChatPanel.tsx | 86 ++++++++----------- .../chat-panel/canvas-upload-helpers.ts | 46 ++++++++++ tests/routes/canvas-chat-helpers.test.ts | 63 ++++++++++++++ 3 files changed, 143 insertions(+), 52 deletions(-) create mode 100644 components/canvas/chat-panel/canvas-upload-helpers.ts diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 396f13bac..bcdedaf94 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -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("图片上传失败"); diff --git a/components/canvas/chat-panel/canvas-upload-helpers.ts b/components/canvas/chat-panel/canvas-upload-helpers.ts new file mode 100644 index 000000000..392c7955e --- /dev/null +++ b/components/canvas/chat-panel/canvas-upload-helpers.ts @@ -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; +} diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts index ba1659ccd..04a9f5266 100644 --- a/tests/routes/canvas-chat-helpers.test.ts +++ b/tests/routes/canvas-chat-helpers.test.ts @@ -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", + }); + }); });