/** * 票 39 真机走查脚本(puppeteer):粗→精影像交接。 * * **本仓现状**:本地 puppeteer 不可用(`@puppeteer/browsers` 目录为空,`ERR_MODULE_NOT_FOUND`), * 故本票的真机证据由维护者授权的 DSH 浏览器逐项采集(读数见票面「证据」一节)。 * 本脚本按既有走查写法保留,供修复 puppeteer 后一键复跑;逻辑与当时的手工采集**逐条对应**。 * * **不做的事(诚实边界)**: * · 「全程无肉眼可辨的方形跳变」是**维护者的目视判据**,本脚本只提供可观测读数与截图,不代判; * · 矩形**内部**随地形 LOD 的逐级换挡**不由整层 alpha 修复**(决策 08 明确)—— * 本脚本只如实记录它是否可辨,修复评估归票 40。 * * 运行(需先起 `yarn dev`,并把浏览器标签页切到前台 —— 隐藏标签页 rAF 冻结,强度推不动): * node .scratch/cockpit-shell-panel-layout/verify/t39-handover-walkthrough.mjs * 按 AGENTS.md:本脚本不在改代码后自动运行,由维护者明确要求时才跑。 */ import { existsSync } from 'node:fs' import { resolve } from 'node:path' const BASE = process.env.COCKPIT_URL || 'http://127.0.0.1:8000/#/cockpit' const results = [] const check = (id, what, ok, detail) => { results.push({ id, what, ok }) console.log(`${ok ? 'PASS' : 'FAIL'} ${id} ${what}${ok ? '' : `\n ${detail}`}`) } const puppeteerPath = resolve('node_modules/puppeteer') if (!existsSync(puppeteerPath)) { console.error('未找到本地 puppeteer(本仓已知问题:@puppeteer/browsers 为空目录)。') console.error('本票真机证据改由 DSH 浏览器采集,逐条记录见票面「证据」一节。') process.exit(2) } const puppeteer = (await import('puppeteer')).default const browser = await puppeteer.launch({ headless: 'new', args: ['--window-size=1733,1000'] }) const page = await browser.newPage() await page.setViewport({ width: 1733, height: 1000 }) const errors = [] page.on('pageerror', (error) => errors.push(String(error && error.message))) page.on('console', (message) => { if (message.type() === 'error') errors.push(message.text()) }) const sleep = (ms) => new Promise((done) => setTimeout(done, ms)) const readHandover = () => page.evaluate(() => { const api = window.__cockpitMap const host = document.querySelector('.cockpit-shell__map .map-canvas') return { handover: api ? api.getHandoverState() : null, fog: api ? api.getFogState() : null, areaState: host ? host.dataset.mapAreaState : null, hostAlpha: host ? host.dataset.mapHandoverAlpha : null, hostPhase: host ? host.dataset.mapHandoverPhase || host.dataset.mapHandover : null, hostBlock: host ? host.dataset.mapHandoverBlock : null, } }) try { await page.goto(BASE, { waitUntil: 'domcontentloaded' }) await page.waitForSelector('.cockpit-shell__map .map-canvas[data-map-engine="ready"]', { timeout: 90000 }) // ---------- A1 全球/引导态:精细层必须是隐藏的 ---------- { const initial = await readHandover() check( 'A1-global-hidden', '全球/引导态:精细层 alpha=0(保留粗底图),相位 global', initial.handover && initial.handover.phase === 'global' && initial.handover.alpha === 0, JSON.stringify(initial.handover), ) check( 'A1b-hidden-still-prefetches', '隐藏期间仍可继续预取瓦片(**不是** show=false)—— 看不见≠停止加载', initial.handover && initial.handover.layers > 0, `layers=${initial.handover && initial.handover.layers}`, ) } // ---------- A2 进入:雾先形成保护,才允许揭示 ---------- { await page.evaluate(() => { const intro = document.querySelector('[data-cockpit-intro]') if (intro) intro.click() window.__cockpitMap.goHome({ immediate: false }) }) // 采样整段进入过程:记录 (fogStrength, alpha) 的先后关系。 const samples = [] for (let i = 0; i < 40; i++) { samples.push(await readHandover()) await sleep(150) } const revealedEarly = samples.some((sample) => sample.handover && sample.handover.alpha > 0.05 && sample.fog && sample.fog.strength < 0.99) check( 'A2-fog-before-imagery', '进入途中:**不得**在雾强度未到位(<0.99)时就把精细层推到可见(>0.05)', !revealedEarly, '采样中发现"影像先亮、雾后到"的帧 —— 正是决策 08 记的"矩形边缘先暴露"', ) const settled = await readHandover() check( 'A2b-revealed-after-fog', '雾到位且瓦片就绪后,精细层应揭示(alpha→1)', settled.handover && (settled.handover.phase === 'revealed' || settled.handover.phase === 'entering'), JSON.stringify(settled.handover), ) check( 'A2c-tiles-signal-is-proxy', '瓦片读数**如实标注**是代理信号(不宣称目标视野逐像素就绪)', settled.handover && settled.handover.tilesReadyIsProxy === true, JSON.stringify(settled.handover && settled.handover.tilesReadyIsProxy), ) } // ---------- A3 返回:影像收起后**才**撤雾(反向交接) ---------- { const order = [] await page.evaluate(() => window.__cockpitMap.returnGlobal()) for (let i = 0; i < 40; i++) { const sample = await readHandover() order.push({ alpha: sample.handover && sample.handover.alpha, fog: sample.fog && sample.fog.strength }) await sleep(120) } // 在 alpha 仍 > 0.05 的任何一帧,雾都不该已经被撤到 0 —— 那就是"影像还在,保护没了"。 const droppedWhileVisible = order.some((frame) => frame.alpha > 0.05 && frame.fog <= 0.01) check( 'A3-fog-held-until-hidden', '返回途中:精细层仍可见(alpha>0.05)时**不得**已把雾撤净', !droppedWhileVisible, '采样中发现"影像还在、雾已撤"的帧 —— 会重现矩形边缘', ) const last = order[order.length - 1] check( 'A3b-both-settled', '返回结束:精细层归 0 且雾归 0', last && last.alpha <= 0.001 && last.fog <= 0.001, JSON.stringify(last), ) // 同步性:影像与雾的推进**重叠**(不是"影像收完→停顿→雾才动")。 const alphaMoves = order.filter((frame) => frame.alpha > 0.001 && frame.alpha < 0.999).length const fogMoves = order.filter((frame) => frame.fog > 0.001 && frame.fog < 0.999).length check( 'A3c-overlap-not-serial', '影像与雾的收束**有重叠**(不是串行等待),符合"高度与雾同步"', alphaMoves > 0 && fogMoves > 0, `alpha 过渡帧=${alphaMoves},雾过渡帧=${fogMoves}`, ) } // ---------- A4 取消 / 快速反向:只认最新目标 ---------- { await page.evaluate(() => { window.__cockpitMap.goHome({ immediate: true }) window.__cockpitMap.enterArea() }) await sleep(300) await page.evaluate(() => window.__cockpitMap.returnGlobal()) await sleep(120) await page.evaluate(() => window.__cockpitMap.enterArea()) await sleep(2500) const after = await readHandover() check( 'A4-latest-target-wins', '进→返回→再进:最终必须落在"已进区"的目标上,旧的反向不得生效', after.areaState === 'locked' && after.handover && after.handover.alpha > 0.5, JSON.stringify({ area: after.areaState, alpha: after.handover && after.handover.alpha }), ) check( 'A4b-no-half-transparent-stuck', '快速反向不得留下半透明卡死态(落定值必须是 0 或 1)', after.handover && (after.handover.alpha === 0 || after.handover.alpha === 1), `alpha=${after.handover && after.handover.alpha}`, ) } // ---------- A5 无"已遮边"承诺 ---------- { const state = await readHandover() check( 'A5-no-false-promise', '读数**不得**提供"边缘已遮住"的承诺(就绪≠已遮住)', state.handover && !('protected' in state.handover) && !('coversEdges' in state.handover), JSON.stringify(state.handover), ) } check('A6-no-page-errors', '全程无页面级报错', errors.length === 0, JSON.stringify(errors.slice(0, 3))) } finally { await browser.close() } const failed = results.filter((item) => !item.ok) console.log(`\nT39 走查:${results.length - failed.length}/${results.length} 通过`) if (failed.length) process.exit(1)