修复画布生成配方恢复与快照失真
保持背景音乐规范提示词与生成输入快照一致。 统一 V2 generationInputs 的项目、素材与完美像素账本水合,并保留 legacy 兼容。 为完美像素、裁扩、去背景和图集切片保存确定性配方并禁止改造。 由服务端按当前账号资源重建安全来源引用,修复布尔元数据显示。 补齐刷新恢复、数值布尔、无标签引用、来源溯源与确定性操作回归测试。
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
import type {
|
||||
CanvasGenerationAction,
|
||||
CanvasGenerationInputField,
|
||||
CanvasGenerationInputReference,
|
||||
CanvasGenerationInputs,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
|
||||
export const CANVAS_GENERATION_ACTIONS = [
|
||||
'image.generate',
|
||||
'spec.generate',
|
||||
'character.generate',
|
||||
'icon.generate',
|
||||
'ui-design.generate',
|
||||
'publication.generate',
|
||||
'video.generate',
|
||||
'audio.sound-effect.generate',
|
||||
'audio.background-music.generate',
|
||||
'character-animation.generate',
|
||||
'image.edit',
|
||||
'ui-design.extract-assets',
|
||||
'image.perfect-pixel',
|
||||
'spritesheet.split',
|
||||
'image.remove-background',
|
||||
'image.crop-expand',
|
||||
] as const satisfies readonly CanvasGenerationAction[];
|
||||
|
||||
export const REMIXABLE_CANVAS_GENERATION_ACTIONS =
|
||||
new Set<CanvasGenerationAction>([
|
||||
'image.generate',
|
||||
'spec.generate',
|
||||
'character.generate',
|
||||
'icon.generate',
|
||||
'ui-design.generate',
|
||||
'publication.generate',
|
||||
'video.generate',
|
||||
'audio.sound-effect.generate',
|
||||
'audio.background-music.generate',
|
||||
'character-animation.generate',
|
||||
'image.edit',
|
||||
'ui-design.extract-assets',
|
||||
]);
|
||||
|
||||
const CANVAS_GENERATION_ACTION_SET = new Set<string>(CANVAS_GENERATION_ACTIONS);
|
||||
const V2_GENERATION_INPUT_KEYS = new Set([
|
||||
'version',
|
||||
'action',
|
||||
'fields',
|
||||
'references',
|
||||
]);
|
||||
const V2_GENERATION_FIELD_KEYS = new Set(['id', 'title', 'value']);
|
||||
const V2_GENERATION_REFERENCE_KEYS = new Set([
|
||||
'id',
|
||||
'title',
|
||||
'label',
|
||||
'refType',
|
||||
'refId',
|
||||
]);
|
||||
const LEGACY_GENERATION_INPUT_KEYS = new Set(['fields', 'references']);
|
||||
const LEGACY_GENERATION_FIELD_KEYS = new Set(['title', 'value']);
|
||||
const LEGACY_GENERATION_REFERENCE_KEYS = new Set([
|
||||
'title',
|
||||
'label',
|
||||
'refType',
|
||||
'refId',
|
||||
]);
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function hasOnlyKeys(
|
||||
value: Record<string, unknown>,
|
||||
allowedKeys: ReadonlySet<string>,
|
||||
) {
|
||||
return Object.keys(value).every((key) => allowedKeys.has(key));
|
||||
}
|
||||
|
||||
function isPresentString(value: unknown): value is string {
|
||||
return typeof value === 'string' && Boolean(value.trim());
|
||||
}
|
||||
|
||||
function isGenerationInputValue(
|
||||
value: unknown,
|
||||
): value is CanvasGenerationInputField['value'] {
|
||||
return (
|
||||
typeof value === 'string' ||
|
||||
(typeof value === 'number' && Number.isFinite(value)) ||
|
||||
typeof value === 'boolean'
|
||||
);
|
||||
}
|
||||
|
||||
export function isCanvasGenerationAction(
|
||||
value: unknown,
|
||||
): value is CanvasGenerationAction {
|
||||
return typeof value === 'string' && CANVAS_GENERATION_ACTION_SET.has(value);
|
||||
}
|
||||
|
||||
export function isNormalizedCanvasGenerationInputsStructure(
|
||||
value: unknown,
|
||||
): value is CanvasGenerationInputs & {
|
||||
version: 2;
|
||||
action: CanvasGenerationAction;
|
||||
} {
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
value.version !== 2 ||
|
||||
!isCanvasGenerationAction(value.action) ||
|
||||
!Array.isArray(value.fields) ||
|
||||
!Array.isArray(value.references)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
value.fields.every(
|
||||
(field) =>
|
||||
isRecord(field) &&
|
||||
hasOnlyKeys(field, V2_GENERATION_FIELD_KEYS) &&
|
||||
isPresentString(field.id) &&
|
||||
typeof field.title === 'string' &&
|
||||
isGenerationInputValue(field.value),
|
||||
) &&
|
||||
value.references.every(
|
||||
(reference) =>
|
||||
isRecord(reference) &&
|
||||
hasOnlyKeys(reference, V2_GENERATION_REFERENCE_KEYS) &&
|
||||
isPresentString(reference.id) &&
|
||||
typeof reference.title === 'string' &&
|
||||
(reference.label === undefined ||
|
||||
typeof reference.label === 'string') &&
|
||||
(reference.refType === 'project-resource' ||
|
||||
reference.refType === 'asset') &&
|
||||
isPresentString(reference.refId),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function cloneV2GenerationInputs(
|
||||
value: Record<string, unknown>,
|
||||
strictWhitelist: boolean,
|
||||
): CanvasGenerationInputs | null {
|
||||
if (
|
||||
(strictWhitelist && !hasOnlyKeys(value, V2_GENERATION_INPUT_KEYS)) ||
|
||||
!isNormalizedCanvasGenerationInputsStructure(value)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
version: 2,
|
||||
action: value.action,
|
||||
fields: value.fields.map((field) => ({ ...field })),
|
||||
references: value.references.map((reference) => ({ ...reference })),
|
||||
};
|
||||
}
|
||||
|
||||
function hydrateLegacyGenerationInputs(
|
||||
value: Record<string, unknown>,
|
||||
strictWhitelist: boolean,
|
||||
): CanvasGenerationInputs | null {
|
||||
if (
|
||||
strictWhitelist &&
|
||||
(!hasOnlyKeys(value, LEGACY_GENERATION_INPUT_KEYS) ||
|
||||
!Array.isArray(value.fields) ||
|
||||
!Array.isArray(value.references))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const rawFields = Array.isArray(value.fields) ? value.fields : [];
|
||||
const rawReferences = Array.isArray(value.references) ? value.references : [];
|
||||
const fields = rawFields.flatMap((field) => {
|
||||
if (
|
||||
!isRecord(field) ||
|
||||
(strictWhitelist && !hasOnlyKeys(field, LEGACY_GENERATION_FIELD_KEYS)) ||
|
||||
!isPresentString(field.title) ||
|
||||
!isPresentString(field.value)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return [{ title: field.title, value: field.value }];
|
||||
});
|
||||
const references = rawReferences.flatMap((reference) => {
|
||||
if (
|
||||
!isRecord(reference) ||
|
||||
(strictWhitelist &&
|
||||
!hasOnlyKeys(reference, LEGACY_GENERATION_REFERENCE_KEYS)) ||
|
||||
!isPresentString(reference.title) ||
|
||||
!isPresentString(reference.label) ||
|
||||
(reference.refType !== 'project-resource' &&
|
||||
reference.refType !== 'asset') ||
|
||||
!isPresentString(reference.refId)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
title: reference.title,
|
||||
label: reference.label,
|
||||
refType: reference.refType,
|
||||
refId: reference.refId,
|
||||
} satisfies CanvasGenerationInputReference,
|
||||
];
|
||||
});
|
||||
if (
|
||||
strictWhitelist &&
|
||||
(fields.length !== rawFields.length ||
|
||||
references.length !== rawReferences.length)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return strictWhitelist || fields.length || references.length
|
||||
? { fields, references }
|
||||
: null;
|
||||
}
|
||||
|
||||
export function hydrateCanvasGenerationInputs(
|
||||
value: unknown,
|
||||
options: { strictWhitelist?: boolean } = {},
|
||||
): CanvasGenerationInputs | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
if ('version' in value || 'action' in value) {
|
||||
return cloneV2GenerationInputs(value, options.strictWhitelist === true);
|
||||
}
|
||||
return hydrateLegacyGenerationInputs(value, options.strictWhitelist === true);
|
||||
}
|
||||
Reference in New Issue
Block a user