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

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

На высоком уровне шаблон работает следующим образом:

  1. Оркестратор вызывает активность, чтобы уведомить человека (отправить SMS-код, отправить подтверждающему письмо и так далее).
  2. Оркестратор запускает устойчивый таймер и одновременно ожидает внешнего события от человека.
  3. Если пользователь отвечает перед запуском таймера, оркестратор обрабатывает ответ.
  4. Если таймер срабатывает первым, оркестратор обрабатывает тайм-аут (например, отклоняя запрос).

Содержание этой статьи

Замечание

Точки поворота в пакете SDK для Устойчивые функции и Durable Task демонстрируют один и тот же шаблон в различных сценариях: Устойчивые функции использует пример проверки телефона через SMS, тогда как SDK для Durable Task используют пример рабочего процесса утверждения.

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

Замечание

Полные примеры кода доступны для C#, JavaScript и Python. Сэмпли PowerShell и Java в настоящее время недоступны.

Замечание

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Определение оркестратора

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

Изолированная рабочая модель
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;

public static partial class PhoneVerification
{
    [Function("E4_SmsPhoneVerification")]
    public static async Task<bool> Run(
        [OrchestrationTrigger] TaskOrchestrationContext context)
    {
        string phoneNumber = context.GetInput<string>()
            ?? 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.
        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)
            {
                if (challengeResponseTask.Result == challengeCode)
                {
                    authorized = true;
                    break;
                }
            }
            else
            {
                break;
            }
        }

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

        return authorized;
    }
}

Замечание

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


Модель внутрипроцессного процесса
[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 отправляет SMS-сообщение с четырёхзначным кодом пользователю.

Изолированная рабочая модель
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using Twilio;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;

public static partial class PhoneVerification
{
    [Function("E4_SendSmsChallenge")]
    public static async Task<int> SendSmsChallenge(
        [ActivityTrigger] string phoneNumber,
        FunctionContext executionContext)
    {
        ILogger logger = executionContext.GetLogger("E4_SendSmsChallenge");

        int challengeCode = Random.Shared.Next(10000);
        logger.LogInformation(
            "Sending verification code {ChallengeCode:0000} to {PhoneNumber}.",
            challengeCode,
            phoneNumber);

        string accountSid = Environment.GetEnvironmentVariable("TwilioAccountSid")
            ?? throw new InvalidOperationException("TwilioAccountSid is not configured.");
        string authToken = Environment.GetEnvironmentVariable("TwilioAuthToken")
            ?? throw new InvalidOperationException("TwilioAuthToken is not configured.");
        string fromNumber = Environment.GetEnvironmentVariable("TwilioPhoneNumber")
            ?? throw new InvalidOperationException("TwilioPhoneNumber is not configured.");

        TwilioClient.Init(accountSid, authToken);
        await MessageResource.CreateAsync(
            to: new PhoneNumber(phoneNumber),
            from: new PhoneNumber(fromNumber),
            body: $"Your verification code is {challengeCode:0000}");

        return challengeCode;
    }
}

Замечание

Чтобы запустить изолированный рабочий образец, установите Twilio пакет NuGet. Настройте TwilioAccountSidнастройки , TwilioAuthTokenи TwilioPhoneNumber приложения.


Модель внутрипроцессного процесса
[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-Type: application/json; charset=utf-8

{"id":"741c65651d4c40cea29acdd5bb47baf1",
 "sendEventPostUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/raiseEvent/{eventName}?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}",
 "statusQueryGetUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1?taskHub=...&code={systemKey}",
 "terminatePostUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/terminate?reason={text}&taskHub=...&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":"2026-04-23T19:10:49Z","lastUpdatedTime":"2026-04-23T19: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":"2026-04-23T19:20:49Z","lastUpdatedTime":"2026-04-23T19:22:23Z"}

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

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

    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}");

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

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

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

  • Внешние события: использование WaitForExternalEvent для ожидания входных данных
  • Устойчивые таймеры: использование CreateTimer для реализации времени ожидания
  • Гоночные задачи: использование WhenAny, when_any или anyOf для обработки той задачи, которая завершится первой