Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/HuaJiBot.NET.Plugin.Calendar/PluginMain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
98 changes: 86 additions & 12 deletions src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ internal class ReminderTask : IDisposable
private readonly Func<Ical.Net.Calendar?> _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,
Expand All @@ -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;
}

Expand All @@ -41,6 +43,7 @@ public void Start()
}

private DateTimeOffset _scheduledTimeEnd = Utils.NetworkTime.Now; //截止到该时间点的日程已经在Task队列中列入计划了
private readonly Dictionary<string, DateTimeOffset> _scheduledEvents = new(); //已经计划的事件,防止重复提醒,值为提醒时间

private void ForEachMatchedGroup(CalendarEvent e, Action<Action<string>> callback)
{
Expand Down Expand Up @@ -92,6 +95,41 @@ private void ForEachMatchedGroup(CalendarEvent e, Action<Action<string>> 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()
{
Expand All @@ -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 事件发生前 提前 _ 分钟提醒
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -201,23 +256,42 @@ private void InvokeCheck()
private void ScheduleStartReminder(
TimeSpan waiting,
CalendarEvent e,
string eventKey,
Action<CalendarEvent> 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<CalendarEvent> start)
private void SendReminder(CalendarEvent e, string eventKey, Action<CalendarEvent> 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();
}
}