import type { EnabledImageCapability, GenerationStatus } from "@/lib/types"; export type ImageCreationEngine = "jimeng" | "evolink"; export type EvolinkImageSettings = { apiKey?: string; baseUrl: string; model: string; quality?: string; resolution?: string; }; export type EvolinkImageConfig = EvolinkImageSettings & { apiKey: string; }; export type EvolinkTaskResponse = { id?: string; task_id?: string; status?: string; results?: unknown; data?: unknown; error?: { code?: string | number; message?: string; } | null; message?: string; }; export function getSelectedImageEngine(): ImageCreationEngine { const value = (process.env.IMAGE_CREATION_ENGINE || process.env.IMAGE_PROVIDER || "jimeng").trim().toLowerCase(); return value === "evolink" ? "evolink" : "jimeng"; } export function getEffectiveImageEngine(capability: EnabledImageCapability): ImageCreationEngine { if (capability === "image.upscale") return "jimeng"; if (capability === "image.generate") return selectedEngineFrom(process.env.IMAGE_GENERATE_ENGINE); if (capability === "image.inpaint") return selectedEngineFrom(process.env.IMAGE_INPAINT_ENGINE); return getSelectedImageEngine(); } function selectedEngineFrom(value: string | undefined): ImageCreationEngine { const normalized = (value || "").trim().toLowerCase(); if (normalized === "evolink") return "evolink"; if (normalized === "jimeng") return "jimeng"; return getSelectedImageEngine(); } export function getEvolinkImageSettings(): EvolinkImageSettings { return { apiKey: process.env.EVOLINK_API_KEY?.trim() || undefined, baseUrl: (process.env.EVOLINK_BASE_URL || "https://api.evolink.ai").replace(/\/+$/, ""), model: process.env.EVOLINK_IMAGE_MODEL || "gpt-image-2", quality: cleanOptional(process.env.EVOLINK_IMAGE_QUALITY), resolution: cleanOptional(process.env.EVOLINK_IMAGE_RESOLUTION || "2K") }; } export function getEvolinkImageConfig(): EvolinkImageConfig | null { const settings = getEvolinkImageSettings(); if (!settings.apiKey) return null; return { ...settings, apiKey: settings.apiKey }; } export function shouldMockEvolinkApi(): boolean { const flag = (process.env.EVOLINK_MOCK || "auto").trim().toLowerCase(); if (flag === "1" || flag === "true") return true; if (flag === "0" || flag === "false") return false; return getEvolinkImageConfig() === null; } export function buildEvolinkImagePayload( capability: EnabledImageCapability, input: Record, settings = getEvolinkImageSettings() ): Record { if (capability === "image.upscale") { throw new Error("EvoLink image engine does not support upscale in this integration."); } const prompt = String(input.prompt || "").trim(); const imageUrls = asStringArray(input.imageUrls); const payload: Record = { model: settings.model, prompt: prompt || (capability === "image.inpaint" ? "删除" : ""), n: 1 }; if (!payload.prompt) throw new Error("Prompt is required for image generation."); const quality = cleanOptional(typeof input.quality === "string" ? input.quality : undefined) || settings.quality; if (quality) payload.quality = quality; if (settings.resolution) payload.resolution = settings.resolution; assignSize(payload, input); if (capability === "image.inpaint") { if (imageUrls.length !== 2) { throw new Error("EvoLink inpainting requires original image and mask URLs."); } payload.image_urls = [imageUrls[0]]; payload.mask_url = imageUrls[1]; return payload; } if (imageUrls.length) payload.image_urls = imageUrls; return payload; } export async function submitEvolinkImageTask( payload: Record, config = getEvolinkImageConfig() ): Promise { if (!config) throw new Error("EvoLink API key is not configured."); return callEvolinkApi("/v1/images/generations", config, { method: "POST", body: JSON.stringify(payload) }); } export async function queryEvolinkTask( taskId: string, config = getEvolinkImageConfig() ): Promise { if (!config) throw new Error("EvoLink API key is not configured."); return callEvolinkApi(`/v1/tasks/${encodeURIComponent(taskId)}`, config, { method: "GET" }); } export function getEvolinkTaskId(response: EvolinkTaskResponse): string | undefined { const data = asRecord(response.data); return firstString(response.id, response.task_id, data?.id, data?.task_id); } export function mapEvolinkStatus(response: EvolinkTaskResponse): GenerationStatus { const status = String(response.status || asRecord(response.data)?.status || "").toLowerCase(); if (["completed", "complete", "succeeded", "success", "done"].includes(status)) return "succeeded"; if (["running", "processing", "generating", "in_progress"].includes(status)) return "running"; if (["queued", "pending", "created", "waiting", "in_queue"].includes(status)) return "queued"; if (["expired", "not_found"].includes(status)) return "expired"; if (["cancelled", "canceled"].includes(status)) return "cancelled"; if (["failed", "error"].includes(status)) return "failed"; return "running"; } export function extractEvolinkResultUrls(response: EvolinkTaskResponse): string[] { const data = asRecord(response.data); const candidates = [ response.results, data?.results, data?.images, data?.image_urls, data?.output, data?.outputs ]; const urls = new Set(); for (const candidate of candidates) collectUrls(candidate, urls); return [...urls]; } function cleanOptional(value: string | undefined): string | undefined { const normalized = value?.trim(); return normalized || undefined; } function assignSize(payload: Record, input: Record) { if (typeof input.size === "string" && input.size.trim()) { payload.size = input.size.trim(); return; } const width = asPositiveInteger(input.width); const height = asPositiveInteger(input.height); if (!width || !height) return; payload.size = ratioFromDimensions(width, height) || `${width}x${height}`; } function asStringArray(value: unknown): string[] { if (!Array.isArray(value)) return []; return value.map(String).map((item) => item.trim()).filter(Boolean); } function asPositiveInteger(value: unknown): number | undefined { const parsed = Number(value); if (!Number.isInteger(parsed) || parsed <= 0) return undefined; return parsed; } function firstString(...values: unknown[]): string | undefined { for (const value of values) { if (typeof value === "string" && value.trim()) return value.trim(); } return undefined; } function ratioFromDimensions(width: number, height: number): string | undefined { const divisor = gcd(width, height); const ratio = `${width / divisor}:${height / divisor}`; return supportedRatios.has(ratio) ? ratio : undefined; } function gcd(a: number, b: number): number { return b === 0 ? a : gcd(b, a % b); } const supportedRatios = new Set([ "1:1", "1:2", "2:1", "1:3", "3:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "9:21", "21:9" ]); function asRecord(value: unknown): Record | undefined { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : undefined; } function collectUrls(value: unknown, urls: Set) { if (!value) return; if (typeof value === "string") { if (/^https?:\/\//.test(value)) urls.add(value); return; } if (Array.isArray(value)) { for (const item of value) collectUrls(item, urls); return; } const record = asRecord(value); if (!record) return; for (const key of ["url", "image_url", "imageUrl", "result_url", "resultUrl"]) { collectUrls(record[key], urls); } } async function callEvolinkApi( path: string, config: EvolinkImageConfig, init: RequestInit ): Promise { const headers = new Headers(init.headers); headers.set("Content-Type", "application/json"); headers.set("Authorization", `Bearer ${config.apiKey}`); const response = await fetch(`${config.baseUrl}${path}`, { ...init, headers }); const text = await response.text(); let json: unknown; try { json = text ? JSON.parse(text) : {}; } catch { throw new Error(`EvoLink returned non-JSON response: ${response.status} ${text.slice(0, 240)}`); } if (!response.ok) { const message = typeof json === "object" && json && "message" in json ? String(json.message) : text; throw new Error(`EvoLink HTTP ${response.status}: ${message}`); } return json as T; }