Co-authored-by: linxi <linxi@enjoysoftware.cn>
This commit is contained in:
@@ -36,7 +36,7 @@ type PublicCanvasShare struct {
|
||||
|
||||
var publicCanvasMetadataKeys = map[string]bool{
|
||||
"content": true, "composerContent": true, "prompt": true, "status": true, "fontSize": true,
|
||||
"generationMode": true, "generationType": true, "model": true, "size": true, "quality": true,
|
||||
"generationMode": true, "generationType": true, "model": true, "size": true, "quality": true, "transparentBackground": true,
|
||||
"count": true, "seconds": true, "vquality": true, "generateAudio": true, "watermark": true,
|
||||
"audioVoice": true, "audioFormat": true, "audioSpeed": true, "audioInstructions": true,
|
||||
"naturalWidth": true, "naturalHeight": true, "freeResize": true, "isBatchRoot": true,
|
||||
|
||||
@@ -31,24 +31,25 @@ type canvasGenerationInput struct {
|
||||
}
|
||||
|
||||
type providerConfig struct {
|
||||
ChannelID string `json:"channelId"`
|
||||
APIFormat string `json:"apiFormat"`
|
||||
InterfaceType string `json:"interfaceType"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
APIKey string `json:"apiKey"`
|
||||
Model string `json:"model"`
|
||||
Size string `json:"size"`
|
||||
Quality string `json:"quality"`
|
||||
Count string `json:"count"`
|
||||
VideoSeconds string `json:"videoSeconds"`
|
||||
VQuality string `json:"vquality"`
|
||||
VideoGenerateAudio string `json:"videoGenerateAudio"`
|
||||
VideoWatermark string `json:"videoWatermark"`
|
||||
AudioVoice string `json:"audioVoice"`
|
||||
AudioFormat string `json:"audioFormat"`
|
||||
AudioSpeed string `json:"audioSpeed"`
|
||||
AudioInstructions string `json:"audioInstructions"`
|
||||
SystemPrompt string `json:"systemPrompt"`
|
||||
ChannelID string `json:"channelId"`
|
||||
APIFormat string `json:"apiFormat"`
|
||||
InterfaceType string `json:"interfaceType"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
APIKey string `json:"apiKey"`
|
||||
Model string `json:"model"`
|
||||
Size string `json:"size"`
|
||||
Quality string `json:"quality"`
|
||||
TransparentBackground string `json:"transparentBackground"`
|
||||
Count string `json:"count"`
|
||||
VideoSeconds string `json:"videoSeconds"`
|
||||
VQuality string `json:"vquality"`
|
||||
VideoGenerateAudio string `json:"videoGenerateAudio"`
|
||||
VideoWatermark string `json:"videoWatermark"`
|
||||
AudioVoice string `json:"audioVoice"`
|
||||
AudioFormat string `json:"audioFormat"`
|
||||
AudioSpeed string `json:"audioSpeed"`
|
||||
AudioInstructions string `json:"audioInstructions"`
|
||||
SystemPrompt string `json:"systemPrompt"`
|
||||
}
|
||||
|
||||
const providerHTTPTimeout = 5 * time.Minute
|
||||
@@ -325,6 +326,9 @@ func systemChannelIDFromBaseURL(baseURL string) string {
|
||||
}
|
||||
|
||||
func runImageTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) {
|
||||
if input.Config.TransparentBackground == "true" && !supportsTransparentImageBackground(input.Config) {
|
||||
return nil, errors.New("当前模型不支持透明背景,请关闭透明背景或切换至 GPT Image 1 系列模型")
|
||||
}
|
||||
var payload imageResponse
|
||||
if len(input.ReferenceImages) > 0 || input.Mask != nil {
|
||||
body := &bytes.Buffer{}
|
||||
@@ -334,6 +338,9 @@ func runImageTask(ctx context.Context, input canvasGenerationInput) (map[string]
|
||||
writeField(writer, "n", "1")
|
||||
writeField(writer, "response_format", "b64_json")
|
||||
writeField(writer, "output_format", "png")
|
||||
if input.Config.TransparentBackground == "true" {
|
||||
writeField(writer, "background", "transparent")
|
||||
}
|
||||
if input.Config.Quality != "" {
|
||||
writeField(writer, "quality", normalizeImageQuality(input.Config.Quality))
|
||||
}
|
||||
@@ -364,6 +371,9 @@ func runImageTask(ctx context.Context, input canvasGenerationInput) (map[string]
|
||||
"response_format": "b64_json",
|
||||
"output_format": "png",
|
||||
}
|
||||
if input.Config.TransparentBackground == "true" {
|
||||
body["background"] = "transparent"
|
||||
}
|
||||
if input.Config.Quality != "" {
|
||||
body["quality"] = normalizeImageQuality(input.Config.Quality)
|
||||
}
|
||||
@@ -1762,6 +1772,12 @@ func isArkPlanVideoConfig(config providerConfig) bool {
|
||||
return strings.Contains(strings.ToLower(config.BaseURL), "/api/plan/v3")
|
||||
}
|
||||
|
||||
// 透明背景必须按模型能力显式开放,避免上游静默忽略参数后返回不透明图片。
|
||||
func supportsTransparentImageBackground(config providerConfig) bool {
|
||||
model := strings.ToLower(strings.TrimSpace(config.Model))
|
||||
return strings.EqualFold(strings.TrimSpace(config.APIFormat), "openai") && (model == "gpt-image-1" || strings.HasPrefix(model, "gpt-image-1-") || strings.HasPrefix(model, "gpt-image-1."))
|
||||
}
|
||||
|
||||
func normalizeImageQuality(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "1k":
|
||||
|
||||
@@ -36,6 +36,7 @@ const GENERATION_OPTION_PROPERTIES = {
|
||||
model: { type: "string" },
|
||||
size: { type: "string" },
|
||||
quality: { type: "string" },
|
||||
transparentBackground: { type: "string", enum: ["true", "false"] },
|
||||
count: { type: "number" },
|
||||
seconds: { type: "string" },
|
||||
vquality: { type: "string" },
|
||||
@@ -1239,6 +1240,7 @@ function configNodeOp(id: string, input: Record<string, unknown>, x: number, y:
|
||||
model: resolveGenerationModel(config, mode, stringOptional(input.model)),
|
||||
size: stringOptional(input.size) || config.size,
|
||||
quality: stringOptional(input.quality) || config.quality,
|
||||
transparentBackground: stringOptional(input.transparentBackground) || config.transparentBackground,
|
||||
count: numberOptional(input.count) ?? generationCount(mode === "image" ? config.canvasImageCount || config.count : config.count),
|
||||
seconds: stringOptional(input.seconds) || config.videoSeconds,
|
||||
vquality: stringOptional(input.vquality) || config.vquality,
|
||||
@@ -1538,6 +1540,7 @@ function backendAgentProviderConfig(config: ReturnType<typeof resolveModelReques
|
||||
model: config.model,
|
||||
size: config.size,
|
||||
quality: config.quality,
|
||||
transparentBackground: config.transparentBackground,
|
||||
count: config.count,
|
||||
videoSeconds: config.videoSeconds,
|
||||
vquality: config.vquality,
|
||||
|
||||
@@ -214,6 +214,7 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can
|
||||
model,
|
||||
quality: node.metadata?.quality || globalConfig.quality || defaultConfig.quality,
|
||||
size: node.metadata?.size || globalConfig.size || defaultConfig.size,
|
||||
transparentBackground: (node.metadata?.transparentBackground || globalConfig.transparentBackground) === "true" ? "true" : "false",
|
||||
videoSeconds: normalizeVideoDuration(node.metadata?.seconds || globalConfig.videoSeconds || defaultConfig.videoSeconds),
|
||||
vquality: normalizeVideoResolution(node.metadata?.vquality || globalConfig.vquality || defaultConfig.vquality),
|
||||
videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio,
|
||||
|
||||
@@ -29,7 +29,8 @@ export function CanvasImageSettingsPopover({ config, onConfigChange, onOpenChang
|
||||
const quality = config.quality || "auto";
|
||||
const count = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1)));
|
||||
const activeSize = config.size || "auto";
|
||||
const summary = showCount ? `${imageQualityLabel(quality)} · ${imageSizeLabel(activeSize)} · ${count} 张` : `${imageQualityLabel(quality)} · ${imageSizeLabel(activeSize)}`;
|
||||
const transparentLabel = config.transparentBackground === "true" ? " · 透明" : "";
|
||||
const summary = showCount ? `${imageQualityLabel(quality)} · ${imageSizeLabel(activeSize)} · ${count} 张${transparentLabel}` : `${imageQualityLabel(quality)} · ${imageSizeLabel(activeSize)}${transparentLabel}`;
|
||||
const updateOpen = (nextOpen: boolean) => {
|
||||
setOpen(nextOpen);
|
||||
onOpenChange?.(nextOpen);
|
||||
|
||||
@@ -377,6 +377,7 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can
|
||||
model,
|
||||
quality: node.metadata?.quality || globalConfig.quality || defaultConfig.quality,
|
||||
size: node.metadata?.size || globalConfig.size || defaultConfig.size,
|
||||
transparentBackground: (node.metadata?.transparentBackground || globalConfig.transparentBackground) === "true" ? "true" : "false",
|
||||
videoSeconds: normalizeVideoDuration(node.metadata?.seconds || globalConfig.videoSeconds || defaultConfig.videoSeconds),
|
||||
vquality: normalizeVideoResolution(node.metadata?.vquality || globalConfig.vquality || defaultConfig.vquality),
|
||||
videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type ReactNode, useState } from "react";
|
||||
import { ConfigProvider, Switch } from "antd";
|
||||
|
||||
import { type CanvasTheme } from "@/lib/canvas-theme";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
import { supportsTransparentImageBackground, type AiConfig } from "@/stores/use-config-store";
|
||||
|
||||
const qualityOptions = [
|
||||
{ value: "auto", label: "自动" },
|
||||
@@ -30,7 +30,7 @@ const aspectOptions = [
|
||||
|
||||
type ImageSettingsPanelProps = {
|
||||
config: AiConfig;
|
||||
onConfigChange: (key: "quality" | "size" | "count", value: string) => void;
|
||||
onConfigChange: (key: "quality" | "size" | "transparentBackground" | "count", value: string) => void;
|
||||
theme: CanvasTheme;
|
||||
showTitle?: boolean;
|
||||
showCount?: boolean;
|
||||
@@ -42,6 +42,8 @@ type ImageSettingsPanelProps = {
|
||||
export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle = true, showCount = true, className = "w-[304px] space-y-3 rounded-2xl px-1 py-0.5", maxCount = 15, quickCount = 10 }: ImageSettingsPanelProps) {
|
||||
const [snapDimensionToStep, setSnapDimensionToStep] = useState(true);
|
||||
const quality = config.quality || "auto";
|
||||
const transparentBackground = config.transparentBackground === "true";
|
||||
const transparentBackgroundSupported = supportsTransparentImageBackground(config, config.model || config.imageModel);
|
||||
const count = Math.max(1, Math.min(maxCount, Math.floor(Math.abs(Number(config.count)) || 1)));
|
||||
const activeSize = config.size || "auto";
|
||||
const selectedAspect = aspectOptions.find((item) => (item.size || item.value) === activeSize || item.value === activeSize);
|
||||
@@ -79,6 +81,22 @@ export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle =
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<SettingTitle color={theme.node.muted}>透明背景</SettingTitle>
|
||||
<div className="mt-1 text-[11px]" style={{ color: transparentBackgroundSupported ? theme.node.muted : theme.accent.danger }}>
|
||||
{transparentBackgroundSupported ? "输出保留 Alpha 通道的 PNG" : transparentBackground ? "当前模型不支持,生成会被阻止" : "当前模型不支持透明背景"}
|
||||
</div>
|
||||
</div>
|
||||
<span title={transparentBackgroundSupported ? "使用透明背景生成 PNG 图片" : "仅 GPT Image 1 系列模型支持透明背景"} onMouseDown={(event) => event.stopPropagation()}>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={transparentBackground}
|
||||
disabled={!transparentBackgroundSupported && !transparentBackground}
|
||||
onChange={(checked) => onConfigChange("transparentBackground", checked ? "true" : "false")}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<SettingTitle color={theme.node.muted}>尺寸</SettingTitle>
|
||||
|
||||
@@ -107,6 +107,7 @@ export function backendProviderConfig(config: AiConfig) {
|
||||
model: requestConfig.model,
|
||||
size: config.size,
|
||||
quality: config.quality,
|
||||
transparentBackground: config.transparentBackground,
|
||||
count: config.count,
|
||||
videoSeconds: config.videoSeconds,
|
||||
vquality: config.vquality,
|
||||
@@ -177,6 +178,7 @@ export function buildImageGenerationMetadata(type: CanvasImageGenerationType, co
|
||||
model: config.model,
|
||||
size: config.size,
|
||||
quality: config.quality,
|
||||
transparentBackground: config.transparentBackground,
|
||||
count,
|
||||
references: references.map(referenceUrl).filter((url): url is string => Boolean(url)),
|
||||
};
|
||||
@@ -338,6 +340,7 @@ export function buildGenerationConfig(config: AiConfig, node: CanvasNodeData | u
|
||||
model,
|
||||
quality: node?.metadata?.quality || config.quality || defaultConfig.quality,
|
||||
size: node?.metadata?.size || config.size || defaultConfig.size,
|
||||
transparentBackground: (node?.metadata?.transparentBackground || config.transparentBackground) === "true" ? "true" : "false",
|
||||
videoSeconds: normalizeVideoDuration(node?.metadata?.seconds || config.videoSeconds || defaultConfig.videoSeconds),
|
||||
vquality: normalizeVideoResolution(node?.metadata?.vquality || config.vquality || defaultConfig.vquality),
|
||||
videoGenerateAudio: node?.metadata?.generateAudio || config.videoGenerateAudio || defaultConfig.videoGenerateAudio,
|
||||
|
||||
@@ -1030,6 +1030,8 @@ function InfiniteCanvasPage() {
|
||||
prompt: "",
|
||||
model: effectiveConfig.imageModel || effectiveConfig.model,
|
||||
size: effectiveConfig.size,
|
||||
quality: effectiveConfig.quality,
|
||||
transparentBackground: effectiveConfig.transparentBackground,
|
||||
count: getGenerationCount(effectiveConfig.canvasImageCount || effectiveConfig.count),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -62,7 +62,7 @@ export function useCanvasGenerationRetry({ projectId, activatedSkills, nodesRef,
|
||||
const retryMode: CanvasNodeGenerationMode = node.type === CanvasNodeType.Text ? "text" : node.type === CanvasNodeType.Video ? "video" : node.type === CanvasNodeType.Audio ? "audio" : "image";
|
||||
const generationConfig =
|
||||
hasSavedImageMetadata && savedImageMetadata
|
||||
? { ...effectiveConfig, model: savedImageMetadata.model || effectiveConfig.imageModel || effectiveConfig.model, quality: savedImageMetadata.quality || effectiveConfig.quality, size: savedImageMetadata.size || effectiveConfig.size, count: "1" }
|
||||
? { ...effectiveConfig, model: savedImageMetadata.model || effectiveConfig.imageModel || effectiveConfig.model, quality: savedImageMetadata.quality || effectiveConfig.quality, size: savedImageMetadata.size || effectiveConfig.size, transparentBackground: (savedImageMetadata.transparentBackground || effectiveConfig.transparentBackground) === "true" ? "true" : "false", count: "1" }
|
||||
: { ...buildGenerationConfig(effectiveConfig, sourceNode, retryMode), count: "1" };
|
||||
if (!isAiConfigReady(generationConfig, generationConfig.model)) {
|
||||
openConfigDialog(true);
|
||||
@@ -135,7 +135,7 @@ export function useCanvasGenerationRetry({ projectId, activatedSkills, nodesRef,
|
||||
const imageConfig = NODE_DEFAULT_SIZE[CanvasNodeType.Image];
|
||||
const imageSize = fitNodeSize(uploadedImage.width, uploadedImage.height, imageConfig.width, imageConfig.height);
|
||||
const generationMetadata = savedImageMetadata?.generationType
|
||||
? { generationType: savedImageMetadata.generationType, model: generationConfig.model, size: generationConfig.size, quality: generationConfig.quality, count: savedImageMetadata.count || 1, references: savedImageMetadata.references }
|
||||
? { generationType: savedImageMetadata.generationType, model: generationConfig.model, size: generationConfig.size, quality: generationConfig.quality, transparentBackground: generationConfig.transparentBackground, count: savedImageMetadata.count || 1, references: savedImageMetadata.references }
|
||||
: buildImageGenerationMetadata(useReferenceImages ? "edit" : "generation", generationConfig, 1, retryImages);
|
||||
setNodes((current) => current.map((item) => (item.id === node.id ? { ...item, type: CanvasNodeType.Image, width: imageSize.width, height: imageSize.height, metadata: { ...item.metadata, ...imageMetadata(uploadedImage), prompt, ...generationMetadata } } : item)));
|
||||
} catch (error) {
|
||||
|
||||
@@ -278,6 +278,7 @@ export function useCanvasUpload({
|
||||
model: undefined,
|
||||
size: undefined,
|
||||
quality: undefined,
|
||||
transparentBackground: undefined,
|
||||
count: undefined,
|
||||
references: undefined,
|
||||
primaryImageId: undefined,
|
||||
|
||||
@@ -493,6 +493,7 @@ function backendProviderConfig(config: ReturnType<typeof resolveModelRequestConf
|
||||
model: config.model,
|
||||
size: config.size,
|
||||
quality: config.quality,
|
||||
transparentBackground: config.transparentBackground,
|
||||
count: config.count,
|
||||
videoSeconds: config.videoSeconds,
|
||||
vquality: config.vquality,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from "axios";
|
||||
|
||||
import { buildApiUrl, isSystemProxyBaseUrl, resolveBackendApiUrl, resolveModelRequestConfig, type AiConfig, type ModelChannel } from "@/stores/use-config-store";
|
||||
import { buildApiUrl, isSystemProxyBaseUrl, resolveBackendApiUrl, resolveModelRequestConfig, supportsTransparentImageBackground, type AiConfig, type ModelChannel } from "@/stores/use-config-store";
|
||||
import { nanoid } from "nanoid";
|
||||
import { dataUrlToFile } from "@/lib/image-utils";
|
||||
import { buildImageReferencePromptText } from "@/lib/image-reference-prompt";
|
||||
@@ -233,6 +233,13 @@ function readStatusError(status: number | undefined, fallback: string) {
|
||||
return status ? `${fallback}:${status}` : fallback;
|
||||
}
|
||||
|
||||
function assertTransparentBackgroundSupported(config: AiConfig, model: string) {
|
||||
if (config.transparentBackground !== "true") return;
|
||||
if (!supportsTransparentImageBackground(config, model)) {
|
||||
throw new Error("当前模型不支持透明背景,请关闭“透明背景”或切换至 GPT Image 1 系列模型");
|
||||
}
|
||||
}
|
||||
|
||||
function withSystemPrompt(config: AiConfig, prompt: string) {
|
||||
const systemPrompt = config.systemPrompt.trim();
|
||||
return systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt;
|
||||
@@ -756,7 +763,9 @@ function parseGeminiImagePayload(payload: GeminiPayload) {
|
||||
}
|
||||
|
||||
export async function requestGeneration(config: AiConfig, prompt: string, options?: RequestOptions) {
|
||||
const requestConfig = resolveModelRequestConfig(config, config.model || config.imageModel);
|
||||
const selectedModel = config.model || config.imageModel;
|
||||
assertTransparentBackgroundSupported(config, selectedModel);
|
||||
const requestConfig = resolveModelRequestConfig(config, selectedModel);
|
||||
const n = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1)));
|
||||
if (requestConfig.apiFormat === "gemini") {
|
||||
try {
|
||||
@@ -778,6 +787,7 @@ export async function requestGeneration(config: AiConfig, prompt: string, option
|
||||
...(requestSize ? { size: requestSize } : {}),
|
||||
response_format: "b64_json",
|
||||
output_format: IMAGE_OUTPUT_FORMAT,
|
||||
...(config.transparentBackground === "true" ? { background: "transparent" } : {}),
|
||||
},
|
||||
{
|
||||
headers: aiHeaders(requestConfig, "application/json"),
|
||||
@@ -792,7 +802,9 @@ export async function requestGeneration(config: AiConfig, prompt: string, option
|
||||
}
|
||||
|
||||
export async function requestEdit(config: AiConfig, prompt: string, references: ReferenceImage[], mask?: ReferenceImage, options?: RequestOptions) {
|
||||
const requestConfig = resolveModelRequestConfig(config, config.model || config.imageModel);
|
||||
const selectedModel = config.model || config.imageModel;
|
||||
assertTransparentBackgroundSupported(config, selectedModel);
|
||||
const requestConfig = resolveModelRequestConfig(config, selectedModel);
|
||||
const n = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1)));
|
||||
const requestPrompt = buildImageReferencePromptText(prompt, references);
|
||||
if (requestConfig.apiFormat === "gemini") {
|
||||
@@ -811,6 +823,9 @@ export async function requestEdit(config: AiConfig, prompt: string, references:
|
||||
formData.set("n", String(n));
|
||||
formData.set("response_format", "b64_json");
|
||||
formData.set("output_format", IMAGE_OUTPUT_FORMAT);
|
||||
if (config.transparentBackground === "true") {
|
||||
formData.set("background", "transparent");
|
||||
}
|
||||
if (quality) {
|
||||
formData.set("quality", quality);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ export type AiConfig = {
|
||||
audioModels: string[];
|
||||
quality: string;
|
||||
size: string;
|
||||
transparentBackground: string;
|
||||
count: string;
|
||||
canvasImageCount: string;
|
||||
};
|
||||
@@ -101,6 +102,7 @@ export const defaultConfig: AiConfig = {
|
||||
audioModels: ["default::gpt-4o-mini-tts"],
|
||||
quality: "auto",
|
||||
size: "1:1",
|
||||
transparentBackground: "false",
|
||||
count: "1",
|
||||
canvasImageCount: "1",
|
||||
};
|
||||
@@ -274,6 +276,7 @@ export function normalizeConfigSnapshot(snapshot: ConfigStoreSnapshot) {
|
||||
vquality: normalizeVideoResolution(config.vquality),
|
||||
videoGenerateAudio: config.videoGenerateAudio || "true",
|
||||
videoWatermark: config.videoWatermark || "false",
|
||||
transparentBackground: config.transparentBackground === "true" ? "true" : "false",
|
||||
canvasImageCount: config.canvasImageCount || defaultConfig.canvasImageCount,
|
||||
imageModels,
|
||||
videoModels,
|
||||
@@ -379,6 +382,13 @@ 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 supportsTransparentImageBackground(config: AiConfig, value: string) {
|
||||
const channel = resolveModelChannel(config, value);
|
||||
const model = modelOptionName(value).trim().toLowerCase();
|
||||
return channel.apiFormat === "openai" && (model === "gpt-image-1" || model.startsWith("gpt-image-1-") || model.startsWith("gpt-image-1."));
|
||||
}
|
||||
|
||||
export function resolveModelRequestConfig(config: AiConfig, value: string) {
|
||||
const channel = resolveModelChannel(config, value);
|
||||
return {
|
||||
|
||||
@@ -146,6 +146,7 @@ export type CanvasNodeMetadata = {
|
||||
model?: string;
|
||||
size?: string;
|
||||
quality?: string;
|
||||
transparentBackground?: string;
|
||||
count?: number;
|
||||
seconds?: string;
|
||||
vquality?: string;
|
||||
|
||||
Reference in New Issue
Block a user