From 6ed1eb5a7caf044ee523b67465359378fe21526d Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Tue, 25 Aug 2026 15:06:45 +0800 Subject: [PATCH] =?UTF-8?q?fix(canvas):=20=E7=94=BB=E5=B8=83=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E6=81=A2=E5=A4=8D=20-=20=E7=94=9F=E6=88=90=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E4=B8=8B=E8=BD=BD=E6=88=96=20OSS=20=E8=90=BD=E7=9B=98?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E6=94=AF=E6=8C=81=E5=8E=9F=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E9=87=8D=E6=96=B0=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/content/docs/progress/pending-test.mdx | 8 +++ .../components/canvas/canvas-node-content.tsx | 34 +++++++++-- web/src/components/canvas/canvas-node.tsx | 7 ++- .../lib/canvas/canvas-generation-task-sync.ts | 10 +++- .../lib/canvas/canvas-image-batch-retry.ts | 3 + .../lib/canvas/canvas-project-generation.ts | 1 + .../canvas-image-generation-executor.ts | 2 + .../canvas-media-generation-executors.ts | 1 + .../canvas/canvas-project-world-layers.tsx | 2 + .../canvas/canvas-text-generation-executor.ts | 2 +- web/src/pages/canvas/project.tsx | 17 ++++++ .../canvas/use-canvas-generation-executor.ts | 3 +- .../canvas/use-canvas-generation-retry.ts | 2 +- web/src/pages/canvas/use-canvas-generation.ts | 58 +++++++++++-------- web/src/types/canvas.ts | 1 + 15 files changed, 116 insertions(+), 35 deletions(-) diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 409742b0a..126fd6968 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -72,3 +72,11 @@ description: 影策已实现功能的手工验证清单与未确认边界 - [ ] 在一台依赖已安装和一台全新工作区分别运行 `.\scripts\start-local.ps1`。 - [ ] 验证端口被占用时脚本会安全退出,且不会关闭已有进程或使用正式开发数据目录。 + +## 画布生成结果资源恢复 + +- [x] `web` 前端 TypeScript 检查与生产构建通过。 +- [ ] 模拟图片生成任务已成功但结果下载失败,确认节点显示“重新加载资源”和“重新生成”两个按钮。 +- [ ] 点击“重新加载资源”,确认只重新查询并消费原任务结果,不创建新的生成任务或重复扣费。 +- [ ] 模拟视频结果落盘/OSS 上传失败后再次加载,确认恢复成功会清除错误状态并保留原任务信息。 +- [ ] 点击“重新生成”,确认仍走原有生成重试流程;普通生成失败仍只显示“重新生成”。 diff --git a/web/src/components/canvas/canvas-node-content.tsx b/web/src/components/canvas/canvas-node-content.tsx index 8bd8546e4..0771bb12e 100644 --- a/web/src/components/canvas/canvas-node-content.tsx +++ b/web/src/components/canvas/canvas-node-content.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode, type RefObject } from "react"; -import { AlertCircle, BookOpenCheck, Clock3, FileText, Image as ImageIcon, LoaderCircle, Music2, Pencil, Play, RefreshCw, Video } from "lucide-react"; +import { AlertCircle, BookOpenCheck, Clock3, Download, FileText, Image as ImageIcon, LoaderCircle, Music2, Pencil, Play, RefreshCw, Video } from "lucide-react"; import { VideoPlayer } from "@/components/video-player"; import { CONTENT_MODERATION_ERROR_CODE, generationErrorMessage, isContentModerationError } from "@/lib/generation-error"; @@ -43,6 +43,7 @@ type CanvasNodeContentProps = { onStopEditing: () => void; mentionReferences: CanvasResourceReference[]; onRetry?: (node: CanvasNodeData) => void; + onReloadResource?: (node: CanvasNodeData) => void; onOpenTaskDetails?: (node: CanvasNodeData) => void; onToggleBatch?: () => void; reduceMediaEffects?: boolean; @@ -58,7 +59,7 @@ export function CanvasNodeContent(props: CanvasNodeContentProps) { if (hasCustomContent && props.renderNodeContent) return props.renderNodeContent(props.node); if (props.isBatchRoot) return ; if (props.node.metadata?.status === "loading") return ; - if (props.node.metadata?.status === "error") return ; + if (props.node.metadata?.status === "error") return ; const Renderer = nodeContentRenderers[props.node.type]; return Renderer ? : ; @@ -196,7 +197,7 @@ function shortTaskId(id: string) { return `${id.slice(0, 14)}...${id.slice(-4)}`; } -function ErrorContent({ node, theme, onRetry }: Pick) { +function ErrorContent({ node, theme, onRetry, onReloadResource }: Pick) { const moderationFailure = node.metadata?.generationErrorCode === CONTENT_MODERATION_ERROR_CODE || isContentModerationError(node.metadata?.errorDetails); const errorDisplayTask = { provider: node.metadata?.taskProvider, @@ -217,6 +218,29 @@ function ErrorContent({ node, theme, onRetry }: Pick 修改节点提示词后,可重新点击生成。 + ) : node.metadata?.resourceReloadAvailable ? ( +
+ + +
) : ( )} @@ -335,7 +359,7 @@ function ImageNodeContent(props: CanvasNodeContentProps) { const content = props.node.metadata?.status === "loading" ? : props.node.metadata?.status === "error" - ? + ? : ; return {content}; } diff --git a/web/src/components/canvas/canvas-node.tsx b/web/src/components/canvas/canvas-node.tsx index 47993f2e3..04ea52ee0 100644 --- a/web/src/components/canvas/canvas-node.tsx +++ b/web/src/components/canvas/canvas-node.tsx @@ -49,6 +49,7 @@ type CanvasNodeProps = { onToggleBatch?: (nodeId: string) => void; onSetBatchPrimary?: (node: CanvasNodeData) => void; onRetry?: (node: CanvasNodeData) => void; + onReloadResource?: (node: CanvasNodeData) => void; onOpenTaskDetails?: (node: CanvasNodeData) => void; onOpenVersions?: (node: CanvasNodeData) => void; onViewImage?: (node: CanvasNodeData) => void; @@ -92,6 +93,7 @@ export const CanvasNode = React.memo(function CanvasNode({ onToggleBatch, onSetBatchPrimary, onRetry, + onReloadResource, onOpenTaskDetails, onOpenVersions, onViewImage, @@ -360,6 +362,7 @@ export const CanvasNode = React.memo(function CanvasNode({ onContentChange={onContentChange} onStopEditing={() => setIsEditingContent(false)} onRetry={onRetry} + onReloadResource={onReloadResource} onOpenTaskDetails={onOpenTaskDetails} onToggleBatch={() => onToggleBatch?.(data.id)} reduceMediaEffects={reduceMediaEffects} @@ -425,7 +428,8 @@ export const CanvasNode = React.memo(function CanvasNode({ {hasImageContent ? ( } onClick={() => onSetBatchPrimary?.(data)} /> ) : null} - {data.metadata?.status === "error" ? } onClick={() => onRetry?.(data)} /> : null} + {data.metadata?.status === "error" && data.metadata.resourceReloadAvailable ? } onClick={() => onReloadResource?.(data)} /> : null} + {data.metadata?.status === "error" ? } onClick={() => onRetry?.(data)} /> : null} {data.metadata?.status === "error" ? } danger onClick={() => deleteNode?.(data)} /> : null} @@ -495,6 +499,7 @@ function areCanvasNodePropsEqual(previous: CanvasNodeProps, next: CanvasNodeProp previous.onToggleBatch === next.onToggleBatch && previous.onSetBatchPrimary === next.onSetBatchPrimary && previous.onRetry === next.onRetry && + previous.onReloadResource === next.onReloadResource && previous.onOpenTaskDetails === next.onOpenTaskDetails && previous.onOpenVersions === next.onOpenVersions && previous.onViewImage === next.onViewImage && diff --git a/web/src/lib/canvas/canvas-generation-task-sync.ts b/web/src/lib/canvas/canvas-generation-task-sync.ts index 0c15840c1..db5f4e00c 100644 --- a/web/src/lib/canvas/canvas-generation-task-sync.ts +++ b/web/src/lib/canvas/canvas-generation-task-sync.ts @@ -35,6 +35,11 @@ export function generationTaskMode(task: GenerationTask, fallback?: CanvasGenera return fallback || "image"; } +export function generationTaskCanReloadResource(task: GenerationTask) { + const mode = generationTaskMode(task); + return task.status === "succeeded" && (mode === "image" || mode === "video" || mode === "audio") && (Boolean(task.resultJson) || Boolean(task.outputs?.length)); +} + export function imageMetadata(image: UploadedImage): CanvasNodeMetadata { return { content: image.url, @@ -46,6 +51,7 @@ export function imageMetadata(image: UploadedImage): CanvasNodeMetadata { mimeType: image.mimeType, errorDetails: undefined, generationErrorCode: undefined, + resourceReloadAvailable: undefined, failedPromptFingerprint: undefined, }; } @@ -62,6 +68,7 @@ export function videoMetadata(video: UploadedFile): CanvasNodeMetadata { durationMs: video.durationMs, errorDetails: undefined, generationErrorCode: undefined, + resourceReloadAvailable: undefined, failedPromptFingerprint: undefined, }; } @@ -76,6 +83,7 @@ export function audioMetadata(audio: UploadedFile): CanvasNodeMetadata { durationMs: audio.durationMs, errorDetails: undefined, generationErrorCode: undefined, + resourceReloadAvailable: undefined, failedPromptFingerprint: undefined, }; } @@ -162,7 +170,7 @@ export async function buildGenerationTaskNodeResult(node: CanvasNodeData, task: return { ...node, type: CanvasNodeType.Text, - metadata: { ...node.metadata, content: result.text, richText: undefined, prompt, ...completedTaskMetadata(task), status: "success", errorDetails: undefined, generationErrorCode: undefined, failedPromptFingerprint: undefined }, + metadata: { ...node.metadata, content: result.text, richText: undefined, prompt, ...completedTaskMetadata(task), status: "success", errorDetails: undefined, generationErrorCode: undefined, resourceReloadAvailable: undefined, failedPromptFingerprint: undefined }, }; } diff --git a/web/src/lib/canvas/canvas-image-batch-retry.ts b/web/src/lib/canvas/canvas-image-batch-retry.ts index 0c2d5e4ac..03afb6eb7 100644 --- a/web/src/lib/canvas/canvas-image-batch-retry.ts +++ b/web/src/lib/canvas/canvas-image-batch-retry.ts @@ -20,6 +20,7 @@ export function markImageBatchRetrying(rootId: string, childIds: string[], nodes ...(node.id === rootId ? { batchFailedCount: childIds.length } : {}), errorDetails: undefined, generationErrorCode: undefined, + resourceReloadAvailable: undefined, failedPromptFingerprint: undefined, }, }; @@ -35,6 +36,7 @@ export function restoreUnsubmittedImageBatchChild(current: CanvasNodeData, origi status: "error", errorDetails: original.metadata?.errorDetails || "重试请求未提交", generationErrorCode: original.metadata?.generationErrorCode, + resourceReloadAvailable: original.metadata?.resourceReloadAvailable, failedPromptFingerprint: original.metadata?.failedPromptFingerprint, }, }; @@ -77,6 +79,7 @@ export function reconcileImageBatchRoot(root: CanvasNodeData, nodes: CanvasNodeD 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; } diff --git a/web/src/lib/canvas/canvas-project-generation.ts b/web/src/lib/canvas/canvas-project-generation.ts index 0514f4c52..de004d519 100644 --- a/web/src/lib/canvas/canvas-project-generation.ts +++ b/web/src/lib/canvas/canvas-project-generation.ts @@ -170,6 +170,7 @@ export function resetGenerationTaskMetadata(metadata: CanvasNodeMetadata | undef status, errorDetails: undefined, generationErrorCode: undefined, + resourceReloadAvailable: undefined, failedPromptFingerprint: undefined, }; delete next.taskId; diff --git a/web/src/pages/canvas/canvas-image-generation-executor.ts b/web/src/pages/canvas/canvas-image-generation-executor.ts index 5392122a7..962ed1c7d 100644 --- a/web/src/pages/canvas/canvas-image-generation-executor.ts +++ b/web/src/pages/canvas/canvas-image-generation-executor.ts @@ -97,6 +97,7 @@ export async function executeImageGeneration({ ...styleMetadata, imageBatchExpanded: count > 1 ? true : undefined, generationErrorCode: undefined, + resourceReloadAvailable: undefined, failedPromptFingerprint: undefined, }, }; @@ -115,6 +116,7 @@ export async function executeImageGeneration({ ...generationMetadata, ...styleMetadata, generationErrorCode: undefined, + resourceReloadAvailable: undefined, failedPromptFingerprint: undefined, }, })); diff --git a/web/src/pages/canvas/canvas-media-generation-executors.ts b/web/src/pages/canvas/canvas-media-generation-executors.ts index 4cb3af8b5..be2a9c7b3 100644 --- a/web/src/pages/canvas/canvas-media-generation-executors.ts +++ b/web/src/pages/canvas/canvas-media-generation-executors.ts @@ -51,6 +51,7 @@ export async function executeVideoGeneration({ status: NODE_STATUS_LOADING, errorDetails: undefined, generationErrorCode: undefined, + resourceReloadAvailable: undefined, failedPromptFingerprint: undefined, model: generationConfig.model, size: generationConfig.size, diff --git a/web/src/pages/canvas/canvas-project-world-layers.tsx b/web/src/pages/canvas/canvas-project-world-layers.tsx index 25a983773..74b3267d3 100644 --- a/web/src/pages/canvas/canvas-project-world-layers.tsx +++ b/web/src/pages/canvas/canvas-project-world-layers.tsx @@ -64,6 +64,7 @@ type CanvasProjectWorldLayersProps = { onToggleBatch: (nodeId: string) => void; onSetBatchPrimary: (node: CanvasNodeData) => void; onRetry: (node: CanvasNodeData) => void; + onReloadResource: (node: CanvasNodeData) => void; onOpenTaskDetails: (node: CanvasNodeData) => void; onOpenVersions: (node: CanvasNodeData) => void; onViewImage: (node: CanvasNodeData) => void; @@ -161,6 +162,7 @@ export const CanvasProjectWorldLayers = memo(function CanvasProjectWorldLayers(p onToggleBatch={props.onToggleBatch} onSetBatchPrimary={props.onSetBatchPrimary} onRetry={props.onRetry} + onReloadResource={props.onReloadResource} onOpenTaskDetails={props.onOpenTaskDetails} onOpenVersions={props.onOpenVersions} onViewImage={props.onViewImage} diff --git a/web/src/pages/canvas/canvas-text-generation-executor.ts b/web/src/pages/canvas/canvas-text-generation-executor.ts index 75e3ecd91..2c24fb0de 100644 --- a/web/src/pages/canvas/canvas-text-generation-executor.ts +++ b/web/src/pages/canvas/canvas-text-generation-executor.ts @@ -85,6 +85,6 @@ export async function executeTextGeneration({ if (controller.signal.aborted) return; void answers; if (isConfigNode) { - setNodes((current) => current.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_SUCCESS, errorDetails: undefined, generationErrorCode: undefined, failedPromptFingerprint: undefined } } : node))); + setNodes((current) => current.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_SUCCESS, errorDetails: undefined, generationErrorCode: undefined, resourceReloadAvailable: undefined, failedPromptFingerprint: undefined } } : node))); } } diff --git a/web/src/pages/canvas/project.tsx b/web/src/pages/canvas/project.tsx index 1a1dc027a..bb482d9ab 100644 --- a/web/src/pages/canvas/project.tsx +++ b/web/src/pages/canvas/project.tsx @@ -88,6 +88,7 @@ import { CanvasProjectWorldLayers } from "./canvas-project-world-layers"; import { CanvasNodeActionContext } from "@/components/canvas/canvas-node-action-context"; import { CanvasNodeGraphContext, type CanvasNodeGraphContextValue } from "@/components/canvas/canvas-node-graph-context"; import { CanvasRefreshShell } from "./canvas-refresh-shell"; +import { queryGenerationTask } from "@/services/api/task-center"; import type { CanvasImageEmotionPayload } from "@/components/canvas/canvas-node-emotion-panel"; import { CanvasEmotionWorkspace } from "@/components/canvas/canvas-emotion-workspace"; import { removeCanvasDrawing } from "@/lib/canvas/canvas-drawing-storage"; @@ -1515,6 +1516,21 @@ function InfiniteCanvasPage() { bindGenerationTask, applyGenerationTaskResult, }); + const reloadCanvasNodeResource = useCallback( + async (node: CanvasNodeData) => { + const taskId = node.metadata?.taskId; + if (!taskId || !node.metadata?.resourceReloadAvailable) return; + setNodes((current) => current.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, status: "loading", taskStage: "正在重新加载资源", errorDetails: undefined } } : item))); + try { + const task = await queryGenerationTask(taskId); + if (task.status !== "succeeded") throw new Error("原生成任务尚未成功,无法重新加载资源"); + await applyGenerationTaskResult(node.id, task); + } catch (error) { + setNodes((current) => current.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, status: "error", errorDetails: error instanceof Error ? error.message : "资源重新加载失败", resourceReloadAvailable: true } } : item))); + } + }, + [applyGenerationTaskResult, setNodes], + ); const reconcileImageBatchRootNode = useCallback((rootId: string) => { setNodes((current) => { const root = current.find((item) => item.id === rootId); @@ -2002,6 +2018,7 @@ function InfiniteCanvasPage() { onToggleBatch={toggleBatchExpanded} onSetBatchPrimary={setBatchPrimary} onRetry={retryCanvasNode} + onReloadResource={reloadCanvasNodeResource} onOpenTaskDetails={openCanvasNodeTaskDetails} onOpenVersions={openCanvasNodeVersions} onViewImage={viewCanvasNodeImage} diff --git a/web/src/pages/canvas/use-canvas-generation-executor.ts b/web/src/pages/canvas/use-canvas-generation-executor.ts index c072cce98..a56caff7b 100644 --- a/web/src/pages/canvas/use-canvas-generation-executor.ts +++ b/web/src/pages/canvas/use-canvas-generation-executor.ts @@ -124,6 +124,7 @@ export function useCanvasGenerationExecutor({ taskCreatedAt: new Date().toISOString(), errorDetails: undefined, generationErrorCode: undefined, + resourceReloadAvailable: undefined, failedPromptFingerprint: undefined, }, } @@ -235,7 +236,7 @@ export function useCanvasGenerationExecutor({ if (markSourceStatus) setNodes((current) => current.map((node) => - node.id === nodeId ? { ...node, metadata: { ...node.metadata, prompt: statusPrompt, status: NODE_STATUS_LOADING, errorDetails: undefined, generationErrorCode: undefined, failedPromptFingerprint: undefined } } : node, + node.id === nodeId ? { ...node, metadata: { ...node.metadata, prompt: statusPrompt, status: NODE_STATUS_LOADING, errorDetails: undefined, generationErrorCode: undefined, resourceReloadAvailable: undefined, failedPromptFingerprint: undefined } } : node, ), ); diff --git a/web/src/pages/canvas/use-canvas-generation-retry.ts b/web/src/pages/canvas/use-canvas-generation-retry.ts index 7cbcccffb..40be4c318 100644 --- a/web/src/pages/canvas/use-canvas-generation-retry.ts +++ b/web/src/pages/canvas/use-canvas-generation-retry.ts @@ -201,7 +201,7 @@ export function useCanvasGenerationRetry({ : undefined; setRunningNodeId(node.id); - setNodes((current) => current.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_LOADING, errorDetails: undefined, generationErrorCode: undefined, failedPromptFingerprint: undefined } } : item))); + setNodes((current) => current.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_LOADING, errorDetails: undefined, generationErrorCode: undefined, resourceReloadAvailable: undefined, failedPromptFingerprint: undefined } } : item))); const controller = startGenerationRequest(node.id, sourceNode.id, node.id); const retryContext = node.metadata?.taskId ? await createGenerationRetryContext(node.metadata.taskId, node.metadata.attemptGroupId) : {}; const runAndConsumeRetry = async (input: Parameters[0]) => { diff --git a/web/src/pages/canvas/use-canvas-generation.ts b/web/src/pages/canvas/use-canvas-generation.ts index e03e40f1d..cf67e6117 100644 --- a/web/src/pages/canvas/use-canvas-generation.ts +++ b/web/src/pages/canvas/use-canvas-generation.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState, type Dispatch, type SetStateA import { useQueryClient } from "@tanstack/react-query"; import { App } from "antd"; -import { applyGenerationTaskResultToNodes, generationTaskNodeId } from "@/lib/canvas/canvas-generation-task-sync"; +import { applyGenerationTaskResultToNodes, generationTaskCanReloadResource, generationTaskNodeId } from "@/lib/canvas/canvas-generation-task-sync"; import { applyCanvasGenerationTaskNodeEffect, isCanvasGenerationDurableAckError } from "@/services/canvas-generation-consumer"; import { consumeGenerationTaskNode, ensureCanvasNodeAsset } from "@/services/project-asset-sync"; import { listGenerationTasks, listTaskLogs, queryGenerationTask, subscribeGenerationTasks, type GenerationTask, type TaskLog } from "@/services/api/task-center"; @@ -120,6 +120,7 @@ export async function recoverCanvasGenerationTaskNode(input: { status: NODE_STATUS_SUCCESS, errorDetails: undefined, generationErrorCode: undefined, + resourceReloadAvailable: undefined, failedPromptFingerprint: undefined, storyboard: { rows: result.rows, visibleColumns: cinematicStoryboardColumns(item.metadata?.storyboard?.visibleColumns), referenceNodeIds: item.metadata?.storyboard?.referenceNodeIds || [] }, }, @@ -261,7 +262,7 @@ export function useCanvasGeneration({ projectId, domainProjectId, projectLoaded, ...node.metadata, ...generationTaskMetadata(task), status: failed ? NODE_STATUS_ERROR : hasCompletedContent ? NODE_STATUS_SUCCESS : NODE_STATUS_LOADING, - ...(failure || { errorDetails: undefined, generationErrorCode: undefined, failedPromptFingerprint: undefined }), + ...(failure || { errorDetails: undefined, generationErrorCode: undefined, resourceReloadAvailable: undefined, failedPromptFingerprint: undefined }), }, }; }), @@ -281,30 +282,37 @@ export function useCanvasGeneration({ projectId, domainProjectId, projectLoaded, const applyGenerationTaskResult = useCallback( async (nodeId: string, task: GenerationTask) => { - if (!task.outputs?.length && task.type === "canvas_text") { - const applied = await applyGenerationTaskResultToNodes(nodesRef.current, task, nodeId); - if (!applied.updated || !applied.node) throw new Error("画布中找不到对应任务节点"); - setNodes((current) => current.map((node) => (node.id === applied.nodeId ? applied.node! : node))); - return; + try { + if (!task.outputs?.length && task.type === "canvas_text") { + const applied = await applyGenerationTaskResultToNodes(nodesRef.current, task, nodeId); + if (!applied.updated || !applied.node) throw new Error("画布中找不到对应任务节点"); + setNodes((current) => current.map((node) => (node.id === applied.nodeId ? applied.node! : node))); + return; + } + await consumeGenerationTaskNode( + task, + nodeId, + 0, + async ({ task: materialized, output, effectKey, signal }) => { + await applyCanvasGenerationTaskNodeEffect({ + projectId, + nodeId, + task: materialized, + output, + effectKey, + signal, + nodesRef, + setNodes, + }); + }, + { signal: consumerControllerRef.current.signal }, + ); + } catch (error) { + if (generationTaskCanReloadResource(task)) { + setNodes((current) => current.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, resourceReloadAvailable: true } } : node))); + } + throw error; } - await consumeGenerationTaskNode( - task, - nodeId, - 0, - async ({ task: materialized, output, effectKey, signal }) => { - await applyCanvasGenerationTaskNodeEffect({ - projectId, - nodeId, - task: materialized, - output, - effectKey, - signal, - nodesRef, - setNodes, - }); - }, - { signal: consumerControllerRef.current.signal }, - ); }, [nodesRef, projectId, setNodes], ); diff --git a/web/src/types/canvas.ts b/web/src/types/canvas.ts index a32161644..aedd014c0 100644 --- a/web/src/types/canvas.ts +++ b/web/src/types/canvas.ts @@ -192,6 +192,7 @@ export type CanvasNodeMetadata = { locked?: boolean; errorDetails?: string; generationErrorCode?: string; + resourceReloadAvailable?: boolean; failedPromptFingerprint?: string; fontSize?: number; generationMode?: CanvasGenerationMode;