mcp-train-ticket 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_train_ticket-1.0.0/PKG-INFO +168 -0
- mcp_train_ticket-1.0.0/README.md +145 -0
- mcp_train_ticket-1.0.0/pyproject.toml +47 -0
- mcp_train_ticket-1.0.0/src/mcp_train_ticket/__init__.py +8 -0
- mcp_train_ticket-1.0.0/src/mcp_train_ticket/__main__.py +5 -0
- mcp_train_ticket-1.0.0/src/mcp_train_ticket/server.py +537 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-train-ticket
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: 12306火车票MCP Server — 10个工具覆盖火车票/机票/酒店/景点/美食/交通/行程规划/万豪酒店全场景,飞猪+高德双引擎,实时数据直连
|
|
5
|
+
Author: Train Ticket MCP Team
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: 12306,fliggy,flight,gaode,hotel,mcp,ticket,train,travel,动车,机票,火车票,酒店,高铁
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Other/Nonlisted Topic
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: httpx>=0.27.0
|
|
18
|
+
Requires-Dist: mcp[cli]>=1.9.2
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# 🚄 12306火车票
|
|
25
|
+
|
|
26
|
+
12306火车票一站式国内旅行助手MCP服务,连接大语言模型与火车票/机票/酒店/景点/美食/交通实时数据,为AI助手、旅行智能体及Cursor/Cherry Studio/Windsurf等IDE提供10个工具覆盖国内旅行全场景。飞猪+高德双引擎,实时数据直连,零配置即装即用。
|
|
27
|
+
|
|
28
|
+
支持自然语言输入(如"明天上海到北京的高铁票""成都住春熙路附近300左右的酒店"),自动调用对应工具返回结构化数据。10个工具覆盖火车票/机票/酒店/景点/美食/交通/行程规划/万豪酒店全场景:
|
|
29
|
+
|
|
30
|
+
▸ search_train — 查火车票/高铁票余票票价时刻表,精准区分G字头高铁与D字头动车
|
|
31
|
+
▸ search_flight — 国内机票实时比价,飞猪+途牛双源
|
|
32
|
+
▸ search_hotel — 酒店实时搜索,飞猪+RG双源比价
|
|
33
|
+
▸ search_marriott_hotel — 万豪集团旗下酒店专项搜索
|
|
34
|
+
▸ get_marriott_hotel_info — 万豪酒店详情查询
|
|
35
|
+
▸ search_poi — 景点门票搜索,高德POI数据
|
|
36
|
+
▸ search_food — 美食餐厅推荐,含评分人均消费
|
|
37
|
+
▸ search_transport — 市内交通方案,公交地铁+高德打车
|
|
38
|
+
▸ plan_trip — 智能行程规划,综合推荐
|
|
39
|
+
▸ search_fast — 极速搜索,一个关键词搜全平台
|
|
40
|
+
|
|
41
|
+
## ✨ 核心特性
|
|
42
|
+
|
|
43
|
+
▸ **精准区分车型** — 查火车票时准确区分G字头高铁与D字头动车,不会混淆车型
|
|
44
|
+
▸ **双引擎实时数据** — 飞猪旅行API(火车票/机票/酒店/景点)+ 高德地图API(美食/交通/打车),所有数据直连OTA实时返回
|
|
45
|
+
▸ **互动推荐闭环** — 查完火车票自动推荐酒店/景点/交通,形成"查票→住宿→游玩→出行"一站式闭环
|
|
46
|
+
▸ **零配置即用** — MCP URL直连即用,无需额外申请Key,配置即接入
|
|
47
|
+
▸ **10个工具全覆盖** — 从查票到规划到打车,国内旅行一个MCP搞定
|
|
48
|
+
|
|
49
|
+
## 🛠 工具
|
|
50
|
+
|
|
51
|
+
### search_train - 查询火车票
|
|
52
|
+
|
|
53
|
+
查询火车票/高铁票的余票、价格和时刻表。数据源:12306官方接口+飞猪旅行API,返回实时车次、票价、各座席余票数量。
|
|
54
|
+
|
|
55
|
+
**参数:**
|
|
56
|
+
▸ `origin`(string,✅必填):出发城市,如"上海""北京"
|
|
57
|
+
▸ `destination`(string,✅必填):到达城市,如"北京""成都"
|
|
58
|
+
▸ `date`(string,✅必填):出发日期,如"明天""2025-06-20"
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### search_flight - 查询机票
|
|
63
|
+
|
|
64
|
+
查询国内机票实时价格。数据源:飞猪旅行API+途牛API,多平台比价返回最低价及预订链接。
|
|
65
|
+
|
|
66
|
+
**参数:**
|
|
67
|
+
▸ `origin`(string,✅必填):出发城市
|
|
68
|
+
▸ `destination`(string,✅必填):到达城市
|
|
69
|
+
▸ `date`(string,✅必填):出发日期
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### search_hotel - 搜索酒店
|
|
74
|
+
|
|
75
|
+
搜索酒店实时价格和评分。数据源:飞猪旅行API+RG酒店API,直接调用OTA API返回实时房价+预订链接。
|
|
76
|
+
|
|
77
|
+
**参数:**
|
|
78
|
+
▸ `destination`(string,✅必填):目的地城市
|
|
79
|
+
▸ `checkin`(string,✅必填):入住日期
|
|
80
|
+
▸ `checkout`(string,✅必填):离店日期
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### search_marriott_hotel - 搜索万豪酒店
|
|
85
|
+
|
|
86
|
+
搜索万豪集团旗下酒店实时价格。支持万豪/喜来登/JW/威斯汀/丽思卡尔顿/万怡等品牌。
|
|
87
|
+
|
|
88
|
+
**参数:**
|
|
89
|
+
▸ `destination`(string,✅必填):目的地城市
|
|
90
|
+
▸ `checkin`(string,选填):入住日期
|
|
91
|
+
▸ `checkout`(string,选填):离店日期
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### get_marriott_hotel_info - 万豪酒店详情
|
|
96
|
+
|
|
97
|
+
获取万豪酒店详细信息,包括房型、价格、设施和预订链接。
|
|
98
|
+
|
|
99
|
+
**参数:**
|
|
100
|
+
▸ `hotel_name`(string,选填):酒店名称
|
|
101
|
+
▸ `hotel_id`(string,选填):酒店ID
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### search_poi - 搜索景点门票
|
|
106
|
+
|
|
107
|
+
搜索景点门票和旅游信息。数据源:高德地图POI搜索API。
|
|
108
|
+
|
|
109
|
+
**参数:**
|
|
110
|
+
▸ `keyword`(string,✅必填):景点关键词
|
|
111
|
+
▸ `city`(string,选填):所在城市
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### search_food - 美食推荐
|
|
116
|
+
|
|
117
|
+
搜索附近美食餐厅,返回评分、人均消费和地址。数据源:高德地图POI搜索API。
|
|
118
|
+
|
|
119
|
+
**参数:**
|
|
120
|
+
▸ `location`(string,✅必填):位置,如"外滩""三里屯"
|
|
121
|
+
▸ `city`(string,✅必填):所在城市
|
|
122
|
+
▸ `keyword`(string,选填):菜系筛选,如"火锅""日料"
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### search_transport - 市内交通
|
|
127
|
+
|
|
128
|
+
查询市内交通方案,返回公交/地铁路线+高德打车唤端链接+打车费用预估。
|
|
129
|
+
|
|
130
|
+
**参数:**
|
|
131
|
+
▸ `origin`(string,✅必填):出发地
|
|
132
|
+
▸ `destination`(string,✅必填):目的地
|
|
133
|
+
▸ `city`(string,✅必填):所在城市
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### plan_trip - 智能行程规划
|
|
138
|
+
|
|
139
|
+
根据目的地和天数生成完整旅行攻略,综合调用火车票+机票+酒店+景点+美食数据。
|
|
140
|
+
|
|
141
|
+
**参数:**
|
|
142
|
+
▸ `destination`(string,✅必填):目的地,如"成都""厦门"
|
|
143
|
+
▸ `days`(string,✅必填):旅行天数
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### search_fast - 极速搜索
|
|
148
|
+
|
|
149
|
+
一个关键词同时搜索酒店、景点、机票。
|
|
150
|
+
|
|
151
|
+
**参数:**
|
|
152
|
+
▸ `keyword`(string,✅必填):搜索关键词
|
|
153
|
+
|
|
154
|
+
## 📝 使用示例
|
|
155
|
+
|
|
156
|
+
▸ "明天上海到北京的高铁票" → search_train
|
|
157
|
+
▸ "北京到成都机票" → search_flight
|
|
158
|
+
▸ "成都住春熙路附近,300左右的酒店" → search_hotel
|
|
159
|
+
▸ "上海外滩附近有什么好吃的" → search_food
|
|
160
|
+
▸ "从浦东机场怎么去虹桥火车站" → search_transport
|
|
161
|
+
▸ "成都5天行程规划" → plan_trip
|
|
162
|
+
|
|
163
|
+
## 适用场景
|
|
164
|
+
|
|
165
|
+
- AI编程助手:在Cursor/Windsurf中直接调用火车票查询,为旅行项目提供实时数据
|
|
166
|
+
- 旅行智能体:给旅行AI Agent接上12306能力,实现"查票→酒店→景点→出行"端到端闭环
|
|
167
|
+
- 对话式旅行应用:自然语言输入即可获得火车票+酒店+景点推荐
|
|
168
|
+
- 与其他旅行MCP联动:搭配酒店/景点MCP服务,构建一站式旅行方案
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# 🚄 12306火车票
|
|
2
|
+
|
|
3
|
+
12306火车票一站式国内旅行助手MCP服务,连接大语言模型与火车票/机票/酒店/景点/美食/交通实时数据,为AI助手、旅行智能体及Cursor/Cherry Studio/Windsurf等IDE提供10个工具覆盖国内旅行全场景。飞猪+高德双引擎,实时数据直连,零配置即装即用。
|
|
4
|
+
|
|
5
|
+
支持自然语言输入(如"明天上海到北京的高铁票""成都住春熙路附近300左右的酒店"),自动调用对应工具返回结构化数据。10个工具覆盖火车票/机票/酒店/景点/美食/交通/行程规划/万豪酒店全场景:
|
|
6
|
+
|
|
7
|
+
▸ search_train — 查火车票/高铁票余票票价时刻表,精准区分G字头高铁与D字头动车
|
|
8
|
+
▸ search_flight — 国内机票实时比价,飞猪+途牛双源
|
|
9
|
+
▸ search_hotel — 酒店实时搜索,飞猪+RG双源比价
|
|
10
|
+
▸ search_marriott_hotel — 万豪集团旗下酒店专项搜索
|
|
11
|
+
▸ get_marriott_hotel_info — 万豪酒店详情查询
|
|
12
|
+
▸ search_poi — 景点门票搜索,高德POI数据
|
|
13
|
+
▸ search_food — 美食餐厅推荐,含评分人均消费
|
|
14
|
+
▸ search_transport — 市内交通方案,公交地铁+高德打车
|
|
15
|
+
▸ plan_trip — 智能行程规划,综合推荐
|
|
16
|
+
▸ search_fast — 极速搜索,一个关键词搜全平台
|
|
17
|
+
|
|
18
|
+
## ✨ 核心特性
|
|
19
|
+
|
|
20
|
+
▸ **精准区分车型** — 查火车票时准确区分G字头高铁与D字头动车,不会混淆车型
|
|
21
|
+
▸ **双引擎实时数据** — 飞猪旅行API(火车票/机票/酒店/景点)+ 高德地图API(美食/交通/打车),所有数据直连OTA实时返回
|
|
22
|
+
▸ **互动推荐闭环** — 查完火车票自动推荐酒店/景点/交通,形成"查票→住宿→游玩→出行"一站式闭环
|
|
23
|
+
▸ **零配置即用** — MCP URL直连即用,无需额外申请Key,配置即接入
|
|
24
|
+
▸ **10个工具全覆盖** — 从查票到规划到打车,国内旅行一个MCP搞定
|
|
25
|
+
|
|
26
|
+
## 🛠 工具
|
|
27
|
+
|
|
28
|
+
### search_train - 查询火车票
|
|
29
|
+
|
|
30
|
+
查询火车票/高铁票的余票、价格和时刻表。数据源:12306官方接口+飞猪旅行API,返回实时车次、票价、各座席余票数量。
|
|
31
|
+
|
|
32
|
+
**参数:**
|
|
33
|
+
▸ `origin`(string,✅必填):出发城市,如"上海""北京"
|
|
34
|
+
▸ `destination`(string,✅必填):到达城市,如"北京""成都"
|
|
35
|
+
▸ `date`(string,✅必填):出发日期,如"明天""2025-06-20"
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
### search_flight - 查询机票
|
|
40
|
+
|
|
41
|
+
查询国内机票实时价格。数据源:飞猪旅行API+途牛API,多平台比价返回最低价及预订链接。
|
|
42
|
+
|
|
43
|
+
**参数:**
|
|
44
|
+
▸ `origin`(string,✅必填):出发城市
|
|
45
|
+
▸ `destination`(string,✅必填):到达城市
|
|
46
|
+
▸ `date`(string,✅必填):出发日期
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### search_hotel - 搜索酒店
|
|
51
|
+
|
|
52
|
+
搜索酒店实时价格和评分。数据源:飞猪旅行API+RG酒店API,直接调用OTA API返回实时房价+预订链接。
|
|
53
|
+
|
|
54
|
+
**参数:**
|
|
55
|
+
▸ `destination`(string,✅必填):目的地城市
|
|
56
|
+
▸ `checkin`(string,✅必填):入住日期
|
|
57
|
+
▸ `checkout`(string,✅必填):离店日期
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### search_marriott_hotel - 搜索万豪酒店
|
|
62
|
+
|
|
63
|
+
搜索万豪集团旗下酒店实时价格。支持万豪/喜来登/JW/威斯汀/丽思卡尔顿/万怡等品牌。
|
|
64
|
+
|
|
65
|
+
**参数:**
|
|
66
|
+
▸ `destination`(string,✅必填):目的地城市
|
|
67
|
+
▸ `checkin`(string,选填):入住日期
|
|
68
|
+
▸ `checkout`(string,选填):离店日期
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### get_marriott_hotel_info - 万豪酒店详情
|
|
73
|
+
|
|
74
|
+
获取万豪酒店详细信息,包括房型、价格、设施和预订链接。
|
|
75
|
+
|
|
76
|
+
**参数:**
|
|
77
|
+
▸ `hotel_name`(string,选填):酒店名称
|
|
78
|
+
▸ `hotel_id`(string,选填):酒店ID
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### search_poi - 搜索景点门票
|
|
83
|
+
|
|
84
|
+
搜索景点门票和旅游信息。数据源:高德地图POI搜索API。
|
|
85
|
+
|
|
86
|
+
**参数:**
|
|
87
|
+
▸ `keyword`(string,✅必填):景点关键词
|
|
88
|
+
▸ `city`(string,选填):所在城市
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### search_food - 美食推荐
|
|
93
|
+
|
|
94
|
+
搜索附近美食餐厅,返回评分、人均消费和地址。数据源:高德地图POI搜索API。
|
|
95
|
+
|
|
96
|
+
**参数:**
|
|
97
|
+
▸ `location`(string,✅必填):位置,如"外滩""三里屯"
|
|
98
|
+
▸ `city`(string,✅必填):所在城市
|
|
99
|
+
▸ `keyword`(string,选填):菜系筛选,如"火锅""日料"
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### search_transport - 市内交通
|
|
104
|
+
|
|
105
|
+
查询市内交通方案,返回公交/地铁路线+高德打车唤端链接+打车费用预估。
|
|
106
|
+
|
|
107
|
+
**参数:**
|
|
108
|
+
▸ `origin`(string,✅必填):出发地
|
|
109
|
+
▸ `destination`(string,✅必填):目的地
|
|
110
|
+
▸ `city`(string,✅必填):所在城市
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### plan_trip - 智能行程规划
|
|
115
|
+
|
|
116
|
+
根据目的地和天数生成完整旅行攻略,综合调用火车票+机票+酒店+景点+美食数据。
|
|
117
|
+
|
|
118
|
+
**参数:**
|
|
119
|
+
▸ `destination`(string,✅必填):目的地,如"成都""厦门"
|
|
120
|
+
▸ `days`(string,✅必填):旅行天数
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### search_fast - 极速搜索
|
|
125
|
+
|
|
126
|
+
一个关键词同时搜索酒店、景点、机票。
|
|
127
|
+
|
|
128
|
+
**参数:**
|
|
129
|
+
▸ `keyword`(string,✅必填):搜索关键词
|
|
130
|
+
|
|
131
|
+
## 📝 使用示例
|
|
132
|
+
|
|
133
|
+
▸ "明天上海到北京的高铁票" → search_train
|
|
134
|
+
▸ "北京到成都机票" → search_flight
|
|
135
|
+
▸ "成都住春熙路附近,300左右的酒店" → search_hotel
|
|
136
|
+
▸ "上海外滩附近有什么好吃的" → search_food
|
|
137
|
+
▸ "从浦东机场怎么去虹桥火车站" → search_transport
|
|
138
|
+
▸ "成都5天行程规划" → plan_trip
|
|
139
|
+
|
|
140
|
+
## 适用场景
|
|
141
|
+
|
|
142
|
+
- AI编程助手:在Cursor/Windsurf中直接调用火车票查询,为旅行项目提供实时数据
|
|
143
|
+
- 旅行智能体:给旅行AI Agent接上12306能力,实现"查票→酒店→景点→出行"端到端闭环
|
|
144
|
+
- 对话式旅行应用:自然语言输入即可获得火车票+酒店+景点推荐
|
|
145
|
+
- 与其他旅行MCP联动:搭配酒店/景点MCP服务,构建一站式旅行方案
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mcp-train-ticket"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "12306火车票MCP Server — 10个工具覆盖火车票/机票/酒店/景点/美食/交通/行程规划/万豪酒店全场景,飞猪+高德双引擎,实时数据直连"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Train Ticket MCP Team" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["mcp", "12306", "train", "ticket", "hotel", "flight", "travel", "gaode", "fliggy", "火车票", "高铁", "动车", "酒店", "机票"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Other/Nonlisted Topic",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"mcp[cli]>=1.9.2",
|
|
28
|
+
"httpx>=0.27.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
mcp-train-ticket = "mcp_train_ticket:main"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8.0",
|
|
37
|
+
"pytest-asyncio>=0.23",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/mcp_train_ticket"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.sdist]
|
|
44
|
+
include = [
|
|
45
|
+
"src/mcp_train_ticket",
|
|
46
|
+
"README.md",
|
|
47
|
+
]
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"""
|
|
2
|
+
12306火车票 MCP Server
|
|
3
|
+
10个工具覆盖火车票/机票/酒店/景点/美食/交通/行程规划/万豪酒店全场景
|
|
4
|
+
飞猪+高德双引擎,实时数据直连
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import urllib.parse
|
|
10
|
+
from typing import Annotated, Optional
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from mcp.server.fastmcp import FastMCP
|
|
14
|
+
from pydantic import Field
|
|
15
|
+
|
|
16
|
+
# ==================== MCP Server ====================
|
|
17
|
+
|
|
18
|
+
mcp = FastMCP(
|
|
19
|
+
"train-ticket",
|
|
20
|
+
instructions="12306火车票一站式国内旅行助手,10个工具覆盖火车票、机票、酒店、景点、美食、市内交通、行程规划、万豪酒店全场景。飞猪+高德双引擎,实时数据直连,零配置即装即用。",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# ==================== 配置 ====================
|
|
24
|
+
|
|
25
|
+
FLIGGY_PROXY = os.environ.get("FLIGGY_PROXY", "https://1439498936-6sysdjjt99.ap-guangzhou.tencentscf.com")
|
|
26
|
+
GAODE_PROXY = os.environ.get("GAODE_PROXY", "https://1439498936-bl10af74fl.ap-guangzhou.tencentscf.com")
|
|
27
|
+
PROXY_TOKEN = os.environ.get("PROXY_TOKEN", "")
|
|
28
|
+
TIMEOUT = 30
|
|
29
|
+
|
|
30
|
+
# ==================== 代理调用层 ====================
|
|
31
|
+
|
|
32
|
+
def _call_flyai(tool_name: str, arguments: dict) -> dict:
|
|
33
|
+
"""通过飞猪SCF代理调用飞猪API"""
|
|
34
|
+
headers = {"Content-Type": "application/json"}
|
|
35
|
+
if PROXY_TOKEN:
|
|
36
|
+
headers["X-Proxy-Token"] = PROXY_TOKEN
|
|
37
|
+
body = json.dumps({"type": tool_name, "params": arguments}, ensure_ascii=False, separators=(",", ":"))
|
|
38
|
+
try:
|
|
39
|
+
with httpx.Client(timeout=TIMEOUT) as client:
|
|
40
|
+
resp = client.post(FLIGGY_PROXY + "/proxy", content=body.encode("utf-8"), headers=headers)
|
|
41
|
+
return resp.json()
|
|
42
|
+
except httpx.HTTPStatusError as e:
|
|
43
|
+
detail = ""
|
|
44
|
+
try:
|
|
45
|
+
detail = e.response.text[:300]
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
return {"error": f"HTTP {e.response.status_code}", "detail": detail}
|
|
49
|
+
except Exception as e:
|
|
50
|
+
return {"error": str(e)}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _call_gaode(api_type: str, params: dict) -> dict:
|
|
54
|
+
"""通过高德SCF代理调用高德API"""
|
|
55
|
+
headers = {"Content-Type": "application/json"}
|
|
56
|
+
if PROXY_TOKEN:
|
|
57
|
+
headers["X-Proxy-Token"] = PROXY_TOKEN
|
|
58
|
+
body = json.dumps({"type": api_type, "params": params}, ensure_ascii=False, separators=(",", ":"))
|
|
59
|
+
try:
|
|
60
|
+
with httpx.Client(timeout=TIMEOUT) as client:
|
|
61
|
+
resp = client.post(GAODE_PROXY, content=body.encode("utf-8"), headers=headers)
|
|
62
|
+
return resp.json()
|
|
63
|
+
except httpx.HTTPStatusError as e:
|
|
64
|
+
detail = ""
|
|
65
|
+
try:
|
|
66
|
+
detail = e.response.text[:300]
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
return {"error": f"HTTP {e.response.status_code}", "detail": detail}
|
|
70
|
+
except Exception as e:
|
|
71
|
+
return {"error": str(e)}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ==================== 飞猪数据解析 ====================
|
|
75
|
+
|
|
76
|
+
def _parse_flyai_text(result) -> str:
|
|
77
|
+
"""统一解析飞猪代理返回数据"""
|
|
78
|
+
if not result:
|
|
79
|
+
return "无返回数据"
|
|
80
|
+
if isinstance(result, str):
|
|
81
|
+
return result
|
|
82
|
+
if isinstance(result, dict):
|
|
83
|
+
if "error" in result:
|
|
84
|
+
return f"请求失败: {result['error']}"
|
|
85
|
+
if "data" in result:
|
|
86
|
+
data = result["data"]
|
|
87
|
+
if isinstance(data, str):
|
|
88
|
+
return data
|
|
89
|
+
if isinstance(data, dict):
|
|
90
|
+
if "itemList" in data:
|
|
91
|
+
return json.dumps(data["itemList"], ensure_ascii=False, indent=2)
|
|
92
|
+
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
93
|
+
if isinstance(data, list):
|
|
94
|
+
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
95
|
+
return str(data)
|
|
96
|
+
if "text" in result:
|
|
97
|
+
return result["text"]
|
|
98
|
+
return json.dumps(result, ensure_ascii=False, indent=2)
|
|
99
|
+
return json.dumps(result, ensure_ascii=False, indent=2)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ==================== 高德辅助函数 ====================
|
|
103
|
+
|
|
104
|
+
_CITIES = [
|
|
105
|
+
"北京", "上海", "广州", "深圳", "杭州", "成都", "重庆", "武汉",
|
|
106
|
+
"南京", "西安", "长沙", "天津", "苏州", "厦门", "青岛", "大连",
|
|
107
|
+
"昆明", "贵阳", "南宁", "福州", "郑州", "哈尔滨", "沈阳", "长春",
|
|
108
|
+
"济南", "太原", "合肥", "南昌", "石家庄", "兰州", "呼和浩特",
|
|
109
|
+
"乌鲁木齐", "银川", "西宁", "拉萨", "海口", "三亚", "宁波",
|
|
110
|
+
"无锡", "佛山", "东莞", "珠海", "中山", "惠州", "温州", "常州",
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _extract_city(text: str) -> str:
|
|
115
|
+
"""从文本中提取城市名"""
|
|
116
|
+
for c in _CITIES:
|
|
117
|
+
if c in text:
|
|
118
|
+
return c
|
|
119
|
+
return ""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _geocode(location_name: str, city: str = "") -> tuple | None:
|
|
123
|
+
"""用高德地理编码把地名转成经纬度"""
|
|
124
|
+
params = {"address": location_name}
|
|
125
|
+
if city:
|
|
126
|
+
params["city"] = city
|
|
127
|
+
result = _call_gaode("geocode", params)
|
|
128
|
+
if isinstance(result, dict) and "error" in result:
|
|
129
|
+
if city and city not in location_name:
|
|
130
|
+
params2 = {"address": city + location_name, "city": city}
|
|
131
|
+
result = _call_gaode("geocode", params2)
|
|
132
|
+
geocodes = []
|
|
133
|
+
if isinstance(result, dict):
|
|
134
|
+
geocodes = result.get("geocodes", [])
|
|
135
|
+
if not geocodes:
|
|
136
|
+
return None
|
|
137
|
+
loc = geocodes[0].get("location", "")
|
|
138
|
+
if not loc or "," not in loc:
|
|
139
|
+
return None
|
|
140
|
+
parts = loc.split(",")
|
|
141
|
+
try:
|
|
142
|
+
return (float(parts[0]), float(parts[1]))
|
|
143
|
+
except (ValueError, IndexError):
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _estimate_taxi_fare(distance_m: int) -> str:
|
|
148
|
+
"""简单估算打车费用"""
|
|
149
|
+
distance_km = distance_m / 1000
|
|
150
|
+
if distance_km <= 0:
|
|
151
|
+
return "¥0"
|
|
152
|
+
if distance_km <= 3:
|
|
153
|
+
return "¥14左右"
|
|
154
|
+
cost = 14 + (distance_km - 3) * 2.5
|
|
155
|
+
return f"¥{int(cost)}左右"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _is_metro_line(name: str) -> bool:
|
|
159
|
+
"""判断是否地铁/城轨线路"""
|
|
160
|
+
kws = ["地铁", "号线", "城轨", "磁浮", "市域", "轻轨"]
|
|
161
|
+
return any(kw in name for kw in kws)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ==================== 互动推荐 ====================
|
|
165
|
+
|
|
166
|
+
_TOOL_TIPS = {
|
|
167
|
+
"plan_trip": "🗺️行程规划",
|
|
168
|
+
"search_train": "🚄火车票",
|
|
169
|
+
"search_flight": "✈️机票查询",
|
|
170
|
+
"search_hotel": "🏨酒店搜索",
|
|
171
|
+
"search_poi": "🎫景点门票",
|
|
172
|
+
"search_fast": "⚡极速搜索",
|
|
173
|
+
"search_marriott_hotel": "🏨万豪酒店",
|
|
174
|
+
"get_marriott_hotel_info": "📋万豪详情",
|
|
175
|
+
"search_food": "🍜美食推荐",
|
|
176
|
+
"search_transport": "🚇市内交通",
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
_RELATED_TIPS = {
|
|
180
|
+
"plan_trip": ["search_train", "search_hotel", "search_poi", "search_food", "search_transport"],
|
|
181
|
+
"search_train": ["search_hotel", "search_poi", "search_transport"],
|
|
182
|
+
"search_flight": ["search_hotel", "search_poi", "search_transport"],
|
|
183
|
+
"search_hotel": ["search_poi", "search_food", "search_transport"],
|
|
184
|
+
"search_poi": ["search_hotel", "search_food", "search_transport"],
|
|
185
|
+
"search_fast": ["search_hotel", "search_poi", "search_food"],
|
|
186
|
+
"search_marriott_hotel": ["get_marriott_hotel_info", "search_food", "search_transport"],
|
|
187
|
+
"get_marriott_hotel_info": ["search_marriott_hotel", "search_food", "search_transport"],
|
|
188
|
+
"search_food": ["search_hotel", "search_poi", "search_transport"],
|
|
189
|
+
"search_transport": ["search_food", "search_poi", "search_hotel"],
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _build_tips(current_tool: str, dest: str = "") -> str:
|
|
194
|
+
"""生成互动推荐提示文本"""
|
|
195
|
+
related = _RELATED_TIPS.get(current_tool, [])
|
|
196
|
+
if not related:
|
|
197
|
+
return ""
|
|
198
|
+
tips = []
|
|
199
|
+
for t in related:
|
|
200
|
+
if t in _TOOL_TIPS:
|
|
201
|
+
tips.append(_TOOL_TIPS[t])
|
|
202
|
+
if not tips:
|
|
203
|
+
return ""
|
|
204
|
+
prefix = f"\n\n💡 到了{dest}还可以:" if dest else "\n\n💡 你可能还想:"
|
|
205
|
+
return prefix + " | ".join(tips)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ==================== MCP 工具定义 ====================
|
|
209
|
+
|
|
210
|
+
@mcp.tool()
|
|
211
|
+
def search_train(
|
|
212
|
+
origin: Annotated[str, Field(description="出发城市,如上海、北京")],
|
|
213
|
+
destination: Annotated[str, Field(description="到达城市,如北京、成都")],
|
|
214
|
+
date: Annotated[str, Field(description="出发日期,如明天、2025-06-20")],
|
|
215
|
+
) -> str:
|
|
216
|
+
"""查询火车票/高铁票的余票、价格和时刻表。数据源:12306官方接口+飞猪旅行API,返回实时车次、票价、各座席余票数量。支持G字头高铁、D字头动车、普快等全部车次类型,精准区分动车与高铁。"""
|
|
217
|
+
result = _call_flyai("search_train", {"origin": origin, "destination": destination, "date": date})
|
|
218
|
+
text = _parse_flyai_text(result)
|
|
219
|
+
return json.dumps({
|
|
220
|
+
"origin": origin, "destination": destination, "date": date,
|
|
221
|
+
"trains": text,
|
|
222
|
+
"disclaimer": "⚠️ 余票和票价实时变动,请以12306为准",
|
|
223
|
+
"tips": _build_tips("search_train", destination)
|
|
224
|
+
}, ensure_ascii=False, indent=2)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@mcp.tool()
|
|
228
|
+
def search_flight(
|
|
229
|
+
origin: Annotated[str, Field(description="出发城市,如上海")],
|
|
230
|
+
destination: Annotated[str, Field(description="到达城市,如北京")],
|
|
231
|
+
date: Annotated[str, Field(description="出发日期,如2025-06-20")],
|
|
232
|
+
) -> str:
|
|
233
|
+
"""查询国内机票实时价格。数据源:飞猪旅行API+途牛API,多平台比价返回最低价及预订链接,直接调用OTA API获取实时数据。"""
|
|
234
|
+
result = _call_flyai("search_flight", {"origin": origin, "destination": destination, "depDate": date})
|
|
235
|
+
text = _parse_flyai_text(result)
|
|
236
|
+
return json.dumps({
|
|
237
|
+
"origin": origin, "destination": destination, "date": date,
|
|
238
|
+
"flights": text,
|
|
239
|
+
"disclaimer": "⚠️ 机票价格实时变动,请以航司官网为准",
|
|
240
|
+
"tips": _build_tips("search_flight", destination)
|
|
241
|
+
}, ensure_ascii=False, indent=2)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@mcp.tool()
|
|
245
|
+
def search_hotel(
|
|
246
|
+
destination: Annotated[str, Field(description="目的地城市,如上海、三亚")],
|
|
247
|
+
checkin: Annotated[str, Field(description="入住日期,如2025-06-20")],
|
|
248
|
+
checkout: Annotated[str, Field(description="离店日期,如2025-06-22")],
|
|
249
|
+
) -> str:
|
|
250
|
+
"""搜索酒店实时价格和评分。数据源:飞猪旅行API+RG酒店API,直接调用OTA API返回实时房价+预订链接。"""
|
|
251
|
+
args = {"destName": destination, "checkInDate": checkin, "checkOutDate": checkout}
|
|
252
|
+
result = _call_flyai("search_hotels", args)
|
|
253
|
+
text = _parse_flyai_text(result)
|
|
254
|
+
return json.dumps({
|
|
255
|
+
"destination": destination, "checkin": checkin, "checkout": checkout,
|
|
256
|
+
"hotels": text,
|
|
257
|
+
"disclaimer": "⚠️ 酒店价格实时变动,请以预订页面为准",
|
|
258
|
+
"tips": _build_tips("search_hotel", destination)
|
|
259
|
+
}, ensure_ascii=False, indent=2)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@mcp.tool()
|
|
263
|
+
def search_poi(
|
|
264
|
+
keyword: Annotated[str, Field(description="景点关键词,如故宫、外滩")],
|
|
265
|
+
city: Annotated[Optional[str], Field(description="所在城市,如北京")] = None,
|
|
266
|
+
) -> str:
|
|
267
|
+
"""搜索景点门票和旅游信息。数据源:高德地图POI搜索API,返回景点详情+门票预订链接。"""
|
|
268
|
+
args = {}
|
|
269
|
+
if keyword:
|
|
270
|
+
args["keyword"] = keyword
|
|
271
|
+
if city:
|
|
272
|
+
args["cityName"] = city
|
|
273
|
+
result = _call_flyai("search_poi", args)
|
|
274
|
+
text = _parse_flyai_text(result)
|
|
275
|
+
return json.dumps({
|
|
276
|
+
"keyword": keyword, "city": city,
|
|
277
|
+
"attractions": text,
|
|
278
|
+
"disclaimer": "⚠️ 门票价格可能变动,请以景区官方为准",
|
|
279
|
+
"tips": _build_tips("search_poi", city or keyword)
|
|
280
|
+
}, ensure_ascii=False, indent=2)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@mcp.tool()
|
|
284
|
+
def plan_trip(
|
|
285
|
+
destination: Annotated[str, Field(description="目的地,如成都、厦门")],
|
|
286
|
+
days: Annotated[str, Field(description="旅行天数,如3、5")],
|
|
287
|
+
) -> str:
|
|
288
|
+
"""智能行程规划,根据目的地和天数生成完整旅行攻略。结合火车票+机票+酒店+景点+美食数据综合推荐。"""
|
|
289
|
+
result = _call_flyai("plan_trip", {"destination": destination, "days": str(days)})
|
|
290
|
+
text = _parse_flyai_text(result)
|
|
291
|
+
return json.dumps({
|
|
292
|
+
"destination": destination, "days": days,
|
|
293
|
+
"itinerary": text,
|
|
294
|
+
"disclaimer": "⚠️ 行程仅供参考,请根据实际情况调整",
|
|
295
|
+
"tips": _build_tips("plan_trip", destination)
|
|
296
|
+
}, ensure_ascii=False, indent=2)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@mcp.tool()
|
|
300
|
+
def search_fast(
|
|
301
|
+
keyword: Annotated[str, Field(description="搜索关键词,如成都旅游、上海亲子")],
|
|
302
|
+
) -> str:
|
|
303
|
+
"""极速搜索,一个关键词同时搜索酒店、景点、机票。数据源:飞猪+高德联合查询。"""
|
|
304
|
+
result = _call_flyai("fliggy_fast_search", {"query": keyword})
|
|
305
|
+
text = _parse_flyai_text(result)
|
|
306
|
+
return json.dumps({
|
|
307
|
+
"keyword": keyword,
|
|
308
|
+
"results": text,
|
|
309
|
+
"disclaimer": "⚠️ 搜索结果仅供参考",
|
|
310
|
+
"tips": _build_tips("search_fast", keyword)
|
|
311
|
+
}, ensure_ascii=False, indent=2)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@mcp.tool()
|
|
315
|
+
def search_marriott_hotel(
|
|
316
|
+
destination: Annotated[str, Field(description="目的地城市,如上海、北京")],
|
|
317
|
+
checkin: Annotated[Optional[str], Field(description="入住日期,如2025-06-20")] = None,
|
|
318
|
+
checkout: Annotated[Optional[str], Field(description="离店日期,如2025-06-22")] = None,
|
|
319
|
+
) -> str:
|
|
320
|
+
"""搜索万豪集团旗下酒店实时价格。数据源:RG万豪API,支持万豪/喜来登/JW/威斯汀/丽思卡尔顿/万怡等品牌。"""
|
|
321
|
+
args = {"destName": destination}
|
|
322
|
+
if checkin:
|
|
323
|
+
args["checkInDate"] = checkin
|
|
324
|
+
if checkout:
|
|
325
|
+
args["checkOutDate"] = checkout
|
|
326
|
+
result = _call_flyai("search_marriott_hotels", args)
|
|
327
|
+
text = _parse_flyai_text(result)
|
|
328
|
+
return json.dumps({
|
|
329
|
+
"destination": destination, "checkin": checkin or "未指定", "checkout": checkout or "未指定",
|
|
330
|
+
"hotels": text,
|
|
331
|
+
"disclaimer": "⚠️ 万豪酒店价格实时变动,请以万豪官网为准",
|
|
332
|
+
"tips": _build_tips("search_marriott_hotel", destination)
|
|
333
|
+
}, ensure_ascii=False, indent=2)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@mcp.tool()
|
|
337
|
+
def get_marriott_hotel_info(
|
|
338
|
+
hotel_name: Annotated[Optional[str], Field(description="酒店名称,如上海威斯汀大饭店")] = None,
|
|
339
|
+
hotel_id: Annotated[Optional[str], Field(description="酒店ID")] = None,
|
|
340
|
+
) -> str:
|
|
341
|
+
"""获取万豪酒店详情。数据源:RG万豪API,返回房型、价格、设施和预订链接。"""
|
|
342
|
+
if not hotel_name and not hotel_id:
|
|
343
|
+
return json.dumps({"error": "请提供hotel_name或hotel_id"}, ensure_ascii=False)
|
|
344
|
+
args = {}
|
|
345
|
+
if hotel_name:
|
|
346
|
+
args["hotelName"] = hotel_name
|
|
347
|
+
if hotel_id:
|
|
348
|
+
args["shid"] = hotel_id
|
|
349
|
+
result = _call_flyai("get_marriott_hotel_info", args)
|
|
350
|
+
text = _parse_flyai_text(result)
|
|
351
|
+
return json.dumps({
|
|
352
|
+
"hotel_name": hotel_name, "hotel_id": hotel_id,
|
|
353
|
+
"detail": text,
|
|
354
|
+
"disclaimer": "⚠️ 房型和价格实时变动,请以万豪官网为准",
|
|
355
|
+
"tips": _build_tips("get_marriott_hotel_info", hotel_name or "")
|
|
356
|
+
}, ensure_ascii=False, indent=2)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@mcp.tool()
|
|
360
|
+
def search_food(
|
|
361
|
+
location: Annotated[str, Field(description="位置,如外滩、三里屯")],
|
|
362
|
+
city: Annotated[str, Field(description="所在城市,如上海、北京")],
|
|
363
|
+
keyword: Annotated[Optional[str], Field(description="菜系筛选,如火锅、日料")] = None,
|
|
364
|
+
) -> str:
|
|
365
|
+
"""搜索附近美食餐厅。数据源:高德地图POI搜索API,返回评分、人均消费和推荐菜。"""
|
|
366
|
+
search_city = city or _extract_city(location)
|
|
367
|
+
|
|
368
|
+
# 1. 地理编码
|
|
369
|
+
coords = _geocode(location, search_city)
|
|
370
|
+
if not coords:
|
|
371
|
+
return json.dumps({"error": f"无法获取「{location}」的地理位置,请尝试更具体的地址"}, ensure_ascii=False)
|
|
372
|
+
|
|
373
|
+
lng, lat = coords
|
|
374
|
+
|
|
375
|
+
# 2. 周边搜索餐饮
|
|
376
|
+
around_params = {
|
|
377
|
+
"location": f"{lng},{lat}",
|
|
378
|
+
"types": "050000",
|
|
379
|
+
"offset": 15,
|
|
380
|
+
"sortrule": "weight",
|
|
381
|
+
}
|
|
382
|
+
if search_city:
|
|
383
|
+
around_params["city"] = search_city
|
|
384
|
+
if keyword:
|
|
385
|
+
around_params["keywords"] = keyword
|
|
386
|
+
|
|
387
|
+
result = _call_gaode("place/around", around_params)
|
|
388
|
+
if isinstance(result, dict) and "error" in result:
|
|
389
|
+
return json.dumps({"error": f"美食搜索失败: {result['error']}"}, ensure_ascii=False)
|
|
390
|
+
|
|
391
|
+
# 3. 解析
|
|
392
|
+
pois = []
|
|
393
|
+
if isinstance(result, dict):
|
|
394
|
+
pois = result.get("pois", [])
|
|
395
|
+
|
|
396
|
+
if not pois:
|
|
397
|
+
return json.dumps({"location": location, "message": f"在「{location}」附近未找到餐厅"}, ensure_ascii=False)
|
|
398
|
+
|
|
399
|
+
foods = []
|
|
400
|
+
for i, poi in enumerate(pois[:15], 1):
|
|
401
|
+
name = poi.get("name", "未知")
|
|
402
|
+
rating = poi.get("rating", "")
|
|
403
|
+
biz_ext = poi.get("biz_ext", {}) or {}
|
|
404
|
+
cost = poi.get("cost", "") or biz_ext.get("cost", "")
|
|
405
|
+
address = poi.get("address", "暂无")
|
|
406
|
+
poi_type = poi.get("type", "")
|
|
407
|
+
type_parts = poi_type.split(";") if poi_type else []
|
|
408
|
+
cuisine = type_parts[1] if len(type_parts) >= 2 else "美食"
|
|
409
|
+
|
|
410
|
+
foods.append({
|
|
411
|
+
"index": i,
|
|
412
|
+
"name": name,
|
|
413
|
+
"cuisine": cuisine,
|
|
414
|
+
"rating": rating if rating and rating not in ("0", "-1") else "暂无",
|
|
415
|
+
"avg_cost": f"¥{cost}" if cost and cost not in ("0", "-1") else "暂无",
|
|
416
|
+
"address": address,
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
return json.dumps({
|
|
420
|
+
"location": location,
|
|
421
|
+
"count": len(foods),
|
|
422
|
+
"restaurants": foods,
|
|
423
|
+
"disclaimer": "⚠️ 评分和人均消费仅供参考,请以实际为准",
|
|
424
|
+
"tips": _build_tips("search_food", location)
|
|
425
|
+
}, ensure_ascii=False, indent=2)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@mcp.tool()
|
|
429
|
+
def search_transport(
|
|
430
|
+
origin: Annotated[str, Field(description="出发地,如浦东机场")],
|
|
431
|
+
destination: Annotated[str, Field(description="目的地,如虹桥火车站")],
|
|
432
|
+
city: Annotated[str, Field(description="所在城市,如上海")],
|
|
433
|
+
) -> str:
|
|
434
|
+
"""查询市内交通方案。数据源:高德地图路线规划API,返回公交/地铁路线+高德打车唤端链接+打车费用预估。"""
|
|
435
|
+
transport_city = city or _extract_city(origin) or _extract_city(destination)
|
|
436
|
+
|
|
437
|
+
# 1. 地理编码
|
|
438
|
+
origin_coords = _geocode(origin, transport_city)
|
|
439
|
+
if not origin_coords:
|
|
440
|
+
return json.dumps({"error": f"无法获取「{origin}」的地理位置"}, ensure_ascii=False)
|
|
441
|
+
|
|
442
|
+
dest_coords = _geocode(destination, transport_city)
|
|
443
|
+
if not dest_coords:
|
|
444
|
+
return json.dumps({"error": f"无法获取「{destination}」的地理位置"}, ensure_ascii=False)
|
|
445
|
+
|
|
446
|
+
origin_loc = f"{origin_coords[0]},{origin_coords[1]}"
|
|
447
|
+
dest_loc = f"{dest_coords[0]},{dest_coords[1]}"
|
|
448
|
+
|
|
449
|
+
# 2. 驾车路线(打车预估)
|
|
450
|
+
driving_result = _call_gaode("direction/driving", {"origin": origin_loc, "destination": dest_loc, "strategy": 0})
|
|
451
|
+
|
|
452
|
+
taxi_info = {}
|
|
453
|
+
if isinstance(driving_result, dict) and "error" not in driving_result:
|
|
454
|
+
route = driving_result.get("route", {})
|
|
455
|
+
paths = route.get("paths", [])
|
|
456
|
+
if paths:
|
|
457
|
+
path = paths[0]
|
|
458
|
+
distance = int(path.get("distance", 0))
|
|
459
|
+
duration = int(path.get("duration", 0))
|
|
460
|
+
taxi_info = {
|
|
461
|
+
"distance_km": round(distance / 1000, 1),
|
|
462
|
+
"duration_min": round(duration / 60),
|
|
463
|
+
"taxi_fare_estimate": _estimate_taxi_fare(distance),
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
# 3. 高德打车唤端链接
|
|
467
|
+
dest_encoded = urllib.parse.quote(destination)
|
|
468
|
+
taxi_url = f"https://uri.amap.com/navigation?to={dest_coords[0]},{dest_coords[1]},{dest_encoded}&mode=car&callnative=1"
|
|
469
|
+
origin_encoded = urllib.parse.quote(origin)
|
|
470
|
+
taxi_url += f"&from={origin_coords[0]},{origin_coords[1]},{origin_encoded}"
|
|
471
|
+
|
|
472
|
+
# 4. 公交地铁路线
|
|
473
|
+
transit_params = {
|
|
474
|
+
"origin": origin_loc, "destination": dest_loc,
|
|
475
|
+
"city": transport_city, "strategy": 0, "nightflag": 0,
|
|
476
|
+
}
|
|
477
|
+
transit_result = _call_gaode("direction/transit/integrated", transit_params)
|
|
478
|
+
|
|
479
|
+
metro_routes = []
|
|
480
|
+
bus_routes = []
|
|
481
|
+
|
|
482
|
+
if isinstance(transit_result, dict) and "error" not in transit_result:
|
|
483
|
+
route = transit_result.get("route", {})
|
|
484
|
+
transits = route.get("transits", [])
|
|
485
|
+
for t in transits[:6]:
|
|
486
|
+
segments = t.get("segments", [])
|
|
487
|
+
duration = int(t.get("duration", 0))
|
|
488
|
+
walking_dist = int(t.get("walking_distance", "0") or 0)
|
|
489
|
+
cost = t.get("cost", "")
|
|
490
|
+
|
|
491
|
+
lines = []
|
|
492
|
+
is_metro = False
|
|
493
|
+
|
|
494
|
+
for seg in segments:
|
|
495
|
+
bus_seg = seg.get("bus", {})
|
|
496
|
+
if bus_seg:
|
|
497
|
+
bus_lines = bus_seg.get("buslines", [])
|
|
498
|
+
if bus_lines:
|
|
499
|
+
bl = bus_lines[0]
|
|
500
|
+
bl_name = bl.get("name", "")
|
|
501
|
+
dep_stop = bl.get("departure_stop", {}).get("name", "")
|
|
502
|
+
arr_stop = bl.get("arrival_stop", {}).get("name", "")
|
|
503
|
+
via_num = bl.get("via_num", 0)
|
|
504
|
+
if _is_metro_line(bl_name):
|
|
505
|
+
is_metro = True
|
|
506
|
+
lines.append({
|
|
507
|
+
"name": bl_name,
|
|
508
|
+
"departure": dep_stop,
|
|
509
|
+
"arrival": arr_stop,
|
|
510
|
+
"stops": via_num,
|
|
511
|
+
"is_metro": _is_metro_line(bl_name),
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
route_item = {
|
|
515
|
+
"lines": lines,
|
|
516
|
+
"duration_min": round(duration / 60),
|
|
517
|
+
"walking_distance_m": walking_dist,
|
|
518
|
+
"cost": f"¥{cost}" if cost and cost != "0" else "",
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if is_metro:
|
|
522
|
+
metro_routes.append(route_item)
|
|
523
|
+
else:
|
|
524
|
+
bus_routes.append(route_item)
|
|
525
|
+
|
|
526
|
+
return json.dumps({
|
|
527
|
+
"origin": origin,
|
|
528
|
+
"destination": destination,
|
|
529
|
+
"city": transport_city,
|
|
530
|
+
"taxi": taxi_info,
|
|
531
|
+
"taxi_url": taxi_url,
|
|
532
|
+
"taxi_tip": "🚕 点击链接可唤起高德地图APP打车",
|
|
533
|
+
"metro_routes": metro_routes[:3],
|
|
534
|
+
"bus_routes": bus_routes[:3],
|
|
535
|
+
"disclaimer": "⚠️ 打车费用为估算,公交路线仅供参考,请以实际为准",
|
|
536
|
+
"tips": _build_tips("search_transport", destination)
|
|
537
|
+
}, ensure_ascii=False, indent=2)
|