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