feat(project): init
This commit is contained in:
130
lib/openai.ts
Normal file
130
lib/openai.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
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";
|
||||
}
|
||||
Reference in New Issue
Block a user