diff --git a/src/HuaJiBot.NET.Plugin.Calendar/PluginMain.cs b/src/HuaJiBot.NET.Plugin.Calendar/PluginMain.cs index ca33cd5..e48c0ea 100644 --- a/src/HuaJiBot.NET.Plugin.Calendar/PluginMain.cs +++ b/src/HuaJiBot.NET.Plugin.Calendar/PluginMain.cs @@ -11,6 +11,9 @@ public class PluginConfig : ConfigBase { public int MinRange = -128; public int MaxRange = 48; + public int ReminderBeforeStartMinutes = 60; // 可配置的开始前提醒时间 + public int ReminderBeforeEndMinutes = 5; // 可配置的结束前提醒时间 + public int CheckIntervalMinutes = 5; // 可配置的检查间隔,默认5分钟提高精度 public ReminderFilterConfig[] ReminderGroups = []; public class ReminderFilterConfig diff --git a/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs b/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs index 2723074..86ff3bd 100644 --- a/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs +++ b/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs @@ -12,9 +12,10 @@ internal class ReminderTask : IDisposable private readonly Func _getCalendar; private Ical.Net.Calendar? Calendar => _getCalendar(); private readonly Timer _timer; - private const int CheckDurationInMinutes = 15; - private const int RemindBeforeStartMinutes = 60; - private const int RemindBeforeEndMinutes = 5; + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private int CheckDurationInMinutes => Config.CheckIntervalMinutes; + private int RemindBeforeStartMinutes => Config.ReminderBeforeStartMinutes; + private int RemindBeforeEndMinutes => Config.ReminderBeforeEndMinutes; public ReminderTask( BotService service, @@ -25,13 +26,14 @@ public ReminderTask( Service = service; Config = config; _getCalendar = getCalendar; - _timer = new(TimeSpan.FromMinutes(CheckDurationInMinutes)); //每15分钟检查一次 + _timer = new(TimeSpan.FromMinutes(CheckDurationInMinutes)); //使用可配置的检查间隔 _timer.Elapsed += (_, _) => InvokeCheck(); - Task.Delay(10_000) + Task.Delay(10_000, _cancellationTokenSource.Token) .ContinueWith(_ => { - InvokeCheck(); - }); //10秒后检查第一次 + if (!_cancellationTokenSource.Token.IsCancellationRequested) + InvokeCheck(); + }, TaskContinuationOptions.OnlyOnRanToCompletion); //10秒后检查第一次 _timer.AutoReset = true; } @@ -41,6 +43,7 @@ public void Start() } private DateTimeOffset _scheduledTimeEnd = Utils.NetworkTime.Now; //截止到该时间点的日程已经在Task队列中列入计划了 + private readonly Dictionary _scheduledEvents = new(); //已经计划的事件,防止重复提醒,值为提醒时间 private void ForEachMatchedGroup(CalendarEvent e, Action> callback) { @@ -92,6 +95,41 @@ private void ForEachMatchedGroup(CalendarEvent e, Action> callbac // } //} + private static string GetEventKey(CalendarEvent e, DateTimeOffset remindTime, string type) + { + // Create unique key combining event details to prevent collisions + // Include event UID if available for better uniqueness + var startTime = e.Start?.ToLocalNetworkTime().ToString("yyyy-MM-dd HH:mm:ss") ?? "unknown"; + var endTime = e.End?.ToLocalNetworkTime().ToString("yyyy-MM-dd HH:mm:ss") ?? "none"; + var summary = e.Summary?.Replace("|", "").Replace(":", "") ?? "nosummary"; + var location = e.Location?.Replace("|", "").Replace(":", "") ?? ""; + var uid = e.Uid ?? ""; + + // Use a more robust key format that's less prone to collisions + return $"{type}:{uid}:{summary}:{startTime}:{endTime}:{location}:{remindTime:yyyy-MM-dd-HH-mm-ss}"; + } + + private void CleanupOldScheduledEvents(DateTimeOffset now) + { + // 清理已经过时的计划事件记录,防止内存无限增长 + // 只移除提醒时间已经过去超过1小时的事件 + var cutoffTime = now.AddHours(-1); + var keysToRemove = _scheduledEvents + .Where(kvp => kvp.Value < cutoffTime) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var key in keysToRemove) + { + _scheduledEvents.Remove(key); + } + + if (keysToRemove.Count > 0) + { + Service.LogDebug($"清理了 {keysToRemove.Count} 个过时的事件记录"); + } + } + [MethodImpl(MethodImplOptions.Synchronized)] //防止多线程同时更新时间节点 private void InvokeCheck() { @@ -102,12 +140,16 @@ private void InvokeCheck() Service.Log("日历为空,跳过检查。(日历未成功同步)"); return; } - Service.LogDebug("Invoke Check"); + Service.LogDebug($"Invoke Check - 检查间隔: {CheckDurationInMinutes}分钟"); var now = Utils.NetworkTime.Now; //现在 var nextEnd = now.AddMinutes(CheckDurationInMinutes); //下次检查的结束时间(避免检查过的时间被重复添加进队列) var start = _scheduledTimeEnd; //从上次结束的时间点开始检查 var end = nextEnd; //到下次结束的时间点结束检查 _scheduledTimeEnd = nextEnd; //更新时间节点 + + // 清理已经过时的计划事件记录,防止内存增长 + CleanupOldScheduledEvents(now); + #region 开始提醒 { //RemindBeforeStartMinutes 事件发生前 提前 _ 分钟提醒 @@ -124,9 +166,16 @@ private void InvokeCheck() var timeRemained = remindTime - now; //计算距离提醒时间还有多久 if (timeRemained < TimeSpan.Zero) //如果已经开始了 continue; //跳过 + + var eventKey = GetEventKey(e, remindTime, "start"); + if (_scheduledEvents.ContainsKey(eventKey)) //如果已经计划过了 + continue; //跳过 + + _scheduledEvents[eventKey] = remindTime; //添加到已计划列表,记录提醒时间 ScheduleStartReminder( timeRemained, e, + eventKey, ev => { Service.Log( @@ -167,9 +216,15 @@ private void InvokeCheck() if (e.End is null) //跳过没有结束时间的事件 continue; + var eventKey = GetEventKey(e, remindTime, "end"); + if (_scheduledEvents.ContainsKey(eventKey)) //如果已经计划过了 + continue; //跳过 + + _scheduledEvents[eventKey] = remindTime; //添加到已计划列表,记录提醒时间 ScheduleStartReminder( timeRemained, e, + eventKey, ev => { Service.Log( @@ -201,23 +256,42 @@ private void InvokeCheck() private void ScheduleStartReminder( TimeSpan waiting, CalendarEvent e, + string eventKey, Action start ) { - Task.Delay(waiting).ContinueWith(_ => SendReminder(e, start)); + Task.Delay(waiting, _cancellationTokenSource.Token) + .ContinueWith(_ => SendReminder(e, eventKey, start), + _cancellationTokenSource.Token, + TaskContinuationOptions.OnlyOnRanToCompletion, + TaskScheduler.Default); Service.Log( $"[日程] 计划发送提醒:{e.Summary}({e.Start?.ToLocalNetworkTime()}) ({waiting.TotalMinutes:F1}分钟后发送)" ); } - private void SendReminder(CalendarEvent e, Action start) + private void SendReminder(CalendarEvent e, string eventKey, Action start) { - Service.Log($"[日程] 发送提醒:{e.Summary}({e.Start?.ToLocalNetworkTime()})"); - start(e); + try + { + Service.Log($"[日程] 发送提醒:{e.Summary}({e.Start?.ToLocalNetworkTime()})"); + start(e); + + // 提醒发送成功后,立即从计划列表中移除,防止内存泄漏 + _scheduledEvents.Remove(eventKey); + } + catch (Exception ex) + { + Service.LogError($"发送提醒失败:{e.Summary}", ex); + // 即使发送失败也要清理,避免重试风暴 + _scheduledEvents.Remove(eventKey); + } } public void Dispose() { + _cancellationTokenSource.Cancel(); _timer.Dispose(); + _cancellationTokenSource.Dispose(); } }