'use strict'; /** * 第二步真实数据验收:走完整分析流程(用高德 Key 覆盖), * 检查真实候选的分组、搜索、折叠与卡片渲染是否有 undefined。 */ const { spawn } = require('child_process'); const fs = require('fs'); const path = require('path'); const os = require('os'); const OUT = path.join(__dirname, '..', 'preview'); const BASE = process.env.BASE || 'http://127.0.0.1:8787'; const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe'; const PORT = 9346; function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } class CDP { constructor(ws) { this.ws = ws; this.id = 0; this.waits = new Map(); } static async connect(url) { const ws = new WebSocket(url); await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; }); const c = new CDP(ws); ws.onmessage = (e) => { const m = JSON.parse(e.data); if (m.id && c.waits.has(m.id)) { c.waits.get(m.id)(m); c.waits.delete(m.id); } }; return c; } send(method, params = {}, sessionId) { const id = ++this.id; return new Promise((res) => { this.waits.set(id, res); this.ws.send(JSON.stringify({ id, method, params, sessionId })); }); } } (async () => { const chrome = spawn(CHROME, [ '--headless=new', '--disable-gpu', '--hide-scrollbars', `--remote-debugging-port=${PORT}`, '--user-data-dir=' + path.join(os.tmpdir(), 'tripverify2' + Date.now()), 'about:blank', ], { stdio: 'ignore' }); let wsUrl = null; for (let i = 0; i < 30 && !wsUrl; i += 1) { await sleep(500); try { const res = await fetch(`http://127.0.0.1:${PORT}/json/version`); wsUrl = (await res.json()).webSocketDebuggerUrl; } catch (e) { /* 还没起来 */ } } if (!wsUrl) { console.error('chrome 未启动'); process.exit(1); } const browser = await CDP.connect(wsUrl); const { result: { targetId } } = await browser.send('Target.createTarget', { url: 'about:blank' }); const { result: { sessionId } } = await browser.send('Target.attachToTarget', { targetId, flatten: true }); const S = (m, p) => browser.send(m, p, sessionId); await S('Page.enable'); await S('Runtime.enable'); await S('Emulation.setDeviceMetricsOverride', { width: 1280, height: 1400, deviceScaleFactor: 1, mobile: false }); await S('Page.navigate', { url: BASE }); await sleep(2500); const evalJs = async (expr) => { const r = await S('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true }); if (r.result?.exceptionDetails) throw new Error(JSON.stringify(r.result.exceptionDetails).slice(0, 400)); return r.result?.result?.value; }; // 直接调服务端分析接口拿真实候选(避开页面的表单流程) const real = await evalJs(`(async () => { const res = await fetch('/api/analyze', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ origin: { name: '河池市金城江区', lat: 24.69562, lon: 108.08091 }, startDate: '2026-11-06', endDate: '2026-11-08', radiusKm: 150, maxOneWayHours: 2.5, excludeLocal: true, transportPrefs: ['不限'], interests: [], }), }); const txt = await res.text(); for (const line of txt.split('\\n')) { if (!line.startsWith('data:')) continue; const j = JSON.parse(line.slice(5).trim()); if (j.type === 'result') return j.data; if (j.type === 'error') return { error: j.message }; } return { error: 'no result' }; })()`); if (real.error) { console.error('分析失败:', real.error); process.exit(1); } console.log('真实候选数 =', real.candidates.length); await evalJs(`(() => { state.analysis = ${JSON.stringify(real)}; gotoStep(2); renderStep2(); renderCards(); return true; })()`); await sleep(1500); const groups = await evalJs(`(() => [...document.querySelectorAll('#cards .place-group')].map(g => ({ key: g.dataset.place, head: g.querySelector('.place-head').innerText.replace(/\\n/g, ' | '), items: [...g.querySelectorAll('.dest-card .card-title h4')].map(x => x.textContent), })))()`); console.log('=== 真实分组 ==='); for (const g of groups) console.log(' ', g.key, '→', g.items.join(', '), '||', g.head); // 检查 undefined const undef = await evalJs(`(() => { const t = document.querySelector('#cards').innerText; const hits = (t.match(/undefined/g) || []).length; return { hits, sample: hits ? t.split('\\n').filter(x => x.includes('undefined')).slice(0, 5) : [] }; })()`); console.log('卡片内 undefined 出现次数 =', undef.hits, undef.sample); // 搜索 const kw = groups[0]?.items[0]?.slice(0, 2) || '柳州'; await evalJs(`(() => { const i = document.querySelector('#candSearch'); i.value='${kw}'; i.dispatchEvent(new Event('input',{bubbles:true})); return true; })()`); await sleep(500); console.log(`搜索「${kw}」→ 卡片`, await evalJs(`document.querySelectorAll('#cards .dest-card').length`), '/ 提示条:', await evalJs(`document.querySelector('#filterNote')?.innerText`)); await evalJs(`document.querySelector('#candSearchClear').click()`); await sleep(500); const box = await evalJs(`(() => { const r = document.querySelector('#panel2').getBoundingClientRect(); return { x: r.x + scrollX, y: r.y + scrollY, width: r.width, height: Math.min(r.height, 2400) }; })()`); const shot = await S('Page.captureScreenshot', { format: 'png', captureBeyondViewport: true, clip: { ...box, scale: 1 } }); fs.writeFileSync(path.join(OUT, 'preview-step2-real.png'), Buffer.from(shot.result.data, 'base64')); console.log('截图已保存 preview/preview-step2-real.png'); browser.ws.close(); chrome.kill(); process.exit(0); })().catch((e) => { console.error('FAIL', e.message); process.exit(1); });