Broaden canvas workflow import compatibility

This commit is contained in:
Codex
2026-05-11 19:39:01 +08:00
parent 927faacc5f
commit 96555bdf51

View File

@@ -223,16 +223,42 @@ function normalizeCanvasProjectState(value: unknown): CanvasProjectState {
};
}
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function firstString(...values: unknown[]) {
return values.find((value): value is string => typeof value === 'string' && value.trim().length > 0)?.trim() || '';
}
function firstNumber(...values: unknown[]) {
return values.find((value): value is number => typeof value === 'number' && Number.isFinite(value));
}
function firstArray(...values: unknown[]) {
return values.find((value): value is unknown[] => Array.isArray(value)) || [];
}
function getExternalNodes(value: unknown): unknown[] {
const input = asRecord(value);
return firstArray(input.nodes, input.elements, input.items, input.cells, input.blocks);
}
function getExternalConnections(value: unknown): unknown[] {
const input = asRecord(value);
return firstArray(input.connections, input.edges, input.links, input.lines);
}
function isExternalCanvasProject(value: unknown) {
if (!value || typeof value !== 'object') return false;
const input = value as { nodes?: unknown; view?: unknown; projectName?: unknown };
if (!Array.isArray(input.nodes)) return false;
return input.nodes.some(node => (
const input = asRecord(value);
const nodes = getExternalNodes(value);
if (nodes.length === 0) return false;
return nodes.some(node => (
!!node
&& typeof node === 'object'
&& typeof (node as { type?: unknown }).type === 'string'
&& ['text-node', 'gen-image', 'image-node', 'image'].includes((node as { type: string }).type)
)) || !!input.view || typeof input.projectName === 'string';
)) || !!input.view || typeof input.projectName === 'string' || typeof input.name === 'string';
}
function getExternalAssetKey(value?: unknown) {
@@ -241,6 +267,22 @@ function getExternalAssetKey(value?: unknown) {
return '';
}
function getAssetLookupKeys(pathOrName: string) {
const normalized = pathOrName.replace(/\\/g, '/').replace(/^\.?\//, '');
const name = normalized.split('/').pop() || normalized;
const withoutExt = normalized.replace(/\.[^.]+$/, '');
const nameWithoutExt = name.replace(/\.[^.]+$/, '');
const afterAssets = normalized.includes('/assets/') ? normalized.split('/assets/').pop() || normalized : normalized.startsWith('assets/') ? normalized.slice('assets/'.length) : '';
return Array.from(new Set([
normalized,
withoutExt,
name,
nameWithoutExt,
afterAssets,
afterAssets ? afterAssets.replace(/\.[^.]+$/, '') : '',
].filter(Boolean)));
}
function resolveExternalAssetUrl(value: unknown, assetMap?: ExternalAssetMap) {
if (typeof value !== 'string' || !value) return '';
const assetKey = getExternalAssetKey(value);
@@ -280,6 +322,17 @@ function getImportCanvasCandidate(payload: unknown) {
|| parsed;
}
function normalizeExternalNodeType(type: string, node: Record<string, unknown>, settings: Record<string, unknown>): CanvasNodeType {
const normalized = type.toLowerCase().replace(/[_\s-]+/g, '');
if (normalized.includes('text')) return 'text';
if (normalized.includes('img2img') || normalized.includes('imagetoimage')) return 'img2img';
if (normalized.includes('text2img') || normalized.includes('txt2img') || normalized.includes('texttoimage')) return 'text2img';
if (normalized.includes('gen') || normalized.includes('generate')) return 'image';
if (normalized.includes('image') || normalized.includes('asset')) return 'image';
if (firstString(settings.text, node.text, node.content) && !firstString(node.content, node.imageUrl, node.url, node.src)) return 'text';
return Array.isArray(settings.generatedImages) || firstString(node.prompt, settings.prompt) ? 'image' : 'text';
}
function normalizeExternalRatio(value: unknown) {
if (typeof value !== 'string' || !value) return undefined;
const match = value.match(/^(\d+)\s*x\s*(\d+)$/i);
@@ -307,32 +360,26 @@ function normalizeExternalResolution(value: unknown) {
}
function convertExternalCanvasProject(value: unknown, assetMap?: ExternalAssetMap): CanvasProjectState {
const input = value as {
nodes?: unknown[];
connections?: unknown[];
groups?: unknown[];
view?: { x?: unknown; y?: unknown; zoom?: unknown };
};
const input = asRecord(value);
const view = asRecord(input.view || input.viewport);
const createdAt = nowIso();
const nodes = (Array.isArray(input.nodes) ? input.nodes : []).map((rawNode, index): CanvasNode | null => {
const nodes = getExternalNodes(value).map((rawNode, index): CanvasNode | null => {
if (!rawNode || typeof rawNode !== 'object') return null;
const node = rawNode as Record<string, unknown>;
const type = typeof node.type === 'string' ? node.type : '';
const settings = node.settings && typeof node.settings === 'object' ? node.settings as Record<string, unknown> : {};
const id = typeof node.id === 'string' ? node.id : createId('external-node');
const title = typeof node.nodeName === 'string' ? node.nodeName : typeof node.title === 'string' ? node.title : '外部节点';
const x = typeof node.x === 'number' ? node.x : 120 + index * 420;
const y = typeof node.y === 'number' ? node.y : 120;
const width = typeof node.width === 'number' ? node.width : 360;
const height = typeof node.height === 'number' ? node.height : 420;
if (type === 'text-node') {
const text = typeof settings.text === 'string'
? settings.text
: typeof node.prompt === 'string'
? node.prompt
: typeof node.content === 'string'
? node.content
: '';
const position = asRecord(node.position);
const size = asRecord(node.size);
const data = asRecord(node.data);
const settings = asRecord(node.settings || data.settings || data);
const rawType = firstString(node.type, data.type, settings.type);
const type = normalizeExternalNodeType(rawType, node, settings);
const id = firstString(node.id, data.id) || createId('external-node');
const title = firstString(node.nodeName, node.title, node.name, data.label, data.title, settings.title) || '外部节点';
const x = firstNumber(node.x, position.x, data.x) ?? 120 + index * 420;
const y = firstNumber(node.y, position.y, data.y) ?? 120;
const width = firstNumber(node.width, size.width, data.width) ?? 360;
const height = firstNumber(node.height, size.height, data.height) ?? 420;
if (type === 'text') {
const text = firstString(settings.text, settings.prompt, node.text, node.prompt, node.content, data.text, data.prompt);
return {
id,
type: 'text',
@@ -347,8 +394,8 @@ function convertExternalCanvasProject(value: unknown, assetMap?: ExternalAssetMa
updatedAt: createdAt,
};
}
const contentUrl = resolveExternalAssetUrl(node.content, assetMap);
const thumbnailUrl = resolveExternalAssetUrl(node.thumbnailUrl, assetMap);
const contentUrl = resolveExternalAssetUrl(node.content || node.imageUrl || node.url || node.src || data.imageUrl || data.url, assetMap);
const thumbnailUrl = resolveExternalAssetUrl(node.thumbnailUrl || node.thumbnail || data.thumbnailUrl, assetMap);
const generatedImages = Array.isArray(settings.generatedImages) ? settings.generatedImages : [];
const outputImages = generatedImages
.map((item) => item && typeof item === 'object' ? item as Record<string, unknown> : null)
@@ -361,12 +408,12 @@ function convertExternalCanvasProject(value: unknown, assetMap?: ExternalAssetMa
})
.filter(Boolean);
const imageUrl = outputImages[0] || contentUrl || thumbnailUrl;
const prompt = typeof node.prompt === 'string' ? node.prompt : '';
const prompt = firstString(node.prompt, settings.prompt, data.prompt);
const ratio = normalizeExternalRatio(settings.ratio);
const resolution = normalizeExternalResolution(settings.ratio || settings.previewSize || settings.previewActualSize);
return {
id,
type: type === 'gen-image' ? 'image' : 'image',
type,
x,
y,
width: Math.max(320, width),
@@ -391,17 +438,19 @@ function convertExternalCanvasProject(value: unknown, assetMap?: ExternalAssetMa
};
}).filter((node): node is CanvasNode => !!node);
const connections = (Array.isArray(input.connections) ? input.connections : []).map((rawConnection): CanvasConnection | null => {
const connections = getExternalConnections(value).map((rawConnection): CanvasConnection | null => {
if (!rawConnection || typeof rawConnection !== 'object') return null;
const connection = rawConnection as Record<string, unknown>;
const sourceNodeId = typeof connection.sourceNodeId === 'string' ? connection.sourceNodeId : typeof connection.from === 'string' ? connection.from : '';
const targetNodeId = typeof connection.targetNodeId === 'string' ? connection.targetNodeId : typeof connection.to === 'string' ? connection.to : '';
const source = asRecord(connection.source);
const target = asRecord(connection.target);
const sourceNodeId = firstString(connection.sourceNodeId, connection.from, connection.source, source.nodeId, source.cell, source.id);
const targetNodeId = firstString(connection.targetNodeId, connection.to, connection.target, target.nodeId, target.cell, target.id);
if (!sourceNodeId || !targetNodeId) return null;
return {
id: typeof connection.id === 'string' ? connection.id : createId('connection'),
id: firstString(connection.id) || createId('connection'),
sourceNodeId,
targetNodeId,
label: typeof connection.label === 'string' ? connection.label : undefined,
label: firstString(connection.label) || undefined,
createdAt,
};
}).filter((connection): connection is CanvasConnection => !!connection);
@@ -418,9 +467,9 @@ function convertExternalCanvasProject(value: unknown, assetMap?: ExternalAssetMa
groups: [],
assets: imageAssets,
viewport: {
x: typeof input.view?.x === 'number' ? input.view.x : 0,
y: typeof input.view?.y === 'number' ? input.view.y : 0,
zoom: typeof input.view?.zoom === 'number' ? input.view.zoom : 1,
x: typeof view.x === 'number' ? view.x : 0,
y: typeof view.y === 'number' ? view.y : 0,
zoom: typeof view.zoom === 'number' ? view.zoom : 1,
},
});
}
@@ -1034,18 +1083,17 @@ export function InfiniteCanvasWorkspace() {
const relativePath = file.webkitRelativePath || file.name;
if (!relativePath.includes('/assets/') && !relativePath.startsWith('assets/')) return false;
if (file.type.startsWith('video/')) return false;
const name = relativePath.split('/').pop() || file.name;
const key = name.replace(/\.[^.]+$/, '');
return requiredAssetKeys.has(key);
return getAssetLookupKeys(relativePath).some(key => requiredAssetKeys.has(key));
});
const assetMap: ExternalAssetMap = new Map();
await Promise.all(assetFiles.map(async (file) => {
const relativePath = file.webkitRelativePath || file.name;
const name = relativePath.split('/').pop() || file.name;
const key = name.replace(/\.[^.]+$/, '');
const url = await fileToDataUrl(file);
const type = file.type.startsWith('video/') ? 'video' : 'image';
assetMap.set(key, { url, name, type });
getAssetLookupKeys(relativePath).forEach(key => {
assetMap.set(key, { url, name, type });
});
}));
const importedState = getImportCanvasState(parsed, assetMap);
if (importedState.nodes.length === 0) {