// 全量样板渲染巡检: 逐套在效果图渲染页切换样板, 收集 JS 异常 + 画布活性(非空白像素占比) // 用法: node scripts/patrol-render-all.mjs [--shot=12] import puppeteer from 'puppeteer-core' import fs from 'fs' const token = process.argv[2] const shotN = Number((process.argv.find(a => a.startsWith('--shot=')) ?? '--shot=12').split('=')[1]) || 12 const browser = await puppeteer.launch({ executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', headless: 'new', args: ['--window-size=1440,900', '--enable-unsafe-swiftshader'], defaultViewport: { width: 1440, height: 900 }, }) const page = await browser.newPage() const errs = [] page.on('pageerror', e => errs.push(String(e).slice(0, 200))) page.on('console', m => { if (m.type() === 'error' && !m.text().includes('favicon')) errs.push('[console] ' + m.text().slice(0, 150)) }) await page.goto(`http://localhost:8000/rendering?token=${encodeURIComponent(token)}`, { waitUntil: 'networkidle2' }) await new Promise(r => setTimeout(r, 1500)) // 取全部样板名 const names = await page.evaluate(async () => { const token = localStorage.getItem('hmc_token') const res = await fetch('/api/windoor/templates?page=1&page_size=254', { headers: { Authorization: `Bearer ${token}` } }) const d = (await res.json()).data const rows = Array.isArray(d) ? d : d.data return rows.map(r => r.name) }) console.log('样板总数:', names.length) const results = [] for (let i = 0; i < names.length; i++) { const name = names[i] errs.length = 0 try { // 打开下拉 → 输入过滤 → 选中 await page.evaluate(() => document.querySelector('.el-select')?.querySelector('.el-select__wrapper, .el-input__inner')?.dispatchEvent(new MouseEvent('click', { bubbles: true }))) await new Promise(r => setTimeout(r, 350)) await page.keyboard.type(name, { delay: 5 }) await new Promise(r => setTimeout(r, 550)) const picked = await page.evaluate(nm => { const items = [...document.querySelectorAll('.el-select-dropdown__item')] const el = items.find(x => x.textContent.trim() === nm) if (!el) return false el.click() return true }, name) if (!picked) { results.push([name, 'SKIP', '下拉未命中']); await page.keyboard.press('Escape'); continue } await new Promise(r => setTimeout(r, 1000)) // 画布活性: 非透明像素占比 const alive = await page.evaluate(() => { const cv = document.querySelector('.render-canvas') if (!cv || !cv.width) return -1 const ctx = cv.getContext('2d') const d = ctx.getImageData(0, 0, cv.width, cv.height).data let n = 0 for (let p = 3; p < d.length; p += 40) if (d[p] > 0) n++ return n / (d.length / 40) }) results.push([name, errs.length ? 'ERR' : (alive > 0.02 ? 'OK' : 'BLANK'), errs.length ? errs[0] : `alive=${alive.toFixed(3)}`]) } catch (e) { results.push([name, 'ERR', String(e).slice(0, 120)]) } if ((i + 1) % 40 === 0) console.log(`进度 ${i + 1}/${names.length}`) } const bad = results.filter(r => r[1] !== 'OK' && r[1] !== 'SKIP') console.log(`\n结果: OK=${results.filter(r => r[1] === 'OK').length} BLANK=${results.filter(r => r[1] === 'BLANK').length} ERR=${results.filter(r => r[1] === 'ERR').length} SKIP=${results.filter(r => r[1] === 'SKIP').length}`) for (const r of bad) console.log(' ' + r.join(' | ')) fs.writeFileSync('scripts/.render-patrol.json', JSON.stringify(results)) await browser.close()