/** * 端到端演示录制:登录 → 6 步评估向导 → 评估详情页。 * * 产出: * - docs/demo/NN-*.png 关键步骤截图(deviceScaleFactor=2,高清) * - docs/demo/walkthrough.webm 全程操作录像(Playwright 原生录制) * * 运行:node scripts/demo-record.mjs */ import { chromium } from 'playwright'; import fs from 'node:fs'; import path from 'node:path'; const ROOT = '/Users/freedak/Documents/AIDashboard/RiskAgent'; const SHOTS = path.join(ROOT, 'docs/demo'); const VIDEO_DIR = path.join(SHOTS, 'video'); const BASE = 'http://localhost:5173'; const W = 1440; const H = 900; fs.mkdirSync(SHOTS, { recursive: true }); fs.mkdirSync(VIDEO_DIR, { recursive: true }); let n = 0; async function shot(page, label) { n += 1; const name = `${String(n).padStart(2, '0')}-${label}.png`; await page.screenshot({ path: path.join(SHOTS, name) }); console.log('📸', name); } async function expandAll(page) { for (let i = 0; i < 15; i += 1) { const collapsed = page.locator('button[aria-expanded="false"]'); const c = await collapsed.count(); if (c === 0) break; try { await collapsed.first().click({ timeout: 5000 }); } catch { break; } await page.waitForTimeout(150); } } (async () => { const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: W, height: H }, deviceScaleFactor: 2, locale: 'zh-CN', recordVideo: { dir: VIDEO_DIR, size: { width: W, height: H } }, }); const page = await context.newPage(); page.setDefaultTimeout(120000); const video = page.video(); // ---------- 登录 ---------- await page.goto(BASE, { waitUntil: 'networkidle' }); await page.waitForTimeout(800); const userBox = page.getByPlaceholder('请输入用户名'); if ((await userBox.count()) > 0) { await shot(page, 'login'); await userBox.fill('销售账号'); await page.getByPlaceholder('请输入密码').fill('123456'); await page.getByRole('button', { name: '登录' }).click(); await page.waitForLoadState('networkidle'); await page.waitForTimeout(1200); } // ---------- 首页:评估历史 ---------- await shot(page, 'dashboard'); // ---------- 进入新建评估 ---------- await page.getByRole('button', { name: /新建评估/ }).first().click(); await page.getByText('① 立项信息').waitFor(); await page.getByPlaceholder('如 某银行信用卡客服 BPO').fill('某全国性股份制银行信用卡中心客服外包项目'); await page.getByPlaceholder('客户公司全称').fill('某全国性股份制商业银行信用卡中心'); await page.locator('select').first().selectOption('上海'); await page.waitForTimeout(300); await shot(page, 'step1-立项信息'); await page.getByRole('button', { name: '下一步', exact: true }).click(); // ---------- 步骤②:项目描述 ---------- await page.getByText('② 项目描述').waitFor(); const desc = '为客户信用卡中心提供电话客服与在线客服外包服务,采用业务/服务外包模式,坐席约 80 人,' + '7×12 小时三班排班;服务质量按接通率、平均处理时长与客户满意度考核;结算账期约 3 个月;' + '需符合金融数据安全与个人信息保护合规要求,涉及呼叫中心系统对接与驻场管理。'; await page.locator('textarea').fill(desc); await page.waitForTimeout(300); await shot(page, 'step2-项目描述'); await page.getByRole('button', { name: /识别业务类型|识别中/ }).click(); // ---------- 步骤③:业务类型确认(LLM 分类) ---------- await page.getByText('③ 业务类型确认').waitFor({ timeout: 120000 }); await page.waitForTimeout(600); await shot(page, 'step3-业务确认-LLM分类'); await page.getByRole('button', { name: /补全指标|加载指标中/ }).click(); // ---------- 步骤④:指标补全(LLM 预填) ---------- await page.getByText('④ 指标补全', { exact: false }).waitFor({ timeout: 120000 }); await page.waitForTimeout(800); await shot(page, 'step4-指标补全-LLM预填'); await page.getByRole('button', { name: /费用输入/ }).click(); // ---------- 步骤⑤:报价与成本 ---------- await page.getByText('⑤ 报价与成本').waitFor(); await page.getByPlaceholder('如 运维工程师').fill('信用卡客服坐席'); await page.getByPlaceholder('10', { exact: true }).fill('80'); await page.getByPlaceholder('10000', { exact: true }).fill('6000'); await page.getByPlaceholder('默认=工资', { exact: true }).fill('6000'); await page.getByPlaceholder('0', { exact: true }).fill('500'); await page.getByPlaceholder('如 16000', { exact: true }).fill('11000'); await page.waitForTimeout(300); await shot(page, 'step5-报价与成本'); await page.getByRole('button', { name: /运行风险与盈利评估|评估运行中/ }).click(); // ---------- 评估详情页 ---------- await page.waitForURL(/\/assessments\/[^/]+$/, { timeout: 180000 }); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000); await shot(page, 'detail-顶部结论'); // 等待 AI 综合研判(异步 LLM),最多 ~25s for (let i = 0; i < 25; i += 1) { if ((await page.getByText(/综合研判|综合意见|AI 综合/).count()) > 0) break; await page.waitForTimeout(1000); } await expandAll(page); await page.waitForTimeout(800); // 逐屏滚动截图,覆盖全部分区 await page.evaluate(() => window.scrollTo(0, 0)); const totalH = await page.evaluate(() => document.body.scrollHeight); const stepY = H - 140; for (let y = stepY; y < totalH; y += stepY) { await page.evaluate((yy) => window.scrollTo(0, yy), y); await page.waitForTimeout(450); await shot(page, 'detail-分区'); if (n > 22) break; } await context.close(); await browser.close(); const vp = await video.path(); const dest = path.join(SHOTS, 'walkthrough.webm'); fs.renameSync(vp, dest); console.log('🎬 video saved:', dest); console.log('✅ done, screenshots:', n); })().catch((e) => { console.error('FAILED:', e); process.exit(1); });