Files
NianAIGC/lib/evolink/image-client.ts
2026-05-29 14:32:02 +08:00

270 lines
8.5 KiB
TypeScript

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<string, unknown>,
settings = getEvolinkImageSettings()
): Record<string, unknown> {
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<string, unknown> = {
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<string, unknown>,
config = getEvolinkImageConfig()
): Promise<EvolinkTaskResponse> {
if (!config) throw new Error("EvoLink API key is not configured.");
return callEvolinkApi<EvolinkTaskResponse>("/v1/images/generations", config, {
method: "POST",
body: JSON.stringify(payload)
});
}
export async function queryEvolinkTask(
taskId: string,
config = getEvolinkImageConfig()
): Promise<EvolinkTaskResponse> {
if (!config) throw new Error("EvoLink API key is not configured.");
return callEvolinkApi<EvolinkTaskResponse>(`/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<string>();
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<string, unknown>, input: Record<string, unknown>) {
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<string, unknown> | undefined {
return typeof value === "object" && value !== null && !Array.isArray(value)
? value as Record<string, unknown>
: undefined;
}
function collectUrls(value: unknown, urls: Set<string>) {
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<T>(
path: string,
config: EvolinkImageConfig,
init: RequestInit
): Promise<T> {
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;
}