详情替换原来把整批生图挂在浏览器保持的对话流上,切换标签页、断网或连接中断后 结果就再也回不来,前端还会继续播放按时间推算的 96% 假进度。 现在改为服务器端后台批次:提交时先落任务,再按参考图逐张生成并把每张结果写进 项目,页面刷新、切页或服务重启后都能按任务状态恢复,不再依赖前端连接。 - 新增 /api/agents/detail-replacement/batches(提交/查询、幂等去重、重试上限) - 前端只在服务端确认批次后显示处理中,卡片显示排队/生成/完成/失败真实状态 - 去掉假进度动画;没有产品输入图时提交前直接拦截 - 服务重启后自动恢复未完成批次,长批次不再被通用 30 分钟清理误判 - 单张失败只落到该卡片并继续后续图片,已完成的图片不重复生成和扣费 - 详情替换默认模型由持续 404 的 gpt-image-2.5-flare 改回 gpt-image-2.5
146 lines
4.6 KiB
TypeScript
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;
|
|
});
|
|
}
|