import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type CanvasNodeMetadata } from "@/types/canvas"; import { resetGenerationTaskMetadata } from "@/lib/canvas/canvas-task-state"; export function hasImageBatchResult(node: CanvasNodeData) { return Boolean(node.metadata?.content || node.metadata?.storageKey); } export function liveImageBatchChildren(root: CanvasNodeData, nodes: CanvasNodeData[]) { if (root.type !== CanvasNodeType.Image) return []; // 清理是破坏性写路径,父节点的旧索引不能证明子节点仍归这个批次所有。 return nodes.filter((node) => node.id !== root.id && node.type === CanvasNodeType.Image && node.metadata?.batchRootId === root.id); } export function retireImageBatchChildren(root: CanvasNodeData, nodes: CanvasNodeData[], connections: CanvasConnection[]) { const children = liveImageBatchChildren(root, nodes); if (!children.length) return { nodes, connections, removedIds: [] as string[] }; const removedIds = children.filter((node) => !hasImageBatchResult(node)).map((node) => node.id); const removed = new Set(removedIds); const kept = children.filter(hasImageBatchResult); const nextNodes = nodes .filter((node) => !removed.has(node.id)) .map((node) => { if (node.id === root.id) { const metadata = { ...node.metadata, imageBatchExpanded: true }; delete metadata.batchChildIds; delete metadata.primaryImageId; delete metadata.batchFailedCount; if (!hasImageBatchResult(node)) metadata.status = "idle"; return { ...node, metadata }; } const index = kept.findIndex((item) => item.id === node.id); if (index < 0) return node; const metadata = { ...node.metadata }; delete metadata.batchRootId; return { ...node, position: { x: root.position.x - node.width - 48, y: root.position.y + index * (node.height + 24) }, metadata, }; }); return { nodes: nextNodes, connections: connections.filter((connection) => !removed.has(connection.fromNodeId) && !removed.has(connection.toNodeId)), removedIds, }; } export function cancelIncompleteImageBatch(rootId: string, childIds: string[], nodes: CanvasNodeData[], connections: CanvasConnection[]) { const childIdSet = new Set(childIds); const removedIds = nodes.filter((node) => childIdSet.has(node.id) && node.metadata?.batchRootId === rootId && !hasImageBatchResult(node)).map((node) => node.id); const removed = new Set(removedIds); const nextNodes = nodes .filter((node) => !removed.has(node.id)) .map((node) => { if (node.id !== rootId) return node; const remaining = (node.metadata?.batchChildIds || []).filter((id) => !removed.has(id) && nodes.some((item) => item.id === id && item.metadata?.batchRootId === rootId && hasImageBatchResult(item))); const hasContent = hasImageBatchResult(node); const metadata = { ...node.metadata, batchChildIds: remaining.length ? remaining : undefined, isBatchRoot: remaining.length > 1 ? true : undefined, imageBatchExpanded: remaining.length > 1 ? true : undefined, status: hasContent ? node.metadata?.status : "idle", }; if (!hasContent) { delete metadata.errorDetails; delete metadata.generationErrorCode; delete metadata.failedPromptFingerprint; } if (remaining.length <= 1) { delete metadata.batchFailedCount; delete metadata.primaryImageId; delete metadata.isBatchRoot; delete metadata.imageBatchExpanded; delete metadata.batchChildIds; } return { ...node, metadata }; }); const remainingIds = new Set( nextNodes.find((node) => node.id === rootId)?.metadata?.batchChildIds || [], ); const detached = nextNodes.map((node) => { if (!childIdSet.has(node.id) || node.metadata?.batchRootId !== rootId || remainingIds.has(node.id)) return node; const metadata = { ...node.metadata }; delete metadata.batchRootId; return { ...node, metadata }; }); return { nodes: detached, connections: connections.filter((connection) => !removed.has(connection.fromNodeId) && !removed.has(connection.toNodeId)), removedIds, }; } export function failedImageBatchChildren(root: CanvasNodeData, nodes: CanvasNodeData[]) { if (root.type !== CanvasNodeType.Image || !root.metadata?.isBatchRoot) return []; const nodeById = new Map(nodes.map((node) => [node.id, node])); return (root.metadata.batchChildIds || []) .map((id) => nodeById.get(id)) .filter((node): node is CanvasNodeData => Boolean(node && node.type === CanvasNodeType.Image && node.metadata?.batchRootId === root.id && node.metadata.status === "error")); } export function markImageBatchRetrying(rootId: string, childIds: string[], nodes: CanvasNodeData[]): CanvasNodeData[] { const retryingIds = new Set(childIds); return nodes.map((node) => { if (node.id !== rootId && !retryingIds.has(node.id)) return node; return { ...node, metadata: { ...node.metadata, status: "loading", ...(node.id === rootId ? { batchFailedCount: childIds.length } : {}), errorDetails: undefined, generationErrorCode: undefined, resourceReloadAvailable: undefined, failedPromptFingerprint: undefined, }, }; }); } export function restoreUnsubmittedImageBatchChild(current: CanvasNodeData, original: CanvasNodeData): CanvasNodeData { if (current.metadata?.status !== "loading") return current; return { ...current, metadata: { ...current.metadata, status: "error", errorDetails: original.metadata?.errorDetails || "重试请求未提交", generationErrorCode: original.metadata?.generationErrorCode, resourceReloadAvailable: original.metadata?.resourceReloadAvailable, failedPromptFingerprint: original.metadata?.failedPromptFingerprint, }, }; } export function reconcileImageBatchRoot(root: CanvasNodeData, nodes: CanvasNodeData[]) { if (root.type !== CanvasNodeType.Image || !root.metadata?.isBatchRoot) return root; const nodeById = new Map(nodes.map((node) => [node.id, node])); const children = (root.metadata.batchChildIds || []) .map((id) => nodeById.get(id)) .filter((node): node is CanvasNodeData => Boolean(node && node.type === CanvasNodeType.Image && node.metadata?.batchRootId === root.id)); if (!children.length) { const metadata: CanvasNodeMetadata = resetGenerationTaskMetadata(root.metadata, root.metadata?.status); delete metadata.batchChildIds; delete metadata.batchFailedCount; delete metadata.primaryImageId; delete metadata.isBatchRoot; delete metadata.imageBatchExpanded; delete metadata.errorDetails; delete metadata.generationErrorCode; delete metadata.resourceReloadAvailable; delete metadata.failedPromptFingerprint; if (!hasImageBatchResult(root)) metadata.status = "idle"; return { ...root, metadata }; } const primary = children.find((node) => node.id === root.metadata?.primaryImageId && hasImageBatchResult(node)) || children.find(hasImageBatchResult); const loading = children.some((node) => node.metadata?.status === "loading"); const failed = children.find((node) => node.metadata?.status === "error"); // 批次父节点只是子任务的投影,不能保留上一轮单图任务的身份/终态。 const metadata: CanvasNodeMetadata = resetGenerationTaskMetadata(root.metadata, root.metadata?.status); metadata.batchFailedCount = children.filter((node) => node.metadata?.status === "error").length; if (primary) { metadata.content = primary.metadata?.content; metadata.storageKey = primary.metadata?.storageKey; metadata.assetId = primary.metadata?.assetId; metadata.mimeType = primary.metadata?.mimeType; metadata.bytes = primary.metadata?.bytes; metadata.naturalWidth = primary.metadata?.naturalWidth; metadata.naturalHeight = primary.metadata?.naturalHeight; metadata.primaryImageId = primary.id; if (primary.metadata?.producedModel) metadata.producedModel = primary.metadata.producedModel; else delete metadata.producedModel; metadata.status = "success"; delete metadata.errorDetails; delete metadata.generationErrorCode; delete metadata.failedPromptFingerprint; } else { delete metadata.content; delete metadata.producedModel; delete metadata.storageKey; delete metadata.assetId; delete metadata.mimeType; delete metadata.bytes; delete metadata.naturalWidth; delete metadata.naturalHeight; delete metadata.primaryImageId; metadata.status = loading ? "loading" : failed ? "error" : "idle"; metadata.errorDetails = failed?.metadata?.errorDetails; metadata.generationErrorCode = failed?.metadata?.generationErrorCode; metadata.resourceReloadAvailable = failed?.metadata?.resourceReloadAvailable; metadata.failedPromptFingerprint = failed?.metadata?.failedPromptFingerprint; } return { ...root, metadata }; }