Vzor lidské interakce

Pattern lidské interakce popisuje pracovní postupy, které se pozastaví a čekají na vstup od člověka, než pokračují. Tento vzor je užitečný pro schvalovací pracovní postupy, vícefaktorovou autentizaci a případy, kdy osoba odpoví v časovém limitu.

Na vysoké úrovni vzor funguje takto:

  1. Orchestrátor zavolá aktivitu, aby upozornil osobu (poslat SMS kód, e-mail schvalovatele a podobně).
  2. Orchestrátor spustí trvalý časovač a současně čeká na externí událost od osoby.
  3. Pokud osoba odpoví před spuštěním časovače, orchestrátor zpracuje odpověď.
  4. Pokud se časovač spustí jako první, orchestrátor zpracuje vypršení časového limitu (například odmítnutím požadavku).

V tomto článku:

Poznámka:

Sady pivotů Durable Functions a Durable Task SDK ukazují stejný vzor s různými scénáři: Durable Functions používá příklad ověření telefonu pomocí SMS, zatímco sady SDK Durable Task používají příklad schvalovacího workflow.

Tato ukázka ukazuje, jak vytvořit orchestraci Durable Functions, která zahrnuje lidskou interakci. Příklad implementuje systém ověření telefonu založený na SMS. Ověřování telefonního čísla a více-faktorového ověřování (MFA) jsou běžné.

Poznámka:

Kompletní ukázky kódu jsou k dispozici pro C#, JavaScript a Python. PowerShell a Java ukázky momentálně nejsou dostupné.

Poznámka:

Verze 4 programovacího modelu Node.js pro Azure Functions je obecně dostupná. Model v4 je navržený tak, aby poskytoval flexibilnější a intuitivnější prostředí pro vývojáře v JavaScriptu a TypeScriptu. Další informace o rozdílech mezi v3 a v4 najdete v průvodci migrací.

V následujících fragmentech kódu JavaScript (PM4) označuje programovací model v4, nové prostředí.

Předpoklady

Tento článek ukazuje, jak implementovat model lidské interakce pomocí sad SDK Durable Task. Příklad implementuje pracovní postup schválení, ve kterém orchestrace čeká na osobu, aby schválila nebo odmítla žádost, než bude moci pokračovat.

Přehled scénáře lidské interakce

Ověření telefonu pomáhá ověřit, že uživatelé, kteří používají vaši aplikaci, nejsou spammery a že řídí telefonní číslo, které poskytují. Vícefaktorové ověřování je běžný způsob ochrany účtů. Vytvoření vlastního ověření telefonu vyžaduje stavovou interakci s osobou. Uživatel obvykle získá kód (například čtyřmístné číslo) a musí odpovědět v přiměřeném časovém intervalu.

Standardní Azure Functions jsou bezstavové (stejně jako mnoho dalších koncových bodů cloudu), takže tento typ interakce vyžaduje uložení stavu v databázi nebo jiném trvalém úložišti. Také rozdělíte interakci mezi několik funkcí a koordinujete je. Jedna funkce například vygeneruje kód, uloží ho a odešle ho do telefonu uživatele. Jiná funkce obdrží odpověď uživatele a namapuje ji na původní požadavek na ověření kódu. Přidejte časový limit, který pomáhá chránit zabezpečení. Tento pracovní postup se rychle zkompiluje.

Durable Functions snižuje složitost tohoto scénáře. V této ukázce spravuje funkce orchestrátoru stavovou interakci bez externího úložiště dat. Protože jsou funkce orchestrátoru odolné, jsou tyto interaktivní toky vysoce spolehlivé.

Pracovní postupy schválení jsou běžné v obchodních aplikacích, kde musí být žádost před pokračováním zkontrolována člověkem. Požadavky na pracovní postup jsou:

  • Čekání na lidskou odpověď po neomezenou dobu nebo na vypršení časového limitu
  • Zpracování výsledků schválení i zamítnutí
  • Vypršení časového limitu podpory při přijetí žádné odpovědi
  • Sledování stavu , aby žadatel mohl zkontrolovat průběh

Sady SDK trvalých úloh zjednodušují tento scénář pomocí následujících:

  • Externí události: Orchestrace může pozastavit a čekat na událost vyvolanou externím systémem nebo uživatelem.
  • Trvalé časovače: Nastavte časový limit, který se aktivuje, pokud není přijata žádná odpověď.
  • Vlastní stav: Sledovat a zpřístupnit aktuální stav pracovního postupu klientům

Konfigurujte integraci Twilio

Tato ukázka zahrnuje použití služby Twilio k odesílání sms zpráv na mobilní telefon. Azure Functions už podporuje Twilio prostřednictvím vazby Twilio a ukázka tuto funkci používá.

První věc, kterou potřebujete, je účet Twilio. Můžete vytvořit jednu zdarma na https://www.twilio.com/try-twilio. Jakmile budete mít účet, přidejte do aplikace funkcí následující tři nastavení aplikace.

Název nastavení aplikace Popis hodnoty
TwilioAccountSid Identifikátor SID pro váš účet Twilio
TwilioAuthToken Ověřovací token pro váš účet Twilio
TwilioPhoneNumber Telefonní číslo přidružené k vašemu účtu Twilio. Slouží k odesílání zpráv SMS.

Definování orchestrátoru

funkce orchestrátoru E4_SmsPhoneVerification

Izolovaný model pracovního procesu
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;
    }
}

Poznámka:

Tento orchestrátor je deterministický, protože CurrentUtcDateTime vrací stejnou hodnotu ve stejném bodě při každém opakování. Toto chování zajišťuje, že winner je stejné pro každé opakované volání Task.WhenAny.


