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


Шаблон взаимодействия с человеком

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

В этом примере показано, как создать оркестрацию Durable Functions, которая включает взаимодействие с человеком. В этом примере реализована система проверки телефонов на основе SMS. В процессах проверки номера телефона и многофакторной аутентификации (MFA) это встречается часто.

Замечание

Общедоступна версия 4 модели программирования Node.js для функций Azure. Модель версии 4 предназначена для обеспечения более гибкого и интуитивно понятного интерфейса для разработчиков JavaScript и TypeScript. Дополнительные сведения о различиях между версиями 3 и 4 см. в руководстве по миграции.

В следующих фрагментах кода JavaScript (PM4) обозначает модель программирования версии 4, новый интерфейс.

Необходимые условия

В этой статье показано, как реализовать шаблон взаимодействия с человеком с помощью пакетов SDK для устойчивых задач. В этом примере реализуется рабочий процесс утверждения, в котором оркестрация ожидает, пока человек утвердит или отклонит запрос, прежде чем продолжить.

Обзор сценария

Проверка телефона помогает подтвердить, что пользователи, использующие ваше приложение, не являются спамерами, и что они контролируют номер телефона, который они предоставляют. Многофакторная проверка подлинности — это распространенный способ защиты учетных записей. Для создания собственной верификации телефона требуется взаимодействие с отслеживанием состояния с человеком. Обычно пользователь получает код (например, четырехзначное число) и должен отвечать в разумный период времени.

Стандартные Azure Functions являются без состояния (как и многие другие конечные точки в облаке), поэтому для такого типа взаимодействия необходимо хранить состояние в базе данных или другом постоянном хранилище. Вы также разделяете взаимодействие между несколькими функциями и координируете их. Например, одна функция создает код, сохраняет его и отправляет его на телефон пользователя. Другая функция получает ответ пользователя и сопоставляет его с исходным запросом для проверки кода. Добавьте время ожидания для защиты безопасности. Этот рабочий процесс быстро становится сложным.

Durable Functions снижает сложность этого сценария. В этом примере функция оркестратора управляет состоянием взаимодействия без использования внешней базы данных. Поскольку функции оркестратора устойчивы, эти интерактивные потоки являются высоконадежными.

Рабочие процессы утверждения распространены в бизнес-приложениях, где запрос должен быть проверен человеком перед продолжением. Требования к рабочему процессу:

  • Ожидайте неопределённого времени для реагирования человека или до тайм-аута
  • Обработка результатов утверждения и отклонения
  • Время ожидания поддержки при отсутствии ответа
  • Отслеживание состояния , чтобы запрашиватель смог проверить ход выполнения

Пакеты SDK для устойчивых задач упрощают этот сценарий:

  • Внешние события: оркестрация может приостановить и ждать события, вызываемого внешней системой или пользователем.
  • Устойчивые таймеры: задайте время ожидания, которое запускается, если ответ не получен.
  • Настраиваемое состояние: отслеживание и предоставление текущего состояния рабочего процесса клиентам

Настройка интеграции Twilio

Для отправки SMS-сообщений на мобильный телефон в этом примере используется служба Twilio. Azure Functions уже поддерживает Twilio с помощью привязки Twilio, и этот пример использует эту возможность.

Первое, что вам нужно — это учетная запись Twilio. Вы можете создать бесплатную учетную запись здесь: https://www.twilio.com/try-twilio. Получив аккаунт, добавьте следующие три параметра приложения в функциональное приложение.

Имя параметра приложения Описание значения
TwilioAccountSid Идентификатор безопасности вашей учетной записи в Twilio.
TwilioAuthToken Токен аутентификации для вашей учетной записи Twilio.
TwilioPhoneNumber Номер телефона, связанный с вашей учетной записью в Twilio. Используется для отправки SMS-сообщений.

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

