Поделиться через


Интеграция собственного пользовательского канала с помощью Direct Line

С помощью Dynamics 365 Contact Center можно реализовать соединитель для интеграции пользовательских каналов обмена сообщениями с помощью API Direct Line 3.0, которая входит в пакет SDK для .NET. Полный пример кода показывает, как можно создать собственный соединитель. Дополнительные сведения об API Direct Line 3.0 см. в статье Основные понятия API Direct Line 3.0.

В этой статье объясняется, как канал подключен к Microsoft Direct Line Bot Framework, который внутренне подключен к Центру контактов Dynamics 365. В следующем разделе представлены фрагменты кода, в которых Direct Line API 3.0 используется для создания клиента Direct Line и интерфейса IChannelAdapter для создания примера соединителя.

Замечание

Исходный код и документация описывают общий поток подключения канала к Центру контактов Dynamics 365 через Direct Line и не сосредоточены на аспектах надежности и масштабируемости.

Components

Адаптер Webhook API Service

Когда пользователь вводит сообщение, из канала вызывается 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. Проверьте подпись запроса входящего сообщения.

Входящий запрос из канала проверяется на основе ключа подписи. Если запрос недействителен, выдается сообщение об исключении «недопустимая подпись». Если запрос действителен, он обрабатывается следующим образом:

  /// <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 КБ.

Этот объект действия включает следующие атрибуты:

Свойство Description
from Хранит информацию об учетной записи канала, которая состоит из уникального идентификатора пользователя и имени (комбинации имени и фамилии, разделенных пробелом).
channelId Указывает идентификатор канала. Для входящих запросов идентификатор канала — directline.
serviceUrl Указывает URL-адрес службы. Для входящих запросов URL-адрес службы — https://directline.botframework.com/.
type Указывает тип действия. Для действий сообщений тип — message.
текст Хранит содержимое сообщения.
id Указывает идентификатор, который адаптер использует для ответа на исходящие сообщения.
channelData Указывает данные канала, которые состоят из channelType, conversationcontext и customercontext.
channelType Указывает имя канала, через который клиент отправляет сообщения. Например, MessageBird, KakaoTalk, Snapchat
conversationcontext Ссылается на объект словаря, который содержит переменные контекста, определенные в рабочем потоке. Dynamics 365 Contact Center использует эту информацию для маршрутизации беседы к правому представителю службы клиентов (представителю службы или представителю). Рассмотрим пример.
"conversationcontext ":{ "ProductName": "Xbox", "Issue":"Installation" }
В этом примере контекст маршрутизирует разговор на оператора отдела обслуживания, который занимается установкой Xbox.
customercontext Ссылается на объект словаря, который содержит сведения о клиенте, такие как номер телефона и адрес электронной почты. Центр контактов Dynamics 365 использует эти сведения для идентификации записи контакта пользователя.
"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. Адаптер канала также должен передавать обработчик событий, который процессор ретрансляции запускает, когда получает исходящее сообщение от Dynamics 365 Contact Center через Direct Line.

Обработка исходящих действий

Процессор ретрансляции вызывает обработчик событий для отправки исходящих действий соответствующему адаптеру канала, а затем адаптер обрабатывает исходящие действия. Адаптер канала выполняет следующие исходящие операции:

  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 процессор ретрансляции проверяет, активен ли разговор для определенного действия

Чтобы узнать, активен ли разговор, процессор ретрансляции поддерживает коллекцию активных разговоров в словаре. Этот словарь содержит ключ как идентификатор пользователя, который однозначно идентифицирует пользователя, и значение как объект следующего класса:

 /// <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, для идентификатором пользователя в словаре.
 /// <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);
    }
}

Замечание

В основе кода, осуществляющего прием сообщения, лежит метод GetActivitiesAsync, принимающий в качестве параметров ConversationId и watermark. Параметр watermark предназначен для извлечения только тех сообщений, которые еще не доставлены Direct Line. Если указан параметр водяного знака, разговор воспроизводится с водяного знака, что гарантирует, что сообщения не будут потеряны.

Отправка действия в 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, когда оператор отдела обслуживания клиентов (оператор отдела обслуживания, оператор) отправляет (исходящее) сообщение, отформатированное с помощью Markdown, боту Direct Line, бот получает это сообщение в определенном формате. Теперь, если бот получает (входящее) отформатированное сообщение от клиента, он должен уметь правильно интерпретировать сообщение, отформатированное с помощью разметки Markdown. Вам как разработчику необходимо правильно использовать Markdown, чтобы сообщение было надлежащим образом отформатировано для ваших операторов отдела обслуживания и клиентов.

Дополнительные сведения о форматах Markdown см. в статье Форматы Markdown для сообщений чата.

Замечание

  • В настоящее время мы не поддерживаем сочетание клавиш <SHIFT+ ВВОД> для добавления нескольких разрывов строк.
  • Для входящих сообщений установите для текста разметки Markdown значение свойства text объекта Действие.
  • В случае исходящих сообщений текст Markdown будет получен в свойстве text объекта Действие (аналогично обычному сообщению).

Дальнейшие шаги

Поддержка чата в реальном времени и асинхронных каналов

Настройка пользовательского канала обмена сообщениями
Справочник по API MessageBird
Рекомендации по настройке ботов