Model v probíhajícím procesu
[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;
    }
}

Poznámka:

Nemusí to být zpočátku zřejmé, ale tento orchestrátor neporušuje deterministické omezení orchestrace. Je deterministický, protože CurrentUtcDateTime vlastnost vypočítá čas vypršení platnosti časovače a vrátí stejnou hodnotu při každém přehrání v tomto okamžiku v kódu orchestrátoru. Toto chování zajišťuje, že winner je stejné pro každé opakované volání Task.WhenAny.


Po spuštění tato funkce orchestrátoru provádí následující kroky:

  1. Získá telefonní číslo pro odeslání SMS oznámení.
  2. Volání E4_SendSmsChallenge slouží k odeslání zprávy SMS uživateli a vrací očekávaný čtyřmístný ověřovací kód.
  3. Vytvoří trvalý časovač, který se aktivuje 90 sekund po aktuálním čase.
  4. Souběžně s časovačem čeká na událost SmsChallengeResponse od uživatele.

Uživatel obdrží zprávu SMS se čtyřmístným kódem. Mají 90 sekund, aby odeslali stejný kód instanci orchestrátoru, aby dokončili ověření. Pokud odesílají nesprávný kód, získají tři další pokusy ve stejném 90sekundovém okně.

Výstraha

Zrušte časovače , které už nepotřebujete. V předchozím příkladu orchestrace ruší časovač, když přijme odpověď na výzvu.

Orchestrátor odešle žádost o schválení a pak čeká na lidskou odpověď nebo časový limit.

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

Tento orchestrátor provádí následující akce:

  1. Odešle žádost o schválení zavoláním aktivity, která schvalovatele upozorní.
  2. Nastaví vlastní stav, aby klienti mohli sledovat průběh.
  3. Vytvoří trvalý časovač pro konečný časový limit.
  4. Čeká na externí událost (approval_response), kterou schvalovatel vyvolá.
  5. Používá WhenAny, when_any nebo anyOf, aby čekal na to, co se dokončí jako první: schválení nebo vypršení časového limitu.
  6. Zpracuje výsledek podle toho, který úkol je dokončen.

Výstraha

Zrušte časovače , které už nepotřebujete. V příkladu jazyka C# orchestrace zruší časovač časového limitu, když obdrží schválení.

Definování aktivit

funkce aktivity E4_SendSmsChallenge

Funkce E4_SendSmsChallenge odešle uživateli SMS zprávu obsahující čtyřmístný kód.

Izolovaný model pracovního procesu
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;
    }
}

Poznámka:

Pro spuštění izolovaného worker vzorku nainstalujte Twilio balíček NuGet. Nastavte TwilioAccountSidnastavení , TwilioAuthToken, a TwilioPhoneNumber aplikace.


Model v probíhajícím procesu
[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;
}

Poznámka:

Pokud chcete spustit ukázku, nainstalujte balíček NuGet Microsoft.Azure.WebJobs.Extensions.Twilio. Neinstalujte hlavní balíček NuGet Twilio , protože může způsobit konflikty verzí a chyby sestavení.


Aktivity odesílají žádost o schválení a zpracovávají odpověď.

Aktivita odeslání žádosti o schválení

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

Aktivita schválení procesu

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

Spusťte ukázku lidské interakce

Pomocí funkcí aktivovaných protokolem HTTP v ukázce spusťte orchestraci odesláním následujícího požadavku 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}"}

Funkce orchestrátoru obdrží telefonní číslo a okamžitě odešle sms zprávu na toto číslo s náhodně vygenerovaným 4místným ověřovacím kódem , 2168například . Funkce pak počká 90 sekund na odpověď.

Pokud chcete odpovědět s kódem, použijte RaiseEventAsync (.NET) nebo raiseEvent (JavaScript a TypeScript) v jiné funkci nebo v odpovědi 202 zavolejte sendEventPostUri koncový bod HTTP POST. Nahraďte {eventName} za 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

Pokud událost odešlete před vypršením platnosti časovače, orchestrace se dokončí a output pole se nastaví na truehodnotu , která označuje úspěšné ověření.

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

Pokud časovač vyprší nebo čtyřikrát zadáte nesprávný kód, zkontrolujte stav, aby byl output nastaven na false, což značí, že ověření telefonu selhalo.

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

Chcete-li spustit ukázku:

  1. Spusťte emulátor plánovače úloh Durable pro místní vývoj. Musí být nainstalovaný Docker.

    docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest
    
  2. Spusťte pracovní proces ke zaregistrování orchestrátoru a aktivit.

  3. Spuštěním klienta naplánujte pracovní postup schválení a odešlete události.

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

Další kroky

Tato ukázka ukazuje pokročilé možnosti Durable Functions, včetně rozhraní API WaitForExternalEvent a CreateTimer. Ukazuje, jak kombinovat Task.WhenAny (C#), context.df.Task.any (JavaScript a TypeScript) nebo context.task_any (Python) a implementovat spolehlivý vzor časového limitu pro pracovní postupy, které čekají na reakce lidí.

Tato ukázka ukazuje, jak pomocí sad SDK durable task implementovat pracovní postupy, které čekají na reakci lidí s konfigurovatelnými časovými limity. Klíčové koncepty:

  • Externí události: Použití WaitForExternalEvent k čekání na vstup
  • Trvalé časovače: Použití CreateTimer k implementaci časových limitů
  • Závodní úkoly: Použití WhenAny, when_any nebo anyOf k vyřízení úkolu, který se dokončí jako první