В этой статье рассматриваются следующие функции в примере приложения:

  • E4_SmsPhoneVerification: функция оркестратора , которая запускает процесс проверки телефона и управляет временем ожидания и повторными попытками.
  • E4_SendSmsChallenge: функция действия , которая отправляет код по текстовому сообщению.

Замечание

Функция HttpStart в приложении-примере и руководстве быстрого старта выполняет роль клиента оркестрации и запускает функцию оркестратора.

В этой статье описываются следующие компоненты в примере приложения:

  • ApprovalOrchestration / approvalOrchestrator / human_interaction_orchestrator: оркестратор, который отправляет запрос на согласование и ожидает ответа человека или тайм-аута.
  • SubmitApprovalRequestActivity / submitRequest / submit_approval_request: действие, которое уведомляет утверждающего человека, например по электронной почте или сообщению чата.
  • ProcessApprovalActivity / processApproval / process_approval: действие, обрабатывающее решение об утверждении.

Orchestrator

Функция оркестратора E4_SmsPhoneVerification

[FunctionName("E4_SmsPhoneVerification")]
public static async Task<bool> Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    string phoneNumber = context.GetInput<string>();
    if (string.IsNullOrEmpty(phoneNumber))
    {
        throw new ArgumentNullException(
            nameof(phoneNumber),
            "A phone number input is required.");
    }

    int challengeCode = await context.CallActivityAsync<int>(
        "E4_SendSmsChallenge",
        phoneNumber);

    using (var timeoutCts = new CancellationTokenSource())
    {
        // The user has 90 seconds to respond with the code they received in the SMS message.
        DateTime expiration = context.CurrentUtcDateTime.AddSeconds(90);
        Task timeoutTask = context.CreateTimer(expiration, timeoutCts.Token);

        bool authorized = false;
        for (int retryCount = 0; retryCount <= 3; retryCount++)
        {
            Task<int> challengeResponseTask =
                context.WaitForExternalEvent<int>("SmsChallengeResponse");

            Task winner = await Task.WhenAny(challengeResponseTask, timeoutTask);
            if (winner == challengeResponseTask)
            {
                // We got back a response! Compare it to the challenge code.
                if (challengeResponseTask.Result == challengeCode)
                {
                    authorized = true;
                    break;
                }
            }
            else
            {
                // Timeout expired
                break;
            }
        }

        if (!timeoutTask.IsCompleted)
        {
            // All pending timers must be complete or canceled before the function exits.
            timeoutCts.Cancel();
        }

        return authorized;
    }
}

Замечание

Это может быть не очевидно сначала, но этот оркестратор не нарушает детерминированное ограничение оркестрации. Это детерминировано, так как CurrentUtcDateTime свойство вычисляет время истечения срока действия таймера, и оно возвращает то же значение для каждого воспроизведения на этом этапе в коде оркестратора. Это поведение обеспечивает, что winner остается одинаковым при каждом повторяющемся вызове Task.WhenAny.

После запуска функция оркестратора выполняет следующие задачи:

  1. Получает номер телефона для отправки SMS-уведомления.
  2. Вызывает E4_SendSmsChallenge для отправки SMS-сообщения пользователю и возвращает ожидаемый четырехзначный код вызова.
  3. Создает устойчивый таймер, который срабатывает через 90 секунд после текущего времени.
  4. Параллельно с таймером ожидается событие SmsChallengeResponse от пользователя.

Пользователи получают SMS-сообщение с 4-значным кодом. У них есть 90 секунд для отправки того же кода экземпляру оркестратора для завершения проверки. Если они отправят неправильный код, они получают три дополнительных попытки в том же 90-секундном окне.

Предупреждение

Отмените таймеры, которые вам больше не нужны. В приведенном выше примере оркестрация отменяет таймер при принятии ответа на вызов.

Оркестратор отправляет запрос на утверждение, а затем ожидает ответа человека или истечения времени ожидания.

using Microsoft.DurableTask;
using System;
using System.Threading;
using System.Threading.Tasks;

