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:
+90
-388
@@ -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<serde_json::Value, String> {
|
||||
async fn geocode(
|
||||
&self,
|
||||
address: &str,
|
||||
city: Option<&str>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
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<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 下的 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::<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)
|
||||
}
|
||||
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<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 address = poi["address"].as_str().unwrap_or("");
|
||||
let loc_str = poi["location"].as_str().unwrap_or("0,0");
|
||||
let parts: Vec<f64> = loc_str
|
||||
.split(',')
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
let parts: Vec<f64> =
|
||||
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<f64> = end_loc
|
||||
.split(',')
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
let end_parts: Vec<f64> = 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<SkillResul
|
||||
mod tests {
|
||||
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]
|
||||
fn test_url_encoding() {
|
||||
let encoded = urlencoding("https://example.com/path?q=hello world");
|
||||
@@ -1224,9 +930,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_poi_search_integration() {
|
||||
if std::env::var("AMAP_WEBSERVICE_KEY").is_err()
|
||||
&& std::env::var("AMAP_KEY").is_err()
|
||||
{
|
||||
if std::env::var("AMAP_WEBSERVICE_KEY").is_err() && std::env::var("AMAP_KEY").is_err() {
|
||||
eprintln!("跳过集成测试: 未设置 AMAP_WEBSERVICE_KEY");
|
||||
return;
|
||||
}
|
||||
@@ -1237,9 +941,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_geocode_integration() {
|
||||
if std::env::var("AMAP_WEBSERVICE_KEY").is_err()
|
||||
&& std::env::var("AMAP_KEY").is_err()
|
||||
{
|
||||
if std::env::var("AMAP_WEBSERVICE_KEY").is_err() && std::env::var("AMAP_KEY").is_err() {
|
||||
eprintln!("跳过集成测试: 未设置 AMAP_WEBSERVICE_KEY");
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user