amap_service.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. """高德地图MCP服务封装"""
  2. import json
  3. import re
  4. from typing import List, Dict, Any, Optional
  5. from ..agents.mcp_tool import MCPTool
  6. from ..config import get_settings
  7. from ..models.schemas import Location, POIInfo, WeatherInfo
  8. # 全局MCP工具实例
  9. _amap_mcp_tool = None
  10. def get_amap_mcp_tool() -> MCPTool:
  11. """
  12. 获取高德地图MCP工具实例(单例模式)
  13. Returns:
  14. MCPTool实例
  15. """
  16. global _amap_mcp_tool
  17. if _amap_mcp_tool is None:
  18. settings = get_settings()
  19. if not settings.amap_api_key:
  20. raise ValueError("高德地图API Key未配置,请在.env文件中设置AMAP_API_KEY")
  21. # 创建MCP工具
  22. _amap_mcp_tool = MCPTool(
  23. name="amap",
  24. description="高德地图服务,支持POI搜索、路线规划、天气查询等功能",
  25. server_command=["uvx", "amap-mcp-server"],
  26. env={"AMAP_MAPS_API_KEY": settings.amap_api_key},
  27. auto_expand=True # 自动展开为独立工具
  28. )
  29. print(f"✅ 高德地图MCP工具初始化成功")
  30. print(f" 工具数量: {len(_amap_mcp_tool._available_tools)}")
  31. # 打印可用工具列表
  32. if _amap_mcp_tool._available_tools:
  33. print(" 可用工具:")
  34. for tool in _amap_mcp_tool._available_tools[:5]: # 只打印前5个
  35. print(f" - {tool.get('name', 'unknown')}")
  36. if len(_amap_mcp_tool._available_tools) > 5:
  37. print(f" ... 还有 {len(_amap_mcp_tool._available_tools) - 5} 个工具")
  38. return _amap_mcp_tool
  39. class AmapService:
  40. """高德地图服务封装类"""
  41. def __init__(self):
  42. """初始化服务"""
  43. self.mcp_tool = get_amap_mcp_tool()
  44. def search_poi(self, keywords: str, city: str, citylimit: bool = True) -> List[POIInfo]:
  45. """
  46. 搜索POI
  47. """
  48. try:
  49. result = self.mcp_tool.run({
  50. "action": "call_tool",
  51. "tool_name": "maps_text_search",
  52. "arguments": {
  53. "keywords": keywords,
  54. "city": city,
  55. "citylimit": str(citylimit).lower()
  56. }
  57. })
  58. # 从 MCP 返回文本中提取 JSON
  59. json_match = re.search(r'\{.*\}', result, re.DOTALL)
  60. if not json_match:
  61. return []
  62. data = json.loads(json_match.group())
  63. pois_data = data.get("pois", [])
  64. pois = []
  65. for p in pois_data:
  66. loc = None
  67. location_str = p.get("location", "")
  68. if location_str and isinstance(location_str, str) and "," in location_str:
  69. try:
  70. lng, lat = location_str.split(",")
  71. loc = Location(longitude=float(lng), latitude=float(lat))
  72. except (ValueError, TypeError):
  73. pass
  74. pois.append(POIInfo(
  75. id=p.get("id", ""),
  76. name=p.get("name", ""),
  77. type=p.get("typecode", p.get("type", "")),
  78. address=p.get("address", ""),
  79. location=loc or Location(longitude=116.4, latitude=39.9),
  80. tel=p.get("tel")
  81. ))
  82. print(f" ✅ POI搜索成功: {len(pois)} 条结果")
  83. return pois
  84. except Exception as e:
  85. print(f"❌ POI搜索失败: {str(e)}")
  86. return []
  87. def get_weather(self, city: str) -> List[WeatherInfo]:
  88. """
  89. 查询天气
  90. """
  91. try:
  92. result = self.mcp_tool.run({
  93. "action": "call_tool",
  94. "tool_name": "maps_weather",
  95. "arguments": {
  96. "city": city
  97. }
  98. })
  99. json_match = re.search(r'\{.*\}', result, re.DOTALL)
  100. if not json_match:
  101. return []
  102. data = json.loads(json_match.group())
  103. # 高德天气返回 forecast 格式
  104. forecasts = data.get("forecasts", [])
  105. weather_list = []
  106. for w in forecasts:
  107. weather_list.append(WeatherInfo(
  108. date=w.get("date", ""),
  109. day_weather=w.get("dayweather", ""),
  110. night_weather=w.get("nightweather", ""),
  111. day_temp=w.get("daytemp", w.get("daytemp_float", 0)),
  112. night_temp=w.get("nighttemp", w.get("nighttemp_float", 0)),
  113. wind_direction=w.get("daywind", ""),
  114. wind_power=w.get("daypower", "")
  115. ))
  116. print(f" ✅ 天气查询成功: {len(weather_list)} 条记录")
  117. return weather_list
  118. except Exception as e:
  119. print(f"❌ 天气查询失败: {str(e)}")
  120. return []
  121. def plan_route(
  122. self,
  123. origin_address: str,
  124. destination_address: str,
  125. origin_city: Optional[str] = None,
  126. destination_city: Optional[str] = None,
  127. route_type: str = "walking"
  128. ) -> Dict[str, Any]:
  129. """
  130. 规划路线,调用高德地图MCP获取真实路线数据
  131. Args:
  132. origin_address: 起点地址
  133. destination_address: 终点地址
  134. origin_city: 起点城市
  135. destination_city: 终点城市
  136. route_type: 路线类型 (walking/driving/transit)
  137. Returns:
  138. 路线信息字典,包含 distance(米)、duration(秒)、type、segments
  139. """
  140. try:
  141. tool_map = {
  142. "walking": "maps_direction_walking_by_address",
  143. "driving": "maps_direction_driving_by_address",
  144. "transit": "maps_direction_transit_integrated_by_address"
  145. }
  146. tool_name = tool_map.get(route_type, "maps_direction_walking_by_address")
  147. arguments = {
  148. "origin_address": origin_address,
  149. "destination_address": destination_address
  150. }
  151. if origin_city:
  152. arguments["origin_city"] = origin_city
  153. if destination_city:
  154. arguments["destination_city"] = destination_city
  155. result = self.mcp_tool.run({
  156. "action": "call_tool",
  157. "tool_name": tool_name,
  158. "arguments": arguments
  159. })
  160. parsed = self._parse_route_response(result, route_type)
  161. if not parsed:
  162. print(f" ⚠️ 路线({route_type})返回空: {result[:150]}")
  163. else:
  164. print(f" ✅ 路线({route_type})成功: {parsed.get('distance',0)}m, {parsed.get('duration',0)}s")
  165. return parsed
  166. except Exception as e:
  167. print(f"❌ 路线规划失败: {str(e)}")
  168. import traceback
  169. traceback.print_exc()
  170. return {}
  171. def _parse_python_repr(self, text: str) -> Optional[Dict]:
  172. """amap-mcp-server 返回的是 Python repr(单引号),尝试解析"""
  173. import ast
  174. try:
  175. result = ast.literal_eval(text)
  176. if isinstance(result, dict):
  177. # 递归将键名统一为str
  178. return json.loads(json.dumps(result))
  179. return None
  180. except Exception:
  181. return None
  182. def _parse_route_response(self, result: str, route_type: str) -> Dict[str, Any]:
  183. """解析MCP路线返回结果为统一格式"""
  184. json_match = re.search(r'\{.*\}', result, re.DOTALL)
  185. if not json_match:
  186. return {}
  187. raw_text = json_match.group()
  188. data = None
  189. # 先尝试标准 JSON
  190. try:
  191. data = json.loads(raw_text)
  192. except json.JSONDecodeError:
  193. # 再尝试 Python repr (单引号)
  194. data = self._parse_python_repr(raw_text)
  195. if not data:
  196. print(f" ⚠️ 无法解析路线返回数据, 前100字符: {raw_text[:100]}")
  197. return {}
  198. route = data.get("route", data)
  199. info = {"distance": 0, "duration": 0, "type": route_type, "segments": []}
  200. if route_type == "transit":
  201. transits = route.get("transits", [])
  202. if transits:
  203. transit = transits[0]
  204. info["duration"] = self._safe_int(
  205. transit.get("cost", {}).get("duration", "0")
  206. )
  207. for seg in transit.get("segments", []):
  208. info["segments"].extend(
  209. self._parse_transit_segment(seg)
  210. )
  211. else:
  212. paths = route.get("paths", [])
  213. if paths:
  214. path = paths[0]
  215. info["distance"] = self._safe_int(path.get("distance", "0"))
  216. info["duration"] = self._safe_int(path.get("duration", "0"))
  217. steps = path.get("steps", [])
  218. for step in steps:
  219. info["segments"].append({
  220. "instruction": step.get("instruction", ""),
  221. "distance": self._safe_int(step.get("distance", "0")),
  222. "duration": self._safe_int(step.get("duration", "0")),
  223. })
  224. if not steps:
  225. info["segments"].append({
  226. "instruction": f"从起点到终点",
  227. "distance": info["distance"],
  228. "duration": info["duration"],
  229. })
  230. return info
  231. def _parse_transit_segment(self, seg: Dict) -> List[Dict]:
  232. """解析公共交通的一个分段"""
  233. segments = []
  234. if "walking" in seg:
  235. walk = seg["walking"]
  236. instr = "步行"
  237. if walk.get("steps"):
  238. instr = walk["steps"][0].get("instruction", "步行")
  239. segments.append({
  240. "instruction": instr,
  241. "distance": self._safe_int(walk.get("distance", "0")),
  242. "duration": self._safe_int(walk.get("duration", "0")),
  243. })
  244. if "bus" in seg:
  245. bus = seg["bus"]
  246. buslines = bus.get("buslines", [])
  247. if buslines:
  248. bl = buslines[0]
  249. segments.append(self._make_vehicle_segment(bl, "公交"))
  250. if "subway" in seg:
  251. subway = seg["subway"]
  252. subwaylines = subway.get("subwaylines", [])
  253. if subwaylines:
  254. sl = subwaylines[0]
  255. segments.append(self._make_vehicle_segment(sl, "地铁"))
  256. return segments
  257. def _make_vehicle_segment(self, line: Dict, mode: str) -> Dict:
  258. """生成交通工具分段"""
  259. pass_num = line.get("pass_stop_num", "0")
  260. return {
  261. "instruction": f"乘坐{line.get('name', mode)}",
  262. "distance": self._safe_int(line.get("distance", "0")),
  263. "duration": self._safe_int(line.get("duration", "0")),
  264. "route_detail": f"经过{pass_num}站",
  265. "departure_stop": line.get("departure_stop", {}).get("name", ""),
  266. "arrival_stop": line.get("arrival_stop", {}).get("name", ""),
  267. }
  268. @staticmethod
  269. def _safe_int(value: Any) -> int:
  270. """安全转int"""
  271. if isinstance(value, (int, float)):
  272. return int(value)
  273. try:
  274. return int(float(str(value).replace(",", "")))
  275. except (ValueError, TypeError):
  276. return 0
  277. def get_route_segments(
  278. self,
  279. origin_address: str,
  280. destination_address: str,
  281. origin_name: str = "",
  282. destination_name: str = "",
  283. origin_city: Optional[str] = None,
  284. destination_city: Optional[str] = None,
  285. route_type: str = "transit"
  286. ) -> List[Dict]:
  287. """
  288. 获取两点之间的交通分段信息,格式化为TransportSegment兼容的字典
  289. Args:
  290. origin_address: 起点地址
  291. destination_address: 终点地址
  292. origin_name: 起点名称(如酒店名/景点名)
  293. destination_name: 终点名称
  294. origin_city: 起点城市
  295. destination_city: 终点城市
  296. route_type: walking/driving/transit
  297. Returns:
  298. List[Dict], 每段包含 type/instruction/from_name/to_name/duration/distance/route_detail
  299. """
  300. raw = self.plan_route(
  301. origin_address=origin_address,
  302. destination_address=destination_address,
  303. origin_city=origin_city,
  304. destination_city=destination_city,
  305. route_type=route_type
  306. )
  307. if not raw:
  308. return []
  309. type_map = {
  310. "walking": "步行",
  311. "driving": "自驾",
  312. "transit": "公共交通",
  313. }
  314. segments = raw.get("segments", [])
  315. result = []
  316. base_minutes = 0
  317. if not segments and raw.get("distance", 0) > 0:
  318. total_dist = raw.get("distance", 0)
  319. total_dur = max(1, raw.get("duration", 0) // 60)
  320. hour = 8 + base_minutes // 60
  321. minute = base_minutes % 60
  322. route_type_cn = type_map.get(route_type, "公共交通")
  323. result.append({
  324. "type": route_type_cn,
  325. "instruction": f"从{origin_name or origin_address}前往{destination_name or destination_address}",
  326. "from_name": origin_name or origin_address,
  327. "to_name": destination_name or destination_address,
  328. "departure_time": f"{hour:02d}:{minute:02d}",
  329. "duration": total_dur,
  330. "distance": total_dist,
  331. "route_detail": f"总距离约{round(total_dist / 1000, 1)}公里" if total_dist >= 1000 else f"总距离{total_dist}米",
  332. })
  333. else:
  334. for seg in segments:
  335. dur_min = max(1, seg.get("duration", 0) // 60)
  336. dist = seg.get("distance", 0)
  337. current_minutes = base_minutes
  338. hour = 8 + current_minutes // 60
  339. minute = current_minutes % 60
  340. instruction = seg.get("instruction", "")
  341. route_detail = seg.get("route_detail", "")
  342. # 判断交通类型
  343. instr_lower = instruction.lower()
  344. if "步行" in instruction or route_type == "walking":
  345. seg_type = "步行"
  346. elif "公交" in instruction or "bus" in instr_lower:
  347. seg_type = "公交"
  348. elif "地铁" in instruction or "subway" in instr_lower:
  349. seg_type = "地铁"
  350. elif route_type == "driving":
  351. seg_type = "自驾"
  352. else:
  353. seg_type = "公共交通"
  354. dep_stop = seg.get("departure_stop", "")
  355. arr_stop = seg.get("arrival_stop", "")
  356. full_instruction = instruction
  357. if dep_stop and arr_stop:
  358. full_instruction = f"从{dep_stop}出发,{instruction}到{arr_stop}"
  359. # 起点/终点名称
  360. seg_from = origin_name
  361. if result:
  362. seg_from = dep_stop or origin_name
  363. seg_to = destination_name
  364. if seg != segments[-1]:
  365. seg_to = arr_stop or destination_name
  366. result.append({
  367. "type": seg_type,
  368. "instruction": full_instruction,
  369. "from_name": seg_from,
  370. "to_name": seg_to,
  371. "departure_time": f"{hour:02d}:{minute:02d}",
  372. "duration": dur_min,
  373. "distance": dist,
  374. "route_detail": route_detail or (f"约{dist}米" if dist else ""),
  375. })
  376. base_minutes += dur_min
  377. return result
  378. def get_route_via_http(
  379. self,
  380. origin_address: str,
  381. destination_address: str,
  382. origin_name: str = "",
  383. destination_name: str = "",
  384. origin_city: Optional[str] = None,
  385. destination_city: Optional[str] = None,
  386. route_type: str = "transit"
  387. ) -> List[Dict]:
  388. """
  389. 通过高德HTTP API直接获取路线(绕过MCP子进程,更快更稳定)
  390. Returns:
  391. List[Dict], 同 get_route_segments 格式
  392. """
  393. import urllib.request, urllib.parse
  394. from ..config import get_settings
  395. settings = get_settings()
  396. if not settings.amap_api_key:
  397. return []
  398. city = origin_city or ""
  399. # origin/destination 先尝试地理编码
  400. origin_lng, origin_lat = self._geocode_sync(origin_address, city)
  401. dest_lng, dest_lat = self._geocode_sync(destination_address, city)
  402. if not origin_lng or not dest_lng:
  403. return []
  404. try:
  405. if route_type == "transit":
  406. params = urllib.parse.urlencode({
  407. "key": settings.amap_api_key,
  408. "origin": f"{origin_lng},{origin_lat}",
  409. "destination": f"{dest_lng},{dest_lat}",
  410. "city": city,
  411. "cityd": city,
  412. }, encoding="utf-8")
  413. url = f"https://restapi.amap.com/v3/direction/transit/integrated?{params}"
  414. elif route_type == "walking":
  415. params = urllib.parse.urlencode({
  416. "key": settings.amap_api_key,
  417. "origin": f"{origin_lng},{origin_lat}",
  418. "destination": f"{dest_lng},{dest_lat}",
  419. }, encoding="utf-8")
  420. url = f"https://restapi.amap.com/v3/direction/walking?{params}"
  421. elif route_type == "driving":
  422. params = urllib.parse.urlencode({
  423. "key": settings.amap_api_key,
  424. "origin": f"{origin_lng},{origin_lat}",
  425. "destination": f"{dest_lng},{dest_lat}",
  426. "city": city,
  427. }, encoding="utf-8")
  428. url = f"https://restapi.amap.com/v3/direction/driving?{params}"
  429. else:
  430. return []
  431. resp = urllib.request.urlopen(url, timeout=10)
  432. data = json.loads(resp.read().decode("utf-8"))
  433. if data.get("status") != "1":
  434. return []
  435. segments = []
  436. base_minutes = 0
  437. if route_type == "transit":
  438. route = data.get("route", {})
  439. transits = route.get("transits", [])
  440. if not transits:
  441. return []
  442. transit = transits[0]
  443. total_dur = self._safe_int(transit.get("duration", "0"))
  444. total_dist = self._safe_int(transit.get("distance", "0"))
  445. # 预检: 如果只有步行段且总距离>500m,返回空让调用者降级
  446. has_vehicle = any("bus" in seg or "subway" in seg for seg in transit.get("segments", []))
  447. total_walk_dist = sum(
  448. self._safe_int(seg["walking"].get("distance", "0"))
  449. for seg in transit.get("segments", []) if "walking" in seg
  450. )
  451. if not has_vehicle and total_walk_dist > 500:
  452. return [] # 全程步行且距离过长,触发调用方降级
  453. for seg in transit.get("segments", []):
  454. dur_min = max(1, self._safe_int(seg.get("duration", "0")) // 60)
  455. dist = self._safe_int(seg.get("distance", "0"))
  456. hour = 8 + base_minutes // 60
  457. minute = base_minutes % 60
  458. if "walking" in seg:
  459. walk = seg["walking"]
  460. walk_dist = self._safe_int(walk.get("distance", "0"))
  461. walk_dur = max(1, self._safe_int(walk.get("duration", "0")) // 60)
  462. instruction = f"步行{walk_dist}米"
  463. if walk_dist > 500 and has_vehicle:
  464. instruction += "(步行距离较长,建议共享单车)"
  465. elif walk_dist > 500:
  466. instruction += "(距离较长,建议乘车)"
  467. segments.append({
  468. "type": "步行",
  469. "instruction": instruction,
  470. "from_name": origin_name if not segments else origin_name,
  471. "to_name": destination_name,
  472. "departure_time": f"{hour:02d}:{minute:02d}",
  473. "duration": walk_dur,
  474. "distance": walk_dist,
  475. "route_detail": f"步行{walk_dist}米",
  476. })
  477. elif "bus" in seg:
  478. for bl in seg["bus"].get("buslines", []):
  479. dep_stop = bl.get("departure_stop", {}).get("name", "")
  480. arr_stop = bl.get("arrival_stop", {}).get("name", "")
  481. pass_num = bl.get("pass_stop_num", "0")
  482. segments.append({
  483. "type": "公交",
  484. "instruction": f"乘坐{bl.get('name', '公交')}",
  485. "from_name": f"{dep_stop}" if dep_stop else origin_name,
  486. "to_name": f"{arr_stop}" if arr_stop else destination_name,
  487. "departure_time": f"{hour:02d}:{minute:02d}",
  488. "duration": max(1, self._safe_int(bl.get("duration", "0")) // 60),
  489. "distance": self._safe_int(bl.get("distance", "0")),
  490. "route_detail": f"{bl.get('name', '')}·经过{pass_num}站",
  491. })
  492. elif "subway" in seg:
  493. for sl in seg["subway"].get("subwaylines", []):
  494. dep_stop = sl.get("departure_stop", {}).get("name", "")
  495. arr_stop = sl.get("arrival_stop", {}).get("name", "")
  496. pass_num = sl.get("pass_stop_num", "0")
  497. segments.append({
  498. "type": "地铁",
  499. "instruction": f"乘坐{sl.get('name', '地铁')}",
  500. "from_name": f"{dep_stop}" if dep_stop else origin_name,
  501. "to_name": f"{arr_stop}" if arr_stop else destination_name,
  502. "departure_time": f"{hour:02d}:{minute:02d}",
  503. "duration": max(1, self._safe_int(sl.get("duration", "0")) // 60),
  504. "distance": self._safe_int(sl.get("distance", "0")),
  505. "route_detail": f"{sl.get('name', '')}·经过{pass_num}站",
  506. })
  507. base_minutes += dur_min
  508. if not segments:
  509. # 只有总数据,生成一个整体段
  510. segments.append({
  511. "type": "公共交通",
  512. "instruction": f"从{origin_name or origin_address}到{destination_name or destination_address}",
  513. "from_name": origin_name or origin_address,
  514. "to_name": destination_name or destination_address,
  515. "departure_time": "08:00",
  516. "duration": max(1, total_dur // 60),
  517. "distance": total_dist,
  518. "route_detail": f"约{round(total_dist/1000,1)}公里",
  519. })
  520. else:
  521. # walking/driving
  522. route = data.get("route", {})
  523. paths = route.get("paths", [])
  524. if paths:
  525. path = paths[0]
  526. total_dist = self._safe_int(path.get("distance", "0"))
  527. total_dur = self._safe_int(path.get("duration", "0"))
  528. road_type_cn = "步行" if route_type == "walking" else "自驾"
  529. segments.append({
  530. "type": road_type_cn,
  531. "instruction": f"从{origin_name or origin_address}到{destination_name or destination_address}",
  532. "from_name": origin_name or origin_address,
  533. "to_name": destination_name or destination_address,
  534. "departure_time": "08:00",
  535. "duration": max(1, total_dur // 60),
  536. "distance": total_dist,
  537. "route_detail": f"约{round(total_dist/1000,1)}公里",
  538. })
  539. return segments
  540. except Exception as e:
  541. print(f" ⚠️ HTTP路线({route_type})失败: {e}")
  542. return []
  543. def _geocode_sync(self, address: str, city: str) -> tuple:
  544. """同步地理编码,返回 (lng, lat)"""
  545. import urllib.request, urllib.parse
  546. from ..config import get_settings
  547. try:
  548. params = urllib.parse.urlencode({
  549. "key": get_settings().amap_api_key,
  550. "address": address,
  551. "city": city,
  552. }, encoding="utf-8")
  553. url = f"https://restapi.amap.com/v3/geocode/geo?{params}"
  554. resp = urllib.request.urlopen(url, timeout=10)
  555. data = json.loads(resp.read().decode("utf-8"))
  556. if data.get("status") == "1" and data.get("geocodes"):
  557. loc = data["geocodes"][0].get("location", "")
  558. if loc and "," in loc:
  559. parts = loc.split(",")
  560. return parts[0], parts[1]
  561. except Exception:
  562. pass
  563. return None, None
  564. def geocode(self, address: str, city: Optional[str] = None) -> Optional[Location]:
  565. """
  566. 地理编码(地址转坐标)
  567. Args:
  568. address: 地址
  569. city: 城市
  570. Returns:
  571. 经纬度坐标
  572. """
  573. try:
  574. arguments = {"address": address}
  575. if city:
  576. arguments["city"] = city
  577. result = self.mcp_tool.run({
  578. "action": "call_tool",
  579. "tool_name": "maps_geo",
  580. "arguments": arguments
  581. })
  582. print(f"地理编码结果: {result[:200]}...")
  583. # TODO: 解析实际的坐标数据
  584. return None
  585. except Exception as e:
  586. print(f"❌ 地理编码失败: {str(e)}")
  587. return None
  588. def get_poi_detail(self, poi_id: str) -> Dict[str, Any]:
  589. """
  590. 获取POI详情
  591. Args:
  592. poi_id: POI ID
  593. Returns:
  594. POI详情信息
  595. """
  596. try:
  597. result = self.mcp_tool.run({
  598. "action": "call_tool",
  599. "tool_name": "maps_search_detail",
  600. "arguments": {
  601. "id": poi_id
  602. }
  603. })
  604. print(f"POI详情结果: {result[:200]}...")
  605. json_match = re.search(r'\{.*\}', result, re.DOTALL)
  606. if json_match:
  607. data = json.loads(json_match.group())
  608. return data
  609. return {"raw": result}
  610. except Exception as e:
  611. print(f"❌ 获取POI详情失败: {str(e)}")
  612. return {}
  613. # 创建全局服务实例
  614. _amap_service = None
  615. def get_amap_service() -> AmapService:
  616. """获取高德地图服务实例(单例模式)"""
  617. global _amap_service
  618. if _amap_service is None:
  619. _amap_service = AmapService()
  620. return _amap_service