Road Trip Planner / 自驾游路线规划
China only — 本 skill 基于高德地图 API,仅适用于中国大陆地区。
Dependency: @lbs-amap/personal-map (ClawHub) / GitHub
Trigger Conditions / 触发条件
Trigger when the user has any of these intents:
当用户出现以下任意意图时,主动调用本 skill:
- Plan a road trip / road trip route / loop route (规划自驾游路线 / 公路旅行 / 环线)
- Design a travel itinerary involving multi-city driving (设计涉及多地驾驶的旅游行程)
- Generate a personal map / travel map / route QR code (生成个人地图 / 旅游地图 / 路线二维码)
- Control daily driving time / avoid fatigued driving (控制每日驾驶时间 / 避免疲劳驾驶)
- Query mileage, gas stations, hospitals along the route (查询沿途里程、加油站、医院等补给点)
Dependencies / 前置依赖
| Dependency | Source | Description |
|---|---|---|
lbs-amap/personal-map | ClawHub | Amap Web Service API wrapper / 高德 Web 服务 API 封装。安装命令:openclaw skills install personal-map。提供 AMapPersonalMapClient 类(import 路径 scripts.amap_personal_map_client),本仓库自身不含该脚本。 |
| Amap API Key | Amap Open Platform | Required env var: AMAP_API_KEY / 需配置环境变量 |
Dependency Availability Check / 依赖可用性检查
Before any API call, verify that the personal-map skill is installed and the API key is configured. If either is missing, stop and tell the user what's needed — do not attempt to proceed.
在任何 API 调用之前,先检查 personal-map skill 是否已安装以及 API Key 是否已配置。任一缺失则停止并告知用户缺失项,不要尝试继续。
Checklist / 检查清单:
1. personal-map skill installed? → No → 提示: "请先安装 personal-map skill,运行: openclaw skills install personal-map"
2. AMAP_API_KEY set? → No → 提示: "请先配置高德 API Key,获取地址 https://lbs.amap.com/"
3. AMapPersonalMapClient importable? → No → 提示: "personal-map skill 版本可能不兼容,请确认其提供 AMapPersonalMapClient 类"
Set the environment variable in the active shell; never put a real key in the skill or source code:
# macOS / Linux
export AMAP_API_KEY='your_key'
# PowerShell
$env:AMAP_API_KEY = 'your_key'
# Windows cmd
set "AMAP_API_KEY=your_key"
Core Principles / 核心原则
-
No fatigued driving / 不疲劳驾驶:Keep daily actual driving time under 6 hours; 4–5 hours is a planning target, while short sightseeing or transfer days may be shorter. For high-altitude, mountainous, or winding roads, further reduce driving time.(每日实际驾驶时间控制在 6 小时以内;4-5 小时是规划目标,纯玩或短途换乘日可更短。高原、山区、弯道多的路段进一步压缩。)
-
Continuous route / 连续路线:Merge all daily waypoints into one continuous route, either a closed loop or with a clear endpoint.(所有天的节点合并为一条连续路线,首尾闭合或明确终点。)
-
Closed loop / 闭合环线:If start and end are the same city, ensure the endpoint is preserved after deduplication.(如果起点和终点是同一城市,确保终点节点在去重后仍然保留在路线末尾。)
-
Daily flexibility / 每日弹性:Leave buffer room in the itinerary; avoid over-scheduling.(行程中保留机动空间,避免把时间排得太满。)
Workflow / 工作流程
Step 1: Confirm itinerary framework / 确认行程框架
Confirm with the user:
- Start point, end point (loop or not)
- Total number of days
- Must-visit attractions / cities
- Travel season (affects road conditions and accommodation)
- Exact departure dates, vehicle type, and any driving/health constraints when safety or road choice depends on them
与用户确认以下信息:起点、终点(是否环线)、总天数、必去景点/途经城市、出发季节、准确出发日期、车型及会影响路线选择的驾驶/健康限制。
If the date is unspecified, provide a provisional seasonal plan and explicitly mark the real-time checks as pending.
Step 2: Design daily waypoints / 设计每日节点
Split waypoints by day following these rules:
- Target 4–6 hours of driving per day, never exceed 6 hours; short sightseeing or transfer days may be shorter
- Each day's
pointslist includes the core locations passed that day - The end city of each day must also serve as the start point for the next day (for mileage calculation)
按天拆分节点:每天驾驶 4-6 小时,不超过 6 小时;每天的 points 列表包含当天途经的核心地点;终点城市需同时作为下一天的起点。
Step 3: Call Amap APIs / 调用高德 API
Uses AMapPersonalMapClient provided by the personal-map skill. The class is at scripts.amap_personal_map_client inside that skill's directory — this is the canonical import path. This repo itself contains no scripts.
import os
from scripts.amap_personal_map_client import AMapPersonalMapClient
# Check API key BEFORE creating client
api_key = os.getenv("AMAP_API_KEY")
if not api_key:
print("AMAP_API_KEY not set. Get a key at https://lbs.amap.com/ and run:")
print(" Set AMAP_API_KEY in your active shell (see Dependencies above).")
# stop here — do not proceed
# Create client (auto-reads AMAP_API_KEY from env):
client = AMapPersonalMapClient()
# Or pass explicitly: client = AMapPersonalMapClient(api_key="your_key")
Normalize every response before using it. The client does not raise API failures, but a failure can be either an error dict or an error dict as the first item of a list (for example, POI search). Always check both shapes:
def api_error_message(result):
if isinstance(result, dict) and "error" in result:
return result.get("message", str(result["error"]))
if (
isinstance(result, list)
and result
and isinstance(result[0], dict)
and "error" in result[0]
):
return result[0].get("message", str(result[0]["error"]))
return None
geo = client.maps_geo("北京市朝阳区", "北京")
if error := api_error_message(geo):
# Stop handling this location; never reuse another location's coordinates.
raise ValueError(f"Cannot resolve this location: {error}")
# Convert external fields once. All later workflow steps use only this shape.
point = {
"name": "地点名称",
"lon": float(geo["longitude"]),
"lat": float(geo["latitude"]),
"poiId": "", # fill from the verified POI-search result
"required": False, # True for the start, end, and user must-visit points
}
For each location:
maps_geo(address, city)— get precise coordinates. Pass the city name as second arg for better accuracy.maps_text_search(keywords, city, offset=5)— get poiId. Useoffset=5to limit results (API default is 20).- Verify search result coordinates are within 30km of geo coordinates to prevent cross-city mismatches
time.sleep(0.3)between API calls to avoid QPS throttling
对每个地点执行:maps_geo 获取坐标 → 立即标准化为 lon/lat → maps_text_search 获取 poiId → 验证距离 < 30km → sleep 0.3s。后续步骤不得直接混用 API 返回的 longitude/latitude 与内部 lon/lat 字段。
Error handling / 错误处理:
- If
AMapPersonalMapClientis not found → check the pre-flight checklist in the Dependencies section, inform user, and stop - If
AMAP_API_KEYis empty → inform user and stop - For every response → call
api_error_message()before reading result fields - If
maps_geofails → do not substitute a previous coordinate. Stop QR generation when this is a start, end, or must-visit point; otherwise list the optional point as unresolved and exclude it from routing - If
maps_text_searchfails or has no result within 30km → retry with{city_name} {location_name}. For a required point, stop QR generation and ask the user to disambiguate; for an optional point, retain it only in the text itinerary as unresolved and do not give it a fake poiId - If QPS exceeded (
CUQPS_HAS_EXCEEDED_THE_LIMITin error message) → wait 1s and retry once
3.1 Supply point search per day / 每日补给搜索
After all waypoint coordinates are resolved, search for supply points near key nodes on days that enter sparsely populated areas or have long uninterrupted stretches:
对于进入人烟稀少区域或存在长距离无补给路段的日期,在关键节点附近搜索补给点:
# For each day flagging a supply concern (long stretch, remote area):
# Search gas stations and hospitals near the day's midpoint or last town before uninhabited area
result = client.maps_around_search(
keywords="加油站|医院",
location=f"{lon},{lat}", # "经度,纬度" 格式
radius=5000, # 搜索半径 5km(默认 1000m)
offset=5
)
if error := api_error_message(result):
print(f"补给搜索失败: {error}")
When to search / 何时搜索:
| Condition / 条件 | Action / 动作 |
|---|---|
| Day mileage > 300km / 日里程超 300km | Search gas stations at midpoint / 在中点搜索加油站 |
| Entering uninhabited area (>100km no town) / 进入无人区(>100km 无城镇) | Search all supply points at the last town before entry / 在进入前最后城镇搜索全部补给点 |
| High-altitude segment (>3500m) / 高海拔路段(>3500m) | Search hospitals at the nearest low-altitude town / 在最近的低海拔城镇搜索医院 |
| All days / 每天 | Search at least one hospital near the day's endpoint (overnight city) / 在每天终点城市至少搜索一家医院 |
Step 4: Driving route planning with cross-day completion / 驾车路径规划(跨天补全)
When calculating daily mileage, must insert the previous day's endpoint as the start of the current day's points list:
for i, day in enumerate(days):
points = [p for p in day["points"] if "lon" in p]
if i > 0:
prev = [p for p in days[i-1]["points"] if "lon" in p]
if prev:
points.insert(0, prev[-1])
# Drive segment by segment using maps_direction_driving:
# For each adjacent pair in points, call:
# result = client.maps_direction_driving(
# origin=f"{points[j]['lon']},{points[j]['lat']}",
# destination=f"{points[j+1]['lon']},{points[j+1]['lat']}"
# )
# Sum distances for the day's total.
# Parameter format: "longitude,latitude" (no spaces).
计算每日里程时,必须把前一天的终点插入当天 points 列表头部作为起点。
maps_direction_driving 的 origin / destination 参数格式为 "经度,纬度"(逗号分隔,无空格)。
Step 5: Generate personal map QR code / 生成个人地图二维码
5.1 Collect valid points / 收集有效点
Collect only normalized points with a non-empty verified poiId. Preserve insertion order (day-by-day). Before collecting, confirm that all required points (start, end, user must-visits) resolved successfully. If any did not, do not generate a QR code that claims to be the complete route.
收集所有天的有效点(已标准化且 poiId 已验证),按天顺序保持插入顺序。收集前必须确认起点、终点和用户必去点均已解析;任一失败时不得生成声称完整的二维码路线。
5.2 Deduplicate by identity / 按身份去重
Rules (apply in order):
- Identity / 身份:Use
poiIdas the primary identity. Only when no poiId is available for a non-QR text item, treat points as duplicates when their coordinates are within a small tolerance; never deduplicate by name alone. - Closed-loop anchor / 环线锚点:If the first and last points have the same identity, keep both, and remove only middle occurrences of that identity. This ensures the route stays closed.
- Intermediate duplicates / 中间重复:For all other repeated identities, keep the first occurrence and remove later ones. Keep the day-by-day itinerary data unchanged; this rule applies only to the QR route list.
- Adjacent duplicates / 连续重复:Remove one only when two adjacent QR points have the same identity. Do not infer a duplicate solely because their display names match.
Deduplication flow:
identities = [p["poiId"] for p in points]
if len(identities) < 2:
→ stop; do not generate a route QR code
elif identities[0] == identities[-1]:
→ keep points[0] and points[-1], remove middle occurrences of this identity
else:
→ for all repeated identities, keep first occurrence and remove rest
5.3 Trim to ≤ 16 nodes / 精简至 ≤ 16 节点
If the deduplicated list exceeds 16 nodes, trim using this priority order:
| Priority | Keep | Drop |
|---|---|---|
| 1 (highest) | Start and end points (环路锚点) | — |
| 2 | User-specified must-visit attractions | — |
| 3 | Major scenic spots / landmarks (景点/地标) | Rest stops, service areas (服务区) |
| 4 | Geographically isolated nodes (唯一覆盖该区域的点) | Dense urban clusters (keep 1 per city) |
| 5 | POI with verified poiId | Optional unresolved POI (text itinerary only; never add to QR route) |
Geographic distribution principle / 地理分布原则:When choosing between nodes of equal priority, prefer nodes that are farther apart (ensure the route visually represents the full journey). Avoid dropping the only node in a sparse region.
Minimum trim / 最小精简:Only remove as many nodes as needed to reach 16. Never remove the start or end anchor.
5.4 Data integrity before calling API / 调用 API 前的数据完整性
# Build lineList structure
point_info_list = [
{"name": p["name"], "lon": p["lon"], "lat": p["lat"], "poiId": p["poiId"]}
for p in deduplicated_points
]
line_list = [{
"title": "Route Name / 路线名称",
"pointInfoList": point_info_list
}]
# Validate before calling maps_schema_personal_map
if len(point_info_list) > 16:
raise ValueError(f"Still {len(point_info_list)} nodes, must be ≤ 16")
if len(point_info_list) < 2:
raise ValueError("A route QR code needs at least a resolved start and end point")
if any(not p["poiId"] for p in point_info_list):
raise ValueError("Empty poiId found")
5.5 Call API and download QR / 调用 API 并下载二维码
result = client.maps_schema_personal_map(
orgName="路线名称",
lineList=line_list,
sceneType=3 # 3 = route planning mode (仅创建路线)
)
# Check for API errors before reading result fields
if error := api_error_message(result):
print(f"地图生成失败: {error}")
# fallback: output the coordinates as text so user can manually create route
else:
qr_url = result["qr_code_url"]
# Download QR image:
import urllib.request
qr_path = "/tmp/road_trip_qr.png"
urllib.request.urlretrieve(qr_url, qr_path)
# Display inline using the local absolute path:
# 
# Fallback link: [在 Amap 中打开](qr_url)
- Call
maps_schema_personal_mapwithsceneType=3(route planning mode / 仅创建路线) - Check for error in result dict (no exceptions thrown)
- Download QR code image and display inline
- Always include the fallback URL link
收集已验证节点 → 按 poiId/坐标身份去重(保留闭环首尾)→ 构建 lineList → 按优先级精简至 ≤ 16 节点 → 数据完整性校验 → 调用 maps_schema_personal_map(sceneType=3) → 检查错误 → 下载二维码 → 显示备用链接。
Step 6: Output Markdown itinerary / 输出 Markdown 行程表
Output directly in the conversation as Markdown, including:
- Itinerary overview table (day, route, mileage, duration, road type, attractions) with total row
- Daily detailed itinerary (waypoints table with coordinates and altitude, supply points)
- Practical tips (altitude sickness, road conditions, tickets, temperature, seasonal notes)
- QR code image and fallback link
在对话中直接输出 Markdown:行程概览表格(含总里程合计行)、每日详细行程(含海拔和补给点)、实用提示、二维码图片和备用链接。
Step 6.1: Verify date-sensitive facts / 核验时效信息
Before presenting a final itinerary, verify the route against the actual departure dates. Static seasonal guidance and route-planning API results are not proof that a road, attraction, or service is currently available.
出最终行程前,必须按准确出发日期核验。静态季节提示和路径规划 API 结果均不能证明道路、景区或服务设施当前可用。
- Check official traffic/road-authority notices for closures, controls, and weather-related restrictions
- Check current weather warnings for high-altitude, mountain, desert, or remote segments
- Check attraction opening status, ticket/reservation rules, and holiday calendar/traffic-freeway policy for that year
- Mark each unavailable source as
未核验rather than inferring a current status; advise the user to recheck immediately before departure
Key Constraints / 关键约束
| Constraint / 约束 | Description / 说明 |
|---|---|
| Dependency check / 依赖检查 | Before any API call, verify personal-map skill is installed AND AMAP_API_KEY is set. Fail fast with clear user-facing messages if missing. |
| Max nodes per route / 单条路线节点上限 | Amap lineList max 16 nodes per pointInfoList |
| poiId required / poiId 必填 | maps_schema_personal_map requires non-empty poiId |
| poiId strategy / poiId 获取策略 | maps_text_search first → retry with city name. Required points must resolve; optional unresolved points stay in text only and never receive a fake poiId. |
| API error pattern / API 错误模式 | API failures do not throw. Check an error dict or an error dict in the first list item before reading result fields. |
| QPS limit / QPS 限制 | ≥ 0.3s interval between calls, or CUQPS_HAS_EXCEEDED_THE_LIMIT |
| API Key / API Key | Set AMAP_API_KEY env var (auto-read by AMapPersonalMapClient() with no args) or pass explicitly |
| Region / 地域限制 | China only — Amap (高德) covers mainland China |
| External dependency / 外部依赖 | This repo contains no scripts. AMapPersonalMapClient lives in the personal-map skill at scripts.amap_personal_map_client. Install via openclaw skills install personal-map. |
Output Template / 输出模板
# [Route Name] Road Trip Guide / 自驾行程指南
> Start/End: [City] > Total: ~X km > Days: X > Max Altitude: ~Xm
## Itinerary Overview / 行程概览
| Day | Route | Est. Mileage | Est. Duration | Road Type | Key Attractions |
|-----|-------|-------------|--------------|-----------|-----------------|
| Day 1 | ... | ... | ... | 高速/国道/山路 | ... |
| **Total** | — | **X km** | **~X h** | — | — |
## Daily Itinerary / 每日详细行程
### Day 1: [Title]
- **Driving mileage**: X km (高速 Xkm / 国道 Xkm / 山路 Xkm)
- **Driving duration**: ~X hours
- **Max altitude**: ~Xm (仅高原线路标注 / only for high-altitude routes)
**Waypoints / 途经点**:
| # | Name | Coordinates (lon, lat) | Altitude | Note |
|---|------|------------|----------|------|
| 1 | ... | (lon, lat) | ~Xm | ... |
**Supply points / 沿途补给**:
- ⛽ Gas stations / 加油站: [name] at [location]
- 🏥 Hospitals / 医院: [name] at [location]
## Practical Tips / 实用提示
1. **Altitude sickness / 高原反应**:...(高海拔线路必须包含 / required for routes >3000m)
2. **Road conditions / 路况**:...
3. **Tickets & booking / 门票与预订**:...
4. **Temperature & clothing / 温差与衣物**:...
## Personal Map QR Code / 个人地图二维码

> Fallback link / 备用链接: [Open in Amap](url)
Advanced Capabilities / 进阶能力
- Supply point search / 沿途补给搜索:Integrated into Step 3.1 — conditions and API calls are defined there. / 已集成到 Step 3.1,按条件触发。
- Mileage anomaly troubleshooting / 里程异常排查:If a day's mileage is 0 or obviously wrong, check whether the previous day's endpoint was correctly inserted (Step 4 cross-day completion), and whether coordinates are duplicated.
- Seasonal adjustments / 季节性调整:For routes affected by holidays, winter closures, rainy seasons, high altitude, desert heat, or peak tourism windows, read references/seasonal.md before finalizing the itinerary, then complete Step 6.1 with current official sources.
References / 参考
- Full examples: examples.md
- Seasonal and regional risk checklist: references/seasonal.md
- Underlying API: @lbs-amap/personal-map skill
评论
加载中…