ai-chatbot/tests/routes/replacement-product-identity.test.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

125 lines
4.9 KiB
TypeScript

import { expect, test } from "@playwright/test";
import {
DEFAULT_DETAIL_REPLACEMENT_IMAGE_MODEL_ID,
normalizeDetailReplacementImageModelConfig,
} from "@/lib/ai/detail-replacement-image-model-config";
import {
buildReplacementProductStructureLockLines,
normalizeReplacementProductImageIndexes,
parseReplacementProductIdentityProfile,
} from "@/lib/ai/replacement-product-identity";
import {
selectDetailReplacementGenerationInputs,
shouldIncludeDetailReplacementModelImage,
} from "@/lib/ai/detail-replacement-inputs";
test("parses a reusable multi-view product structure profile", () => {
const profile = parseReplacementProductIdentityProfile(
`
\`\`\`json
{
"identitySummary": "胡桃木桌面与黑色钢架电脑桌",
"structureSummary": "左右均为闭合口字型侧框,桌面下方由三组矩形横梁连接",
"structuralRisks": "禁止改成 K 型或 X 型腿,禁止横梁断裂或错接",
"viewSummaries": [
{"imageIndex": 4, "cameraSummary": "右前方三分之四视角", "visibleStructure": "右侧框和全部纵向横梁清楚"},
{"imageIndex": 1, "cameraSummary": "左前方三分之四视角", "visibleStructure": "左侧框和桌面比例清楚"},
{"imageIndex": 9, "cameraSummary": "无效编号", "visibleStructure": "应被过滤"}
]
}
\`\`\`
`,
4,
);
expect(profile).toEqual({
identitySummary: "胡桃木桌面与黑色钢架电脑桌",
structureSummary: "左右均为闭合口字型侧框,桌面下方由三组矩形横梁连接",
structuralRisks: "禁止改成 K 型或 X 型腿,禁止横梁断裂或错接",
viewSummaries: [
{
imageIndex: 4,
cameraSummary: "右前方三分之四视角",
visibleStructure: "右侧框和全部纵向横梁清楚",
},
{
imageIndex: 1,
cameraSummary: "左前方三分之四视角",
visibleStructure: "左侧框和桌面比例清楚",
},
],
});
});
test("keeps only two valid, unique, one-based product image indexes", () => {
expect(normalizeReplacementProductImageIndexes([4, "1", 4, 0, 7], 4)).toEqual([
4,
1,
]);
});
test("makes product topology authoritative over the old detail product", () => {
const prompt = buildReplacementProductStructureLockLines({
productImageCount: 2,
structureSummary: "左右均为口字型矩形侧框",
structuralRisks: "禁止变成 K 型或 X 型腿",
}).join("\n");
expect(prompt).toContain("产品结构身份锁");
expect(prompt).toContain("最后一张详情底板只决定");
expect(prompt).toContain("绝不决定新产品的腿型");
expect(prompt).toContain("严禁把口字型框架改成 K/X/A/V 型");
expect(prompt).toContain("旧商品结构必须完整移除");
});
test("binds non-Pro detail replacement to GPT Image 2.5 in database config", () => {
expect(DEFAULT_DETAIL_REPLACEMENT_IMAGE_MODEL_ID).toBe("gpt-image-2.5");
expect(normalizeDetailReplacementImageModelConfig(null)).toEqual({
modelId: "gpt-image-2.5",
});
expect(
normalizeDetailReplacementImageModelConfig({ modelId: "custom-image-model" }),
).toEqual({ modelId: "custom-image-model" });
});
test("keeps model-identity images on people-oriented detail pages", () => {
expect(shouldIncludeDetailReplacementModelImage({ peoplePolicy: "preserve", useModelImage: false })).toBe(true);
expect(shouldIncludeDetailReplacementModelImage({ sceneSummary: "固定模特在客厅展示产品" })).toBe(true);
expect(shouldIncludeDetailReplacementModelImage({ pageKind: "product_detail", peoplePolicy: "none" })).toBe(false);
});
test("keeps the product, required model identity, and final layout within three inputs", () => {
const selected = selectDetailReplacementGenerationInputs({
productImageUrls: ["https://assets.test/product-1.png", "https://assets.test/product-2.png"],
modelImageUrl: "https://assets.test/model.png",
includeModelImage: true,
rerunReferenceImageUrls: ["https://assets.test/rerun.png"],
referenceImageUrl: "https://assets.test/layout.png",
});
expect(selected.imageUrls).toEqual([
"https://assets.test/product-1.png",
"https://assets.test/model.png",
"https://assets.test/layout.png",
]);
expect(selected.inputImageRoleByUrl).toEqual({
"https://assets.test/product-1.png": "product",
"https://assets.test/model.png": "model",
"https://assets.test/layout.png": "layout",
});
});
test("prioritizes an explicit rerun reference while always keeping the final layout", () => {
const selected = selectDetailReplacementGenerationInputs({
productImageUrls: ["https://assets.test/product-1.png", "https://assets.test/product-2.png"],
rerunReferenceImageUrls: ["https://assets.test/rerun.png"],
referenceImageUrl: "https://assets.test/layout.png",
});
expect(selected.imageUrls).toEqual([
"https://assets.test/product-1.png",
"https://assets.test/rerun.png",
"https://assets.test/layout.png",
]);
});