feat(runtime): 即梦本机运行时 - 接入安全生成闭环与桌面本机渠道

Refs #225

Co-authored-by: zkhyww <127009345+zkhyww@users.noreply.github.com>
This commit is contained in:
ddCatandzkhyww committed 2026-08-15 12:45:29 +08:00
1 parent 8f2232feee
commit 57e1820f41
179 files changed
+49746 -1897

No files matched your search

+508 -169
View File
@@ -3,12 +3,17 @@ import { useQueryClient } from "@tanstack/react-query";
import { App } from "antd";
import { applyGenerationTaskResultToNodes, generationTaskNodeId } from "@/lib/canvas/canvas-generation-task-sync";
import { ensureCanvasNodeAsset } from "@/services/project-asset-sync";
import { cancelGenerationTask, listGenerationTasks, listTaskLogs, queryGenerationTask, waitForGenerationTask, type GenerationTask, type TaskLog } from "@/services/api/task-center";
import { applyCanvasGenerationTaskNodeEffect, isCanvasGenerationDurableAckError } from "@/services/canvas-generation-consumer";
import { consumeGenerationTaskNode, ensureCanvasNodeAsset } from "@/services/project-asset-sync";
import { cancelGenerationTask, listGenerationTasks, listTaskLogs, queryGenerationTask, subscribeGenerationTasks, type GenerationTask, type TaskLog } from "@/services/api/task-center";
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
import { useCanvasStore } from "@/stores/canvas/use-canvas-store";
import { cinematicStoryboardColumns, storyboardRowsFromTask } from "@/lib/canvas/canvas-project-domain";
import { generationTaskMetadata } from "@/lib/canvas/canvas-project-generation";
import { generationFailureMetadata } from "@/lib/generation-error";
import { localDreaminaCancellationCopy, localDreaminaCancellationMessage, localDreaminaDetachOutcome } from "@/services/local-dreamina-task-projection";
import { runGenerationConsumer } from "@/services/generation-consumer-lifecycle";
import { consumeCanvasAgentGenerationContinuation } from "./use-canvas-agent-operations";
type CanvasGenerationRequest = {
targetNodeId: string;
@@ -31,12 +36,172 @@ const NODE_STATUS_LOADING = "loading" as const;
const NODE_STATUS_SUCCESS = "success" as const;
const NODE_STATUS_ERROR = "error" as const;
export function subscribeCanvasGenerationRecoveryTasks(ids: readonly string[], listener: (task: GenerationTask) => void, subscribe: (ids: readonly string[], listener: (task: GenerationTask) => void) => () => void = subscribeGenerationTasks) {
return subscribe(Array.from(new Set(ids)), listener);
}
export type CanvasGenerationRecoveryContext = {
projectId: string;
controller: AbortController;
signal: AbortSignal;
isCurrentProject: () => boolean;
};
export function createCanvasGenerationRecoveryCoordinator() {
let active:
| {
token: symbol;
controller: AbortController;
}
| undefined;
let transitionTail = Promise.resolve();
return {
switchProject(projectId: string, operation: (context: CanvasGenerationRecoveryContext) => Promise<void>) {
active?.controller.abort();
const token = Symbol(projectId);
const controller = new AbortController();
const previousTail = transitionTail;
const settled = previousTail.then(async () => {
if (controller.signal.aborted) throw new DOMException("The operation was aborted", "AbortError");
await operation({
projectId,
controller,
signal: controller.signal,
isCurrentProject: () => active?.token === token && !controller.signal.aborted,
});
});
active = { token, controller };
transitionTail = settled.then(
() => undefined,
() => undefined,
);
return settled;
},
async abortAndDrain() {
active?.controller.abort();
active = undefined;
await transitionTail;
},
};
}
export async function recoverCanvasGenerationTaskNode(input: {
projectId: string;
node: CanvasNodeData;
completed: GenerationTask;
continuationOnly: boolean;
nodesRef: { current: CanvasNodeData[] };
setNodes: Dispatch<SetStateAction<CanvasNodeData[]>>;
applyGenerationTaskResult: (nodeId: string, task: GenerationTask) => Promise<void>;
signal: AbortSignal;
isCurrentProject?: () => boolean;
consumeContinuation?: typeof consumeCanvasAgentGenerationContinuation;
}) {
const isCurrentProject = () => !input.signal.aborted && (input.isCurrentProject?.() ?? true);
if (!isCurrentProject()) return;
const consumeContinuation = input.consumeContinuation ?? consumeCanvasAgentGenerationContinuation;
const recoveryBaseNodes = useCanvasStore.getState().projects.find((project) => project.id === input.projectId)?.nodes ?? input.nodesRef.current;
try {
if (input.completed.projectId && input.completed.projectId !== input.projectId) throw new Error("生成任务不属于当前画布");
if (!isCurrentProject()) return;
if (input.completed.status === "failed" || input.completed.status === "cancelled") {
throw new Error(input.completed.error || (input.completed.status === "cancelled" ? "任务已取消" : "任务失败"));
}
if (!input.continuationOnly) {
if (input.node.type === CanvasNodeType.Script && input.completed.type === "agent_storyboard_rows") {
const result = storyboardRowsFromTask(input.completed);
const recoveredNodes = input.nodesRef.current.map((item) =>
item.id === input.node.id
? {
...item,
title: result.title || item.title,
metadata: {
...item.metadata,
...generationTaskMetadata(input.completed),
status: NODE_STATUS_SUCCESS,
errorDetails: undefined,
generationErrorCode: undefined,
failedPromptFingerprint: undefined,
storyboard: { rows: result.rows, visibleColumns: cinematicStoryboardColumns(item.metadata?.storyboard?.visibleColumns), referenceNodeIds: item.metadata?.storyboard?.referenceNodeIds || [] },
},
}
: item,
);
if (!isCurrentProject()) return;
input.nodesRef.current = recoveredNodes;
input.setNodes(recoveredNodes);
} else {
if (!isCurrentProject()) return;
await input.applyGenerationTaskResult(input.node.id, input.completed);
}
}
if (!isCurrentProject()) return;
const continuation = input.nodesRef.current.find((item) => item.id === input.node.id)?.metadata?.agentGenerationContinuation ?? input.node.metadata?.agentGenerationContinuation;
if (continuation?.status === "pending" && continuation.taskId === input.completed.id) {
await consumeContinuation(
input.completed,
continuation,
(nextContinuation) => {
if (!isCurrentProject()) return;
input.setNodes((current) =>
current.map((item) =>
item.id === input.node.id
? {
...item,
metadata: {
...item.metadata,
agentGenerationContinuation: nextContinuation,
...(nextContinuation.effectKey
? {
generationEffectKeys: Array.from(new Set([...(item.metadata?.generationEffectKeys || []), nextContinuation.effectKey])),
}
: {}),
},
}
: item,
),
);
},
{ projectId: input.projectId, nodeId: input.node.id, previousNodes: recoveryBaseNodes, nodesRef: input.nodesRef, setNodes: input.setNodes },
input.signal,
);
}
} catch (error) {
if (!isCurrentProject() || (error instanceof Error && error.name === "AbortError")) return;
if (isCanvasGenerationDurableAckError(error)) return;
const failure = generationFailureMetadata(error, input.node.metadata?.composerContent || input.node.metadata?.prompt || "");
input.setNodes((current) =>
current.map((item) =>
item.id === input.node.id
? {
...item,
metadata: {
...item.metadata,
status: input.continuationOnly ? item.metadata?.status : NODE_STATUS_ERROR,
...(input.continuationOnly ? {} : failure),
...(item.metadata?.agentGenerationContinuation?.status === "pending"
? {
agentGenerationContinuation: { ...item.metadata.agentGenerationContinuation, status: "failed" as const },
}
: {}),
},
}
: item,
),
);
}
}
export function useCanvasGeneration({ projectId, domainProjectId, projectLoaded, nodes, nodesRef, setNodes }: UseCanvasGenerationOptions) {
const { message, modal } = App.useApp();
const queryClient = useQueryClient();
const generationRequestsRef = useRef(new Map<string, CanvasGenerationRequest>());
const recoveringTaskIdsRef = useRef(new Set<string>());
const autoSavedTaskIdsRef = useRef(new Set<string>());
const consumerControllerRef = useRef(new AbortController());
const recoveryCoordinatorRef = useRef<ReturnType<typeof createCanvasGenerationRecoveryCoordinator> | null>(null);
if (!recoveryCoordinatorRef.current) recoveryCoordinatorRef.current = createCanvasGenerationRecoveryCoordinator();
const [runningNodeId, setRunningNodeId] = useState<string | null>(null);
const [taskDetail, setTaskDetail] = useState<GenerationTask | null>(null);
const [taskDetailLogs, setTaskDetailLogs] = useState<TaskLog[]>([]);
@@ -54,179 +219,349 @@ export function useCanvasGeneration({ projectId, domainProjectId, projectLoaded,
if (request?.controller === controller) generationRequestsRef.current.delete(targetNodeId);
}, []);
const stopGenerationByRunningId = useCallback((runningId: string) => {
const affectedNodeIds = new Set<string>();
generationRequestsRef.current.forEach((request) => {
if (request.runningNodeId !== runningId) return;
request.controller.abort();
generationRequestsRef.current.delete(request.targetNodeId);
affectedNodeIds.add(request.targetNodeId);
affectedNodeIds.add(request.originNodeId);
});
setRunningNodeId((current) => (current === runningId ? null : current));
if (!affectedNodeIds.size) return;
setNodes((current) => current.map((node) => affectedNodeIds.has(node.id) && node.metadata?.status === NODE_STATUS_LOADING ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_IDLE, errorDetails: undefined } } : node));
}, [setNodes]);
const stopGenerationByRunningId = useCallback(
(runningId: string) => {
const affectedNodeIds = new Set<string>();
generationRequestsRef.current.forEach((request) => {
if (request.runningNodeId !== runningId) return;
request.controller.abort();
generationRequestsRef.current.delete(request.targetNodeId);
affectedNodeIds.add(request.targetNodeId);
affectedNodeIds.add(request.originNodeId);
});
setRunningNodeId((current) => (current === runningId ? null : current));
if (!affectedNodeIds.size) return;
setNodes((current) => current.map((node) => (affectedNodeIds.has(node.id) && node.metadata?.status === NODE_STATUS_LOADING ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_IDLE, errorDetails: undefined } } : node)));
},
[setNodes],
);
const confirmStopGeneration = useCallback((nodeId: string) => {
modal.confirm({
title: "停止生成?",
content: "当前生成请求会被中断,已经生成完成的内容会保留。",
okText: "停止",
cancelText: "继续生成",
okButtonProps: { danger: true },
onOk: () => stopGenerationByRunningId(nodeId),
});
}, [modal, stopGenerationByRunningId]);
const confirmStopGeneration = useCallback(
(nodeId: string) => {
modal.confirm({
title: "停止生成?",
content: "当前生成请求会被中断,已经生成完成的内容会保留。",
okText: "停止",
cancelText: "继续生成",
okButtonProps: { danger: true },
onOk: () => stopGenerationByRunningId(nodeId),
});
},
[modal, stopGenerationByRunningId],
);
const cancelNodeTask = useCallback((node: CanvasNodeData) => {
const taskId = node.metadata?.taskId;
if (!taskId) {
confirmStopGeneration(node.id);
return;
}
modal.confirm({
title: "取消后台任务?",
content: "任务会在后端停止,已生成完成的内容仍会保留。",
okText: "取消任务",
cancelText: "继续生成",
okButtonProps: { danger: true },
onOk: async () => {
generationRequestsRef.current.get(node.id)?.controller.abort();
const task = await cancelGenerationTask(taskId);
setNodes((current) => current.map((item) => item.id === node.id ? { ...item, metadata: { ...item.metadata, ...generationTaskMetadata(task), status: NODE_STATUS_ERROR, errorDetails: "任务已取消" } } : item));
message.success("任务已取消");
},
});
}, [confirmStopGeneration, message, modal, setNodes]);
const openNodeTaskDetails = useCallback(async (node: CanvasNodeData) => {
const taskId = node.metadata?.taskId;
if (!taskId) return;
setTaskDetailLoading(true);
setTaskDetailLogs([]);
setTaskDetail({
id: taskId,
type: "",
status: (node.metadata?.taskStatus as GenerationTask["status"]) || "running",
stage: node.metadata?.taskStage,
progress: node.metadata?.taskProgress,
prompt: node.metadata?.prompt || "",
attempts: 1,
createdAt: node.metadata?.taskCreatedAt || new Date().toISOString(),
updatedAt: node.metadata?.taskUpdatedAt || new Date().toISOString(),
});
try {
const [task, logs] = await Promise.all([queryGenerationTask(taskId), listTaskLogs(taskId)]);
setTaskDetail(task);
setTaskDetailLogs(logs);
} catch (error) {
message.error(error instanceof Error ? error.message : "任务详情加载失败");
} finally {
setTaskDetailLoading(false);
}
}, [message]);
const bindGenerationTask = useCallback((targetNodeId: string, task: GenerationTask) => {
setNodes((current) => current.map((node) => {
if (node.id !== targetNodeId) return node;
const failed = task.status === "failed" || task.status === "cancelled";
const hasCompletedContent = task.status === "succeeded" && Boolean(node.metadata?.content);
const failure = failed
? generationFailureMetadata(task.error || (task.status === "cancelled" ? "任务已取消" : "任务失败"), node.metadata?.composerContent || node.metadata?.prompt || task.prompt || "")
: undefined;
return {
...node,
metadata: {
...node.metadata,
...generationTaskMetadata(task),
status: failed ? NODE_STATUS_ERROR : hasCompletedContent ? NODE_STATUS_SUCCESS : NODE_STATUS_LOADING,
...(failure || { errorDetails: undefined, generationErrorCode: undefined, failedPromptFingerprint: undefined }),
},
};
}));
}, [setNodes]);
const saveGeneratedAsset = useCallback(async (node: CanvasNodeData, taskId: string) => {
const result = await ensureCanvasNodeAsset({ canvasId: projectId, domainProjectId, node, source: "canvas-generation", taskId });
setNodes((current) => current.map((item) => item.id === node.id ? { ...item, metadata: { ...item.metadata, assetId: result.assetId } } : item));
if (domainProjectId) await queryClient.invalidateQueries({ queryKey: ["project", domainProjectId] });
}, [domainProjectId, projectId, queryClient, setNodes]);
const applyGenerationTaskResult = useCallback(async (nodeId: string, task: GenerationTask) => {
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));
}, [nodesRef, setNodes]);
const recoverInterruptedGenerationTasks = useCallback(async () => {
const recoveryNodes = nodesRef.current.filter((node) => {
const aggregateBatchRoot = node.metadata?.isBatchRoot && node.metadata.batchChildIds?.length && !node.metadata.taskId;
if (aggregateBatchRoot) return false;
return node.metadata?.status === NODE_STATUS_LOADING || node.metadata?.errorDetails === "页面刷新后生成已中断,请重新生成。" || Boolean(node.metadata?.taskId && node.metadata.status !== NODE_STATUS_SUCCESS);
});
const taskIds = Array.from(new Set(recoveryNodes.map((node) => node.metadata?.taskId).filter((id): id is string => Boolean(id))));
const tasks = (await Promise.all(taskIds.map((id) => queryGenerationTask(id).catch(() => undefined)))).filter((task): task is GenerationTask => Boolean(task));
if (recoveryNodes.some((node) => !node.metadata?.taskId)) {
const recentTasks = await listGenerationTasks(30, { projectId }).catch(() => []);
tasks.push(...recentTasks.filter((task) => !tasks.some((item) => item.id === task.id)));
}
const projectTasks = tasks.filter((task) => task.projectId === projectId && (task.type.startsWith("canvas_") || task.type === "agent_storyboard_rows"));
await Promise.all(recoveryNodes.map(async (node) => {
let task = projectTasks.find((item) => item.id === node.metadata?.taskId) || projectTasks.find((item) => generationTaskNodeId(item) === node.id);
if (!task && node.metadata?.taskId) task = await queryGenerationTask(node.metadata.taskId).catch(() => undefined);
if (!task) {
setNodes((current) => current.map((item) => item.id === node.id ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, errorDetails: "页面刷新后找不到对应任务,请重新生成。" } } : item));
const cancelNodeTask = useCallback(
(node: CanvasNodeData) => {
const taskId = node.metadata?.taskId;
if (!taskId) {
confirmStopGeneration(node.id);
return;
}
if (recoveringTaskIdsRef.current.has(task.id)) return;
recoveringTaskIdsRef.current.add(task.id);
bindGenerationTask(node.id, task);
try {
const completed = task.status === "succeeded" ? task : await waitForGenerationTask(task.id, { initialTask: task });
if (node.type === CanvasNodeType.Script && completed.type === "agent_storyboard_rows") {
const result = storyboardRowsFromTask(completed);
setNodes((current) => current.map((item) => item.id === node.id ? { ...item, title: result.title || item.title, metadata: { ...item.metadata, ...generationTaskMetadata(completed), status: NODE_STATUS_SUCCESS, errorDetails: undefined, generationErrorCode: undefined, failedPromptFingerprint: undefined, storyboard: { rows: result.rows, visibleColumns: cinematicStoryboardColumns(item.metadata?.storyboard?.visibleColumns), referenceNodeIds: item.metadata?.storyboard?.referenceNodeIds || [] } } } : item));
} else {
await applyGenerationTaskResult(node.id, completed);
}
} catch (error) {
const failure = generationFailureMetadata(error, node.metadata?.composerContent || node.metadata?.prompt || task.prompt || "");
setNodes((current) => current.map((item) => item.id === node.id ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, ...failure } } : item));
} finally {
recoveringTaskIdsRef.current.delete(task.id);
}
}));
setNodes((current) => current.map((node) => {
if (!node.metadata?.isBatchRoot || !node.metadata.batchChildIds?.length || node.metadata.taskId) return node;
const children = node.metadata.batchChildIds.map((id) => current.find((item) => item.id === id)).filter(Boolean) as CanvasNodeData[];
const primary = children.find((item) => item.id === node.metadata?.primaryImageId && item.metadata?.content) || children.find((item) => item.metadata?.content);
const loading = children.some((item) => item.metadata?.status === NODE_STATUS_LOADING);
const failed = children.find((item) => item.metadata?.status === NODE_STATUS_ERROR);
return {
...node,
metadata: {
...node.metadata,
...(primary ? {
content: primary.metadata?.content,
storageKey: primary.metadata?.storageKey,
mimeType: primary.metadata?.mimeType,
bytes: primary.metadata?.bytes,
naturalWidth: primary.metadata?.naturalWidth,
naturalHeight: primary.metadata?.naturalHeight,
primaryImageId: primary.id,
} : {}),
status: primary ? NODE_STATUS_SUCCESS : loading ? NODE_STATUS_LOADING : NODE_STATUS_ERROR,
errorDetails: primary ? undefined : failed?.metadata?.errorDetails || "全部图片生成失败",
const cancellationCopy = localDreaminaCancellationCopy({
id: taskId,
status: node.metadata?.taskStatus === "queued" ? "queued" : "running",
stage: node.metadata?.taskStage,
receiptRecorded: node.metadata?.taskReceiptRecorded,
});
modal.confirm({
title: cancellationCopy?.kind === "background" ? "转入后台?" : "取消任务?",
content: cancellationCopy?.confirmation || "任务会在后端停止,已生成完成的内容仍会保留。",
okText: cancellationCopy?.action || "取消任务",
cancelText: "继续生成",
okButtonProps: { danger: true },
onOk: async () => {
const task = await cancelGenerationTask(taskId);
const outcome = localDreaminaDetachOutcome(task);
const stopMessage = outcome?.message ?? localDreaminaCancellationMessage(task);
if (outcome?.kind !== "background") generationRequestsRef.current.get(node.id)?.controller.abort();
setNodes((current) =>
current.map((item) =>
item.id === node.id
? {
...item,
metadata: {
...item.metadata,
...generationTaskMetadata(task),
status: outcome?.canvasNodeStatus ?? NODE_STATUS_ERROR,
errorDetails: outcome?.kind === "background" ? undefined : stopMessage,
},
}
: item,
),
);
if (outcome?.kind === "background") message.info(stopMessage);
else message.success(stopMessage);
},
};
}));
}, [applyGenerationTaskResult, bindGenerationTask, nodesRef, projectId, setNodes]);
});
},
[confirmStopGeneration, message, modal, setNodes],
);
const openNodeTaskDetails = useCallback(
async (node: CanvasNodeData) => {
const taskId = node.metadata?.taskId;
if (!taskId) return;
setTaskDetailLoading(true);
setTaskDetailLogs([]);
setTaskDetail({
id: taskId,
type: "",
status: (node.metadata?.taskStatus as GenerationTask["status"]) || "running",
stage: node.metadata?.taskStage,
progress: node.metadata?.taskProgress,
prompt: node.metadata?.prompt || "",
attempts: 1,
createdAt: node.metadata?.taskCreatedAt || new Date().toISOString(),
updatedAt: node.metadata?.taskUpdatedAt || new Date().toISOString(),
});
try {
const [task, logs] = await Promise.all([queryGenerationTask(taskId), listTaskLogs(taskId)]);
setTaskDetail(task);
setTaskDetailLogs(logs);
} catch (error) {
message.error(error instanceof Error ? error.message : "任务详情加载失败");
} finally {
setTaskDetailLoading(false);
}
},
[message],
);
const bindGenerationTask = useCallback(
(targetNodeId: string, task: GenerationTask) => {
setNodes((current) =>
current.map((node) => {
if (node.id !== targetNodeId) return node;
const failed = task.status === "failed" || task.status === "cancelled";
const hasCompletedContent = task.status === "succeeded" && Boolean(node.metadata?.content);
const failure = failed ? generationFailureMetadata(task.error || (task.status === "cancelled" ? "任务已取消" : "任务失败"), node.metadata?.composerContent || node.metadata?.prompt || task.prompt || "") : undefined;
return {
...node,
metadata: {
...node.metadata,
...generationTaskMetadata(task),
status: failed ? NODE_STATUS_ERROR : hasCompletedContent ? NODE_STATUS_SUCCESS : NODE_STATUS_LOADING,
...(failure || { errorDetails: undefined, generationErrorCode: undefined, failedPromptFingerprint: undefined }),
},
};
}),
);
},
[setNodes],
);
const saveGeneratedAsset = useCallback(
async (node: CanvasNodeData, taskId: string, signal?: AbortSignal) => {
const result = await ensureCanvasNodeAsset({ canvasId: projectId, domainProjectId, node, source: "canvas-generation", taskId, signal });
setNodes((current) => current.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, assetId: result.assetId } } : item)));
if (domainProjectId) await queryClient.invalidateQueries({ queryKey: ["project", domainProjectId] });
},
[domainProjectId, projectId, queryClient, setNodes],
);
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;
}
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],
);
const observeSubscribedGenerationTask = useCallback(
(taskId: string, signal: AbortSignal, onUpdate?: (task: GenerationTask) => void) =>
new Promise<GenerationTask>((resolve, reject) => {
let unsubscribe: (() => void) | undefined;
let settled = false;
const cleanup = () => {
signal.removeEventListener("abort", onAbort);
unsubscribe?.();
};
const onAbort = () => {
if (settled) return;
settled = true;
cleanup();
reject(new DOMException("Aborted", "AbortError"));
};
const onTask = (task: GenerationTask) => {
if (settled) return;
onUpdate?.(task);
if (task.status !== "succeeded" && task.status !== "failed" && task.status !== "cancelled") return;
settled = true;
cleanup();
resolve(task);
};
if (signal.aborted) return onAbort();
signal.addEventListener("abort", onAbort, { once: true });
unsubscribe = subscribeCanvasGenerationRecoveryTasks([taskId], onTask);
if (settled) unsubscribe();
}),
[],
);
const recoverInterruptedGenerationTasks = useCallback(
async (startedProjectId: string, signal: AbortSignal, isCurrentProject: () => boolean) => {
if (!isCurrentProject()) return;
const recoveryNodes = nodesRef.current.filter((node) => {
const pendingAgentContinuation = node.metadata?.agentGenerationContinuation?.status === "pending";
const aggregateBatchRoot = node.metadata?.isBatchRoot && node.metadata.batchChildIds?.length && !node.metadata.taskId;
if (aggregateBatchRoot && !pendingAgentContinuation) return false;
return pendingAgentContinuation || node.metadata?.status === NODE_STATUS_LOADING || node.metadata?.errorDetails === "页面刷新后生成已中断,请重新生成。" || Boolean(node.metadata?.taskId && node.metadata.status !== NODE_STATUS_SUCCESS);
});
const needsDiscovery = recoveryNodes.some((node) => !node.metadata?.taskId && !node.metadata?.agentGenerationContinuation?.taskId);
const projectTasks = needsDiscovery
? (
await listGenerationTasks(30, { projectId: startedProjectId }, undefined, signal).catch((error) => {
if (!isCurrentProject()) throw error;
return [];
})
).filter((task) => task.projectId === startedProjectId && (task.type.startsWith("canvas_") || task.type === "agent_storyboard_rows"))
: [];
if (!isCurrentProject()) return;
await Promise.all(
recoveryNodes.map(async (node) => {
const discoveredTask = projectTasks.find((task) => generationTaskNodeId(task) === node.id);
const taskId = node.metadata?.taskId || node.metadata?.agentGenerationContinuation?.taskId || discoveredTask?.id;
if (!taskId) {
if (!isCurrentProject()) return;
setNodes((current) =>
isCurrentProject() ? current.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, errorDetails: "页面刷新后找不到对应任务,请重新生成。" } } : item)) : current,
);
return;
}
if (recoveringTaskIdsRef.current.has(taskId)) return;
recoveringTaskIdsRef.current.add(taskId);
const continuationOnly = !node.metadata?.taskId && node.metadata?.agentGenerationContinuation?.taskId === taskId && !discoveredTask;
try {
const completed = await observeSubscribedGenerationTask(taskId, signal, (task) => {
if (isCurrentProject() && !continuationOnly) bindGenerationTask(node.id, task);
});
if (!isCurrentProject()) return;
await recoverCanvasGenerationTaskNode({
projectId: startedProjectId,
node,
completed,
continuationOnly,
nodesRef,
setNodes,
applyGenerationTaskResult,
signal,
isCurrentProject,
});
} catch (error) {
if (!isCurrentProject() || (error instanceof Error && error.name === "AbortError")) return;
const failure = generationFailureMetadata(error, node.metadata?.composerContent || node.metadata?.prompt || "");
setNodes((current) =>
isCurrentProject()
? current.map((item) =>
item.id === node.id
? {
...item,
metadata: {
...item.metadata,
status: continuationOnly ? item.metadata?.status : NODE_STATUS_ERROR,
...(continuationOnly ? {} : failure),
...(item.metadata?.agentGenerationContinuation?.status === "pending"
? {
agentGenerationContinuation: { ...item.metadata.agentGenerationContinuation, status: "failed" as const },
}
: {}),
},
}
: item,
)
: current,
);
} finally {
recoveringTaskIdsRef.current.delete(taskId);
}
}),
);
if (!isCurrentProject()) return;
setNodes((current) =>
isCurrentProject()
? current.map((node) => {
if (!node.metadata?.isBatchRoot || !node.metadata.batchChildIds?.length || node.metadata.taskId) return node;
const children = node.metadata.batchChildIds.map((id) => current.find((item) => item.id === id)).filter(Boolean) as CanvasNodeData[];
const primary = children.find((item) => item.id === node.metadata?.primaryImageId && item.metadata?.content) || children.find((item) => item.metadata?.content);
const loading = children.some((item) => item.metadata?.status === NODE_STATUS_LOADING);
const failed = children.find((item) => item.metadata?.status === NODE_STATUS_ERROR);
return {
...node,
metadata: {
...node.metadata,
...(primary
? {
content: primary.metadata?.content,
storageKey: primary.metadata?.storageKey,
mimeType: primary.metadata?.mimeType,
bytes: primary.metadata?.bytes,
naturalWidth: primary.metadata?.naturalWidth,
naturalHeight: primary.metadata?.naturalHeight,
primaryImageId: primary.id,
}
: {}),
status: primary ? NODE_STATUS_SUCCESS : loading ? NODE_STATUS_LOADING : NODE_STATUS_ERROR,
errorDetails: primary ? undefined : failed?.metadata?.errorDetails || "全部图片生成失败",
},
};
})
: current,
);
},
[applyGenerationTaskResult, bindGenerationTask, nodesRef, observeSubscribedGenerationTask, setNodes],
);
useEffect(() => {
if (!projectLoaded) return;
void recoverInterruptedGenerationTasks();
}, [projectLoaded, recoverInterruptedGenerationTasks]);
const coordinator = recoveryCoordinatorRef.current!;
if (!projectLoaded) {
void coordinator.abortAndDrain();
return;
}
void coordinator
.switchProject(projectId, async (context) => {
recoveringTaskIdsRef.current.clear();
consumerControllerRef.current = context.controller;
await runGenerationConsumer(context.signal, async (signal) => recoverInterruptedGenerationTasks(context.projectId, signal, () => !signal.aborted && context.isCurrentProject()));
})
.catch((error) => {
if (!(error instanceof Error && error.name === "AbortError")) throw error;
});
return () => {
void coordinator.abortAndDrain();
};
}, [projectId, projectLoaded, recoverInterruptedGenerationTasks]);
useEffect(
() => () => {
void recoveryCoordinatorRef.current?.abortAndDrain();
consumerControllerRef.current.abort();
generationRequestsRef.current.forEach((request) => request.controller.abort());
generationRequestsRef.current.clear();
},
[],
);
useEffect(() => {
if (!projectLoaded) return;
@@ -236,14 +571,18 @@ export function useCanvasGeneration({ projectId, domainProjectId, projectLoaded,
const saveKey = `${taskId}:${node.id}:${domainProjectId || "personal"}`;
if (autoSavedTaskIdsRef.current.has(saveKey)) return;
autoSavedTaskIdsRef.current.add(saveKey);
void saveGeneratedAsset(node, taskId).catch((error) => {
void runGenerationConsumer(consumerControllerRef.current.signal, async (signal) => {
await saveGeneratedAsset(node, taskId, signal);
}).catch((error) => {
autoSavedTaskIdsRef.current.delete(saveKey);
if (error instanceof Error && error.name === "AbortError") return;
message.warning(error instanceof Error ? `生成结果已保留,但项目资产同步失败:${error.message}` : "生成结果已保留,但项目资产同步失败");
});
});
}, [domainProjectId, message, nodes, projectLoaded, saveGeneratedAsset]);
return {
applyGenerationTaskResult,
bindGenerationTask,
cancelNodeTask,
confirmStopGeneration,