import { spawn } from 'node:child_process'; import { mkdir, writeFile } from 'node:fs/promises'; import { existsSync, readFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { chromium } from 'playwright'; const __dirname = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(__dirname, '..'); const apiBase = process.env.MM_API_BASE || resolveApiBase(); const uiBase = process.env.MM_UI_BASE || 'http://127.0.0.1:15173'; const username = process.env.MM_USERNAME || `codex_shots_${Date.now().toString(36)}`; const password = process.env.MM_PASSWORD || 'Codex123456'; const skipStart = process.env.MM_SKIP_START === '1'; const pages = [ { name: 'dashboard', path: '/#/' }, { name: 'accounts', path: '/#/accounts' }, { name: 'works', path: '/#/works' }, { name: 'publish', path: '/#/publish' }, { name: 'analytics-overview', path: '/#/analytics/overview' }, { name: 'analytics-platform', path: '/#/analytics/platform' }, { name: 'analytics-account', path: '/#/analytics/account' }, { name: 'analytics-work', path: '/#/analytics/work' }, ]; function resolveApiBase() { try { const envText = readFileSync(resolve(repoRoot, 'server', '.env'), 'utf8'); const portMatch = envText.match(/^PORT=(.+)$/m); const hostMatch = envText.match(/^HOST=(.+)$/m); const port = portMatch?.[1]?.trim() || '3000'; const rawHost = hostMatch?.[1]?.trim() || '127.0.0.1'; const host = rawHost === '0.0.0.0' ? '127.0.0.1' : rawHost; return `http://${host}:${port}`; } catch { return 'http://127.0.0.1:3000'; } } function getTimestamp() { return new Date().toISOString().replace(/[:.]/g, '-'); } function createOutputDir() { const explicitDir = process.argv[2]; return explicitDir ? resolve(repoRoot, explicitDir) : resolve(repoRoot, 'minimax-output', 'regression', getTimestamp()); } function startProcess(label, args) { const child = spawnPnpm(args, { cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, }); let logs = ''; const appendLog = (chunk) => { logs += chunk.toString(); if (logs.length > 6000) { logs = logs.slice(-6000); } }; child.stdout.on('data', appendLog); child.stderr.on('data', appendLog); child.on('exit', (code) => { if (code && code !== 0) { console.error(`[${label}] exited with code ${code}`); console.error(logs); } }); return { label, child, getLogs: () => logs }; } async function stopProcess(proc) { if (!proc || proc.child.killed) return; proc.child.kill('SIGTERM'); await new Promise((resolveStop) => setTimeout(resolveStop, 1500)); if (!proc.child.killed && process.platform === 'win32') { spawn('taskkill', ['/PID', String(proc.child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true, }); } } async function waitForUrl(url, timeoutMs = 60000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { try { const response = await fetch(url); if (response.ok) return; } catch { // retry } await new Promise((resolveWait) => setTimeout(resolveWait, 1000)); } throw new Error(`Timed out waiting for ${url}`); } async function ensurePlaywrightBrowser() { if (existsSync(chromium.executablePath())) return; console.log('[shots] Installing Playwright Chromium'); await new Promise((resolveInstall, rejectInstall) => { const installer = spawnPnpm(['exec', 'playwright', 'install', 'chromium'], { cwd: repoRoot, stdio: 'inherit', windowsHide: true, }); installer.on('exit', (code) => { if (code === 0) resolveInstall(); else rejectInstall(new Error(`playwright install exited with code ${code}`)); }); }); } function spawnPnpm(args, options) { if (process.platform === 'win32') { return spawn('cmd.exe', ['/d', '/s', '/c', 'pnpm', ...args], options); } return spawn('pnpm', args, options); } async function login(page) { await page.addInitScript((serverUrl) => { localStorage.setItem( 'server_configs', JSON.stringify([{ id: 'server_default', name: '本地 Node 服务', url: serverUrl, isDefault: true }]), ); localStorage.setItem('current_server', 'server_default'); }, apiBase); await page.goto(`${uiBase}/#/login`, { waitUntil: 'domcontentloaded' }); await page.locator('.splash-screen').waitFor({ state: 'hidden', timeout: 15000 }).catch(() => {}); await page.locator('.login-form input').nth(0).fill(username); await page.locator('.login-form input').nth(1).fill(password); await Promise.all([ page.waitForURL(/#\/$/, { timeout: 30000 }), page.locator('.login-btn').click(), ]); await page.locator('.main-layout, .dashboard-page').first().waitFor({ timeout: 30000 }); } async function capturePages(outputDir) { const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1600, height: 1200 }, deviceScaleFactor: 1, }); const page = await context.newPage(); await login(page); const manifest = []; for (const item of pages) { const targetPath = join(outputDir, `${item.name}.png`); console.log(`[shots] Capturing ${item.name}`); await page.goto(`${uiBase}${item.path}`, { waitUntil: 'domcontentloaded' }); await page.locator('.splash-screen').waitFor({ state: 'hidden', timeout: 15000 }).catch(() => {}); await page.waitForTimeout(1800); await page.screenshot({ path: targetPath, fullPage: true }); manifest.push({ page: item.name, path: targetPath }); } await writeFile(join(outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8'); await browser.close(); } async function ensureRegressionUser() { if (process.env.MM_USERNAME && process.env.MM_PASSWORD) return; const payload = { username, password, email: `${username}@example.com`, }; const response = await fetch(`${apiBase}/api/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (response.ok) return; const text = await response.text(); if (!text.includes('已存在') && !text.includes('exists')) { throw new Error(`Failed to create regression user: ${text}`); } } async function main() { const outputDir = createOutputDir(); await mkdir(outputDir, { recursive: true }); await ensurePlaywrightBrowser(); let serverProc; let clientProc; try { if (!skipStart) { console.log('[shots] Starting local server and client'); serverProc = startProcess('server', ['--filter', '@media-manager/server', 'dev']); clientProc = startProcess('client', ['--filter', '@media-manager/client', 'exec', 'vite', '--host', '127.0.0.1', '--port', '15173']); } await waitForUrl(`${apiBase}/api/health`, 90000); await waitForUrl(uiBase, 90000); await ensureRegressionUser(); await capturePages(outputDir); console.log(`[shots] Done: ${outputDir}`); } finally { await stopProcess(clientProc); await stopProcess(serverProc); } } main().catch((error) => { console.error('[shots] Failed'); console.error(error); process.exitCode = 1; });