共用方式為


使用 Direct Line 整合您自己的自訂管道

借助 Omnichannel for Customer Service,您可以通過實施連接器並使用 Direct Line API 3.0(.NET SDK 的一部分)來整合自定義消息傳遞渠道。 完整的 範例代碼 說明瞭如何創建自己的連接器。 若要瞭解有關 Direct Line API 3.0 的詳細資訊,請參閱 Direct Line 3.0 API 中的關鍵概念

本文說明如何將管道連接到內部附加至 Customer Service 全通路的 Microsoft Direct Line Bot Framework。 以下部分包括使用 Direct Line API 3.0 建立 Direct Line 用戶端的代碼片段,以及用於構建示例連接器的 IChannelAdapter 介面。

備註

原始程式碼和文檔描述了管道如何通過 Direct Line 連接到 Customer Service 全渠道的整體流程,不關注可靠性和可擴充性方面。

元件

配接器 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 活動。

傳入要求承載會轉換成 Bot Framework 可以理解的活動。

備註

活動負載不得超過 28 KB 的消息大小限制。

此 Activity 物件包括以下屬性:

屬性 說明
存儲由使用者和名稱的唯一標識碼(名字和姓氏的組合,用空格分隔符分隔)組成的頻道帳戶資訊。
channelId 指示通道標識碼。 對於入站請求,通道 ID 為 directline
serviceUrl 指示服務URL。 對於入站請求,服務 URL 為 https://directline.botframework.com/.
類型 指示活動類型。 對於訊息活動,此類型是 message
文字 存儲消息內容。
識別碼 指示適配器用於回應出站消息的標識碼。
通道數據 指示由 channelTypeconversationcontextcustomercontext組成的通道數據。
channelType (通道類型) 指示客戶通過其發送消息的通道名稱。 例如,MessageBird、KakaoTalk、Snapchat
對話上下文 引用包含工作流中定義的上下文變數的字典物件。 全通路客戶服務使用此資訊將對話路由到合適的客戶服務代表(客服代表或代表)。 例如:
"conversationcontext ":{ "ProductName" : "Xbox", "Issue":"Installation" }
在此範例中,上下文將對話路由到處理 Xbox 安裝的服務代表。
客戶上下文 引用包含客戶詳細資訊(如電話號碼和電子郵件位址)的字典物件。 全渠道客戶服務利用此資訊來識別用戶的聯絡記錄。
“customercontext”:{ “email”:email@email.com, “電話號碼”:“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 收到來自 Customer Service 全管道的出站消息時調用該處理程式。

處理傳出活動

中繼處理器調用事件處理程式以將出站活動發送到相應的通道適配器,然後適配器處理出站活動。 管道配接器會執行下列傳出活動:

  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,該 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 中結束對話

後續步驟

支援即時聊天和非同步管道
在使用 Direct Line 的自訂管道中的 Markdown 格式

設定自訂傳訊管道
MessageBird API 參考
設定 Bot 的最佳做法