131 lines
3.1 KiB
TypeScript
131 lines
3.1 KiB
TypeScript
import "server-only";
|
|
|
|
import { appConfig } from "@/lib/config";
|
|
import type {
|
|
GenerateOutputFormat,
|
|
GeneratePayload,
|
|
GenerateResult,
|
|
GeneratedImage,
|
|
HttpError,
|
|
} from "@/lib/types";
|
|
|
|
interface OpenAiImageEntry {
|
|
b64_json?: string;
|
|
revised_prompt?: string | null;
|
|
}
|
|
|
|
interface OpenAiErrorBody {
|
|
message?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface OpenAiImageResponse {
|
|
created?: number | null;
|
|
data?: OpenAiImageEntry[];
|
|
error?: OpenAiErrorBody;
|
|
usage?: unknown;
|
|
}
|
|
|
|
export async function generateImagesWithOpenAI(
|
|
payload: GeneratePayload,
|
|
): Promise<GenerateResult> {
|
|
const startedAt = Date.now();
|
|
const response = await fetch(
|
|
`${appConfig.openAiBaseUrl.replace(/\/$/, "")}/images/generations`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${appConfig.openAiApiKey}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
signal: AbortSignal.timeout(appConfig.openAiTimeoutMs),
|
|
},
|
|
);
|
|
const responseBody = await parseJsonSafely(response);
|
|
|
|
if (!response.ok) {
|
|
const error = new Error(
|
|
responseBody?.error?.message ?? `OpenAI 返回了 ${response.status} 错误。`,
|
|
) as HttpError;
|
|
error.statusCode = response.status;
|
|
error.detail = responseBody?.error ?? null;
|
|
throw error;
|
|
}
|
|
|
|
const images = Array.isArray(responseBody?.data)
|
|
? responseBody.data
|
|
.map((entry, index) => mapGeneratedImage(entry, index, payload))
|
|
.filter((image): image is GeneratedImage => image !== null)
|
|
: [];
|
|
|
|
if (images.length === 0) {
|
|
const error = new Error(
|
|
"上游已返回成功,但没有拿到图片数据。",
|
|
) as HttpError;
|
|
error.statusCode = 502;
|
|
throw error;
|
|
}
|
|
|
|
return {
|
|
images,
|
|
meta: {
|
|
model: payload.model,
|
|
size: payload.size,
|
|
quality: payload.quality,
|
|
outputFormat: payload.output_format,
|
|
background: payload.background,
|
|
moderation: payload.moderation,
|
|
outputCompression: payload.output_compression,
|
|
count: images.length,
|
|
durationMs: Date.now() - startedAt,
|
|
created: responseBody?.created ?? null,
|
|
usage: responseBody?.usage ?? null,
|
|
},
|
|
};
|
|
}
|
|
|
|
async function parseJsonSafely(
|
|
response: Response,
|
|
): Promise<OpenAiImageResponse | null> {
|
|
try {
|
|
return (await response.json()) as OpenAiImageResponse;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function mapGeneratedImage(
|
|
entry: OpenAiImageEntry,
|
|
index: number,
|
|
payload: GeneratePayload,
|
|
): GeneratedImage | null {
|
|
if (!entry?.b64_json) {
|
|
return null;
|
|
}
|
|
|
|
const fileExtension =
|
|
payload.output_format === "jpeg" ? "jpg" : payload.output_format;
|
|
const mimeType = resolveMimeType(payload.output_format);
|
|
|
|
return {
|
|
id: `image-${index + 1}`,
|
|
base64: entry.b64_json,
|
|
mimeType,
|
|
fileName: `gpt-image-${Date.now()}-${index + 1}.${fileExtension}`,
|
|
revisedPrompt: entry.revised_prompt ?? null,
|
|
};
|
|
}
|
|
|
|
function resolveMimeType(outputFormat: GenerateOutputFormat): string {
|
|
if (outputFormat === "jpeg") {
|
|
return "image/jpeg";
|
|
}
|
|
|
|
if (outputFormat === "webp") {
|
|
return "image/webp";
|
|
}
|
|
|
|
return "image/png";
|
|
}
|