From 5a60e993dcb3d99dbc7a4b77cf8de2804e86cbba Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Wed, 2 Sep 2026 22:55:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(asset-library):=20=E7=B4=A0=E6=9D=90?= =?UTF-8?q?=E5=BA=93=20-=20=E5=85=BC=E5=AE=B9=E5=8E=86=E5=8F=B2=E7=B4=A0?= =?UTF-8?q?=E6=9D=90=E7=BC=BA=E5=A4=B1=E5=AD=97=E6=AE=B5=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=B4=A9=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/lib/asset-storage-revision.test.ts | 23 +++++++++++++++++ web/src/lib/asset-storage-revision.ts | 29 ++++++++++++++++++++-- web/src/pages/assets/index.tsx | 5 ++-- 3 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 web/src/lib/asset-storage-revision.test.ts diff --git a/web/src/lib/asset-storage-revision.test.ts b/web/src/lib/asset-storage-revision.test.ts new file mode 100644 index 000000000..7d9eb5b0a --- /dev/null +++ b/web/src/lib/asset-storage-revision.test.ts @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +// @ts-expect-error -- Node 原生 TypeScript 测试运行器需要保留扩展名。 +import { normalizeAssetRecord } from "./asset-storage-revision.ts"; + +test("缺失图片尺寸的历史素材会补齐可渲染数据", () => { + const asset = normalizeAssetRecord({ + id: "legacy-image", + kind: "image", + title: "历史图片", + coverUrl: "https://example.com/image.png", + tags: [], + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + data: undefined, + } as never); + + assert.equal(asset.kind, "image"); + assert.equal(asset.data.width, 0); + assert.equal(asset.data.height, 0); + assert.equal(asset.data.dataUrl, "https://example.com/image.png"); +}); diff --git a/web/src/lib/asset-storage-revision.ts b/web/src/lib/asset-storage-revision.ts index 0c4ce56dd..33b8fa96f 100644 --- a/web/src/lib/asset-storage-revision.ts +++ b/web/src/lib/asset-storage-revision.ts @@ -11,13 +11,38 @@ export type AssetStorageDocument = { export function normalizeAssetRecord(asset: Asset): Asset { const category = normalizeAssetCategory(asset.category, defaultAssetCategoryForKind(asset.kind)); const folderId = typeof asset.folderId === "string" ? asset.folderId.trim() : ""; - if (Array.isArray(asset.tags) && asset.tags.every((tag) => typeof tag === "string") && asset.category === category && (asset.folderId || "") === folderId) return asset; + const normalizedData = normalizeAssetData(asset); + if (Array.isArray(asset.tags) && asset.tags.every((tag) => typeof tag === "string") && asset.category === category && (asset.folderId || "") === folderId && normalizedData === asset.data) return asset; return { ...asset, tags: Array.isArray(asset.tags) ? asset.tags.filter((tag): tag is string => typeof tag === "string") : [], folderId: folderId || undefined, category, - }; + data: normalizedData, + } as Asset; +} + +function normalizeAssetData(asset: Asset): Asset["data"] { + const raw = asset.data && typeof asset.data === "object" ? asset.data as Record : {}; + const numberValue = (value: unknown) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0; + if (asset.kind === "text") return typeof raw.content === "string" ? asset.data : { content: "" }; + if (asset.kind === "image") { + if (typeof raw.dataUrl === "string" && typeof raw.width === "number" && typeof raw.height === "number") return asset.data; + return { dataUrl: typeof raw.dataUrl === "string" ? raw.dataUrl : asset.coverUrl || "", storageKey: typeof raw.storageKey === "string" ? raw.storageKey : undefined, width: numberValue(raw.width), height: numberValue(raw.height), bytes: numberValue(raw.bytes), mimeType: typeof raw.mimeType === "string" ? raw.mimeType : "image/*" }; + } + if (asset.kind === "video") { + if (typeof raw.url === "string" && typeof raw.width === "number" && typeof raw.height === "number") return asset.data; + return { url: typeof raw.url === "string" ? raw.url : asset.coverUrl || "", storageKey: typeof raw.storageKey === "string" ? raw.storageKey : undefined, width: numberValue(raw.width), height: numberValue(raw.height), durationMs: numberValue(raw.durationMs), hasAudio: raw.hasAudio === true, bytes: numberValue(raw.bytes), mimeType: typeof raw.mimeType === "string" ? raw.mimeType : "video/*" }; + } + if (asset.kind === "audio") { + if (typeof raw.url === "string") return asset.data; + return { url: "", storageKey: typeof raw.storageKey === "string" ? raw.storageKey : undefined, durationMs: numberValue(raw.durationMs), bytes: numberValue(raw.bytes), mimeType: typeof raw.mimeType === "string" ? raw.mimeType : "audio/*" }; + } + if (asset.kind === "model") { + if (typeof raw.url === "string" && typeof raw.fileName === "string") return asset.data; + return { url: "", storageKey: typeof raw.storageKey === "string" ? raw.storageKey : undefined, bytes: numberValue(raw.bytes), mimeType: typeof raw.mimeType === "string" ? raw.mimeType : "model/*", fileName: typeof raw.fileName === "string" ? raw.fileName : asset.title || "未命名模型" }; + } + return asset.data; } function normalizeAssets(assets: Asset[]) { diff --git a/web/src/pages/assets/index.tsx b/web/src/pages/assets/index.tsx index be84cc7b1..6f43c9d12 100644 --- a/web/src/pages/assets/index.tsx +++ b/web/src/pages/assets/index.tsx @@ -11,6 +11,7 @@ import { AssetMediaPreview } from "@/components/asset-media-preview"; import { AssetLibraryCard, AssetLibraryCardMedia } from "@/components/assets/asset-library-card"; import { saveAs } from "file-saver"; import { cn } from "@/lib/utils"; +import { normalizeAssetRecord } from "@/lib/asset-storage-revision"; import { useCopyText } from "@/hooks/use-copy-text"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; @@ -123,7 +124,7 @@ export default function AssetsPage() { }); const folders = foldersQuery.data?.folders || []; - const allLibraryAssets = useMemo(() => assets.filter((asset): asset is LibraryAsset => asset.kind !== "entity"), [assets]); + const allLibraryAssets = useMemo(() => assets.map(normalizeAssetRecord).filter((asset): asset is LibraryAsset => asset.kind !== "entity"), [assets]); const activeAssets = useMemo(() => allLibraryAssets.filter((asset) => asset.status !== "archived"), [allLibraryAssets]); const trashAssets = useMemo(() => allLibraryAssets.filter((asset) => asset.status === "archived"), [allLibraryAssets]); const validAssets = viewMode === "trash" ? trashAssets : activeAssets; @@ -162,7 +163,7 @@ export default function AssetsPage() { return filteredAssets.slice(start, start + pageSize); }, [filteredAssets, page, pageSize]); const visibleAssets = useMemo( - () => (assetPageQuery.data?.assets || localVisibleAssets).filter((asset): asset is LibraryAsset => asset.kind !== "entity"), + () => (assetPageQuery.data?.assets || localVisibleAssets).map(normalizeAssetRecord).filter((asset): asset is LibraryAsset => asset.kind !== "entity"), [assetPageQuery.data?.assets, localVisibleAssets], ); const visibleAssetIds = useMemo(() => visibleAssets.map((asset) => asset.id), [visibleAssets]);