fix(canvas): 画布生成 - 隔离派生任务状态并修复视频模型切换

This commit is contained in:
ddCat
2026-08-15 03:30:08 +08:00
parent 3c332d7699
commit 93b466b7a2
4 changed files with 23 additions and 8 deletions

View File

@@ -1,6 +1,6 @@
import { type GenerationTask } from "@/services/api/task-center";
import { backendProviderConfig, runBackendGenerationTask } from "@/services/api/generation-task";
import { defaultConfig, modelMatchesCapability, modelOptionName, normalizeModelOptionValue, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
import { configuredModelMatchesCapability, defaultConfig, normalizeModelOptionValue, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
import { resolveImageUrl, uploadImage } from "@/services/image-storage";
import { resolveMediaUrl } from "@/services/file-storage";
import { resourceIdFromStorageKey } from "@/services/api/resources";
@@ -316,7 +316,7 @@ export function resolveCanvasGenerationModel(config: AiConfig, model: string | u
if (!model) return "";
const normalized = normalizeModelOptionValue(model, config.channels);
if (!normalized) return "";
return modelMatchesCapability(modelOptionName(normalized), mode) ? normalized : "";
return configuredModelMatchesCapability(config, normalized, mode) ? normalized : "";
}
export function supportsVideoReferenceAudio(config: AiConfig) {

View File

@@ -35,6 +35,7 @@ export async function executeVideoGeneration({
const isEmptyVideoNode = sourceNode?.type === CanvasNodeType.Video && !sourceNode.metadata?.content;
const isExistingVideoNode = sourceNode?.type === CanvasNodeType.Video && Boolean(sourceNode.metadata?.content);
const videoId = isEmptyVideoNode ? nodeId : nanoid();
const versionRootId = isExistingVideoNode && sourceNode ? sourceNode.metadata?.versionOfNodeId || sourceNode.id : undefined;
const parent = sourceNode?.position || { x: 0, y: 0 };
const videoGenerationMetadata = buildVideoGenerationMetadata(sourceNode, generationContext);
const videoNode: CanvasNodeData = {
@@ -63,17 +64,19 @@ export async function executeVideoGeneration({
},
};
registerPendingNodeIds([videoId]);
// 待生成版本先加入版本族,但只有成功结果才能替换当前主版本。
setNodes((current) => {
if (isEmptyVideoNode) return current.map((node) => (node.id === nodeId ? { ...node, ...videoNode } : node));
if (!isExistingVideoNode || !sourceNode) return [...current.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_SUCCESS } } : node)), videoNode];
const rootId = sourceNode.metadata?.versionOfNodeId || sourceNode.id;
const rootId = versionRootId!;
const nextLabel = nextCanvasVersionLabel(rootId, current);
const hasPrimaryVersion = current.some((node) => (node.metadata?.versionOfNodeId || node.id) === rootId && node.metadata?.versionPrimary);
return [
...current.map((node) => {
if ((node.metadata?.versionOfNodeId || node.id) !== rootId) return node;
return { ...node, metadata: { ...node.metadata, versionOfNodeId: rootId, versionLabel: node.metadata?.versionLabel || "A", versionPrimary: false, status: node.id === nodeId ? NODE_STATUS_SUCCESS : node.metadata?.status } };
return { ...node, metadata: { ...node.metadata, versionOfNodeId: rootId, versionLabel: node.metadata?.versionLabel || "A", versionPrimary: node.metadata?.versionPrimary || (!hasPrimaryVersion && node.id === sourceNode.id), status: node.id === nodeId ? NODE_STATUS_SUCCESS : node.metadata?.status } };
}),
{ ...videoNode, metadata: { ...videoNode.metadata, versionOfNodeId: rootId, versionLabel: nextLabel, versionPrimary: true } },
{ ...videoNode, metadata: { ...videoNode.metadata, versionOfNodeId: rootId, versionLabel: nextLabel, versionPrimary: false } },
];
});
// 重新生成已有视频时,新节点继承源视频的上游连接,与源视频保持并行关系,而不是作为其下游子节点。
@@ -91,9 +94,10 @@ export async function executeVideoGeneration({
const video = await storeGeneratedVideo({ url: result.video.dataUrl, mimeType: result.video.mimeType || "video/mp4" });
const videoSize = fitNodeSize(video.width || spec.width, video.height || spec.height, VIDEO_NODE_MAX_SIZE.width, VIDEO_NODE_MAX_SIZE.height);
setNodes((current) => current.map((node) => {
if (node.id !== videoId) return node;
const isVersionSibling = Boolean(versionRootId && (node.metadata?.versionOfNodeId || node.id) === versionRootId);
if (node.id !== videoId) return isVersionSibling ? { ...node, metadata: { ...node.metadata, versionPrimary: false } } : node;
const geometry = node.metadata?.locked ? {} : { width: videoSize.width, height: videoSize.height, position: { x: node.position.x + node.width / 2 - videoSize.width / 2, y: node.position.y + node.height / 2 - videoSize.height / 2 } };
return { ...node, ...geometry, metadata: { ...node.metadata, ...videoMetadata(video), prompt: effectivePrompt, model: generationConfig.model, size: generationConfig.size, seconds: generationConfig.videoSeconds, vquality: generationConfig.vquality, generateAudio: generationConfig.videoGenerateAudio, watermark: generationConfig.videoWatermark, references: generationReferenceUrls(generationContext), ...videoGenerationMetadata } };
return { ...node, ...geometry, metadata: { ...node.metadata, ...videoMetadata(video), prompt: effectivePrompt, model: generationConfig.model, size: generationConfig.size, seconds: generationConfig.videoSeconds, vquality: generationConfig.vquality, generateAudio: generationConfig.videoGenerateAudio, watermark: generationConfig.videoWatermark, references: generationReferenceUrls(generationContext), ...videoGenerationMetadata, ...(versionRootId ? { versionPrimary: true } : {}) } };
}));
} finally {
finishGenerationRequest(videoId, controller);

View File

@@ -204,7 +204,8 @@ export function useCanvasGenerationExecutor({
return;
}
const markSourceStatus = sourceNode?.type !== CanvasNodeType.Image && !editingTextNode;
// 已有内容节点只是本次生成的来源;任务状态归新目标所有,不能覆盖已成功结果。
const markSourceStatus = !sourceNode?.metadata?.content && !editingTextNode;
const statusPrompt = sourceNode?.type === CanvasNodeType.Config ? effectivePrompt : prompt;
if (!effectivePrompt && (mode === "text" || mode === "audio")) {
finishGenerationRequest(nodeId, controller);

View File

@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import { canvasConnectionError } from "../src/lib/canvas/canvas-connection-policy";
import { buildGenerationConfig, resolveCanvasGenerationModel } from "../src/lib/canvas/canvas-project-generation";
import { defaultModelCapabilityConfig } from "../src/lib/model-capabilities";
import { groupModelsByDisplayName, modelCompatibilityError, modelGroupReferenceLimits, resolveCompatibleModel } from "../src/lib/model-selection";
import { defaultConfig, type AiConfig, type ModelChannel } from "../src/stores/use-config-store";
@@ -52,6 +53,15 @@ function node(id: string, type: CanvasNodeType, generationMode?: "image" | "vide
}
describe("逻辑模型选择", () => {
test("后台标注的视频模型不因内部标识缺少视频关键词而回退", () => {
const config = policyConfig();
const selectedModel = config.videoModels[0]!;
const videoNode = { ...node("video", CanvasNodeType.Video), metadata: { model: selectedModel } };
expect(resolveCanvasGenerationModel(config, selectedModel, "video")).toBe(selectedModel);
expect(buildGenerationConfig(config, videoNode, "video").model).toBe(selectedModel);
});
test("同渠道同显示名称合并为一个逻辑模型", () => {
const config = policyConfig();
const groups = groupModelsByDisplayName(config, config.videoModels);