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:
+63
-16
@@ -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
@@ -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(¶ms).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 → 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 搜索
|
// 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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user