路书生成器
把用户的一句话目的地,做成可核实、图文匹配、可打印的本地单文件 HTML 路书。
总原则:可靠性 > 完整性 > 华丽度。 一次信息编造、或图片与途经点对不上的交付,比朴素但可靠的交付差得多。 任何环节出问题都降级交付,绝不停摆。
取数优于回忆。 路线与里程走高德 REST 的查询时点路线计算(国内路网数据最新最全的来源之一), 在地情报走小红书检索(只采信 2 年内 + 同季节的笔记)。 不要凭训练数据里的印象写里程、路况或景区规则 —— 那些信息通常已过时数年。
交付形态:本地单文件 HTML。 不需要部署、不需要服务器、不需要任何账号。 双击即可打开、可直接打印/导出 PDF。不做上线。
四个必须避开的坑(实证)
| 坑 | 后果 | 做法 |
|---|---|---|
| 里程只核总量不核逐段 | 总量仅差 39 km,但单日虚高 198 km、另一日漏算 265 km | 逐段核验 + 单日时长排序(Phase 1、2) |
| 出发地只问到市级 | "市"实为其下辖县,相距 100 km,单段误差 198 km | 强制问到县级(Phase 0) |
| 手填景区坐标 | 某景区纬度错 0.28° ≈ 31 km,当天里程虚高 76 km | POI 坐标必先校准(Phase 2.2) |
| 图片盲取第 N 张即入页面 | 抓到头像 / 攻略信息图 / 与途经点不符的图 | 分级闸门校验(Phase 5,默认轻量档) |
Phase -1 · 环境预检(每次必跑,不可跳过)
开工前先体检,不要跑到一半才发现缺 Key / 没装库。
PY="<你的 python 绝对路径>"
"$PY" scripts/preflight.py --workdir "<路书工作目录>"
输出三类结论:
✅ 全部依赖就位 可做全量核验(含高德 Key 与浏览器工具)
🟡 可以开工(降级模式) 只缺「建议项」—— 照常开工、走备选方案,交付时标注未核实项
❌ 缺少必需项 只有这一档真的挡住你,必须补齐
依赖分三档 —— 只有第一档会挡住你
| 依赖 | 档 | 缺了怎么办 |
|---|---|---|
| Python 3.10+ | 🔴 必需 | WorkBuddy 自带 3.12/3.13/3.14,用管理版绝对路径 |
| 高德 REST Key | 🟡 建议(可跳过) | 见下方「高德 Key 与浏览器工具都是可选的」 |
| 浏览器自动化工具 | 🟡 建议(可跳过) | 同上;任选其一,不锁死实现 |
| Pillow | 🟡 建议(可跳过) | "<python>" -m pip install pillow;或走 Phase 5 A 档不做图 |
| fontTools | ⚪ 可选 | 仅自托管字体子集化需要;用系统字体栈则免 |
高德 Key 与浏览器工具都是可选的
这两样决定的是信息质量,不是能不能开工。开工前顺手问一次,用户不愿意就跳过。
| 依赖 | 提供了能得到什么 | 不提供时的备选 | 代价 |
|---|---|---|---|
| 高德 REST Key | 逐段里程/时长/收费来自路网数据库,精确到 km、可交叉验证 | ① OSRM 公共 API(免 Key,只信里程不信时长) ② 用户自己在地图 App 查,报读数或截图 ③ 通用经验估算 | 里程精度下降;里程/时长全部标 ⏳ 待核实,交付时说明「未做 API 核验」 |
| 浏览器自动化工具 | 自动搜到同季节、2 年内的真实笔记,拿到施工/排队/坑点等在地情报 | ① 用户直接贴攻略链接或截图(xhs-shared-note-extract 无需登录即可读正文) ② 联网搜索 |
在地情报变薄;时效三筛可能无法完整执行 |
问法示例(并在 Phase 0 第 1 轮里问,不要为它单独开一轮):
顺带问一下:你愿意提供高德开放平台的 Key 吗?有它我能把每天每段的里程/时长/收费 从路网数据库里逐段核出来(精确到公里),而不是凭经验估。 另外若本机有浏览器自动化工具,我能自动去小红书搜同季节的攻略。 两者都不强求 —— 不愿意我就走备选方案,只是数字会标成「待核实」。
浏览器自动化工具:任选其一,不锁死实现
技能只要求三项能力:打开页面 / 取整页 HTML / 在页面上下文执行 JS,且能复用用户的登录态。
| 工具 | 安装 | 说明 |
|---|---|---|
| browser-skill (bsk) ⭐首选 | npm i -g browser-skill |
直接复用本机 Chrome 登录态,无需另配 profile,最省事 |
| browser-use | pip install browser-use |
通用浏览器自动化 agent |
| Agent-Browser | npm i -g agent-browser |
通用浏览器自动化 CLI |
| Playwright CLI | npm i -g playwright |
官方框架,需自备持久化 profile |
用 bsk 时额外一步:在 Chrome 里装 BrowserSkill 扩展,并在该 Chrome 登录 xiaohongshu.com
(bsk 复用登录态,这是它比爬虫可靠的原因)。验证:bsk status --json → browsers connected 非 0。
本文档后续涉及浏览器操作的命令一律以 bsk 为例。 换用别的工具时, 只需找到对应的三个动作(打开页面 / 取 HTML / 执行 JS)—— 命令等价,不必照抄 bsk 语法。
高德 Key 获取(免费,仅在用户愿意时提供):
- https://lbs.amap.com 注册 + 实名认证(个人开发者即可)
- 控制台 → 应用管理 → 创建应用 → 添加 Key
- 🔴 服务平台必须选「Web 服务」 —— 选成「Web端(JS API)」调 REST 会返回
INVALID_USER_KEY,而人会误以为 Key 本身错了。这是最高频的坑。 - 数字签名建议不启用(启用了每次请求要额外算 sig)
- 存成文件:
~/.workbuddy/keys/amap.key
🔴 密钥纪律:绝不要把 Key 贴进对话 —— 对话通道会对疑似密钥做截断/脱敏 (实测 40 字符的凭据只收到 24 字符)。一律让用户写成文件,脚本读文件。
preflight.py也只回报长度,绝不回显密钥本体。
🔴 工具 / Key 不在标准位置时:
preflight.py按 环境变量 → 标准位置 → PATH 顺序查找, 不含任何写死的路径。设环境变量即可,不必改脚本:BROWSER_TOOL_PATH=<你的浏览器自动化工具可执行文件> # 旧名 BSK_PATH 仍兼容 AMAP_KEY_FILE=<你的 amap.key 路径>
Phase 0 · 交互式需求采集(核心环节)
🎯 目标是"问够",不是"问少"。 用户说"帮我规划 X 天 Y 地之旅"时,真实信息量往往不足以支撑一次准确的行程设计。 宁可多问一轮,也不要拿假设硬做 —— 每一条错误的假设,都会在交付时变成返工。
0.1 采集策略:三轮递进,先给东西再追问
第 1 轮 · 开场(必问,一次问完)
→ 把「硬约束」一次性问清(出发地/时间/天数/同行人/交通方式)
→ 这批答案缺失时无法开工
第 2 轮 · 骨架确认(第 1 轮答复后)
→ 先给一个「暂定路线骨架」(不必精确,够用户判断方向即可)
→ 指着骨架追问细节(每天想开多久/哪些必去/哪些可舍/住宿偏好)
→ ★ 关键:让用户对着具体方案提意见,比对着空白提问高效得多
第 3 轮 · 收口(进入制作前)
→ 补问剩余的可选项(图片模块 / 打印需求 / 节奏偏好 / 设计倾向)
→ 复述一遍全部结论请用户确认,再开工
为什么要"先给骨架再追问":用户往往说不清自己想要什么,但看到一版具体方案立刻能指出哪里不对。这是反复验证过最高效的交互形态。
0.2 必问清单(13 项,分优先级)
🔴 第一批 · 硬约束(缺一项就无法开工)
| # | 字段 | 为什么必须问 | 不问的后果(实证) |
|---|---|---|---|
| 1 | 出发地(到区/县级) | 县级差异可达上百公里 | 只问到"市"级时,若实际从下辖县出发、且该县距市区约 100 km,单段误差即达 198 km |
| 2 | 出发日期 / 日期窗 | 决定季节、天气、票价、抢票窗口;还决定 Phase 2 只采信哪些季节的攻略 | 决定季节性硬截止(如预约制景区的放票日);日期不明则攻略季节筛选无从做起 |
| 3 | 行程总天数 | 决定落脚点密度与日均里程 | 天数定错,整条路线的分段全部要重做 |
| 4 | 返回日期 / 是否必须当天回到出发地 | 末段常不可再分 | 返程最后一天往往是整条线最硬的一段 |
| 5 | 同行人数 + 关系(家人/朋友/独自) | 决定住宿房型、餐饮、节奏 | 影响预算与停留偏好 |
| 6 | 交通方式(自驾 / 高铁飞机落地租车 / 包车 / 公共交通) | 决定整条链路 | 自驾与非自驾是两套完全不同的规划逻辑 |
🟡 第二批 · 关键变量(强烈建议问全)
| # | 字段 | 为什么必须问 | 不问的后果(实证) |
|---|---|---|---|
| 7 | 几人开车 / 是否轮换 | 里程测算第一变量 | 单人 450–500 km/天,两人轮换 600–700 km/天 —— 差一个答案就是两个设计 |
| 8 | 车型 + 能源类型 + 实测续航 | 纯电车是完全不同的约束体系 | 标称 600–700 km 实测仅约 400 km;低温还要再打折 → 安全续航压到 300 km |
| 9 | 预算档位 | 决定住宿与补能策略 | 经济 / 舒适 / 品质 三档的选择贯穿全篇 |
| 10 | 偏好权重 | 决定停留时长分配 | 景观摄影 / 人文历史 / 亲子 / 美食 —— 同一目的地会产生完全不同的日程 |
🟢 第三批 · 偏好微调与设计倾向(11–12 有则更好;13 必问,不可跳过)
| # | 字段 | 用途 |
|---|---|---|
| 11 | 住宿偏好 | 连锁品牌 / 民宿 / 是否在意地铁距离 / 能否接受青旅 |
| 12 | 饮食限制与忌口 | 影响餐饮推荐的有效性 |
| 13 | 设计倾向(必问 —— 风格感觉 + 是否打印) | 供 Phase 4 收窄形态 / 配色候选;具体方向不在此定死 —— 由 Phase 4 §4.0 出可视化预览后由用户选定,不设默认值、不可省略 |
另需主动确认的(不属"问用户",但要覆盖):
- 图上要不要放美食/景点图片(Phase 5 会用到;也可在 HTML 出来后单独确认)
- 是否需要打印友好(默认需要 —— 路书是带在手上的东西)
- 节奏偏好:赶路型(多看几个点)还是松弛型(少开多停)
0.3 提问形式(照此执行)
一、成批提问,不要逐条挤牙膏。 一轮 4–6 个问题,每个问题附上"为什么问" —— 用户理解了理由,才会给出准确的答案。
示例(第 1 轮开场):
开工前需要确认几件事,都是会直接影响行程设计的硬约束:
1. 出发地具体到区/县?(例:不要停在"XX 市",要问到"XX 市 XX 县/区")
→ 县级差异可能上百公里,会直接改变第一天的落脚点
2. 出发日期与返回日期?(或可浮动的日期窗)
→ 决定季节、天气、票价,也决定有没有必须提前抢的预约
3. 计划玩几天?
4. 几个人去?什么关系?(家人/朋友/独自)
5. 交通方式:自驾 / 飞到当地租车 / 包车 / 公共交通?
→ 自驾和非自驾是两套完全不同的规划逻辑
6. 如果是自驾:几个人会开车、是否轮换?
→ 这是里程测算第一变量:单人约 450–500 km/天,两人轮换可到 600–700 km/天
二、用户给的信息不全时,不要停下来等。 先按其已给信息产出一版暂定骨架(可明确标注"占位,待确认"), 然后指着骨架追问缺的项。首响就要有可看的内容,而不是一串问题。
三、用户说"你看着办"时,也不要真的自己决定。 改为给出选项让用户挑:把每个关键选择的 2–3 个方案并列(含各自代价), 请用户点选。举手之劳的回答,好过一句"随便"。
四、有些字段可以从对话里推断,推断后要复述确认。 比如用户说"我们一家三口带个 6 岁的娃",可推断:3 人 / 亲子偏好 / 需要儿童友好住宿 / 节奏不宜太赶。推断完后必须复述一句请其确认,不要默默采用。
五、进入制作前复述全部结论。 用一张清单把 13 项答案列出来请用户过目 —— 这一步能拦掉大量返工。
0.4 若用户直接丢来一份攻略链接
此时不要放弃采集,而是把它当作已回答的部分,然后补齐缺口:
- 解析链接内容(
xhs-shared-note-extract/ bsk) - 抽取其中已明确的字段(目的地/天数/景点)
- 列出仍未明确的项,有针对性地追问(通常缺的是:出发地县级、人数、 驾驶人数、预算档、交通方式)
- 若攻略里的里程/时间与你的核验结果冲突,在 Phase 2 用 API 裁定, 并把差异明确告知用户
0.5 需求卡(采集完成后立即输出,作为开工依据)
把结论整理成一张卡,同时供用户确认与后续复用:
【需求卡】
出发地 :___(省/市/区县,含"从县城还是市区出发")
日期 :___ 至 ___(共 __ 天)
同行 :__ 人(关系:___)
交通 :自驾 / 租车 / 包车 / 公共交通
驾驶 :__ 人轮换 / 单人;车型 ___,能源 ___,实测续航 ___ km
预算 :经济 / 舒适 / 品质
偏好权重 :景观摄影 __ / 人文历史 __ / 亲子 __ / 美食 __
住宿 :___
忌口 :___
节奏 :赶路型 / 松弛型
图片 :需要 / 不需要(要哪些天)
设计倾向 :风格 ___(清爽 / 厚重 / 温暖 / 科技感 …)
→ 形态与配色待 Phase 4 §4.0 出预览后选定(**不要在此替用户定死**)
打印 :需要 / 不需要(影响形态候选:地图叙事打印差)
已确认 :✅ 用户已确认以上全部内容
这张卡要一直保留到交付 —— 中途每次做取舍都回来对照它。
Phase 1 · 路线规划
核心是先定骨架,再核里程。骨架来自情报检索,里程由 API 裁定。
1.1 三条铁律(实践踩出来的)
① 环线 ≠ 必须闭合。 出发地不在环线上时,"闭合回枢纽城市"是范式污染。一次实测:返程不绕回枢纽、 直接顺方向东出,省 264 km / 5.7 h / 133 元。 → 判据:若出发地距环线枢纽需绕行超过日均时长的 15%,就不要闭合,直接顺方向出。
② 落脚点比选看纯驾驶时长,不看里程;候选点必须在主线上。
- 9.6 h 与 6.9 h 的差别才是疲劳,905 km 与 651 km 不是
- "过长"和"过短"都是错 —— D1 太短会让 D2 变成怪物,这两件事是一回事
- 零绕路落脚点的快速验证:看原路线 API 返回的
steps里已经出现了哪些城市 —— 路线本来就经过的城市就是最佳落脚点;停在它们之前的城市=白停 - 对比时看最长单日,不是平均值
③ 单日上限硬约束。 纯驾驶 > 8 h 的日子必须单独标出(疲劳风险点)。实测长线行程的全程最长单日应控制在 约 616 km / 8.0 h 量级,超过就要重新切分。
1.2 落脚点比选三步法
1) 拿「起点 → 终点」直连数据,算日均 T = 总纯驾驶时长 ÷ 天数
2) 沿走廊把候选城市全算一遍「起点→X」与「X→终点」,挑出距起点 1–1.2T 的
3) 对 2–3 种组合比「最长单日」,再看总里程 / 通行费 / 补能条件
顺带核:候选城市的品牌自营超充数量(纯电车尤其重要 —— 主流品牌的官方自营站
通常比第三方更可靠)。过夜用自营站能省掉排队与第三方 App 的不确定性;
只有一个自营站的城市要降权。
查法:WebSearch "<品牌>超充 <城市1> <城市2>"。
交付时要说清"代价":换落脚点后总里程 / 总天数 / 通行费各变了多少。 若三项几乎不变,明确写"这个改法的代价是 0",用户才好决策。
Phase 2 · 多渠道交叉验证(一致性关卡)
用户明确要求「小红书 + 高德 + OSRM 交叉验证」。但三者角色不同,不可平权 —— 必须定优先级,否则冲突时无法裁决:
高德 REST /v3/direction/driving ← 里程·时长·收费的【主口径】,权威
↑ strategy 必须是 0(速度优先)
↑ ⚠️ 返回的是【查询时点的路线估算】,不是实时路况(详见 §2.1 边界说明)
OSRM router.project-osrm.org ← 独立路网【交叉引擎】,免 Key
↑ 偏差 >10% 必须人工看;时长模型对中国限速不准,只信里程
高德不可用(无 Key)时 → 主口径退化为:OSRM(只信里程)/ 用户查读数 / 经验估算
↑ 无论哪种,全部标 ⏳ 待核实,并在交付时说明「未做 API 核验」(见 Phase -1)
小红书 ← 现实情报源(施工/封闭/排队/坑点)
↑ 🔴 只采信【2 年内发布】且【与本次出发季节相同】的笔记(详见 §2.5)
↑ 必须找到【时效分水岭】,分水岭前的信息整体排除
↑ ⚠️ 是「近期在地体感线索」,**不是权威交通源**
↑ 通道:任一浏览器自动化工具;都不可用 → 用户贴链接/截图,或联网搜索(见 Phase -1)
官方路况源(省交通厅 / 12328 / 景区官微)← 施工封闭的【唯一权威】
2.1 高德 REST(主口径)—— 复用 amap-route-verify 技能
# 关键参数
/v3/direction/driving?origin=lo,la&destination=lo,la&strategy=0&extensions=base&key=<KEY>
- 🔴 爬地图网页是死路:高德/百度/腾讯 PC 与移动 Web 全是 SPA + 滑块 + 登录墙, HTML 里 0 条路线数据。正确路径只有官方 REST API。
strategy必须用 0(速度优先)。对比过:1费用优先会把 905 km 的段算成 971 km / 18.8 h(走国道),不能用作口径;2距离优先偶尔省 8–13 km 但慢 0.2 h。- 返回
paths[0]取distance/duration/tolls;paths数组长度 = 备选方案数。
🔴 边界说明:这是「查询时点的路线估算」,不是「实时路况」。
按高德官方文档:
| 事实 | 出处 |
|---|---|
strategy=0 的官方描述是「速度优先,此路线不一定距离最短」—— 全文不提路况 |
路径规划 API 文档 · strategy 表 |
| 官方明说「由于道路/数据/算法的变更,很可能存在间隔一段时间后请求相同起终点的经纬度返回不同结果」 | 同上 · 产品介绍 |
返回字段 distance / duration / tolls / traffic_lights 均为估算值,无"当前拥堵状态"字段 |
同上 · 返回结果表 |
只有 strategy 取 4/8/9/12/15/17/18/20 时才「考虑路况」「躲避拥堵」 |
同上 · strategy 表 |
所以:
- ✅ 可以说:这是路网数据最新最全的来源之一,是对"模型记忆"的实质改进
- ❌ 不能说:这是"实时路况""实时导航数据"
- 若要实时拥堵 / 施工 / 封路 / 交通管制 → 必须引用交通状态字段或官方交通源
(省交通厅 / 12328 / 景区官微),不能拿一次
strategy=0的结果当路况证据 - 输出时标注为「查询时点估算」,不要写成「实时路况」
想让高德返回更贴近路况的结果,可换
strategy=10/12(考虑路况、躲避拥堵)。 但换了策略就换了口径:里程会变,历史数据不可直接比 —— 除非全行程重算,否则别混用。
2.2 POI 坐标校准(极易漏的前置动作)
🔴 任何非城区 POI(景区、垭口、服务区、收费站),必须先经 POI 搜索校准坐标再算里程。
/v3/place/text?keywords=<名称>&city=<城市>&key=<KEY> # 取 location = "经度,纬度"
实证代价:手填某景区坐标纬度差 0.28° ≈ 31 km,导致当天里程虚高 76 km; 校准后正好贴合。凡景区支线段失真,先查坐标。
2.3 OSRM(交叉引擎)
https://router.project-osrm.org/route/v1/driving/{lon1},{lat1};{lon2},{lat2}?overview=false
- 免 Key,纯 JSON,可在高德配额用尽时兜底
- 主干高速段与高德偏差 ≤5%,可互证
- ⚠️ OSRM 的时长模型对中国限速不准(285 km 给 3.5 h ≈ 86 km/h,山区高速不可能) → 只信里程,不信时长
- 偏差 >10% 的段:通常是新建高速/匝道/景区支线,OSM 收录滞后 → 以高德为准
2.4 冲突裁决规则(三条)
| 冲突情形 | 裁决 |
|---|---|
| 里程冲突 | 以高德为准;OSRM 偏差 >10% 标记「需复核」,人工看 steps 判断是否改道 |
| 小红书 vs 官方 | 官方赢;且小红书须先过 §2.5 的时效三筛(2 年内 + 同季节 + 分水岭后) |
| 总量 vs 逐段 | 逐段优先。总量对 ≠ 分布对,风险藏在单日里 |
2.5 攻略时效筛选(小红书情报的第一道硬筛)
🔴 两条硬性时间要求 —— 不满足直接不进候选池:
① 只采信 2 年内发布的笔记。
判据:
今天 − 笔记发布日期 ≤ 2 年。 依据:中国路网、补能设施、景区政策、门票规则的更新周期普遍在 1–2 年;超过 2 年的攻略, 错误率已高于参考价值。搜索时就用平台的时间筛选,不要先抓一堆再逐条过滤。
② 只采信与本次行程出发时间季节相同的笔记。
季节的判定不看月份数字,看体感与客观条件:气温带、昼夜长度、雨季/旱季、 结冰期、旅游旺季/淡季、植被与水位状态。分四季(春 3–5 / 夏 6–8 / 秋 9–11 / 冬 12–2) 作粗筛即可,但交界月份要按实际条件判断(例:高海拔地区 10 月上旬已近冬季气温, 不能因为"10 月属秋"就套用 5 月或 7 月的攻略)。
反例(必须排除):
- 用户 10 月出发,参考 7 月的草原攻略 → 草已枯黄、已过雨季,景观与路况都不同
- 用户 1 月出发,参考 10 月的攻略 → 结冰、封路、日照时间差 2–3 小时,风险完全不同
若相同季节的笔记数量不足,宁可减少情报量,也不跨季节硬凑 —— 并明确告知用户 "该目的地同期素材较少,以下结论主要来自官方源与 API 核验"。
③ 在上述两条之后,再找该目的地的时效分水岭。
- 记下每条关键信息的发布日期(同时满足①②才有资格进入这一步)
- 找出时效分水岭(某个改变现状的事件:新路通车、充电桩投运、景区改制、施工封闭)
- 分水岭之前的所有攻略信息整体作废,不逐条采信
实证:某线路的分水岭 = 2025-09(沿线充电桩集中投运),早于此的所有补能攻略全失效。
①②③ 是递进关系:先用时间与季节砍掉大部分,再用分水岭砍掉剩下的过时信息。 三道筛完仍留下的,才是可信情报。
2.6 逐段核验清单(产出物)
核验后必须给出下列判断,不要只贴表格:
- 最长单日是哪天 —— 按纯驾驶时长排序,>8 h 单独标出
- 补能次数是否够 —— 里程 ÷ 安全续航,向上取整再 +1 次冗余(纯电车硬约束)
- 在途总时长 —— 纯驾驶 + 充电时间(每次 40–60 分钟),换算成 8 小时工作日
- 过路费 —— 直接用 API 的
tolls合计,比自己估准 - 哪段要出发前复核 —— 跨无人区、新通车、施工管制段
Phase 3 · 路书内容规划
3.1 页面结构(信息维度,视觉实现见 Phase 4)
① 头部 标题 + 副标题 + 标签(季节·档位·风格)
② 总览概要 天数 / 城市串 / 预估人均 / 行程风格 + 3 条核心亮点
③ 交通方案 方案对比表 + 推荐详情 + 特殊交通(观光车/索道/船票)
④ 住宿推荐 片区对比表 + 具体酒店卡(地址+地铁距离+价格+周边+批注)
⑤ 每日行程 ★核心:主题化标题 + 小时级时间轴 + 顺路校验 + 餐饮嵌入 + ☔雨天备选
⑥ 美食专题 店名+地址+人均+必点菜+口味适配(少则嵌入⑤)
⑦ 费用预算 账本表格 + 可视化柱状 + 三档对照(经济/舒适/品质)
⑧ 行前清单 证件/电子/衣物/药品/App 五类
⑨ 避坑总结 醒目红框,≥5 条,含反幻觉提示
⑩ 应急方案 7 类情景,每张卡含 5 要素
⑪ 导出分享 打印/PDF 提示 + 版本页脚
3.2 内容分层标注(可信度的来源)
每条信息都要能溯源。 在页面上用徽章区分:
| 标注 | 含义 | 例 |
|---|---|---|
| 🔎 已核实 | 来自本次 API 实测 / 官方页面 / 用户订单 | 里程 1 234 km |
| ⏳ 待核实 | 基于经验或旧知识,必须附官方核验渠道 | 景区开放时间 |
| 📝 批注 | 主观判断/个人体验(可与数据分离展示) | "这条路下午逆光" |
| ⚠️ 风险 | 需要降级预案的节点 | 某段半幅施工 |
实测一份 16 天长线路书用了 72 个置信徽章覆盖全页,这是长文档可信度的来源。
禁止臆造:具体车次号、票价、放票时间、实时开放状态、餐厅营业时间、天气、实时路况 —— 这些要么来自本次取数,要么来自用户投喂;都没有就标 ⏳ + 渠道。
路线数字(里程/时长/收费)统一标为「查询时点估算」,不要写成「实时路况」—— 详见 §2.1 边界说明。
Phase 4 · HTML 生成
4.0 设计方向确认(动手前必须先做)
🔴 日卡形态与配色方向都必须由用户选定。 不要套用任何"默认值" —— 这两样是页面最显性的部分,选错了整页都要重做。
做法:出可视化预览让用户挑,不要用文字让用户想象。
- 形态(规范 §1.2,四选一):把候选画成线框预览(内联 SVG 或等价的可视化渲染), 让用户直接看到"铁路图长什么样、编辑长卷长什么样",而不是读一段描述。
- 配色(规范 §2.1,四选一):把每个方向的色板与一张样卡渲染出来给用户看。
给推荐,但不替用户决定。 若 Phase 0 已登记设计倾向与打印需求,据此先把候选收窄到 2 个再出预览,避免让用户面对四个方向无从判断。 仍然要说明推荐理由(行程天数、打印需求、内容特征),然后请用户点选。 用户说"你定"时也要先展示再确认("我按 X 方向做,可以吗"),不要默默采用。
为什么必须可视化:用户说不清自己想要什么,但看到具体的一版立刻能指出哪里不对。 线框阶段改一处成本极低;页面做完再改,就是整页重做。
4.1 视觉与内容规范 = references/roadbook-spec-v1.0.md(权威)
这是 HTML 层的权威规范,直接执行它,不另立标准。
规范涵盖(摘要,细节以原文为准):
| 章 | 内容 | 关键取舍 |
|---|---|---|
| §1 结构 | 五层骨架(报头→总览→逐日→附录→页脚)、日卡字段清单、附录模块组成、信息守恒 | 四形态可选(铁路图时间轴 / 编辑长卷 / 分段仪表盘 / 地图叙事);按天数与打印需求给推荐,由用户选定;目录条必备 |
| §2 视觉 | 低饱和双专色纪律、四方向色板、三栈字体、布局留白、地图时间线、图文混排、CSS 交互、七档响应式、a11y | 四方向可选(暖沙纸 / 暗夜星轨 / 冷灰编辑 / 极简数据舱);由用户选定;阶段色独立于站点语义色 |
| §3 内容 | 可靠性总纲、数据标注格式表、置信度四级、语言风格与禁用词、各模块必备字段、版本溯源 | 数字格式必须统一(半角空格千分位 / en dash 区间 / 「约」前置) |
| §4 形态差异 | 四形态取舍汇总表 | — |
| §5 工程约束 | 零外链 / 无 JS / 字体子集化 / 交付自检 / 构建脚本与产物独立存在 / 不要求部署上线 | 与 §4.2 一致 |
硬性条目(摘录,交付前必须逐条满足):
- 日卡七个字段一个不能少:日期+星期 / 路线标题 / 路线串 / 一句话账 / 站点时间线 / 统计条 / 图片位
- 字号下限:正文 ≥13px,标签徽章 ≥10.5px,图注 ≥12px,任何位置禁 <10px
- 图片统一 3:4 竖图成对,宽合计 ≤67% 栏宽并居中;只用真实图源,无图就不渲染图片位
- 风险卡全部展开,不许折叠
- 禁用词:绝绝子 / yyds / 打卡圣地 / 小众秘境 / 宝藏 / 氛围感 / 出片率 及一切带货腔
- 同一概念全篇同词(「安全续航 300 km」不得又写「规划续航」)
- 版本标识四处同步:
<title>/ 页头徽章 / 页脚 colophon / 更新说明行 - 日卡形态与配色方向必须由用户选定(采集见 Phase 0 第 13 项、确认方式见 §4.0) —— 不设默认值,不要擅自决定,也不要套用任何"看起来合理"的方向
若用户另有自己的规范:
- 让用户提供 —— 以其规范为准,
references/roadbook-spec-v1.0.md仅用作兜底 - 无法获取、也无需迁就时,回退到技能
travel-roadbook-html(编辑极简 / 零依赖 / 三栈字体 / 内联 SVG 信息图) - 并在交付时说明"视觉用了回退方案,若你有既定规范请提供,我按规范重做"
4.2 位置无关的硬约束(与视觉方案无关,任何情况下不可破)
| 约束 | 说明 |
|---|---|
| 零外网依赖 | 无 CDN / 外链字体 / 外链图片 / url(http...)。全部样式内联在 <head><style>。<a href> 外跳允许 |
| 脚本极简 | 优先 0 段;最多 1 段渐进增强且必须可降级 |
| 无固定 px 布局 | 用 % / rem / flex / grid |
| 移动端不横向滚动 | 图表类需加横滑容器 |
| 响应式断点 | 至少覆盖 360 / 390 / 640 / 768 / 1024 / 1220 / 1440 |
| 无 emoji 依赖 | 正式路书用图标 SVG 或衬线符号,emoji 会破坏调性 |
| CSS 变量不得未定义 | 引用前必须有定义 |
4.3 字体子集化(若自托管中文衬线,有严格顺序)
🔴 顺序必须是:先改完所有文案 → 最后跑子集化脚本。
❌ 错误顺序:跑 subset → 再改文案 → 新字静默回退系统衬线体
(不报错、无破绽、肉眼极难发现)
✅ 正确顺序:改完文案 → 跑 subset → 脚本逐字校验覆盖率 → 缺字直接报错
三条硬经验(详见 travel-roadbook-html):
- fontTools 是懒加载的 ——
TTFont(path)只读表目录,截断文件也能骗过它。 完整性校验必须逐个真读:ft = TTFont(p) for tag in list(ft.reader.keys()): if tag == 'GlyphOrder': continue _ = ft[tag] # 触发真实读取与解压 - 精简
layout_features能省 15% —— 收窄到['kern','liga','clig','calt','locl','ccmp','mark','mkmk'](locl/ccmp/kern是中文关键项,不要省)。实测 22 MB → 305 KB。 - 大文件下载靠重试次数,不靠换源 —— 22 MB 字体经代理通常每 1–5 MB 断一次,
但 HTTP Range 续传有效。用流式 256 KB 记录字节 +
Range: bytes=N-+ 多源轮换 +tries=40。
4.4 内联 SVG 信息图(比换皮值钱的一层)
信息图是路书的差异化价值:路线示意地图 / 补能空档条形图 / 海拔剖面 / 气温带。
🔴 两条铁律:
- 装饰性图形不要画得像数据。 一次交付中日卡顶部曾有"海拔走势带"曲线,被用户当场质疑 "这个曲线是什么" → 已删除。 → 要表达趋势就必须用真实数据拟合;否则干脆不做。 任何自造图形的描述要保守。
- 排完图必跑「包围盒重叠检测」 —— 肉眼审查不可靠。
// 遍历所有 <text>,两两比 getBBox() // ox > 1.5 && oy > 1.5 (用户坐标)→ 判定重叠
4.5 改动后的连带清理(最易漏的一步)
🔴 改路线 / 改数字后,必须回头清理全部"下游结论"。
一次实测需同步 33 处:总里程、日均、通行费、充电次数、预算表、
风险卡、复核清单、信息图坐标、版本号(<title> / 徽章 / colophon / 更新时间行四处)。
→ 做一张「受影响的结论清单」再逐项勾掉,不要凭记忆。
Phase 5 · 图片补充与校验
🎯 判断准则:图片是加分项,不是核心交付物。
路书的价值在路线、里程、补能、风险这些可核实的事实上。 图片能让页面更好看,但不影响这份路书能不能用。 如果花在图上的时间开始挤压核验工作,就是做过头了 —— 退回轻量档或直接不放图。
因此本 Phase 默认走轻量档:挡掉破图与畸形图即可,不追求每张图的语义精确匹配。
5.0 与用户确认(必做,30 秒的事)
HTML 生成后,先问四件事:
- 要不要图?(不要 → 走 A 档,本节结束)
- 哪些天要?
- 每天几张?
- 要不要美食图?
(曾有用户明确要求"最后两天不要图片模块" —— 这类偏好必须问出来,不能默认。)
5.1 三档路径(选一档,不要混做)
| 档 | 适用 | 跑什么 | 成本 |
|---|---|---|---|
| A · 不放图 | 用户不需要 / 行程短 / 时间紧 | 跳过整个 Phase 5。页面没有图片位也完全成立 | 0 |
| B · 轻量 ⭐默认 | 用户要图,不苛求精确匹配 | 取候选 + 闸门①②(有效性 + 尺寸)+ 落盘 + 收尾断言 | 极低 |
| C · 完整 | 用户明确说"图必须对得上",或图片位多、途经点易混 | 五道闸门全跑(额外含素材类型 + 语义一致性) | 中高 |
默认 B。 绝大多数情况用户在意的是"别放破图、别放明显不相关的图", 而不是"每张图都逐字验证语义"。C 是为高要求场景准备的,不是必备流程。
# B 档(默认):只跑 ①②
"$PY" scripts/imgverify.py --cand cands.json --out verdict.json
# C 档:五道全跑
"$PY" scripts/imgverify.py --cand cands.json --out verdict.json --gates full
# 自选(如只跑 ①④⑤)
"$PY" scripts/imgverify.py --cand cands.json --gates 145
以下为 B / C 档的共有步骤
5.2 取图:不要盲取第 N 张
❌ 正则抓 CDN URL 后 --pick N 盲选 —— 内容完全不可控,
抓到头像 / 攻略信息图 / 与途经点不符的图是必然,不是意外。
✅ 在页面上下文里取结构化候选(B 档取到尺寸就够;C 档还需要 near 文案):
// bsk evaluate --session <id> "<IIFE>"
Array.from(document.querySelectorAll('.note-slider img, .swiper-slide img, #noteContainer img'))
.filter(i => i.naturalWidth > 0)
.map(i => ({
src: i.currentSrc || i.src,
w: i.naturalWidth,
h: i.naturalHeight,
alt: i.alt || '',
// ★ 语义线索:仅 C 档需要(闸门④用)
near: (i.closest('figure,div,section')?.innerText || '').slice(0, 150),
is_avatar: !!i.closest('[class*=avatar], [class*=Avatar]')
}))
near 是图片旁边的文案,也就是语义证据 —— 比看 URL 可靠得多。只有 C 档需要它。
5.3 闸门(B 跑①②,C 跑全部)
scripts/imgverify.py 逐道判,任一不过即淘汰,换下一候选:
| # | 闸门 | 判据 | 默认阈值 | B | C |
|---|---|---|---|---|---|
| ① | 有效性 | naturalWidth>0;文件字节下限;非头像元素 |
min_bytes = 20 KB |
✅ | ✅ |
| ② | 尺寸合理性 | 最短边达标;宽高比合理(挡长图拼接/横条) | min_side = 480px;ratio ∈ [0.5, 2.0] |
✅ | ✅ |
| ③ | 素材类型 | 排除头像/图标/水印;排除攻略信息图(邻近文案命中多个"攻略/清单/价格表"类词,或文字密度过高) | max_text_ratio = 0.35 |
— | ✅ |
| ④ | 语义一致性 | keywords 必须命中 near/alt/笔记标题,且不能只命中泛词;cat=food 须有食物词族、cat=sight 须有景物词族 |
— | — | ✅ |
| ⑤ | 命名互锁 | photos/d{天:02d}-{cat}.webp(两位补零)与 photos.json 的 cat 一致 |
— | — | ✅ |
闸门④是 C 档的核心价值 —— 它拦的正是"图与途经点不匹配"。设计要点:
- 候选 JSON 的
keywords字段必须来自该 POI 本身(用该景点/菜品的专名),不是泛描述 - 泛词黑名单(
旅行/攻略/打卡/美食/风景…)命中不计入 —— 否则任何图都能过 - 分类词族校验:防止把菜单图配给景点位
⚠️ 闸门③④依赖
near文案,只有走 bsk 取候选那条路才有。 (下文命令以 bsk 为例;换用 browser-use / Agent-Browser / playwright 时按同思路替换。) 若图来自别处(用户提供、官方图库),跑 C 档也只会空转 —— 这时用 B 档 + ⑤ 命名检查即可。
5.4 落盘:命名与元数据互锁
"$PY" scripts/imgcrop.py --src raw/d09.jpg --day 9 --cat sight
# → photos/d09-sight.webp (裁 3:4 → 480×640 → WebP q78)
🔴 为什么互锁:曾出现某天的景点图(sight)被存成 dNN-food.webp ——
根源是"先出图、后填表"。互锁后这类错位不可能发生。
裁切规则:中心裁 3:4;竖图略偏上(top = max(0, (h-nh)//3))保主体。
5.5 拿不到图时(一条降级链,绝不硬塞)
合格图 → 换更精确关键词重搜 → 仍无:该位不放图,记入 skip(页面自然收窄,不破版)
→ 用户想补:按 图片替换/D{天}-{类型}-{名称}.png 丢文件,自动识别替换
原则:宁可少一张图,不要多一张错图。 少一张图页面照样成立; 一张错图的代价是读者对整个文档的信任。
用户手选图通道(命名约定驱动,无需配置):
"$PY" scripts/imgcrop.py --replace "<图片替换目录>" --photos photos/ --backup backup/photos
# D10-景点-某地标.png → d10-sight.webp
脚本会:备份原图 → 裁切落盘 → 写报告 → 提示"必须重跑页面构建同步 photos.json"。
5.6 图片来源仅供内部追溯,不对外链接
🔴 图片是纯图片,不带任何跳转链接。
photos.json 的 xhs 字段只用于内部留痕(事后想回溯"这张图哪来的"),
不写进 HTML、不作为超链接、读者看不到它。
photos.json(内部文件) → HTML 页面
xhs: <笔记标识> <img src="photos/d09-sight.webp">
↳ 仅供团队追溯,不入页面 ↳ 纯图片,无 <a> 包裹
理由:小红书的笔记链接带有时效的访问参数(xsec_token 一类),写进对外页面
过一阵就会失效 —— 读者点进去看到"内容不存在",比不放链接更糟。
路书是带在手上的离线文档,图片的价值在于"看到",不在于"点得动"。
所以:
- ❌ 不生成"图片来源"外链区块
- ❌ 不给
<img>套<a href> - ❌
photos.json的xhs字段不写入 HTML 产物 - ✅ 图片只作为图片呈现;确需标注出处时,只写平台名 + 标题这类不含访问参数的纯文本
与 4.x「零外链」同一条原则:整个 HTML 不依赖任何外部可达性。
5.7 两条收尾断言(无论 B 还是 C 都必做)
// 零破图
[...document.images].filter(i => i.naturalWidth === 0).map(i => i.src)
// 无横向溢出(只看 documentElement,别只看单元素)
document.documentElement.scrollWidth > window.innerWidth
若自托管字体,再加一条 document.fonts.check('16px <字体名>')。
再加一条:页面内零 xsec_token / xsec_source / cookie 等访问参数 ——
若出现,说明有链接漏进了页面:
/xsec_token|xsec_source|cookie/i.test(document.documentElement.outerHTML)
Phase 6 · 交付前自检
6.0 需求对照(第一道关,先过这个)
把需求卡拿出来逐项回对 —— 13 项每一项都要能在产物里找到落点:
| 项 | 在产物里对应什么 |
|---|---|
| 出发地 / 日期 / 天数 / 返程 | 路线串、日卡日期链、总里程 |
| 同行 / 交通 / 驾驶 | 住宿房型、补能策略、单日里程上限 |
| 预算档 | 住宿与餐饮档次、附录预算表 |
| 偏好权重 | 各点停留时长分配 |
| 住宿 / 忌口 / 节奏 | 落脚点选择、餐饮推荐、日卡密度 |
| 设计倾向 → 形态 + 配色 | 页面形态与配色,且必须是用户从预览里选出来的 |
🔴 重点检查设计方向:形态与配色是不是用户在看过预览之后选定的? 若发现自己其实是"按某个默认值做的",停下来回 Phase 4 §4.0 补做,不要等交付后再改。
6.1 静态自检(8 组)
| 组 | 检查项 |
|---|---|
| 1 | 外链 / 依赖 —— 绝对外链为 0、url(http) 为 0、字体文件存在 |
| 2 | 脚本与事件 —— <script> ≤ 1、无内联 on* |
| 3 | emoji —— 无 |
| 4 | CSS 变量 —— 引用前均已有定义 |
| 5 | 标签闭合 —— 逐个计数平衡(含 SVG 标签) |
| 6 | 结构数量 —— 与内容清单逐项比对(见 6.2) |
| 7 | 可访问性 —— lang / viewport / role=img / title+desc / 表头 th / focus-visible / prefers-reduced-motion / print 样式 |
| 8 | 版本标识 —— <title> / 徽章 / colophon / 更新时间四处同步 |
🆕 一个关键技巧:查外链前必须先剥掉 <a> 超链接,否则笔记直链会被误判为外链:
h_no_a = re.sub(r'<a\b[^>]*>', '', h)
ext = re.findall(r'(?:src|href)\s*=\s*["'](?:https?:)?//[^"']+', h_no_a)
6.2 数据守恒校验(重写/改版最易出错处)
内容清单 → 逐项断言数量,例如一份 16 天长线路书的基准:
16 日卡 / 54 站点条目 / 77 统计条 / 12 风险卡 / 4 提示卡 /
6 充电明细表 / 28 图片卡 / 4 信息图 / 3 数据表 / N 个图标引用
加关键事实探针(不可改写的数字字符串),任一缺失即报错:
for probe in ["<总里程>", "<总时长>", "<通行费合计>", "<最长单日里程>", "<抢票日>", "300 km"]:
if probe not in html: fail.append(probe)
若改版后类名全换,必须同步更新检查脚本里的选择器与数量基准, 否则检查项会空转(计到 0 却"通过")。
6.3 浏览器实测(必做,别靠肉眼猜)
bsk session start --json
bsk navigate "file:///<绝对路径>/路书.html" --session <id>
bsk evaluate --session <id> "...探针 JS..."
bsk session stop <id>
四项必测:零破图 / 无横向溢出 / 字体生效 / 控制台零报错。 命令以 bsk 为例;换工具时只需保证能「加载本地 HTML + 执行页面内 JS 探针」。
6.4 脚本一律容错模式(本环境硬要求)
🔴 含大段 HTML 的 heredoc 脚本可能 exit=1 且 stdout/stderr 全空。
→ 所有脚本一律:
rep = []
def log(m): rep.append(str(m))
try:
...逐项 try...
except Exception:
log("FATAL:\n" + traceback.format_exc())
io.open(RPT, "w", encoding="utf-8").write("\n".join(rep)) # 不依赖 stdout
其他环境陷阱:/tmp 是 Git Bash 虚拟路径 Python 读不到(用 Windows 绝对路径);
subprocess 调 bsk 要完整路径 bsk.exe(否则 WinError 2);
bsk get-html 优先用 --out <path> 直接落盘,彻底绕开 stdout 被吞。
五阶段自检思维链
生成成品前依次以 5 个角色自检,任一角色否决就回炉:
角色 1 · 情报核验员
检查:每条推荐能否溯源到平台(OTA/点评/美团/抖音/地图/官方)?车次票价时长有来源?
否决:出现无法溯源的具体车次/票价/放票时间/营业时间 → 改 ⏳+渠道,或删除
角色 2 · 行程架构师
检查:每日路线顺路不绕路?时段精确到小时?体力/天气/闭馆日考虑了吗?
否决:同天跨城折返、单点停留与路耗不匹配、周一闭馆景点排进周一 → 重排
角色 3 · 预算会计师
检查:分类小计相加 = 总计?人均 AA 对?退款已剔除?三档各自自洽?
否决:总额与分项不符、人均算错 → 重算
角色 4 · 风险审查官
检查:避坑 ≥5 条且覆盖真实高发坑?每个关键环节有 Plan B?
否决:避坑空泛("注意安全")、抢票/索道/船班等高危环节无备选 → 补写
角色 5 · 呈现设计师
检查:零外链?移动端不横向滚动?零破图?圆角阴影一致?
否决:存在 CDN/外链、固定 px 布局、破图 → 修正后再交付
追加角色 6 · 图片审计员
检查:每张图都过了所选档位的闸门?命名与 cat 互锁?破图为 0?
否决:有未校验的图、命名错位、破图 → 回炉重选或降级不放图
ⓘ 注意:图片是加分项,不是核心交付物 —— 不值得为它牺牲核验工作
质量自检清单(交付前逐项过)
结构与内容
- 11 个信息模块齐全(或按用户要求裁剪,并说明裁了什么)
- 每日行程主题化标题 + 小时级时间轴 + ☔雨天备选
- 所有餐厅有具体店名与人均;所有酒店有地址与地铁距离
- 所有必去景点有预约渠道与放票时间(或标 ⏳ + 渠道)
- 避坑 ≥5 条;应急 7 类情景齐备(每张卡 5 要素)
- 预算总额 = 各分类小计之和;人均 AA 正确;退款已剔除;三档各自自洽
信息可信度
- 易变信息均已标 🔎/⏳,无臆造车次/价格/开放状态
- 每条美食/酒店/玩法都标注了情报来源平台
- 小红书情报已过时效三筛:全部 2 年内发布 + 同季节 + 分水岭之后(§2.5)
- 已定位并排除时效分水岭之前的信息
- 里程逐段核验(不是只核总量);出发地问到了县级
- 所有非城区 POI 坐标已经 POI 搜索校准
图片(按所选档位检查)
- 已与用户确认过「要不要图 / 哪些天 / 每天几张 / 要不要美食图」
- 每张图都过了所选档位的闸门(B 档=①②;C 档=①~⑤)
- 命名与
photos.json的cat互锁,无错位 - 浏览器实测零破图
- 无合格图的位已降级(不放图),未硬塞
- 图片无任何跳转链接(
<img>未被<a>包裹、无"来源"外链区)
可靠性
- 零外链(含剥掉
<a>后复查) - 页面内零访问参数(无
xsec_token/xsec_source/cookie) - 移动端零横向溢出
- 脚本为容错模式,报告写文件
- 联网/工具失败时已降级交付,未中断
收尾
- 数据守恒校验通过(数量 + 关键事实探针)
- 版本号四处同步
- 若改过文案且自托管字体 → 已重跑子集化且覆盖校验通过
- 改过路线/数字 → 已清理全部下游结论(列出受影响清单逐项确认)
可复用偏好卡(对话结束时输出,供下次免对齐)
记下并复用:
- 出发地(到县级) / 常驻城市
- 通常几人开车、是否轮换
- 车型 + 实测续航(纯电尤其重要)
- 预算档位(经济/舒适/品质)
- 偏好权重(景观摄影/人文历史/亲子/美食)
- 住宿偏好(连锁品牌/地铁距离/是否接受民宿)
- 忌口与饮食限制
- 是否要图片模块、要哪些天
- 打印 / 导出 PDF 的需求
配套技能(编排它们,不重复其内容)
| 技能 | 覆盖 | 何时调用 |
|---|---|---|
amap-route-verify |
高德 REST 里程核验 + 落脚点比选 | Phase 2 |
xhs-bsk-research |
bsk 抓小红书正文 / 搜索 bypass(bsk 实现;换别的浏览器工具时按同思路自行编排) | Phase 2 情报 + Phase 5 取图 |
travel-roadbook-html |
HTML 视觉系统 / 字体子集 / SVG 信息图 | Phase 4(默认实现) |
china-roadtrip-planner |
需求识别 → 候选 → 评分框架 | Phase 0–1 可选 |
自带文件
| 文件 | 作用 |
|---|---|
scripts/preflight.py |
Phase -1 环境依赖体检(+缺失指引) |
scripts/imgverify.py |
分级闸门图片校验(默认轻量 ①② / --gates full 五道)(+已落盘图片扫描) |
scripts/imgcrop.py |
3:4 裁切落盘 / 命名断言 / 用户手选图替换通道 |
references/roadbook-spec-v1.0.md |
HTML 结构·视觉·内容规范(Phase 4 权威) |
references/image-validation.md |
图片校验机制的设计说明与调参指南 |