Files
tripsense/tools/diag-ui.cjs
T
tamakyi c5c2ffae68 TripSense 行知 · 周边出行顾问:首版
零依赖 Node 服务,三步流程(选出发地 → 分析候选 → 生成方案 + 路线图 + 住宿参考)。

- 候选发现:高德 place/text 类型词跨城检索 + 内置景区/城市/小众库 + Photon 兜底
- 区县级「排除同城」判定(adname),过滤发生在轮转取样之前
- 路线图画的是 AI 方案里实际安排的每个地点,按正文顺序编号 + 虚线动线 + 真实高德底图
- 住宿:RollingGo 只读 CLI 取实时报价,无价格源时如实标注不编造
- 服务端内存缓存(指纹 + TTL + LRU),数据源层与 HTTP 层分别缓存
- 上下文瘦身:逐日天气只给 rank1,紧凑 JSON,实测约 1335 token
2026-11-05 20:25:03 +08:00

131 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/* 真实浏览器诊断:跑完整三步,dump 路线图 DOM 与住宿区 DOM */
const fs = require('fs');
const path = require('path');
const OUT = path.join(__dirname, '..', 'preview');
const BASE = process.env.BASE || 'http://127.0.0.1:8788';
const AMAP = process.env.AMAP_KEY || '';
const sleep = (ms) => 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 { webSocketDebuggerUrl } = await (await fetch('http://127.0.0.1:9333/json/version')).json();
const browser = await CDP.connect(webSocketDebuggerUrl);
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: 1360, height: 1800, deviceScaleFactor: 1, mobile: false });
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;
};
// 预置设置(含高德 Key),这样能跑通真实底图与周边 POI
await S('Page.navigate', { url: BASE });
await sleep(2500);
if (AMAP) {
await evalJs(`(() => {
const raw = JSON.parse(localStorage.getItem('tripsense.ai') || '{}');
raw.amapKey = ${JSON.stringify(AMAP)};
localStorage.setItem('tripsense.ai', JSON.stringify(raw));
return true;
})()`);
await S('Page.reload');
await sleep(2500);
console.log('amap key injected:', AMAP ? AMAP.slice(0, 6) + '…' : '(none)');
}
// 出发地
await evalJs(`(() => { const el = document.querySelector('#originInput'); el.value = '柳州'; el.dispatchEvent(new Event('input', { bubbles: true })); return true; })()`);
for (let i = 0; i < 20; i++) {
await sleep(1000);
const ok = await evalJs(`(() => { const it = document.querySelector('#suggestBox .suggest-item'); if (it && !document.querySelector('#suggestBox').classList.contains('hidden')) { it.click(); return true; } return false; })()`);
if (ok) break;
}
console.log('origin =', await evalJs(`document.querySelector('#originInput')?.value`));
// 分析
await evalJs(`document.querySelector('#analyzeBtn')?.click()`);
for (let i = 0; i < 40; i++) {
await sleep(1500);
const n = await evalJs(`document.querySelectorAll('.dest-card').length`);
if (n) { console.log('candidates =', n); break; }
if (i === 39) console.log('candidates = 0 (超时)');
}
console.log('alerts =', await evalJs(`(document.querySelector('#alerts')?.innerText || '').slice(0, 300)`));
// 选 3 个
await evalJs(`(() => { const b = [...document.querySelectorAll('.dest-card .pick-btn')].slice(0,3); b.forEach(x => x.click()); return b.length; })()`);
await sleep(2000);
// 进入第三步(否则面板 display:none,getBoundingClientRect 全是 0)
await evalJs(`document.querySelector('#toStep3')?.click()`);
await sleep(1200);
console.log('step3 active =', await evalJs(`document.querySelector('#panel3')?.classList.contains('active')`));
// 生成方案
await evalJs(`document.querySelector('#planBtn')?.click()`);
for (let i = 0; i < 30; i++) {
await sleep(1500);
const t = await evalJs(`document.querySelector('#outputMeta')?.textContent || ''`);
if (t.includes('生成完成') || t.includes('中断')) { console.log('outputMeta =', t); break; }
if (i === 29) console.log('outputMeta =', t);
}
await sleep(4000);
// ===== 关键诊断:路线图 =====
console.log('\n=== 路线图诊断 ===');
console.log('planExtra hidden =', await evalJs(`document.querySelector('#planExtra')?.classList.contains('hidden')`));
console.log('routeNote =', JSON.stringify(await evalJs(`document.querySelector('#routeNote')?.textContent`)));
console.log('routeMap children =', await evalJs(`document.querySelector('#routeMap')?.children.length`));
console.log('routeMap html (600) =', JSON.stringify(await evalJs(`(document.querySelector('#routeMap')?.innerHTML||'').slice(0,600)`)));
console.log('routeMap box =', JSON.stringify(await evalJs(`(() => { const e = document.querySelector('#routeMap'); if(!e) return null; const r = e.getBoundingClientRect(); return {w: Math.round(r.width), h: Math.round(r.height)}; })()`)));
console.log('svg count =', await evalJs(`document.querySelectorAll('#routeMap svg').length`));
console.log('img count =', await evalJs(`document.querySelectorAll('#routeMap img').length`));
// ===== 住宿诊断 =====
console.log('\n=== 住宿诊断 ===');
console.log('stayNote =', JSON.stringify(await evalJs(`document.querySelector('#stayNote')?.textContent`)));
console.log('hotelBox (900) =', JSON.stringify(await evalJs(`(document.querySelector('#hotelBox')?.innerText||'').slice(0,900)`)));
// 截图:先整页,再分别截路线图与住宿卡
await evalJs(`document.querySelector('#planExtra')?.scrollIntoView({block:'start'})`);
await sleep(1200);
const shot = await S('Page.captureScreenshot', { format: 'png' });
if (shot.result?.data) fs.writeFileSync(path.join(OUT, 'preview-diag.png'), Buffer.from(shot.result.data, 'base64'));
const clipOf = async (sel, file) => {
const box = await evalJs(`(() => { const e = document.querySelector(${JSON.stringify(sel)}); if(!e) return null; const r = e.getBoundingClientRect(); return {x:r.x+window.scrollX,y:r.y+window.scrollY,width:r.width,height:r.height}; })()`);
if (!box || box.width < 2) { console.log('no box for', sel); return; }
const s = await S('Page.captureScreenshot', { format: 'png', clip: { ...box, scale: 1 } });
if (s.result?.data) fs.writeFileSync(path.join(OUT, file), Buffer.from(s.result.data, 'base64'));
};
await clipOf('#routeMap', 'preview-diag-route.png');
await clipOf('#hotelBox', 'preview-diag-hotel.png');
console.log('\nscreenshots saved to preview/');
browser.ws.close();
process.exit(0);
})().catch((e) => { console.error('FAIL', e.message); process.exit(1); });