通过


使用 Direct Line 集成您自己的自定义渠道

借助 Dynamics 365 Contact Center,可以实现连接器,以便使用 Direct Line API 3.0(即 .NET SDK 的一部分)集成自定义消息传递通道。 完整的 示例代码 说明了如何创建自己的连接器。 若要了解有关 Direct Line API 3.0 的详细信息,请参阅 Direct Line 3.0 API 中的关键概念

本文解释如何将频道连接到 Microsoft Direct Line Bot Framework,该框架内部与 Dynamics 365 Contact Center 相连。 以下部分包括使用 Direct Line API 3.0 创建 Direct Line 客户端的代码片段,以及用于构建示例连接器的 IChannelAdapter 接口。

注释

源代码和文档描述了通道如何通过 Direct Line 连接到 Dynamics 365 Contact Center 的总体流程,并且不专注于可靠性和可伸缩性的各个方面。

组件

适配器 Webhook API 服务

当用户输入消息时,将从通道调用适配器 API。 它处理入站请求并发送成功或失败状态作为响应。 适配器 API 服务必须实现该 IChannelAdapter 接口,并将入站请求发送到相应的通道适配器以处理请求。

/// <summary>
/// Accept an incoming web-hook request from MessageBird Channel
/// </summary>
/// <param name="requestPayload">Inbound request Object</param>
/// <returns>Executes the result operation of the action method asynchronously.</returns>
    [HttpPost("postactivityasync")]
    public async Task<IActionResult> PostActivityAsync(JToken requestPayload)
    {
        if (requestPayload == null)
        {
            return BadRequest("Request payload is invalid.");
        }

        try
        {
            await _messageBirdAdapter.ProcessInboundActivitiesAsync(requestPayload, Request).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            _logger.LogError($"postactivityasync: {ex}");
            return StatusCode(500, "An error occured while handling your request.");
        }

        return StatusCode(200);
    }

渠道适配器

通道适配器处理入站和出站活动,并且必须实现接口 IAdapterBuilder

处理入站活动

渠道适配器执行以下入站活动:

  1. 验证入站消息请求签名。

来自通道的入站请求将根据签名密钥进行验证。 如果请求无效,则会引发 “invalid signature” 异常消息。 如果请求有效,则按如下方式进行:

  /// <summary>
  /// Validate Message Bird Request
  /// </summary>
  /// <param name="content">Request Content</param>
  /// <param name="request">HTTP Request</param>
  /// <param name="messageBirdSigningKey">Message Bird Signing Key</param>
  /// <returns>True if there request is valid, false if there aren't.</returns>
  public static bool ValidateMessageBirdRequest(string content, HttpRequest request, string messageBirdSigningKey)
  {
      if (string.IsNullOrWhiteSpace(messageBirdSigningKey))
      {
          throw new ArgumentNullException(nameof(messageBirdSigningKey));
      }
      if (request == null)
      {
          throw new ArgumentNullException(nameof(request));
      }
      if (string.IsNullOrWhiteSpace(content))
      {
          throw new ArgumentNullException(nameof(content));
      }
      var messageBirdRequest = new MessageBirdRequest(
          request.Headers?["Messagebird-Request-Timestamp"],
          request.QueryString.Value?.Equals("?",
              StringComparison.CurrentCulture) != null
              ? string.Empty
              : request.QueryString.Value,
          GetBytes(content));

      var messageBirdRequestSigner = new MessageBirdRequestSigner(GetBytes(messageBirdSigningKey));
      string expectedSignature = request.Headers?["Messagebird-Signature"];
      return messageBirdRequestSigner.IsMatch(expectedSignature, messageBirdRequest);
  }
  1. 将入站请求转换为机器人活动。

入站请求有效负载将转换为 Bot Framework 可理解的活动。

注释

活动负载不得超过 28 KB 的消息大小限制。

此 Activity 对象包括以下属性:

