aps-agent/_shots/capture3.mjs

131 lines
4.9 KiB
JavaScript

// 第三轮补拍:柔性甘特 / 世界对比 / 利用率(复制现场世界,修复页签滚动)
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, 'tmp3');
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';
const JMS = {
tenant: process.env.E2E_JMS_TENANT?.trim() || '',
username: process.env.E2E_JMS_USER?.trim() || '',
password: process.env.E2E_JMS_PASSWORD || '',
};
const missingJmsConfig = Object.entries(JMS).filter(([, value]) => !value).map(([key]) => key);
if (missingJmsConfig.length) {
throw new Error(`截图脚本缺少 JMS 登录环境变量:${missingJmsConfig.join(', ')}`);
}
for (const f of ['world.json', 'master.db', 'approvals.json', '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}3.log`));
const p = spawn(cmd, args, { cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'], ...opts });
p.stdout.pipe(log); p.stderr.pipe(log);
children.push(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_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);
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1.5 });
async function shot(name) {
await page.waitForTimeout(700);
await page.screenshot({ path: path.join(SHOTS, name + '.png') });
console.log(' ✓', name);
}
// 登录
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.locator('input[placeholder="请输入企业名称"]').fill(JMS.tenant);
await dialog.locator('input[placeholder="请输入用户名"]').fill(JMS.username);
await dialog.locator('input[placeholder="请输入密码"]').fill(JMS.password);
await dialog.locator('.login-submit').click();
const composer = page.locator('textarea[placeholder^="输入排产指令"]');
await composer.waitFor({ state: 'visible', timeout: 60_000 });
await page.waitForTimeout(2500);
// 柔性排产(让柔性甘特有数据)
await composer.fill('跑一版柔性排产');
await composer.press('Enter');
await page.locator('.flex-block-title', { hasText: '柔性排产' }).first()
.waitFor({ state: 'visible', timeout: 170_000 });
await page.waitForTimeout(1500);
// 打开视口
const resultBtn = page.locator('.right-rail button[data-tip="会话结果"], .right-rail button[data-tip="再点收起"]').first();
if (!(await page.locator('.vp-toolbar').isVisible().catch(() => false))) {
try { await resultBtn.click(); } catch {}
}
await page.locator('.vp-toolbar').waitFor({ state: 'visible', timeout: 20_000 }).catch(() => {});
const tab = async (label, name) => {
try {
const t = page.locator('.seg .tab', { hasText: label }).first();
await t.scrollIntoViewIfNeeded({ timeout: 8_000 });
await t.click({ timeout: 8_000 });
await page.waitForTimeout(1500);
await shot(name);
} catch (e) { console.log(' ✗', label, e.message.slice(0, 60)); }
};
await tab('柔性甘特', '21-柔性甘特');
await tab('世界对比', '22-世界对比');
await tab('利用率', '23-利用率');
await browser.close();
console.log('补拍完成');
cleanup();
process.exit(0);