fix(create): 素材参考 - 允许选择未缓存的远端素材
This commit is contained in:
@@ -5,6 +5,14 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
|
|||||||
|
|
||||||
# 待测试
|
# 待测试
|
||||||
|
|
||||||
|
## 创作参考内容的远端素材选择
|
||||||
|
|
||||||
|
- 已修复:创作页按素材类型与当前模式能力校验远端条目,不再把本地未缓存的素材误判为不适用;确认时使用弹窗返回的素材记录,仅按需缓存实际选中的远端素材。
|
||||||
|
- 已修复:确认时重新校验媒体类型与视频模型能力,缺失或不支持的条目明确报错;保持现有参考数量上限。
|
||||||
|
- [x] 新增远端素材回归测试,并与创作引用、素材库入口和画布素材选择测试一同运行,35 项通过;前端类型检查通过。
|
||||||
|
- [ ] 在本地未缓存云端素材的浏览器中选择图片、视频和音频,确认支持的条目可选并能添加;跨页选择后确认应保留选择顺序,再次打开弹窗应保留已选状态。
|
||||||
|
- [ ] 切换到图片创作及不同视频模型,确认不支持的媒体类型保持禁用,参考数量仍受模型上限约束;云端素材读取失败时保留弹窗并显示错误。
|
||||||
|
|
||||||
## 灵感页路由与导航
|
## 灵感页路由与导航
|
||||||
|
|
||||||
- 已修复:Nginx 对 `/inspirations` 与 `/inspirations/` 精确返回 SPA 入口,避免同名静态目录触发带容器端口的 301;目录下的 JSON 仍按静态文件提供。
|
- 已修复:Nginx 对 `/inspirations` 与 `/inspirations/` 精确返回 SPA 入口,避免同名静态目录触发带容器端口的 301;目录下的 JSON 仍按静态文件提供。
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ type Props = {
|
|||||||
allowCharacters?: boolean;
|
allowCharacters?: boolean;
|
||||||
/** 调用方能处理 onConfirm 的 pickedItems 时设为 true:远端分页里不在本地缓存的素材也可选。 */
|
/** 调用方能处理 onConfirm 的 pickedItems 时设为 true:远端分页里不在本地缓存的素材也可选。 */
|
||||||
acceptRemoteItems?: boolean;
|
acceptRemoteItems?: boolean;
|
||||||
|
/** 远端条目不依赖本地缓存,仍需按调用方当前能力校验。 */
|
||||||
|
remoteItemDisabledReason?: (asset: Asset) => string | undefined;
|
||||||
/** 左侧「媒体类型」筛选项;只有一种类型或已由 remoteKind 固定时不展示该分组。 */
|
/** 左侧「媒体类型」筛选项;只有一种类型或已由 remoteKind 固定时不展示该分组。 */
|
||||||
mediaKinds?: AssetPickerMediaKind[];
|
mediaKinds?: AssetPickerMediaKind[];
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -95,6 +97,7 @@ export function AssetLibraryPickerModal({
|
|||||||
remoteKind,
|
remoteKind,
|
||||||
allowCharacters = false,
|
allowCharacters = false,
|
||||||
acceptRemoteItems = false,
|
acceptRemoteItems = false,
|
||||||
|
remoteItemDisabledReason,
|
||||||
mediaKinds = DEFAULT_MEDIA_KINDS,
|
mediaKinds = DEFAULT_MEDIA_KINDS,
|
||||||
open,
|
open,
|
||||||
items,
|
items,
|
||||||
@@ -166,20 +169,8 @@ export function AssetLibraryPickerModal({
|
|||||||
() =>
|
() =>
|
||||||
(remoteQuery.data?.assets || [])
|
(remoteQuery.data?.assets || [])
|
||||||
.filter((asset) => asset.kind !== "model" && (asset.kind !== "entity" || allowCharacters))
|
.filter((asset) => asset.kind !== "model" && (asset.kind !== "entity" || allowCharacters))
|
||||||
.map((asset) => {
|
.map((asset) => remoteAssetToPickerItem(asset, items.find((item) => item.id === asset.id), acceptRemoteItems, remoteItemDisabledReason?.(asset))),
|
||||||
const known = items.find((item) => item.id === asset.id);
|
[acceptRemoteItems, allowCharacters, remoteItemDisabledReason, remoteQuery.data, items],
|
||||||
return {
|
|
||||||
id: asset.id,
|
|
||||||
title: asset.title,
|
|
||||||
category: asset.category || "other",
|
|
||||||
archived: asset.status === "archived",
|
|
||||||
kindLabel: asset.kind === "image" ? "图片" : asset.kind === "video" ? "视频" : asset.kind === "audio" ? "音频" : asset.kind === "entity" ? "角色" : "文本",
|
|
||||||
searchText: (asset.tags ?? []).join(" "),
|
|
||||||
...(known || (acceptRemoteItems ? {} : { disabledReason: "此素材不适用于当前操作" })),
|
|
||||||
asset,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
[acceptRemoteItems, allowCharacters, remoteQuery.data, items],
|
|
||||||
);
|
);
|
||||||
const uploadInputRef = useRef<HTMLInputElement>(null);
|
const uploadInputRef = useRef<HTMLInputElement>(null);
|
||||||
const pickedRemoteRef = useRef(new Map<string, AssetLibraryPickerItem>());
|
const pickedRemoteRef = useRef(new Map<string, AssetLibraryPickerItem>());
|
||||||
@@ -648,6 +639,20 @@ export function AssetLibraryPickerModal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function remoteAssetToPickerItem(asset: Asset, known: AssetLibraryPickerItem | undefined, acceptRemoteItems: boolean, disabledReason?: string): AssetLibraryPickerItem {
|
||||||
|
return {
|
||||||
|
id: asset.id,
|
||||||
|
title: asset.title,
|
||||||
|
category: asset.category || "other",
|
||||||
|
archived: asset.status === "archived",
|
||||||
|
kindLabel: asset.kind === "image" ? "图片" : asset.kind === "video" ? "视频" : asset.kind === "audio" ? "音频" : asset.kind === "entity" ? "角色" : "文本",
|
||||||
|
searchText: (asset.tags ?? []).join(" "),
|
||||||
|
...known,
|
||||||
|
disabledReason: known?.disabledReason || (!known && !acceptRemoteItems ? "此素材不适用于当前操作" : disabledReason),
|
||||||
|
asset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 角色卡、3D 模型等非媒体条目没有媒体类型,媒体筛选生效时不参与匹配。
|
// 角色卡、3D 模型等非媒体条目没有媒体类型,媒体筛选生效时不参与匹配。
|
||||||
export function pickerItemMediaKind(item: AssetLibraryPickerItem): AssetPickerMediaKind | undefined {
|
export function pickerItemMediaKind(item: AssetLibraryPickerItem): AssetPickerMediaKind | undefined {
|
||||||
if (item.mediaKind) return item.mediaKind;
|
if (item.mediaKind) return item.mediaKind;
|
||||||
|
|||||||
32
web/src/pages/create/creation-library.ts
Normal file
32
web/src/pages/create/creation-library.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { AssetLibraryPickerItem } from "@/components/assets/asset-library-picker-modal";
|
||||||
|
import type { CreationMode } from "@/lib/creation-mode";
|
||||||
|
import type { CreationReferenceLimits } from "./creation-references";
|
||||||
|
import { creationAttachmentFromAsset, creationAttachmentFromAudioAsset, creationAttachmentFromExternalAsset, creationAttachmentFromVideoAsset, type CreationAttachment } from "./creation-assets";
|
||||||
|
|
||||||
|
export function creationLibraryDisabledReason(mode: CreationMode, kind: string | undefined, videoLimits?: CreationReferenceLimits) {
|
||||||
|
if (kind !== "image" && kind !== "video" && kind !== "audio") return "此类型素材暂不支持作为创作参考";
|
||||||
|
if (mode === "image") return kind === "image" ? undefined : "图片创作仅支持参考图";
|
||||||
|
if (mode !== "video") return undefined;
|
||||||
|
|
||||||
|
const maximum = kind === "image" ? videoLimits?.maxImages : kind === "video" ? videoLimits?.maxVideos : videoLimits?.maxAudios;
|
||||||
|
if ((maximum || 0) > 0) return undefined;
|
||||||
|
const label = kind === "video" ? "视频" : kind === "audio" ? "音频" : "图片";
|
||||||
|
return `当前视频模型不支持参考${label}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function creationLibrarySelectionToAttachments(ids: string[], items: AssetLibraryPickerItem[], mode: CreationMode, videoLimits?: CreationReferenceLimits): CreationAttachment[] {
|
||||||
|
const itemsById = new Map(items.map((item) => [item.id, item]));
|
||||||
|
return ids.map((id) => {
|
||||||
|
const item = itemsById.get(id);
|
||||||
|
if (!item) throw new Error("所选素材已不存在,请重新选择");
|
||||||
|
const kind = item.external?.item.kind || item.asset?.kind;
|
||||||
|
const disabledReason = item.disabledReason || creationLibraryDisabledReason(mode, kind, videoLimits);
|
||||||
|
if (disabledReason) throw new Error(`“${item.title}”:${disabledReason}`);
|
||||||
|
if (item.external) return creationAttachmentFromExternalAsset(item.external);
|
||||||
|
const asset = item.asset;
|
||||||
|
if (asset?.kind === "image") return creationAttachmentFromAsset(asset);
|
||||||
|
if (asset?.kind === "video") return creationAttachmentFromVideoAsset(asset);
|
||||||
|
if (asset?.kind === "audio") return creationAttachmentFromAudioAsset(asset);
|
||||||
|
throw new Error("所选素材已不存在,请重新选择");
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import { loadCreationConversations, pendingCreationTaskIds, removeCreationConver
|
|||||||
import { resolveModelChannel, selectableModelsByCapability, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
|
import { resolveModelChannel, selectableModelsByCapability, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
|
||||||
import { useCreationPreferencesStore } from "@/stores/use-creation-preferences-store";
|
import { useCreationPreferencesStore } from "@/stores/use-creation-preferences-store";
|
||||||
import { useAssetStore, type Asset } from "@/stores/use-asset-store";
|
import { useAssetStore, type Asset } from "@/stores/use-asset-store";
|
||||||
|
import { loadAssetsForUse } from "@/services/user-data-sync";
|
||||||
import { useAppearanceStore } from "@/stores/use-appearance-store";
|
import { useAppearanceStore } from "@/stores/use-appearance-store";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useUserStore } from "@/stores/use-user-store";
|
import { useUserStore } from "@/stores/use-user-store";
|
||||||
@@ -27,8 +28,9 @@ import type { PromptOptimizerProvider } from "@/lib/plugins/plugin-types";
|
|||||||
import { promptOptimizerPlugin, PROMPT_OPTIMIZER_PLUGIN_ID } from "@/lib/plugins/builtin/prompt-optimizer";
|
import { promptOptimizerPlugin, PROMPT_OPTIMIZER_PLUGIN_ID } from "@/lib/plugins/builtin/prompt-optimizer";
|
||||||
import { createPluginHostContext } from "@/services/plugin-host";
|
import { createPluginHostContext } from "@/services/plugin-host";
|
||||||
import { usePluginStore } from "@/stores/use-plugin-store";
|
import { usePluginStore } from "@/stores/use-plugin-store";
|
||||||
import { buildCreationMentionReferences, expandCreationPrompt, reconcileCreationAttachmentLimit, reconcileCreationAttachmentLimits, removeCreationReferenceTokens, replaceCreationAttachmentReference, selectedCreationReferences, type CreationReference, type CreationReferenceLimits } from "./creation-references";
|
import { buildCreationMentionReferences, expandCreationPrompt, reconcileCreationAttachmentLimit, reconcileCreationAttachmentLimits, removeCreationReferenceTokens, replaceCreationAttachmentReference, selectedCreationReferences, type CreationReference } from "./creation-references";
|
||||||
import { creationAttachmentFromAsset, creationAttachmentFromAudio, creationAttachmentFromAudioAsset, creationAttachmentFromDocument, creationAttachmentFromExternalAsset, creationAttachmentFromImage, creationAttachmentFromVideo, creationAttachmentFromVideoAsset, creationAttachmentKind, creationAudioAsset, creationFileAccepted, creationImageAsset, creationMediaAspectRatio, creationUploadAccept, creationVideoAsset, removeCreationAttachment, splitCreationAttachments, type CreationAttachment } from "./creation-assets";
|
import { creationAttachmentFromAudio, creationAttachmentFromDocument, creationAttachmentFromImage, creationAttachmentFromVideo, creationAttachmentKind, creationAudioAsset, creationFileAccepted, creationImageAsset, creationMediaAspectRatio, creationUploadAccept, creationVideoAsset, removeCreationAttachment, splitCreationAttachments, type CreationAttachment } from "./creation-assets";
|
||||||
|
import { creationLibraryDisabledReason, creationLibrarySelectionToAttachments } from "./creation-library";
|
||||||
import { defaultCreationMode, modeLabels, type CreationConversation, type CreationMessage, type CreationMode, type CreationRetryContext, type CreationSettings, type CreationShotRailEntry, type CreationStatus } from "./creation-types";
|
import { defaultCreationMode, modeLabels, type CreationConversation, type CreationMessage, type CreationMode, type CreationRetryContext, type CreationSettings, type CreationShotRailEntry, type CreationStatus } from "./creation-types";
|
||||||
import { attachCreationTaskContexts, completedCreationGenerationTask, conversationTimestamp, creationShotRail, creationVideoShotOrdinal, isImageAttachment, isVideoAttachment, materializeCreationTaskResults, newConversation, newMessage, reconcileCreationTaskMessages } from "./creation-conversations";
|
import { attachCreationTaskContexts, completedCreationGenerationTask, conversationTimestamp, creationShotRail, creationVideoShotOrdinal, isImageAttachment, isVideoAttachment, materializeCreationTaskResults, newConversation, newMessage, reconcileCreationTaskMessages } from "./creation-conversations";
|
||||||
import { CreationComposer, CreationEmptySuggest, CreationHistoryDrawer, CreationMessageView, CreationModeTabs, CreationWorkspaceToolbar, creationAssetCategoryLabels } from "./creation-workspace";
|
import { CreationComposer, CreationEmptySuggest, CreationHistoryDrawer, CreationMessageView, CreationModeTabs, CreationWorkspaceToolbar, creationAssetCategoryLabels } from "./creation-workspace";
|
||||||
@@ -46,17 +48,6 @@ type CreationRuntime = Awaited<ReturnType<typeof loadCreationRuntime>>;
|
|||||||
const TEXT_STREAMING_PREF_KEY = "creation.composer.text-streaming";
|
const TEXT_STREAMING_PREF_KEY = "creation.composer.text-streaming";
|
||||||
const TEXT_THINKING_PREF_KEY = "creation.composer.text-thinking";
|
const TEXT_THINKING_PREF_KEY = "creation.composer.text-thinking";
|
||||||
|
|
||||||
function creationLibraryDisabledReason(mode: CreationMode, kind: string | undefined, videoLimits?: CreationReferenceLimits) {
|
|
||||||
if (!kind) return undefined;
|
|
||||||
if (mode === "image") return kind === "image" ? undefined : "图片创作仅支持参考图";
|
|
||||||
if (mode !== "video") return undefined;
|
|
||||||
|
|
||||||
const maximum = kind === "image" ? videoLimits?.maxImages : kind === "video" ? videoLimits?.maxVideos : kind === "audio" ? videoLimits?.maxAudios : 0;
|
|
||||||
if ((maximum || 0) > 0) return undefined;
|
|
||||||
const label = kind === "video" ? "视频" : kind === "audio" ? "音频" : kind === "image" ? "图片" : "此类素材";
|
|
||||||
return `当前视频模型不支持参考${label}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readComposerPref(key: string, fallback: boolean): boolean {
|
function readComposerPref(key: string, fallback: boolean): boolean {
|
||||||
try {
|
try {
|
||||||
const stored = window.localStorage.getItem(key);
|
const stored = window.localStorage.getItem(key);
|
||||||
@@ -493,16 +484,13 @@ export default function CreatePage() {
|
|||||||
return assetIds;
|
return assetIds;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLibrarySelect = (selectedIds: string[]) => {
|
const handleLibrarySelect = async (selectedIds: string[], pickedItems = libraryItems) => {
|
||||||
const next = selectedIds.flatMap((id): CreationAttachment[] => {
|
const next = creationLibrarySelectionToAttachments(selectedIds, pickedItems, mode, videoReferenceLimits);
|
||||||
const asset = assets.find((item) => item.id === id);
|
|
||||||
if (asset?.kind === "image") return [creationAttachmentFromAsset(asset)];
|
|
||||||
if (asset?.kind === "video" && mode !== "image") return [creationAttachmentFromVideoAsset(asset)];
|
|
||||||
if (asset?.kind === "audio" && mode !== "image") return [creationAttachmentFromAudioAsset(asset)];
|
|
||||||
const external = libraryItems.find((item) => item.id === id)?.external;
|
|
||||||
return external ? [creationAttachmentFromExternalAsset(external)] : [];
|
|
||||||
});
|
|
||||||
if (!next.length) return;
|
if (!next.length) return;
|
||||||
|
const selected = new Set(selectedIds);
|
||||||
|
const cachedIds = new Set(useAssetStore.getState().assets.map((asset) => asset.id));
|
||||||
|
const uncachedAssetIds = pickedItems.filter((item) => item.asset && selected.has(item.id) && !cachedIds.has(item.id)).map((item) => item.id);
|
||||||
|
if (uncachedAssetIds.length) await loadAssetsForUse(uncachedAssetIds);
|
||||||
setAttachments((current) => {
|
setAttachments((current) => {
|
||||||
const candidates = [...current.filter((item) => !next.some((candidate) => candidate.id === item.id)), ...next];
|
const candidates = [...current.filter((item) => !next.some((candidate) => candidate.id === item.id)), ...next];
|
||||||
const reconciled = mode === "video" && videoReferenceLimits
|
const reconciled = mode === "video" && videoReferenceLimits
|
||||||
@@ -1123,6 +1111,8 @@ export default function CreatePage() {
|
|||||||
<CreationLoginDialog open={loginDialogOpen} onClose={() => setLoginDialogOpen(false)} />
|
<CreationLoginDialog open={loginDialogOpen} onClose={() => setLoginDialogOpen(false)} />
|
||||||
{libraryOpen ? <Suspense fallback={null}><AssetLibraryPickerModal
|
{libraryOpen ? <Suspense fallback={null}><AssetLibraryPickerModal
|
||||||
remoteLibrary
|
remoteLibrary
|
||||||
|
acceptRemoteItems
|
||||||
|
remoteItemDisabledReason={(asset) => creationLibraryDisabledReason(mode, asset.kind, videoReferenceLimits)}
|
||||||
open={libraryOpen}
|
open={libraryOpen}
|
||||||
items={libraryItems}
|
items={libraryItems}
|
||||||
categoryLabels={{ ...creationAssetCategoryLabels, ...externalAssetSources.categoryLabels }}
|
categoryLabels={{ ...creationAssetCategoryLabels, ...externalAssetSources.categoryLabels }}
|
||||||
|
|||||||
74
web/test/creation-library.test.ts
Normal file
74
web/test/creation-library.test.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { remoteAssetToPickerItem, type AssetLibraryPickerItem } from "@/components/assets/asset-library-picker-modal";
|
||||||
|
import { creationLibraryDisabledReason, creationLibrarySelectionToAttachments } from "@/pages/create/creation-library";
|
||||||
|
import type { Asset } from "@/stores/use-asset-store";
|
||||||
|
|
||||||
|
const base = { title: "远端素材", coverUrl: "", tags: [], createdAt: "2026-10-01T00:00:00Z", updatedAt: "2026-10-01T00:00:00Z" };
|
||||||
|
const image: Asset = { ...base, id: "remote-image", kind: "image", data: { dataUrl: "https://example.com/image.png", width: 1280, height: 720, bytes: 1024, mimeType: "image/png" } };
|
||||||
|
const video: Asset = { ...base, id: "remote-video", kind: "video", data: { url: "https://example.com/video.mp4", storageKey: "resource:video", width: 1280, height: 720, durationMs: 1000, bytes: 2048, mimeType: "video/mp4" } };
|
||||||
|
const audio: Asset = { ...base, id: "remote-audio", kind: "audio", data: { url: "https://example.com/audio.mp3", storageKey: "resource:audio", durationMs: 1000, bytes: 512, mimeType: "audio/mpeg" } };
|
||||||
|
|
||||||
|
function remoteItem(asset: Asset, mode: "text" | "image" | "video" = "text") {
|
||||||
|
return remoteAssetToPickerItem(asset, undefined, true, creationLibraryDisabledReason(mode, asset.kind));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("creation remote library", () => {
|
||||||
|
test("uncached remote media can be selected and converted without a local asset record", () => {
|
||||||
|
const items = [remoteItem(image), remoteItem(video), remoteItem(audio)];
|
||||||
|
expect(items.every((item) => !item.disabledReason)).toBe(true);
|
||||||
|
const attachments = creationLibrarySelectionToAttachments([audio.id, image.id, video.id], items, "text");
|
||||||
|
expect(attachments.map((item) => item.id)).toEqual([`asset:${audio.id}`, `asset:${image.id}`, `asset:${video.id}`]);
|
||||||
|
expect(attachments.map((item) => item.type)).toEqual(["audio/mpeg", "image/png", "video/mp4"]);
|
||||||
|
expect(attachments[0].url).toBe(audio.data.url);
|
||||||
|
expect(attachments[1].url).toBe(image.data.dataUrl);
|
||||||
|
expect(attachments[2].storageKey).toBe(video.data.storageKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unselected items from other pages are not added to the references", () => {
|
||||||
|
const attachments = creationLibrarySelectionToAttachments([video.id, image.id], [remoteItem(image), remoteItem(audio), remoteItem(video)], "text");
|
||||||
|
expect(attachments.map((item) => item.id)).toEqual([`asset:${video.id}`, `asset:${image.id}`]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("other callers still reject uncached assets unless they accept remote records", () => {
|
||||||
|
expect(remoteAssetToPickerItem(image, undefined, false).disabledReason).toBe("此素材不适用于当前操作");
|
||||||
|
const known = { ...remoteItem(image), disabledReason: "素材不可用" };
|
||||||
|
expect(remoteAssetToPickerItem(image, known, true).disabledReason).toBe("素材不可用");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("image creation applies the same media restrictions before selection and on confirmation", () => {
|
||||||
|
expect(remoteItem(image, "image").disabledReason).toBeUndefined();
|
||||||
|
expect(remoteItem(video, "image").disabledReason).toBe("图片创作仅支持参考图");
|
||||||
|
expect(remoteItem(audio, "image").disabledReason).toBe("图片创作仅支持参考图");
|
||||||
|
expect(() => creationLibrarySelectionToAttachments([image.id, video.id], [remoteItem(image), remoteItem(video)], "image")).toThrow("图片创作仅支持参考图");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("video creation revalidates changed model capabilities on confirmation", () => {
|
||||||
|
const limits = { maxImages: 1, maxVideos: 1, maxAudios: 0 };
|
||||||
|
expect(creationLibraryDisabledReason("video", "image", limits)).toBeUndefined();
|
||||||
|
expect(creationLibraryDisabledReason("video", "video", limits)).toBeUndefined();
|
||||||
|
expect(remoteAssetToPickerItem(audio, undefined, true, creationLibraryDisabledReason("video", "audio", limits)).disabledReason).toBe("当前视频模型不支持参考音频");
|
||||||
|
expect(() => creationLibrarySelectionToAttachments([audio.id], [remoteItem(audio)], "video", limits)).toThrow("当前视频模型不支持参考音频");
|
||||||
|
expect(creationLibrarySelectionToAttachments([video.id], [remoteItem(video)], "video", limits)[0].id).toBe(`asset:${video.id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("missing, disabled and unsupported records fail explicitly", () => {
|
||||||
|
expect(() => creationLibrarySelectionToAttachments([image.id, "missing"], [remoteItem(image)], "text")).toThrow("所选素材已不存在");
|
||||||
|
expect(() => creationLibrarySelectionToAttachments([image.id], [{ ...remoteItem(image), disabledReason: "素材不可用" }], "text")).toThrow("素材不可用");
|
||||||
|
const text: Asset = { ...base, id: "remote-text", kind: "text", data: { content: "参考文本" } };
|
||||||
|
expect(remoteItem(text).disabledReason).toBe("此类型素材暂不支持作为创作参考");
|
||||||
|
expect(() => creationLibrarySelectionToAttachments([text.id], [remoteAssetToPickerItem(text, undefined, true)], "text")).toThrow("此类型素材暂不支持作为创作参考");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plugin assets are still converted alongside remote library assets", () => {
|
||||||
|
const external: AssetLibraryPickerItem = {
|
||||||
|
id: "external:eagle:image",
|
||||||
|
title: "Eagle 图片",
|
||||||
|
category: "external:eagle",
|
||||||
|
kindLabel: "图片",
|
||||||
|
external: { sourceId: "eagle", sourceName: "Eagle", item: { id: "image", title: "Eagle 图片", kind: "image", fileUrl: "https://example.com/external.png" } },
|
||||||
|
};
|
||||||
|
const attachments = creationLibrarySelectionToAttachments([external.id, image.id], [remoteItem(image), external], "text");
|
||||||
|
expect(attachments.map((item) => item.id)).toEqual([external.id, `asset:${image.id}`]);
|
||||||
|
expect(attachments[0].url).toBe("https://example.com/external.png");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user