旅行攻略构建器
把「一趟旅行」变成一份单文件、可交互、可分享的 HTML 攻略。
核心认知(决定了这个 Skill 该怎么用)
- 这不是「排路线」工具,是「把排好的路线做准、做成能用」的流水线。
真实案例里,11 天 / 81 个 POI / 4 段航班的行程骨架在第一版就冻结了,
后面 10 个版本全在修数据准确性和交互可用性。
所以价值在于确定性、可复现、能自己找错。
唯一的例外是第 0 阶段的「自动找点」(见下):用户给城市 + 天数,
流水线能把每天的候选点找齐、排上时刻,但「去不去」仍由人拍板。
- 三层架构,只有中间和下层每趟要动:
| 层 | 内容 | 每趟 |
|---|---|---|
| 外壳模板层 | 地图(多底图 + 每日配色)、成本编辑器(人数 + 汇率 + 双币种:当地币 + 人民币)、tab 切换、打印/存 PDF、出发倒计时、预订/打包清单派生视图、多导航服务商 | 完全复用 |
| 数据契约层 | trip.json(TRIP / Day / POI / Extra / hotel / Flight) |
换数据 |
| 目的地知识层 | knowledge.json(自驾须知 / 天气穿搭 / 签证证件 / 实用信息 / 固定预订 / 打包清单) |
每趟重写 |
-
两种读者。 用户本人要能改成本、看全貌;同行人要能直接照着走(时刻、导航、预订状态一眼可见)。
这两类派生视图不是「多余的交互」,是刚需。 -
币种与底图都由数据决定,不要改模板。
当地币种由目的地识别(scripts/currency.py),页面固定「当地币 + 人民币」双币展示;
底图与导航服务商由meta决定(境内默认腾讯,境外默认 Google + 腾讯双通道)。
改模板去适配某个国家,是上一版最大的坑(v11 把 AUD 写死)。
目录
assets/template.html 参数化模板(单文件,内联 Leaflet 的 JS 与 CSS)
assets/example-wa/ 脱敏演示:trip.json + knowledge.json(占位数据,仅作结构参考,可直接改/删)
scripts/new_trip.py ★输入「城市 + 天数」生成骨架(自动识别币种 / 底图 / 导航)
scripts/currency.py 目的地 → 当地币识别(符号 / 格式化 / 近似汇率)
scripts/coord.py ★坐标基准换算 WGS-84 / GCJ-02 / BD-09,可整份 trip.json 换基准并重生成链接
scripts/poi_plan.py ① 从骨架生成「地图检索工单」(本地,不联网)
scripts/poi_raw_from_mcp.py ②' 把地图服务返回的文本转成 poi_raw/<请求ID>.json
scripts/poi_ingest.py ③ 原始检索结果 → 归一化 / 类别映射 / 去重 / 打分 / 候选池
scripts/poi_apply.py ⑤ 候选合并进 trip.json:排时刻 / 算车程 / 重算当日汇总 / 生成链接
scripts/validate.py 数据自检(R1–R25)
scripts/build.py 注入数据 → 产出单文件 HTML(默认先校验 + 语法自检)
scripts/verify_render.js jsdom 真跑一遍产物,抓运行时异常
references/data-contract.md 字段规范(trip.json)
references/knowledge-slots.md 目的地知识层要写什么
references/poi-discovery.md ★自动找 POI 的完整流程(第 0 阶段)
references/validate-rules.md 25 条校验规则及各自对应的真实事故
references/pitfalls.md 模板隐式契约与工作流陷阱 ★接手先读这个
references/source-recovery.md 从已有攻略 / 旧版本取回数据
references/map-compliance.md 地图合规:白名单、腾讯底图现状、境内迁移待办
标准流程
A. 「我说城市和天数,你帮我排」——最短路径
用户给出「去哪几个城市、各待几天、什么时候出发、几个人」时,从这里开始:
cd <skill>/scripts
python new_trip.py --out ./关西5日 --title "大阪 · 京都 5 天" \
--cities "大阪:3,京都:2" --start 2026-11-01 --people 4 --build
--cities "城市:天数,..." 一次给出规划。脚本据此自动完成:
| 自动项 | 规则 |
|---|---|
| 币种 | 从城市名识别(大阪/京都 → JPY),并写入 currency 与近似汇率 |
| 逐日骨架 | 按天数铺开每一天,日期自动推算,region 用城市名,人数落到 head |
| 地图配置 | 境内 → coordSystem=gcj02 + 默认腾讯底图 + navProviders=["tencent","amap"];境外 → wgs84 + 默认「街道」底图 + ["google","tencent"] |
| 城市中心 | 供 poi_plan 定位圆心用(--centers 可覆盖) |
骨架里不放示例 POI(--demo-pois 才放)。空骨架会渲染成「这一天还没有排点」的引导态,
等第 0 阶段把点排进来——这正是「自动排点」的入口。
> 币种想手工指定就 --currency JPY;想覆盖内置汇率用 --rate 0.048(内置汇率只是量级占位,出发前必须核对)。
B. 自动找点(承接 A,把骨架填满)
分工是刻意的:机器负责「找得准」,人负责「去不去」。
坐标 / 名称 / 类别 / 营业时间 / 车程 / 时刻由脚本负责(这些正是历史上翻车最多的),
desc / tip / book / 具体价位留给人。完整说明见 references/poi-discovery.md。
# ① 生成检索工单(本地,不联网)。同区域的连续多天会自动合并成一批检索
python poi_plan.py --trip 关西5日/trip.json --knowledge 关西5日/knowledge.json
# ② 用地图能力执行工单里的每条 query(★ 首选腾讯地图连接器)
# 把返回文本存成文件,再用脚本转成结构化 JSON;顺便调「路线规划」存 routes.json
python poi_raw_from_mcp.py --id D1-景点 --text raw_D1景点.txt --dir 关西5日/poi_raw
# ③ 归一化 + 打分,产出候选池与评审表
python poi_ingest.py --plan 关西5日/poi_plan.json --raw 关西5日/poi_raw \
--trip 关西5日/trip.json --keep-rejected
# ④ 用户勾选 → selection.json(agent 根据用户口头选择生成)
# ⑤ 先预览,再落盘(自动备份)
python poi_apply.py --trip 关西5日/trip.json --candidates 关西5日/poi_candidates.json \
--selection 关西5日/selection.json [--routes 关西5日/routes.json]
python poi_apply.py ... --write
要点:
- 新点的坐标直接来自地图服务,天然是官方 GPS —— 这一档正好治 R1/R2/R3 那三个坐标事故。
- 默认只填当天的空档,不动已有时刻。 手工排满的日子会被如实拒绝,这时要么加
--reschedule(会改动原有时刻,逐条报告),要么这一批留到下一版——别硬塞。 - 空白天会按类别理想时段铺开(美食落午/晚餐档、夜生活落晚上),不会出现「酒吧排在 08:00」。
- 合并后这些点带
src: auto/verif: false,validate.py的 R20 会把它们列成「逐条过目」清单。
--strict下 R20 不通过 = 还没定稿。 - 住宿类永不自动检索,酒店由用户拍板。
- 港澳台 / 境外行程:合并完还要换一次坐标基准。 腾讯检索回来的是 GCJ-02,
而 Google 按 WGS-84 解释,直接用会整体偏约 600 m:
bash python coord.py rewrite --trip 香港/trip.json --from gcj02 --to wgs84 --write
(中国大陆行程不要做这一步。R26 会替你把关。)
C. 手工填数据(没有走自动找点时)
按 references/data-contract.md 填 trip.json,按 references/knowledge-slots.md 重写 knowledge.json:
python validate.py --trip 北海道/trip.json --knowledge 北海道/knowledge.json --ref-per-person 15000
python build.py --trip 北海道/trip.json --knowledge 北海道/knowledge.json \
--out 北海道 --name "北海道8天自驾" --version v1 --date 2026-02-01 --note "初版"
NODE_PATH=<node workspace>/node_modules node verify_render.js 北海道/北海道8天自驾_v1.html 北海道/trip.json
D. 改一版(用户提了修改意见)
- 改
trip.json/knowledge.json。 - 用 baseline 抓漏改——这一步能自动发现「改了 POI 没改 tags / 里程 / drive.note」:
bash python validate.py --trip new.json --knowledge knowledge.json --baseline old_trip.json - 逐条过
references/pitfalls.md第二节的联动表。 build.py --version vN --note "本版改了什么",只产出一个带版本号的文件。
E. 把一份已有攻略复用到新一趟
- 取回旧攻略源码、抽出
TRIP数据与面板 — 见references/source-recovery.md
(含docs.qq.com/page/...这类链接拿不到真实 file_id 的处理方式)。 - 旧数据当作
trip.json的起点,逐字段核对而不是照抄——旧数据里的错误会一起搬过来。 - 目的地知识层整段重写(换国家/地区必须全部重查)。
币种是数据驱动的(不要再改模板)
- 模板里有
CUR_INFO表(符号 / 名称 / locale / 小数位),金额格式化只有一套fmtCur();页面固定双币显示。 trip.currency两种写法都吃:"JPY"—— 简写,汇率回退到meta.rate;{"code":"JPY","symbol":"¥","cnyRate":0.048}—— 推荐,可覆盖符号与汇率。- 支持 48 种币种(AUD/USD/EUR/GBP/JPY/KRW/THB/VND/HKD/TWD…)。中国内地/香港/澳门/台湾是 CNY/HKD/MOP/TWD 四种货币(币种是货币事实)。
- 单独识别:
bash python scripts/currency.py detect "日本 北海道 8 天" # → JPY python scripts/currency.py list # 全部币种档案 python scripts/currency.py rules # 识别规则 - 内置汇率是量级近似值,写进页面只为让骨架能算数;出发前必须核对,或直接改
trip.currency.cnyRate(页面顶部与成本 tab 也能实时改)。 build.py会做币种自检:模板里散落的写死币种代码会被揪出来;validate.py的 R24/R25 核对字段合法性与「币种 ↔ 目的地」是否对得上。
地图:底图与导航都由 meta 决定
先决定「这一趟算境外还是境内」
| 目的地 | 导航(第一个写进数据) | 坐标基准 | 默认底图 |
|---|---|---|---|
| 中国大陆(CNY) | 腾讯 → 高德 | GCJ-02 | 腾讯 |
| 港澳台 + 所有境外国家 | Google → 腾讯 | WGS-84 | 街道(Esri) |
港澳台按「境外旅游」处理(用户明确要求:境外导航优先 Google,保留腾讯为备选)。
代价是要记住一条硬事实:
> 腾讯 / 高德给港澳台的坐标也是 GCJ-02,与 WGS-84 相差约 500–600 m
> (香港实测 596 m、台北 501 m)。把这种数字交给 Google 按 WGS-84 解释,
> 点位会偏到隔壁街区。所以港澳台/境外行程只要用了腾讯检索的点,就必须先换算:
>
> bash
> python coord.py rewrite --trip 香港/trip.json --from gcj02 --to wgs84 --write
>
>
> 它会逐个点报告位移(596 m)、按 meta.navProviders[0] 重生成导航链接、
> 并把 meta.coordSystem 改成 wgs84。官网/预约类外链不会被误改。
meta 键
meta 键 |
作用 |
|---|---|
baseLayers |
挑选/覆盖底图列表(元素可为 "tencent" 这样的 id,或自定义 {id,name,url,…} 对象)。缺省用模板内置全部图层 |
defaultLayer |
默认打开的底图层 id。境内应设 tencent,境外缺省 street |
navProviders |
导航服务商优先级列表,如 ["tencent","amap"];第一个用于生成数据里的 poi.link,页面会给每个 provider 渲染一个导航按钮 |
navApp |
导航 URI 的 referer / src 来源名 |
coordSystem |
gcj02 或 wgs84,必须等于数据里坐标的真实基准。wgs84 时生成链接会显式声明(腾讯 coord_type=1、高德 coordinate=wgs84),否则导航会偏几百米。R26 会核对它与链接、默认底图是否自洽 |
navProvider |
单值旧写法,仍兼容(navProviders 优先) |
内置底图库(LAYER_LIB,每层带 datum 标注坐标基准):
street/sat(Esri)、voyager(CARTO)、osm——datum: wgs84,历史遗留层,境外可看,对外分享有合规风险;tencent/tencent2——datum: gcj02,腾讯地图瓦片,免 key,用 Leaflet 原生{-y}翻转(腾讯 y 轴原点在左下)。境内这是合规底图,也是境内行程的默认层。
一组坐标只能有一个基准,底图选错基准会整体偏移数百米。 模板对此有三重保护:
meta.defaultLayer若与meta.coordSystem冲突,自动跳过它,改用第一个基准相符的层;- 基准不符的底图按钮会带
⚠与提示(腾讯 ⚠); validate.py的 R26 直接判 FAIL:coordSystem=gcj02却用了 Google 链接、
或wgs84却用腾讯/高德链接而没带coord_type=1/coordinate=wgs84、或默认底图基准不符。
导航服务商:google / tencent / amap / baidu(百度仅在 gcj02 下出现)。
validate.py 的 coords_from_link() 能反解这四家的链接做 R1 校验。
交付前必做的四件事
validate.py0 个 FAIL;WARN 逐条看过,再用--strict跑一遍。
有自动收录的点时重点看 R20(逐条过目清单)与 R23(车程仍是估算值)——
这两条不消掉就不算定稿。build.py的内联脚本语法自检通过(构建时自动做),以及模板层目的地残留扫描(--strict-template命中即中止)。verify_render.js判定OK——它会逐日渲染每个 tab,能抓出「语法合法但一运行就报错」。- 人眼过一遍:地图能开、点几个 POI、切成本 tab 改个数看人均联动、点一次导航与小红书按钮。
(jsdom 没有布局,地图相关断言它验不了。)
铁律
cost = 0是「未核实」,不是「免费」。 预算初稿提交后必须做一次整体上浮复核(历史上首版低估 14%)。- 坐标必须官方 GPS,不要凭印象填。 一个坐标在模板里存了三份(
lat/lng、导航链接、地图标记),
改一处必须改三处;换住宿地还要连带改次日出发口径与当日里程。 book首字符只认🔴🟠🟡⚪,其余会静默降级为「无需预订」。- 只生成一个 HTML,文件名带版本号。
- 涉及第三方平台行为的结论必须附验证证据,否则会在下一版被推翻、再推翻回来。
- 酒店只推荐区域与一晚预算区间,不要指定具体酒店(由用户本人拍板)。
- 人数与预算上限都是参数,每次旅行都可能不同,不可硬编码。
- 验证脚本宁可少报,不可错报。 粗断言造成的假阳性和真 bug 一样耗时。
- 自动找来的点默认是「草稿」,不是「已定案」。 带
src=auto的点坐标准、名称准,
但「值不值得去、待多久、花多少钱、要不要预订」机器不猜——book故意留空。
清单见 R20,不逐条过目就不要交付。 - 估算车程不能活到定稿。 没有真实路线数据时用直线×系数估算,并且会在
drive.note里自报家门;
R23 会把每一处都揪出来。里程错是历史上最高频的一类错误。 - 模板层不得出现本趟的目的地 / 城市名,也不得写死币种。 一旦写死,换一趟就会直接输出错误内容
(v11 把「6 人 · 人均 4,500 HKD」写死在整团费用说明里,换目的地后那句话就是错的)。
这类文字一律进trip.meta.extrasNote/meta.badges或知识层。
build.py会自动扫一遍模板并报出「专名 → 行号」与写死的币种;--strict-template可让它直接中止构建。 - 坐标基准必须与导航链接、底图三者一致(R26)。 中国大陆的腾讯/高德/百度对港澳台同样套用 GCJ-02,
与 WGS-84 差约 500–600 m(香港实测 596 m)。所以:港澳台/境外只要用了腾讯检索的点,
走 Google 之前必须coord.py rewrite --from gcj02 --to wgs84。
一组坐标只能有一个基准——把 GCJ 的数字交给 Google 按 WGS-84 解释,点位会偏到隔壁街区,
而页面上看不出任何异常。 - 地图能力的合规红线见
references/map-compliance.md:只允许腾讯 / 高德 / 百度 / 天地图。
腾讯地图已作为合规底图接入(免 key),中国大陆行程默认用它;Esri/CARTO/OSM 是历史遗留层,
不要在此基础上再引入新的第三方地图源。境内目的地必须先迁移渲染层到腾讯地图 GL JS(见该文档的迁移清单)。
港澳台已按「境外旅游」处理(默认街道底图 + Google 优先),这是用户明确选择的取舍,
合规风险已记录在该文档,不要擅自改回。
已知限制(不要误判为已完成)
| 限制 | 影响 | 现状 |
|---|---|---|
| 境内 / 港澳台渲染层未迁移 | 底图已有腾讯瓦片(免 key,合规),但渲染容器仍是 Leaflet。中国大陆要正式对外分享须迁到腾讯地图 GL JS(底图合规 ≠ 渲染合规);港澳台现在默认走 Esri 街道底图(历史遗留层),合规风险更高一档 | 部分完成。见 map-compliance.md 的迁移清单 |
| 港澳台坐标需要人工换算一步 | 腾讯/高德返回的是 GCJ-02,走 Google 前必须 coord.py rewrite。忘了这一步不会报错,只会整体偏 600 m——但 R26 会拦下来 |
有工具 + 有校验,但流程上仍是独立一步 |
| 腾讯地图连接器只覆盖中国境内 | 境外 / 港澳台的检索会静默返回境内坐标——实测:地理编码「希腊 雅典 卫城」返回的是深圳南山 22.53,113.97;在雅典卫城坐标周边搜「景点」返回「未找到结果」。比不返回更危险,一个真坐标加一个假坐标混在同一份数据里很难发现 | 境外行程不要用地图连接器批量找点:坐标必须逐点取可核查来源(Wikipedia 的坐标是 WGS-84)并标注出处;new_trip.py 的骨架与手工录入仍照常可用 |
| 自动检索依赖地图连接器 | 脚本只负责出工单与消费结果,真正的检索要靠 agent 触发地图连接器;poi_raw_from_mcp.py 负责把返回文本转成 JSON |
依赖连接器授权(腾讯地图连接器已连时,中国大陆行程可直接跑通) |
| 没有回归测试 | 改 scripts/ 里的脚本只能手工冒烟,容易改坏而不自知 |
无 tests/。改动后务必重跑「西澳真实数据 + 一个空骨架(JPY)+ 港澳台(HKD,验证坐标换算)」三轮 |
| 没有 schema 校验 | 字段类型写错(如 cost 写成 "待定")不会在构建前被拦下,可能到渲染才炸 |
R17 只查必填项的有无,不查类型 |
| 路线合理性无法自动校验 | 擅长「把已有的行程做准」,也能「给区域补点」,但不擅长从零排出好路线 | 属方法论边界,非缺陷 |
| 内置汇率是近似量级 | 只是占位值,不是报价 | 页面可实时改;出发前必须核对 |
| 坐标换算只解决基准,不提升精度 | coord.py 不会让点位更准——它只保证「同一组数字不被两种基准解释」 |
原始点位质量仍取决于地图服务 |
环境依赖
- Python 3(标准库即可)。
- 可选:Node.js ——
build.py用它做语法自检;verify_render.js需要jsdom
(装在 WorkBuddy 托管目录<node workspace>/node_modules,通过NODE_PATH引用)。
没有 Node 时构建照常,只是少了自动验证这两道关。 - 可选:地图连接器(腾讯地图)—— 第 0 阶段「自动找点」需要它。