初始提交
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
|
||||
namespace MAF1.Tools;
|
||||
|
||||
public static class FileTools
|
||||
{
|
||||
[Description("Read the full UTF-8 text of a local file. Always call this before judging city names.")]
|
||||
public static string ReadTextFile([Description("Absolute or relative path of the file to read.")] string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return "未提供文件路径。";
|
||||
}
|
||||
|
||||
string? resolved = Resolve(path.Trim().Trim('"'));
|
||||
if (resolved is null)
|
||||
{
|
||||
return $"找不到文件:{path}";
|
||||
}
|
||||
|
||||
FileInfo info = new(resolved);
|
||||
if (info.Length > 64 * 1024)
|
||||
{
|
||||
return $"文件过大({info.Length} 字节),请换一个不超过 64KB 的文本文件。";
|
||||
}
|
||||
|
||||
return File.ReadAllText(resolved, Encoding.UTF8);
|
||||
}
|
||||
|
||||
private static string? Resolve(string path)
|
||||
{
|
||||
if (Path.IsPathRooted(path) && File.Exists(path))
|
||||
{
|
||||
return Path.GetFullPath(path);
|
||||
}
|
||||
|
||||
string fromCwd = Path.GetFullPath(path);
|
||||
if (File.Exists(fromCwd))
|
||||
{
|
||||
return fromCwd;
|
||||
}
|
||||
|
||||
foreach (string root in SearchRoots())
|
||||
{
|
||||
string candidate = Path.GetFullPath(Path.Combine(root, path));
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> SearchRoots()
|
||||
{
|
||||
yield return AppContext.BaseDirectory;
|
||||
string? contentRoot = Environment.GetEnvironmentVariable("MAF1_CONTENT_ROOT");
|
||||
if (!string.IsNullOrWhiteSpace(contentRoot))
|
||||
{
|
||||
yield return contentRoot;
|
||||
}
|
||||
|
||||
DirectoryInfo? parent = Directory.GetParent(AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar));
|
||||
if (parent is not null)
|
||||
{
|
||||
yield return parent.FullName;
|
||||
if (parent.Parent is not null)
|
||||
{
|
||||
yield return parent.Parent.FullName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace MAF1.Tools;
|
||||
|
||||
public sealed class WeatherOptions
|
||||
{
|
||||
public string Provider { get; set; } = "Wttr";
|
||||
public string Language { get; set; } = "zh";
|
||||
public WttrOptions Wttr { get; set; } = new();
|
||||
public OpenWeatherOptions OpenWeather { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class WttrOptions
|
||||
{
|
||||
public string UrlTemplate { get; set; } = "https://wttr.in/{location}?lang={lang}&format=3";
|
||||
}
|
||||
|
||||
public sealed class OpenWeatherOptions
|
||||
{
|
||||
public string UrlTemplate { get; set; } =
|
||||
"https://api.openweathermap.org/data/2.5/weather?q={location}&appid={apiKey}&units=metric&lang={lang}";
|
||||
|
||||
public string ApiKey { get; set; } = "";
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using System.ComponentModel;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MAF1.Tools;
|
||||
|
||||
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}%";
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user