You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET Bot Framework SDK v4:同时间提醒重复发送自适应卡片的解决方法

Teams机器人重复发送提醒卡片问题排查与解决

问题现象

使用Bot Framework SDK v4在Teams频道实现提醒功能,提醒数据存储于SharePoint,通过定时器在预设时间拉取提醒并发送自适应卡片。当提醒设置为不同时间时,每个提醒对应一张卡片,功能正常;但创建n个同一时间的提醒时,会收到n*n张重复卡片,不符合预期。曾尝试用字典以ReminderId为键去重,但未生效。

相关代码

private async Task SetReminderTimer(ITurnContext turnContext, ReminderListDto reminderTask, DateTime dateTime, string userInputTimeZoneOffset)
{
    try
    {
        // Calculate the time until the reminder should trigger
        var delay = dateTime - DateTime.Now;

        _reminderTimer = new Timer(async _ =>
        {
            // Retrieve the reminder item from SharePoint
            var reminders = await _sharePointLogger.GetReminderFromSharePointAsync(reminderTask, userInputTimeZoneOffset);

            foreach (var reminder in reminders)
            {
                // If the reminder item exists and the reminder time is in the past, send the reminder message to the user
                if (reminder != null && DateTime.Now > reminderTask.Time)
                {
                    // Send a reminder message to the user
                    var conversationReference = turnContext.Activity.GetConversationReference();
                    await ((BotAdapter)turnContext.Adapter).ContinueConversationAsync(_botId, conversationReference, async (turnContext, cancellationToken) =>
                    {
                        var displayReminder = ReminderList.DisplayReminder(reminder.Time.ToString("HH:mm"), reminder.ReminderType, reminder.Priority, reminder.ReminderDetails);
                        var adaptiveCardAttachment = new Attachment()
                        {
                            ContentType = "application/vnd.microsoft.card.adaptive",
                            Content = JsonConvert.DeserializeObject(displayReminder),
                        };
                        await turnContext.SendActivityAsync(MessageFactory.Attachment(adaptiveCardAttachment), cancellationToken);
                    }, default);
                    //await DeleteReminderFromSharePointAsync(reminder.ReminderId);

                }
            }

        }, null, (int)delay.TotalMilliseconds, Timeout.Infinite);
    }
    catch (Exception ex)
    {
        _logger.LogInformation($"error at sedning reminder to user {ex.Message} ");
        throw;
    }

}

问题根源

  1. 定时器重复创建:每创建一个同时间的提醒,就会调用一次SetReminderTimer,生成独立的定时器实例。当到达预设时间时,每个定时器都会执行回调逻辑,从SharePoint拉取所有同时间的提醒并逐个发送。例如创建3个同时间提醒,会生成3个定时器,每个定时器遍历3条提醒发送,最终产生9张重复卡片。
  2. 去重逻辑无效:之前尝试的字典去重仅在单个定时器的回调内部生效,每个定时器的字典是独立实例,无法跨定时器去重;或者字典未持久化,每次回调执行时重新初始化,起不到全局去重作用。

解决方案

方案一:按时间维度聚合提醒,统一管理定时器

维护全局字典,按触发时间(标准化到分钟级别,避免毫秒差异)分组管理提醒列表和定时器实例,确保同一时间仅存在一个定时器:

// 全局字典,线程安全操作需加锁
private readonly Dictionary<DateTime, (List<ReminderListDto> Reminders, Timer Timer)> _timedReminders = new Dictionary<DateTime, (List<ReminderListDto>, Timer)>();

private async Task SetReminderTimer(ITurnContext turnContext, ReminderListDto reminderTask, DateTime triggerTime, string userInputTimeZoneOffset)
{
    try
    {
        // 标准化触发时间,消除毫秒级差异
        var normalizedTime = new DateTime(triggerTime.Year, triggerTime.Month, triggerTime.Day, triggerTime.Hour, triggerTime.Minute, 0);
        
        lock (_timedReminders)
        {
            if (_timedReminders.TryGetValue(normalizedTime, out var existingEntry))
            {
                // 已有对应定时器,直接添加提醒到列表
                existingEntry.Reminders.Add(reminderTask);
                return;
            }

            var delay = normalizedTime - DateTime.Now;
            var newReminders = new List<ReminderListDto> { reminderTask };
            
            var timer = new Timer(async _ =>
            {
                // 遍历该时间下的所有提醒发送
                foreach (var reminder in newReminders)
                {
                    var conversationReference = turnContext.Activity.GetConversationReference();
                    await ((BotAdapter)turnContext.Adapter).ContinueConversationAsync(_botId, conversationReference, async (ctx, cancellationToken) =>
                    {
                        var displayReminder = ReminderList.DisplayReminder(reminder.Time.ToString("HH:mm"), reminder.ReminderType, reminder.Priority, reminder.ReminderDetails);
                        var adaptiveCardAttachment = new Attachment()
                        {
                            ContentType = "application/vnd.microsoft.card.adaptive",
                            Content = JsonConvert.DeserializeObject(displayReminder),
                        };
                        await ctx.SendActivityAsync(MessageFactory.Attachment(adaptiveCardAttachment), cancellationToken);
                    }, default);
                    // 发送后删除SharePoint中的提醒
                    await DeleteReminderFromSharePointAsync(reminder.ReminderId);
                }

                // 触发完成后移除全局字典中的对应条目
                lock (_timedReminders)
                {
                    _timedReminders.Remove(normalizedTime);
                }

            }, null, (int)delay.TotalMilliseconds, Timeout.Infinite);

            _timedReminders.Add(normalizedTime, (newReminders, timer));
        }
    }
    catch (Exception ex)
    {
        _logger.LogInformation($"error at setting reminder timer: {ex.Message}");
        throw;
    }
}

方案二:通过SharePoint标记提醒发送状态

在SharePoint的提醒表中新增IsSent布尔字段(默认值false),发送前先标记状态,确保每个提醒仅发送一次:

// 修改定时器回调中的逻辑
var reminders = await _sharePointLogger.GetReminderFromSharePointAsync(reminderTask, userInputTimeZoneOffset);
foreach (var reminder in reminders.Where(r => !r.IsSent && DateTime.Now > r.Time))
{
    // 先将提醒标记为已发送(需确保SharePoint操作的原子性,避免并发问题)
    await _sharePointLogger.MarkReminderAsSentAsync(reminder.ReminderId);
    
    // 再发送自适应卡片
    var conversationReference = turnContext.Activity.GetConversationReference();
    await ((BotAdapter)turnContext.Adapter).ContinueConversationAsync(_botId, conversationReference, async (turnContext, cancellationToken) =>
    {
        var displayReminder = ReminderList.DisplayReminder(reminder.Time.ToString("HH:mm"), reminder.ReminderType, reminder.Priority, reminder.ReminderDetails);
        var adaptiveCardAttachment = new Attachment()
        {
            ContentType = "application/vnd.microsoft.card.adaptive",
            Content = JsonConvert.DeserializeObject(displayReminder),
        };
        await turnContext.SendActivityAsync(MessageFactory.Attachment(adaptiveCardAttachment), cancellationToken);
    }, default);
}

方案三:清理重复定时器(应急方案)

若暂时无法重构定时器管理逻辑,可在创建新定时器前,取消同一时间的旧定时器(不推荐长期使用,建议采用方案一或二):

  • 维护全局定时器列表,创建新定时器时遍历列表,取消所有相同触发时间的旧定时器,再创建新的。

内容的提问来源于stack exchange,提问作者Abhay Gupta

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 16:45:30