2026-08-26 18:06:02 +08:00
|
|
|
|
using System.ComponentModel;
|
|
|
|
|
|
using System.Net.Http.Headers;
|
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
|
|
|
|
|
|
|
namespace MAF1.Tools;
|
|
|
|
|
|
|
2026-08-28 11:51:48 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 天气 HTTP 实现。Agent 通过 GetWeather 间接调用这里,而不是自己拼 URL。
|
|
|
|
|
|
/// Provider=Wttr 免费无需 Key;OpenWeather 需要 ApiKey。
|
|
|
|
|
|
/// </summary>
|
2026-08-26 18:06:02 +08:00
|
|
|
|
public sealed class WeatherTools(WeatherOptions options, HttpClient http)
|
|
|
|
|
|
{
|
|
|
|
|
|
[Description("Look up live weather for a city or location. Always call this instead of guessing.")]
|
|
|
|
|
|
public async Task<string> GetWeather(
|
|
|
|
|
|
[Description("City or location name, for example Amsterdam or 北京.")] string location)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(location))
|
|
|
|
|
|
{
|
|
|
|
|
|
return "未提供地点,无法查询天气。";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
string provider = options.Provider.Trim();
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (provider.Equals("OpenWeather", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(options.OpenWeather.ApiKey))
|
|
|
|
|
|
{
|
|
|
|
|
|
return "OpenWeather 已启用,但 appsettings.json 里 Weather:OpenWeather:ApiKey 为空。请填入密钥,或把 Provider 改回 Wttr。";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return await QueryOpenWeatherAsync(location);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return await QueryWttrAsync(location);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
return $"查询天气失败({provider} / {location}):{ex.Message}";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<string> QueryWttrAsync(string location)
|
|
|
|
|
|
{
|
|
|
|
|
|
string url = Expand(options.Wttr.UrlTemplate, location, apiKey: null);
|
|
|
|
|
|
using HttpResponseMessage response = await http.GetAsync(url);
|
|
|
|
|
|
string body = (await response.Content.ReadAsStringAsync()).Trim();
|
|
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
|
|
|
|
{
|
|
|
|
|
|
return $"wttr.in 返回 {(int)response.StatusCode}:{body}";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return string.IsNullOrWhiteSpace(body)
|
|
|
|
|
|
? $"wttr.in 没有返回 {location} 的天气。"
|
|
|
|
|
|
: body;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<string> QueryOpenWeatherAsync(string location)
|
|
|
|
|
|
{
|
|
|
|
|
|
string url = Expand(options.OpenWeather.UrlTemplate, location, options.OpenWeather.ApiKey);
|
|
|
|
|
|
using HttpResponseMessage response = await http.GetAsync(url);
|
|
|
|
|
|
string body = await response.Content.ReadAsStringAsync();
|
|
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
|
|
|
|
{
|
|
|
|
|
|
return $"OpenWeather 返回 {(int)response.StatusCode}:{body}";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
using JsonDocument doc = JsonDocument.Parse(body);
|
|
|
|
|
|
JsonElement root = doc.RootElement;
|
|
|
|
|
|
string name = root.TryGetProperty("name", out JsonElement nameEl) ? nameEl.GetString() ?? location : location;
|
|
|
|
|
|
string description = root.TryGetProperty("weather", out JsonElement weather)
|
|
|
|
|
|
&& weather.ValueKind == JsonValueKind.Array
|
|
|
|
|
|
&& weather.GetArrayLength() > 0
|
|
|
|
|
|
&& weather[0].TryGetProperty("description", out JsonElement descEl)
|
|
|
|
|
|
? descEl.GetString() ?? ""
|
|
|
|
|
|
: "";
|
|
|
|
|
|
double temp = root.TryGetProperty("main", out JsonElement main) && main.TryGetProperty("temp", out JsonElement tempEl)
|
|
|
|
|
|
? tempEl.GetDouble()
|
|
|
|
|
|
: double.NaN;
|
|
|
|
|
|
int humidity = main.ValueKind == JsonValueKind.Object && main.TryGetProperty("humidity", out JsonElement humidityEl)
|
|
|
|
|
|
? humidityEl.GetInt32()
|
|
|
|
|
|
: 0;
|
|
|
|
|
|
|
|
|
|
|
|
return double.IsNaN(temp)
|
|
|
|
|
|
? body
|
|
|
|
|
|
: $"{name}:{description},气温 {temp:0.#}°C,湿度 {humidity}%";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-28 11:51:48 +08:00
|
|
|
|
/// <summary>把 appsettings 里的 URL 模板换成真实地址。城市名要做 Uri.EscapeDataString。</summary>
|
2026-08-26 18:06:02 +08:00
|
|
|
|
private string Expand(string template, string location, string? apiKey)
|
|
|
|
|
|
{
|
|
|
|
|
|
return template
|
|
|
|
|
|
.Replace("{location}", Uri.EscapeDataString(location), StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
|
.Replace("{lang}", Uri.EscapeDataString(options.Language), StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
|
.Replace("{apiKey}", apiKey ?? "", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-28 11:51:48 +08:00
|
|
|
|
/// <summary>带超时和 User-Agent。部分天气站点会拒绝没有 UA 的请求。</summary>
|
2026-08-26 18:06:02 +08:00
|
|
|
|
public static HttpClient CreateHttpClient()
|
|
|
|
|
|
{
|
|
|
|
|
|
HttpClient http = new() { Timeout = TimeSpan.FromSeconds(20) };
|
|
|
|
|
|
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MAF1", "1.0"));
|
|
|
|
|
|
http.DefaultRequestHeaders.AcceptLanguage.ParseAdd("zh-CN,zh;q=0.9,en;q=0.8");
|
|
|
|
|
|
return http;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|