TripSense 行知 · 周边出行顾问:首版
零依赖 Node 服务,三步流程(选出发地 → 分析候选 → 生成方案 + 路线图 + 住宿参考)。 - 候选发现:高德 place/text 类型词跨城检索 + 内置景区/城市/小众库 + Photon 兜底 - 区县级「排除同城」判定(adname),过滤发生在轮转取样之前 - 路线图画的是 AI 方案里实际安排的每个地点,按正文顺序编号 + 虚线动线 + 真实高德底图 - 住宿:RollingGo 只读 CLI 取实时报价,无价格源时如实标注不编造 - 服务端内存缓存(指纹 + TTL + LRU),数据源层与 HTTP 层分别缓存 - 上下文瘦身:逐日天气只给 rank1,紧凑 JSON,实测约 1335 token
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
'use strict';
|
||||
/* 缓存命中验证:连续两次「分析周边目的地」,第二次应秒回并出现缓存提示条 */
|
||||
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: 1400, 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;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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`));
|
||||
|
||||
const runAnalyze = async (label) => {
|
||||
const t0 = Date.now();
|
||||
await evalJs(`(() => { const cards = document.querySelectorAll('.dest-card'); cards.forEach(c => c.remove()); const b = document.querySelector('#analyzeBtn'); if (b) b.click(); return true; })()`);
|
||||
let n = 0;
|
||||
for (let i = 0; i < 40; i++) {
|
||||
await sleep(500);
|
||||
n = await evalJs(`document.querySelectorAll('.dest-card').length`);
|
||||
if (n) break;
|
||||
}
|
||||
const ms = Date.now() - t0;
|
||||
console.log(`\n[${label}] candidates=${n} elapsed=${ms}ms`);
|
||||
console.log(' progress =', JSON.stringify(await evalJs(`(document.querySelector('#progress')?.innerText||'').slice(0,200)`)));
|
||||
console.log(' cacheBar hidden =', await evalJs(`document.querySelector('#cacheBar')?.classList.contains('hidden')`));
|
||||
console.log(' cacheBar text =', JSON.stringify(await evalJs(`(document.querySelector('#cacheBar')?.innerText||'').slice(0,300)`)));
|
||||
return ms;
|
||||
};
|
||||
|
||||
const cold = await runAnalyze('第一次(冷)');
|
||||
await sleep(1500);
|
||||
const warm = await runAnalyze('第二次(应命中缓存)');
|
||||
|
||||
// 点「重新获取最新数据」
|
||||
await evalJs(`document.querySelector('#cacheBar button, #cacheBar .link-btn')?.click()`);
|
||||
for (let i = 0; i < 60; i++) {
|
||||
await sleep(500);
|
||||
const hit = await evalJs(`document.querySelector('#cacheBar')?.classList.contains('hidden')`);
|
||||
if (hit) break;
|
||||
}
|
||||
console.log('\n=== 强制刷新后 ===');
|
||||
console.log(' cacheBar hidden =', await evalJs(`document.querySelector('#cacheBar')?.classList.contains('hidden')`));
|
||||
console.log(' cacheBar text =', JSON.stringify(await evalJs(`(document.querySelector('#cacheBar')?.innerText||'').slice(0,200)`)));
|
||||
console.log(' progress =', JSON.stringify(await evalJs(`(document.querySelector('#progress')?.innerText||'').slice(0,120)`)));
|
||||
console.log(' candidates =', await evalJs(`document.querySelectorAll('.dest-card').length`));
|
||||
|
||||
// 服务端缓存统计
|
||||
const health = await (await fetch(BASE + '/api/health')).json();
|
||||
console.log('\nserver cache =', JSON.stringify(health.cache));
|
||||
|
||||
const shot = await S('Page.captureScreenshot', { format: 'png' });
|
||||
if (shot.result?.data) fs.writeFileSync(path.join(OUT, 'preview-cache.png'), Buffer.from(shot.result.data, 'base64'));
|
||||
console.log('screenshot -> preview/preview-cache.png');
|
||||
browser.ws.close();
|
||||
process.exit(0);
|
||||
})().catch((e) => { console.error('FAIL', e.message); process.exit(1); });
|
||||
@@ -0,0 +1,130 @@
|
||||
'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); });
|
||||
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
/* 对比两次 analyze 请求体,定位缓存指纹不稳定的原因 */
|
||||
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');
|
||||
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;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
// 拦截 fetch,记录 /api/analyze 的请求体
|
||||
await evalJs(`(() => {
|
||||
window.__bodies = [];
|
||||
const of = window.fetch;
|
||||
window.fetch = async function (u, o) {
|
||||
if (String(u).includes('/api/analyze') && o && o.body) window.__bodies.push(o.body);
|
||||
return of.apply(this, arguments);
|
||||
};
|
||||
return true;
|
||||
})()`);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
await evalJs(`(() => { const b = document.querySelector('#analyzeBtn'); if (b) b.click(); return true; })()`);
|
||||
for (let k = 0; k < 40; k++) {
|
||||
await sleep(500);
|
||||
const n = await evalJs(`document.querySelectorAll('.dest-card').length`);
|
||||
if (n) break;
|
||||
}
|
||||
await sleep(1500);
|
||||
console.log(`run ${i} done, cards =`, await evalJs(`document.querySelectorAll('.dest-card').length`));
|
||||
}
|
||||
|
||||
const bodies = await evalJs(`window.__bodies`);
|
||||
console.log('\n=== captured analyze bodies ===');
|
||||
(bodies || []).forEach((b, i) => console.log(`[${i}]`, b));
|
||||
if ((bodies || []).length >= 2) {
|
||||
console.log('\nidentical =', bodies[0] === bodies[1]);
|
||||
}
|
||||
browser.ws.close();
|
||||
process.exit(0);
|
||||
})().catch((e) => { console.error('FAIL', e.message); process.exit(1); });
|
||||
@@ -0,0 +1,133 @@
|
||||
'use strict';
|
||||
/* 验收脚本:真实场景(南宁青秀区出发 → 柳州为目的地)→ 选柳州 → 生成方案 → 检查路线图 */
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const OUT = path.join(__dirname, '..', 'preview');
|
||||
const BASE = process.env.BASE || 'http://127.0.0.1:8791';
|
||||
const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
|
||||
|
||||
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
||||
|
||||
async function getWsUrl() {
|
||||
const res = await fetch('http://127.0.0.1:9333/json/version');
|
||||
return (await res.json()).webSocketDebuggerUrl;
|
||||
}
|
||||
|
||||
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 browser = await CDP.connect(await getWsUrl());
|
||||
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: 1400, height: 1700, 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, 500));
|
||||
return r.result?.result?.value;
|
||||
};
|
||||
|
||||
// 1) 直接注入出发地坐标(河池市金城江区),绕过联想
|
||||
await evalJs(`(() => {
|
||||
state.origin = { name:'河池市金城江区', label:'广西壮族自治区 · 河池市 · 金城江区', lat:24.69562, lon:108.08091, admin1:'广西壮族自治区', country:'中国' };
|
||||
document.querySelector('#originInput').value = '河池市金城江区';
|
||||
const r = document.querySelector('#radius');
|
||||
if (r) { r.value = '250'; r.dispatchEvent(new Event('change',{bubbles:true})); }
|
||||
return true;
|
||||
})()`);
|
||||
await evalJs(`document.querySelector('#analyzeBtn')?.click()`);
|
||||
// 分析要跑 20~60s(高德检索 + 天气)
|
||||
for (let i = 0; i < 60; i += 1) {
|
||||
await sleep(2000);
|
||||
const n = await evalJs(`document.querySelectorAll('.dest-card').length`);
|
||||
if (n > 0) { console.log('candidates =', n, 'after', (i + 1) * 2, 's'); break; }
|
||||
}
|
||||
|
||||
// 2) 找到「柳州市」并勾选它(真实场景:目的地是柳州)
|
||||
const picked = await evalJs(`(() => {
|
||||
const cards = [...document.querySelectorAll('.dest-card')];
|
||||
const hit = cards.find(c => (c.innerText||'').includes('柳州市'));
|
||||
if (!hit) return 'NO-LIUZHOU';
|
||||
const btn = hit.querySelector('.pick-btn');
|
||||
btn.click();
|
||||
return 'clicked';
|
||||
})()`);
|
||||
console.log('pick 柳州市 =', picked);
|
||||
await sleep(1500);
|
||||
console.log('selected =', await evalJs(`document.querySelector('#selectedCount')?.textContent`));
|
||||
|
||||
// 3) 生成方案
|
||||
await evalJs(`document.querySelector('#planBtn')?.click()`);
|
||||
for (let i = 0; i < 60; i += 1) {
|
||||
await sleep(2000);
|
||||
const t = await evalJs(`(document.querySelector('#planOutput')?.innerText||'').length`);
|
||||
if (t > 200) { console.log('plan chars =', t, 'after', (i + 1) * 2, 's'); break; }
|
||||
}
|
||||
await sleep(4000);
|
||||
|
||||
// 4) 检查路线图状态
|
||||
const route = await evalJs(`(() => ({
|
||||
note: document.querySelector('#routeNote')?.textContent || '',
|
||||
legend: document.querySelector('#routeMap .map-legend')?.textContent || '',
|
||||
hasImg: !!document.querySelector('#routeMap img'),
|
||||
circles: document.querySelectorAll('#routeMap .map-overlay circle').length,
|
||||
polyline: !!document.querySelector('#routeMap .map-overlay polyline'),
|
||||
names: [...document.querySelectorAll('#routeMap .map-overlay .map-name')].map(t=>t.textContent),
|
||||
hints: [...document.querySelectorAll('#routeMap .map-hint')].map(t=>t.textContent.slice(0,90)),
|
||||
routePoints: (state.routePoints||[]).map(p=>p.name+'@'+p.lat+','+p.lon),
|
||||
missing: state.routeMissing || [],
|
||||
}))()`);
|
||||
console.log('ROUTE =', JSON.stringify(route, null, 1));
|
||||
|
||||
await evalJs(`gotoStep(3)`);
|
||||
await sleep(1200);
|
||||
await evalJs(`document.querySelector('#routeMap')?.scrollIntoView({block:'center'})`);
|
||||
await sleep(1200);
|
||||
const shot = await S('Page.captureScreenshot', { format: 'png' });
|
||||
fs.writeFileSync(path.join(OUT, 'route-plan.png'), Buffer.from(shot.result.data, 'base64'));
|
||||
console.log('screenshot -> preview/route-plan.png');
|
||||
|
||||
// 单独截路线图区域(clip 按元素文档坐标截,不依赖滚动位置)
|
||||
const box = await evalJs(`(() => {
|
||||
const el = document.querySelector('#routeMap');
|
||||
if (!el) return null;
|
||||
const r = el.getBoundingClientRect();
|
||||
return { x: r.left + window.scrollX, y: r.top + window.scrollY, width: Math.round(r.width), height: Math.round(r.height) };
|
||||
})()`);
|
||||
console.log('clip box =', JSON.stringify(box));
|
||||
if (box && box.height > 20) {
|
||||
const shot2 = await S('Page.captureScreenshot', {
|
||||
format: 'png',
|
||||
captureBeyondViewport: true,
|
||||
clip: { x: Math.max(0, box.x - 8), y: Math.max(0, box.y - 8), width: box.width + 16, height: box.height + 16, scale: 1 },
|
||||
});
|
||||
fs.writeFileSync(path.join(OUT, 'route-map.png'), Buffer.from(shot2.result.data, 'base64'));
|
||||
console.log('screenshot -> preview/route-map.png');
|
||||
}
|
||||
|
||||
browser.ws.close();
|
||||
process.exit(0);
|
||||
})().catch((e) => { console.error('FAIL', e.message); process.exit(1); });
|
||||
@@ -0,0 +1,117 @@
|
||||
'use strict';
|
||||
/* 临时验收脚本:走 CDP 打开首页 → 填表 → 分析 → 选目的地 → 生成方案 → 截图 */
|
||||
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:8788';
|
||||
const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
|
||||
|
||||
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
||||
|
||||
async function getWsUrl() {
|
||||
const port = 9333;
|
||||
const res = await fetch(`http://127.0.0.1:${port}/json/version`);
|
||||
const j = await res.json();
|
||||
return j.webSocketDebuggerUrl;
|
||||
}
|
||||
|
||||
class CDP {
|
||||
constructor(ws) { this.ws = ws; this.id = 0; this.waits = new Map(); this.events = []; }
|
||||
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); }
|
||||
else if (m.method) c.events.push(m);
|
||||
};
|
||||
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 wsUrl = await getWsUrl();
|
||||
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: 1600, 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));
|
||||
return r.result?.result?.value;
|
||||
};
|
||||
|
||||
// 填出发地
|
||||
await evalJs(`(() => {
|
||||
const el = document.querySelector('#originInput');
|
||||
el.value = '柳州';
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
return true;
|
||||
})()`);
|
||||
// 等联想(网络有波动,最多等 20s)
|
||||
let picked = false;
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
await sleep(1000);
|
||||
picked = await evalJs(`(() => {
|
||||
const it = document.querySelector('#suggestBox .suggest-item');
|
||||
if (it && !document.querySelector('#suggestBox').classList.contains('hidden')) { it.click(); return true; }
|
||||
return false;
|
||||
})()`);
|
||||
if (picked) break;
|
||||
}
|
||||
console.log('suggest picked =', picked, '| box =', await evalJs(`document.querySelector('#suggestBox')?.textContent?.slice(0,60)`));
|
||||
await sleep(1500);
|
||||
const originName = await evalJs(`document.querySelector('#originInput')?.value`);
|
||||
console.log('origin =', originName);
|
||||
|
||||
// 点击分析
|
||||
await evalJs(`document.querySelector('#analyzeBtn')?.click()`);
|
||||
await sleep(12000);
|
||||
const candCount = await evalJs(`document.querySelectorAll('.dest-card').length`);
|
||||
console.log('candidates =', candCount);
|
||||
|
||||
// 选前 3 个
|
||||
await evalJs(`(() => {
|
||||
const btns = [...document.querySelectorAll('.dest-card .pick-btn')].slice(0,3);
|
||||
btns.forEach(b => b.click());
|
||||
return btns.length;
|
||||
})()`);
|
||||
await sleep(2000);
|
||||
console.log('selected =', await evalJs(`document.querySelector('#selectedCount')?.textContent`));
|
||||
|
||||
// 生成方案
|
||||
await evalJs(`document.querySelector('#planBtn')?.click()`);
|
||||
await sleep(12000);
|
||||
const meta = await evalJs(`document.querySelector('#outputMeta')?.textContent`);
|
||||
console.log('outputMeta =', meta);
|
||||
console.log('plan head =', await evalJs(`(document.querySelector('#planOutput')?.innerText||'').slice(0,80)`));
|
||||
|
||||
await evalJs(`document.querySelector('#panel3')?.scrollIntoView()`);
|
||||
await sleep(800);
|
||||
const shot = await S('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false });
|
||||
if (!shot.result || !shot.result.data) throw new Error('no screenshot data: ' + JSON.stringify(shot).slice(0, 300));
|
||||
fs.writeFileSync(path.join(OUT, 'preview-ctx-token.png'), Buffer.from(shot.result.data, 'base64'));
|
||||
console.log('screenshot saved');
|
||||
|
||||
browser.ws.close();
|
||||
process.exit(0);
|
||||
})().catch((e) => { console.error('FAIL', e.message); process.exit(1); });
|
||||
Reference in New Issue
Block a user