# Travel Roadbook Pipeline

> 端到端旅游路书生成器。当用户给出旅游目的地（"帮我做XX的路书/行程规划"、亲子/自驾/多城/节假日旅行），或要把一份小红书/抖音攻略链接变成可执行路书，或需要核验行程里程与图片真实性时使用。关键信息取数而非凭记忆——调用高德 REST 路线计算核验逐段里程与时长（查询时点估算，非实时路况），检索小红书攻略（只采信 2 年内、同季节的笔记，并找时效分水岭），避免过时攻略。编排链：环境预检 → 交互式需求采集（分批追问，收齐 13 项必要信息）→ 路线规划 → 多渠道交叉验证 → 路书内容规划 → HTML 生成 → 图片补充与分级闸门校验 → 交付前自检。触发词：旅行攻略、路书、行程规划、自驾路线、几天怎么玩、行程核验、攻略图片、做一份路书。

- Skill: `wexexortexort/travel-roadbook-pipeline` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add wexexortexort/travel-roadbook-pipeline`
- Raw SKILL.md: https://api.skillmd.com/api/skills/wexexortexort/travel-roadbook-pipeline/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: Wexexortexort (https://skillmd.com/u/wexexortexort)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/wexexortexort/travel-roadbook-pipeline

---


# 路书生成器

把用户的一句话目的地，做成**可核实、图文匹配、可打印**的本地单文件 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 / 没装库**。

```bash
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 获取**（免费，仅在用户愿意时提供）：
1. https://lbs.amap.com 注册 + **实名认证**（个人开发者即可）
2. 控制台 → 应用管理 → 创建应用 → 添加 Key
3. 🔴 **服务平台必须选「Web 服务」** —— 选成「Web端(JS API)」调 REST 会返回
   `INVALID_USER_KEY`，而人会误以为 Key 本身错了。这是最高频的坑。
4. 数字签名**建议不启用**（启用了每次请求要额外算 sig）
5. 存成文件：`~/.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 若用户直接丢来一份攻略链接

此时**不要放弃采集**，而是把它当作已回答的部分，然后补齐缺口：

1. 解析链接内容（`xhs-shared-note-extract` / bsk）
2. 抽取其中已明确的字段（目的地/天数/景点）
3. **列出仍未明确的项**，有针对性地追问（通常缺的是：出发地县级、人数、
   驾驶人数、预算档、交通方式）
4. 若攻略里的里程/时间与你的核验结果冲突，**在 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` 技能

```python
# 关键参数
/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 搜索校准坐标再算里程。**

```python
/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 核验"。

**③ 在上述两条之后，再找该目的地的时效分水岭。**

1. **记下每条关键信息的发布日期**（同时满足①②才有资格进入这一步）
2. 找出**时效分水岭**（某个改变现状的事件：新路通车、充电桩投运、景区改制、施工封闭）
3. **分水岭之前的所有攻略信息整体作废**，不逐条采信

实证：某线路的分水岭 = **2025-09**（沿线充电桩集中投运），早于此的所有补能攻略全失效。

> ①②③ 是**递进关系**：先用时间与季节砍掉大部分，再用分水岭砍掉剩下的过时信息。
> 三道筛完仍留下的，才是可信情报。

### 2.6 逐段核验清单（产出物）

核验后必须给出下列判断，**不要只贴表格**：

1. **最长单日是哪天** —— 按纯驾驶时长排序，>8 h 单独标出
2. **补能次数是否够** —— 里程 ÷ 安全续航，向上取整再 +1 次冗余（纯电车硬约束）
3. **在途总时长** —— 纯驾驶 + 充电时间（每次 40–60 分钟），换算成 8 小时工作日
4. **过路费** —— 直接用 API 的 `tolls` 合计，比自己估准
5. **哪段要出发前复核** —— 跨无人区、新通车、施工管制段

---

## 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. **形态**（规范 §1.2，四选一）：把候选画成**线框预览**（内联 SVG 或等价的可视化渲染），
   让用户直接看到"铁路图长什么样、编辑长卷长什么样"，而不是读一段描述。
2. **配色**（规范 §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） —— **不设默认值，不要擅自决定，也不要套用任何"看起来合理"的方向**

**若用户另有自己的规范**：

1. 让用户提供 —— 以其规范为准，`references/roadbook-spec-v1.0.md` 仅用作兜底
2. 无法获取、也无需迁就时，回退到技能 `travel-roadbook-html`（编辑极简 / 零依赖 / 三栈字体 / 内联 SVG 信息图）
3. 并在交付时说明"视觉用了回退方案，若你有既定规范请提供，我按规范重做"

### 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`）：

1. **fontTools 是懒加载的** —— `TTFont(path)` 只读表目录，**截断文件也能骗过它**。
   完整性校验必须逐个真读：
   ```python
   ft = TTFont(p)
   for tag in list(ft.reader.keys()):
       if tag == 'GlyphOrder': continue
       _ = ft[tag]          # 触发真实读取与解压
   ```