特征 DESCRIPTION
起始 存储由用户和名称的唯一标识符(名字和姓氏的组合,用空格分隔符分隔)组成的频道帐户信息。
channelId 指示通道标识符。 对于入站请求,通道 ID 为 directline
serviceUrl 指示服务 URL。 对于入站请求,服务 URL 为 https://directline.botframework.com/.
类型 指示活动类型。 对于消息活动,类型为 message.
文本 存储消息内容。
id 指示适配器用于响应出站消息的标识符。
通道数据 指示由 channelTypeconversationcontextcustomercontext组成的通道数据。
channelType (通道类型) 指示客户通过其发送消息的通道名称。 例如,MessageBird、KakaoTalk、Snapchat
对话上下文 引用包含工作流中定义的上下文变量的字典对象。 Dynamics 365 Contact Center 使用此信息将对话路由到正确的客户服务代表(服务代表或代表)。 例如:
“conversationcontext ”:{ “ProductName”: “Xbox”, “Issue”:“安装” }
在此示例中,上下文将对话路由到处理 Xbox 安装的服务代表。
客户上下文 引用包含客户详细信息(如电话号码和电子邮件地址)的字典对象。 Dynamics 365 Contact Center 使用此信息来标识用户的联系人记录。
“customercontext”:{ “email”:email@email.com, “phonenumber”:“1234567890” }
  /// <summary>
  /// Build Bot Activity type from the inbound MessageBird request payload<see cref="Activity"/>
  /// </summary>
  /// <param name = "messagePayload"> Message Bird Activity Payload</param>
  /// <returns>Direct Line Activity</returns>
  public static Activity PayloadToActivity(MessageBirdRequestModel messagePayload)
  {
  if (messagePayload == null)
  {
      throw new ArgumentNullException(nameof(messagePayload));
  }
  if (messagePayload.Message?.Direction == ConversationMessageDirection.Sent ||
  messagePayload.Type == ConversationWebhookMessageType.MessageUpdated)
  {
      return null;
  }
  var channelData = new ActivityExtension
  {
      ChannelType = ChannelType.MessageBird,
      // Add Conversation Context in below dictionary object. Please refer the document for more information.
      ConversationContext = new Dictionary<string, string>(),
      // Add Customer Context in below dictionary object. Please refer the document for more information.
      CustomerContext = new Dictionary<string, string>()
  };
  var activity = new Activity
      {
          From = new ChannelAccount(messagePayload.Message?.From, messagePayload.Contact?.DisplayName),
          Text = messagePayload.Message?.Content?.Text,
          Type = ActivityTypes.Message,
          Id = messagePayload.Message?.ChannelId,
          ServiceUrl = Constant.DirectLineBotServiceUrl,
          ChannelData = channelData
      };

      return activity;
  }

示例 JSON 有效负载如下所示:

{
    "type": "message",
    "id": "bf3cc9a2f5de...",    
    "serviceUrl": https://directline.botframework.com/,
    "channelId": "directline",
    "from": {
        "id": "1234abcd",// userid which uniquely identify the user
        "name": "customer name" // customer name as First Name <space> Last Name
    },
    "text": "Hi,how are you today.",
    "channeldata":{
        "channeltype":"messageBird",
        "conversationcontext ":{ // this holds context variables defined in Workstream
            "ProductName" : "XBox",
            "Issue":"Installation"
        },
        "customercontext":{            
            "email":email@email.com,
            "phonenumber":"1234567890"           
        }
    }
}

  1. 将活动发送到消息中继处理器。

生成活动有效负载后,它会调用消息中继处理器的 PostActivityAsync 方法,将活动发送到 Direct Line。 通道适配器还应传递事件处理程序,中继处理器在通过 Direct Line 从 Dynamics 365 Contact Center 接收出站消息时调用该事件处理程序。

处理出站活动

中继处理器调用事件处理程序以将出站活动发送到相应的通道适配器,然后适配器处理出站活动。 渠道适配器执行以下出站活动:

  1. 将出站活动转换为渠道响应模型。

Direct Line 的活动将转换为渠道特定的响应模型。

  /// <summary>
  /// Creates MessageBird response object from a Bot Framework <see cref="Activity"/>.
  /// </summary>
  /// <param name="activities">The outbound activities.</param>
  /// <param name="replyToId">Reply Id of Message Bird user.</param>
  /// <returns>List of MessageBird Responses.</returns>
  public static List<MessageBirdResponseModel> ActivityToMessageBird(IList<Activity> activities, string replyToId)
  {
      if (string.IsNullOrWhiteSpace(replyToId))
      {
          throw new ArgumentNullException(nameof(replyToId));
      }

      if (activities == null)
      {
          throw new ArgumentNullException(nameof(activities));
      }

      return activities.Select(activity => new MessageBirdResponseModel
      {
          To = replyToId,
          From = activity.ChannelId,
          Type = "text",
          Content = new Content
          {
              Text = activity.Text
          }
      }).ToList();
  }
  1. 通过通道 REST API 发送响应。

