修复 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 说明
This commit is contained in:
2026-11-06 09:58:06 +08:00
parent 7ad86c6619
commit 1f902bc49b
6 changed files with 551 additions and 23 deletions
+32 -6
View File
@@ -183,18 +183,20 @@ node tools/build-admin-dv.cjs C:/tmp/datav-admin.json
#### 离线回归测试
归属逻辑不能只靠真实网络验证——Photon / Overpass 随时会挂,挂了就没法回归。仓库里有三个**不发外部请求**的离线测试:
归属逻辑不能只靠真实网络验证——Photon / Overpass 随时会挂,挂了就没法回归。仓库里有**不发外部请求**的离线测试:
```bash
node tools/test-admin-lookup.cjs # 归属判定单测:同名县消歧、已知边界点、狭长县远端点
node tools/test-overpass-fallback.cjs # 起本地假 Overpass:验兜底路径、简称归一、200+HTML 错误页
node tools/test-grouping-regression.cjs # 起本地假 Photon:20 个候选的完整分组回归
node tools/test-admin-lookup.cjs # 归属判定单测:同名县消歧、已知边界点、狭长县远端点
node tools/test-overpass-fallback.cjs # 起本地假 Overpass:验兜底路径、简称归一、200+HTML 错误页
node tools/test-grouping-regression.cjs # 起本地假 Photon:20 个候选的完整分组回归
node tools/test-route-map-followup.cjs # vm 里跑真实 extractPlanPlaces:初版/追加轮两版路线点对比 + 源码断言
```
另有端到端 UI 验收(起假 Photon + 真实无头 Chrome,读真实 DOM):
另有端到端 UI 验收(真实无头 Chrome,读真实 DOM):
```bash
node tools/verify-ui-full.cjs # 分组 / undefined / 空组名 / 搜索 / 折叠 + 截图
node tools/verify-ui-full.cjs # 起假 Photon:分组 / undefined / 空组名 / 搜索 / 折叠 + 截图
node tools/verify-route-map-followup.cjs # 追加轮路线图点数(1 → 9)+ 版本切换重绘 + 截图
```
写这类测试时注意:**`cfg.SEARCH.maxCandidates` 缺省会静默返回 0 候选**(`selected.length < undefined` 恒假);**第二轮跑要清 `require.cache` 或换 origin**,否则命中第一轮缓存。
@@ -443,6 +445,25 @@ rgh status # 确认已登录
- 无 Key 时降级为按真实经纬度等距投影(经度按 `cos(lat)` 修正)的示意图。
- 地图服务商只使用合规白名单(高德 / 腾讯 / 百度 / 天地图),不使用 Google / Apple / Mapbox / OSM 直连瓦片。
**追加轮不能塌成一个点**(用户反馈后修复)
现象:与 AI 优化改变行程后,第二次「行程路线示意」只剩一个点。根因不在地图,在**多轮数据流**:
- `runPlan()` 里只有 `!isFollowUp` 才查 `/api/nearby-pois`,末尾又无条件 `state.nearby = nearby`——追加轮 `nearby` 为 `null`,`payload.nearby` 也是 `null`。模型上下文里没有「可点名的地点」,第二版退化成「某片区 + 自行核实」,甚至不再重复加粗点名。
- `extractPlanPlaces()` 的三类来源里,`nearby`(带真实坐标)空了;加粗词常只剩说明性词,且**不带坐标**;没配高德 Key 时 `resolveRoutePoints()` 把它们全部判为 missing 丢掉 → 只剩「已选目的地」,而用户常常只勾了 1 个 → 图上孤零零一个点。
修复是组合拳,缺一不可:
1. **追加轮沿用 `state.nearby`**:`else if (state.nearby && shortlist.some(c => c.name === state.nearby.area)) nearby = state.nearby;`,`payload.nearby` 继续带上;已选目的地变了才失效。
2. **提示词约束**:追加轮末条 user 消息要求「上一版加粗点名的具体地点继续写出名称,不要退化成某片区/自行核实;新增地点从 `nearby` / `shortlist` 取真实名并同样加粗」。
3. **继承上一版坐标**:`extractPlanPlaces(text, inherit)` 多一个继承池,只把「本版正文里仍出现」且**不在否定语境**里的点补回坐标;`push()` 支持「带坐标的同名点覆盖只有名字的加粗词」。
4. **路线点属于版本**:`planVersions[i]` 存 `routePoints` / `routeMissing`,`#versionSelect` 切换、「回到初版」、载入保存方案都同步重绘;`drawRouteMap()` 改用 `state.routeMissing` 真正显示「有 N 个地点未能定位」。
5. **底稿取当前版本**:`previous` 从 `state.planText` 改为 `currentPlanText()`,查看历史版时不会基于最后一版调整。
否定语境护栏(`删掉|取消|去掉|不再|…` + 名字前 12 字)同时作用于 `nearby` 沿用与继承两条路径:模型按要求在「风险与备选」里写「删掉 XX」时,该地点不会被画回图上。
实测(`tools/verify-route-map-followup.cjs`,真实 Chrome):初版 11 点 / 图上 9(抽稀上限);**修复前追加轮 1 点 / 图上 1**;修复后 9 点 / 图上 9;切回第 1 版重绘为 11 点 / 图上 9。
### 多轮调整
方案不是一次性的。生成完成后,输出框下方会出现输入条,写一句要求(「第二天改成室内为主」「总预算压到 1200」)点「继续优化」即可迭代。
@@ -451,6 +472,8 @@ rgh status # 确认已登录
- 输出区顶部显示「第 N 版」,右侧「回到初版」可退回第一版做对比。
- 演示模式下规则引擎无法真正理解增量需求,会在开头注明并重新生成通用方案,不会假装已经改好。
- 上一版超过 12000 字会被截断,避免把上下文撑爆;`previous` 只用于多轮消息,不会重复塞进上下文 JSON。
- 追加轮**沿用第一轮查到的 `nearby` 周边地点**(已选目的地没变时),并把它继续放进上下文 JSON;清空会让第二版失去可点名的地点,路线图塌成一个点。详见上文「路线图 → 追加轮不能塌成一个点」。
- 每一版都存自己的 `routePoints`,切换版本 / 回到初版 / 载入保存方案时路线图同步重绘;`previous` 用 `currentPlanText()`,保证「正在看哪一版就基于哪一版改」。
### AI 接口自检
@@ -743,9 +766,12 @@ tripsense/
- **缓存失效的根因(隐蔽)**:`$('#analyzeBtn').addEventListener('click', runAnalyze)` 会把 **Event 对象**传进 `runAnalyze(force)`,而 `Boolean(event)` 恒为 `true` —— 于是每一次点击都是「强制刷新」,缓存永远不命中。改为只认严格的 `force === true`,并把监听器包成 `() => runAnalyze()`。实测:冷启动 12.8s → 第二次 518ms(24 倍),提示条正确显示;点「重新获取最新数据」后提示条正确隐藏。
- **反向地理编码的假地名**:`reverseGeocode` 彻底失败时兜底返回「当前位置」这个**假地名**,拿它去查酒店库必然落空,还会被误判成"该数据源没数据"。新增 `resolveCityName()` 把假地名视为失败并重试一次;数据源层缓存也**不缓存**假地名。
- **缓存分层实测**:`/api/analyze` 层缓存 30 分钟;数据源层(`lib/sources.js`)另有 Photon 分类检索 / 高德 POI / 天气 / AQI / 路网 / 铁路各自的缓存。清空后重建:`discoverCandidates` 首次 8137ms → 第二次 **0ms**;`roadMatrix` 首次 127ms → 第二次 **0ms**。
- **AI 追加轮路线图只剩一个点**:根因是 `runPlan()` 追加轮把 `state.nearby` 置 `null`(`let nearby = null; if (!isFollowUp) {...查 POI...} state.nearby = nearby;`),`payload.nearby` 随之为空;第二版正文不再点名 nearby 地点,`extractPlanPlaces()` 里带坐标的来源 1 空,加粗词又没坐标,无 Key 时全被 `resolveRoutePoints()` 判为 missing 丢掉 → 只剩 1 个已选目的地。修复:追加轮沿用 nearby + 提示词要求保留加粗点名 + `extractPlanPlaces(text, inherit)` 继承上一版坐标(含否定语境护栏)+ 每个版本存 `routePoints` 并在切版本时重绘 + `previous` 改用 `currentPlanText()`。真实 Chrome 验收:初版 11 点 → 修复前追加轮 1 点 → 修复后 9 点 → 切回初版 11 点。另修两个隐性 bug:`drawRouteMap()` 的 `missing` 是永远为空的局部数组,导致「有 N 个地点未能定位」提示从不显示;「回到初版」未同步 `state.viewing`,正文与版本下拉不跟着变。
预览图见 `preview/preview-step1.png`、`preview/preview-step2.png`、`preview/preview-step2-detail.png`(展开逐日天气 + 对比面板)、`preview/preview-step3.png`、`preview/preview-settings.png`(设置弹窗含高德 Key)、`preview/preview-amap-error.png`(Key 无效时的提示)、`preview/preview-map-fallback.png` 与 `preview/preview-map-price.png`(只含目的地的路线图 + 自定义价位 + 住宿价格缺失说明)。
本轮(缓存 + 路线图修复 + 真实浏览器验收)新增:`preview/preview-diag-route.png`(高德真实底图 + 3 个编号标记 + 折线连线)、`preview/preview-diag-hotel.png`(RollingGo 真实报价 ¥2416/晚)、`preview/preview-cache.png`(缓存命中提示条)。
追加轮路线图修复新增:`preview/preview-route-followup.png`(修复后的多点行程路线示意 + 完整行程点清单)。
> 说明:上面这组截图恰好是在 Photon 公共实例全部超时(`stats.fallbackOnly=true`)时拍的,因此 Step 2 顶部能看到降级提示条——这正好是「实时源挂掉时页面怎么表现」的真实记录;天气、AQI、路况仍为实时数据。