Files
seaislee1209andClaude Opus 4.8 c8f3f91d38 feat(images): 期2 图片趴三类 — 模特上身图/平台套图/自由创作 归类+成组+接模特库
后端:
- 独立生图三类归类:model+product→model_tryon(模特上身图,引用模特库,不送审) / cover→platform_kit(平台套图) / image→free_create(自由创作);生成演员(model 无 product)仍→person(视频角色,留期3)
- 成组:每次提交一个 batch_id 串起整批,落 asset.metadata;另记 mode + model_entity_id(上身图溯源模特库)
- generate-image 端点透传 model_entity_id;资产库 _tab_q+summary 加 tryon/kits/creations 三类桶
- 单测 StandaloneCategoryTests 直跑 worker 验四态归类全绿(绕开异步 .delay)

前端:
- 资产库 library.tsx 加三类 tab(模特上身图/平台套图/自由创作)+ 计数 + 筛选维度
- ai-tools 模特选择器数据源 person→模特库(listModels 映射,选中传形象图当参考 = 引用模特库),ActorLibrary 同源
- api.ts submitGenerateImage 加 model_entity_id

验收:tsc+build 全绿;无头 0 console error(资产库三类 tab 各归类正确、模特选择器 5 张取自模特库)
基线既有 3 失败(StandaloneImageReferenceTests 异步漂移)零新增

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 16:36:29 +08:00

53 lines
2.4 KiB
JavaScript

// 期2 图片趴三类走查:资产库三类 tab(归类) + 图片生成页模特选择器接模特库 + 抓 console/pageerror。
import { chromium } from "playwright";
import fs from "node:fs";
import path from "node:path";
const BASE = process.env.BASE || "http://localhost:5188";
const OUT = path.resolve("../../../_qa_shots/models-p2");
fs.mkdirSync(OUT, { recursive: true });
const browser = await chromium.launch({ headless: true });
const r = { consoleErrors: [], library: {}, picker: {} };
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const p = await ctx.newPage();
p.on("console", (m) => { if (m.type() === "error") r.consoleErrors.push(m.text()); });
p.on("pageerror", (e) => r.consoleErrors.push("PAGEERR:" + e.message));
// 登录
await p.goto(BASE + "/login", { waitUntil: "load" });
await p.waitForTimeout(400);
await p.fill("#auth-username", "airshelf");
await p.fill("#auth-pwd", "Restraint2026");
await p.click("button.btn-cta");
await p.waitForFunction(() => !location.pathname.startsWith("/login"), { timeout: 12000 });
// ── 资产库:三类 tab 存在 + 计数 + 逐 tab 截图 ──
await p.goto(BASE + "/library", { waitUntil: "load" });
await p.waitForTimeout(2600);
for (const key of ["tryon", "kits", "creations"]) {
r.library[key + "_tabExists"] = await p.locator(`.tab[data-tab="${key}"]`).count();
}
async function openTab(key) {
await p.locator(`.tab[data-tab="${key}"]`).click();
await p.waitForTimeout(2200);
const cards = await p.locator(".asset-card, .lib-card, .ph-frame, .a-card").count();
await p.screenshot({ path: path.join(OUT, `library-${key}.png`), fullPage: true });
return cards;
}
r.library.tryonCards = await openTab("tryon");
r.library.kitsCards = await openTab("kits");
r.library.creationsCards = await openTab("creations");
// ── 图片生成页:模特上身图 → 模特选择器接模特库 ──
await p.goto(BASE + "/model-photo", { waitUntil: "load" });
await p.waitForTimeout(2600);
r.picker.url = p.url();
r.picker.modelCards = await p.locator(".model-grid .model-card").count();
r.picker.firstModelName = await p.locator(".model-grid .model-card .m-name").first().innerText().catch(() => "");
await p.screenshot({ path: path.join(OUT, "model-photo-picker.png"), fullPage: true });
await browser.close();
console.log(JSON.stringify(r, null, 2));
fs.writeFileSync(path.join(OUT, "summary.json"), JSON.stringify(r, null, 2));