零依赖 Node 服务,三步流程(选出发地 → 分析候选 → 生成方案 + 路线图 + 住宿参考)。 - 候选发现:高德 place/text 类型词跨城检索 + 内置景区/城市/小众库 + Photon 兜底 - 区县级「排除同城」判定(adname),过滤发生在轮转取样之前 - 路线图画的是 AI 方案里实际安排的每个地点,按正文顺序编号 + 虚线动线 + 真实高德底图 - 住宿:RollingGo 只读 CLI 取实时报价,无价格源时如实标注不编造 - 服务端内存缓存(指纹 + TTL + LRU),数据源层与 HTTP 层分别缓存 - 上下文瘦身:逐日天气只给 rank1,紧凑 JSON,实测约 1335 token
118 lines
4.5 KiB
JavaScript
118 lines
4.5 KiB
JavaScript
'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); });
|