Files
tripsense/tools/cache-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

109 lines
5.2 KiB
JavaScript

'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); });