详情替换原来把整批生图挂在浏览器保持的对话流上,切换标签页、断网或连接中断后 结果就再也回不来,前端还会继续播放按时间推算的 96% 假进度。 现在改为服务器端后台批次:提交时先落任务,再按参考图逐张生成并把每张结果写进 项目,页面刷新、切页或服务重启后都能按任务状态恢复,不再依赖前端连接。 - 新增 /api/agents/detail-replacement/batches(提交/查询、幂等去重、重试上限) - 前端只在服务端确认批次后显示处理中,卡片显示排队/生成/完成/失败真实状态 - 去掉假进度动画;没有产品输入图时提交前直接拦截 - 服务重启后自动恢复未完成批次,长批次不再被通用 30 分钟清理误判 - 单张失败只落到该卡片并继续后续图片,已完成的图片不重复生成和扣费 - 详情替换默认模型由持续 404 的 gpt-image-2.5-flare 改回 gpt-image-2.5
72 lines
2.9 KiB
TypeScript
72 lines
2.9 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import {
|
|
completedDetailReplacementCardIds,
|
|
detailReplacementCardMessageId,
|
|
detailReplacementSubmissionKey,
|
|
isDetailReplacementBatchRequest,
|
|
type DetailReplacementBatchResult,
|
|
} from "@/lib/agents/detail-replacement-batch";
|
|
|
|
test("recognizes only durable detail replacement batches", () => {
|
|
expect(isDetailReplacementBatchRequest({ taskKind: "agent-detail-replacement-batch" })).toBe(true);
|
|
expect(isDetailReplacementBatchRequest({ taskKind: "style-pro" })).toBe(false);
|
|
expect(isDetailReplacementBatchRequest(null)).toBe(false);
|
|
});
|
|
|
|
test("card message identity stays stable across retries and page reloads", () => {
|
|
const id = detailReplacementCardMessageId("batch-a", "card-01");
|
|
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
|
expect(detailReplacementCardMessageId("batch-a", "card-01")).toBe(id);
|
|
expect(detailReplacementCardMessageId("batch-a", "card-02")).not.toBe(id);
|
|
});
|
|
|
|
test("restart skips successful cards but retries failed ones", () => {
|
|
const result: DetailReplacementBatchResult = {
|
|
taskKind: "agent-detail-replacement-batch",
|
|
currentCardId: null,
|
|
cards: [
|
|
{ cardId: "one", status: "completed", imageUrl: "https://example.com/one.png", thumbnailUrl: "https://example.com/one.png", text: "" },
|
|
{ cardId: "two", status: "fallback", imageUrl: "https://example.com/two.png", thumbnailUrl: "https://example.com/two.png", text: "生成失败" },
|
|
{ cardId: "three", status: "passthrough", imageUrl: "https://example.com/three.png", thumbnailUrl: "https://example.com/three.png", text: "" },
|
|
],
|
|
};
|
|
expect([...completedDetailReplacementCardIds(result)]).toEqual(["one", "three"]);
|
|
});
|
|
|
|
const submission = {
|
|
projectId: "11111111-1111-1111-1111-111111111111",
|
|
model: "gpt-image-2.5",
|
|
prompt: "保持版式,替换产品主体",
|
|
userPrompt: "详情替换",
|
|
imageSize: "2K",
|
|
replacementStrategy: "scene_replace",
|
|
productImageUrls: ["https://example.com/p2.png", "https://example.com/p1.png"],
|
|
modelImageUrl: null,
|
|
references: [
|
|
{ id: "card-01", label: "首屏", imageUrl: "https://example.com/a.png" },
|
|
{ id: "card-02", label: "第二屏", imageUrl: "https://example.com/b.png" },
|
|
],
|
|
};
|
|
|
|
test("an identical double click resolves to the same submission key", () => {
|
|
expect(detailReplacementSubmissionKey(submission)).toBe(
|
|
detailReplacementSubmissionKey({
|
|
...submission,
|
|
productImageUrls: [...submission.productImageUrls].reverse(),
|
|
references: [...submission.references].reverse(),
|
|
}),
|
|
);
|
|
});
|
|
|
|
test("re-running a single card while a batch runs is not treated as a duplicate", () => {
|
|
const full = detailReplacementSubmissionKey(submission);
|
|
expect(detailReplacementSubmissionKey({
|
|
...submission,
|
|
references: [submission.references[0]!],
|
|
})).not.toBe(full);
|
|
expect(detailReplacementSubmissionKey({
|
|
...submission,
|
|
prompt: "换一个背景",
|
|
})).not.toBe(full);
|
|
});
|