2. **精简 `layout_features` 能省 15%** —— 收窄到
   `['kern','liga','clig','calt','locl','ccmp','mark','mkmk']`（`locl`/`ccmp`/`kern`
   是中文关键项，不要省）。实测 22 MB → 305 KB。
3. **大文件下载靠重试次数，不靠换源** —— 22 MB 字体经代理通常每 1–5 MB 断一次，
   但 **HTTP Range 续传有效**。用流式 256 KB 记录字节 + `Range: bytes=N-` + 多源轮换 +
   `tries=40`。

### 4.4 内联 SVG 信息图（比换皮值钱的一层）

信息图是路书的差异化价值：路线示意地图 / 补能空档条形图 / 海拔剖面 / 气温带。

🔴 **两条铁律**：

1. **装饰性图形不要画得像数据。** 一次交付中日卡顶部曾有"海拔走势带"曲线，被用户当场质疑
   "这个曲线是什么" → 已删除。
   → **要表达趋势就必须用真实数据拟合；否则干脆不做。** 任何自造图形的描述要保守。
2. **排完图必跑「包围盒重叠检测」** —— 肉眼审查不可靠。
   ```javascript
   // 遍历所有 <text>，两两比 getBBox()
   // ox > 1.5 && oy > 1.5 （用户坐标）→ 判定重叠
   ```

### 4.5 改动后的连带清理（**最易漏的一步**）

🔴 **改路线 / 改数字后，必须回头清理全部"下游结论"。**

一次实测需同步 **33 处**：总里程、日均、通行费、充电次数、预算表、
风险卡、复核清单、信息图坐标、版本号（`<title>` / 徽章 / colophon / 更新时间行四处）。

→ 做一张「受影响的结论清单」再逐项勾掉，不要凭记忆。

---

## Phase 5 · 图片补充与校验

> 🎯 **判断准则：图片是加分项，不是核心交付物。**
>
> 路书的价值在路线、里程、补能、风险这些**可核实的事实**上。
> 图片能让页面更好看，但不影响这份路书能不能用。
> **如果花在图上的时间开始挤压核验工作，就是做过头了** —— 退回轻量档或直接不放图。
>
> 因此本 Phase **默认走轻量档**：挡掉破图与畸形图即可，不追求每张图的语义精确匹配。

### 5.0 与用户确认（必做，30 秒的事）

HTML 生成后，先问四件事：

1. **要不要图？**（不要 → 走 A 档，本节结束）
2. 哪些天要？
3. 每天几张？
4. 要不要美食图？

（曾有用户明确要求"最后两天不要图片模块" —— 这类偏好必须问出来，不能默认。）

### 5.1 三档路径（**选一档，不要混做**）

| 档 | 适用 | 跑什么 | 成本 |
|---|---|---|---|
| **A · 不放图** | 用户不需要 / 行程短 / 时间紧 | 跳过整个 Phase 5。**页面没有图片位也完全成立** | 0 |
| **B · 轻量** ⭐**默认** | 用户要图，不苛求精确匹配 | 取候选 + **闸门①②**（有效性 + 尺寸）+ 落盘 + 收尾断言 | 极低 |
| **C · 完整** | 用户明确说"图必须对得上"，或图片位多、途经点易混 | 五道闸门全跑（额外含素材类型 + 语义一致性） | 中高 |

**默认 B。** 绝大多数情况用户在意的是"别放破图、别放明显不相关的图"，
而不是"每张图都逐字验证语义"。C 是为高要求场景准备的，不是必备流程。

```bash
# 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` 文案）：

```javascript
// 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 落盘：命名与元数据互锁

```bash
"$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 丢文件，自动识别替换
```

**原则：宁可少一张图，不要多一张错图。** 少一张图页面照样成立；
一张错图的代价是读者对整个文档的信任。

用户手选图通道（命名约定驱动，无需配置）：

```bash
"$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 都必做**）

```javascript
// 零破图
[...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` 等访问参数** ——
若出现，说明有链接漏进了页面：

```javascript
/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>` 超链接，否则笔记直链会被误判为外链：

```python
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 个图标引用
```

**加关键事实探针**（不可改写的数字字符串），任一缺失即报错：

```python
for probe in ["<总里程>", "<总时长>", "<通行费合计>", "<最长单日里程>", "<抢票日>", "300 km"]:
    if probe not in html: fail.append(probe)
```

> 若改版后类名全换，**必须同步更新检查脚本里的选择器与数量基准**，
> 否则检查项会空转（计到 0 却"通过"）。

### 6.3 浏览器实测（必做，别靠肉眼猜）

```bash
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 全空。**

→ 所有脚本一律：

```python
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` | 图片校验机制的设计说明与调参指南 |

