ai-chatbot/lib/agents/agent-project-result-persistence.ts
134 0907aaff67 fix(agent): make detail replacement results survive page and server restarts
详情替换原来把整批生图挂在浏览器保持的对话流上,切换标签页、断网或连接中断后
结果就再也回不来,前端还会继续播放按时间推算的 96% 假进度。

现在改为服务器端后台批次:提交时先落任务,再按参考图逐张生成并把每张结果写进
项目,页面刷新、切页或服务重启后都能按任务状态恢复,不再依赖前端连接。

- 新增 /api/agents/detail-replacement/batches(提交/查询、幂等去重、重试上限)
- 前端只在服务端确认批次后显示处理中,卡片显示排队/生成/完成/失败真实状态
- 去掉假进度动画;没有产品输入图时提交前直接拦截
- 服务重启后自动恢复未完成批次,长批次不再被通用 30 分钟清理误判
- 单张失败只落到该卡片并继续后续图片,已完成的图片不重复生成和扣费
- 详情替换默认模型由持续 404 的 gpt-image-2.5-flare 改回 gpt-image-2.5
2026-09-26 02:39:11 +08:00

146 lines
4.6 KiB
TypeScript

import "server-only";
import { and, eq } from "drizzle-orm";
import { extractAgentProjectStateFromCanvasData, isAgentProjectRow } from "@/lib/agents/agent-project-compat";
import {
appendAgentProjectResultForSave,
mergeAgentProjectStateForSave,
} from "@/lib/agents/agent-project-save-merge";
import { db } from "@/lib/db";
import { canvasProject } from "@/lib/db/schema";
import type {
AgentGenerationReplacementResultRecord,
AgentGenerationResultModeType,
} from "@/types/agent";
const UUID_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
function getCanvasDataRecord(value: unknown) {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
export async function persistAgentProjectGeneratedResult(params: {
projectId: string;
userId: string;
imageUrl: string;
modeType: AgentGenerationResultModeType;
}) {
const projectId = String(params.projectId || "").trim();
const userId = String(params.userId || "").trim();
const imageUrl = String(params.imageUrl || "").trim();
if (!UUID_PATTERN.test(projectId) || !UUID_PATTERN.test(userId) || !/^https?:\/\//i.test(imageUrl)) {
return false;
}
return db.transaction(async (tx) => {
const [project] = await tx
.select()
.from(canvasProject)
.where(and(eq(canvasProject.id, projectId), eq(canvasProject.userId, userId)))
.limit(1)
.for("update");
if (!project || !isAgentProjectRow(project)) {
return false;
}
const nextProjectState = appendAgentProjectResultForSave({
existingState: extractAgentProjectStateFromCanvasData(project.canvasData),
modeType: params.modeType,
imageUrl,
});
const canvasData = getCanvasDataRecord(project.canvasData);
const [updatedProject] = await tx
.update(canvasProject)
.set({
canvasData: {
...canvasData,
nodes: Array.isArray(canvasData.nodes) ? canvasData.nodes : [],
agentProjectState: nextProjectState,
} as unknown as typeof canvasProject.$inferInsert.canvasData,
thumbnail: project.thumbnail || imageUrl,
workspaceType: "agent",
updatedAt: new Date(),
})
.where(and(eq(canvasProject.id, projectId), eq(canvasProject.userId, userId)))
.returning({ id: canvasProject.id });
return Boolean(updatedProject);
});
}
export async function persistAgentDetailReplacementCard(params: {
projectId: string;
userId: string;
cardId: string;
imageUrl: string;
referenceImageUrl: string;
label: string;
status: AgentGenerationReplacementResultRecord["status"];
text: string;
}) {
if (
!UUID_PATTERN.test(params.projectId) ||
!UUID_PATTERN.test(params.userId) ||
!params.cardId ||
!/^https?:\/\//i.test(params.imageUrl)
) {
return false;
}
return db.transaction(async (tx) => {
const [project] = await tx
.select()
.from(canvasProject)
.where(and(eq(canvasProject.id, params.projectId), eq(canvasProject.userId, params.userId)))
.limit(1)
.for("update");
if (!project || !isAgentProjectRow(project)) return false;
const existingState = extractAgentProjectStateFromCanvasData(project.canvasData);
const record: AgentGenerationReplacementResultRecord = {
url: params.imageUrl,
text: params.text,
status: params.status,
updatedAt: Date.now(),
compareSourceUrl: params.referenceImageUrl,
referenceImageUrl: params.referenceImageUrl,
label: params.label,
};
const nextState = mergeAgentProjectStateForSave({
existingState,
incomingState: {
...existingState,
resultCollections: {
...existingState.resultCollections,
detail_replacement: [
...(existingState.resultCollections?.detail_replacement || []),
params.imageUrl,
],
},
replacementResultState: {
...existingState.replacementResultState,
detail_replacement: {
...existingState.replacementResultState?.detail_replacement,
[params.cardId]: record,
},
},
},
});
const canvasData = getCanvasDataRecord(project.canvasData);
await tx.update(canvasProject).set({
canvasData: {
...canvasData,
nodes: Array.isArray(canvasData.nodes) ? canvasData.nodes : [],
agentProjectState: nextState,
} as typeof canvasProject.$inferInsert.canvasData,
thumbnail: project.thumbnail || params.imageUrl,
updatedAt: new Date(),
}).where(eq(canvasProject.id, params.projectId));
return true;
});
}