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
@@ -12,7 +12,7 @@ import type { CameraControlOptions } from "@/lib/canvas/camera-prompt-library";
|
||||
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
|
||||
export type BatchGenerationSettings = Pick<AiConfig, "model" | "imageModel" | "quality" | "size" | "transparentBackground" | "count"> & { cameraControl?: CameraControlOptions };
|
||||
export type BatchGenerationSettings = Pick<AiConfig, "model" | "imageModel" | "imageResolution" | "imageParameters" | "quality" | "size" | "transparentBackground" | "count"> & { cameraControl?: CameraControlOptions };
|
||||
|
||||
type BatchGenerationSettingsDialogProps = {
|
||||
open: boolean;
|
||||
@@ -35,7 +35,7 @@ export function BatchGenerationSettingsDialog({ open, config, rowCount, concurre
|
||||
}
|
||||
}, [config, open]);
|
||||
|
||||
const handleConfigChange = (key: "quality" | "size" | "transparentBackground" | "count", value: string) => {
|
||||
const handleConfigChange = (key: "quality" | "imageResolution" | "imageParameters" | "size" | "transparentBackground" | "count", value: string) => {
|
||||
setGenerationConfig((current) => ({ ...current, [key]: value }));
|
||||
};
|
||||
|
||||
@@ -102,6 +102,8 @@ export function BatchGenerationSettingsDialog({ open, config, rowCount, concurre
|
||||
onClick={() => onConfirm({
|
||||
model: generationConfig.model,
|
||||
imageModel: generationConfig.imageModel,
|
||||
imageResolution: generationConfig.imageResolution,
|
||||
imageParameters: generationConfig.imageParameters,
|
||||
quality: generationConfig.quality,
|
||||
size: generationConfig.size,
|
||||
transparentBackground: generationConfig.transparentBackground,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { agentApprovalModel, agentApprovalModelSelection, type AgentImageApprova
|
||||
import { modelCapabilityConfigFor, normalizeImageValue } from "@/lib/model-capabilities";
|
||||
import { resolveCompatibleModel, type ModelRequirements } from "@/lib/model-selection";
|
||||
import type { AgentMediaSettings } from "@/services/api/agent";
|
||||
import { PUBLIC_MODEL_CATALOG_ID, useEffectiveConfig } from "@/stores/use-config-store";
|
||||
import { PUBLIC_MODEL_CATALOG_ID, resolveModelRequestConfig, useEffectiveConfig } from "@/stores/use-config-store";
|
||||
|
||||
export function CanvasAgentImageApprovalSettings({ initial, value, onChange, theme, disabled }: {
|
||||
initial: AgentImageApproval;
|
||||
@@ -29,12 +29,13 @@ export function CanvasAgentImageApprovalSettings({ initial, value, onChange, the
|
||||
if (disabled) return;
|
||||
const profile = modelCapabilityConfigFor(config, next).image;
|
||||
if (!profile) return;
|
||||
const normalized = normalizeImageValue(profile, settings);
|
||||
onChange({ ...agentApprovalModelSelection(config, next), size: normalized.size, quality: profile.quality.supported ? normalized.quality : "" });
|
||||
const normalized = normalizeImageValue(profile, settings, resolveModelRequestConfig(config, next).interfaceType);
|
||||
onChange({ ...agentApprovalModelSelection(config, next), size: normalized.size, imageResolution: normalized.imageResolution, imageParameters: normalized.imageParameters, quality: profile.quality.supported ? normalized.quality : "" });
|
||||
};
|
||||
const changeOption = (key: "quality" | "size" | "transparentBackground" | "count", next: string) => {
|
||||
if (disabled || (key !== "size" && key !== "quality")) return;
|
||||
const options = { size: settings.size, quality: settings.quality, [key]: next };
|
||||
const changeOption = (key: "quality" | "imageResolution" | "imageParameters" | "size" | "transparentBackground" | "count", next: string) => {
|
||||
if (disabled || (key !== "size" && key !== "quality" && key !== "imageResolution" && key !== "imageParameters")) return;
|
||||
if (key === "imageParameters") { onChange({ ...settings, imageParameters: next }); return; }
|
||||
const options = { size: settings.size, imageResolution: settings.imageResolution, quality: settings.quality, [key]: next };
|
||||
const selected = resolveCompatibleModel(config, model, { ...requirements, imageSize: options.size, options: { ...options, count: 1 } });
|
||||
if (!selected) return;
|
||||
onChange({ ...agentApprovalModelSelection(config, selected), ...options });
|
||||
@@ -44,8 +45,8 @@ export function CanvasAgentImageApprovalSettings({ initial, value, onChange, the
|
||||
<div className="text-xs" style={{ color: theme.node.muted }}>生成模型</div>
|
||||
<ModelPicker config={config} capability="image" value={model} onChange={changeModel} requirements={requirements} variant="creation" fullWidth showOptionPrices placeholder="选择生成模型" popoverClassName="agent-model-picker-popover" />
|
||||
</div>
|
||||
{model ? <ImageSettingsPanel config={{ ...config, model, imageModel: model, size: settings.size, quality: settings.quality, count: "1" }} onConfigChange={changeOption} theme={theme} showTitle={false} showCount={false} showTransparent={false} className="min-w-0 space-y-3" /> : <p className="text-xs" style={{ color: theme.node.muted }}>当前模型不在可选目录中,可重新选择;提交时将重新校验模型与规格。</p>}
|
||||
<p className="text-xs" style={{ color: theme.node.text }}>本次规格:{settings.size}{settings.quality ? ` · ${settings.quality}` : ""}</p>
|
||||
{model ? <ImageSettingsPanel config={{ ...config, model, imageModel: model, size: settings.size, imageResolution: settings.imageResolution || "auto", imageParameters: settings.imageParameters, quality: settings.quality, count: "1" }} onConfigChange={changeOption} theme={theme} showTitle={false} showCount={false} showTransparent={false} className="min-w-0 space-y-3" /> : <p className="text-xs" style={{ color: theme.node.muted }}>当前模型不在可选目录中,可重新选择;提交时将重新校验模型与规格。</p>}
|
||||
<p className="text-xs" style={{ color: theme.node.text }}>本次规格:{settings.size}{settings.imageResolution && settings.imageResolution !== "auto" ? ` · ${settings.imageResolution}` : ""}{settings.quality ? ` · ${settings.quality}` : ""}</p>
|
||||
<p className="text-xs" style={{ color: theme.node.muted }}>修改仅用于本次生成,费用按最终模型和规格计算。</p>
|
||||
</fieldset>;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { Switch } from "@/components/ui/base/switch";
|
||||
import { configuredModelMatchesCapability, defaultConfig, modelOptionName, normalizeRunningHubCapability, resolveModelChannel, useEffectiveConfig, type AiConfig, type RunningHubCapability, type RunningHubWorkflow, type RunningHubWorkflowKind } from "@/stores/use-config-store";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-generation-options";
|
||||
import { defaultModelCapabilityConfig, modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue, workflowFieldChoiceValues, workflowFieldCurrentValue, workflowFieldKey, workflowFieldNumberBounds, workflowFieldRandomKey, workflowFieldSubmissionValue, workflowFieldValueError, workflowImageCapabilityConfig, workflowOutputSizeValue, workflowParameterFields, workflowVideoCapabilityConfig, workflowVideoFieldsFromJson, type WorkflowVideoFieldLike } from "@/lib/model-capabilities";
|
||||
import { defaultModelCapabilityConfig, modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue, workflowFieldChoiceValues, workflowFieldCurrentValue, workflowFieldIsRatioChoices, workflowFieldKey, workflowFieldNumberBounds, workflowFieldOptionValue, 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, nodeGenerationPrompt } from "@/lib/canvas/generation-contract";
|
||||
@@ -363,11 +363,20 @@ export function WorkflowParameterControls({ fields, node, theme, onConfigChange,
|
||||
const fieldType = ["FLOAT", "INT", "INTEGER"].includes(rawFieldType) ? "NUMBER" : rawFieldType;
|
||||
const options = workflowFieldChoiceValues(field);
|
||||
const selectOptions = options.length ? options : fieldType === "SELECT" && value !== "" ? [value] : [];
|
||||
const ratioOptions = workflowFieldIsRatioChoices(field, selectOptions);
|
||||
const bounds = workflowFieldNumberBounds(field);
|
||||
const numeric = fieldType === "NUMBER" || fieldType === "SLIDER" || typeof value === "number" || bounds.min !== undefined || bounds.max !== undefined || bounds.step !== undefined;
|
||||
const numericValue = value === "" || !Number.isFinite(Number(value)) ? undefined : Number(value);
|
||||
const valueError = randomEnabled ? "" : workflowFieldValueError(field, value);
|
||||
const control = fieldType === "SELECT" || selectOptions.length ? (
|
||||
const control = ratioOptions ? (
|
||||
<div className="flex min-w-0 flex-wrap gap-1">
|
||||
{selectOptions.map((option) => {
|
||||
const optionValue = workflowFieldOptionValue(option);
|
||||
const selected = optionValue === workflowFieldOptionValue(value);
|
||||
return <button key={optionValue} type="button" className="min-h-7 rounded-md px-2 py-1 text-[var(--fs-label)] font-medium transition-colors hover:brightness-110 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1" style={{ background: selected ? theme.accent.primary : "transparent", color: selected ? theme.accent.onPrimary : theme.node.text, outlineColor: theme.node.muted }} aria-pressed={selected} onClick={() => update(field, optionValue)}>{optionValue}</button>;
|
||||
})}
|
||||
</div>
|
||||
) : fieldType === "SELECT" || selectOptions.length ? (
|
||||
<Select status={valueError ? "error" : undefined} size="small" className="w-full" value={value === "" ? undefined : value as string | number} options={selectOptions.map((option) => ({ label: workflowParameterOptionLabel(option), value: workflowParameterOptionValue(option) }))} onChange={(next) => update(field, next)} />
|
||||
) : fieldType === "BOOLEAN" || typeof value === "boolean" ? (
|
||||
<Switch size="sm" checked={value === true || value === "true"} onChange={(checked) => update(field, checked)} />
|
||||
@@ -533,6 +542,8 @@ function buildModelNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode
|
||||
mode === "image"
|
||||
? {
|
||||
size: node.metadata?.size,
|
||||
imageResolution: node.metadata?.imageResolution,
|
||||
imageParameters: node.metadata?.imageParameters,
|
||||
quality: node.metadata?.quality,
|
||||
transparentBackground: node.metadata?.transparentBackground,
|
||||
count: String(node.metadata?.count || globalConfig.canvasImageCount || globalConfig.count || defaultConfig.count),
|
||||
@@ -546,6 +557,8 @@ function buildModelNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode
|
||||
},
|
||||
{
|
||||
size: globalConfig.size || defaultConfig.size,
|
||||
imageResolution: globalConfig.imageResolution || defaultConfig.imageResolution,
|
||||
imageParameters: globalConfig.imageParameters ?? "{}",
|
||||
quality: globalConfig.quality || defaultConfig.quality,
|
||||
transparentBackground: globalConfig.transparentBackground || defaultConfig.transparentBackground,
|
||||
count: String(globalConfig.canvasImageCount || globalConfig.count || defaultConfig.count),
|
||||
@@ -561,6 +574,8 @@ function buildModelNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode
|
||||
taskWorkflowProvider: "model",
|
||||
model,
|
||||
quality: generationDefaults.quality || globalConfig.quality || defaultConfig.quality,
|
||||
imageResolution: generationDefaults.imageResolution || globalConfig.imageResolution || defaultConfig.imageResolution,
|
||||
imageParameters: generationDefaults.imageParameters ?? globalConfig.imageParameters ?? "{}",
|
||||
size: generationDefaults.size ?? globalConfig.size ?? defaultConfig.size,
|
||||
transparentBackground: generationDefaults.transparentBackground || "false",
|
||||
videoSeconds: generationDefaults.videoSeconds ?? normalizeVideoDuration(globalConfig.videoSeconds || defaultConfig.videoSeconds),
|
||||
|
||||
@@ -27,8 +27,10 @@ export function CanvasImageSettingsPopover({ config, onConfigChange, showCount =
|
||||
if (profile.quality.supported) parts.push(imageQualityLabel(current.quality));
|
||||
if (showCount && profile.maxOutputs > 1) parts.push(`${current.count} 张`);
|
||||
if (profile.transparentBackground.supported && current.transparentBackground === "true") parts.push("透明");
|
||||
if (!parts.length && !profile.transparentBackground.supported) return null;
|
||||
return <CanvasSettingsPopover {...popover} label="图像设置" summary={parts.join(" · ") || "背景"} width={420}>
|
||||
<ImageSettingsPanel config={config} onConfigChange={onConfigChange} theme={theme} showCount={showCount} quickCount={3} className="space-y-3" />
|
||||
</CanvasSettingsPopover>;
|
||||
if (!parts.length && !profile.transparentBackground.supported && !profile.parameters?.some((p) => p.enabled !== false)) return null;
|
||||
return (
|
||||
<CanvasSettingsPopover {...popover} label="图像设置" summary={parts.join(" · ") || "背景"} width={420}>
|
||||
<ImageSettingsPanel config={config} onConfigChange={onConfigChange} theme={theme} showCount={showCount} quickCount={3} className="space-y-3" />
|
||||
</CanvasSettingsPopover>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,8 @@ export function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mo
|
||||
mode === "image"
|
||||
? {
|
||||
size: node.metadata?.size,
|
||||
imageResolution: node.metadata?.imageResolution,
|
||||
imageParameters: node.metadata?.imageParameters,
|
||||
quality: node.metadata?.quality,
|
||||
transparentBackground: node.metadata?.transparentBackground,
|
||||
videoWatermark: node.metadata?.watermark,
|
||||
@@ -48,6 +50,8 @@ export function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mo
|
||||
},
|
||||
{
|
||||
size: globalConfig.size || defaultConfig.size,
|
||||
imageResolution: globalConfig.imageResolution || defaultConfig.imageResolution,
|
||||
imageParameters: globalConfig.imageParameters ?? "{}",
|
||||
quality: globalConfig.quality || defaultConfig.quality,
|
||||
transparentBackground: globalConfig.transparentBackground || defaultConfig.transparentBackground,
|
||||
count: String(globalConfig.canvasImageCount || globalConfig.count || defaultConfig.count),
|
||||
@@ -61,6 +65,8 @@ export function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mo
|
||||
...globalConfig,
|
||||
model,
|
||||
quality: defaults.quality ?? globalConfig.quality ?? defaultConfig.quality,
|
||||
imageResolution: defaults.imageResolution ?? globalConfig.imageResolution ?? defaultConfig.imageResolution,
|
||||
imageParameters: defaults.imageParameters ?? globalConfig.imageParameters ?? "{}",
|
||||
size: defaults.size ?? globalConfig.size ?? defaultConfig.size,
|
||||
transparentBackground: defaults.transparentBackground ?? "false",
|
||||
videoSeconds: defaults.videoSeconds ?? normalizeVideoDuration(globalConfig.videoSeconds ?? defaultConfig.videoSeconds),
|
||||
|
||||
Reference in new issue
Block a user