refactor(core): 任务与模型协议治理 - 拆分服务职责并禁止已发起任务取消
This commit is contained in:
1 parent
7418ddfa9c
commit
633560a5d8
91 files changed
+6380
-4247
No files matched your search
@@ -64,7 +64,6 @@ type CanvasProjectWorldLayersProps = {
|
||||
onToggleBatch: (nodeId: string) => void;
|
||||
onSetBatchPrimary: (node: CanvasNodeData) => void;
|
||||
onRetry: (node: CanvasNodeData) => void;
|
||||
onCancelTask: (node: CanvasNodeData) => void;
|
||||
onOpenTaskDetails: (node: CanvasNodeData) => void;
|
||||
onOpenVersions: (node: CanvasNodeData) => void;
|
||||
onViewImage: (node: CanvasNodeData) => void;
|
||||
@@ -162,7 +161,6 @@ export const CanvasProjectWorldLayers = memo(function CanvasProjectWorldLayers(p
|
||||
onToggleBatch={props.onToggleBatch}
|
||||
onSetBatchPrimary={props.onSetBatchPrimary}
|
||||
onRetry={props.onRetry}
|
||||
onCancelTask={props.onCancelTask}
|
||||
onOpenTaskDetails={props.onOpenTaskDetails}
|
||||
onOpenVersions={props.onOpenVersions}
|
||||
onViewImage={props.onViewImage}
|
||||
|
||||
@@ -413,8 +413,6 @@ function InfiniteCanvasPage() {
|
||||
const {
|
||||
applyGenerationTaskResult,
|
||||
bindGenerationTask,
|
||||
cancelNodeTask,
|
||||
confirmStopGeneration,
|
||||
finishGenerationRequest,
|
||||
openNodeTaskDetails,
|
||||
runningNodeId,
|
||||
@@ -1417,7 +1415,7 @@ function InfiniteCanvasPage() {
|
||||
generateNodeRef.current = handleGenerateNode;
|
||||
}, [handleGenerateNode]);
|
||||
|
||||
const { cancelSubmittedBatchItem, enqueueGenerationBatch, retryFailedBatchItems, stopRemainingBatchItems } = useCanvasGenerationBatches({
|
||||
const { enqueueGenerationBatch, retryFailedBatchItems, stopRemainingBatchItems } = useCanvasGenerationBatches({
|
||||
projectId,
|
||||
projectLoaded,
|
||||
nodes,
|
||||
@@ -1532,7 +1530,6 @@ function InfiniteCanvasPage() {
|
||||
onPromptChange={handleNodePromptChange}
|
||||
onConfigChange={handleConfigNodeChange}
|
||||
onGenerate={handleGenerateNode}
|
||||
onStop={confirmStopGeneration}
|
||||
workspaceMode={workspaceMode}
|
||||
onImageSettingsOpenChange={(open) => {
|
||||
setNodeImageSettingsOpen(open);
|
||||
@@ -1541,7 +1538,7 @@ function InfiniteCanvasPage() {
|
||||
/>
|
||||
);
|
||||
},
|
||||
[configInputsById, confirmStopGeneration, handleConfigNodeChange, handleGenerateNode, handleNodePromptChange, mentionReferencesByNodeId, runningNodeId, skillMentionReferences, workspaceMode],
|
||||
[configInputsById, handleConfigNodeChange, handleGenerateNode, handleNodePromptChange, mentionReferencesByNodeId, runningNodeId, skillMentionReferences, workspaceMode],
|
||||
);
|
||||
|
||||
const renderCanvasNodeContent = useCallback(
|
||||
@@ -1575,7 +1572,6 @@ function InfiniteCanvasPage() {
|
||||
onRetryBatch={(batchId) => retryFailedBatchItems(contentNode.id, batchId)}
|
||||
onRetryBatchItem={(batchId, itemId) => retryFailedBatchItems(contentNode.id, batchId, itemId)}
|
||||
onStopBatch={(batchId) => stopRemainingBatchItems(contentNode.id, batchId)}
|
||||
onCancelBatchItem={(batchId, itemId) => cancelSubmittedBatchItem(contentNode.id, batchId, itemId)}
|
||||
onAddRow={() => addScriptRow(contentNode.id)}
|
||||
onRemoveRow={(rowId) => removeScriptRow(contentNode.id, rowId)}
|
||||
onUpdateRow={(rowId, patch) => updateScriptRow(contentNode.id, rowId, patch)}
|
||||
@@ -1614,7 +1610,6 @@ function InfiniteCanvasPage() {
|
||||
inputSummary={getInputSummary(configInputsById.get(contentNode.id) || [])}
|
||||
onConfigChange={handleConfigNodeChange}
|
||||
onComposerToggle={() => setDialogNodeId((current) => (current === contentNode.id ? null : contentNode.id))}
|
||||
onStop={confirmStopGeneration}
|
||||
onGenerate={(nodeId) => {
|
||||
const target = nodesRef.current.find((item) => item.id === nodeId);
|
||||
void handleGenerateNode(nodeId, target?.metadata?.generationMode || "image", target?.metadata?.composerContent ?? target?.metadata?.prompt ?? "");
|
||||
@@ -1625,9 +1620,7 @@ function InfiniteCanvasPage() {
|
||||
},
|
||||
[
|
||||
addScriptRow,
|
||||
cancelSubmittedBatchItem,
|
||||
configInputsById,
|
||||
confirmStopGeneration,
|
||||
createAndGenerateScriptVideos,
|
||||
createScriptActionBoards,
|
||||
createScriptImageNodes,
|
||||
@@ -1926,7 +1919,6 @@ function InfiniteCanvasPage() {
|
||||
onToggleBatch={toggleBatchExpanded}
|
||||
onSetBatchPrimary={setBatchPrimary}
|
||||
onRetry={retryCanvasNode}
|
||||
onCancelTask={cancelNodeTask}
|
||||
onOpenTaskDetails={openCanvasNodeTaskDetails}
|
||||
onOpenVersions={openCanvasNodeVersions}
|
||||
onViewImage={viewCanvasNodeImage}
|
||||
|
||||
@@ -253,7 +253,7 @@ export default function SharedCanvasPage() {
|
||||
setContextMenu(null);
|
||||
dragRef.current = { primaryId: nodeId, nodeIds: [nodeId], startX: event.clientX, startY: event.clientY, origins: new Map([[nodeId, target.position]]), moved: false };
|
||||
document.body.style.cursor = "grabbing";
|
||||
}} onHoverStart={keepToolbar} onHoverEnd={hideToolbar} onConnectStart={unauthorized} onResize={() => undefined} onContentChange={unauthorized} onRetry={unauthorized} onCancelTask={unauthorized} onOpenTaskDetails={unauthorized} onViewImage={(target) => setInfoNodeId(target.id)} onContextMenu={(event, nodeId) => openContextMenu(event, nodeId)} />)}
|
||||
}} onHoverStart={keepToolbar} onHoverEnd={hideToolbar} onConnectStart={unauthorized} onResize={() => undefined} onContentChange={unauthorized} onRetry={unauthorized} onOpenTaskDetails={unauthorized} onViewImage={(target) => setInfoNodeId(target.id)} onContextMenu={(event, nodeId) => openContextMenu(event, nodeId)} />)}
|
||||
</InfiniteCanvas>
|
||||
|
||||
<CanvasNodeToolbar node={dragRef.current ? null : toolbarNode} viewport={viewport} containerRef={containerRef} onKeep={keepToolbar} onLeave={hideToolbar} onInfo={(node) => setInfoNodeId(node.id)} onEditText={unauthorized} onDecreaseFont={unauthorized} onIncreaseFont={unauthorized} onToggleDialog={unauthorized} onAnnotate={unauthorized} onGenerateImage={unauthorized} onUpload={unauthorized} onDownload={unauthorized} onSaveAsset={unauthorized} onMaskEdit={unauthorized} onEmotion={unauthorized} onPortraitTexture={unauthorized} onCrop={unauthorized} onSplit={unauthorized} onUpscale={unauthorized} onSuperResolve={unauthorized} onAngle={unauthorized} onViewImage={unauthorized} onExtractVideoLastFrame={unauthorized} onExtractAudioFromVideo={unauthorized} onTrimVideoRegenerate={unauthorized} extractingVideoFrame={false} extractingAudio={false} trimmingVideo={false} onSubtitles={unauthorized} onTimeline={unauthorized} onReversePrompt={unauthorized} onRetry={unauthorized} onToggleFreeResize={unauthorized} onToggleLocked={unauthorized} onDelete={unauthorized} />
|
||||
|
||||
@@ -5,8 +5,7 @@ import { nanoid } from "nanoid";
|
||||
import { generationBatchStatus, isGenerationCostUncertainError } from "@/lib/canvas/canvas-generation-batch";
|
||||
import { buildGenerationConfig, createGenerationRetryContext, generationTaskMetadata, resetGenerationTaskMetadata } from "@/lib/canvas/canvas-project-generation";
|
||||
import { unchangedModeratedPrompt } from "@/lib/generation-error";
|
||||
import { cancelGenerationTask, listGenerationTasks } from "@/services/api/task-center";
|
||||
import { localDreaminaCancellationCopy, localDreaminaCancellationMessage, localDreaminaDetachOutcome } from "@/services/local-dreamina-task-projection";
|
||||
import { listGenerationTasks } from "@/services/api/task-center";
|
||||
import { useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
import type { CanvasGenerationBatch, CanvasGenerationBatchItem, CanvasGenerationBatchMode, CanvasNodeData } from "@/types/canvas";
|
||||
@@ -305,7 +304,9 @@ export function useCanvasGenerationBatches({ projectId, projectLoaded, nodes, no
|
||||
const batch = findBatch(nodesRef.current, sourceNodeId, batchId);
|
||||
if (!batch) return;
|
||||
const nodeById = new Map(nodesRef.current.map((node) => [node.id, node]));
|
||||
const stoppableItems = batch.items.filter((item) => (item.status === "waiting" || item.status === "submitting") && !nodeById.get(item.nodeId)?.metadata?.taskId);
|
||||
// 只允许停止还在本地等待队列中的项目。进入 submitting 后请求可能已经被服务端接收,
|
||||
// 即使前端尚未拿到 taskId 也不能再把它标成取消,避免隐藏已计费任务。
|
||||
const stoppableItems = batch.items.filter((item) => item.status === "waiting" && !nodeById.get(item.nodeId)?.metadata?.taskId);
|
||||
if (!stoppableItems.length) return message.info("没有尚未提交的任务");
|
||||
modal.confirm({
|
||||
title: "停止剩余任务?",
|
||||
@@ -315,9 +316,11 @@ export function useCanvasGenerationBatches({ projectId, projectLoaded, nodes, no
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => {
|
||||
const latestNodeById = new Map(nodesRef.current.map((node) => [node.id, node]));
|
||||
const latestStoppableItems = stoppableItems.filter((item) => !latestNodeById.get(item.nodeId)?.metadata?.taskId);
|
||||
const latestBatch = findBatch(nodesRef.current, sourceNodeId, batchId);
|
||||
const latestStoppableItems = latestBatch
|
||||
? latestBatch.items.filter((item) => item.status === "waiting" && stoppableItems.some((candidate) => candidate.id === item.id) && !latestNodeById.get(item.nodeId)?.metadata?.taskId)
|
||||
: [];
|
||||
const stoppableIds = new Set(latestStoppableItems.map((item) => item.id));
|
||||
latestStoppableItems.forEach((item) => controllersRef.current.get(batchItemKey(batchId, item.id))?.abort());
|
||||
updateBatch(sourceNodeId, batchId, (current) => {
|
||||
const items = current.items.map((item) => (stoppableIds.has(item.id) ? { ...item, status: "cancelled" as const, errorDetails: undefined } : item));
|
||||
const nextBatch = { ...current, items, updatedAt: new Date().toISOString() };
|
||||
@@ -329,65 +332,6 @@ export function useCanvasGenerationBatches({ projectId, projectLoaded, nodes, no
|
||||
[message, modal, nodesRef, updateBatch],
|
||||
);
|
||||
|
||||
const cancelSubmittedBatchItem = useCallback(
|
||||
(sourceNodeId: string, batchId: string, itemId: string) => {
|
||||
const batch = findBatch(nodesRef.current, sourceNodeId, batchId);
|
||||
const item = batch?.items.find((candidate) => candidate.id === itemId);
|
||||
const node = item ? nodesRef.current.find((candidate) => candidate.id === item.nodeId) : undefined;
|
||||
const taskId = item?.taskId || node?.metadata?.taskId;
|
||||
if (!item || !taskId) return;
|
||||
const cancellationCopy = localDreaminaCancellationCopy({
|
||||
id: taskId,
|
||||
status: node?.metadata?.taskStatus === "queued" ? "queued" : "running",
|
||||
stage: node?.metadata?.taskStage,
|
||||
receiptRecorded: node?.metadata?.taskReceiptRecorded,
|
||||
});
|
||||
modal.confirm({
|
||||
title: "取消这个后台任务?",
|
||||
content: cancellationCopy?.confirmation || "任务会在后端停止,已经生成完成的其他镜头不会受影响。",
|
||||
okText: cancellationCopy?.action || "取消任务",
|
||||
cancelText: "继续生成",
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
try {
|
||||
const task = await cancelGenerationTask(taskId);
|
||||
const outcome = localDreaminaDetachOutcome(task);
|
||||
const stopMessage = outcome?.message ?? localDreaminaCancellationMessage(task);
|
||||
if (outcome?.kind !== "background") controllersRef.current.get(batchItemKey(batchId, item.id))?.abort();
|
||||
setNodes((current) =>
|
||||
current.map((currentNode) => {
|
||||
if (currentNode.id === item.nodeId)
|
||||
return {
|
||||
...currentNode,
|
||||
metadata: {
|
||||
...currentNode.metadata,
|
||||
...generationTaskMetadata(task),
|
||||
status: outcome?.canvasNodeStatus ?? "error",
|
||||
errorDetails: outcome?.kind === "background" ? undefined : stopMessage,
|
||||
},
|
||||
};
|
||||
if (currentNode.id !== sourceNodeId) return currentNode;
|
||||
const batches = (currentNode.metadata?.generationBatches || []).map((currentBatch) =>
|
||||
currentBatch.id === batchId
|
||||
? withUpdatedItem(currentBatch, item.id, {
|
||||
status: outcome?.batchItemStatus ?? "cancelled",
|
||||
taskId,
|
||||
errorDetails: outcome?.kind === "background" ? undefined : stopMessage,
|
||||
})
|
||||
: currentBatch,
|
||||
);
|
||||
return { ...currentNode, metadata: { ...currentNode.metadata, generationBatches: batches } };
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "任务取消失败");
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
[message, modal, nodesRef, setNodes],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectLoaded) return;
|
||||
reconcileBatches();
|
||||
@@ -404,7 +348,6 @@ export function useCanvasGenerationBatches({ projectId, projectLoaded, nodes, no
|
||||
}, [projectLoaded, reconcileBatches, scheduleWaitingItems]);
|
||||
|
||||
return {
|
||||
cancelSubmittedBatchItem,
|
||||
enqueueGenerationBatch,
|
||||
retryFailedBatchItems,
|
||||
stopRemainingBatchItems,
|
||||
|
||||
@@ -5,13 +5,12 @@ import { App } from "antd";
|
||||
import { applyGenerationTaskResultToNodes, 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 { cancelGenerationTask, listGenerationTasks, listTaskLogs, queryGenerationTask, subscribeGenerationTasks, type GenerationTask, type TaskLog } from "@/services/api/task-center";
|
||||
import { 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";
|
||||
|
||||
@@ -31,7 +30,6 @@ type UseCanvasGenerationOptions = {
|
||||
setNodes: Dispatch<SetStateAction<CanvasNodeData[]>>;
|
||||
};
|
||||
|
||||
const NODE_STATUS_IDLE = "idle" as const;
|
||||
const NODE_STATUS_LOADING = "loading" as const;
|
||||
const NODE_STATUS_SUCCESS = "success" as const;
|
||||
const NODE_STATUS_ERROR = "error" as const;
|
||||
@@ -194,7 +192,7 @@ export async function recoverCanvasGenerationTaskNode(input: {
|
||||
}
|
||||
|
||||
export function useCanvasGeneration({ projectId, domainProjectId, projectLoaded, nodes, nodesRef, setNodes }: UseCanvasGenerationOptions) {
|
||||
const { message, modal } = App.useApp();
|
||||
const { message } = App.useApp();
|
||||
const queryClient = useQueryClient();
|
||||
const generationRequestsRef = useRef(new Map<string, CanvasGenerationRequest>());
|
||||
const recoveringTaskIdsRef = useRef(new Set<string>());
|
||||
@@ -219,84 +217,6 @@ 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 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;
|
||||
}
|
||||
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);
|
||||
},
|
||||
});
|
||||
},
|
||||
[confirmStopGeneration, message, modal, setNodes],
|
||||
);
|
||||
|
||||
const openNodeTaskDetails = useCallback(
|
||||
async (node: CanvasNodeData) => {
|
||||
const taskId = node.metadata?.taskId;
|
||||
@@ -584,8 +504,6 @@ export function useCanvasGeneration({ projectId, domainProjectId, projectLoaded,
|
||||
return {
|
||||
applyGenerationTaskResult,
|
||||
bindGenerationTask,
|
||||
cancelNodeTask,
|
||||
confirmStopGeneration,
|
||||
finishGenerationRequest,
|
||||
openNodeTaskDetails,
|
||||
runningNodeId,
|
||||
|
||||
Reference in new issue
Block a user