通道适配器调用 REST API 以向通道发送出站响应,然后将其发送给用户。

  /// <summary>
  /// Send Outbound Messages to Message Bird
  /// </summary>
  /// <param name="messageBirdResponses">Message Bird Response object</param>
  /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
  public async Task SendMessagesToMessageBird(IList<MessageBirdResponseModel> messageBirdResponses)
  {
      if (messageBirdResponses == null)
      {
          throw new ArgumentNullException(nameof(messageBirdResponses));
      }

      foreach (var messageBirdResponse in messageBirdResponses)
      {
          using (var request = new HttpRequestMessage(HttpMethod.Post, $"{MessageBirdDefaultApi}/send"))
          {
              var content = JsonConvert.SerializeObject(messageBirdResponse);
              request.Content = new StringContent(content, Encoding.UTF8, "application/json");
              await _httpClient.SendAsync(request).ConfigureAwait(false);
          }
      }
  }

消息中继处理器

消息中继处理器从通道适配器接收入站活动,并执行活动模型验证。 在将此活动发送到 Direct Line 之前,中继处理器会检查会话是否针对该特定活动处于活动状态。

为了查找会话是否处于活动状态,中继处理器会在字典中维护一组活动会话。 该字典中包含的键为用户ID,它唯一标识用户,值是以下类的对象:

 /// <summary>
/// Direct Line Conversation to store as an Active Conversation
/// </summary>
public class DirectLineConversation
{
    /// <summary>
    /// .NET SDK Client to connect to Direct Line Bot
    /// </summary>
    public DirectLineClient DirectLineClient { get; set; }

    /// <summary>
    /// Direct Line response after start a new conversation
    /// </summary>
    public Conversation Conversation { get; set; }

    /// <summary>
    /// Watermark to guarantee that no messages are lost
    /// </summary>
    public string WaterMark { get; set; }
}

如果对话对于中继处理器收到的活动不可用,它将执行以下步骤:

  1. 使用 Direct Line 启动会话,并将 Direct Line 针对用户 ID 发送的会话对象存储在字典中。
 /// <summary>
 /// Initiate Conversation with Direct Line Bot
 /// </summary>
 /// <param name="inboundActivity">Inbound message from Aggregator/Channel</param>
 /// <param name="adapterCallBackHandler">Call Back to send activities to Messaging API</param>
 /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 private async Task InitiateConversation(Activity inboundActivity, EventHandler<IList<Activity>> adapterCallBackHandler)
 {
     var directLineConversation = new DirectLineConversation
     {
         DirectLineClient = new DirectLineClient(_relayProcessorConfiguration.Value.DirectLineSecret)
     };
     // Start a conversation with Direct Line Bot
     directLineConversation.Conversation = await directLineConversation.DirectLineClient.Conversations.
         StartConversationAsync().ConfigureAwait(false);

     await directLineConversation.DirectLineClient.Conversations.
         StartConversationAsync().ConfigureAwait(false);
     if (directLineConversation.Conversation == null)
     {
         throw new Exception(
             "An error occurred while starting the Conversation with direct line. Please validate the direct line secret in the configuration file.");
     }

     // Adding the Direct Line Conversation object to the lookup dictionary and starting a thread to poll the activities from the direct line bot.
     if (ActiveConversationCache.ActiveConversations.TryAdd(inboundActivity.From.Id, directLineConversation))
     {
         // Starts a new thread to poll the activities from Direct Line Bot
         new Thread(async () => await PollActivitiesFromBotAsync(
             directLineConversation.Conversation.ConversationId, inboundActivity, adapterCallBackHandler).ConfigureAwait(false))
         .Start();
     }
 }
  1. 启动新的执行绪,以基于配置文件中配置的轮询间隔从 Direct Line 机器人轮询出站活动。 在从 Direct Line 接收对话活动结束之前,轮询执行绪一直处于有效状态。
