From 8dc232ebab8ca4ecd3d48a07b2957f5d6c5f953a Mon Sep 17 00:00:00 2001 From: wunianxiao Date: Thu, 4 Jun 2026 13:27:04 +0800 Subject: [PATCH] =?UTF-8?q?refactor(amap):=20=E7=AE=80=E5=8C=96=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E6=A0=BC=E5=BC=8F=EF=BC=8C=E8=BF=94=E5=9B=9E=E5=8E=9F?= =?UTF-8?q?=E5=A7=8BAPI=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除自定义文本格式化函数(haversine_distance, format_distance, parse_location 等) - 移除 build_route_output 函数 - execute 函数改为返回简短确认消息 + 原始 API JSON 数据 - CLI 命令同时输出 r.output 和 r.data(JSON) - 移除 driving/riding API 的 show_fields=cost 参数 - 清理已删除函数的冗余单元测试 --- src/main.rs | 79 ++++-- src/tools/builtins/amap.rs | 478 +++++++------------------------------ 2 files changed, 153 insertions(+), 404 deletions(-) diff --git a/src/main.rs b/src/main.rs index f0f1ba0..4947356 100644 --- a/src/main.rs +++ b/src/main.rs @@ -849,7 +849,6 @@ async fn cmd_send( } } - use tools::types::SkillResult; async fn builtin_approve(ctx: &ExecutionContext, name: &str) -> Result { @@ -875,7 +874,6 @@ async fn builtin_approve(ctx: &ExecutionContext, name: &str) -> Result { let mut params = serde_json::json!({"address": address}); - if let Some(c) = city { params["city"] = serde_json::json!(c); } + if let Some(c) = city { + params["city"] = serde_json::json!(c); + } let result = tools::builtins::amap::execute("amap_geocode", params).await; - if let Some(r) = result { println!("{}", r.output); } + if let Some(r) = result { + println!( + "{}\n{}", + r.output, + serde_json::to_string(&r.data).unwrap_or_default() + ); + } } AmapAction::ReverseGeocode { location } => { let params = serde_json::json!({"location": location}); let result = tools::builtins::amap::execute("amap_reverse_geocode", params).await; - if let Some(r) = result { println!("{}", r.output); } + if let Some(r) = result { + println!( + "{}\n{}", + r.output, + serde_json::to_string(&r.data).unwrap_or_default() + ); + } } AmapAction::RoutePlan { r#type, @@ -982,11 +1006,23 @@ async fn cmd_amap(action: AmapAction) { "origin": origin, "destination": destination, }); - if let Some(c) = city { params["city"] = serde_json::json!(c); } - if let Some(w) = waypoints { params["waypoints"] = serde_json::json!(w); } - if let Some(s) = strategy { params["strategy"] = serde_json::json!(s); } + if let Some(c) = city { + params["city"] = serde_json::json!(c); + } + if let Some(w) = waypoints { + params["waypoints"] = serde_json::json!(w); + } + if let Some(s) = strategy { + params["strategy"] = serde_json::json!(s); + } let result = tools::builtins::amap::execute("amap_route_plan", params).await; - if let Some(r) = result { println!("{}", r.output); } + if let Some(r) = result { + println!( + "{}\n{}", + r.output, + serde_json::to_string(&r.data).unwrap_or_default() + ); + } } AmapAction::TravelPlan { city, @@ -1003,15 +1039,26 @@ async fn cmd_amap(action: AmapAction) { "route_type": route_type, }); let result = tools::builtins::amap::execute("amap_travel_plan", params).await; - if let Some(r) = result { println!("{}", r.output); } + if let Some(r) = result { + println!( + "{}\n{}", + r.output, + serde_json::to_string(&r.data).unwrap_or_default() + ); + } } AmapAction::MapLink { data } => { let map_data: serde_json::Value = serde_json::from_str(&data).unwrap_or(serde_json::json!([])); let params = serde_json::json!({"map_data": map_data}); let result = tools::builtins::amap::execute("amap_map_link", params).await; - if let Some(r) = result { println!("{}", r.output); } + if let Some(r) = result { + println!( + "{}\n{}", + r.output, + serde_json::to_string(&r.data).unwrap_or_default() + ); + } } } } - diff --git a/src/tools/builtins/amap.rs b/src/tools/builtins/amap.rs index d2af46c..9d7807e 100644 --- a/src/tools/builtins/amap.rs +++ b/src/tools/builtins/amap.rs @@ -16,6 +16,7 @@ use reqwest::Client; use std::time::Duration; +use tracing::info; use crate::tools::types::{RiskLevel, SkillResult, SkillSpec}; @@ -130,7 +131,11 @@ impl AmapClient { } /// 地理编码:地址 → 坐标 (v3) - async fn geocode(&self, address: &str, city: Option<&str>) -> Result { + async fn geocode( + &self, + address: &str, + city: Option<&str>, + ) -> Result { let mut params = vec![ ("key", self.key.as_str()), ("address", address), @@ -236,13 +241,18 @@ impl AmapClient { ("origin", origin), ("destination", destination), ("strategy", &ss), - ("show_fields", "cost"), + // ("show_fields", "cost"), ("appname", "amap-lbs-skill"), ]; if let Some(wp) = waypoints { params.push(("waypoints", wp)); } + info!( + "parms: {:?}", + serde_json::to_string(¶ms).unwrap_or_default() + ); + let resp: serde_json::Value = self .http .get("https://restapi.amap.com/v5/direction/driving") @@ -255,6 +265,10 @@ impl AmapClient { .map_err(|e| format!("解析驾车路线响应失败: {}", e))?; if resp["status"].as_str() != Some("1") { + info!( + "parms: {:?}", + serde_json::to_string(&resp).unwrap_or_default() + ); return Err(format!( "驾车路线失败: {}", resp["info"].as_str().unwrap_or("未知错误") @@ -276,7 +290,7 @@ impl AmapClient { ("key", self.key.as_str()), ("origin", origin), ("destination", destination), - ("show_fields", "cost"), + // ("show_fields", "cost"), ("appname", "amap-lbs-skill"), ]) .send() @@ -367,72 +381,9 @@ impl AmapClient { } // ═══════════════════════════════════════════════ -// 辅助:距离计算与格式化 +// 辅助:URL 编码 // ═══════════════════════════════════════════════ -/// Haversine 公式计算两点间距离(米) -fn haversine_distance(lng1: f64, lat1: f64, lng2: f64, lat2: f64) -> f64 { - const R: f64 = 6_371_000.0; - let d_lat = (lat2 - lat1).to_radians(); - let d_lng = (lng2 - lng1).to_radians(); - let a = (d_lat / 2.0).sin().powi(2) - + lat1.to_radians().cos() * lat2.to_radians().cos() * (d_lng / 2.0).sin().powi(2); - let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt()); - R * c -} - -/// 格式化距离: ≥1000m → km,<1000m → m,0 → 空 -fn format_distance(meters: f64) -> String { - if meters >= 1000.0 { - format!("{:.1} km", meters / 1000.0) - } else if meters >= 10.0 { - format!("{:.0} m", meters) - } else { - String::new() - } -} - -/// 解析 "经度,纬度" 字符串为 (lng, lat) -fn parse_location(s: &str) -> Option<(f64, f64)> { - let parts: Vec = s.split(',').filter_map(|p| p.trim().parse().ok()).collect(); - if parts.len() == 2 { Some((parts[0], parts[1])) } else { None } -} - -// ═══════════════════════════════════════════════ -// 辅助:解析 v5 路线中的数值字段(可能是字符串或数字) -// ═══════════════════════════════════════════════ - -fn val_f64(v: &serde_json::Value) -> f64 { - v.as_str() - .and_then(|s| s.parse().ok()) - .or_else(|| v.as_f64()) - .unwrap_or(0.0) -} - -/// 获取 path 下的 distance(v5 为字符串) -fn path_distance(path: &serde_json::Value) -> f64 { - val_f64(&path["distance"]) -} - -/// 获取 path.cost.duration(v5 需 show_fields=cost) -fn path_duration(path: &serde_json::Value) -> f64 { - val_f64(&path["cost"]["duration"]) -} - -/// 获取 path.cost.tolls(驾车) -fn path_tolls(path: &serde_json::Value) -> f64 { - val_f64(&path["cost"]["tolls"]) -} - -/// 获取 path.traffic_lights(驾车 v5 直接在 path 下) -fn path_traffic_lights(path: &serde_json::Value) -> u32 { - path["traffic_lights"] - .as_str() - .and_then(|s| s.parse().ok()) - .or_else(|| path["traffic_lights"].as_u64().map(|v| v as u32)) - .unwrap_or(0) -} - // ═══════════════════════════════════════════════ // 1. amap_poi_search — POI 搜索 // ═══════════════════════════════════════════════ @@ -479,73 +430,25 @@ pub async fn poi_search_execute(params: serde_json::Value) -> SkillResult { Err(e) => return SkillResult::error(e), }; - // 解析参考坐标(用于计算每个 POI 的距离) - let ref_loc: Option<(f64, f64)> = location.and_then(|s| parse_location(s)); - - let result = if let Some((rlng, rlat)) = ref_loc { - tracing::info!("📍 周边搜索: {} @ {},{} (radius={}m, sort=distance)", keywords, rlng, rlat, radius); - client.poi_around(keywords, &format!("{rlng},{rlat}"), radius, page_num, page_size, "distance").await + let result = if let Some(loc) = location { + tracing::info!( + "📍 周边搜索: {} @ {} (radius={}m, sort=distance)", + keywords, + loc, + radius + ); + client + .poi_around(keywords, loc, radius, page_num, page_size, "distance") + .await } else { tracing::info!("🔍 POI 搜索: {} city={:?}", keywords, city); - client.poi_text_search(keywords, city, page_num, page_size).await + client + .poi_text_search(keywords, city, page_num, page_size) + .await }; match result { - Ok(data) => { - let count = data["count"].as_str().unwrap_or("?"); - let pois = &data["pois"]; - let poi_count = pois.as_array().map(|a| a.len()).unwrap_or(0); - let mut text = format!( - "✅ 搜索「{keywords}」共 {count} 条结果,当前页显示 {poi_count} 条:\n", - ); - if let Some(arr) = pois.as_array() { - for (i, poi) in arr.iter().enumerate() { - let name = poi["name"].as_str().unwrap_or("未知"); - let address = poi["address"].as_str().unwrap_or(""); - let loc = poi["location"].as_str().unwrap_or("未知"); - let ptype = poi["type"].as_str().unwrap_or(""); - - // 距离:优先用 Haversine 计算(有参考坐标时),否则用 API 返回值 - let dist = match ref_loc { - Some((rlng, rlat)) => { - if let Some((plng, plat)) = parse_location(loc) { - let d = haversine_distance(rlng, rlat, plng, plat); - format!(" | {}", format_distance(d)) - } else { - String::new() - } - } - None => poi["distance"] - .as_str() - .filter(|s| !s.is_empty()) - .and_then(|s| s.parse::().ok()) - .map(|d| format!(" | {}", format_distance(d))) - .unwrap_or_default(), - }; - - let tel = poi["tel"] - .as_str() - .filter(|t| !t.is_empty()) - .map(|t| format!(" | 📞 {t}")) - .unwrap_or_default(); - let rating = poi["rating"] - .as_str() - .filter(|s| !s.is_empty()) - .map(|s| format!(" | ⭐{s}")) - .unwrap_or_default(); - let cost = poi["cost"] - .as_str() - .filter(|s| !s.is_empty()) - .map(|s| format!(" | 人均{s}元")) - .unwrap_or_default(); - text.push_str(&format!( - " {}. {name} [{ptype}]\n 📍 {address}{dist}{tel}{rating}{cost}\n 🗺️ {loc}\n", - i + 1 - )); - } - } - SkillResult::ok_with_data(text, data) - } + Ok(data) => SkillResult::ok_with_data("✅ POI 搜索完成".to_string(), data), Err(e) => SkillResult::error(e), } } @@ -588,31 +491,7 @@ pub async fn geocode_execute(params: serde_json::Value) -> SkillResult { tracing::info!("📍 地理编码: {} city={:?}", address, city); match client.geocode(address, city).await { - Ok(data) => { - let geocodes = &data["geocodes"]; - if let Some(arr) = geocodes.as_array() { - if arr.is_empty() { - return SkillResult::error(format!("未找到地址: {address}")); - } - let loc = arr[0]["location"].as_str().unwrap_or("未知"); - let formatted = arr[0]["formatted_address"].as_str().unwrap_or(address); - let pname = arr[0]["pname"].as_str().unwrap_or(""); - let cityname = arr[0]["cityname"].as_str().unwrap_or(""); - let adname = arr[0]["adname"].as_str().unwrap_or(""); - let text = format!("📍 {formatted}({pname}{cityname}{adname})坐标: {loc}"); - let out = serde_json::json!({ - "address": formatted, - "location": loc, - "province": pname, - "city": cityname, - "district": adname, - "top_result": arr[0] - }); - SkillResult::ok_with_data(text, out) - } else { - SkillResult::error(format!("未找到地址: {address}")) - } - } + Ok(data) => SkillResult::ok_with_data("✅ 地理编码完成".to_string(), data), Err(e) => SkillResult::error(e), } } @@ -653,14 +532,7 @@ pub async fn reverse_geocode_execute(params: serde_json::Value) -> SkillResult { tracing::info!("📍 逆地理编码: {}", location); match client.regeocode(location).await { - Ok(data) => { - let regeocode = &data["regeocode"]; - let addr = regeocode["formatted_address"] - .as_str() - .unwrap_or("未知地址"); - let text = format!("📍 坐标 {location} → {addr}"); - SkillResult::ok_with_data(text, data) - } + Ok(data) => SkillResult::ok_with_data("✅ 逆地理编码完成".to_string(), data), Err(e) => SkillResult::error(e), } } @@ -699,9 +571,7 @@ pub async fn route_plan_execute(params: serde_json::Value) -> SkillResult { let destination = params["destination"].as_str().unwrap_or(""); if route_type.is_empty() || origin.is_empty() || destination.is_empty() { - return SkillResult::error( - "参数错误:缺少 type/origin/destination", - ); + return SkillResult::error("参数错误:缺少 type/origin/destination"); } let client = match AmapClient::new() { @@ -709,52 +579,50 @@ pub async fn route_plan_execute(params: serde_json::Value) -> SkillResult { Err(e) => return SkillResult::error(e), }; - tracing::info!("🛣️ 路径规划: type={}, {} → {}", route_type, origin, destination); + tracing::info!( + "🛣️ 路径规划: type={}, {} → {}", + route_type, + origin, + destination + ); match route_type { "walking" => match client.walking_route(origin, destination).await { - Ok(data) => { - let (summary, out) = build_route_output(&data, "walking"); - SkillResult::ok_with_data(summary, out) - } + Ok(data) => SkillResult::ok_with_data("✅ 步行路线规划完成".to_string(), data), Err(e) => SkillResult::error(e), }, "driving" => { let waypoints = params["waypoints"].as_str(); let strategy = params["strategy"].as_u64().unwrap_or(32) as u32; - match client.driving_route(origin, destination, waypoints, strategy).await { - Ok(data) => { - let (summary, out) = build_route_output(&data, "driving"); - SkillResult::ok_with_data(summary, out) - } + match client + .driving_route(origin, destination, waypoints, strategy) + .await + { + Ok(data) => SkillResult::ok_with_data("✅ 驾车路线规划完成".to_string(), data), Err(e) => SkillResult::error(e), } } "riding" => match client.riding_route(origin, destination).await { - Ok(data) => { - let (summary, out) = build_route_output(&data, "riding"); - SkillResult::ok_with_data(summary, out) - } + Ok(data) => SkillResult::ok_with_data("✅ 骑行路线规划完成".to_string(), data), Err(e) => SkillResult::error(e), }, "electrobike" => match client.electrobike_route(origin, destination).await { - Ok(data) => { - let (summary, out) = build_route_output(&data, "electrobike"); - SkillResult::ok_with_data(summary, out) - } + Ok(data) => SkillResult::ok_with_data("✅ 电动车路线规划完成".to_string(), data), Err(e) => SkillResult::error(e), }, "transit" => { let city_code = params["city"].as_str().unwrap_or(""); if city_code.is_empty() { - return SkillResult::error("公交路线需要提供 city 参数(城市 citycode,如 010 表示北京)"); + return SkillResult::error( + "公交路线需要提供 city 参数(城市 citycode,如 010 表示北京)", + ); } let strategy = params["strategy"].as_u64().unwrap_or(0) as u32; - match client.transit_route(origin, destination, city_code, city_code, strategy).await { - Ok(data) => { - let (summary, out) = build_route_output(&data, "transit"); - SkillResult::ok_with_data(summary, out) - } + match client + .transit_route(origin, destination, city_code, city_code, strategy) + .await + { + Ok(data) => SkillResult::ok_with_data("✅ 公交路线规划完成".to_string(), data), Err(e) => SkillResult::error(e), } } @@ -764,99 +632,6 @@ pub async fn route_plan_execute(params: serde_json::Value) -> SkillResult { } } -/// 构建路线输出:文本摘要 + JSON 数据 -fn build_route_output(data: &serde_json::Value, route_type: &str) -> (String, serde_json::Value) { - let route = &data["route"]; - - match route_type { - "walking" | "riding" | "electrobike" => { - let icon = match route_type { - "walking" => "🚶", - "riding" => "🚴", - "electrobike" => "🛵", - _ => "📍", - }; - let cn = match route_type { - "walking" => "步行", - "riding" => "骑行", - "electrobike" => "电动车骑行", - _ => "", - }; - if let Some(path) = route["paths"].as_array().and_then(|a| a.first()) { - let dist = path_distance(path) / 1000.0; - let dur = (path_duration(path) / 60.0) as u32; - let text = format!("{icon} {cn}路线:距离 {dist:.2} 公里,预计 {dur} 分钟"); - let out = serde_json::json!({ - "route_type": route_type, - "origin": route["origin"], - "destination": route["destination"], - "distance_km": format!("{:.2}", dist), - "duration_min": dur, - "path_count": route["paths"].as_array().map(|a| a.len()).unwrap_or(0), - }); - (text, out) - } else { - (format!("{icon} {cn}路线规划完成"), serde_json::json!({"route_type": route_type})) - } - } - "driving" => { - if let Some(path) = route["paths"].as_array().and_then(|a| a.first()) { - let dist = path_distance(path) / 1000.0; - let dur = (path_duration(path) / 60.0) as u32; - let tolls = path_tolls(path); - let lights = path_traffic_lights(path); - let restriction = path["restriction"] - .as_str() - .map(|r| if r == "0" { "无/已规避" } else { "有限行路段" }) - .unwrap_or("未知"); - let text = format!( - "🚗 驾车路线:距离 {dist:.2} 公里,预计 {dur} 分钟,过路费 {tolls} 元,红绿灯 {lights} 个,限行 {restriction}" - ); - let out = serde_json::json!({ - "route_type": "driving", - "origin": route["origin"], - "destination": route["destination"], - "distance_km": format!("{:.2}", dist), - "duration_min": dur, - "tolls": tolls, - "traffic_lights": lights, - "restriction": restriction, - "path_count": route["paths"].as_array().map(|a| a.len()).unwrap_or(0), - }); - (text, out) - } else { - ("🚗 驾车路线规划完成".into(), serde_json::json!({"route_type": "driving"})) - } - } - "transit" => { - let transits = route["transits"].as_array(); - let count = transits.map(|a| a.len()).unwrap_or(0); - if let Some(t) = transits.and_then(|a| a.first()) { - let dur = val_f64(&t["cost"]["duration"]); - let fee = val_f64(&t["cost"]["transit_fee"]); - let dist = val_f64(&t["distance"]) / 1000.0; - let text = format!( - "🚌 公交路线:共 {count} 个方案,推荐方案 {dist:.2} 公里,预计 {} 分钟,费用 {fee} 元", - (dur / 60.0) as u32 - ); - let out = serde_json::json!({ - "route_type": "transit", - "origin": route["origin"], - "destination": route["destination"], - "scheme_count": count, - "distance_km": format!("{:.2}", dist), - "duration_min": (dur / 60.0) as u32, - "cost": fee, - }); - (text, out) - } else { - (format!("🚌 公交路线规划完成,共 {count} 个方案"), serde_json::json!({"route_type": "transit", "scheme_count": count})) - } - } - _ => ("路线规划完成".into(), serde_json::json!({})), - } -} - // ═══════════════════════════════════════════════ // 5. amap_travel_plan — 旅游规划 // ═══════════════════════════════════════════════ @@ -906,7 +681,12 @@ pub async fn travel_plan_execute(params: serde_json::Value) -> SkillResult { Err(e) => return SkillResult::error(e), }; - tracing::info!("🗺️ 旅游规划: city={}, interests={:?}, route={}", city, interests, route_type); + tracing::info!( + "🗺️ 旅游规划: city={}, interests={:?}, route={}", + city, + interests, + route_type + ); let mut all_pois: Vec = Vec::new(); let mut map_tasks: Vec = Vec::new(); @@ -921,10 +701,8 @@ pub async fn travel_plan_execute(params: serde_json::Value) -> SkillResult { let name = poi["name"].as_str().unwrap_or("未知"); let address = poi["address"].as_str().unwrap_or(""); let loc_str = poi["location"].as_str().unwrap_or("0,0"); - let parts: Vec = loc_str - .split(',') - .filter_map(|s| s.parse().ok()) - .collect(); + let parts: Vec = + loc_str.split(',').filter_map(|s| s.parse().ok()).collect(); let lng = parts.first().copied().unwrap_or(0.0); let lat = parts.get(1).copied().unwrap_or(0.0); @@ -960,10 +738,7 @@ pub async fn travel_plan_execute(params: serde_json::Value) -> SkillResult { .split(',') .filter_map(|s| s.parse().ok()) .collect(); - let end_parts: Vec = end_loc - .split(',') - .filter_map(|s| s.parse().ok()) - .collect(); + let end_parts: Vec = end_loc.split(',').filter_map(|s| s.parse().ok()).collect(); let start_name = start["name"].as_str().unwrap_or("?"); let end_name = end["name"].as_str().unwrap_or("?"); @@ -1071,17 +846,28 @@ fn urlencoding(s: &str) -> String { for c in s.chars() { match c { ' ' => out.push_str("%20"), - ':' => out.push_str("%3A"), '/' => out.push_str("%2F"), - '?' => out.push_str("%3F"), '#' => out.push_str("%23"), - '[' => out.push_str("%5B"), ']' => out.push_str("%5D"), - '@' => out.push_str("%40"), '!' => out.push_str("%21"), - '$' => out.push_str("%24"), '&' => out.push_str("%26"), - '\'' => out.push_str("%27"), '(' => out.push_str("%28"), - ')' => out.push_str("%29"), '*' => out.push_str("%2A"), - '+' => out.push_str("%2B"), ',' => out.push_str("%2C"), - ';' => out.push_str("%3B"), '=' => out.push_str("%3D"), - '%' => out.push_str("%25"), '{' => out.push_str("%7B"), - '}' => out.push_str("%7D"), '"' => out.push_str("%22"), + ':' => out.push_str("%3A"), + '/' => out.push_str("%2F"), + '?' => out.push_str("%3F"), + '#' => out.push_str("%23"), + '[' => out.push_str("%5B"), + ']' => out.push_str("%5D"), + '@' => out.push_str("%40"), + '!' => out.push_str("%21"), + '$' => out.push_str("%24"), + '&' => out.push_str("%26"), + '\'' => out.push_str("%27"), + '(' => out.push_str("%28"), + ')' => out.push_str("%29"), + '*' => out.push_str("%2A"), + '+' => out.push_str("%2B"), + ',' => out.push_str("%2C"), + ';' => out.push_str("%3B"), + '=' => out.push_str("%3D"), + '%' => out.push_str("%25"), + '{' => out.push_str("%7B"), + '}' => out.push_str("%7D"), + '"' => out.push_str("%22"), '\\' => out.push_str("%5C"), other => out.push(other), } @@ -1124,86 +910,6 @@ pub async fn execute(name: &str, params: serde_json::Value) -> Option