无法序列化 IDialogContext;需要从 MS Bot Framework 中的 Dialog 对象中获取用户信息

Cannot serialize IDialogContext; need to get user information from inside a Dialog object in MS Bot Framework

我正在使用 MS 机器人框架并尝试让机器人发送预定消息。为此,我使用了 Hangfire 框架。

我正在尝试使用此代码进行调度,其中上下文是从我的对话框传递的 IDialogContext 对象,而 SendScheduledMessageToUser 方法刚刚调用 context.PostAsync() 向用户发送消息:

            BackgroundJob.Schedule(() => CurrencyDialog.SendScheduledMessageToUser(context), new DateTimeOffset(when));

问题是上下文结果为空。我在调用它的控制台中看到一个序列化异常。我假设你不能序列化上下文对象,因为它内部有循环引用。

因此,为了向用户发送预定消息,我最好的办法是获取用户信息(ID、对话 ID、频道、服务 URL 等),然后将这个简单的数据传递给scheduled 方法,以便它可以向用户发送消息。然而,令人难以置信的是,似乎没有办法从 IDialog 的实现内部获取用户数据。 IDialogContext 有该数据,但它被标记为私有或内部,所以我无法获取它。而且我无法在对话框首次启动时将 Activity 对象传递给对话框,因为没有构造函数。

关于从 IDialog 的实现中获取用户信息或以其他方式获取一些可以序列化的数据以便向用户发送预定消息的任何想法?

您可以将原始activity传递给后台作业

我的示例使用的是Quartz.NET,但应该类似于Hangfire

public class ReminderJob : IJob
{
    public void Execute(IJobExecutionContext context)
    {
        var dataMap = context.Trigger.JobDataMap;
        var originalActivity = dataMap["originalActivity"] as Activity;
        var message = dataMap["reply"] as string;

        if (originalActivity == null) return;

        var connector = new ConnectorClient(new Uri(originalActivity.ServiceUrl));
        var reply = originalActivity.CreateReply(message);
        connector.Conversations.ReplyToActivity(reply);
    }
}

public class JobScheduler
{
    public static void StartReminderJob(ITrigger trigger)
    {
        var scheduler = StdSchedulerFactory.GetDefaultScheduler();
        scheduler.Start();

        var job = JobBuilder.Create<ReminderJob>().Build();
        scheduler.ScheduleJob(job, trigger);
    }
}

我是这样使用的

// how to use
var jobDetail = TriggerBuilder
            .Create()
            .StartAt(new DateTimeOffset(result.Start.Value.ToUniversalTime()))
            .Build();

jobDetail.JobDataMap["originalActivity"] = originalMessage;
jobDetail.JobDataMap["reply"] = $"test";

JobScheduler.StartReminderJob(jobDetail);

好的,所以我最终使用的解决方案非常蹩脚,但有效。

由于 Hangfire 在序列化 IDialogContext 或 Activity 时抛出异常,我在 Dialog 对象和 Hangfire 调用的方法的参数中保存了一些字符串参数,然后在必要时重新创建activity。

在我的对话框中 class:

public class CurrencyDialog : IDialog<object>
{
    public string serviceUrl;
    public string from;
    public string recipient;
    public string conversation;
    public string channelId;
    public string activityId;

对话框启动时,将字符串保存在对象中:

    public async Task StartAsync(IDialogContext context)
    {
        //await context.PostAsync("What rate would you like to check today?");
        context.Wait(BeginCurrencyDialog);
    }

    public async Task BeginCurrencyDialog(IDialogContext context, IAwaitable<IMessageActivity> argument)
    {
        //this needs to be saved because neither Activity nor IDialogContext are serializable, but Hangfire needs it
        IMessageActivity activity = await argument;
        serviceUrl = activity.ServiceUrl;
        from = activity.From.Id;
        recipient = activity.Recipient.Id;
        conversation = activity.Conversation.Id;
        channelId = activity.ChannelId;
        activityId = activity.Id;

当你想安排一个工作时,将这些传递给你在 Hangfire 中使用的方法:

最后,到时候在Hangfire调用的方法中,用这个方法重新创建activity,然后你可以用它来发送一个答案:

    //creates an activity that can be used to send rich messages in response; cannot use original activity because it is not serializable
    public Activity CreateActivity()
    {
        if (activityId == null) { return null; }

        ChannelAccount fromAccount = new ChannelAccount(id: from);
        ChannelAccount recipientAccount = new ChannelAccount(id: recipient);
        ConversationAccount conversationAccount = new ConversationAccount(id: conversation);

        return new Activity()
        {
            Type = ActivityTypes.Message,
            ServiceUrl = serviceUrl,
            From = fromAccount,
            Recipient = recipientAccount,
            Conversation = conversationAccount,
            ChannelId = channelId,
            Id = activityId
        };
    }

之后,您可以使用 activity.CreateReply() 创建回复并使用 context.PostAsync 将其发送给用户。

还有另一种获取用户数据的方法。 在您的控制器 Post 方法中执行此操作 -

case ActivityTypes.Message:

    StateClient stateClient = activity.GetStateClient();        
    BotData userData = await stateClient.BotState.GetUserDataAsync(activity.ChannelId, activity.From.Id);
    userData.SetProperty<string>("name", activity.From.Name);
    //other properties you want to set.
    await stateClient.BotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, userData);

然后在您的意图方法中,像这样访问它们 -

[LuisIntent("Greetings")]
public async Task Greetings(IDialogContext context, LuisResult result)
{
var username = context.UserData.Get<string>("name");
await context.PostAsync($"hi {username}");
    context.Wait(MessageReceived);
}

这比手动创建活动要优雅得多。