fix(canvas): 画布资源恢复 - 生成结果下载或 OSS 落盘失败支持原任务重新加载

This commit is contained in:
ddCat
2026-08-25 15:06:45 +08:00
parent 0cbe24a19f
commit 6ed1eb5a7c
15 changed files with 116 additions and 35 deletions

View File

@@ -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>;
}

View File

@@ -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 &&