feat: 多功能更新

This commit is contained in:
2026-04-26 00:39:28 +08:00
parent abd1b03529
commit e26e2f5491
26 changed files with 992 additions and 330 deletions
+109 -70
View File
@@ -1,20 +1,29 @@
import { randomUUID } from "node:crypto";
import { consola } from "consola";
import type {
IImageGenerateData,
IImageGenerateRequest
} from "#shared/types/openai";
import {
askImgStream,
clearNewApiAuthCookies,
createApiLogger,
createErrorResponse,
createRunningImageGeneration,
downloadImageAsBase64,
createSuccessResponse,
createUpstreamErrorResponse,
finishImageGenerationFailed,
finishImageGenerationSuccess,
getAiArtStudioTokenKey,
getNewApiUserIdFromCookie,
isUnauthorizedError
getUserArchiveIdentity,
isUnauthorizedError,
toSafeLogError,
uploadImageFromUrl
} from "~~/server/utils";
type ApiLogger = ReturnType<typeof createApiLogger>;
export default defineEventHandler(async (event) => {
const requestId = randomUUID();
const logger = createApiLogger("images.generate");
let stage = "read_body";
const requestBody = await readBody<Partial<IImageGenerateRequest> | null>(
event
@@ -25,87 +34,95 @@ export default defineEventHandler(async (event) => {
requestBody !== null &&
(typeof requestBody !== "object" || Array.isArray(requestBody))
) {
logger.warn("请求体格式错误", {
bodyType: Array.isArray(requestBody) ? "array" : typeof requestBody
});
return createErrorResponse(400, "请求体必须是 JSON 对象");
}
const prompt = requestBody?.prompt?.trim();
if (!prompt) {
logger.warn("缺少提示词");
return createErrorResponse(400, "请输入图片描述");
}
logger.info("开始", {
promptLength: prompt.length
});
let record: { id: bigint; startedAt: Date } | null = null;
try {
consola.info("[images.generate] 开始", {
requestId,
promptLength: prompt.length
});
stage = "read_user_id";
const userId = getNewApiUserIdFromCookie(event);
consola.info("[images.generate] 读取用户成功", {
requestId,
logger.info("读取用户成功", {
userId
});
stage = "create_running_record";
record = await createRunningImageGeneration(userId, prompt);
consola.info("[images.generate] 创建生图记录成功", {
requestId,
logger.info("创建生图记录成功", {
recordId: record.id.toString()
});
stage = "get_api_key";
const apiKey = await getAiArtStudioTokenKey(event);
consola.info("[images.generate] 获取服务端 key 成功", {
requestId,
logger.info("获取服务端 key 成功", {
recordId: record.id.toString()
});
stage = "call_image_api";
const result = await askImg({
stage = "call_image_stream_api";
const result = await askImgStream({
apiKey,
prompt
});
consola.info("[images.generate] 上游生图成功", {
requestId,
logger.info("上游流式生图成功", {
recordId: record.id.toString(),
hasImageUrl: Boolean(result.imageUrl)
hasImageUrl: Boolean(result.imageUrl),
contentLength: getStreamContentLength(result.upstreamResponse),
hasUsage: hasStreamUsage(result.upstreamResponse)
});
stage = "download_image_base64";
const downloadedImage = await downloadGeneratedImage(result.imageUrl);
consola.info("[images.generate] 图片归档下载完成", {
requestId,
stage = "upload_lsky";
const archiveResult = await archiveGeneratedImage({
imageUrl: result.imageUrl,
userId,
recordId: record.id,
createdAt: record.startedAt,
logger
});
logger.info("图床归档完成", {
recordId: record.id.toString(),
hasBase64: Boolean(downloadedImage.base64),
mimeType: downloadedImage.mimeType
hosted: Boolean(archiveResult.hostedImageUrl),
mimeType: archiveResult.imageMimeType
});
stage = "finish_success_record";
await finishImageGenerationSuccess(record.id, record.startedAt, {
imageUrl: result.imageUrl,
imageBase64: downloadedImage.base64,
imageMimeType: downloadedImage.mimeType,
hostedImageUrl: archiveResult.hostedImageUrl,
imageMimeType: archiveResult.imageMimeType,
revisedPrompt: result.revisedPrompt,
upstreamResponse: result.upstreamResponse,
errorMessage: downloadedImage.errorMessage
hostedResponse: archiveResult.hostedResponse,
errorMessage: archiveResult.errorMessage
});
consola.info("[images.generate] 完成", {
requestId,
recordId: record.id.toString()
logger.done("成", {
recordId: record.id.toString(),
hosted: Boolean(archiveResult.hostedImageUrl)
});
return createSuccessResponse<IImageGenerateData>(
{
imageUrl: result.imageUrl,
hostedImageUrl: archiveResult.hostedImageUrl,
revisedPrompt: result.revisedPrompt
},
"图片生成成功"
);
} catch (error) {
consola.error("[images.generate] 失败", {
requestId,
logger.error("失败", {
stage,
recordId: record?.id.toString() ?? null,
error: toSafeLogError(error)
@@ -114,8 +131,7 @@ export default defineEventHandler(async (event) => {
if (record) {
await finishImageGenerationFailed(record.id, record.startedAt, error).catch(
(recordError) => {
consola.error("[images.generate] 更新失败记录失败", {
requestId,
logger.error("更新失败记录失败", {
recordId: record?.id.toString() ?? null,
error: toSafeLogError(recordError)
});
@@ -132,52 +148,75 @@ export default defineEventHandler(async (event) => {
}
});
/** 图片 URL 仍然可用时,base64 保存失败不阻断本次生成结果 */
const downloadGeneratedImage = async (
imageUrl: string
): Promise<{
base64: string | null;
mimeType: string | null;
const archiveGeneratedImage = async ({
imageUrl,
userId,
recordId,
createdAt,
logger
}: {
imageUrl: string;
userId: number;
recordId: bigint;
createdAt: Date;
logger: ApiLogger;
}): Promise<{
hostedImageUrl: string | null;
imageMimeType: string | null;
hostedResponse: unknown;
errorMessage: string | null;
}> => {
try {
const image = await downloadImageAsBase64(imageUrl);
// 图床归档失败不能阻断本次生成结果,前端仍可使用上游图片 URL。
const identity = await getUserArchiveIdentity(userId);
const uploaded = await uploadImageFromUrl({
imageUrl,
userId,
username: identity.username,
recordId,
createdAt
});
return {
base64: image.base64,
mimeType: image.mimeType,
hostedImageUrl: uploaded.publicUrl,
imageMimeType: uploaded.mimetype,
hostedResponse: uploaded.response,
errorMessage: null
};
} catch (error) {
consola.error("[images.generate] 保存生成图片 base64 失败", {
logger.error("图床归档失败", {
recordId: recordId.toString(),
error: toSafeLogError(error)
});
return {
base64: null,
mimeType: null,
errorMessage: "图片生成成功,但 base64 保存失败"
hostedImageUrl: null,
imageMimeType: null,
hostedResponse: null,
errorMessage: "图片生成成功,但图床归档失败"
};
}
};
const toSafeLogError = (error: unknown) => {
const maybeError = error as {
data?: unknown;
message?: string;
name?: string;
response?: {
_data?: unknown;
status?: number;
};
stack?: string;
status?: number;
statusCode?: number;
};
// 日志只读取流式聚合结果的元信息,不记录完整提示词、key 或图片内容。
const getStreamContentLength = (upstreamResponse: unknown) => {
if (
upstreamResponse &&
typeof upstreamResponse === "object" &&
"content" in upstreamResponse &&
typeof upstreamResponse.content === "string"
) {
return upstreamResponse.content.length;
}
return {
name: maybeError.name,
message: maybeError.message,
status: maybeError.status ?? maybeError.statusCode ?? maybeError.response?.status,
data: maybeError.data ?? maybeError.response?._data ?? null,
stack: maybeError.stack
};
return 0;
};
const hasStreamUsage = (upstreamResponse: unknown) => {
return (
upstreamResponse !== null &&
typeof upstreamResponse === "object" &&
"usage" in upstreamResponse &&
Boolean(upstreamResponse.usage)
);
};