/// <summary>
/// Polling the activities from BOT for the active conversation
/// </summary>
/// <param name="conversationId">Direct Line Conversation Id</param>
/// <param name="inboundActivity">Inbound Activity from Channel/Aggregator</param>
/// <param name="lineActivitiesReceived">Call Back to send activities to Messaging API</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
private async Task PollActivitiesFromBotAsync(string conversationId, Activity inboundActivity, EventHandler<IList<Activity>> lineActivitiesReceived)
{
    if (!int.TryParse(_relayProcessorConfiguration.Value.PollingIntervalInMilliseconds, out var pollingInterval))
    {
        throw new FormatException($"Invalid Configuration value of PollingIntervalInMilliseconds: {_relayProcessorConfiguration.Value.PollingIntervalInMilliseconds}");
    }
    if (!ActiveConversationCache.ActiveConversations.TryGetValue(inboundActivity.From.Id,
        out var conversationContext))
    {
        throw new KeyNotFoundException($"No active conversation found for {inboundActivity.From.Id}");
    }
    while (true)
    {
        var watermark = conversationContext.WaterMark;
        // Retrieve the activity set from the bot.
        var activitySet = await conversationContext.DirectLineClient.Conversations.
            GetActivitiesAsync(conversationId, watermark).ConfigureAwait(false);
        // Set the watermark to the message received
        watermark = activitySet?.Watermark;

        // Extract the activities sent from our bot.
        if (activitySet != null)
        {
            var activities = (from activity in activitySet.Activities
                              where activity.From.Id == _relayProcessorConfiguration.Value.BotHandle
                              select activity).ToList();
            if (activities.Count > 0)
            {
                SendReplyActivity(activities, inboundActivity, lineActivitiesReceived);
            }
            // Update Watermark
            ActiveConversationCache.ActiveConversations[inboundActivity.From.Id].WaterMark = watermark;
            if (activities.Exists(a => a.Type.Equals("endOfConversation", StringComparison.InvariantCulture)))
            {
                if (ActiveConversationCache.ActiveConversations.TryRemove(inboundActivity.From.Id, out _))
                {
                    Thread.CurrentThread.Abort();
                }
            }
        }
        await Task.Delay(TimeSpan.FromMilliseconds(pollingInterval)).ConfigureAwait(false);
    }
}

注释

接收消息的代码的核心是使用 ConversationIdwatermark 作为参数的 GetActivitiesAsync 方法。 参数 watermark 的目的是检索 Direct Line 未传递的消息。 如果指定了 watermark 参数,则会话将从 watermark 重播,因此不会丢失任何消息。

将活动发送到 Direct Line

 /// <summary>
 /// Send the activity to the bot using Direct Line client
 /// </summary>
 /// <param name="inboundActivity">Inbound message from Aggregator/Channel</param>
 /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 private static async Task SendActivityToBotAsync(Activity inboundActivity)
 {
     if (!ActiveConversationCache.ActiveConversations.TryGetValue(inboundActivity.From.Id,
         out var conversationContext))
     {
         throw new KeyNotFoundException($"No active conversation found for {inboundActivity.From.Id}");
     }
     await conversationContext.DirectLineClient.Conversations.PostActivityAsync(
         conversationContext.Conversation.ConversationId, inboundActivity).ConfigureAwait(false);
 }

如果会话是针对中继处理器接收的活动而激活的,则将活动发送到消息中继处理器。

结束会话

若要结束对话,请参阅 在 Direct Line 中结束对话

自定义通道中的 Markdown 格式

可以使用 Direct Line API 3.0 在自定义消息通道中使用 Markdown 发送和接收格式化的消息。 了解 Markdown 格式如何通过通道传递,并了解格式的详细信息有助于更新自己的用户界面中的 HTML 样式和标记。

在 Direct Line 渠道中,当客户服务代表(服务代表或代表)向 Direct Line 机器人发送外发消息时,若消息采用 Markdown 格式,机器人将以特定格式接收该消息。 现在,如果机器人收到来自客户的格式化消息(入站),则必须能够正确解释使用 Markdown 格式化的消息。 作为开发人员,你需要适当地使用 Markdown,以便为服务代表和客户正确设置消息的格式。

了解更多有关 用于聊天消息的 Markdown 格式的信息。

注释

  • 目前,我们不支持 <Shift + Enter> 组合键来添加多个换行符。
  • 对于入站消息,请将 Markdown 文本设置为 Activity 对象的 text 属性。
  • 对于出站消息,Markdown 文本在 Activity 对象的 text 属性(类似于普通消息)中接收。

后续步骤

支持实时聊天和异步渠道

配置自定义消息渠道
MessageBird API 参考
配置机器人的最佳实践