refactor(amap): 简化输出格式,返回原始API数据

- 移除自定义文本格式化函数(haversine_distance, format_distance, parse_location 等)
- 移除 build_route_output 函数
- execute 函数改为返回简短确认消息 + 原始 API JSON 数据
- CLI 命令同时输出 r.output 和 r.data(JSON)
- 移除 driving/riding API 的 show_fields=cost 参数
- 清理已删除函数的冗余单元测试
This commit is contained in:
2026-06-04 13:27:04 +08:00
parent fec50b7be8
commit 8dc232ebab
2 changed files with 153 additions and 404 deletions
+63 -16
View File
@@ -849,7 +849,6 @@ async fn cmd_send(
} }
} }
use tools::types::SkillResult; use tools::types::SkillResult;
async fn builtin_approve(ctx: &ExecutionContext, name: &str) -> Result<bool, String> { async fn builtin_approve(ctx: &ExecutionContext, name: &str) -> Result<bool, String> {
@@ -875,7 +874,6 @@ async fn builtin_approve(ctx: &ExecutionContext, name: &str) -> Result<bool, Str
} }
} }
// ─── 工具 CLI 命令 ─── // ─── 工具 CLI 命令 ───
async fn cmd_tool(cmd: ToolCommand) { async fn cmd_tool(cmd: ToolCommand) {
@@ -952,22 +950,48 @@ async fn cmd_amap(action: AmapAction) {
"page": page, "page": page,
"offset": offset, "offset": offset,
}); });
if let Some(c) = city { params["city"] = serde_json::json!(c); } if let Some(c) = city {
if let Some(l) = location { params["location"] = serde_json::json!(l); } params["city"] = serde_json::json!(c);
if let Some(r) = radius { params["radius"] = serde_json::json!(r); } }
if let Some(l) = location {
params["location"] = serde_json::json!(l);
}
if let Some(r) = radius {
params["radius"] = serde_json::json!(r);
}
let result = tools::builtins::amap::execute("amap_poi_search", params).await; let result = tools::builtins::amap::execute("amap_poi_search", 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::Geocode { address, city } => { AmapAction::Geocode { address, city } => {
let mut params = serde_json::json!({"address": address}); 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; 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 } => { AmapAction::ReverseGeocode { location } => {
let params = serde_json::json!({"location": location}); let params = serde_json::json!({"location": location});
let result = tools::builtins::amap::execute("amap_reverse_geocode", params).await; 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 { AmapAction::RoutePlan {
r#type, r#type,
@@ -982,11 +1006,23 @@ async fn cmd_amap(action: AmapAction) {
"origin": origin, "origin": origin,
"destination": destination, "destination": destination,
}); });
if let Some(c) = city { params["city"] = serde_json::json!(c); } if let Some(c) = city {
if let Some(w) = waypoints { params["waypoints"] = serde_json::json!(w); } params["city"] = serde_json::json!(c);
if let Some(s) = strategy { params["strategy"] = serde_json::json!(s); } }
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; 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 { AmapAction::TravelPlan {
city, city,
@@ -1003,15 +1039,26 @@ async fn cmd_amap(action: AmapAction) {
"route_type": route_type, "route_type": route_type,
}); });
let result = tools::builtins::amap::execute("amap_travel_plan", params).await; 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 } => { AmapAction::MapLink { data } => {
let map_data: serde_json::Value = let map_data: serde_json::Value =
serde_json::from_str(&data).unwrap_or(serde_json::json!([])); serde_json::from_str(&data).unwrap_or(serde_json::json!([]));
let params = serde_json::json!({"map_data": map_data}); let params = serde_json::json!({"map_data": map_data});
let result = tools::builtins::amap::execute("amap_map_link", params).await; 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()
);
}
} }
} }
} }
+90 -388
View File
@@ -16,6 +16,7 @@
use reqwest::Client; use reqwest::Client;
use std::time::Duration; use std::time::Duration;
use tracing::info;
use crate::tools::types::{RiskLevel, SkillResult, SkillSpec}; use crate::tools::types::{RiskLevel, SkillResult, SkillSpec};
@@ -130,7 +131,11 @@ impl AmapClient {
} }
/// 地理编码:地址 → 坐标 (v3) /// 地理编码:地址 → 坐标 (v3)
async fn geocode(&self, address: &str, city: Option<&str>) -> Result<serde_json::Value, String> { async fn geocode(
&self,
address: &str,
city: Option<&str>,
) -> Result<serde_json::Value, String> {
let mut params = vec![ let mut params = vec![
("key", self.key.as_str()), ("key", self.key.as_str()),
("address", address), ("address", address),
@@ -236,13 +241,18 @@ impl AmapClient {
("origin", origin), ("origin", origin),
("destination", destination), ("destination", destination),
("strategy", &ss), ("strategy", &ss),
("show_fields", "cost"), // ("show_fields", "cost"),
("appname", "amap-lbs-skill"), ("appname", "amap-lbs-skill"),
]; ];
if let Some(wp) = waypoints { if let Some(wp) = waypoints {
params.push(("waypoints", wp)); params.push(("waypoints", wp));
} }
info!(
"parms: {:?}",
serde_json::to_string(&params).unwrap_or_default()
);
let resp: serde_json::Value = self let resp: serde_json::Value = self
.http .http
.get("https://restapi.amap.com/v5/direction/driving") .get("https://restapi.amap.com/v5/direction/driving")
@@ -255,6 +265,10 @@ impl AmapClient {
.map_err(|e| format!("解析驾车路线响应失败: {}", e))?; .map_err(|e| format!("解析驾车路线响应失败: {}", e))?;
if resp["status"].as_str() != Some("1") { if resp["status"].as_str() != Some("1") {
info!(
"parms: {:?}",
serde_json::to_string(&resp).unwrap_or_default()
);
return Err(format!( return Err(format!(
"驾车路线失败: {}", "驾车路线失败: {}",
resp["info"].as_str().unwrap_or("未知错误") resp["info"].as_str().unwrap_or("未知错误")
@@ -276,7 +290,7 @@ impl AmapClient {
("key", self.key.as_str()), ("key", self.key.as_str()),
("origin", origin), ("origin", origin),
("destination", destination), ("destination", destination),
("show_fields", "cost"), // ("show_fields", "cost"),
("appname", "amap-lbs-skill"), ("appname", "amap-lbs-skill"),
]) ])
.send() .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 → m0 → 空
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<f64> = 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 下的 distancev5 为字符串)
fn path_distance(path: &serde_json::Value) -> f64 {
val_f64(&path["distance"])
}
/// 获取 path.cost.durationv5 需 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 搜索 // 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), Err(e) => return SkillResult::error(e),
}; };
// 解析参考坐标(用于计算每个 POI 的距离) let result = if let Some(loc) = location {
let ref_loc: Option<(f64, f64)> = location.and_then(|s| parse_location(s)); tracing::info!(
"📍 周边搜索: {} @ {} (radius={}m, sort=distance)",
let result = if let Some((rlng, rlat)) = ref_loc { keywords,
tracing::info!("📍 周边搜索: {} @ {},{} (radius={}m, sort=distance)", keywords, rlng, rlat, radius); loc,
client.poi_around(keywords, &format!("{rlng},{rlat}"), radius, page_num, page_size, "distance").await radius
);
client
.poi_around(keywords, loc, radius, page_num, page_size, "distance")
.await
} else { } else {
tracing::info!("🔍 POI 搜索: {} city={:?}", keywords, city); 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 { match result {
Ok(data) => { Ok(data) => SkillResult::ok_with_data("✅ POI 搜索完成".to_string(), 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::<f64>().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)
}
Err(e) => SkillResult::error(e), Err(e) => SkillResult::error(e),
} }
} }
@@ -588,31 +491,7 @@ pub async fn geocode_execute(params: serde_json::Value) -> SkillResult {
tracing::info!("📍 地理编码: {} city={:?}", address, city); tracing::info!("📍 地理编码: {} city={:?}", address, city);
match client.geocode(address, city).await { match client.geocode(address, city).await {
Ok(data) => { Ok(data) => SkillResult::ok_with_data("✅ 地理编码完成".to_string(), 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}"))
}
}
Err(e) => SkillResult::error(e), Err(e) => SkillResult::error(e),
} }
} }
@@ -653,14 +532,7 @@ pub async fn reverse_geocode_execute(params: serde_json::Value) -> SkillResult {
tracing::info!("📍 逆地理编码: {}", location); tracing::info!("📍 逆地理编码: {}", location);
match client.regeocode(location).await { match client.regeocode(location).await {
Ok(data) => { Ok(data) => SkillResult::ok_with_data("✅ 逆地理编码完成".to_string(), 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)
}
Err(e) => SkillResult::error(e), 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(""); let destination = params["destination"].as_str().unwrap_or("");
if route_type.is_empty() || origin.is_empty() || destination.is_empty() { if route_type.is_empty() || origin.is_empty() || destination.is_empty() {
return SkillResult::error( return SkillResult::error("参数错误:缺少 type/origin/destination");
"参数错误:缺少 type/origin/destination",
);
} }
let client = match AmapClient::new() { 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), Err(e) => return SkillResult::error(e),
}; };
tracing::info!("🛣️ 路径规划: type={}, {} → {}", route_type, origin, destination); tracing::info!(
"🛣️ 路径规划: type={}, {} → {}",
route_type,
origin,
destination
);
match route_type { match route_type {
"walking" => match client.walking_route(origin, destination).await { "walking" => match client.walking_route(origin, destination).await {
Ok(data) => { Ok(data) => SkillResult::ok_with_data("✅ 步行路线规划完成".to_string(), data),
let (summary, out) = build_route_output(&data, "walking");
SkillResult::ok_with_data(summary, out)
}
Err(e) => SkillResult::error(e), Err(e) => SkillResult::error(e),
}, },
"driving" => { "driving" => {
let waypoints = params["waypoints"].as_str(); let waypoints = params["waypoints"].as_str();
let strategy = params["strategy"].as_u64().unwrap_or(32) as u32; let strategy = params["strategy"].as_u64().unwrap_or(32) as u32;
match client.driving_route(origin, destination, waypoints, strategy).await { match client
Ok(data) => { .driving_route(origin, destination, waypoints, strategy)
let (summary, out) = build_route_output(&data, "driving"); .await
SkillResult::ok_with_data(summary, out) {
} Ok(data) => SkillResult::ok_with_data("✅ 驾车路线规划完成".to_string(), data),
Err(e) => SkillResult::error(e), Err(e) => SkillResult::error(e),
} }
} }
"riding" => match client.riding_route(origin, destination).await { "riding" => match client.riding_route(origin, destination).await {
Ok(data) => { Ok(data) => SkillResult::ok_with_data("✅ 骑行路线规划完成".to_string(), data),
let (summary, out) = build_route_output(&data, "riding");
SkillResult::ok_with_data(summary, out)
}
Err(e) => SkillResult::error(e), Err(e) => SkillResult::error(e),
}, },
"electrobike" => match client.electrobike_route(origin, destination).await { "electrobike" => match client.electrobike_route(origin, destination).await {
Ok(data) => { Ok(data) => SkillResult::ok_with_data("✅ 电动车路线规划完成".to_string(), data),
let (summary, out) = build_route_output(&data, "electrobike");
SkillResult::ok_with_data(summary, out)
}
Err(e) => SkillResult::error(e), Err(e) => SkillResult::error(e),
}, },
"transit" => { "transit" => {
let city_code = params["city"].as_str().unwrap_or(""); let city_code = params["city"].as_str().unwrap_or("");
if city_code.is_empty() { 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; let strategy = params["strategy"].as_u64().unwrap_or(0) as u32;
match client.transit_route(origin, destination, city_code, city_code, strategy).await { match client
Ok(data) => { .transit_route(origin, destination, city_code, city_code, strategy)
let (summary, out) = build_route_output(&data, "transit"); .await
SkillResult::ok_with_data(summary, out) {
} Ok(data) => SkillResult::ok_with_data("✅ 公交路线规划完成".to_string(), data),
Err(e) => SkillResult::error(e), 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 — 旅游规划 // 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), 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<serde_json::Value> = Vec::new(); let mut all_pois: Vec<serde_json::Value> = Vec::new();
let mut map_tasks: Vec<serde_json::Value> = Vec::new(); let mut map_tasks: Vec<serde_json::Value> = 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 name = poi["name"].as_str().unwrap_or("未知");
let address = poi["address"].as_str().unwrap_or(""); let address = poi["address"].as_str().unwrap_or("");
let loc_str = poi["location"].as_str().unwrap_or("0,0"); let loc_str = poi["location"].as_str().unwrap_or("0,0");
let parts: Vec<f64> = loc_str let parts: Vec<f64> =
.split(',') loc_str.split(',').filter_map(|s| s.parse().ok()).collect();
.filter_map(|s| s.parse().ok())
.collect();
let lng = parts.first().copied().unwrap_or(0.0); let lng = parts.first().copied().unwrap_or(0.0);
let lat = parts.get(1).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(',') .split(',')
.filter_map(|s| s.parse().ok()) .filter_map(|s| s.parse().ok())
.collect(); .collect();
let end_parts: Vec<f64> = end_loc let end_parts: Vec<f64> = end_loc.split(',').filter_map(|s| s.parse().ok()).collect();
.split(',')
.filter_map(|s| s.parse().ok())
.collect();
let start_name = start["name"].as_str().unwrap_or("?"); let start_name = start["name"].as_str().unwrap_or("?");
let end_name = end["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() { for c in s.chars() {
match c { match c {
' ' => out.push_str("%20"), ' ' => out.push_str("%20"),
':' => out.push_str("%3A"), '/' => out.push_str("%2F"), ':' => out.push_str("%3A"),
'?' => out.push_str("%3F"), '#' => out.push_str("%23"), '/' => out.push_str("%2F"),
'[' => out.push_str("%5B"), ']' => out.push_str("%5D"), '?' => out.push_str("%3F"),
'@' => out.push_str("%40"), '!' => out.push_str("%21"), '#' => out.push_str("%23"),
'$' => out.push_str("%24"), '&' => out.push_str("%26"), '[' => out.push_str("%5B"),
'\'' => out.push_str("%27"), '(' => out.push_str("%28"), ']' => out.push_str("%5D"),
')' => out.push_str("%29"), '*' => out.push_str("%2A"), '@' => out.push_str("%40"),
'+' => out.push_str("%2B"), ',' => out.push_str("%2C"), '!' => out.push_str("%21"),
';' => out.push_str("%3B"), '=' => out.push_str("%3D"), '$' => out.push_str("%24"),
'%' => out.push_str("%25"), '{' => out.push_str("%7B"), '&' => out.push_str("%26"),
'}' => out.push_str("%7D"), '"' => out.push_str("%22"), '\'' => 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"), '\\' => out.push_str("%5C"),
other => out.push(other), other => out.push(other),
} }
@@ -1124,86 +910,6 @@ pub async fn execute(name: &str, params: serde_json::Value) -> Option<SkillResul
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn test_val_f64_string() {
let v = serde_json::json!("1234.5");
assert!((val_f64(&v) - 1234.5).abs() < 0.01);
}
#[test]
fn test_val_f64_number() {
let v = serde_json::json!(1234.5);
assert!((val_f64(&v) - 1234.5).abs() < 0.01);
}
#[test]
fn test_val_f64_null() {
let v = serde_json::json!(null);
assert!((val_f64(&v) - 0.0).abs() < 0.01);
}
#[test]
fn test_build_route_output_walking() {
let data = serde_json::json!({
"route": {
"origin": "116.397,39.909",
"destination": "116.427,39.904",
"paths": [{
"distance": "1500",
"cost": { "duration": "1200" }
}]
}
});
let (text, out) = build_route_output(&data, "walking");
assert!(text.contains("1.50"));
assert!(text.contains("20"));
assert_eq!(out["distance_km"], "1.50");
assert_eq!(out["duration_min"], 20);
}
#[test]
fn test_build_route_output_driving() {
let data = serde_json::json!({
"route": {
"origin": "116.397,39.909",
"destination": "116.427,39.904",
"paths": [{
"distance": "5000",
"cost": { "duration": "600", "tolls": "15" },
"traffic_lights": "8",
"restriction": "0"
}]
}
});
let (text, out) = build_route_output(&data, "driving");
assert!(text.contains("5.00"));
assert!(text.contains("10"));
assert!(text.contains("15"));
assert!(text.contains("8"));
assert_eq!(out["tolls"], 15.0);
assert_eq!(out["traffic_lights"], 8);
assert_eq!(out["restriction"], "无/已规避");
}
#[test]
fn test_build_route_output_transit() {
let data = serde_json::json!({
"route": {
"origin": "116.397,39.909",
"destination": "116.427,39.904",
"transits": [{
"distance": "3200",
"cost": { "duration": "1800", "transit_fee": "3" }
}]
}
});
let (text, out) = build_route_output(&data, "transit");
assert!(text.contains("3.20"));
assert!(text.contains("30"));
assert_eq!(out["scheme_count"], 1);
assert_eq!(out["cost"], 3.0);
}
#[test] #[test]
fn test_url_encoding() { fn test_url_encoding() {
let encoded = urlencoding("https://example.com/path?q=hello world"); let encoded = urlencoding("https://example.com/path?q=hello world");
@@ -1224,9 +930,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_poi_search_integration() { async fn test_poi_search_integration() {
if std::env::var("AMAP_WEBSERVICE_KEY").is_err() if std::env::var("AMAP_WEBSERVICE_KEY").is_err() && std::env::var("AMAP_KEY").is_err() {
&& std::env::var("AMAP_KEY").is_err()
{
eprintln!("跳过集成测试: 未设置 AMAP_WEBSERVICE_KEY"); eprintln!("跳过集成测试: 未设置 AMAP_WEBSERVICE_KEY");
return; return;
} }
@@ -1237,9 +941,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_geocode_integration() { async fn test_geocode_integration() {
if std::env::var("AMAP_WEBSERVICE_KEY").is_err() if std::env::var("AMAP_WEBSERVICE_KEY").is_err() && std::env::var("AMAP_KEY").is_err() {
&& std::env::var("AMAP_KEY").is_err()
{
eprintln!("跳过集成测试: 未设置 AMAP_WEBSERVICE_KEY"); eprintln!("跳过集成测试: 未设置 AMAP_WEBSERVICE_KEY");
return; return;
} }