Files
tripsense/tools/verify-route-map-followup.cjs
T
tamakyi 1f902bc49b 修复 AI 追加轮后路线示意只剩一个点
根因: runPlan 追加轮把 state.nearby 置 null, payload.nearby 随之为空, 模型第二版不再点名具体地点; extractPlanPlaces 的 nearby 来源(带坐标)空了, 加粗词又无坐标, 无 Key 时全被判 missing -> 只剩 1 个已选目的地。

- 追加轮沿用 state.nearby 并继续放进上下文; 提示词要求保留加粗点名
- extractPlanPlaces(text, inherit) 继承上一版坐标 + 否定语境护栏
- planVersions 存 routePoints, 版本切换/回到初版/载入保存方案同步重绘
- previous 改用 currentPlanText(), drawRouteMap 用 state.routeMissing
- 新增 test-route-map-followup.cjs / verify-route-map-followup.cjs 与 README 说明
2026-11-06 09:58:06 +08:00

246 lines
9.3 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';
/**
* 端到端验收:AI 追加轮之后「行程路线示意」不能只剩一个点。
*
* 用真实无头 Chrome 打开站点,注入一份 mock 分析结果与两版方案正文,
* 直接调用页面里的 refreshRouteFromText / drawRouteMap,读真实 DOM:
* - 初版:多点点
* - 复现修复前(nearby 清空、不继承):只剩 1 个
* - 修复后(nearby 沿用 + 继承上一版坐标):恢复多点
* - 切换回第 1 版:路线图跟着版本重绘
*
* 用法:node tools/verify-route-map-followup.cjs
*/
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
const OUT = path.join(__dirname, '..', 'preview');
const CDP_PORT = 9361;
const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
function sleep(ms) { return 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 }));
});
}
}
const LAT = 24.32;
const LON = 109.42;
const pt = (name, i) => ({
name,
lat: +(LAT + (i % 5) * 0.004).toFixed(4),
lon: +(LON + Math.floor(i / 5) * 0.004).toFixed(4),
type: '地点',
});
const SIGHT_NAMES = ['龙潭公园', '马鞍山公园', '窑埠古镇', '柳侯公园', '程阳八寨景区'];
const FOOD_NAMES = ['螺蛳粉老店', '青云菜市', '五星步行街', '水上公交码头', '龙潭公园餐厅'];
const NEARBY = {
area: '柳州市',
radiusKm: 8,
sights: SIGHT_NAMES.map((n, i) => pt(n, i)),
food: FOOD_NAMES.map((n, i) => pt(n, i + 5)),
};
const ANALYSIS = {
origin: { name: '河池市金城江区', lat: 24.69562, lon: 108.08091 },
candidates: [{ id: 'd1', name: '柳州市', lat: LAT, lon: LON, score: 9.1 }],
timeRange: { start: '2026-11-20', end: '2026-11-21', days: 2 },
dataNotes: [],
};
const V1 = `# 柳州两日行程
## 方案总览
两天都在柳州市区与近郊,节奏适中。
## 逐日安排
### 第 1 天
上午去 **龙潭公园**、**马鞍山公园**,中午在 **螺蛳粉老店** 吃饭;
下午逛 **窑埠古镇**、**柳侯公园**,傍晚到 **五星步行街** 与 **青云菜市**。
### 第 2 天
去 **程阳八寨景区**、**水上公交码头**,晚上在 **龙潭公园餐厅** 收尾。
`;
const V2 = `# 柳州两日行程(第 2 版:少走路)
## 方案总览
按「少走路」重排:第 1 天压缩成市区一圈,第 2 天只留一个远点。
## 逐日安排
### 第 1 天
上午去龙潭公园、马鞍山公园,中午在螺蛳粉老店吃饭;
下午逛窑埠古镇与柳侯公园,傍晚到五星步行街。
### 第 2 天
只保留程阳八寨景区,删掉水上公交码头与青云菜市(走路太多)。
`;
let server = null;
let chrome = null;
(async () => {
if (!fs.existsSync(CHROME)) { console.error('找不到 Chrome:' + CHROME); process.exit(1); }
fs.mkdirSync(OUT, { recursive: true });
// ---- 服务端(端口可能被占用自动顺延,从启动日志里读真实端口)
server = spawn(process.execPath, ['server.js'], {
cwd: path.join(__dirname, '..'),
stdio: ['ignore', 'pipe', 'pipe'],
});
let banner = '';
server.stdout.on('data', (d) => { banner += d.toString(); });
server.stderr.on('data', (d) => { banner += d.toString(); });
await sleep(2800);
const m = banner.match(/http:\/\/127\.0\.0\.1:(\d+)/);
if (!m) { console.error('服务端未打印监听地址:' + banner); process.exit(1); }
const base = `http://127.0.0.1:${m[1]}`;
console.log('服务端 =', base);
// ---- 无头 Chrome + CDP
chrome = spawn(CHROME, [
'--headless=new', '--disable-gpu', '--hide-scrollbars',
`--remote-debugging-port=${CDP_PORT}`,
'--user-data-dir=' + path.join(os.tmpdir(), 'tripverifyroute' + Date.now()),
'about:blank',
], { stdio: 'ignore' });
let wsUrl = null;
for (let i = 0; i < 30 && !wsUrl; i += 1) {
await sleep(500);
try {
const res = await fetch(`http://127.0.0.1:${CDP_PORT}/json/version`);
wsUrl = (await res.json()).webSocketDebuggerUrl;
} catch (e) { /* 还没起来 */ }
}
if (!wsUrl) { console.error('Chrome 未启动'); process.exit(1); }
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 = (method, params) => browser.send(method, params, sessionId);
await S('Page.enable');
await S('Runtime.enable');
await S('Emulation.setDeviceMetricsOverride', { width: 1280, height: 1400, 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;
};
// 初始化 mock 状态;强制走「方位示意」分支,保证 SVG 里能数到编号点
await evalJs(`(() => {
state.analysis = ${JSON.stringify(ANALYSIS)};
state.selected = new Set(['d1']);
state.nearby = ${JSON.stringify(NEARBY)};
state.ai.amapKey = '';
if (!state.config) state.config = {};
state.config.features = { ...(state.config.features || {}), amapRoad: false };
if (typeof gotoStep === 'function') gotoStep(3);
window.__V1 = ${JSON.stringify(V1)};
window.__V2 = ${JSON.stringify(V2)};
window.__ver1 = { n: 1, text: window.__V1, routePoints: null, routeMissing: [] };
window.__ver2 = { n: 2, text: window.__V2, routePoints: null, routeMissing: [] };
return true;
})()`);
const readMap = `(() => ({
points: (state.routePoints || []).length,
dots: document.querySelectorAll('#routeMap .rm-idx').length,
note: document.querySelector('#routeNote')?.textContent || '',
hasSvg: !!document.querySelector('#routeMap .route-svg'),
}))()`;
// 1) 初版
const first = await evalJs(`(async () => {
await refreshRouteFromText(window.__V1, null, window.__ver1);
await drawRouteMap();
return ${readMap};
})()`);
console.log('初版 →', first);
// 2) 复现修复前:追加轮把 nearby 清空、不继承上一版坐标
const broken = await evalJs(`(async () => {
state.nearby = null;
await refreshRouteFromText(window.__V2, null, null);
await drawRouteMap();
return ${readMap};
})()`);
console.log('修复前追加轮 →', broken);
// 3) 修复后:nearby 沿用 + 继承上一版坐标
const fixed = await evalJs(`(async () => {
state.nearby = ${JSON.stringify(NEARBY)};
await refreshRouteFromText(window.__V2, window.__ver1.routePoints, window.__ver2);
await drawRouteMap();
return ${readMap};
})()`);
console.log('修复后追加轮 →', fixed);
// 4) 通过真实下拉事件切回第 1 版,路线图应跟着重绘
await evalJs(`(() => {
state.planVersions = [window.__ver1, window.__ver2];
state.viewing = 1;
renderVersionBar();
const sel = document.querySelector('#versionSelect');
sel.value = 'v0';
sel.dispatchEvent(new Event('change', { bubbles: true }));
return true;
})()`);
await sleep(400);
const switched = await evalJs(readMap);
console.log('切回第 1 版 →', switched);
// 截图:路线图区域
const box = await evalJs(`(() => {
const r = document.querySelector('#routeMap').getBoundingClientRect();
return { x: r.x + scrollX, y: r.y + scrollY, width: Math.max(r.width, 640), height: Math.max(r.height, 380) };
})()`);
const shot = await S('Page.captureScreenshot', {
format: 'png', captureBeyondViewport: true,
clip: { ...box, scale: 1 },
});
const shotPath = path.join(OUT, 'preview-route-followup.png');
fs.writeFileSync(shotPath, Buffer.from(shot.result.data, 'base64'));
console.log('截图已保存 preview/preview-route-followup.png');
const fails = [];
if (first.dots < 2) fails.push('初版路线图点数不足');
if (broken.dots !== 1) fails.push('未能复现「只剩一个点」(期望 1,实际 ' + broken.dots + ')');
if (fixed.dots < 2) fails.push('修复后追加轮仍只有一个点');
if (fixed.points <= broken.points) fails.push('修复后路线点数没有增加');
if (switched.dots < 2) fails.push('切换版本后路线图未重绘');
if (fails.length) {
console.error('\n失败:' + fails.join(';'));
process.exitCode = 1;
} else {
console.log('\n✅ 端到端验收通过:追加轮仍有多个点,版本切换会重绘');
}
})().catch((e) => {
console.error(e.stack || e.message);
process.exitCode = 1;
}).finally(() => {
if (chrome) { try { chrome.kill(); } catch (e) { /* ignore */ } }
if (server) { try { server.kill(); } catch (e) { /* ignore */ } }
});