fix: persist generated images before gallery save
This commit is contained in:
parent
afac47add2
commit
42e1175830
@ -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);
|
||||
|
||||
@ -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) => ``)
|
||||
return persisted
|
||||
.map(
|
||||
({ originalUrl, 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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user