- {props.mode === "video" || props.imageProfile.size.parameter !== "none" ?
{ratios.map((value) => { const selected = props.mode === "image" && usesImageResolutionPicker ? value === activeImageRatio : value === props.ratio; return ; })}
{props.mode !== "video" && props.imageProfile.size.allowCustom && (customRatioOpen ?
:
)}
: null}
+ {props.mode === "video" || mergedProfile.size.parameter !== "none" ?
{ratios.map((value) => { const selected = props.mode === "image" && usesImageResolutionPicker ? value === activeImageRatio : value === props.ratio; return ; })}
{props.mode !== "video" && mergedProfile.size.allowCustom && (customRatioOpen ?
:
)}
: null}
{props.mode === "video" ?
{resolutions.map((option) => )}
: <>
{imageResolutionChoiceOptions.length ?
{imageResolutionChoiceOptions.map((choice) => )}
: null}
{props.imageProfile.quality.supported ?
item.value === "1k" || item.value === "2k") ? "分辨率" : "图片质量"} value={qualityLabel}>{activeQualityOptions.map((option) => )}
: null}
From ad0923a892fc49157a5e9a61cac7fe073dd927e7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=9C=A8await?= <1335360669@qq.com>
Date: Mon, 17 Aug 2026 11:27:53 +0800
Subject: [PATCH 2/3] =?UTF-8?q?fix(web):=20=E6=A8=A1=E5=9E=8B=E5=88=86?=
=?UTF-8?q?=E7=BB=84=20-=20=E7=94=9F=E6=88=90/=E9=87=8D=E8=AF=95/=E5=B7=A5?=
=?UTF-8?q?=E5=85=B7=E9=93=BE=E6=8C=89=E5=BD=93=E5=89=8D=E5=B0=BA=E5=AF=B8?=
=?UTF-8?q?=E8=B7=AF=E7=94=B1=E6=9C=80=E4=BD=8E=E4=BB=B7=E5=85=BC=E5=AE=B9?=
=?UTF-8?q?=E6=A8=A1=E5=9E=8B=EF=BC=8C=E5=B0=BA=E5=AF=B8=E6=A3=80=E6=9F=A5?=
=?UTF-8?q?=E4=B8=8D=E5=86=8D=E4=BE=9D=E8=B5=96=E8=BE=93=E5=85=A5=E6=91=98?=
=?UTF-8?q?=E8=A6=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
web/src/lib/canvas/canvas-project-generation.ts | 5 ++++-
web/src/lib/model-selection.ts | 15 ++++++++++-----
2 files changed, 14 insertions(+), 6 deletions(-)
diff --git a/web/src/lib/canvas/canvas-project-generation.ts b/web/src/lib/canvas/canvas-project-generation.ts
index 6d545f728..627b770b6 100644
--- a/web/src/lib/canvas/canvas-project-generation.ts
+++ b/web/src/lib/canvas/canvas-project-generation.ts
@@ -373,7 +373,10 @@ export function buildGenerationConfig(config: AiConfig, node: CanvasNodeData | u
const storedModel = resolveCanvasGenerationModel(config, node?.metadata?.model, mode);
const preferredModel = storedModel || resolveCanvasGenerationModel(config, defaultModel, mode) || fallbackModel;
const imageSize = mode === "image" ? node?.metadata?.size || config.size || defaultConfig.size : undefined;
- const model = resolveCompatibleModel(config, preferredModel, imageSize ? { ...requirements, imageSize } : requirements) || preferredModel;
+ // 无 requirements 的调用(重试、媒体工具等)也按当前能力与尺寸路由到组内最低价兼容模型,
+ // 避免旧 metadata.model 不支持当前尺寸导致生成时被 normalize 回退。
+ const baseRequirements = requirements?.capability ? requirements : { capability: mode };
+ const model = resolveCompatibleModel(config, preferredModel, imageSize ? { ...baseRequirements, imageSize } : baseRequirements) || preferredModel;
const imageProfile = mode === "image" ? modelCapabilityConfigFor(config, model).image! : undefined;
const normalizedImage = imageProfile
? normalizeImageValue(imageProfile, {
diff --git a/web/src/lib/model-selection.ts b/web/src/lib/model-selection.ts
index 2afc949a7..7621a0f7d 100644
--- a/web/src/lib/model-selection.ts
+++ b/web/src/lib/model-selection.ts
@@ -50,30 +50,35 @@ export function configuredModelDisplayName(config: AiConfig, value: string) {
export function modelCompatibilityError(config: AiConfig, model: string, requirements?: ModelRequirements) {
const capability = requirements?.capability;
+ if (!capability) return "";
const input = requirements?.input;
- if (!capability || !input) return "";
- const visualInputCount = input.imageCount + input.characterCount;
+ const visualInputCount = input ? input.imageCount + input.characterCount : 0;
if (capability === "image") {
+ const image = modelCapabilityConfigFor(config, model).image!;
+ // 尺寸兼容不依赖输入摘要:无输入时(如画布重试/工具链)也要按尺寸过滤组内模型。
+ if (requirements.imageSize && !image.size.allowCustom && !image.size.values.includes(requirements.imageSize)) return "不支持当前尺寸";
+ if (!input) return "";
if (input.videoCount > 0) return "图片模型不支持参考视频";
if (input.audioCount > 0) return "图片模型不支持参考音频";
- const image = modelCapabilityConfigFor(config, model).image!;
if (visualInputCount > image.references.maxImages) return `最多支持 ${image.references.maxImages} 张参考图`;
- if (requirements.imageSize && !image.size.allowCustom && !image.size.values.includes(requirements.imageSize)) return "不支持当前尺寸";
return "";
}
if (capability === "video") {
const profile = modelCapabilityConfigFor(config, model).video!;
+ if (requirements.videoSeconds && !videoDurationAllowed(profile, Number(requirements.videoSeconds))) return "不支持当前视频时长";
+ if (!input) return "";
if (visualInputCount > profile.references.maxImages) return `最多支持 ${profile.references.maxImages} 张参考图`;
if (input.videoCount > profile.references.maxVideos) return `最多支持 ${profile.references.maxVideos} 个参考视频`;
if (input.audioCount > profile.references.maxAudios) return `最多支持 ${profile.references.maxAudios} 个参考音频`;
- if (requirements.videoSeconds && !videoDurationAllowed(profile, Number(requirements.videoSeconds))) return "不支持当前视频时长";
const operation = resolveVideoOperation(input, requirements.videoOperation);
if (operation !== "concat" && !profile.operations.includes(operation)) return `不支持${videoOperationLabel(operation)}`;
return "";
}
+ if (!input) return "";
+
if (capability === "text") {
return input.audioCount > 0 ? "文本模型不支持参考音频" : "";
}
From cbd598e8c13f826fd3a160a8a453b9b0bb757d8f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=9C=A8await?= <1335360669@qq.com>
Date: Mon, 17 Aug 2026 11:34:02 +0800
Subject: [PATCH 3/3] =?UTF-8?q?fix(web):=20=E6=A8=A1=E5=9E=8B=E5=88=86?=
=?UTF-8?q?=E7=BB=84=20-=20=E7=94=BB=E5=B8=83=E5=9B=BE=E7=89=87=E5=8F=82?=
=?UTF-8?q?=E6=95=B0=E5=AF=B9=E9=BD=90=E5=88=9B=E4=BD=9C=E9=A1=B5=E8=A7=84?=
=?UTF-8?q?=E5=88=99=EF=BC=8C=E5=AE=BD=E9=AB=98=E6=AF=94=E7=9B=B4=E6=8E=A5?=
=?UTF-8?q?=E5=8F=96=E6=A8=A1=E5=9E=8B=E7=BB=84=E5=B9=B6=E9=9B=86=20values?=
=?UTF-8?q?=EF=BC=8C=E8=B4=A8=E9=87=8F=E7=AD=89=E8=83=BD=E5=8A=9B=E5=8F=96?=
=?UTF-8?q?=E8=B7=AF=E7=94=B1=E6=A8=A1=E5=9E=8B=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
web/src/components/image-settings-panel.tsx | 30 ++++++++++++++++++++-
web/src/lib/model-selection.ts | 7 +++--
2 files changed, 34 insertions(+), 3 deletions(-)
diff --git a/web/src/components/image-settings-panel.tsx b/web/src/components/image-settings-panel.tsx
index 72249081e..e6126d9eb 100644
--- a/web/src/components/image-settings-panel.tsx
+++ b/web/src/components/image-settings-panel.tsx
@@ -68,7 +68,7 @@ export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle =
? []
: usesResolutionPicker && activeResolution
? resolutionOptions.filter((item) => item.tier === activeResolution.tier).map((item) => ({ value: item.ratio, label: item.ratio, size: item.size, width: item.width, height: item.height, icon: item.width === item.height ? "square" : item.width > item.height ? "landscape" : "portrait" }))
- : aspectOptions.filter((item) => imageOptionAllowed(profile, item));
+ : imageAspectOptions(profile);
const selectedAspect = availableAspects.find((item) => imageOptionValue(profile, item) === activeSize || item.value === activeSize) || availableAspects.find((item) => item.label === activeRatio);
const dimensions = readSizeDimensions(activeSize, selectedAspect || aspectOptions[0]);
const activeQualityOptions = profile.quality.values.map((value) => qualityOptions.find((item) => item.value === value) || { value, label: value });
@@ -199,6 +199,34 @@ function imageOptionAllowed(profile: ImageCapabilityConfig, option: AspectOption
return [option.value, option.size, option.width && option.height ? `${option.width}x${option.height}` : ""].filter(Boolean).some((value) => profile.size.values.includes(String(value)));
}
+// 宽高比选项直接取模型配置 values(与创作页面一致),不在白名单里的比例(如 8:1)也能显示。
+function imageAspectOptions(profile: ImageCapabilityConfig): AspectOption[] {
+ if (profile.size.parameter === "none") return [];
+ const values = profile.size.values.filter((value) => value.trim().toLowerCase() !== "auto");
+ if (!values.length) return profile.size.allowCustom ? aspectOptions.filter((item) => item.value !== "auto") : [];
+ return values.map((value) => {
+ const known = aspectOptions.find((item) => (item.size || item.value) === value || item.value === value);
+ if (known) return known;
+ const parts = ratioParts(value);
+ return { value, label: value, size: value, width: parts?.width || 0, height: parts?.height || 0, icon: "custom" };
+ });
+}
+
+function ratioParts(value: string) {
+ const pixel = value.trim().match(/^(\d+)x(\d+)$/i);
+ if (pixel) {
+ const divisor = gcd(Number(pixel[1]), Number(pixel[2]));
+ return { width: Number(pixel[1]) / divisor, height: Number(pixel[2]) / divisor };
+ }
+ const ratio = value.trim().match(/^(\d+):(\d+)$/);
+ if (!ratio) return undefined;
+ return { width: Number(ratio[1]), height: Number(ratio[2]) };
+}
+
+function gcd(a: number, b: number): number {
+ return b ? gcd(b, a % b) : a;
+}
+
function imageOptionValue(profile: ImageCapabilityConfig, option: AspectOption) {
const candidates = [option.size, option.value, option.width && option.height ? `${option.width}x${option.height}` : ""].filter(Boolean).map(String);
return candidates.find((value) => profile.size.values.includes(value)) || option.size || option.value || "auto";
diff --git a/web/src/lib/model-selection.ts b/web/src/lib/model-selection.ts
index 7621a0f7d..0d6b513e8 100644
--- a/web/src/lib/model-selection.ts
+++ b/web/src/lib/model-selection.ts
@@ -110,14 +110,17 @@ export function resolveCompatibleModel(config: AiConfig, selected: string, requi
// 同显示名分组的模型族:尺寸/比例/分辨率选项取组内全部模型配置的并集,
// 让用户能看到并选择任意成员支持的能力,选中后由兼容路由落到具体模型。
+// 质量、透明背景等其余能力取当前选中(路由后)模型的配置,与创作页面一致。
export function mergedImageCapabilityConfig(config: AiConfig, selected: string): ImageCapabilityConfig {
const options = selectableModelsByCapability(config, "image");
const group = options.length ? groupModelsByDisplayName(config, options).find((item) => item.models.includes(selected)) : undefined;
const models = group?.models.length ? group.models : [selected];
+ const selectedProfile = modelCapabilityConfigFor(config, selected).image;
const profiles = models.map((model) => modelCapabilityConfigFor(config, model).image).filter((profile): profile is ImageCapabilityConfig => Boolean(profile));
- if (profiles.length <= 1) return profiles[0] || defaultImageCapabilityConfig();
+ if (profiles.length <= 1) return selectedProfile || defaultImageCapabilityConfig();
const values = Array.from(new Set(profiles.flatMap((profile) => profile.size.values)));
- return { ...profiles[0], size: { ...profiles[0].size, values } };
+ const base = selectedProfile || profiles[0];
+ return { ...base, size: { ...base.size, values } };
}
// 切换模型后初始化图片参数为该模型能力默认值,避免旧参数在目标模型族不兼容导致无法切换。