chore(scripts): 新增共创网络灌数据脚本 + 截图工具支持只截现有作品
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled

seed-game-distribution-co-creation.mjs: 走真实 API 建树/复用(标题判重、不绑账号)、封面按树分色系、主题与收藏、断言血缘
This commit is contained in:
2026-10-07 00:30:33 +08:00
parent bde909315e
commit 626dc6d185
2 changed files with 1294 additions and 0 deletions
+203
View File
@@ -503,7 +503,210 @@ async function loadPlaywright() {
return requireFromDir('playwright');
}
/// 「只截图、不造树」模式:给已有作品(例如 scripts/seed-game-distribution-co-creation.mjs 灌的共创网络)
/// 出验收截图。设 `E2E_CAPTURE_LINEAGE_URL` 即进入该模式——不注册账号、不建作品、不下架,纯 GET + 截图。
async function captureExistingUrls() {
const lineageUrl = (process.env.E2E_CAPTURE_LINEAGE_URL ?? '').trim();
const themeUrl = (process.env.E2E_CAPTURE_THEME_URL ?? '').trim();
const selectTitle = (process.env.E2E_CAPTURE_SELECT_TITLE ?? '').trim();
// 视口可配:全树总览常常比 960 高,给个高的窗口才能真的「一眼看全」。
const sizeMatch = /^(\d{3,4})x(\d{3,4})$/u.exec(
(process.env.E2E_CAPTURE_WINDOW ?? '').trim(),
);
const desktopViewport = sizeMatch
? { width: Number(sizeMatch[1]), height: Number(sizeMatch[2]) }
: { width: 1440, height: 960 };
const { chromium } = await loadPlaywright();
const executablePath = (process.env.E2E_CHROMIUM_EXECUTABLE ?? '').trim();
const browser = await chromium.launch({
headless: true,
...(executablePath ? { executablePath } : {}),
});
const shotDir = await mkdtemp(
path.join(tmpdir(), 'genarrative-co-creation-visual-'),
);
const shots = [];
const take = async (target, name) => {
const file = path.join(shotDir, name);
await target.screenshot({ path: file, fullPage: false });
shots.push(file);
console.log(`[lineage-visual] 截图 ${file}`);
return file;
};
try {
// 封面是远端资源:等所有 <img> 落地再截,否则会出现灰块假象。
const waitImages = async (target) => {
await target
.waitForFunction(
() =>
[...document.images].every(
(img) => img.complete && img.naturalWidth > 0,
),
undefined,
{ timeout: 60_000 },
)
.catch(() => {});
await target.waitForTimeout(600);
};
// ---------- 桌面:全树总览 ----------
const desktop = await browser.newContext({
viewport: desktopViewport,
});
const page = await desktop.newPage();
await page.goto(lineageUrl, { waitUntil: 'commit', timeout: 120_000 });
await page.waitForFunction(
() => document.querySelectorAll('.game-lineage-node').length >= 3,
undefined,
{ timeout: 120_000 },
);
await page.waitForTimeout(1400);
await waitImages(page);
const nodeCount = await page.locator('.game-lineage-node').count();
check(
'截图模式:/games/lineage 已渲染多代节点',
nodeCount >= 3,
`nodes=${nodeCount} url=${lineageUrl}`,
);
// 首屏视野契约(与 lineage 工具同口径):要么真的全览,要么按可读缩放下限 + 提示。
const fit = await page.evaluate(() => {
const canvas = document
.querySelector('.game-lineage-canvas')
?.getBoundingClientRect();
if (!canvas) return { allInside: false, hint: false };
const allInside = [
...document.querySelectorAll('.game-lineage-node'),
].every((node) => {
const box = node.getBoundingClientRect();
return (
box.left >= canvas.left - 2 &&
box.top >= canvas.top - 2 &&
box.right <= canvas.right + 2 &&
box.bottom <= canvas.bottom + 2
);
});
return {
allInside,
hint: [...document.querySelectorAll('*')].some((element) =>
(element.textContent ?? '').includes('已按可读缩放显示'),
),
};
});
check(
'截图模式:首屏要么全树在画布内,要么给出「已按可读缩放显示」提示',
fit.allInside || fit.hint,
`allInside=${fit.allInside} hint=${fit.hint} 视口=${desktopViewport.width}x${desktopViewport.height}`,
);
await take(page, '01-lineage-overview.png');
// ---------- 桌面:选中态(信息面板 + 面包屑) ----------
// 注意:卡片主体是「打开详情」按钮(stopPropagation),点它会跳走;要「选中/锁定」得点卡片**内边距**(6,6)。
const card = selectTitle
? page
.locator('.game-lineage-node')
.filter({ hasText: selectTitle })
.first()
: page.locator('.game-lineage-node').nth(1);
let selected = false;
if ((await card.count()) > 0) {
await card.click({ position: { x: 6, y: 6 } });
selected = true;
} else {
await page
.locator('.game-lineage-node')
.nth(1)
.click({ position: { x: 6, y: 6 } });
selected = true;
}
if (selected) {
await page
.locator('.game-lineage-panel')
.first()
.waitFor({ state: 'visible', timeout: 30_000 });
await page.waitForTimeout(700);
const crumbs = await page
.locator('.game-lineage-breadcrumb__crumb')
.count();
check(
'截图模式:选中节点后出现信息面板与面包屑',
crumbs >= 1,
`面包屑=${crumbs}`,
);
await waitImages(page);
await take(page, '02-lineage-selected.png');
}
// ---------- 窄屏 ----------
const mobile = await browser.newContext({
viewport: { width: 390, height: 844 },
});
const mobilePage = await mobile.newPage();
await mobilePage.goto(lineageUrl, {
waitUntil: 'commit',
timeout: 120_000,
});
await mobilePage.waitForFunction(
() => document.querySelectorAll('.game-lineage-node').length >= 3,
undefined,
{ timeout: 120_000 },
);
await mobilePage.waitForTimeout(1400);
// 窄屏首次进入会有「移动端仅支持作品展示」的欢迎弹窗(未 dismiss 时弹):先点掉再截图,
// 否则截出来只剩一个遮罩弹窗,看不到窄屏族谱本身。
const welcomeOk = mobilePage.getByRole('button', {
name: '好',
exact: true,
});
if ((await welcomeOk.count()) > 0) {
await welcomeOk.first().click();
await mobilePage.waitForTimeout(800);
}
await waitImages(mobilePage);
const narrow = await mobilePage.evaluate(() => ({
canvas: Boolean(document.querySelector('.game-lineage-canvas')),
nodes: document.querySelectorAll('.game-lineage-node').length,
overflow: document.documentElement.scrollWidth - window.innerWidth > 1,
}));
check(
'截图模式:窄屏下族谱画布可见、节点已渲染且页面不横向溢出',
narrow.canvas && narrow.nodes >= 3 && !narrow.overflow,
`canvas=${narrow.canvas} nodes=${narrow.nodes} overflow=${narrow.overflow}`,
);
await take(mobilePage, '03-lineage-narrow.png');
await mobile.close();
// ---------- 主题页 ----------
if (themeUrl) {
const themePage = await desktop.newPage();
await themePage.goto(themeUrl, { waitUntil: 'commit', timeout: 120_000 });
await themePage
.locator('.game-theme-root')
.first()
.waitFor({ state: 'visible', timeout: 60_000 });
await themePage.waitForTimeout(1000);
await waitImages(themePage);
const roots = await themePage.locator('.game-theme-root').count();
check('截图模式:主题页已渲染作品树', roots >= 1, `roots=${roots}`);
await take(themePage, '04-theme-page.png');
void themePage;
}
await desktop.close();
console.log(`[lineage-visual] 截图目录=${shotDir}`);
console.log(`[lineage-visual] 结果:check=${checks} fail=${failures}`);
} finally {
await browser.close();
}
return shots;
}
async function main() {
if ((process.env.E2E_CAPTURE_LINEAGE_URL ?? '').trim()) {
await captureExistingUrls();
const failed = failures > 0;
console.log(
`[lineage-visual] 只用现有作品截图:check=${checks} fail=${failures}`,
);
process.exit(failed ? 1 : 0);
}
console.log(`[lineage-visual] api-server=${API} web=${WEB}`);
const stamp = Date.now();
const suffix = String(stamp).slice(-6);
File diff suppressed because it is too large Load Diff