aps-agent/_shots/capture.mjs

199 lines
7.8 KiB
JavaScript
Raw Normal View History

// ============================================================
// 操作手册截图自动化 v2:全新演示种子数据(不复制现场 world)
// 隔离后端(8100) + Vite(5399) + Playwright,结束杀子进程。
// ============================================================
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
const ROOT = path.resolve(import.meta.dirname, '..');
const require = createRequire(path.join(ROOT, 'apps', 'web', 'package.json'));
const { chromium } = require('playwright');
const SHOTS = path.join(ROOT, '_shots');
const TMP = path.join(SHOTS, 'tmp2');
fs.mkdirSync(SHOTS, { recursive: true });
fs.rmSync(TMP, { recursive: true, force: true });
fs.mkdirSync(TMP, { recursive: true });
const PY = path.join(ROOT, '.venv', 'Scripts', 'python.exe');
const VITE = path.join(ROOT, 'apps', 'web', 'node_modules', 'vite', 'bin', 'vite.js');
const API = 'http://127.0.0.1:8100';
const UI = 'http://localhost:5399';
// 只带知识与配置,世界/主库走全新演示种子
for (const f of ['knowledge.json', 'config.json']) {
const src = path.join(ROOT, 'server', 'data', f);
if (fs.existsSync(src)) fs.copyFileSync(src, path.join(TMP, f));
}
const children = [];
function run(name, cmd, args, opts = {}) {
const log = fs.createWriteStream(path.join(SHOTS, `${name}2.log`));
const p = spawn(cmd, args, { cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'], ...opts });
p.stdout.pipe(log); p.stderr.pipe(log);
children.push(p);
return p;
}
function cleanup() {
for (const p of children) {
try { spawn('taskkill', ['/PID', String(p.pid), '/T', '/F'], { stdio: 'ignore' }); } catch {}
}
}
process.on('exit', cleanup);
async function waitHttp(url, timeoutMs) {
const t0 = Date.now();
while (Date.now() - t0 < timeoutMs) {
try { const r = await fetch(url); if (r.status < 500) return; } catch {}
await new Promise(r => setTimeout(r, 1000));
}
throw new Error('waitHttp timeout: ' + url);
}
run('backend', PY, ['-m', 'uvicorn', 'server.main:app', '--port', '8100'], {
env: {
...process.env,
APS_PORT: '8100',
APS_HOME: TMP,
APS_WORLD_PATH: path.join(TMP, 'world.json'),
APS_DB_PATH: path.join(TMP, 'master.db'),
APS_APPROVAL_PATH: path.join(TMP, 'approvals.json'),
APS_KNOWLEDGE_PATH: path.join(TMP, 'knowledge.json'),
APS_WEB_DEMO_SEED: '1',
},
});
run('frontend', process.execPath, [VITE, '--port', '5399', '--strictPort'], {
cwd: path.join(ROOT, 'apps', 'web'),
env: { ...process.env, VITE_API_TARGET: API },
});
console.log('等待服务就绪…');
await waitHttp(API + '/api/health', 120_000);
await waitHttp(UI, 120_000);
console.log('服务就绪');
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1.5 });
const results = [];
async function shot(name) {
await page.waitForTimeout(700);
await page.screenshot({ path: path.join(SHOTS, name + '.png') });
results.push(name);
console.log(' ✓', name);
}
async function tryStep(name, fn, timeoutMs = 12_000) {
try { await Promise.race([fn(), new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), timeoutMs))]); return true; }
catch (e) { console.log(' ✗ 跳过', name, '-', e.message.slice(0, 60)); return false; }
}
// 01 登录弹窗
await page.goto(UI);
await page.locator('.guest-login-trigger').waitFor({ state: 'visible', timeout: 60_000 });
await page.locator('.guest-login-trigger').click();
const dialog = page.locator('.auth-login-dialog');
await dialog.waitFor({ state: 'visible' });
await shot('01-登录');
await dialog.locator('input[placeholder="请输入企业名称"]').fill('platform');
await dialog.locator('input[placeholder="请输入用户名"]').fill('zhangzhen');
await dialog.locator('input[placeholder="请输入密码"]').fill('sz@zz@1116');
await dialog.locator('.login-submit').click();
const composer = page.locator('textarea[placeholder^="输入排产指令"]');
await composer.waitFor({ state: 'visible', timeout: 60_000 });
await tryStep('主界面稳定', async () => {
await page.locator('text=快捷指令').first().waitFor({ state: 'visible', timeout: 40_000 });
}, 45_000);
await page.waitForTimeout(2000);
await shot('02-主界面');
// 03 对话:固定轨试排(交期优先)→ 甘特有数据
await composer.fill('试排一版交期优先');
await composer.press('Enter');
await tryStep('试排结果', async () => {
await page.locator('.msg.agent .bubble', { hasText: /排产|版本|工单/ }).last()
.waitFor({ state: 'visible', timeout: 150_000 });
}, 160_000);
await page.waitForTimeout(2500);
await shot('03-对话试排');
// 视口页签(固定轨视图)
const resultBtn = page.locator('.right-rail button[data-tip="会话结果"], .right-rail button[data-tip="再点收起"]').first();
if (!(await page.locator('.vp-toolbar').isVisible().catch(() => false))) {
await tryStep('打开会话结果', () => resultBtn.click());
}
await page.locator('.vp-toolbar').waitFor({ state: 'visible', timeout: 20_000 }).catch(() => {});
const tab = async (label, name) => {
const ok = await tryStep('页签:' + label, async () => {
await page.locator('.seg .tab', { hasText: label }).first().click();
await page.waitForTimeout(1200);
});
if (ok) await shot(name);
};
await tab('甘特图', '04-甘特图');
await tab('负荷热力', '05-负荷热力');
await tab('交期承诺', '06-交期承诺');
// 07 对话:柔性排产 → 柔性视图有数据
await tryStep('柔性排产', async () => {
await composer.fill('跑一版柔性排产');
await composer.press('Enter');
await page.locator('.flex-block-title', { hasText: '柔性排产' }).first()
.waitFor({ state: 'visible', timeout: 150_000 });
}, 160_000);
await page.waitForTimeout(1500);
await shot('07-对话柔性排产');
// 柔性相关视口页签
if (!(await page.locator('.vp-toolbar').isVisible().catch(() => false))) {
await tryStep('再开会话结果', () => resultBtn.click());
}
await tab('柔性甘特', '08-柔性甘特');
await tab('产能池', '09-产能池');
await tab('分桶计划', '10-分桶计划');
await tab('KPI', '11-KPI仪表盘');
await tab('对比表', '12-对比表');
// 右侧工具面板
const rail = async (tip, name, waitSel) => {
const ok = await tryStep('面板:' + tip, async () => {
await page.locator(`.right-rail button[data-tip="${tip}"]`).click();
if (waitSel) await page.locator(waitSel).waitFor({ state: 'visible', timeout: 12_000 });
await page.waitForTimeout(1000);
});
if (ok) await shot(name);
await tryStep('收起', () => page.locator('.right-rail button[data-tip="再点收起"]').click(), 4_000);
};
await rail('柔性工作台', '13-柔性工作台', '.flex-bench');
await rail('订单管理', '14-订单管理');
await rail('主数据', '15-主数据');
await rail('排产结构', '16-排产结构');
await rail('多智能体编排', '17-多智能体编排');
await rail('图纸识别', '18-图纸识别');
// 19 命令面板
await page.keyboard.press('Control+K');
await tryStep('命令面板', async () => {
await page.locator('.palette input').waitFor({ state: 'visible', timeout: 8_000 });
await page.locator('.palette input').fill('柔性');
await page.waitForTimeout(500);
});
await shot('19-命令面板');
await page.keyboard.press('Escape');
// 20 对话:多策略对比
await tryStep('策略对比', async () => {
await composer.fill('对比几种策略');
await composer.press('Enter');
await page.locator('text=采用').first().waitFor({ state: 'visible', timeout: 130_000 });
}, 140_000);
await page.waitForTimeout(1000);
await shot('20-对话策略对比');
await browser.close();
fs.writeFileSync(path.join(SHOTS, 'manifest2.json'), JSON.stringify(results, null, 2));
console.log('完成,共', results.length, '张');
cleanup();
process.exit(0);