[DurableTask(nameof(ApprovalOrchestration))]
public class ApprovalOrchestration : TaskOrchestrator<ApprovalRequestData, ApprovalResult>
{
    public override async Task<ApprovalResult> RunAsync(
        TaskOrchestrationContext context, ApprovalRequestData input)
    {
        string requestId = input.RequestId;
        double timeoutHours = input.TimeoutHours;

        // Step 1: Submit the approval request (notify approver)
        SubmissionResult submissionResult = await context.CallActivityAsync<SubmissionResult>(
            nameof(SubmitApprovalRequestActivity), input);

        // Make the status available via custom status
        context.SetCustomStatus(submissionResult);

        // Step 2: Create a durable timer for the timeout
        DateTime timeoutDeadline = context.CurrentUtcDateTime.AddHours(timeoutHours);

        using var timeoutCts = new CancellationTokenSource();
        Task timeoutTask = context.CreateTimer(timeoutDeadline, timeoutCts.Token);

        // Step 3: Wait for an external event (approval/rejection)
        Task<ApprovalResponseData> approvalTask = context.WaitForExternalEvent<ApprovalResponseData>(
            "approval_response");

        // Step 4: Wait for either the timeout or the approval response
        Task completedTask = await Task.WhenAny(approvalTask, timeoutTask);

        // Step 5: Process based on which task completed
        ApprovalResult result;

        if (completedTask == approvalTask)
        {
            // Human responded in time - cancel the timeout timer
            timeoutCts.Cancel();

            ApprovalResponseData approvalData = approvalTask.Result;

            // Process the approval
            result = await context.CallActivityAsync<ApprovalResult>(
                nameof(ProcessApprovalActivity),
                new ProcessApprovalInput
                {
                    RequestId = requestId,
                    IsApproved = approvalData.IsApproved,
                    Approver = approvalData.Approver
                });
        }
        else
        {
            // Timeout occurred
            result = new ApprovalResult
            {
                RequestId = requestId,
                Status = "Timeout",
                ProcessedAt = context.CurrentUtcDateTime.ToString("o")
            };
        }

        return result;
    }
}

Этот оркестратор выполняет следующие действия:

  1. Отправляет запрос на утверждение, вызвав действие, которое уведомляет утверждающего.
  2. Задает настраиваемое состояние, чтобы клиенты могли отслеживать ход выполнения.
  3. Создает надёжный таймер для дедлайна ожидания.
  4. Ожидает внешнего события (approval_response), которое инициирует утверждающий.
  5. Использует WhenAny, when_any, или anyOf чтобы дождаться того, что завершится первым: утверждение или тайм-аут.
  6. Обрабатывает результат в зависимости от того, какая задача завершится.

Предупреждение

Отмените таймеры, которые вам больше не нужны. В примере C# оркестрация отменяет таймер ожидания при получении утверждения.

Действия

Активная функция E4_SendSmsChallenge

Функция E4_SendSmsChallenge использует привязку Twilio для отправки SMS-сообщения, включающего четырехзначный код пользователю.

[FunctionName("E4_SendSmsChallenge")]
public static int SendSmsChallenge(
    [ActivityTrigger] string phoneNumber,
    ILogger log,
    [TwilioSms(AccountSidSetting = "TwilioAccountSid", AuthTokenSetting = "TwilioAuthToken", From = "%TwilioPhoneNumber%")]
        out CreateMessageOptions message)
{
    // Get a random number generator with a random seed (not time-based)
    var rand = new Random(Guid.NewGuid().GetHashCode());
    int challengeCode = rand.Next(10000);

    log.LogInformation($"Sending verification code {challengeCode} to {phoneNumber}.");

    message = new CreateMessageOptions(new PhoneNumber(phoneNumber));
    message.Body = $"Your verification code is {challengeCode:0000}";

    return challengeCode;
}

Замечание

