fix: persist generated images before gallery save

This commit is contained in:
134 2026-08-22 17:25:34 +08:00
parent afac47add2
commit 42e1175830
2 changed files with 106 additions and 19 deletions

View File

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { generatedImage } from "@/lib/db/schema";
import { eq, desc, sql } from "drizzle-orm";
import { generatedImage, imageArchiveTask } from "@/lib/db/schema";
import { and, desc, eq, isNull, sql } from "drizzle-orm";
import { auth } from "@/app/(auth)/auth";
export async function GET(request: NextRequest) {
@ -19,12 +19,23 @@ export async function GET(request: NextRequest) {
const page = Number.parseInt(searchParams.get("page") || "1");
const pageSize = Number.parseInt(searchParams.get("pageSize") || "24");
const offset = (page - 1) * pageSize;
const visibleImageFilter = and(
eq(generatedImage.userId, userId),
isNull(imageArchiveTask.id)
);
// 查询总数
const countResult = await db
.select({ count: sql<number>`count(*)::int` })
.from(generatedImage)
.where(eq(generatedImage.userId, userId));
.leftJoin(
imageArchiveTask,
and(
eq(imageArchiveTask.sourceUrl, generatedImage.originalUrl),
eq(imageArchiveTask.status, "abandoned")
)
)
.where(visibleImageFilter);
const total = countResult[0]?.count || 0;
@ -42,7 +53,14 @@ export async function GET(request: NextRequest) {
modelId: generatedImage.modelId, // 添加 modelId 字段
})
.from(generatedImage)
.where(eq(generatedImage.userId, userId))
.leftJoin(
imageArchiveTask,
and(
eq(imageArchiveTask.sourceUrl, generatedImage.originalUrl),
eq(imageArchiveTask.status, "abandoned")
)
)
.where(visibleImageFilter)
.orderBy(desc(generatedImage.createdAt))
.limit(pageSize)
.offset(offset);

View File

@ -1,5 +1,7 @@
import "server-only";
import { randomUUID } from "node:crypto";
import sharp from "sharp";
import { createLogger } from "@/lib/observability/logger";
import {
enqueueImageArchiveTasks,
@ -31,6 +33,10 @@ export function isDeferredImageArchiveEnabled() {
return process.env.AI_IMAGE_DEFERRED_ARCHIVE !== "false";
}
export function isImmediateImageArchiveEnabled() {
return process.env.AI_IMAGE_IMMEDIATE_ARCHIVE !== "false";
}
export async function registerGeneratedImageTemporaryUrls(params: {
urls: string[];
metadata?: DeferredImageArchiveMetadata | null;
@ -80,18 +86,19 @@ export async function formatDeferredGeneratedImageMarkdown(params: {
throw new Error("AI 未返回可用图片地址");
}
await registerGeneratedImageTemporaryUrls({
urls,
metadata: {
providerType: params.providerType,
modelId: params.modelId,
imageSize: params.imageSize,
...(params.metadata || {}),
},
});
const metadata = {
providerType: params.providerType,
modelId: params.modelId,
imageSize: params.imageSize,
...(params.metadata || {}),
};
const persisted = await persistGeneratedImageUrls({ urls, metadata });
return urls
.map((url) => `![thumb|||${url}|||${params.imageSize}](${url})`)
return persisted
.map(
({ originalUrl, thumbnailUrl }) =>
`![thumb|||${originalUrl}|||${params.imageSize}](${thumbnailUrl})`
)
.join("\n\n");
}
@ -102,17 +109,61 @@ export async function createDeferredUploadedImage(
const url = String(sourceUrl || "").trim();
if (!isDeferredImageArchiveEnabled() || !isHttpUrl(url)) return null;
await registerGeneratedImageTemporaryUrls({
const [persisted] = await persistGeneratedImageUrls({
urls: [url],
metadata: metadata || null,
});
return {
thumbnail: url,
original: url,
thumbnail: persisted?.thumbnailUrl || url,
original: persisted?.originalUrl || url,
};
}
async function persistGeneratedImageUrls(params: {
urls: string[];
metadata?: DeferredImageArchiveMetadata | null;
}) {
if (!isImmediateImageArchiveEnabled()) {
await registerGeneratedImageTemporaryUrls(params);
return params.urls.map((url) => ({
originalUrl: url,
thumbnailUrl: url,
}));
}
return await Promise.all(
params.urls.map(async (url) => {
try {
const archived = await archiveTemporaryImage(url);
logger.info("generated image archived before response", {
providerType: params.metadata?.providerType,
modelId: params.metadata?.modelId,
bytes: archived.bytes,
});
return {
originalUrl: archived.url,
thumbnailUrl: archived.thumbnailUrl || archived.url,
};
} catch (error) {
logger.error("immediate generated image archive failed; queued for retry", {
providerType: params.metadata?.providerType,
modelId: params.metadata?.modelId,
error: error instanceof Error ? error.message : String(error),
});
await registerGeneratedImageTemporaryUrls({
urls: [url],
metadata: params.metadata || null,
});
return {
originalUrl: url,
thumbnailUrl: url,
};
}
})
);
}
function getImageExtension(contentType: string | null, sourceUrl: string) {
const normalized = String(contentType || "").split(";")[0].trim().toLowerCase();
const byType: Record<string, string> = {
@ -187,10 +238,28 @@ export async function downloadTemporaryImage(params: {
export async function archiveTemporaryImage(sourceUrl: string) {
const downloaded = await downloadTemporaryImage({ sourceUrl });
const filename = `ai-generated-archive-${Date.now()}.${downloaded.extension}`;
const objectSuffix = `${Date.now()}-${randomUUID()}`;
const filename = `ai-generated-archive-${objectSuffix}.${downloaded.extension}`;
const uploaded = await uploadFile(filename, toArrayBuffer(downloaded.buffer));
const thumbnailBuffer = await sharp(downloaded.buffer, { failOn: "none" })
.resize(640, 640, { fit: "inside", withoutEnlargement: true })
.webp({ quality: 82 })
.toBuffer()
.catch(() => null);
const thumbnailUploaded = thumbnailBuffer
? await uploadFile(
`ai-generated-archive-thumb-${objectSuffix}.webp`,
toArrayBuffer(thumbnailBuffer)
).catch((error) => {
logger.warn("generated image thumbnail upload failed", {
error: error instanceof Error ? error.message : String(error),
});
return null;
})
: null;
return {
url: uploaded.url,
thumbnailUrl: thumbnailUploaded?.url || uploaded.url,
pathname: uploaded.pathname,
contentType: uploaded.contentType,
bytes: downloaded.buffer.length,