改为行引用保存画布参考图元数据

生成输入参考图只保存 refType/refId 行引用
生成器布局快照只保存参考图行指针并在加载时恢复临时源
上传参考图优先创建项目资源行并回写 resourceId
元数据弹窗改为展示参考图行引用信息
更新图片画布技术文档和项目决策记录
This commit is contained in:
2026-06-21 17:27:23 +08:00
parent 29d9c9ff5b
commit de9eaef28f
19 changed files with 822 additions and 264 deletions
@@ -190,6 +190,100 @@ type CanvasGenerationDialogSnapshot = EditorProjectLayerSnapshot & {
export type CanvasLayoutItems = EditorProjectLayerSnapshot[];
function isPersistedReferenceResourceId(resourceId: string | null | undefined) {
const normalizedResourceId = resourceId?.trim();
return Boolean(
normalizedResourceId &&
!normalizedResourceId.startsWith('local-') &&
!normalizedResourceId.startsWith('generation-dialog:'),
);
}
function isPersistedReferenceAssetId(assetId: string | null | undefined) {
const normalizedAssetId = assetId?.trim();
return Boolean(normalizedAssetId && !normalizedAssetId.startsWith('upload-'));
}
function resolveReferencePointer(reference: CharacterReferenceImage) {
const resourceId = reference.resourceId?.trim();
if (isPersistedReferenceResourceId(resourceId)) {
return {
src: `ref:project-resource:${resourceId}`,
resourceId,
sourceAssetId: reference.sourceAssetId,
};
}
const sourceAssetId = reference.sourceAssetId?.trim();
if (isPersistedReferenceAssetId(sourceAssetId)) {
return {
src: `ref:asset:${sourceAssetId}`,
resourceId: reference.resourceId,
sourceAssetId,
};
}
return null;
}
function serializeGenerationReference(
reference: CharacterReferenceImage | null | undefined,
): CharacterReferenceImage | null {
if (!reference) {
return null;
}
const pointer = resolveReferencePointer(reference);
if (!pointer) {
return null;
}
return {
id: reference.id,
label: reference.label,
src: pointer.src,
mediaType: reference.mediaType,
mimeType: reference.mimeType,
sizeBytes: reference.sizeBytes,
durationSeconds: reference.durationSeconds,
resourceId: pointer.resourceId,
sourceAssetId: pointer.sourceAssetId,
};
}
function serializeGenerationReferences(
references: CharacterReferenceImage[] | undefined,
) {
if (!references) {
return undefined;
}
return references.flatMap((reference) => {
const serialized = serializeGenerationReference(reference);
return serialized ? [serialized] : [];
});
}
function serializeDialogReferences(
dialog: CanvasGenerationDialogState,
): CanvasGenerationDialogState {
return {
...dialog,
specReference: serializeGenerationReference(dialog.specReference),
generationReferences: serializeGenerationReferences(
dialog.generationReferences,
),
characterSpecReference: serializeGenerationReference(
dialog.characterSpecReference,
),
characterReferences: serializeGenerationReferences(
dialog.characterReferences,
),
iconSpecReference: serializeGenerationReference(dialog.iconSpecReference),
publicationReferences: serializeGenerationReferences(
dialog.publicationReferences,
),
uiDesignSpecReference: serializeGenerationReference(
dialog.uiDesignSpecReference,
),
};
}
export function serializeCanvasGenerationDialog(
dialog: CanvasGenerationDialogState,
): CanvasGenerationDialogSnapshot {
@@ -197,7 +291,7 @@ export function serializeCanvasGenerationDialog(
itemType: 'generation-dialog',
layerId: `generation-dialog:${dialog.id}`,
resourceId: `generation-dialog:${dialog.id}`,
dialog,
dialog: serializeDialogReferences(dialog),
};
}
@@ -229,6 +323,7 @@ export function isCanvasGenerationDialogLayoutItem(
export function splitCanvasLayoutItems(
items: EditorProjectLayerSnapshot[],
resourcesById: Map<string, CanvasLayerResourceMetadata> = new Map(),
): {
layerItems: EditorProjectLayerSnapshot[];
generationDialogs: CanvasGenerationDialogState[];
@@ -238,7 +333,7 @@ export function splitCanvasLayoutItems(
items.forEach((item) => {
if (isCanvasGenerationDialogLayoutItem(item)) {
const dialog = hydrateCanvasGenerationDialog(item.dialog);
const dialog = hydrateCanvasGenerationDialog(item.dialog, resourcesById);
if (dialog) {
generationDialogs.push(dialog);
}
@@ -252,6 +347,7 @@ export function splitCanvasLayoutItems(
export function hydrateCanvasGenerationDialog(
value: unknown,
resourcesById: Map<string, CanvasLayerResourceMetadata> = new Map(),
): CanvasGenerationDialogState | null {
if (!value || typeof value !== 'object') {
return null;
@@ -278,17 +374,26 @@ export function hydrateCanvasGenerationDialog(
? snapshot.specType
: undefined,
specValues: hydrateSpecFormValues(snapshot.specValues),
specReference: hydrateCharacterReference(snapshot.specReference),
specReference: hydrateCharacterReference(
snapshot.specReference,
resourcesById,
),
generationReferences: hydrateCharacterReferences(
snapshot.generationReferences,
resourcesById,
),
characterSpecReference: hydrateCharacterReference(
snapshot.characterSpecReference,
resourcesById,
),
characterReferences: hydrateCharacterReferences(
snapshot.characterReferences,
resourcesById,
),
iconSpecReference: hydrateCharacterReference(
snapshot.iconSpecReference,
resourcesById,
),
iconSpecReference: hydrateCharacterReference(snapshot.iconSpecReference),
iconDescriptions: Array.isArray(snapshot.iconDescriptions)
? snapshot.iconDescriptions.filter(
(description): description is string =>
@@ -303,9 +408,11 @@ export function hydrateCanvasGenerationDialog(
),
publicationReferences: hydrateCharacterReferences(
snapshot.publicationReferences,
resourcesById,
),
uiDesignSpecReference: hydrateCharacterReference(
snapshot.uiDesignSpecReference,
resourcesById,
),
imageModel: stringOrUndefined(snapshot.imageModel),
videoModel:
@@ -707,12 +814,19 @@ export function generationInputsOrNull(
const item = reference as {
title?: unknown;
label?: unknown;
src?: unknown;
refType?: unknown;
refId?: unknown;
};
const title = stringOrNull(item.title);
const label = stringOrNull(item.label);
const src = stringOrNull(item.src);
return title && label && src ? [{ title, label, src }] : [];
const refType: 'project-resource' | 'asset' | null =
item.refType === 'project-resource' || item.refType === 'asset'
? item.refType
: null;
const refId = stringOrNull(item.refId);
return title && label && refType && refId
? [{ title, label, refType, refId }]
: [];
})
: [];
@@ -829,7 +943,20 @@ function hydrateSpecFormValues(
};
}
function hydrateCharacterReference(value: unknown): CharacterReferenceImage | null {
function resolveHydratedReferenceResource(
resourceId: string | undefined,
resourcesById: Map<string, CanvasLayerResourceMetadata>,
) {
if (resourceId) {
return resourcesById.get(resourceId);
}
return undefined;
}
function hydrateCharacterReference(
value: unknown,
resourcesById: Map<string, CanvasLayerResourceMetadata> = new Map(),
): CharacterReferenceImage | null {
if (!value || typeof value !== 'object') {
return null;
}
@@ -837,6 +964,8 @@ function hydrateCharacterReference(value: unknown): CharacterReferenceImage | nu
const id = stringOrNull(snapshot.id);
const label = stringOrNull(snapshot.label);
const src = stringOrNull(snapshot.src);
const resourceId = stringOrUndefined(snapshot.resourceId);
const sourceAssetId = stringOrUndefined(snapshot.sourceAssetId);
const mediaType: CanvasMediaType =
snapshot.mediaType === 'video' || snapshot.mediaType === 'audio'
? snapshot.mediaType
@@ -845,29 +974,37 @@ function hydrateCharacterReference(value: unknown): CharacterReferenceImage | nu
const sizeBytes = numberFromSnapshot(snapshot.sizeBytes, 0) || undefined;
const durationSeconds =
numberFromSnapshot(snapshot.durationSeconds, 0) || undefined;
const objectKey = stringOrUndefined(snapshot.objectKey);
const assetObjectId = stringOrUndefined(snapshot.assetObjectId);
return id && label && src
const resource = resolveHydratedReferenceResource(resourceId, resourcesById);
const objectKey =
stringOrUndefined(snapshot.objectKey) ?? stringOrUndefined(resource?.objectKey);
const assetObjectId =
stringOrUndefined(snapshot.assetObjectId) ??
stringOrUndefined(resource?.assetObjectId);
const resolvedSrc = resource?.imageSrc || objectKey || src;
return id && label && resolvedSrc
? {
id,
label,
src,
src: resolvedSrc,
mediaType,
mimeType,
sizeBytes,
durationSeconds,
objectKey,
assetObjectId,
resourceId,
sourceAssetId,
}
: null;
}
function hydrateCharacterReferences(
value: unknown,
resourcesById: Map<string, CanvasLayerResourceMetadata> = new Map(),
): CharacterReferenceImage[] | undefined {
return Array.isArray(value)
? value.flatMap((reference) => {
const hydrated = hydrateCharacterReference(reference);
const hydrated = hydrateCharacterReference(reference, resourcesById);
return hydrated ? [hydrated] : [];
})
: undefined;