'use strict'; /** * 前端分组调试:起本地假 Photon + 服务端,然后在真实页面里读候选字段。 */ const { spawn } = require('child_process'); const path = require('path'); const os = require('os'); const http = require('http'); const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe'; const PORT = 9356; const PHOTON_PORT = 9357; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const REAL = [ ['荔波小七孔', 25.2461, 107.8567, '黔南州', '荔波县'], ['社更穿洞北门', 24.5701, 107.7234, '河池市', '都安瑶族自治县'], ['万寿谷', 24.3312, 107.8123, '河池市', '都安瑶族自治县'], ['柳州之门', 24.3089, 109.4112, '柳州市', '城中区'], ['八里九弯', 23.6712, 107.2345, '百色市', '平果市'], ['加榜梯田', 25.8571, 108.6388, '黔东南州', '从江县'], ]; class CDP { constructor(ws) { this.ws = ws; this.id = 0; this.w = new Map(); } static async connect(u) { const ws = new WebSocket(u); 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.w.has(m.id)) { c.w.get(m.id)(m); c.w.delete(m.id); } }; return c; } send(method, params = {}, sessionId) { const id = ++this.id; return new Promise((res) => { this.w.set(id, res); this.ws.send(JSON.stringify({ id, method, params, sessionId })); }); } } (async () => { const photon = http.createServer((req, res) => { const u = new URL(req.url, 'http://x'); const lim = Number(u.searchParams.get('limit') || 8); const feats = REAL.slice(0, lim).map((r, i) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [r[2], r[1]] }, properties: { name: r[0], osm_type: 'N', osm_id: 1000 + i, county: r[3], city: r[4], district: '', state: 'X' }, })); res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ features: feats })); }); await new Promise((r) => photon.listen(PHOTON_PORT, '127.0.0.1', r)); const server = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), env: { ...process.env, PHOTON_ENDPOINT: `http://127.0.0.1:${PHOTON_PORT}/api` }, stdio: ['ignore', 'pipe', 'pipe'], }); let banner = ''; server.stdout.on('data', (d) => { banner += d.toString(); }); server.stderr.on('data', (d) => { banner += d.toString(); }); await sleep(3000); const base = (banner.match(/http:\/\/127\.0\.0\.1:(\d+)/) || [])[1]; console.log('base =', base); const chrome = spawn(CHROME, ['--headless=new', '--disable-gpu', '--hide-scrollbars', `--remote-debugging-port=${PORT}`, '--user-data-dir=' + path.join(os.tmpdir(), 'dbg' + Date.now()), 'about:blank'], { stdio: 'ignore' }); let wsUrl = null; for (let i = 0; i < 30 && !wsUrl; i++) { await sleep(500); try { const r = await fetch(`http://127.0.0.1:${PORT}/json/version`); wsUrl = (await r.json()).webSocketDebuggerUrl; } catch (e) { /* wait */ } } const b = await CDP.connect(wsUrl); const { result: { targetId } } = await b.send('Target.createTarget', { url: 'about:blank' }); const { result: { sessionId } } = await b.send('Target.attachToTarget', { targetId, flatten: true }); const S = (m, p) => b.send(m, p, sessionId); await S('Page.enable'); await S('Runtime.enable'); await S('Page.navigate', { url: `http://127.0.0.1:${base}` }); await sleep(2500); const ev = async (e) => { const r = await S('Runtime.evaluate', { expression: e, awaitPromise: true, returnByValue: true }); if (r.result?.exceptionDetails) throw new Error(JSON.stringify(r.result.exceptionDetails).slice(0, 300)); return r.result?.result?.value; }; const raw = await ev(`(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-20', endDate: '2026-11-22', radiusKm: 150, maxOneWayHours: 2.5, excludeLocal: true }), }); 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; } return null; })()`); console.log('接口原始 candidates[0] 字段:', Object.keys(raw.candidates[0]).join(',')); console.log('接口原始 place =', raw.candidates.map((c) => `${c.name}:${c.place}/${c.placeKey}/${c.placeDetail}`).join(' | ')); await ev(`(() => { state.analysis = ${JSON.stringify(raw)}; gotoStep(2); renderStep2(); renderCards(); return true; })()`); await sleep(1200); const after = await ev(`(() => { const c = state.analysis.candidates.find(x => x.name === '社更穿洞北门'); return { cand: c ? { name: c.name, place: c.place, placeKey: c.placeKey, placeDetail: c.placeDetail } : null, groups: groupByPlace(sortedCandidates()).map(g => g.key + '|' + g.place) }; })()`); console.log('注入后:', JSON.stringify(after, null, 1)); b.ws.close(); chrome.kill(); server.kill(); photon.close(); process.exit(0); })().catch((e) => { console.error('FAIL', e.message); process.exit(1); });