Чтобы запустить пример, установите пакет NuGet Microsoft.Azure.WebJobs.Extensions.Twilio. Не устанавливайте основной пакет NuGet Twilio , так как он может вызвать конфликты версий и ошибки сборки.

Действия передают запрос на утверждение и обрабатывают ответ.

Отправка запроса на утверждение задачи

using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

[DurableTask(nameof(SubmitApprovalRequestActivity))]
public class SubmitApprovalRequestActivity : TaskActivity<ApprovalRequestData, SubmissionResult>
{
    private readonly ILogger<SubmitApprovalRequestActivity> _logger;

    public SubmitApprovalRequestActivity(ILogger<SubmitApprovalRequestActivity> logger)
    {
        _logger = logger;
    }

    public override Task<SubmissionResult> RunAsync(
        TaskActivityContext context, ApprovalRequestData input)
    {
        _logger.LogInformation(
            "Submitting approval request {RequestId} from {Requester} for {Item}",
            input.RequestId, input.Requester, input.Item);

        // In a real system, this would send an email, notification, or update a database
        var result = new SubmissionResult
        {
            RequestId = input.RequestId,
            Status = "Pending",
            SubmittedAt = DateTime.UtcNow.ToString("o"),
            ApprovalUrl = $"http://localhost:8000/api/approvals/{input.RequestId}"
        };

        return Task.FromResult(result);
    }
}

Действие утверждения процесса

using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

[DurableTask(nameof(ProcessApprovalActivity))]
public class ProcessApprovalActivity : TaskActivity<ProcessApprovalInput, ApprovalResult>
{
    private readonly ILogger<ProcessApprovalActivity> _logger;

    public ProcessApprovalActivity(ILogger<ProcessApprovalActivity> logger)
    {
        _logger = logger;
    }

    public override Task<ApprovalResult> RunAsync(
        TaskActivityContext context, ProcessApprovalInput input)
    {
        string status = input.IsApproved ? "Approved" : "Rejected";
        _logger.LogInformation(
            "Processing {Status} request {RequestId} by {Approver}",
            status, input.RequestId, input.Approver);

        // In a real system, this would update a database, trigger workflows, etc.
        var result = new ApprovalResult
        {
            RequestId = input.RequestId,
            Status = status,
            ProcessedAt = DateTime.UtcNow.ToString("o"),
            Approver = input.Approver
        };

        return Task.FromResult(result);
    }
}

// Data classes
public class ApprovalRequestData
{
    public string RequestId { get; set; } = string.Empty;
    public string Requester { get; set; } = string.Empty;
    public string Item { get; set; } = string.Empty;
    public double TimeoutHours { get; set; } = 24.0;
}

public class ApprovalResponseData
{
    public bool IsApproved { get; set; }
    public string Approver { get; set; } = string.Empty;
}

public class SubmissionResult
{
    public string RequestId { get; set; } = string.Empty;
    public string Status { get; set; } = string.Empty;
    public string SubmittedAt { get; set; } = string.Empty;
    public string ApprovalUrl { get; set; } = string.Empty;
}

public class ProcessApprovalInput
{
    public string RequestId { get; set; } = string.Empty;
    public bool IsApproved { get; set; }
    public string Approver { get; set; } = string.Empty;
}

public class ApprovalResult
{
    public string RequestId { get; set; } = string.Empty;
    public string Status { get; set; } = string.Empty;
    public string ProcessedAt { get; set; } = string.Empty;
    public string? Approver { get; set; }
}

Запустите пример

Используйте функции, запускаемые HTTP в примере, чтобы начать оркестрацию, отправив следующий HTTP-запрос POST:

POST http://{host}/orchestrators/E4_SmsPhoneVerification
Content-Length: 14
Content-Type: application/json

"+1425XXXXXXX"
HTTP/1.1 202 Accepted
Content-Length: 695
Content-Type: application/json; charset=utf-8
Location: http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}

