Files
RiskAgent/scripts/demo-record.mjs
T
freedakgmail c670b9e454 外包风险评估系统:领域引擎+前端+服务端持久化与生产部署
- 确定性领域引擎(分类/评分/分级/红线/费用/裁决)+LLM(通义千问)语言理解
- 6步评估向导、服务端草稿持久化(跨设备/编辑草稿保护)
- 工作流(草稿→风控→管理层)、RBAC、报告导出、校准、客户/费率/红线/最低工资管理
- 专业图标体系替换全部emoji、看板美化
- 生产化:API_BASE可配置(同源反代)、auth密钥惰性读取修复RBAC
- 444单测+204前端测试+51 e2e
2026-06-13 01:06:39 +08:00

156 lines
6.1 KiB
JavaScript

/**
* 端到端演示录制:登录 → 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);
});