fix(canvas): 项目画风执行 - 自动同步模型路由与画风计划

This commit is contained in:
ddCat
2026-08-21 14:52:50 +08:00
parent 38f596e28a
commit e6a278bfeb
6 changed files with 84 additions and 29 deletions

View File

@@ -1,5 +1,5 @@
import { applyStyleExecutionPlan, createStyleProfileSnapshot, parseStyleProfile, resolveStyleExecutionPlan, serializeStyleProfile, type StyleExecutionPlan, type StyleProfileSnapshot } from "@/lib/canvas/style-profile";
import { resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
import { logicalModelIDForConfig, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
import type { CanvasNodeData } from "@/types/canvas";
export type CanvasStyleExecutionRuntime = {
@@ -16,7 +16,8 @@ export function resolveCanvasStyleExecution(nodes: CanvasNodeData[], sourceNode:
if (!profile) return null;
const requestConfig = resolveModelRequestConfig(config, config.model);
const plan = resolveStyleExecutionPlan(profile, { mode, model: requestConfig.model, interfaceType: requestConfig.interfaceType || requestConfig.apiFormat });
if (plan.status === "blocked") throw new Error(`项目画风与当前模型不兼容:${plan.warnings.join(";")}`);
// 平台模型的真实供应模型由后端入队时路由,前端只能预览,最终兼容性由后端重算。
if (plan.status === "blocked" && !logicalModelIDForConfig(config)) throw new Error(`当前模型无法完整执行项目画风:${plan.warnings.join(";")}。请切换模型,或在项目设置中停用对应画风资产`);
return { profile, profileJson: serializeStyleProfile(profile), plan, prompt: applyStyleExecutionPlan(prompt, plan) };
}

View File

@@ -1,20 +1,21 @@
import { runBackendCanvasGenerationTask } from "@/lib/canvas/canvas-project-generation";
import { resolveStyleExecutionPlan, serializeStyleProfile, type StyleProfileSnapshot } from "@/lib/canvas/style-profile";
import { resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
import { logicalModelIDForConfig, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
type ProjectStylePrompt = { id: string; title: string; prompt: string; profile?: StyleProfileSnapshot };
export async function generateCharacterTurnaround(input: { projectId: string; assetId: string; versionId: string; name: string; definition: Record<string, unknown>; projectStyle?: ProjectStylePrompt; config: AiConfig }) {
const promptTemplateVariables = characterTurnaroundVariables(input.name, input.definition, input.projectStyle);
const requestConfig = resolveModelRequestConfig(input.config, input.config.imageModel || input.config.model);
const generationConfig = { ...input.config, model: input.config.imageModel || input.config.model, count: "1" };
const requestConfig = resolveModelRequestConfig(generationConfig, generationConfig.model);
const styleExecutionPlan = input.projectStyle?.profile ? resolveStyleExecutionPlan(input.projectStyle.profile, { mode: "image", model: requestConfig.model, interfaceType: requestConfig.interfaceType || requestConfig.apiFormat }) : undefined;
if (styleExecutionPlan?.status === "blocked") throw new Error(`项目画风与当前图片模型不兼容:${styleExecutionPlan.warnings.join(";")}`);
if (styleExecutionPlan?.status === "blocked" && !logicalModelIDForConfig(generationConfig)) throw new Error(`当前图片模型无法完整执行项目画风:${styleExecutionPlan.warnings.join(";")}。请切换图片模型,或在项目设置中停用对应画风资产`);
await runBackendCanvasGenerationTask({
projectId: input.projectId,
nodeId: `character-turnaround:${input.assetId}`,
mode: "image",
prompt: "使用当前启用的角色三视图模板。",
config: { ...input.config, model: input.config.imageModel || input.config.model, count: "1" },
config: generationConfig,
metadata: { operation: "character_turnaround", promptTemplateOperation: "character_turnaround", promptTemplateVariables, characterAssetId: input.assetId, stylePresetId: input.projectStyle?.id, styleProfileJson: input.projectStyle?.profile ? serializeStyleProfile(input.projectStyle.profile) : undefined, styleExecutionPlan, resolvedCharacterVersions: [{ assetId: input.assetId, versionId: input.versionId }] },
});
}

View File

@@ -7,12 +7,14 @@ import { isLocalDreaminaBackgroundTask, localDreaminaTaskId, projectLocalDreamin
import { modelCapabilityConfigFor } from "@/lib/model-capabilities";
import { grokImagePromptLimitError } from "@/lib/grok-image-prompt-limit";
import { resolveVideoOperation } from "@/lib/model-selection";
import { modelOptionName, resolveModelChannel, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
import { logicalModelIDForConfig, modelOptionName, resolveModelChannel, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
import { useLocalDreaminaModelStore } from "@/stores/use-local-dreamina-model-store";
import type { ReferenceImage } from "@/types/image";
import type { ReferenceAudio, ReferenceVideo } from "@/types/media";
import { buildBackendToolRequests, type ResponseFunctionTool, type ResponseInputMessage, type ToolChoice, type ToolResponseResult } from "@/services/api/image";
export { logicalModelIDForConfig };
export type BackendGenerationMode = "text" | "image" | "video" | "audio";
export type BackendGenerationResult = {
@@ -500,11 +502,6 @@ export function backendProviderConfig(config: AiConfig) {
};
}
export function logicalModelIDForConfig(config: AiConfig) {
const channel = resolveModelChannel(config, config.model);
return channel.modelCosts?.find((item) => item.model === modelOptionName(config.model))?.logicalModelId || "";
}
function logicalCapabilityOptions(config: AiConfig, mode: BackendGenerationMode) {
const channel = resolveModelChannel(config, config.model);
const spec = channel.modelCosts?.find((item) => item.model === modelOptionName(config.model))?.logicalCapabilitySpec;

View File

@@ -494,6 +494,11 @@ export function resolveModelChannel(config: AiConfig, value: string) {
return matched || config.channels[0] || createModelChannel({ id: "default", name: "默认渠道", baseUrl: config.baseUrl, apiKey: config.apiKey, apiFormat: config.apiFormat, models: config.models.map(modelOptionName) });
}
export function logicalModelIDForConfig(config: AiConfig) {
const channel = resolveModelChannel(config, config.model);
return channel.modelCosts?.find((item) => item.model === modelOptionName(config.model))?.logicalModelId || "";
}
export function channelConnectionSignature(channel: ModelChannel) {
return [channel.baseUrl.trim(), channel.apiKey.trim(), channel.secretKey?.trim() || "", channel.apiFormat, channel.interfaceType || "auto", channel.allowLocalChannel === true ? "local:1" : "local:0", JSON.stringify(channel.headers || [])].join("\n");
}