feat(image): 以插件声明统一图片参数、尺寸选择与定价合同 (#760)
* feat(image): 图片参数 - 统一插件声明配置与生成合同 * docs(image): 图片参数 - 说明声明合同与验证边界 * fix(plugins): 图片声明 - 保留内联素材支持并同步制品文档 * fix(image): 参数合同 - 对齐审批意图和尺寸预检调用方 * fix(image): 调用链 - 保留公开意图并接通独立分辨率 * fix(image): 图片参数 - 补齐分辨率接线与目录价格投影
This commit is contained in:
1 parent
88de4ce422
commit
7466119867
104 files changed
+6013
-568
No files matched your search
@@ -1,11 +1,36 @@
|
||||
import type { ModelProtocol, ModelProtocolWorkflow } from "@/lib/model-protocols";
|
||||
import { projectImageParameters, normalizeImageParameters, imageParameterRequest } from "./image-parameters";
|
||||
import type { ImageResolutionOption, ImageResolutionTier } from "@/lib/image-resolution-tiers";
|
||||
import { imagePresetForRatio } from "./image-size-presets";
|
||||
import { IMAGE_RATIOS, IMAGE_RESOLUTIONS, imagePresetForRatio } from "@/lib/image-size-presets";
|
||||
import { kemeiSeedreamPresets } from "./kemei-image-presets";
|
||||
import { type WorkflowVideoFieldLike, workflowImageCapabilityConfig, workflowVideoCapabilityConfig } from "./model-capabilities-workflow";
|
||||
import { resolveWorkflowVideoScreenSpec } from "./video-screen-specs";
|
||||
|
||||
export { workflowFieldChoiceValues, workflowFieldConfigurationError, workflowFieldCurrentValue, workflowFieldHasStoredValue, workflowFieldKey, workflowFieldNumberBounds, workflowFieldPresetOptions, workflowFieldRandomKey, workflowFieldRole, workflowFieldSafeToOverride, workflowFieldSource, workflowFieldSubmissionValue, workflowFieldValueError, workflowImageCapabilityConfig, workflowOutputSizeValue, workflowParameterFields, workflowVideoCapabilityConfig, workflowVideoDefaultSize, workflowVideoFieldsFromJson, type WorkflowFieldNumberBounds, type WorkflowVideoFieldLike } from "./model-capabilities-workflow";
|
||||
export {
|
||||
workflowFieldChoiceValues,
|
||||
workflowFieldConfigurationError,
|
||||
workflowFieldCurrentValue,
|
||||
workflowFieldHasStoredValue,
|
||||
workflowFieldIsRatioChoices,
|
||||
workflowFieldKey,
|
||||
workflowFieldNumberBounds,
|
||||
workflowFieldOptionValue,
|
||||
workflowFieldPresetOptions,
|
||||
workflowFieldRandomKey,
|
||||
workflowFieldRole,
|
||||
workflowFieldSafeToOverride,
|
||||
workflowFieldSource,
|
||||
workflowFieldSubmissionValue,
|
||||
workflowFieldValueError,
|
||||
workflowImageCapabilityConfig,
|
||||
workflowOutputSizeValue,
|
||||
workflowParameterFields,
|
||||
workflowVideoCapabilityConfig,
|
||||
workflowVideoDefaultSize,
|
||||
workflowVideoFieldsFromJson,
|
||||
type WorkflowFieldNumberBounds,
|
||||
type WorkflowVideoFieldLike,
|
||||
} from "./model-capabilities-workflow";
|
||||
|
||||
export type ModelCapabilityConfig = {
|
||||
version: number;
|
||||
@@ -44,6 +69,7 @@ export type ImageSizeParameter = "none" | "size" | "aspect_ratio";
|
||||
export const DEFAULT_VIDEO_PROMPT_MAX_CHARS = 8000;
|
||||
|
||||
export type ImageCapabilityConfig = {
|
||||
parameters?: import("./image-parameters").ImageParameter[];
|
||||
references: {
|
||||
promptMaxChars: number;
|
||||
maxImages: number;
|
||||
@@ -56,12 +82,16 @@ export type ImageCapabilityConfig = {
|
||||
default: string;
|
||||
allowCustom: boolean;
|
||||
presets?: ImageResolutionOption[];
|
||||
defaultTier?: string;
|
||||
};
|
||||
resolution: { supported: boolean; values: string[]; default: string };
|
||||
quality: {
|
||||
supported: boolean;
|
||||
values: string[];
|
||||
default: string;
|
||||
};
|
||||
/** Protocol-declared mapping where quality values select image resolution. */
|
||||
resolutionUsesQuality?: boolean;
|
||||
transparentBackground: { supported: boolean; default: boolean };
|
||||
responseFormat: { supported: boolean };
|
||||
outputFormat: { supported: boolean };
|
||||
@@ -104,19 +134,7 @@ export type VideoCapabilityConfig = {
|
||||
export type VideoScreenSpecConfig = Pick<VideoCapabilityConfig, "ratios" | "defaultRatio" | "resolutions" | "defaultResolution">;
|
||||
|
||||
// 旧版本的“允许自定义”可能只保存了 `*`,前台需要用这组标准值恢复可选项。
|
||||
export const STANDARD_IMAGE_SIZE_VALUES = [
|
||||
"1:1",
|
||||
"3:2",
|
||||
"2:3",
|
||||
"4:3",
|
||||
"3:4",
|
||||
"16:9",
|
||||
"21:9",
|
||||
"9:16",
|
||||
"1024x1024",
|
||||
"1536x1024",
|
||||
"1024x1536",
|
||||
] as const;
|
||||
export const STANDARD_IMAGE_SIZE_VALUES = ["1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "21:9", "9:16", "1024x1024", "1536x1024", "1024x1536"] as const;
|
||||
|
||||
export function normalizeCapabilityString(value: string) {
|
||||
const normalized = value.trim();
|
||||
@@ -167,6 +185,20 @@ export function normalizeModelCapabilityConfig(config: ModelCapabilityConfig): M
|
||||
};
|
||||
}
|
||||
|
||||
// Only these shipped protocol contracts prove that legacy quality values were resolution tiers.
|
||||
// This is a read projection; persisted capability JSON is not migrated.
|
||||
export function normalizeLegacyImageResolutionConfig(protocol: ModelProtocol | undefined, image: ImageCapabilityConfig): ImageCapabilityConfig {
|
||||
if (image.parameters) return projectImageParameters(image, image.parameters);
|
||||
image = { ...image, resolutionUsesQuality: protocol === "gemini-image" };
|
||||
if (image.resolution.supported || !image.quality.supported) return image;
|
||||
const allowed = protocol === "grok-image" ? new Set(["1k", "2k"]) : protocol === "agnes-image" ? new Set(["1k", "2k", "4k"]) : undefined;
|
||||
if (!allowed) return image;
|
||||
const values = image.quality.values.map((value) => value.trim().toLowerCase());
|
||||
const defaultValue = image.quality.default.trim().toLowerCase();
|
||||
if (!values.length || values.some((value) => !allowed.has(value)) || !allowed.has(defaultValue) || !values.includes(defaultValue)) return image;
|
||||
return { ...image, resolution: { supported: true, values, default: defaultValue }, quality: { supported: false, values: [], default: "auto" } };
|
||||
}
|
||||
|
||||
// Keep explicit pixel presets for each resolution tier so the settings panel can
|
||||
// switch between 1K, 2K and 4K without silently converting the requested ratio.
|
||||
const defaultImageSizes = [
|
||||
@@ -215,6 +247,7 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = "
|
||||
const image: ImageCapabilityConfig = {
|
||||
references: { promptMaxChars: 32000, maxImages: 16, maxImageBytes: 30 * 1024 * 1024, maskSupported: true },
|
||||
size: { parameter: "size", values: [...defaultImageSizes], default: "1:1", allowCustom: true },
|
||||
resolution: { supported: false, values: [], default: "auto" },
|
||||
quality: { supported: true, values: ["auto", "low", "medium", "high"], default: "auto" },
|
||||
transparentBackground: { supported: true, default: false },
|
||||
responseFormat: { supported: true },
|
||||
@@ -251,10 +284,15 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = "
|
||||
}
|
||||
if (protocol === "cangyuan-midjourney-v7" || protocol === "cangyuan-midjourney-v82") {
|
||||
const v7 = protocol === "cangyuan-midjourney-v7";
|
||||
const tier = !v7 && model.trim().toLowerCase().replace(/^models\//, "") === "midjourney-2k" ? "2k" : "1k";
|
||||
const ratios = v7
|
||||
? ["1:1", "3:2", "2:3", "4:3", "3:4", "4:5", "5:4", "16:9", "9:16", "21:9"]
|
||||
: ["1:1", "3:2", "2:3", "4:3", "3:4", "4:5", "5:4", "16:9", "9:16", "1:2", "6:11", "5:6", "2:1", "11:6", "6:5"];
|
||||
const tier =
|
||||
!v7 &&
|
||||
model
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^models\//, "") === "midjourney-2k"
|
||||
? "2k"
|
||||
: "1k";
|
||||
const ratios = v7 ? ["1:1", "3:2", "2:3", "4:3", "3:4", "4:5", "5:4", "16:9", "9:16", "21:9"] : ["1:1", "3:2", "2:3", "4:3", "3:4", "4:5", "5:4", "16:9", "9:16", "1:2", "6:11", "5:6", "2:1", "11:6", "6:5"];
|
||||
image.references = { ...image.references, promptMaxChars: v7 ? 4000 : 32000, maxImages: v7 ? 5 : 1, maskSupported: !v7 };
|
||||
image.size = { parameter: "aspect_ratio", values: ["auto", ...ratios], default: "auto", allowCustom: false, presets: ratios.map((ratio) => imagePresetForRatio(tier, ratio)) };
|
||||
image.quality = { supported: false, values: [], default: "auto" };
|
||||
@@ -275,7 +313,8 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = "
|
||||
default: "1:1",
|
||||
allowCustom: false,
|
||||
};
|
||||
image.quality = { supported: true, values: ["1k", "2k"], default: "2k" };
|
||||
image.resolution = { supported: true, values: ["1k", "2k"], default: "2k" };
|
||||
image.quality = { supported: false, values: [], default: "auto" };
|
||||
image.transparentBackground = { supported: false, default: false };
|
||||
image.responseFormat = { supported: true };
|
||||
image.outputFormat = { supported: false };
|
||||
@@ -287,6 +326,11 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = "
|
||||
image.responseFormat.supported = false;
|
||||
image.outputFormat.supported = false;
|
||||
}
|
||||
if (protocol === "openai-image") {
|
||||
// OpenAI Images 只收 WIDTHxHEIGHT:默认配置声明了 1K/2K/4K,就必须自带档位换算表,
|
||||
// 否则运行时换算会被静默跳过,实际出图档位与所选档位不一致。取值来自本地唯一那张比例表。
|
||||
image.size.presets = IMAGE_RESOLUTIONS.flatMap((tier) => IMAGE_RATIOS.map((ratio) => imagePresetForRatio(tier, ratio)));
|
||||
}
|
||||
if (protocol === "volcengine-jimeng-image") {
|
||||
image.references.maxImages = 14;
|
||||
image.references.maskSupported = false;
|
||||
@@ -308,6 +352,7 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = "
|
||||
image.responseFormat = { supported: false };
|
||||
image.outputFormat = { supported: false };
|
||||
image.maxOutputs = 4;
|
||||
image.resolutionUsesQuality = true;
|
||||
}
|
||||
if (protocol === "agnes-image") {
|
||||
// Agnes 图像:size 必填,取 1K/2K/3K/4K 档位或 WxH 精确尺寸,画面比例走独立的 ratio 字段;
|
||||
@@ -321,7 +366,8 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = "
|
||||
allowCustom: false,
|
||||
};
|
||||
// 官方档位是 1K/2K/3K/4K,统一层只提供 1k/2k/4k 三档;3K 保留在协议映射里但不在界面露出。
|
||||
image.quality = { supported: true, values: ["1k", "2k", "4k"], default: "2k" };
|
||||
image.resolution = { supported: true, values: ["1k", "2k", "4k"], default: "2k" };
|
||||
image.quality = { supported: false, values: [], default: "auto" };
|
||||
image.transparentBackground = { supported: false, default: false };
|
||||
// 顶层 response_format 是官方明确的错误写法,输出格式只能在 extra_body 内声明。
|
||||
image.responseFormat = { supported: false };
|
||||
@@ -338,7 +384,8 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = "
|
||||
default: "1:1",
|
||||
allowCustom: false,
|
||||
};
|
||||
image.quality = { supported: true, values: ["1k", "2k"], default: "2k" };
|
||||
image.resolution = { supported: true, values: ["1k", "2k"], default: "2k" };
|
||||
image.quality = { supported: false, values: [], default: "auto" };
|
||||
image.transparentBackground = { supported: false, default: false };
|
||||
image.responseFormat = { supported: true };
|
||||
image.outputFormat = { supported: false };
|
||||
@@ -467,7 +514,7 @@ export function modelCapabilityConfigFor(config: { channels: Array<{ id: string;
|
||||
const text = capabilityConfig.text ? { ...fallback.text!, ...capabilityConfig.text, references: { ...fallback.text!.references, ...capabilityConfig.text.references } } : fallback.text;
|
||||
const video = capabilityConfig.video ? { ...fallback.video!, ...capabilityConfig.video, references: { ...fallback.video!.references, ...capabilityConfig.video.references } } : fallback.video;
|
||||
const configuredImage = capabilityConfig.image;
|
||||
const image = configuredImage
|
||||
const configuredImageProfile = configuredImage
|
||||
? (() => {
|
||||
const configuredSize = configuredImage.size;
|
||||
const configuredValues = configuredSize?.values?.map(normalizeCapabilityString);
|
||||
@@ -486,33 +533,69 @@ export function modelCapabilityConfigFor(config: { channels: Array<{ id: string;
|
||||
default: defaultValue,
|
||||
allowCustom,
|
||||
},
|
||||
resolution: {
|
||||
...fallback.image!.resolution,
|
||||
...configuredImage.resolution,
|
||||
values: normalizeCapabilityStrings(configuredImage.resolution?.values || []),
|
||||
default: normalizeCapabilityString(configuredImage.resolution?.default || fallback.image!.resolution.default),
|
||||
},
|
||||
};
|
||||
})()
|
||||
: fallback.image;
|
||||
const image = configuredImageProfile ? normalizeLegacyImageResolutionConfig(cost?.protocol, configuredImageProfile) : fallback.image;
|
||||
return { ...fallback, ...capabilityConfig, text, image, video };
|
||||
}
|
||||
|
||||
// 工作流字段是供应商参数的唯一事实来源;不能用普通视频模型的固定清晰度列表覆盖它。
|
||||
|
||||
export function normalizeImageValue(profile: ImageCapabilityConfig, value: { size?: string; quality?: string; count?: string; transparentBackground?: string }) {
|
||||
export function normalizeImageValue(profile: ImageCapabilityConfig, value: { size?: string; quality?: string; imageResolution?: string; imageParameters?: string; count?: string; transparentBackground?: string }, protocol?: ModelProtocol) {
|
||||
if (profile.parameters) {
|
||||
const values = imageParameterRequest(profile, value) || {};
|
||||
return {
|
||||
size: String(values.size ?? ""),
|
||||
quality: String(values.quality ?? ""),
|
||||
imageResolution: String(values.imageResolution ?? ""),
|
||||
count: String(values.count ?? 1),
|
||||
transparentBackground: String(values.transparentBackground ?? false),
|
||||
imageParameters: JSON.stringify(normalizeImageParameters(profile.parameters, value.imageParameters)),
|
||||
};
|
||||
}
|
||||
const size = normalizeImageSizeSetting(profile, value.size);
|
||||
const requestedQuality = String(value.quality || "").trim().toLowerCase();
|
||||
const requestedQuality = String(value.quality || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
// 比例协议的固定分辨率预设没有独立 quality 字段时,UI 仍需把当前比例对应的
|
||||
// 预设档位带入请求。仅在 quality 未声明支持时启用,避免与 auto/low/medium/high
|
||||
// 这组真实图片质量语义混用。
|
||||
const presetTier = !profile.quality.supported ? imagePresetTierForSelection(profile, size) : undefined;
|
||||
const presetTier = !profile.quality.supported && !profile.resolution.supported ? imagePresetTierForSelection(profile, size) : undefined;
|
||||
const quality = profile.quality.supported
|
||||
? requestedQuality === "auto" || requestedQuality === "any"
|
||||
? "auto"
|
||||
: value.quality && profile.quality.values.includes(value.quality)
|
||||
? value.quality
|
||||
: profile.quality.default || "auto"
|
||||
: requestedQuality === "1k" || requestedQuality === "1.5k" || requestedQuality === "2k" || requestedQuality === "4k"
|
||||
? value.quality
|
||||
: profile.quality.default || "auto"
|
||||
: profile.resolution.supported
|
||||
? profile.quality.default || "auto"
|
||||
: requestedQuality === "1k" || requestedQuality === "1.5k" || requestedQuality === "2k" || requestedQuality === "4k"
|
||||
? requestedQuality
|
||||
: presetTier || profile.quality.default || "auto";
|
||||
const count = String(Math.max(1, Math.min(profile.maxOutputs, Math.floor(Math.abs(Number(value.count)) || 1))));
|
||||
const transparentBackground = profile.transparentBackground.supported && value.transparentBackground === "true" ? "true" : "false";
|
||||
return { size, quality, count, transparentBackground };
|
||||
const requestedResolution = String(value.imageResolution || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const configuredResolution = profile.resolution.values.find((item) => item.trim().toLowerCase() === requestedResolution);
|
||||
const legacyResolutionProtocols = protocol === "grok-image" || protocol === "agnes-image";
|
||||
const legacyResolution = legacyResolutionProtocols && !profile.quality.supported ? profile.resolution.values.find((item) => item.trim().toLowerCase() === requestedQuality) : undefined;
|
||||
const imageResolution = profile.resolution.supported ? configuredResolution || legacyResolution || profile.resolution.default || "auto" : "auto";
|
||||
return {
|
||||
size,
|
||||
quality: profile.parameters && !profile.quality.supported ? "auto" : quality,
|
||||
...(profile.resolution.supported ? { imageResolution } : {}),
|
||||
count,
|
||||
transparentBackground,
|
||||
...(profile.parameters ? { imageParameters: JSON.stringify(normalizeImageParameters(profile.parameters, value.imageParameters)) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function imagePresetTierForSelection(profile: ImageCapabilityConfig, size: string): ImageResolutionTier | undefined {
|
||||
|
||||
Reference in new issue
Block a user