feat: 添加图床归档任务支持,优化归档逻辑和错误处理
This commit is contained in:
@@ -7,7 +7,11 @@ import type {
|
||||
IImageHistoryListData
|
||||
} from "#shared/types/openai";
|
||||
import { consola } from "consola";
|
||||
import { ImageGenerationStatus, Prisma } from "~~/app/generated/prisma/client";
|
||||
import {
|
||||
ImageArchiveStatus,
|
||||
ImageGenerationStatus,
|
||||
Prisma
|
||||
} from "~~/app/generated/prisma/client";
|
||||
import { prisma } from "~~/server/utils/prisma";
|
||||
|
||||
const GLOBAL_STATS_ID = "global";
|
||||
@@ -31,6 +35,19 @@ interface IFinishImageGenerationArchiveSuccessInput {
|
||||
hostedResponse: unknown;
|
||||
}
|
||||
|
||||
export interface IImageArchiveTask {
|
||||
/** 生图记录 ID */
|
||||
id: bigint;
|
||||
/** NewAPI 用户 ID */
|
||||
userId: number;
|
||||
/** 上游生成图片 URL */
|
||||
imageUrl: string;
|
||||
/** 记录创建时间,用于构造图床文件名 */
|
||||
startedAt: Date;
|
||||
/** 当前这次归档是第几次尝试 */
|
||||
archiveAttempts: number;
|
||||
}
|
||||
|
||||
/** 登录或恢复登录时保存 NewAPI 用户快照 */
|
||||
export const upsertUserSnapshot = (user: IUserBasicData) => {
|
||||
return prisma.user.upsert({
|
||||
@@ -135,7 +152,13 @@ export const finishImageGenerationSuccess = async (
|
||||
revisedPrompt: input.revisedPrompt || null,
|
||||
upstreamResponse: input.upstreamResponse as Prisma.InputJsonValue,
|
||||
hostedResponse: Prisma.DbNull,
|
||||
errorMessage: null
|
||||
errorMessage: null,
|
||||
archiveStatus: ImageArchiveStatus.PENDING,
|
||||
archiveAttempts: 0,
|
||||
archiveNextRunAt: new Date(),
|
||||
archiveLockedAt: null,
|
||||
archiveLockedBy: null,
|
||||
archiveLastError: null
|
||||
}
|
||||
});
|
||||
|
||||
@@ -182,6 +205,86 @@ export const finishImageGenerationFailed = async (
|
||||
});
|
||||
};
|
||||
|
||||
/** 领取可执行的图床归档任务;通过 updateMany 条件锁避免多实例重复处理 */
|
||||
export const claimRunnableImageArchiveTasks = async ({
|
||||
limit,
|
||||
workerId,
|
||||
lockTtlMs
|
||||
}: {
|
||||
limit: number;
|
||||
workerId: string;
|
||||
lockTtlMs: number;
|
||||
}): Promise<IImageArchiveTask[]> => {
|
||||
if (limit <= 0) return [];
|
||||
|
||||
const now = new Date();
|
||||
const staleBefore = new Date(now.getTime() - lockTtlMs);
|
||||
const runnableWhere = buildRunnableArchiveWhere(now, staleBefore);
|
||||
const candidates = await prisma.imageGeneration.findMany({
|
||||
where: runnableWhere,
|
||||
select: {
|
||||
id: true
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
archiveNextRunAt: "asc"
|
||||
},
|
||||
{
|
||||
createdAt: "asc"
|
||||
}
|
||||
],
|
||||
take: limit * 3
|
||||
});
|
||||
const tasks: IImageArchiveTask[] = [];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (tasks.length >= limit) break;
|
||||
|
||||
const claimed = await prisma.imageGeneration.updateMany({
|
||||
where: {
|
||||
id: candidate.id,
|
||||
...runnableWhere
|
||||
},
|
||||
data: {
|
||||
archiveStatus: ImageArchiveStatus.RUNNING,
|
||||
archiveAttempts: {
|
||||
increment: 1
|
||||
},
|
||||
archiveLockedAt: now,
|
||||
archiveLockedBy: workerId,
|
||||
archiveLastError: null
|
||||
}
|
||||
});
|
||||
|
||||
if (claimed.count !== 1) continue;
|
||||
|
||||
const task = await prisma.imageGeneration.findUnique({
|
||||
where: {
|
||||
id: candidate.id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
imageUrl: true,
|
||||
startedAt: true,
|
||||
archiveAttempts: true
|
||||
}
|
||||
});
|
||||
|
||||
if (task?.imageUrl) {
|
||||
tasks.push({
|
||||
id: task.id,
|
||||
userId: task.userId,
|
||||
imageUrl: task.imageUrl,
|
||||
startedAt: task.startedAt,
|
||||
archiveAttempts: task.archiveAttempts
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tasks;
|
||||
};
|
||||
|
||||
/** 后台归档成功后回写图床地址、MIME 和图床响应 */
|
||||
export const finishImageGenerationArchiveSuccess = async (
|
||||
recordId: bigint,
|
||||
@@ -195,12 +298,42 @@ export const finishImageGenerationArchiveSuccess = async (
|
||||
hostedImageUrl: input.hostedImageUrl,
|
||||
imageMimeType: input.imageMimeType,
|
||||
hostedResponse: input.hostedResponse as Prisma.InputJsonValue,
|
||||
errorMessage: null,
|
||||
archiveStatus: ImageArchiveStatus.SUCCEEDED,
|
||||
archiveNextRunAt: null,
|
||||
archiveLockedAt: null,
|
||||
archiveLockedBy: null,
|
||||
archiveLastError: null
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 归档可重试失败时释放任务锁并安排下一次执行,前端仍显示归档中 */
|
||||
export const scheduleImageGenerationArchiveRetry = async ({
|
||||
recordId,
|
||||
nextRunAt,
|
||||
errorMessage
|
||||
}: {
|
||||
recordId: bigint;
|
||||
nextRunAt: Date;
|
||||
errorMessage: string;
|
||||
}) => {
|
||||
await prisma.imageGeneration.update({
|
||||
where: {
|
||||
id: recordId
|
||||
},
|
||||
data: {
|
||||
archiveStatus: ImageArchiveStatus.PENDING,
|
||||
archiveNextRunAt: nextRunAt,
|
||||
archiveLockedAt: null,
|
||||
archiveLockedBy: null,
|
||||
archiveLastError: errorMessage,
|
||||
errorMessage: null
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 后台归档失败时仅补写本地错误文案,不影响已成功的生图状态 */
|
||||
/** 后台归档最终失败时仅补写本地错误文案,不影响已成功的生图状态 */
|
||||
export const finishImageGenerationArchiveFailed = async (
|
||||
recordId: bigint,
|
||||
errorMessage: string = "图片归档失败"
|
||||
@@ -210,7 +343,12 @@ export const finishImageGenerationArchiveFailed = async (
|
||||
id: recordId
|
||||
},
|
||||
data: {
|
||||
errorMessage
|
||||
archiveStatus: ImageArchiveStatus.FAILED,
|
||||
archiveNextRunAt: null,
|
||||
archiveLockedAt: null,
|
||||
archiveLockedBy: null,
|
||||
archiveLastError: errorMessage,
|
||||
errorMessage: "图片归档失败"
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -350,6 +488,38 @@ const buildStatsCreateInput = (update: Prisma.GenerationStatsUpdateInput) => {
|
||||
};
|
||||
};
|
||||
|
||||
/** 只领取成功生成、未删除、已到执行时间或锁超时的归档任务 */
|
||||
const buildRunnableArchiveWhere = (now: Date, staleBefore: Date) => {
|
||||
return {
|
||||
status: ImageGenerationStatus.SUCCEEDED,
|
||||
deletedAt: null,
|
||||
imageUrl: {
|
||||
not: null
|
||||
},
|
||||
OR: [
|
||||
{
|
||||
archiveStatus: ImageArchiveStatus.PENDING,
|
||||
OR: [
|
||||
{
|
||||
archiveNextRunAt: null
|
||||
},
|
||||
{
|
||||
archiveNextRunAt: {
|
||||
lte: now
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
archiveStatus: ImageArchiveStatus.RUNNING,
|
||||
archiveLockedAt: {
|
||||
lt: staleBefore
|
||||
}
|
||||
}
|
||||
]
|
||||
} satisfies Prisma.ImageGenerationWhereInput;
|
||||
};
|
||||
|
||||
/** 从 Prisma increment 操作里取出增量;非 increment 操作在创建时按 0 处理 */
|
||||
const getIncrementValue = (value: unknown): number => {
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user