由外部服务触发时恢复 Bot Framework 对话框

Resume Bot Framework dialog when triggered by external service

场景

我有一个使用 Bot Framework 构建的带有一系列对话框的机器人。其中一个对话框让用户可以选择通过网页输入一些复杂的数据,方法是向他们显示一个按钮。单击按钮,他们将被带到该站点,填写数据,保存,然后被引导回机器人。

我希望我的机器人暂停对话,直到它从我的网页收到一个事件,告诉我用户已保存数据,然后继续向用户提问。

之前

我实现了一个版本,在该版本中,我会在用户单击按钮之前存储一个 ConversationReference,然后当外部事件发生时,我会从 webhook 发送我想显示的卡片和下一条消息(不在对话框中) ,这很好,但它变得相当 complicated/messy - 我宁愿将整个应用程序保持在一个连续的对话框中。

思路一:使用DirectLineAPI

我做了一些调查,很多人建议使用 DirectLine API。所以我实现了这个:

public async Task SendEventAsync(InternalEventMessage message, ConversationReference reference) {
        var client = new DirectLineClient(!String.IsNullOrEmpty(_settings.DirectLineSecret) ? _settings.DirectLineSecret : null);
        if (_settings.SiteUrl.Contains("localhost")) {
            client.BaseUri = new Uri(_settings.DirectLineServiceUrl);
        }

        var eventMessage = Activity.CreateEventActivity();
        //Wrong way round?!?
        eventMessage.From = reference.Bot;
        eventMessage.Type = ActivityTypes.Event;
        eventMessage.Value = message;
        var conversation = await client.Conversations.PostActivityAsync(reference.Conversation.Id, eventMessage as Activity);
    }

这使用 DirectLine 客户端使用存储的 ConversationReference 向 serviceUrl 发送事件消息,基本上是在模仿用户(机器人和用户在 SDK 中似乎是错误的方式)。检查 localhost 是为了让 DirectLine 库指向模拟器服务器而不是 https://directline.botframework.com.

在我的对话中我调用:

//method above shows input button and links to web page
context.Wait(WaitForAddressInput);
}

private async Task WaitForAddressInput(IDialogContext context, IAwaitable<IActivity> result) {
    var message = await result;
    switch (message.Type) {
        case ActivityTypes.Message:
            //TODO: Add response
            break;

        case ActivityTypes.Event:
            var eventMessage = message as IEventActivity;
            if (((JObject)eventMessage.Value).ToObject<InternalEventMessage>().Type == EventType.AddressInputComplete) {
                _addressResult = (await _tableService.ReadOrderById(Order.OrderId)).Address;
                await context.PostAsync($"Great}");
                context.Done(_addressResult);
            }
            break;
    }
}

这会在按钮显示后等待来自用户的任何消息,如果我们的事件匹配,那么我们将继续对话。

这可以使用模拟器在本地运行,但令人沮丧的是,它无法运行。它无法识别通过网络聊天或 Messenger 创建的频道。此处解释:

For security reasons, you can't use DirectLine to spy on messages from another conversation.

所以我无法访问不是使用 DirectLine 创建的频道。

思路二:BotConnector

所以我想我会使用类似的代码尝试 BotConnector:

public async Task SendEventAsync(InternalEventMessage message, Microsoft.Bot.Connector.DirectLine.ConversationReference reference) {
    var botAccount = new ChannelAccount(reference.User.Id, reference.User.Name);
    var userAccount = new ChannelAccount(reference.Bot.Id, reference.Bot.Name);

    MicrosoftAppCredentials.TrustServiceUrl(reference.ServiceUrl);
    var connector = new ConnectorClient(new Uri(reference.ServiceUrl), new MicrosoftAppCredentials("xxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxx"));

    connector.Credentials.InitializeServiceClient();

    var eventMessage = Activity.CreateMessageActivity();
    eventMessage.Recipient = botAccount;
    eventMessage.From = userAccount;
    eventMessage.Type = ActivityTypes.Event;
    eventMessage.Conversation = new ConversationAccount(id: reference.Conversation.Id);
    eventMessage.ServiceUrl = reference.ServiceUrl;
    eventMessage.Timestamp = DateTimeOffset.UtcNow;
    eventMessage.LocalTimestamp = DateTime.Now;
    eventMessage.ChannelId = reference.ChannelId;

    var result = await connector.Conversations.SendToConversationAsync(eventMessage as Microsoft.Bot.Connector.Activity);
}

这不会崩溃,我可以看到该事件出现在模拟器请求控制台中,但什么也没有发生,它似乎被忽略了!

思路三:尝试模仿bot服务调用我的bot

我还没有尝试过,因为我认为这可能是最耗时的,但我正在阅读 here 有关服务身份验证的内容,想知道是否可以模仿托管机器人服务发送一个消息并使用所需数据以这种方式发送我的事件?

这似乎是一个相当常见的场景,所以令我惊讶的是我还没有找到执行此操作的方法。如果有人对我如何从外部服务向我的 bot 发送事件消息有任何其他想法,那么我很想听听。

更新: 请参阅 Eric 下面我的回答,看看我做了什么。

想法 1:

DirectLine 是一个频道,而不是用于连接频道的库。 (例如:您不会使用 Facebook Messenger 连接到 Skype)DirectLineClient 对于创建连接到 DirectLine 通道的客户端应用程序非常有用Direct Line 连接器服务。

想法 2:

这个方法应该有效。事实上,BotAuth 库将此方法用于 CallbackController 中的 MagicNumber 登录流程:https://github.com/MicrosoftDX/botauth/blob/9a0a9f1b665f4aa95b6d60d09346dda90d8b314e/CSharp/BotAuth/Controllers/CallbackController.cs

对于您的场景,您应该能够构造一个 CardAction 类型的 ActionTypes.OpenUrl,其中包含一个值ConversationReference 编码在 url 中。单击该按钮将调用显示页面的 mvc 控制器(将 ConversationReference 保存在 cookie 或其他内容中),当用户在页面上添加完地址后,使用 ConversationReference 向机器人发送事件(类似于 BotAuth 如何在 CallbackController 中恢复对话)。

想法 3:

这会绕过连接器服务,并且不受支持。您分享的 link 详细解释了身份验证在 Bot 框架中的工作原理,而不是如何绕过连接器服务。

Eric 的回答让我使用 BotAuth 示例解决了这个问题,但为了完整起见,这是我使用想法 2 所做的。

我在 Bot Framework 端点上创建了一个 CallbackController,然后使用以下代码将事件发送回等待对话框:

MicrosoftAppCredentials.TrustServiceUrl(reference.ServiceUrl);
            var message = reference.GetPostToBotMessage();
            message.Value = new InternalEventMessage(type);
            message.Type = ActivityTypes.Event;

            await Conversation.ResumeAsync(reference, message);

对话框等待此代码并继续:

context.Wait(WaitForAddressInput);
        }

        private async Task WaitForAddressInput(IDialogContext context,
                                      IAwaitable<IActivity> result)
        {
            var message = await result;
            switch (message.Type)
            {
                case ActivityTypes.Message:
                    //TODO: Add response
                    break;

                case ActivityTypes.Event:
                    //Process event and continue!
                    break;
            }
        }

这是我遇到过的最复杂的 Bot 框架问题,我发现文档有点欠缺。希望这对某人有所帮助!