import type { CanvasGenerationAction, CanvasGenerationInputField, CanvasGenerationInputReference, CanvasGenerationInputs, CanvasGenerationInputsHydrationState, } from './ImageCanvasEditorTypes'; export const CANVAS_GENERATION_ACTIONS = [ 'image.generate', 'scene.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[]; const REMIXABLE_CANVAS_GENERATION_ACTIONS = new Set([ 'image.generate', 'scene.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', 'ui-design.extract-assets', ]); // 中文注释:image.edit 是图片快速编辑的原位替换记录,结果层就是被覆盖后的原层; // 其 source 指向替换前的编辑目标,不能再作为“改造”新产物可靠恢复。继续修改应重新 // 使用“快速编辑”,所以 image.edit 只属于已知 action,不属于可改造 action。 const CANVAS_GENERATION_ACTION_SET = new Set(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 { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } function hasOnlyKeys( value: Record, allowedKeys: ReadonlySet, ) { 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); } function hasNormalizedCanvasGenerationInputsStructure( value: unknown, strictWhitelist: boolean, ): 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) && (!strictWhitelist || hasOnlyKeys(field, V2_GENERATION_FIELD_KEYS)) && isPresentString(field.id) && typeof field.title === 'string' && isGenerationInputValue(field.value), ) && value.references.every( (reference) => isRecord(reference) && (!strictWhitelist || 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), ) ); } export function isNormalizedCanvasGenerationInputsStructure( value: unknown, ): value is CanvasGenerationInputs & { version: 2; action: CanvasGenerationAction; } { return hasNormalizedCanvasGenerationInputsStructure(value, true); } export function isRemixableCanvasGenerationAction( action: CanvasGenerationAction, ) { return REMIXABLE_CANVAS_GENERATION_ACTIONS.has(action); } function cloneV2GenerationInputs( value: Record, strictWhitelist: boolean, ): CanvasGenerationInputs | null { if ( (strictWhitelist && !hasOnlyKeys(value, V2_GENERATION_INPUT_KEYS)) || !hasNormalizedCanvasGenerationInputsStructure(value, strictWhitelist) ) { return null; } return { version: 2, action: value.action, fields: value.fields.map((field) => ({ id: field.id, title: field.title, value: field.value, })), references: value.references.map((reference) => ({ id: reference.id, title: reference.title, ...(reference.label === undefined ? {} : { label: reference.label }), refType: reference.refType, refId: reference.refId, })), }; } function hydrateLegacyGenerationInputs( value: Record, 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)) || (strictWhitelist ? typeof field.title !== 'string' : !isPresentString(field.title)) || (strictWhitelist ? typeof field.value !== 'string' : !isPresentString(field.value)) ) { return []; } return [{ title: field.title as string, value: field.value as string }]; }); const references = rawReferences.flatMap((reference) => { if ( !isRecord(reference) || (strictWhitelist && !hasOnlyKeys(reference, LEGACY_GENERATION_REFERENCE_KEYS)) || (strictWhitelist ? typeof reference.title !== 'string' : !isPresentString(reference.title)) || (strictWhitelist ? typeof reference.label !== 'string' : !isPresentString(reference.label)) || (reference.refType !== 'project-resource' && reference.refType !== 'asset') || (strictWhitelist ? typeof reference.refId !== 'string' : !isPresentString(reference.refId)) ) { return []; } return [ { title: reference.title as string, label: reference.label as string, refType: reference.refType, refId: reference.refId as string, } satisfies CanvasGenerationInputReference, ]; }); if ( strictWhitelist && (fields.length !== rawFields.length || references.length !== rawReferences.length) ) { return null; } return strictWhitelist || fields.length || references.length ? { fields, references } : null; } export type CanvasGenerationInputsHydrationResult = { state: CanvasGenerationInputsHydrationState; inputs: CanvasGenerationInputs | null; }; export function hydrateCanvasGenerationInputsResult( value: unknown, options: { strictWhitelist?: boolean } = {}, ): CanvasGenerationInputsHydrationResult { if (!isRecord(value)) { return { state: 'absent', inputs: null }; } if ('version' in value || 'action' in value) { const inputs = cloneV2GenerationInputs( value, options.strictWhitelist === true, ); return inputs ? { state: 'valid', inputs } : { state: 'invalid-versioned', inputs: null }; } const inputs = hydrateLegacyGenerationInputs( value, options.strictWhitelist === true, ); return inputs ? { state: 'valid', inputs } : { state: 'absent', inputs: null }; } export function hydrateCanvasGenerationInputs( value: unknown, options: { strictWhitelist?: boolean } = {}, ): CanvasGenerationInputs | null { return hydrateCanvasGenerationInputsResult(value, options).inputs; }