- name
- travel-planner
- description
- 智能旅行攻略规划师。根据用户提供的出行信息(人数、男女比例、关系、出发地、目的地、日期、天数), 直接调用高德地图 REST API 获取实时路线、POI、距离等数据,生成完整的个性化旅行攻略。 攻略包含:创意名称、出行方式建议(含当地租车判断)、分时段行程(早午晚三餐均有具体推荐)、 景点推荐(含高铁站→景点距离)、住宿推荐(1-2家性价比酒店)、出行物资清单、注意事项、费用汇总。 当用户说"帮我做旅行攻略"、"规划一下行程"、"出去玩怎么安排"等时触发。
- env
- [{"name":"AMAP_KEY","description":"高德地图 REST API Key(在 lbs.amap.com 控制台创建,平台类型选「Web服务」)","required":true}]
- requires
- ["curl","python3"]
# Travel Planner — 智能旅行攻略规划师
直接调用**高德地图 REST API** 获取实时数据,无需 GUI 容器。
## Step 0:检查 API Key
```bash
[ -n "$AMAP_KEY" ] && echo "AMAP_KEY: configured" || echo "AMAP_KEY: not set"
```
- **输出 configured** → 配置成功,跳到 Step 1
- **输出 not set** → 提示用户:
> 需要配置高德地图 API Key 才能获取实时数据:
> 1. 前往 https://lbs.amap.com 注册并创建应用
> 2. 添加 Key,平台选择「**Web服务**」(不是 JS API)
> 3. 在高德控制台为该 Key 设置「IP 白名单」和「服务权限」(仅开启本技能需要的接口),限制 Key 的使用范围
> 4. 在当前终端执行:`export AMAP_KEY=你的ApiKey`
>
> **安全提示**:不建议将 API Key 写入 `~/.zshrc` 明文保存,建议每次使用前在终端临时 export,用完后关闭终端即失效。
---
## Step 1:收集信息(生成攻略前必须完成)
如果用户未提供以下信息,**一次性**把缺少的问题打包问完,不要多轮反复追问:
**必需信息:**
- 出行人数、男女比例、同行关系(情侣 / 同性朋友 / 混合朋友 / 亲子家庭 / 其他)
- 出发地(具体城市或地址)
- 目的地(城市 / 景区 / 区域)
- 出行日期及天数
**开场四问**(收集到基本信息后,一条消息问完,不要分多次问):
> 还有四个小问题帮我规划得更好:
> 1. 你希望行程**紧凑一些**(多逛景点)还是**轻松一些**(慢慢玩)?
> 2. 这次大概的**预算**是多少(人均或总预算均可)?
> 3. 有没有**一定要去**的景点,或者**非吃不可**的东西?
> 4. 到了目的地打算**自驾**还是坐**公共交通**出行?
预算处理规则:若用户报的预算明显偏低,直接给出性价比最优方案,备注"这是最合适的方案了",不必纠结预算准确性。
---
## Step 2:数据收集(调用高德 REST API)
所有用户输入(地址、城市名等文本)通过**环境变量**传入 Python,由 `urllib.parse.urlencode` 统一编码后发起 HTTP 请求,不经过 shell 拼接,避免命令注入风险。坐标由 API 返回(格式固定为 `经度,纬度`),可直接使用。
### 2.1 地理编码(获取坐标)
先获取出发地、目的地、目的地高铁站的坐标,后续所有 API 都依赖这些坐标:
```bash
# 出发地坐标
ADDR="{出发地}" python3 - << 'EOF'
import urllib.request, urllib.parse, json, os
params = urllib.parse.urlencode({'address': os.environ['ADDR'], 'key': os.environ['AMAP_KEY']})
with urllib.request.urlopen('https://restapi.amap.com/v3/geocode/geo?' + params) as r:
d = json.load(r)
g = d.get('geocodes', [])
print(g[0]['location'] if g else 'NOT_FOUND')
EOF
# 目的地坐标
ADDR="{目的地}" python3 - << 'EOF'
import urllib.request, urllib.parse, json, os
params = urllib.parse.urlencode({'address': os.environ['ADDR'], 'key': os.environ['AMAP_KEY']})
with urllib.request.urlopen('https://restapi.amap.com/v3/geocode/geo?' + params) as r:
d = json.load(r)
g = d.get('geocodes', [])
print(g[0]['location'] if g else 'NOT_FOUND')
EOF
# 目的地高铁站坐标
ADDR="{目的地}高铁站" python3 - << 'EOF'
import urllib.request, urllib.parse, json, os
params = urllib.parse.urlencode({'address': os.environ['ADDR'], 'key': os.environ['AMAP_KEY']})
with urllib.request.urlopen('https://restapi.amap.com/v3/geocode/geo?' + params) as r:
d = json.load(r)
g = d.get('geocodes', [])
print(g[0]['location'] if g else 'NOT_FOUND')
EOF
```
记录三组坐标(格式:经度,纬度),供后续步骤使用。
### 2.2 出发地→目的地路线
坐标为 API 返回值,格式固定(`\d+\.\d+,\d+\.\d+`),直接传参安全:
```bash
# 驾车路线
python3 - << 'EOF'
import urllib.request, urllib.parse, json, os
params = urllib.parse.urlencode({
'origin': '{出发地坐标}', 'destination': '{目的地坐标}', 'key': os.environ['AMAP_KEY']
})
with urllib.request.urlopen('https://restapi.amap.com/v3/direction/driving?' + params) as r:
d = json.load(r)
paths = d.get('route', {}).get('paths', [])
if paths:
p = paths[0]
print(f"驾车: {int(p['distance'])//1000}km / {int(p['duration'])//60}分钟 / 过路费¥{p.get('tolls',0)}")
EOF
# 公共交通路线(含高铁)
CITY="{目的地城市}" python3 - << 'EOF'
import urllib.request, urllib.parse, json, os
params = urllib.parse.urlencode({
'origin': '{出发地坐标}', 'destination': '{目的地坐标}',
'city': os.environ['CITY'], 'key': os.environ['AMAP_KEY']
})
with urllib.request.urlopen('https://restapi.amap.com/v3/direction/transit/integrated?' + params) as r:
d = json.load(r)
transits = d.get('route', {}).get('transits', [])
if transits:
t = transits[0]
print(f"公交/高铁: {int(t['duration'])//60}分钟 / ¥{t.get('cost',0)}")
EOF
```
### 2.3 景点搜索
```bash
KW="{目的地}景点" CITY="{目的城市}" python3 - << 'EOF'
import urllib.request, urllib.parse, json, os
params = urllib.parse.urlencode({
'keywords': os.environ['KW'], 'city': os.environ['CITY'],
'citylimit': 'true', 'pageSize': '15', 'key': os.environ['AMAP_KEY']
})
with urllib.request.urlopen('https://restapi.amap.com/v3/place/text?' + params) as r:
d = json.load(r)
for p in d.get('pois', []):
rating = p.get('biz_ext', {}).get('rating', '无')
print(f"{p['name']} | {p['address']} | 评分:{rating} | 坐标:{p['location']}")
EOF
```
根据行程节奏(紧凑/轻松)选 3-6 个景点,**记录每个景点坐标**,按地理位置聚合排序避免折返。
### 2.4 高铁站→各景点距离
对每个主要景点,分别查询高铁站到该景点的驾车距离与时间:
```bash
# 对每个景点执行(替换坐标,坐标为 API 返回值,格式固定安全)
python3 - << 'EOF'
import urllib.request, urllib.parse, json, os
params = urllib.parse.urlencode({
'origin': '{高铁站坐标}', 'destination': '{景点坐标}', 'key': os.environ['AMAP_KEY']
})
with urllib.request.urlopen('https://restapi.amap.com/v3/direction/driving?' + params) as r:
d = json.load(r)
paths = d.get('route', {}).get('paths', [])
if paths:
p = paths[0]
print(f"驾车约{int(p['distance'])//1000}km / {int(p['duration'])//60}分钟")
EOF
```
汇总结果,用于后续「出行方式建议」判断是否推荐当地租车。
### 2.5 餐厅搜索
分三餐分别搜索,优先评分高、距景点近的:
```bash
# 早餐:当地特色早点
KW="{目的地}特色早餐 早点" CITY="{目的城市}" python3 - << 'EOF'
import urllib.request, urllib.parse, json, os
params = urllib.parse.urlencode({
'keywords': os.environ['KW'], 'city': os.environ['CITY'],
'citylimit': 'true', 'pageSize': '10', 'key': os.environ['AMAP_KEY']
})
with urllib.request.urlopen('https://restapi.amap.com/v3/place/text?' + params) as r:
d = json.load(r)
for p in d.get('pois', [])[:5]:
print(f"{p['name']} | {p['address']} | 评分:{p.get('biz_ext',{}).get('rating','无')}")
EOF
# 午餐:主要景点周边(使用第一个景点坐标,坐标为 API 返回值安全)
KW="餐厅 午餐" python3 - << 'EOF'
import urllib.request, urllib.parse, json, os
params = urllib.parse.urlencode({
'location': '{主要景点坐标}', 'keywords': os.environ['KW'],
'radius': '1500', 'pageSize': '10', 'key': os.environ['AMAP_KEY']
})
with urllib.request.urlopen('https://restapi.amap.com/v3/place/around?' + params) as r:
d = json.load(r)
for p in d.get('pois', [])[:5]:
print(f"{p['name']} | {p['address']} | 评分:{p.get('biz_ext',{}).get('rating','无')}")
EOF
# 晚餐:当地特色菜/口碑餐厅
KW="{目的地}特色菜 晚餐 人气" CITY="{目的城市}" python3 - << 'EOF'
import urllib.request, urllib.parse, json, os
params = urllib.parse.urlencode({
'keywords': os.environ['KW'], 'city': os.environ['CITY'],
'citylimit': 'true', 'pageSize': '10', 'key': os.environ['AMAP_KEY']
})
with urllib.request.urlopen('https://restapi.amap.com/v3/place/text?' + params) as r:
d = json.load(r)
for p in d.get('pois', [])[:5]:
print(f"{p['name']} | {p['address']} | 评分:{p.get('biz_ext',{}).get('rating','无')}")
EOF
```
每餐各选 1-2 家推荐,写明地址、推荐菜品和人均价格。
### 2.6 住宿搜索
```bash
KW="酒店 宾馆" python3 - << 'EOF'
import urllib.request, urllib.parse, json, os
params = urllib.parse.urlencode({
'location': '{主要景区坐标}', 'keywords': os.environ['KW'],
'radius': '2000', 'pageSize': '15', 'key': os.environ['AMAP_KEY']
})
with urllib.request.urlopen('https://restapi.amap.com/v3/place/around?' + params) as r:
d = json.load(r)
for p in d.get('pois', [])[:10]:
rating = p.get('biz_ext', {}).get('rating', '无')
price = p.get('biz_ext', {}).get('avg_price', '无')
print(f"{p['name']} | {p['address']} | 评分:{rating} | 均价:¥{price}")
EOF
```
根据预算和评分,选出 **1-2 家**性价比最优酒店(评分高且价格与预算相符),按同行关系决定房型:
- 情侣 → 大床房
- 同性朋友 → 标间
- 亲子 → 亲子房 / 大床
- 混合多人 → 多间标间
### 2.7 天气查询
使用 **WebSearch** 搜索:`{目的地} {出行日期} 天气预报`,获取天气状况、气温范围、风力、降水概率。
---
## Step 3:生成攻略
### 出行方式建议逻辑(必须在攻略开头体现)
结合用户选择(自驾/公共交通)+ 景点距离数据,决定是否建议当地租车:
**用户选自驾:** 正常规划驾车路线,标注主要停车场。
**用户选公共交通:**
- 统计各景点间的距离
- 若**任意两景点之间距离 > 15km**,或**高铁站到最远景点 > 20km**,在攻略开头加提示:
> 💡 **出行建议**:行程中 {景点A} 与 {景点B} 相距约 {X} km,公交换乘需 {X} 次约 {X} 分钟。建议考虑在当地**租一天车**(约 ¥150-300/天),时间更自由,整体性价比更高。
### 攻略命名规则
根据人数 + 地点 + 关系 + 男女比例组合创意名称,要有趣、押韵或谐音,禁止起"XX之旅"这类通用名:
| 场景 | 名称思路 | 示例 |
|------|---------|------|
| 4人出行 | 谐音流行词 | 宝宝巴4(宝宝巴士)、四叶草小队 |
| 情侣2人 | 浪漫+地点 | 西湖·两人世界、成都恋爱进行时 |
| 纯男生团 | 硬核/兄弟风 | 兄弟的远征、XY染色体旅行团 |
| 纯女生团 | 甜美/闺蜜风 | 姐妹花漫游记、女子图鉴·{城市} |
| 亲子家庭 | 温馨+人数 | 三口之家的{城市}奇遇 |
| 6人及以上 | 大部队风格 | {N}人帮出没、乌合之众·{目的地} |
### 路线规划原则
1. 按地理位置聚合景点,同一区域安排在同一时段
2. 上午精力好 → 安排体力消耗大或热门景点
3. 午饭选景点内或步行 10 分钟内的餐厅
4. 晚上轻松收尾,避免长途折返
### 时间段划分
- 1-3 天行程:每天细化到 早上 / 上午 / 中午 / 下午 / 晚上(五段,含早餐)
- 4 天及以上行程:只展示 Day 1 / Day 2 / Day N,不细化时间段,相邻景点聚合同一天减少折返
### 注意事项生成逻辑
- 天气晴热 → 防晒霜、帽子、墨镜、遮阳伞
- 雨天 → 雨伞、防滑鞋
- 带孩子 → 儿童防晒、备零食、注意落水/走失
Ver en GitHub