| name | chinese-route-to-gpx |
| description | Turn a text description of a Chinese road route — a sequence of place names and roads like "村A → 路X → 路Y → 村B → 路Z → 村C" — into a single road-following GPX track file. Drives the user's real browser (via Kimi WebBridge) against an online Chinese mapping service because OSM coverage of small Chinese roads and villages is too sparse for this task. Use this skill whenever the user gives a Chinese road route (cycling race route, driving itinerary, hiking sequence) and wants a GPX, even if they don't say "gpx" — phrases like "我有一段路线", "把这段变成轨迹", "生成 gpx", "赛事路线 gpx", "环陵问水", "国内骑行路线", "自行车赛路书", or pasting any "→"-arrowed sequence of Chinese place/road names should trigger this. Also use whenever the user mentions following an exact road order — single-segment OSRM routing cannot handle this because (a) the API has a waypoint limit and (b) OSM data lacks the road and POI coverage. The output is exactly one .gpx file (WGS-84, standard 1.1), with no intermediate JSON or screenshot files left behind. The skill auto-detects route problems (backtracks, U-turns) by reading rendered map screenshots and iterates until clean, so the user gets a polished result without intervention. |
Chinese road route → GPX
End-to-end: given a Chinese road route as text, deliver one polished WGS-84 GPX file.
Why this is mostly agent-driven, not script-driven
The mapping page and gpx.studio are interactive web apps whose DOM, network format, and behavior change without notice. A skill made of brittle scripts that scrape these pages would break the moment a CSS class is renamed. So this skill flips it around: you (the agent) drive the browser directly through Kimi WebBridge, using your ability to read screenshots and adapt. The few stable parts — math, file format conversion — are isolated into one helper script.
You are also the better judge for two key decisions: (1) which picker results look geographically wrong (you can see the map and know the route is in Beijing, not Hunan), and (2) which waypoint is causing a backtrack (you can see the actual loop in the rendered map). These would be unreliable as code, so they're your job.
Prerequisites
Before any step:
~/.kimi-webbridge/bin/kimi-webbridge status
Must report running: true and extension_connected: true. If not, surface to the user and stop.
Then verify the user is logged in to the mapping service — the routing API page returns errors when not authenticated. Look for the post-login signals (a 控制台 link, a 退出 link, or a visible username) rather than searching for the word "登录" which appears in many places in body text:
curl -s -X POST http://127.0.0.1:10086/command \
-H 'Content-Type: application/json' \
-d '{"action":"navigate","args":{"url":"https://lbs.amap.com/api/webservice/summary","newTab":true},"session":"route-check"}'
sleep 3
curl -s -X POST http://127.0.0.1:10086/command \
-H 'Content-Type: application/json' \
-d '{"action":"evaluate","args":{"code":"(() => { const allA = Array.from(document.querySelectorAll(\"a, button, span\")); const hasConsole = allA.some(a => a.textContent.trim() === \"控制台\" && a.offsetParent !== null); const hasLogout = allA.some(a => /^(退出|logout)$/i.test((a.textContent || \"\").trim()) && a.offsetParent !== null); const hasLoginCTA = allA.some(a => /^(登录|登 录|sign\\s*in|log\\s*in)$/i.test((a.textContent || \"\").trim()) && a.offsetParent !== null); return {logged_in: (hasConsole || hasLogout) && !hasLoginCTA, hasConsole, hasLogout, hasLoginCTA}; })()"},"session":"route-check"}'
Interpret the JSON: if logged_in is true (i.e., 控制台 or 退出 visible AND no prominent 登录 button), proceed. If false, tell the user: 请先在浏览器里登录 https://lbs.amap.com (账号密码或扫码都行),登录后让我继续。 Stop and wait — do not try to drive the login flow yourself. Close the check session before continuing:
curl -s -X POST http://127.0.0.1:10086/command -d '{"action":"close_session","args":{},"session":"route-check"}'
The workflow
- Parse the user's text into an ordered point list, classifying each as place or road.
- Plan picker queries: merge adjacent road-road pairs into intersection queries.
- Pick coordinates on https://lbs.amap.com/tools/picker — driven step-by-step. See
references/picker.md.
- Visually validate the coordinates by checking the map markers; re-pick anomalies with region prefix. See
references/picker.md.
- Plan segments ≤ 16 waypoints each, split at a natural landmark.
- Run driving direction on https://lbs.amap.com/api/webservice/summary, one segment at a time. See
references/driving.md.
- Merge to GPX with
python scripts/route_to_gpx.py -o /tmp/route_attempt.gpx /tmp/seg1.json [seg2.json ...] — converts GCJ-02 → WGS-84.
- Verify visually on https://gpx.studio/app via drag-drop + screenshot. See
references/verify.md.
- Identify any anomalies in the screenshot (backtracks, U-turns, off-route loops). See
references/anomalies.md for the patterns and how to figure out which waypoint is the cause.
- Auto-fix: delete the suspect waypoint, re-run only the affected segment, re-merge, re-verify.
- Iterate until the screenshot is clean or 6 attempts elapsed.
- Deliver: move the final GPX to the user's working directory with a clean name, delete all
/tmp/ intermediates, close webbridge sessions.
Track this loop with TaskCreate so you don't lose your place. Don't shortcut the verify step — it's what makes the output reliable.
Step 1-2 · Parse and prepare queries
Read the user's text. Split on arrows (→ -> ↓ >>), numbered/bulleted lists. Preserve order. Strip parentheticals like (爬坡计时起点) — those are notes, not separate points.
Classify each point:
| Heuristic | Class |
|---|
ends in 路 街 大道 巷 线 | road |
ends in 村 镇 苑 区 园 观景台 公园 学校 大厦 广场 站 寺 桥 | place |
| Otherwise — best guess; default to place | |
Build the picker query list:
- For each adjacent road→road pair, emit ONE intersection query
road_i,road_{i+1} (comma-joined, no space). This represents the transition between the two roads — far more accurate than picking each road separately, because single-road picker often returns a road terminal.
- For place→road / road→place / place→place: emit each as its own query.
- Same intersection in reverse later (outbound
西拓路,李流路 and return 李流路,西拓路) is the SAME physical point — pick once, reuse.
- Keep duplicates in the route: if a road or place appears more than once in the user's list, that's a fact about the route (e.g., the rider doubles back over a road). Keep both occurrences in your internal point list initially — they share a coordinate. The visual verification step (step 9) will tell you if one of them is causing trouble; the typical fix for a same-coord duplicate is to drop the later occurrence.
The why: picker accuracy depends on what the service has indexed. A single road name like 高芹路 returns one arbitrary point — often a terminal that's far from the actual stretch the route uses. An intersection 路A,路B returns the actual cross — a real, specific transition. This trick alone fixed a 50 km of phantom mileage in the original Liucun test.
Step 3-4 · Picking coordinates
Read references/picker.md carefully — it has the exact JS snippets to batch-search the picker page, the road-intersection comma syntax, the region prefix fallback, and how to recognize a bad result.
Step 5 · Planning segments
Total waypoints between origin and destination = len(query_list) - 2. The driving API rejects > 16. If you're over, split at a landmark — usually the most distant or topologically important point (climb peak, turnaround village). The split point becomes both segment 1's destination and segment 2's origin. route_to_gpx.py will dedupe the boundary when it merges.
Example from the Liucun loop: 19 points → split at 北齐岭观景台 (the climb peak) → segment 1 has 12 points (10 waypoints), segment 2 has 8 points (6 waypoints).
The split point must be the same exact coordinate in both segments (segment 1's destination == segment 2's origin). If you use the picker coordinate consistently this is automatic. If they differ even slightly, route_to_gpx.py won't dedupe the boundary and you may see a tiny gap or jump in the merged track.
If you're under 16 total, don't split — one segment is simpler.
Step 6 · Calling the driving API
Read references/driving.md for the exact webbridge commands (DOM selectors, network capture, run-button click). For each segment, save the JSON response to /tmp/segN.json.
Step 7 · Convert to GPX
This is the one stable part — pure conversion math, no UI scraping. The script lives at <skill-dir>/scripts/route_to_gpx.py (the skill loader tells you the base directory). Use an absolute path so it works regardless of your shell's pwd:
python /Users/moonshot/Downloads/work/amazing_tools/skills/chinese-route-to-gpx/scripts/route_to_gpx.py \
-o /tmp/route_attempt.gpx /tmp/seg1.json /tmp/seg2.json
Always include the GCJ-02 → WGS-84 conversion (which is the default — never use --no-convert). Without it, the GPX appears offset by 300–600 m on standard maps.
Step 8 · Visually verify
Read references/verify.md for the exact gpx.studio drag-drop sequence. After upload, take a screenshot and read it.
Step 9 · Identify anomalies
This is YOUR job, not a script's. Read references/anomalies.md for the visual patterns to look for and how to identify which waypoint is causing each one. Then in Step 10 you delete that waypoint and re-run.
Step 10-11 · Auto-fix loop
The fix is almost always delete the offending waypoint, not add more. Picker imprecision puts some waypoints in geographically wrong spots; removing them lets the router pick the natural connecting road, which is what the user actually meant.
For each iteration:
- Identify the worst anomaly in the screenshot
- Identify which waypoint is closest to it / responsible for it
- Remove that waypoint from the relevant segment's waypoint list (overwrite the in-memory list — also overwrite
/tmp/segN.json when you re-run it)
- Re-run ONLY the affected segment with
references/driving.md instructions
- Re-merge with
scripts/route_to_gpx.py — same command, same output path (this overwrites)
- Re-verify with
references/verify.md
Keep an in-memory list of which waypoints you've already dropped so you don't oscillate (drop W1 → bad → drop W2 → restore W1 → same shape as iteration 1). If the anomaly recurs after a removal, try a DIFFERENT waypoint rather than restoring the previously-removed one. If you can't identify a clear culprit, or the same fix doesn't help after two tries, look at references/anomalies.md § "when stuck."
Stop iterating when:
- The screenshot looks clean (no backtracks, no tight loops, no off-route detours), OR
- Six iterations have elapsed (further tweaking usually doesn't help — small residual issues are road-network features the route can't avoid)
Step 12 · Deliver
When done:
- Move the final GPX to the user's working directory — that's wherever the shell is currently running (
pwd). Name it route_<keyword>.gpx where <keyword> is a short distinctive piece of the route (e.g. route_liucun_loop.gpx). If a file with that name already exists, suffix with _v2 etc., or ask once at the end how to handle the overwrite.
- Delete all intermediates:
rm -f /tmp/seg*.json /tmp/route_attempt*.gpx /tmp/verify*.jpg /tmp/picker_*.jpg /tmp/queries.txt. The user asked for exactly one .gpx out — keep that promise.
- Close webbridge sessions:
for s in route-picker route-driving gpx-verify route-check; do curl -s -X POST http://127.0.0.1:10086/command -d "{\"action\":\"close_session\",\"args\":{},\"session\":\"$s\"}"; done
- Tell the user the final distance, total duration, and the GPX path. One or two sentences — don't dump every iteration's stats. Example:
route_liucun_loop.gpx 已生成,57.7 km / 83 分钟,与官方 57.46 km 基本一致。
Known limitations to mention if relevant
- The GPX has no
<ele> (elevation) tags — the driving API doesn't return elevation. If the user needs an elevation profile (for a climb analysis, for example), they'll need to post-process with a DEM lookup. Don't try to add elevation yourself unless asked.
- Routing is
direction/driving (车辆路径). For cycling/hiking routes the actual path may differ in places (one-way streets that bikes don't follow, etc.) — usually close enough, but worth flagging if the route looks oddly main-road-biased.
Style notes
- Don't ask mid-loop. Picker mishits, deletion choices — figure them out from the data you have. Brief end-of-turn summary is good; mid-run check-ins are noise.
- One file out. No README, no waypoint dump, no leftover screenshots. The skill's promise is "text in, one gpx out."
/tmp/ is your scratch space. Use it freely, clean up at the end.
- Iteration is correct. Deleting waypoints is the right fix, not a workaround. Picker imprecision is the root cause.
- Don't undertrigger. Any Chinese road route the user wants as a track file is in scope, even if they don't say "GPX" explicitly.
Session hygiene — close unused webbridge windows promptly
Each kimi-webbridge session name opens its own browser tab. Leaving stale tabs lying around causes real problems: the extension's per-tab state can interfere (network capture sometimes spills across tabs of the same domain, a stale picker page can capture a navigate intended for a new session, screenshots may grab the wrong active tab). The rule is close a session as soon as you're done with its page.
The expected session lifecycle for one run:
| Session | Opens | Closes |
|---|
route-check | Prereqs login check | Right after — before doing anything else |
route-picker | Step 3 | After ALL picking is done (including region-prefix re-picks) — before opening route-driving |
route-driving | Step 6 | After the final iteration's driving call — before delivery cleanup |
gpx-verify | Step 8 | After the final verification screenshot — at delivery time |
Concretely: you should never have all four sessions open at the same time. If you find yourself with three mapping tabs open, close the ones whose job is done. The closing command is curl -s -X POST http://127.0.0.1:10086/command -d '{"action":"close_session","args":{},"session":"<name>"}' — call it the moment the page's purpose is complete, not just at the end of the run.
References — load when you need them
references/picker.md — the picker page: exact webbridge commands, road-intersection comma syntax, region prefix fallback, recognizing bad results
references/driving.md — driving direction API: exact webbridge sequence to switch dropdown, fill inputs, capture response, validate
references/verify.md — gpx.studio: how to clear stale state, the drag-drop trick (drop on canvas not body), reading stats from the rendered page
references/anomalies.md — Visual patterns: backtrack/U-turn/lollipop shapes; how to identify the culprit waypoint from each shape; what to delete
references/troubleshooting.md — Failure modes you may hit and what to do (name collisions, API errors, daemon issues, gpx.studio stale state)
What's in scripts/
Only one script — scripts/route_to_gpx.py. It does GCJ-02 → WGS-84 conversion and concatenates step polylines into a standard GPX 1.1 file. Pure stdlib Python, no web interaction, deterministic. Everything else is you driving the browser directly.