Files
MAF1/MAF1.Core/Tools/WeatherTools.cs
T
admin777 b631acd42e feat: 将插件 stdin 协议定为 v1,并按声明注入凭据
进程外插件改为只读环境变量和自身配置,避免读宿主 appsettings;宿主按 plugin.json 声明注入 LLM / OpenWeather 等凭据。
2026-09-11 10:46:52 +08:00

107 lines
4.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.ComponentModel;
using System.Net.Http.Headers;
using System.Text.Json;
namespace MAF1.Tools;
/// <summary>
/// 天气 HTTP 实现。Agent 通过 GetWeather 间接调用这里,而不是自己拼 URL。
/// Provider=Wttr 免费无需 KeyOpenWeather 需要 ApiKey。
/// </summary>
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 已启用,但 API Key 为空。请配置 OPENWEATHER_API_KEY(插件凭据或环境变量),或把 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}%";
}
/// <summary>把 appsettings 里的 URL 模板换成真实地址。城市名要做 Uri.EscapeDataString。</summary>
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);
}
/// <summary>带超时和 User-Agent。部分天气站点会拒绝没有 UA 的请求。</summary>
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;
}
}