修改节点提示词后,可重新点击生成。
+ ) : 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;