366 lines
13 KiB
C#
366 lines
13 KiB
C#
|
|
using ModelContextProtocol;
|
|||
|
|
using ModelContextProtocol.Server;
|
|||
|
|
using System.ComponentModel;
|
|||
|
|
using System.Text.Json;
|
|||
|
|
using System.Text.Json.Serialization;
|
|||
|
|
|
|||
|
|
namespace AspNetCoreMcpServer.Tools;
|
|||
|
|
|
|||
|
|
[McpServerToolType]
|
|||
|
|
public sealed class WeatherTools
|
|||
|
|
{
|
|||
|
|
private readonly IHttpClientFactory _httpClientFactory;
|
|||
|
|
private const string WeatherApiUrl = "http://10.1.57.124:8002/ledclock/weather.txt";
|
|||
|
|
|
|||
|
|
public WeatherTools(IHttpClientFactory httpClientFactory)
|
|||
|
|
{
|
|||
|
|
_httpClientFactory = httpClientFactory;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[McpServerTool, Description("获取今天的天气信息")]
|
|||
|
|
[McpMeta("category", "weather")]
|
|||
|
|
[McpMeta("dataSource", "local")]
|
|||
|
|
public async Task<string> GetTodayWeather()
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var weatherData = await FetchWeatherDataAsync();
|
|||
|
|
|
|||
|
|
if (weatherData?.Data == null || weatherData.Data.Count == 0)
|
|||
|
|
{
|
|||
|
|
return "无法获取天气数据";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取今天的日期
|
|||
|
|
var today = DateTime.Now.ToString("yyyy-MM-dd");
|
|||
|
|
|
|||
|
|
// 查找今天的天气数据
|
|||
|
|
var todayWeather = weatherData.Data.FirstOrDefault(d => d.Date == today);
|
|||
|
|
|
|||
|
|
if (todayWeather == null)
|
|||
|
|
{
|
|||
|
|
// 如果没有今天的数据,返回第一天的数据
|
|||
|
|
todayWeather = weatherData.Data.First();
|
|||
|
|
return $"""
|
|||
|
|
今天天气信息 (数据日期: {todayWeather.Date})
|
|||
|
|
地区: {weatherData.Province}/{weatherData.City}
|
|||
|
|
天气: {todayWeather.WeatherDescription}
|
|||
|
|
温度: {todayWeather.MinTemperature}°C ~ {todayWeather.MaxTemperature}°C
|
|||
|
|
风向风力: {todayWeather.WindDescription}
|
|||
|
|
|
|||
|
|
注意: 未找到今日准确数据,显示的是最近日期的天气信息。
|
|||
|
|
""";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $"""
|
|||
|
|
今天天气信息
|
|||
|
|
地区: {weatherData.Province}/{weatherData.City}
|
|||
|
|
日期: {todayWeather.Date}
|
|||
|
|
天气: {todayWeather.WeatherDescription}
|
|||
|
|
温度: {todayWeather.MinTemperature}°C ~ {todayWeather.MaxTemperature}°C
|
|||
|
|
风向风力: {todayWeather.WindDescription}
|
|||
|
|
|
|||
|
|
{GetWeatherAdvice(todayWeather)}
|
|||
|
|
""";
|
|||
|
|
}
|
|||
|
|
catch (HttpRequestException ex)
|
|||
|
|
{
|
|||
|
|
return $"网络请求失败: {ex.Message}";
|
|||
|
|
}
|
|||
|
|
catch (JsonException ex)
|
|||
|
|
{
|
|||
|
|
return $"数据解析失败: {ex.Message}";
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
return $"获取天气信息时发生错误: {ex.Message}";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[McpServerTool, Description("获取未来几天的天气预报")]
|
|||
|
|
[McpMeta("category", "weather")]
|
|||
|
|
[McpMeta("dataSource", "local")]
|
|||
|
|
public async Task<string> GetWeatherForecast(
|
|||
|
|
[Description("预报天数,默认为3天,最多15天")] int days = 3)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if (days < 1 || days > 15)
|
|||
|
|
{
|
|||
|
|
return "预报天数必须在1-15天之间";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var weatherData = await FetchWeatherDataAsync();
|
|||
|
|
|
|||
|
|
if (weatherData?.Data == null || weatherData.Data.Count == 0)
|
|||
|
|
{
|
|||
|
|
return "无法获取天气数据";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var forecasts = weatherData.Data.Take(days).Select((weather, index) =>
|
|||
|
|
{
|
|||
|
|
var dateLabel = index == 0 ? "今天" : index == 1 ? "明天" : index == 2 ? "后天" : weather.Date;
|
|||
|
|
return $"""
|
|||
|
|
{dateLabel} ({weather.Date})
|
|||
|
|
天气: {weather.WeatherDescription}
|
|||
|
|
温度: {weather.MinTemperature}°C ~ {weather.MaxTemperature}°C
|
|||
|
|
风向风力: {weather.WindDescription}
|
|||
|
|
""";
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return $"""
|
|||
|
|
{weatherData.Province}/{weatherData.City} 未来{days}天天气预报
|
|||
|
|
|
|||
|
|
{string.Join("\n---\n", forecasts)}
|
|||
|
|
""";
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
return $"获取天气预报时发生错误: {ex.Message}";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[McpServerTool, Description("获取指定日期的天气信息")]
|
|||
|
|
[McpMeta("category", "weather")]
|
|||
|
|
[McpMeta("dataSource", "local")]
|
|||
|
|
public async Task<string> GetWeatherByDate(
|
|||
|
|
[Description("日期,格式: yyyy-MM-dd,例如: 2025-11-15")] string date)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 验证日期格式
|
|||
|
|
if (!DateTime.TryParseExact(date, "yyyy-MM-dd", null,
|
|||
|
|
System.Globalization.DateTimeStyles.None, out var parsedDate))
|
|||
|
|
{
|
|||
|
|
return "日期格式错误,请使用 yyyy-MM-dd 格式,例如: 2025-11-15";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var weatherData = await FetchWeatherDataAsync();
|
|||
|
|
|
|||
|
|
if (weatherData?.Data == null || weatherData.Data.Count == 0)
|
|||
|
|
{
|
|||
|
|
return "无法获取天气数据";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var targetWeather = weatherData.Data.FirstOrDefault(d => d.Date == date);
|
|||
|
|
|
|||
|
|
if (targetWeather == null)
|
|||
|
|
{
|
|||
|
|
var availableDates = string.Join(", ", weatherData.Data.Select(d => d.Date).Take(5));
|
|||
|
|
return $"未找到日期 {date} 的天气数据\n\n可用的日期: {availableDates}...";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var daysFromNow = (parsedDate - DateTime.Now).Days;
|
|||
|
|
var dateLabel = daysFromNow switch
|
|||
|
|
{
|
|||
|
|
0 => "今天",
|
|||
|
|
1 => "明天",
|
|||
|
|
2 => "后天",
|
|||
|
|
_ => daysFromNow > 0 ? $"{daysFromNow}天后" : $"{-daysFromNow}天前"
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return $"""
|
|||
|
|
{dateLabel}的天气信息
|
|||
|
|
地区: {weatherData.Province}/{weatherData.City}
|
|||
|
|
日期: {targetWeather.Date}
|
|||
|
|
天气: {targetWeather.WeatherDescription}
|
|||
|
|
温度: {targetWeather.MinTemperature}°C ~ {targetWeather.MaxTemperature}°C
|
|||
|
|
风向风力: {targetWeather.WindDescription}
|
|||
|
|
|
|||
|
|
{GetWeatherAdvice(targetWeather)}
|
|||
|
|
""";
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
return $"获取天气信息时发生错误: {ex.Message}";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[McpServerTool, Description("获取完整的天气数据概览")]
|
|||
|
|
[McpMeta("category", "weather")]
|
|||
|
|
[McpMeta("dataSource", "local")]
|
|||
|
|
public async Task<string> GetWeatherOverview()
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var weatherData = await FetchWeatherDataAsync();
|
|||
|
|
|
|||
|
|
if (weatherData?.Data == null || weatherData.Data.Count == 0)
|
|||
|
|
{
|
|||
|
|
return "无法获取天气数据";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 统计信息
|
|||
|
|
var totalDays = weatherData.Data.Count;
|
|||
|
|
var avgMaxTemp = weatherData.Data.Average(d => int.Parse(d.MaxTemperature));
|
|||
|
|
var avgMinTemp = weatherData.Data.Average(d => int.Parse(d.MinTemperature));
|
|||
|
|
var rainyDays = weatherData.Data.Count(d => d.WeatherDescription.Contains("雨"));
|
|||
|
|
var sunnyDays = weatherData.Data.Count(d => d.WeatherDescription.Contains("晴"));
|
|||
|
|
|
|||
|
|
// 今天的天气
|
|||
|
|
var today = DateTime.Now.ToString("yyyy-MM-dd");
|
|||
|
|
var todayWeather = weatherData.Data.FirstOrDefault(d => d.Date == today)
|
|||
|
|
?? weatherData.Data.First();
|
|||
|
|
|
|||
|
|
return $"""
|
|||
|
|
天气数据概览
|
|||
|
|
地区: {weatherData.Province}/{weatherData.City}
|
|||
|
|
数据范围: {weatherData.Data.First().Date} 至 {weatherData.Data.Last().Date}
|
|||
|
|
|
|||
|
|
今日天气:
|
|||
|
|
{todayWeather.WeatherDescription} | {todayWeather.MinTemperature}°C ~ {todayWeather.MaxTemperature}°C | {todayWeather.WindDescription}
|
|||
|
|
|
|||
|
|
统计信息 (未来{totalDays}天):
|
|||
|
|
- 平均最高温度: {avgMaxTemp:F1}°C
|
|||
|
|
- 平均最低温度: {avgMinTemp:F1}°C
|
|||
|
|
- 晴天: {sunnyDays}天
|
|||
|
|
- 雨天: {rainyDays}天
|
|||
|
|
|
|||
|
|
温馨提示: {GetOverviewAdvice(weatherData.Data)}
|
|||
|
|
""";
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
return $"获取天气概览时发生错误: {ex.Message}";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 私有辅助方法 ====================
|
|||
|
|
|
|||
|
|
private async Task<WeatherResponse?> FetchWeatherDataAsync()
|
|||
|
|
{
|
|||
|
|
var client = _httpClientFactory.CreateClient();
|
|||
|
|
client.Timeout = TimeSpan.FromSeconds(10);
|
|||
|
|
|
|||
|
|
using var response = await client.GetAsync(WeatherApiUrl);
|
|||
|
|
response.EnsureSuccessStatusCode();
|
|||
|
|
|
|||
|
|
var content = await response.Content.ReadAsStringAsync();
|
|||
|
|
|
|||
|
|
var options = new JsonSerializerOptions
|
|||
|
|
{
|
|||
|
|
PropertyNameCaseInsensitive = true
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return JsonSerializer.Deserialize<WeatherResponse>(content, options);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static string GetWeatherAdvice(WeatherData weather)
|
|||
|
|
{
|
|||
|
|
var advice = new List<string>();
|
|||
|
|
|
|||
|
|
// 根据天气状况给建议
|
|||
|
|
if (weather.WeatherDescription.Contains("雨"))
|
|||
|
|
{
|
|||
|
|
advice.Add("☔ 记得带伞");
|
|||
|
|
}
|
|||
|
|
else if (weather.WeatherDescription.Contains("晴"))
|
|||
|
|
{
|
|||
|
|
advice.Add("☀️ 天气晴朗,适合外出");
|
|||
|
|
}
|
|||
|
|
else if (weather.WeatherDescription.Contains("多云"))
|
|||
|
|
{
|
|||
|
|
advice.Add("⛅ 天气多云,可以外出活动");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据温度给建议
|
|||
|
|
var maxTemp = int.Parse(weather.MaxTemperature);
|
|||
|
|
var minTemp = int.Parse(weather.MinTemperature);
|
|||
|
|
var tempDiff = maxTemp - minTemp;
|
|||
|
|
|
|||
|
|
if (maxTemp > 30)
|
|||
|
|
{
|
|||
|
|
advice.Add("🌡️ 高温天气,注意防暑降温");
|
|||
|
|
}
|
|||
|
|
else if (minTemp < 10)
|
|||
|
|
{
|
|||
|
|
advice.Add("🧥 气温较低,注意保暖");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (tempDiff > 10)
|
|||
|
|
{
|
|||
|
|
advice.Add("👔 昼夜温差大,适当增减衣物");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return advice.Count > 0 ? $"温馨提示: {string.Join(";", advice)}" : "";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static string GetOverviewAdvice(List<WeatherData> weatherList)
|
|||
|
|
{
|
|||
|
|
var adviceList = new List<string>();
|
|||
|
|
|
|||
|
|
// 检查是否有连续雨天
|
|||
|
|
var consecutiveRainyDays = 0;
|
|||
|
|
var maxConsecutiveRainy = 0;
|
|||
|
|
foreach (var weather in weatherList)
|
|||
|
|
{
|
|||
|
|
if (weather.WeatherDescription.Contains("雨"))
|
|||
|
|
{
|
|||
|
|
consecutiveRainyDays++;
|
|||
|
|
maxConsecutiveRainy = Math.Max(maxConsecutiveRainy, consecutiveRainyDays);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
consecutiveRainyDays = 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (maxConsecutiveRainy >= 3)
|
|||
|
|
{
|
|||
|
|
adviceList.Add($"未来有连续{maxConsecutiveRainy}天降雨,注意出行安全");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查温度变化
|
|||
|
|
var firstTemp = int.Parse(weatherList.First().MaxTemperature);
|
|||
|
|
var lastTemp = int.Parse(weatherList.Last().MaxTemperature);
|
|||
|
|
var tempChange = lastTemp - firstTemp;
|
|||
|
|
|
|||
|
|
if (Math.Abs(tempChange) > 5)
|
|||
|
|
{
|
|||
|
|
adviceList.Add(tempChange > 0 ? "未来气温逐渐上升" : "未来气温逐渐下降");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return adviceList.Count > 0 ? string.Join(";", adviceList) : "天气状况平稳";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 数据模型 ====================
|
|||
|
|
|
|||
|
|
internal class WeatherResponse
|
|||
|
|
{
|
|||
|
|
[JsonPropertyName("code")]
|
|||
|
|
public int Code { get; set; }
|
|||
|
|
|
|||
|
|
[JsonPropertyName("message")]
|
|||
|
|
public string Message { get; set; } = "";
|
|||
|
|
|
|||
|
|
[JsonPropertyName("province")]
|
|||
|
|
public string Province { get; set; } = "";
|
|||
|
|
|
|||
|
|
[JsonPropertyName("city")]
|
|||
|
|
public string City { get; set; } = "";
|
|||
|
|
|
|||
|
|
[JsonPropertyName("data")]
|
|||
|
|
public List<WeatherData> Data { get; set; } = new();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
internal class WeatherData
|
|||
|
|
{
|
|||
|
|
[JsonPropertyName("date")]
|
|||
|
|
public string Date { get; set; } = "";
|
|||
|
|
|
|||
|
|
[JsonPropertyName("weatherDescription")]
|
|||
|
|
public string WeatherDescription { get; set; } = "";
|
|||
|
|
|
|||
|
|
[JsonPropertyName("maxTemperature")]
|
|||
|
|
public string MaxTemperature { get; set; } = "";
|
|||
|
|
|
|||
|
|
[JsonPropertyName("minTemperature")]
|
|||
|
|
public string MinTemperature { get; set; } = "";
|
|||
|
|
|
|||
|
|
[JsonPropertyName("wea_img")]
|
|||
|
|
public string WeaImg { get; set; } = "";
|
|||
|
|
|
|||
|
|
[JsonPropertyName("windDescription")]
|
|||
|
|
public string WindDescription { get; set; } = "";
|
|||
|
|
}
|