diff --git a/Katheryne.Tests/Modules/WeatherModuleTests.cs b/Katheryne.Tests/Modules/WeatherModuleTests.cs
new file mode 100644
index 0000000..757e30e
--- /dev/null
+++ b/Katheryne.Tests/Modules/WeatherModuleTests.cs
@@ -0,0 +1,26 @@
+using Katheryne.Modules;
+using Xunit.Abstractions;
+
+namespace Katheryne.Tests.Modules;
+
+public class WeatherModuleTests
+{
+ private readonly ITestOutputHelper _output;
+
+ public WeatherModuleTests(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ [Fact]
+ public void WeatherModuleTest()
+ {
+ var weather = new WeatherModule();
+
+ Assert.True(weather.ContainsParam("text"));
+ Assert.True(weather.ContainsParam("temp"));
+
+ _output.WriteLine(weather["text"]);
+ _output.WriteLine(weather["temp"]);
+ }
+}
\ No newline at end of file
diff --git a/Katheryne/Exceptions/ModuleException.cs b/Katheryne/Exceptions/ModuleException.cs
new file mode 100644
index 0000000..41dbf10
--- /dev/null
+++ b/Katheryne/Exceptions/ModuleException.cs
@@ -0,0 +1,22 @@
+namespace Katheryne.Exceptions;
+
+///
+/// 调用模块中发生的异常
+///
+public class ModuleException : Exception
+{
+ public ModuleException() : base()
+ {
+
+ }
+
+ public ModuleException(string message) : base(message)
+ {
+
+ }
+
+ public ModuleException(string message, Exception innerException) : base(message, innerException)
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/Katheryne/Modules/ModuleBase.cs b/Katheryne/Modules/ModuleBase.cs
new file mode 100644
index 0000000..61553c3
--- /dev/null
+++ b/Katheryne/Modules/ModuleBase.cs
@@ -0,0 +1,30 @@
+using System.Text.Json;
+using Katheryne.Abstractions;
+
+namespace Katheryne.Modules;
+
+public abstract class ModuleBase : IParamsModule
+{
+ protected readonly Dictionary> Functions = new();
+
+ protected readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true
+ };
+
+ public abstract string ModuleName { get; }
+
+ public string this[string param]
+ {
+ get
+ {
+ Func func = Functions[param];
+ return func();
+ }
+ }
+
+ public bool ContainsParam(string param)
+ {
+ return Functions.ContainsKey(param);
+ }
+}
\ No newline at end of file
diff --git a/Katheryne/Modules/WeatherModule.cs b/Katheryne/Modules/WeatherModule.cs
new file mode 100644
index 0000000..4fb79dc
--- /dev/null
+++ b/Katheryne/Modules/WeatherModule.cs
@@ -0,0 +1,67 @@
+using System.Net.Http.Json;
+using System.Text.Json.Serialization;
+using Katheryne.Exceptions;
+
+namespace Katheryne.Modules;
+
+public class WeatherModule : ModuleBase
+{
+ private static readonly HttpClient s_httpClient = new();
+
+ private const string WeatherApi =
+ "https://api.seniverse.com/v3/weather/now.json?key=S7s93MkxJ1q7mgHoj&location=beijing&language=zh-Hans&unit=c";
+
+ private WeatherDto? _weatherDto;
+
+ public override string ModuleName => "weather";
+
+ public WeatherModule()
+ {
+ Functions.Add("text", () => FetchWeatherDate().Now.Text);
+ Functions.Add("temp", () => FetchWeatherDate().Now.Temperature);
+ }
+
+ private WeatherDto FetchWeatherDate()
+ {
+ if (_weatherDto is not null)
+ {
+ if (DateTime.Now - _weatherDto.LastUpdate < TimeSpan.FromMinutes(5))
+ {
+ return _weatherDto;
+ }
+ }
+
+ try
+ {
+ Task>?> task = s_httpClient.GetFromJsonAsync>>(
+ WeatherApi, JsonOptions);
+ task.Wait();
+ Dictionary>? response = task.Result;
+ WeatherDto? weather = response?["results"][0];
+
+ _weatherDto = weather ?? throw new ModuleException("Failed to fetch weather data.");
+ return _weatherDto;
+ }
+ catch (HttpRequestException e)
+ {
+ throw new ModuleException("Failed to fetch weather data.", e);
+ }
+ }
+
+ private class WeatherNowDto
+ {
+ public required string Text { get; set; }
+
+ public required string Code { get; set; }
+
+ public required string Temperature { get; set; }
+ }
+
+ private class WeatherDto
+ {
+ public required WeatherNowDto Now { get; set; }
+
+ [JsonPropertyName("last_update")]
+ public DateTime LastUpdate { get; set; }
+ }
+}
\ No newline at end of file