feat(canvas): 生成合同 - 统一节点参数并补充模型上下文能力

This commit is contained in:
ddCat
2026-09-19 20:32:05 +08:00
parent a59b77cf70
commit 3bf21d79af
18 changed files with 965 additions and 40 deletions

View File

@@ -11,6 +11,7 @@ import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-ge
import { defaultModelCapabilityConfig, modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue, workflowFieldChoiceValues, workflowFieldCurrentValue, workflowFieldKey, workflowFieldNumberBounds, workflowFieldRandomKey, workflowFieldSubmissionValue, workflowFieldValueError, workflowImageCapabilityConfig, workflowOutputSizeValue, workflowParameterFields, workflowVideoCapabilityConfig, workflowVideoFieldsFromJson, type WorkflowVideoFieldLike } from "@/lib/model-capabilities";
import { defaultImageParamsForModel, modelCompatibilityError, modelRequestOptions, resolveCompatibleModel, resolveModelGenerationDefaults, type ModelRequirements } from "@/lib/model-selection";
import { resolveCanvasWorkflowProvider } from "@/lib/canvas/canvas-workflow";
import { canonicalGenerationMetadata } from "@/lib/canvas/generation-contract";
import type { CanvasAudioSettingKey } from "./canvas-audio-settings-popover";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import { workflowProviderPluginEnabled } from "@/lib/plugins/builtin/workflows";
@@ -66,6 +67,7 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC
const runtimeStatuses = usePluginStore((state) => state.runtimeStatuses);
const theme = canvasThemes[useActiveTheme()];
const mode = node.metadata?.generationMode === "video" || node.metadata?.generationMode === "audio" ? node.metadata.generationMode : "image";
node = { ...node, metadata: canonicalGenerationMetadata(node, mode) };
const simpleMode = workspaceMode === "simple";
const resolvedProvider = resolveCanvasWorkflowProvider(node.metadata);
const workflowProvider = resolvedProvider;
@@ -74,16 +76,16 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC
capability: mode,
input: inputSummary,
videoOperation: node.metadata?.videoEditOperation,
videoSeconds: mode === "video" ? node.metadata?.seconds || globalConfig.videoSeconds : undefined,
videoSeconds: mode === "video" ? node.metadata?.seconds ?? globalConfig.videoSeconds : undefined,
options: modelRequestOptions({
...globalConfig,
size: node.metadata?.size || globalConfig.size,
quality: node.metadata?.quality || globalConfig.quality,
count: String(node.metadata?.count || globalConfig.count),
videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds,
videoSeconds: node.metadata?.seconds ?? globalConfig.videoSeconds,
vquality: node.metadata?.vquality || globalConfig.vquality,
videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio,
videoWatermark: node.metadata?.watermark || globalConfig.videoWatermark,
videoGenerateAudio: node.metadata?.generateAudio ?? globalConfig.videoGenerateAudio,
videoWatermark: node.metadata?.watermark ?? globalConfig.videoWatermark,
audioVoice: node.metadata?.audioVoice || globalConfig.audioVoice,
audioFormat: node.metadata?.audioFormat || globalConfig.audioFormat,
audioSpeed: node.metadata?.audioSpeed || globalConfig.audioSpeed,
@@ -450,6 +452,7 @@ function WorkflowSelectedLabel({ kind, label }: { kind: "app" | "workflow"; labe
}
function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: CanvasGenerationMode, requirements: ModelRequirements): AiConfig {
node = { ...node, metadata: canonicalGenerationMetadata(node, mode) };
const workflowProvider = mode === "text" ? "model" : resolveCanvasWorkflowProvider(node.metadata);
if (workflowProvider === "model") return buildModelNodeConfig(globalConfig, node, mode, requirements);
const defaultModel = mode === "image" ? globalConfig.imageModel : mode === "video" ? globalConfig.videoModel : mode === "audio" ? globalConfig.audioModel : globalConfig.textModel;
@@ -476,7 +479,7 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can
const workflowParameter = (source: string) => workflowParameters[`source:${source}`] === undefined ? "" : String(workflowParameters[`source:${source}`]);
const normalizedImage = imageProfile ? normalizeImageValue(imageProfile, { size: workflowOutputSize || node.metadata?.size || globalConfig.size || defaultConfig.size, quality: node.metadata?.quality || workflowParameter("quality") || globalConfig.quality || defaultConfig.quality, transparentBackground: node.metadata?.transparentBackground || globalConfig.transparentBackground, count: String(node.metadata?.count || globalConfig.canvasImageCount || globalConfig.count || defaultConfig.count) }) : undefined;
const videoProfile = mode === "video" ? capabilityProfile.video! : undefined;
const rawVideoSettings = { seconds: node.metadata?.seconds || workflowParameter("videoSeconds") || globalConfig.videoSeconds || defaultConfig.videoSeconds, ratio: workflowOutputSize || node.metadata?.size || globalConfig.size || defaultConfig.size, resolution: node.metadata?.vquality || workflowParameter("vquality") || globalConfig.vquality || defaultConfig.vquality };
const rawVideoSettings = { seconds: node.metadata?.seconds ?? workflowParameter("videoSeconds") ?? globalConfig.videoSeconds ?? defaultConfig.videoSeconds, ratio: workflowOutputSize || node.metadata?.size || globalConfig.size || defaultConfig.size, resolution: node.metadata?.vquality || workflowParameter("vquality") || globalConfig.vquality || defaultConfig.vquality };
const normalizedVideo = videoProfile
? { seconds: String(rawVideoSettings.seconds), ratio: rawVideoSettings.ratio, resolution: rawVideoSettings.resolution }
: undefined;
@@ -489,10 +492,10 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can
quality: workflowParameter("quality") || normalizedImage?.quality || node.metadata?.quality || globalConfig.quality || defaultConfig.quality,
size: normalizedImage?.size || normalizedVideo?.ratio || node.metadata?.size || globalConfig.size || defaultConfig.size,
transparentBackground: normalizedImage?.transparentBackground || ((node.metadata?.transparentBackground || globalConfig.transparentBackground) === "true" ? "true" : "false"),
videoSeconds: normalizedVideo?.seconds || normalizeVideoDuration(node.metadata?.seconds || globalConfig.videoSeconds || defaultConfig.videoSeconds),
videoSeconds: normalizedVideo?.seconds ?? normalizeVideoDuration(node.metadata?.seconds ?? globalConfig.videoSeconds ?? defaultConfig.videoSeconds),
vquality: String(node.metadata?.vquality || globalConfig.vquality || defaultConfig.vquality),
videoGenerateAudio: videoProfile?.generateAudio.supported ? node.metadata?.generateAudio || globalConfig.videoGenerateAudio || String(videoProfile.generateAudio.default) : "false",
videoWatermark: videoProfile?.watermark.supported ? node.metadata?.watermark || globalConfig.videoWatermark || String(videoProfile.watermark.default) : "false",
videoGenerateAudio: videoProfile?.generateAudio.supported ? node.metadata?.generateAudio ?? globalConfig.videoGenerateAudio ?? String(videoProfile.generateAudio.default) : "false",
videoWatermark: videoProfile?.watermark.supported ? node.metadata?.watermark ?? globalConfig.videoWatermark ?? String(videoProfile.watermark.default) : "false",
audioVoice: node.metadata?.audioVoice || globalConfig.audioVoice || defaultConfig.audioVoice,
audioFormat: node.metadata?.audioFormat || globalConfig.audioFormat || defaultConfig.audioFormat,
audioSpeed: node.metadata?.audioSpeed || globalConfig.audioSpeed || defaultConfig.audioSpeed,
@@ -502,6 +505,7 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can
}
function buildModelNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: CanvasGenerationMode, requirements: ModelRequirements): AiConfig {
node = { ...node, metadata: canonicalGenerationMetadata(node, mode) };
const defaultModel = mode === "image" ? globalConfig.imageModel : mode === "video" ? globalConfig.videoModel : mode === "audio" ? globalConfig.audioModel : globalConfig.textModel;
const fallbackModel = mode === "image" ? defaultConfig.imageModel : mode === "video" ? defaultConfig.videoModel : mode === "audio" ? defaultConfig.audioModel : defaultConfig.textModel;
const storedModel = node.metadata?.model;
@@ -544,10 +548,10 @@ function buildModelNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode
quality: generationDefaults.quality || globalConfig.quality || defaultConfig.quality,
size: generationDefaults.size ?? globalConfig.size ?? defaultConfig.size,
transparentBackground: generationDefaults.transparentBackground || "false",
videoSeconds: generationDefaults.videoSeconds || normalizeVideoDuration(globalConfig.videoSeconds || defaultConfig.videoSeconds),
videoSeconds: generationDefaults.videoSeconds ?? normalizeVideoDuration(globalConfig.videoSeconds || defaultConfig.videoSeconds),
vquality: generationDefaults.vquality ?? normalizeVideoResolution(globalConfig.vquality || defaultConfig.vquality),
videoGenerateAudio: videoProfile?.generateAudio.supported ? generationDefaults.videoGenerateAudio || String(videoProfile.generateAudio.default) : "false",
videoWatermark: videoProfile?.watermark.supported ? generationDefaults.videoWatermark || String(videoProfile.watermark.default) : "false",
videoGenerateAudio: videoProfile?.generateAudio.supported ? generationDefaults.videoGenerateAudio ?? String(videoProfile.generateAudio.default) : "false",
videoWatermark: videoProfile?.watermark.supported ? generationDefaults.videoWatermark ?? String(videoProfile.watermark.default) : "false",
audioVoice: node.metadata?.audioVoice || globalConfig.audioVoice || defaultConfig.audioVoice,
audioFormat: node.metadata?.audioFormat || globalConfig.audioFormat || defaultConfig.audioFormat,
audioSpeed: node.metadata?.audioSpeed || globalConfig.audioSpeed || defaultConfig.audioSpeed,