use serde::Deserialize;
const GEOCODING_URL: &str = "https://geocoding-api.open-meteo.com/v1/search";
const FORECAST_URL: &str = "https://api.open-meteo.com/v1/forecast";
#[derive(Debug, Deserialize)]
struct GeocodingResponse {
results: Option<Vec<GeoResult>>,
}
#[derive(Debug, Deserialize)]
struct GeoResult {
name: String,
latitude: f64,
longitude: f64,
timezone: String,
#[serde(default)]
country: String,
}
#[derive(Debug, Deserialize)]
struct ForecastResponse {
current: Option<CurrentWeather>,
}
#[derive(Debug, Deserialize)]
struct CurrentWeather {
temperature_2m: f64,
relative_humidity_2m: Option<f64>,
weather_code: Option<f64>,
wind_speed_10m: Option<f64>,
}
fn weather_code_text(code: f64) -> &'static str {
match code as u32 {
0 => "晴",
1 => "大部晴朗",
2 => "少云",
3 => "多云",
45 => "雾",
48 => "冻雾",
51..=57 => "毛毛雨",
61..=67 => "雨",
71..=77 => "雪",
80..=82 => "阵雨",
85..=86 => "阵雪",
95 => "雷暴",
96..=99 => "雷暴伴冰雹",
_ => "未知",
}
}
pub fn run(args_json: &str, timeout_secs: u64) -> String {
match run_inner(args_json, timeout_secs) {
Ok(s) => s,
Err(e) => e,
}
}
fn parse_weather_city(args_json: &str) -> Result<String, String> {
let args: super::tool_param_types::GetWeatherArgs =
serde_json::from_str(args_json).map_err(|e| format!("参数 JSON 无效: {e}"))?;
let city = args
.city
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.or_else(|| {
args.location
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
});
match city {
Some(s) if s.len() >= 2 => Ok(s.to_string()),
_ => Err("错误:请提供 city 或 location 参数(至少 2 个字符)".to_string()),
}
}
fn build_blocking_http_client(timeout_secs: u64) -> Result<reqwest::blocking::Client, String> {
reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(timeout_secs))
.build()
.map_err(|e| format!("请求客户端创建失败:{}", e))
}
fn geocode_city_blocking(
client: &reqwest::blocking::Client,
city: &str,
) -> Result<GeoResult, String> {
let geo: GeocodingResponse = match client
.get(GEOCODING_URL)
.query(&[("name", city), ("count", "1"), ("language", "zh")])
.send()
{
Ok(res) if res.status().is_success() => match res.json() {
Ok(j) => j,
Err(e) => return Err(format!("解析地理编码结果失败:{}", e)),
},
Ok(res) => return Err(format!("地理编码请求失败:{}", res.status())),
Err(e) => return Err(format!("网络请求失败:{}", e)),
};
geo.results
.and_then(|r| r.into_iter().next())
.ok_or_else(|| format!("未找到与「{}」匹配的地点,请换一个城市或地区名重试。", city))
}
fn forecast_blocking(
client: &reqwest::blocking::Client,
loc: &GeoResult,
) -> Result<ForecastResponse, String> {
let lat = loc.latitude.to_string();
let lon = loc.longitude.to_string();
match client
.get(FORECAST_URL)
.query(&[
("latitude", lat.as_str()),
("longitude", lon.as_str()),
(
"current",
"temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m",
),
("timezone", loc.timezone.as_str()),
])
.send()
{
Ok(res) if res.status().is_success() => match res.json() {
Ok(j) => Ok(j),
Err(e) => Err(format!("解析天气结果失败:{}", e)),
},
Ok(res) => Err(format!("天气请求失败:{}", res.status())),
Err(e) => Err(format!("网络请求失败:{}", e)),
}
}
fn format_weather_line(loc: &GeoResult, cur: &CurrentWeather) -> String {
let desc = cur.weather_code.map(weather_code_text).unwrap_or("—");
let hum = cur
.relative_humidity_2m
.map(|h| format!("湿度 {}%", h as i32))
.unwrap_or_default();
let wind = cur
.wind_speed_10m
.map(|w| format!("风速 {} km/h", w as i32))
.unwrap_or_default();
let extra = [hum, wind]
.into_iter()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(",");
let location_name = if loc.country.is_empty() {
loc.name.clone()
} else {
format!("{}({})", loc.name, loc.country)
};
format!(
"{}:{},气温 {}°C{}{}",
location_name,
desc,
cur.temperature_2m as i32,
if extra.is_empty() { "" } else { "," },
extra
)
}
fn run_inner(args_json: &str, timeout_secs: u64) -> Result<String, String> {
let city = parse_weather_city(args_json)?;
let client = build_blocking_http_client(timeout_secs)?;
let loc = geocode_city_blocking(&client, &city)?;
let forecast = forecast_blocking(&client, &loc)?;
let cur = forecast
.current
.ok_or_else(|| "未获取到当前天气数据".to_string())?;
Ok(format_weather_line(&loc, &cur))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_run_missing_city() {
let out = run("{}", 15);
assert!(
out.contains("city") || out.contains("location"),
"缺少参数应提示,得到: {}",
out
);
}
#[test]
fn test_run_city_too_short() {
let out = run(r#"{"city":"x"}"#, 15);
assert!(
out.contains("至少 2 个字符") || out.contains("city") || out.contains("location"),
"得到: {}",
out
);
}
}