From f865cc24513f2d0ea337ccce6f2fbb85f9812d23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:32:18 +0000 Subject: [PATCH 1/4] Initial plan From 04c7176ca98b050a6acad0e0ac67f541481b3ee8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:42:56 +0000 Subject: [PATCH 2/4] Optimize ReminderTask logic with memory management, deduplication, and configurable intervals Co-authored-by: LazuliKao <46601807+LazuliKao@users.noreply.github.com> --- .../PluginMain.cs | 3 + .../ReminderTask.cs | 86 ++++++++++++++++--- 2 files changed, 78 insertions(+), 11 deletions(-) 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..76b2f1f 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 HashSet _scheduledEvents = new(); //已经计划的事件,防止重复提醒 private void ForEachMatchedGroup(CalendarEvent e, Action> callback) { @@ -92,6 +95,39 @@ private void ForEachMatchedGroup(CalendarEvent e, Action> callbac // } //} + private static string GetEventKey(CalendarEvent e, DateTimeOffset remindTime, string type) + { + // Create unique key combining event summary, start time, and reminder type + return $"{type}_{e.Summary}_{e.Start?.ToLocalNetworkTime():yyyy-MM-dd HH:mm}_{remindTime:yyyy-MM-dd HH:mm}"; + } + + private void CleanupOldScheduledEvents(DateTimeOffset now) + { + // 清理1小时前的已计划事件记录,防止内存无限增长 + var cutoffTime = now.AddHours(-1); + var keysToRemove = _scheduledEvents + .Where(key => + { + var parts = key.Split('_'); + if (parts.Length >= 4 && DateTime.TryParse($"{parts[3]}_{parts[4]}", out var eventTime)) + { + return eventTime < cutoffTime; + } + return false; + }) + .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 +138,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,6 +164,12 @@ private void InvokeCheck() var timeRemained = remindTime - now; //计算距离提醒时间还有多久 if (timeRemained < TimeSpan.Zero) //如果已经开始了 continue; //跳过 + + var eventKey = GetEventKey(e, remindTime, "start"); + if (_scheduledEvents.Contains(eventKey)) //如果已经计划过了 + continue; //跳过 + + _scheduledEvents.Add(eventKey); //添加到已计划列表 ScheduleStartReminder( timeRemained, e, @@ -167,6 +213,11 @@ private void InvokeCheck() if (e.End is null) //跳过没有结束时间的事件 continue; + var eventKey = GetEventKey(e, remindTime, "end"); + if (_scheduledEvents.Contains(eventKey)) //如果已经计划过了 + continue; //跳过 + + _scheduledEvents.Add(eventKey); //添加到已计划列表 ScheduleStartReminder( timeRemained, e, @@ -204,7 +255,11 @@ private void ScheduleStartReminder( Action start ) { - Task.Delay(waiting).ContinueWith(_ => SendReminder(e, start)); + Task.Delay(waiting, _cancellationTokenSource.Token) + .ContinueWith(_ => SendReminder(e, start), + _cancellationTokenSource.Token, + TaskContinuationOptions.OnlyOnRanToCompletion, + TaskScheduler.Default); Service.Log( $"[日程] 计划发送提醒:{e.Summary}({e.Start?.ToLocalNetworkTime()}) ({waiting.TotalMinutes:F1}分钟后发送)" ); @@ -212,12 +267,21 @@ Action start private void SendReminder(CalendarEvent e, Action start) { - Service.Log($"[日程] 发送提醒:{e.Summary}({e.Start?.ToLocalNetworkTime()})"); - start(e); + try + { + Service.Log($"[日程] 发送提醒:{e.Summary}({e.Start?.ToLocalNetworkTime()})"); + start(e); + } + catch (Exception ex) + { + Service.LogError($"发送提醒失败:{e.Summary}", ex); + } } public void Dispose() { + _cancellationTokenSource.Cancel(); _timer.Dispose(); + _cancellationTokenSource.Dispose(); } } From cb5f0c9176a78b2aba6753b610072900046210e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:46:09 +0000 Subject: [PATCH 3/4] Fix event key format and cleanup logic for better reliability Co-authored-by: LazuliKao <46601807+LazuliKao@users.noreply.github.com> --- .../ReminderTask.cs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs b/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs index 76b2f1f..f507f06 100644 --- a/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs +++ b/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs @@ -98,7 +98,11 @@ private void ForEachMatchedGroup(CalendarEvent e, Action> callbac private static string GetEventKey(CalendarEvent e, DateTimeOffset remindTime, string type) { // Create unique key combining event summary, start time, and reminder type - return $"{type}_{e.Summary}_{e.Start?.ToLocalNetworkTime():yyyy-MM-dd HH:mm}_{remindTime:yyyy-MM-dd HH:mm}"; + // Use | as separator to avoid conflicts with underscores in event names + var startTime = e.Start?.ToLocalNetworkTime().ToString("yyyy-MM-dd HH:mm") ?? "unknown"; + var remindTimeStr = remindTime.ToString("yyyy-MM-dd HH:mm"); + var summary = e.Summary?.Replace("|", "_") ?? "nosummary"; // 替换可能的分隔符 + return $"{type}|{summary}|{startTime}|{remindTimeStr}"; } private void CleanupOldScheduledEvents(DateTimeOffset now) @@ -108,10 +112,24 @@ private void CleanupOldScheduledEvents(DateTimeOffset now) var keysToRemove = _scheduledEvents .Where(key => { - var parts = key.Split('_'); - if (parts.Length >= 4 && DateTime.TryParse($"{parts[3]}_{parts[4]}", out var eventTime)) + var parts = key.Split('|'); + // 事件键格式: {type}|{summary}|{startTime}|{remindTime} + if (parts.Length >= 4) { - return eventTime < cutoffTime; + try + { + // 最后一部分是提醒时间 + var remindTimeStr = parts[3]; + if (DateTime.TryParse(remindTimeStr, out var eventTime)) + { + return eventTime < cutoffTime; + } + } + catch + { + // 如果解析失败,保守地保留这个记录 + return false; + } } return false; }) From a21b4ee2c307b50a2ff41ab839c80d8d6b0e1a6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Sep 2025 09:29:48 +0000 Subject: [PATCH 4/4] Optimize event deduplication mechanism and ensure _scheduledEvents cleanup Co-authored-by: LazuliKao <46601807+LazuliKao@users.noreply.github.com> --- .../ReminderTask.cs | 66 ++++++++----------- 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs b/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs index f507f06..86ff3bd 100644 --- a/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs +++ b/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs @@ -43,7 +43,7 @@ public void Start() } private DateTimeOffset _scheduledTimeEnd = Utils.NetworkTime.Now; //截止到该时间点的日程已经在Task队列中列入计划了 - private readonly HashSet _scheduledEvents = new(); //已经计划的事件,防止重复提醒 + private readonly Dictionary _scheduledEvents = new(); //已经计划的事件,防止重复提醒,值为提醒时间 private void ForEachMatchedGroup(CalendarEvent e, Action> callback) { @@ -97,42 +97,26 @@ private void ForEachMatchedGroup(CalendarEvent e, Action> callbac private static string GetEventKey(CalendarEvent e, DateTimeOffset remindTime, string type) { - // Create unique key combining event summary, start time, and reminder type - // Use | as separator to avoid conflicts with underscores in event names - var startTime = e.Start?.ToLocalNetworkTime().ToString("yyyy-MM-dd HH:mm") ?? "unknown"; - var remindTimeStr = remindTime.ToString("yyyy-MM-dd HH:mm"); - var summary = e.Summary?.Replace("|", "_") ?? "nosummary"; // 替换可能的分隔符 - return $"{type}|{summary}|{startTime}|{remindTimeStr}"; + // 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小时前的已计划事件记录,防止内存无限增长 + // 清理已经过时的计划事件记录,防止内存无限增长 + // 只移除提醒时间已经过去超过1小时的事件 var cutoffTime = now.AddHours(-1); var keysToRemove = _scheduledEvents - .Where(key => - { - var parts = key.Split('|'); - // 事件键格式: {type}|{summary}|{startTime}|{remindTime} - if (parts.Length >= 4) - { - try - { - // 最后一部分是提醒时间 - var remindTimeStr = parts[3]; - if (DateTime.TryParse(remindTimeStr, out var eventTime)) - { - return eventTime < cutoffTime; - } - } - catch - { - // 如果解析失败,保守地保留这个记录 - return false; - } - } - return false; - }) + .Where(kvp => kvp.Value < cutoffTime) + .Select(kvp => kvp.Key) .ToList(); foreach (var key in keysToRemove) @@ -184,13 +168,14 @@ private void InvokeCheck() continue; //跳过 var eventKey = GetEventKey(e, remindTime, "start"); - if (_scheduledEvents.Contains(eventKey)) //如果已经计划过了 + if (_scheduledEvents.ContainsKey(eventKey)) //如果已经计划过了 continue; //跳过 - _scheduledEvents.Add(eventKey); //添加到已计划列表 + _scheduledEvents[eventKey] = remindTime; //添加到已计划列表,记录提醒时间 ScheduleStartReminder( timeRemained, e, + eventKey, ev => { Service.Log( @@ -232,13 +217,14 @@ private void InvokeCheck() continue; var eventKey = GetEventKey(e, remindTime, "end"); - if (_scheduledEvents.Contains(eventKey)) //如果已经计划过了 + if (_scheduledEvents.ContainsKey(eventKey)) //如果已经计划过了 continue; //跳过 - _scheduledEvents.Add(eventKey); //添加到已计划列表 + _scheduledEvents[eventKey] = remindTime; //添加到已计划列表,记录提醒时间 ScheduleStartReminder( timeRemained, e, + eventKey, ev => { Service.Log( @@ -270,11 +256,12 @@ private void InvokeCheck() private void ScheduleStartReminder( TimeSpan waiting, CalendarEvent e, + string eventKey, Action start ) { Task.Delay(waiting, _cancellationTokenSource.Token) - .ContinueWith(_ => SendReminder(e, start), + .ContinueWith(_ => SendReminder(e, eventKey, start), _cancellationTokenSource.Token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default); @@ -283,16 +270,21 @@ Action start ); } - private void SendReminder(CalendarEvent e, Action start) + private void SendReminder(CalendarEvent e, string eventKey, Action start) { try { Service.Log($"[日程] 发送提醒:{e.Summary}({e.Start?.ToLocalNetworkTime()})"); start(e); + + // 提醒发送成功后,立即从计划列表中移除,防止内存泄漏 + _scheduledEvents.Remove(eventKey); } catch (Exception ex) { Service.LogError($"发送提醒失败:{e.Summary}", ex); + // 即使发送失败也要清理,避免重试风暴 + _scheduledEvents.Remove(eventKey); } }