export const DEFAULT_BFL_MODEL = "flux-2-klein-4b"; export const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai"; export type RealtimeCanvasConfig = { enabled?: boolean; bflApiKey?: string; bflModel?: string; bflBaseUrl?: string; runwareApiKey?: string; runwareModel?: string; falApiKey?: string; apiKey?: string; fallbackEnabled?: boolean; fallbackBflApiKey?: string; fallbackBflModel?: string; fallbackBflBaseUrl?: string; fallbackRunwareApiKey?: string; fallbackRunwareModel?: string; fallbackFalApiKey?: string; fallbackApiKey?: string; negativePrompt?: string; renderMode?: "strict-structure" | "balanced" | "stylize"; strictSketch?: boolean; throttleInterval?: number; maxBuffering?: number; maxInputResolution?: number; jpegQuality?: number; defaults?: { steps?: number; cfgScale?: number; strength?: number; scheduleMu?: number; feedback?: number; imageSize?: "square" | "square_hd"; }; }; export type RealtimeProviderConfig = { apiKey: string; baseUrl: string; model: string; source: "primary" | "fallback"; label: string; }; function trimString(value: unknown) { return String(value || "").trim(); } export function normalizeBaseUrl(raw: string | undefined) { const next = trimString(raw || DEFAULT_BFL_BASE_URL); return (next || DEFAULT_BFL_BASE_URL).replace(/\/+$/, ""); } function hasFallbackIntent(config: RealtimeCanvasConfig) { if (config.fallbackEnabled === true) return true; if (config.fallbackEnabled === false) return false; return Boolean( trimString( config.fallbackBflApiKey || config.fallbackRunwareApiKey || config.fallbackFalApiKey || config.fallbackApiKey || config.fallbackBflBaseUrl || config.fallbackBflModel || config.fallbackRunwareModel ) ); } export function resolvePrimaryRealtimeProvider( config: RealtimeCanvasConfig, env: NodeJS.ProcessEnv = process.env ): RealtimeProviderConfig | null { const apiKey = trimString( config.bflApiKey || config.runwareApiKey || config.falApiKey || config.apiKey || env.BFL_API_KEY || env.BFL_KEY ); if (!apiKey) return null; const model = trimString(config.bflModel || config.runwareModel || env.BFL_MODEL || env.RUNWARE_MODEL) || DEFAULT_BFL_MODEL; const baseUrl = normalizeBaseUrl(config.bflBaseUrl || env.BFL_BASE_URL); return { apiKey, model, baseUrl, source: "primary", label: "主实时服务", }; } export function resolveFallbackRealtimeProvider( config: RealtimeCanvasConfig ): RealtimeProviderConfig | null { if (!hasFallbackIntent(config)) return null; const apiKey = trimString( config.fallbackBflApiKey || config.fallbackRunwareApiKey || config.fallbackFalApiKey || config.fallbackApiKey ); if (!apiKey) return null; const model = trimString(config.fallbackBflModel || config.fallbackRunwareModel) || DEFAULT_BFL_MODEL; const baseUrl = normalizeBaseUrl(config.fallbackBflBaseUrl || DEFAULT_BFL_BASE_URL); return { apiKey, model, baseUrl, source: "fallback", label: "备用实时服务", }; } export function resolveRealtimeProviders( config: RealtimeCanvasConfig, env: NodeJS.ProcessEnv = process.env ) { const providers: RealtimeProviderConfig[] = []; const primary = resolvePrimaryRealtimeProvider(config, env); const fallback = resolveFallbackRealtimeProvider(config); if (primary) { providers.push(primary); } if ( fallback && !providers.some( (provider) => provider.apiKey === fallback.apiKey && provider.baseUrl === fallback.baseUrl && provider.model === fallback.model ) ) { providers.push(fallback); } return providers; } export function normalizeRealtimeCanvasConfigForStorage( value: Record, env: NodeJS.ProcessEnv = process.env ): RealtimeCanvasConfig { const renderMode = value.renderMode === "strict-structure" || value.renderMode === "stylize" || value.renderMode === "balanced" ? value.renderMode : "balanced"; const primary = resolvePrimaryRealtimeProvider(value as RealtimeCanvasConfig, env); const fallback = resolveFallbackRealtimeProvider(value as RealtimeCanvasConfig); return { ...value, bflApiKey: primary?.apiKey || "", bflModel: primary?.model || DEFAULT_BFL_MODEL, bflBaseUrl: primary?.baseUrl || normalizeBaseUrl(env.BFL_BASE_URL), runwareApiKey: primary?.apiKey || "", falApiKey: primary?.apiKey || "", apiKey: primary?.apiKey || "", runwareModel: primary?.model || DEFAULT_BFL_MODEL, fallbackEnabled: Boolean(fallback), fallbackBflApiKey: fallback?.apiKey || "", fallbackBflModel: fallback?.model || DEFAULT_BFL_MODEL, fallbackBflBaseUrl: fallback?.baseUrl || DEFAULT_BFL_BASE_URL, fallbackRunwareApiKey: fallback?.apiKey || "", fallbackFalApiKey: fallback?.apiKey || "", fallbackApiKey: fallback?.apiKey || "", fallbackRunwareModel: fallback?.model || DEFAULT_BFL_MODEL, renderMode, }; }