{"id":"741c65651d4c40cea29acdd5bb47baf1","statusQueryGetUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}","sendEventPostUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/raiseEvent/{eventName}?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}","terminatePostUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/terminate?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}"}

Функция оркестратора получает номер телефона и немедленно отправляет SMS-сообщение в этот номер со случайным образом созданным 4-цифрным кодом проверки, например 2168. Функция ожидает ответ в течение 90 секунд.

Чтобы ответить с помощью кода, используйте RaiseEventAsync (.NET) или raiseEvent (JavaScript и TypeScript) в другой функции, либо вызовите конечную точку HTTP POST sendEventPostUri в ответе с кодом 202. Замените {eventName} на SmsChallengeResponse:

POST http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/raiseEvent/SmsChallengeResponse?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
Content-Length: 4
Content-Type: application/json

2168

Если событие отправляется до истечения таймера, оркестрация завершается, а output поле имеет true значение, указывающее на успешную проверку.

GET http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
HTTP/1.1 200 OK
Content-Length: 144
Content-Type: application/json; charset=utf-8

{"runtimeStatus":"Completed","input":"+1425XXXXXXX","output":true,"createdTime":"2017-06-29T19:10:49Z","lastUpdatedTime":"2017-06-29T19:12:23Z"}

Если срок действия таймера истек или вы ввели неправильный код четыре раза, проверьте состояние, чтобы увидеть, что output установлено в false, что указывает на сбой проверки телефона.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 145

{"runtimeStatus":"Completed","input":"+1425XXXXXXX","output":false,"createdTime":"2017-06-29T19:20:49Z","lastUpdatedTime":"2017-06-29T19:22:23Z"}

Чтобы запустить пример, выполните следующие действия:

  1. Запустите эмулятор планировщика устойчивых задач для локальной разработки.

    docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest
    
  2. Запустите рабочий процесс , чтобы зарегистрировать оркестратор и операции.

  3. Запустите клиент , чтобы запланировать рабочий процесс утверждения и отправить события.

using System;
using System.Threading.Tasks;

var client = DurableTaskClientBuilder.UseDurableTaskScheduler(connectionString).Build();

// Schedule the approval workflow
var input = new ApprovalRequestData
{
    RequestId = "request-" + Guid.NewGuid().ToString(),
    Requester = "john.doe@example.com",
    Item = "Vacation Request - 5 days",
    TimeoutHours = 24
};

string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
    nameof(ApprovalOrchestration), input);

Console.WriteLine($"Started approval workflow: {instanceId}");

// Simulate human approving the request
Console.WriteLine("Simulating approval...");
await Task.Delay(2000);

// Raise the approval event
var approvalResponse = new ApprovalResponseData
{
    IsApproved = true,
    Approver = "manager@example.com"
};

await client.RaiseEventAsync(instanceId, "approval_response", approvalResponse);

// Wait for completion
var result = await client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true);
Console.WriteLine($"Result: {result.ReadOutputAs<ApprovalResult>().Status}");

Дальнейшие действия

В этом примере показаны расширенные возможности Durable Functions, включая API WaitForExternalEvent и CreateTimer. В нем показано, как объединить Task.WhenAny (C#), context.df.Task.any (JavaScript и TypeScript) или context.task_any (Python) для реализации надежного шаблона времени ожидания для рабочих процессов, ожидающих реагирования пользователей. Узнайте больше о Durable Functions в серии статей, посвященных конкретным темам.

В этом примере показано, как использовать пакеты SDK для устойчивых задач для реализации рабочих процессов, ожидающих реагирования пользователей с настраиваемым временем ожидания. Основные понятия:

  • Внешние события: использование WaitForExternalEvent для ожидания входных данных

  • Устойчивые таймеры: использование CreateTimer для реализации времени ожидания

  • Гоночные задачи: использование WhenAny, when_any или anyOf для обработки той задачи, которая завершится первой

  • Durable Task JavaScript SDK на GitHub