fix(canvas): 画布资源恢复 - 生成结果下载或 OSS 落盘失败支持原任务重新加载
This commit is contained in:
@@ -72,3 +72,11 @@ description: 影策已实现功能的手工验证清单与未确认边界
|
||||
|
||||
- [ ] 在一台依赖已安装和一台全新工作区分别运行 `.\scripts\start-local.ps1`。
|
||||
- [ ] 验证端口被占用时脚本会安全退出,且不会关闭已有进程或使用正式开发数据目录。
|
||||
|
||||
## 画布生成结果资源恢复
|
||||
|
||||
- [x] `web` 前端 TypeScript 检查与生产构建通过。
|
||||
- [ ] 模拟图片生成任务已成功但结果下载失败,确认节点显示“重新加载资源”和“重新生成”两个按钮。
|
||||
- [ ] 点击“重新加载资源”,确认只重新查询并消费原任务结果,不创建新的生成任务或重复扣费。
|
||||
- [ ] 模拟视频结果落盘/OSS 上传失败后再次加载,确认恢复成功会清除错误状态并保留原任务信息。
|
||||
- [ ] 点击“重新生成”,确认仍走原有生成重试流程;普通生成失败仍只显示“重新生成”。
|
||||
|
||||
@@ -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 <ImageNodeContent {...props} />;
|
||||
if (props.node.metadata?.status === "loading") return <LoadingContent node={props.node} theme={props.theme} onOpenTaskDetails={props.onOpenTaskDetails} />;
|
||||
if (props.node.metadata?.status === "error") return <ErrorContent node={props.node} theme={props.theme} onRetry={props.onRetry} />;
|
||||
if (props.node.metadata?.status === "error") return <ErrorContent node={props.node} theme={props.theme} onRetry={props.onRetry} onReloadResource={props.onReloadResource} />;
|
||||
|
||||
const Renderer = nodeContentRenderers[props.node.type];
|
||||
return Renderer ? <Renderer {...props} /> : <UnknownNodeContent theme={props.theme} />;
|
||||
@@ -196,7 +197,7 @@ function shortTaskId(id: string) {
|
||||
return `${id.slice(0, 14)}...${id.slice(-4)}`;
|
||||
}
|
||||
|
||||
function ErrorContent({ node, theme, onRetry }: Pick<CanvasNodeContentProps, "node" | "theme" | "onRetry">) {
|
||||
function ErrorContent({ node, theme, onRetry, onReloadResource }: Pick<CanvasNodeContentProps, "node" | "theme" | "onRetry" | "onReloadResource">) {
|
||||
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<CanvasNodeContentProps, "no
|
||||
<div className="rounded-[var(--r-sm)] px-3 py-2 text-[var(--fs-label)] leading-4" style={{ background: theme.toolbar.itemHover, color: theme.node.muted }}>
|
||||
修改节点提示词后,可重新点击生成。
|
||||
</div>
|
||||
) : node.metadata?.resourceReloadAvailable ? (
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-8 items-center gap-1.5 rounded-[var(--r-md)] px-3 text-xs font-medium transition-colors"
|
||||
style={{ background: theme.accent.primary, color: theme.accent.onPrimary }}
|
||||
onClick={(event) => { event.stopPropagation(); onReloadResource?.(node); }}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<Download className="size-3.5" />
|
||||
重新加载资源
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-8 items-center gap-1.5 rounded-[var(--r-md)] px-3 text-xs font-medium transition-colors"
|
||||
style={{ background: theme.toolbar.itemHover, color: theme.node.text }}
|
||||
onClick={(event) => { event.stopPropagation(); onRetry?.(node); }}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<RefreshCw className="size-3.5" />
|
||||
重新生成
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
@@ -229,7 +253,7 @@ function ErrorContent({ node, theme, onRetry }: Pick<CanvasNodeContentProps, "no
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<RefreshCw className="size-3.5" />
|
||||
{node.metadata?.isBatchRoot ? "重试失败项" : "重试"}
|
||||
{node.metadata?.isBatchRoot ? "重新生成失败项" : "重新生成"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -335,7 +359,7 @@ function ImageNodeContent(props: CanvasNodeContentProps) {
|
||||
const content = props.node.metadata?.status === "loading"
|
||||
? <LoadingContent node={props.node} theme={props.theme} />
|
||||
: props.node.metadata?.status === "error"
|
||||
? <ErrorContent node={props.node} theme={props.theme} onRetry={props.onRetry} />
|
||||
? <ErrorContent node={props.node} theme={props.theme} onRetry={props.onRetry} onReloadResource={props.onReloadResource} />
|
||||
: <EmptyImageContent {...props} isBatchRoot={false} />;
|
||||
return <BatchFrame batchCount={props.batchCount} batchExpanded={props.batchExpanded} batchOpening={props.batchOpening} batchRecovering={props.batchRecovering} theme={props.theme} onToggleBatch={props.onToggleBatch}>{content}</BatchFrame>;
|
||||
}
|
||||
|
||||
@@ -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 ? (
|
||||
<BatchChildActionButton theme={theme} label={batchPrimary ? "当前主图" : "设为主图"} icon={<Star className={`size-3.5 ${batchPrimary ? "fill-current" : ""}`} style={{ color: theme.accent.primary }} />} onClick={() => onSetBatchPrimary?.(data)} />
|
||||
) : null}
|
||||
{data.metadata?.status === "error" ? <BatchChildActionButton theme={theme} label="重试" icon={<RefreshCw className="size-3.5" />} onClick={() => onRetry?.(data)} /> : null}
|
||||
{data.metadata?.status === "error" && data.metadata.resourceReloadAvailable ? <BatchChildActionButton theme={theme} label="重新加载资源" icon={<Download className="size-3.5" />} onClick={() => onReloadResource?.(data)} /> : null}
|
||||
{data.metadata?.status === "error" ? <BatchChildActionButton theme={theme} label="重新生成" icon={<RefreshCw className="size-3.5" />} onClick={() => onRetry?.(data)} /> : null}
|
||||
{data.metadata?.status === "error" ? <BatchChildActionButton theme={theme} label="删除" icon={<Trash2 className="size-3.5" />} danger onClick={() => deleteNode?.(data)} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -170,6 +170,7 @@ export function resetGenerationTaskMetadata(metadata: CanvasNodeMetadata | undef
|
||||
status,
|
||||
errorDetails: undefined,
|
||||
generationErrorCode: undefined,
|
||||
resourceReloadAvailable: undefined,
|
||||
failedPromptFingerprint: undefined,
|
||||
};
|
||||
delete next.taskId;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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<typeof runBackendCanvasGenerationTask>[0]) => {
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
@@ -192,6 +192,7 @@ export type CanvasNodeMetadata = {
|
||||
locked?: boolean;
|
||||
errorDetails?: string;
|
||||
generationErrorCode?: string;
|
||||
resourceReloadAvailable?: boolean;
|
||||
failedPromptFingerprint?: string;
|
||||
fontSize?: number;
|
||||
generationMode?: CanvasGenerationMode;
|
||||
|
||||
Reference in New Issue
Block a user