import sharp from "sharp"; import { ensurePublicImageUrl } from "@/lib/realtime/image-input"; import { collectFalImageUrls, normalizeFalCredentials } from "@/lib/realtime/fal-shared"; const DEFAULT_TIMEOUT_MS = 25_000; export type FalCapabilityCode = | "SUPPORTED" | "AUTH_FAILED" | "MODEL_INVALID" | "UPSTREAM_ERROR"; export type FalModelProbeResult = { supported: boolean; code: FalCapabilityCode; message: string; status?: number; }; type FalProbeParams = { apiKey: string; modelId?: string; timeoutMs?: number; }; let cachedProbeImageUrl: string | null = null; async function getProbeImageUrl() { if (cachedProbeImageUrl) return cachedProbeImageUrl; const buffer = await sharp({ create: { width: 64, height: 64, channels: 3, background: { r: 255, g: 255, b: 255 }, }, }) .png() .toBuffer(); cachedProbeImageUrl = await ensurePublicImageUrl({ imageUrl: `data:image/png;base64,${buffer.toString("base64")}`, prefix: "realtime-fal-probe", }); return cachedProbeImageUrl; } function toErrorMessage(error: unknown, fallback: string) { if (!error) return fallback; if (typeof error === "string") return error.trim() || fallback; if (typeof error === "object") { const anyError = error as Record; const message = String( anyError.message || anyError.error || anyError.detail || anyError.reason || "" ).trim(); if (message) return message; } if (error instanceof Error && error.message.trim()) { return error.message.trim(); } return fallback; } function isModelInvalid(message: string) { const normalized = message.toLowerCase(); return ( normalized.includes("model") && (normalized.includes("not found") || normalized.includes("invalid") || normalized.includes("unknown") || normalized.includes("does not exist")) ); } export async function probeFalModelSupport( params: FalProbeParams ): Promise { const apiKey = normalizeFalCredentials(params.apiKey); const modelId = String(params.modelId || "").trim(); if (!apiKey) { return { supported: false, code: "AUTH_FAILED", message: "缺少 FAL API Key。", }; } if (!modelId) { return { supported: false, code: "MODEL_INVALID", message: "缺少 FAL 模型 ID。", }; } try { const imageUrl = await getProbeImageUrl(); const fal = await import("@fal-ai/serverless-client"); fal.config({ credentials: apiKey, }); const result = await fal.subscribe(modelId, { input: { prompt: "white square photo", image_urls: [imageUrl], }, logs: false, timeout: Math.max(5_000, Number(params.timeoutMs || DEFAULT_TIMEOUT_MS)), }); const urls = collectFalImageUrls(result); if (urls.length > 0) { return { supported: true, code: "SUPPORTED", message: "模型可用。", status: 200, }; } return { supported: false, code: "UPSTREAM_ERROR", message: "FAL 探针已返回,但未发现可用图片输出。", status: 502, }; } catch (error) { const message = toErrorMessage(error, "FAL 探针请求失败。"); if (/401|403|unauthorized|forbidden|invalid api key|authentication/i.test(message)) { return { supported: false, code: "AUTH_FAILED", message: "FAL API Key 无效或无权限,请检查后重试。", status: 401, }; } if (isModelInvalid(message)) { return { supported: false, code: "MODEL_INVALID", message: `模型 ${modelId} 不存在或不可用,请更换模型后重试。`, status: 404, }; } return { supported: false, code: "UPSTREAM_ERROR", message, status: 502, }; } }