在 Cortana 中与后台应用交互

警告

自 Windows 10 2020 年 5 月更新(版本 2004,codename“20H1”)起,不再支持此功能。

在执行语音命令时,通过 Cortana 画布中的语音和文本输入使用户与后台应用交互

Cortana 支持应用的完整分步工作流。 此工作流由应用定义,可支持如下功能:

  • 成功完成
  • 转交
  • 进度
  • 确认
  • 消除歧义
  • 错误

撰写反馈字符串

提示

必备条件

如果你还不熟悉通用 Windows 平台 (UWP) 应用开发,请查看这些主题来熟悉此处讨论的技术。

用户体验指南

有关如何将你的应用与 Cortana 和语音交互集成的信息,请参阅 Cortana 设计指南,了解有关设计有用且具有吸引力的支持语音的应用的有用提示。

撰写由 Cortana 显示和说出的反馈字符串

Cortana 设计指南提供了有关为 Cortana 撰写字符串的建议

内容卡可为用户提供其他上下文,并帮助保持反馈字符串简洁。

Cortana 支持以下内容卡模板(完成屏幕上只能使用一个模板)

  • 仅标题
  • 最多包含三行文本的标题
  • 带图像的标题
  • 最多包含三行文本的带图像标题

图像可以是以下尺寸:

  • 68w x 68h
  • 68w x 92h
  • 280w x 140h

还可以让用户单击卡片或指向应用的文本链接,在前台启用应用。

完成屏幕

完成屏幕为用户提供有关已完成语音命令任务的信息。

如果应用响应任务的时间少于 500 毫秒,并且无需用户提供额外的信息,则无需与 Cortana 进一步交互即可完成任务。 Cortana 会简单地显示完成屏幕。

此处,我们使用 Adventure Works 应用显示语音命令请求的完成屏幕,以显示即将前往伦敦的行程

即将开始的行程的 Cortana 后台应用完成的屏幕截图

语音命令是在 AdventureWorksCommands.xml 中定义的:

<Command Name="whenIsTripToDestination">
  <Example> When is my trip to Las Vegas?</Example>
  <ListenFor RequireAppName="BeforeOrAfterPhrase"> when is [my] trip to {destination}</ListenFor>
  <ListenFor RequireAppName="ExplicitlySpecified"> when is [my] {builtin:AppName} trip to {destination} </ListenFor>
  <Feedback> Looking for trip to {destination}</Feedback>
  <VoiceCommandService Target="AdventureWorksVoiceCommandService"/>
</Command>

AdventureWorksVoiceCommandService.cs 包含完成消息方法:

/// <summary>
/// Show details for a single trip, if the trip can be found. 
/// This demonstrates a simple response flow in Cortana.
/// </summary>
/// <param name="destination">The destination, expected to be in the phrase list.</param>
private async Task SendCompletionMessageForDestination(string destination)
{
    // If this operation is expected to take longer than 0.5 seconds, the task must
    // supply a progress response to Cortana before starting the operation, and
    // updates must be provided at least every 5 seconds.
    string loadingTripToDestination = string.Format(
               cortanaResourceMap.GetValue("LoadingTripToDestination", cortanaContext).ValueAsString,
               destination);
    await ShowProgressScreen(loadingTripToDestination);
    Model.TripStore store = new Model.TripStore();
    await store.LoadTrips();

    // Query for the specified trip. 
    // The destination should be in the phrase list. However, there might be  
    // multiple trips to the destination. We pick the first.
    IEnumerable<Model.Trip> trips = store.Trips.Where(p => p.Destination == destination);

    var userMessage = new VoiceCommandUserMessage();
    var destinationsContentTiles = new List<VoiceCommandContentTile>();
    if (trips.Count() == 0)
    {
        string foundNoTripToDestination = string.Format(
               cortanaResourceMap.GetValue("FoundNoTripToDestination", cortanaContext).ValueAsString,
               destination);
        userMessage.DisplayMessage = foundNoTripToDestination;
        userMessage.SpokenMessage = foundNoTripToDestination;
    }
    else
    {
        // Set plural or singular title.
        string message = "";
        if (trips.Count() > 1)
        {
            message = cortanaResourceMap.GetValue("PluralUpcomingTrips", cortanaContext).ValueAsString;
        }
        else
        {
            message = cortanaResourceMap.GetValue("SingularUpcomingTrip", cortanaContext).ValueAsString;
        }
        userMessage.DisplayMessage = message;
        userMessage.SpokenMessage = message;

        // Define a tile for each destination.
        foreach (Model.Trip trip in trips)
        {
            int i = 1;
            
            var destinationTile = new VoiceCommandContentTile();

            destinationTile.ContentTileType = VoiceCommandContentTileType.TitleWith68x68IconAndText;
            destinationTile.Image = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///AdventureWorks.VoiceCommands/Images/GreyTile.png"));

            destinationTile.AppLaunchArgument = trip.Destination;
            destinationTile.Title = trip.Destination;
            if (trip.StartDate != null)
            {
                destinationTile.TextLine1 = trip.StartDate.Value.ToString(dateFormatInfo.LongDatePattern);
            }
            else
            {
                destinationTile.TextLine1 = trip.Destination + " " + i;
            }

            destinationsContentTiles.Add(destinationTile);
            i++;
        }
    }

    var response = VoiceCommandResponse.CreateResponse(userMessage, destinationsContentTiles);

    if (trips.Count() > 0)
    {
        response.AppLaunchArgument = destination;
    }

    await voiceServiceConnection.ReportSuccessAsync(response);
}

转交屏幕

识别语音命令后,Cortana 必须调用 ReportSuccessAsync,并大约在 500 毫秒内提供反馈。如果应用服务无法在 500 毫秒内完成语音命令指定的操作,Cortana 将显示一个转交屏幕,直到应用调用 ReportSuccessAsync 或等待最多 5 秒

如果应用服务未调用 ReportSuccessAsync 或任何其他 VoiceCommandServiceConnection 方法,则用户会收到错误消息,并取消应用服务调用。

下面是 Adventure Works 应用的转交屏幕示例。 此示例中,用户查询了 Cortana,想要了解即将开始的行程。 转交屏幕包含使用应用服务名称自定义的消息、图标和反馈字符串自定义的消息

[!注意] 可以在 VCD 文件中声明一个反馈字符串。 此字符串不会影响 Cortana 画布上显示的 UI 文本,它只会影响 Cortana 说出的文本

Cortana 背景应用切换屏幕的屏幕截图

进度屏幕

如果应用服务调用 ReportSuccessAsync 的时间超过 500 毫秒,Cortana 会为用户提供进度屏幕。 将显示应用图标,必须同时提供 GUI 和 TTS 进度字符串来表明正在积极处理任务。

Cortana 显示进度屏幕的时间最长为 5 秒。 5 秒后,Cortana 会向用户显示一条错误消息,并且应用服务将关闭。 如果应用服务需要 5 秒以上才能完成操作,它可以继续使用进度屏幕更新 Cortana 应用

下面是 Adventure Works 应用的进度屏幕示例。 此示例中,用户取消了前往拉斯维加斯的行程。 进度屏幕包括为操作自定义的消息、图标和包含已取消行程信息的内容磁贴。

带后台应用进度屏幕的 Cortana 屏幕截图

AdventureWorksVoiceCommandService.cs 包含以下进度消息方法,该方法调用 ReportProgressAsync 以显示 Cortana 中的进度屏幕

/// <summary>
/// Show a progress screen. These should be posted at least every 5 seconds for a 
/// long-running operation.
/// </summary>
/// <param name="message">The message to display, relating to the task being performed.</param>
/// <returns></returns>
private async Task ShowProgressScreen(string message)
{
    var userProgressMessage = new VoiceCommandUserMessage();
    userProgressMessage.DisplayMessage = userProgressMessage.SpokenMessage = message;

    VoiceCommandResponse response = VoiceCommandResponse.CreateResponse(userProgressMessage);
    await voiceServiceConnection.ReportProgressAsync(response);
}

确认屏幕

当语音命令指定的操作不可逆、具有显著影响或识别可信度不高时,应用服务可以请求确认。

下面是 Adventure Works 应用的确认屏幕示例。 此示例中,用户已指示应用服务通过 Cortana 取消前往拉斯维加斯的行程。 应用服务为 Cortana 提供了一个确认屏幕,提示用户在取消行程之前提供“是”或“否”答案

如果用户提供“是”或“否”以外的回答,Cortana 将无法确定问题的答案。 在这种情况下,Cortana 会向用户提示应用服务提供的类似问题

第二次提示时,如果用户仍然不说“是”或“否”, Cortana 会第三次提示用户,其前缀为道歉。 如果用户仍然不说“是”或“否”, Cortana 将停止侦听语音输入,并要求用户改为点击其中一个按钮。

确认屏幕包括为操作自定义的消息、图标和包含已取消行程信息的内容磁贴。

带后台应用确认屏幕的 Cortana 屏幕截图

AdventureWorksVoiceCommandService.cs 包含以下行程取消方法,该方法调用 RequestConfirmationAsync在 Cortana 中显示确认屏幕

/// <summary>
/// Handle the Trip Cancellation task. This task demonstrates how to prompt a user
/// for confirmation of an operation, show users a progress screen while performing
/// a long-running task, and show a completion screen.
/// </summary>
/// <param name="destination">The name of a destination.</param>
/// <returns></returns>
private async Task SendCompletionMessageForCancellation(string destination)
{
    // Begin loading data to search for the target store. 
    // Consider inserting a progress screen here, in order to prevent Cortana from timing out. 
    string progressScreenString = string.Format(
        cortanaResourceMap.GetValue("ProgressLookingForTripToDest", cortanaContext).ValueAsString,
        destination);
    await ShowProgressScreen(progressScreenString);

    Model.TripStore store = new Model.TripStore();
    await store.LoadTrips();

    IEnumerable<Model.Trip> trips = store.Trips.Where(p => p.Destination == destination);
    Model.Trip trip = null;
    if (trips.Count() > 1)
    {
        // If there is more than one trip, provide a disambiguation screen.
        // However, if a significant number of items are returned, you might want to 
        // just display a link to your app and provide a deeper search experience.
        string disambiguationDestinationString = string.Format(
            cortanaResourceMap.GetValue("DisambiguationWhichTripToDest", cortanaContext).ValueAsString,
            destination);
        string disambiguationRepeatString = cortanaResourceMap.GetValue("DisambiguationRepeat", cortanaContext).ValueAsString;
        trip = await DisambiguateTrips(trips, disambiguationDestinationString, disambiguationRepeatString);
    }
    else
    {
        trip = trips.FirstOrDefault();
    }

    var userPrompt = new VoiceCommandUserMessage();
    
    VoiceCommandResponse response;
    if (trip == null)
    {
        var userMessage = new VoiceCommandUserMessage();
        string noSuchTripToDestination = string.Format(
            cortanaResourceMap.GetValue("NoSuchTripToDestination", cortanaContext).ValueAsString,
            destination);
        userMessage.DisplayMessage = userMessage.SpokenMessage = noSuchTripToDestination;

        response = VoiceCommandResponse.CreateResponse(userMessage);
        await voiceServiceConnection.ReportSuccessAsync(response);
    }
    else
    {
        // Prompt the user for confirmation that this is the correct trip to cancel.
        string cancelTripToDestination = string.Format(
            cortanaResourceMap.GetValue("CancelTripToDestination", cortanaContext).ValueAsString,
            destination);
        userPrompt.DisplayMessage = userPrompt.SpokenMessage = cancelTripToDestination;
        var userReprompt = new VoiceCommandUserMessage();
        string confirmCancelTripToDestination = string.Format(
            cortanaResourceMap.GetValue("ConfirmCancelTripToDestination", cortanaContext).ValueAsString,
            destination);
        userReprompt.DisplayMessage = userReprompt.SpokenMessage = confirmCancelTripToDestination;
        
        response = VoiceCommandResponse.CreateResponseForPrompt(userPrompt, userReprompt);

        var voiceCommandConfirmation = await voiceServiceConnection.RequestConfirmationAsync(response);

        // If RequestConfirmationAsync returns null, Cortana has likely been dismissed.
        if (voiceCommandConfirmation != null)
        {
            if (voiceCommandConfirmation.Confirmed == true)
            {
                string cancellingTripToDestination = string.Format(
               cortanaResourceMap.GetValue("CancellingTripToDestination", cortanaContext).ValueAsString,
               destination);
                await ShowProgressScreen(cancellingTripToDestination);

                // Perform the operation to remove the trip from app data. 
                // As the background task runs within the app package of the installed app,
                // we can access local files belonging to the app without issue.
                await store.DeleteTrip(trip);

                // Provide a completion message to the user.
                var userMessage = new VoiceCommandUserMessage();
                string cancelledTripToDestination = string.Format(
                    cortanaResourceMap.GetValue("CancelledTripToDestination", cortanaContext).ValueAsString,
                    destination);
                userMessage.DisplayMessage = userMessage.SpokenMessage = cancelledTripToDestination;
                response = VoiceCommandResponse.CreateResponse(userMessage);
                await voiceServiceConnection.ReportSuccessAsync(response);
            }
            else
            {
                // Confirm no action for the user.
                var userMessage = new VoiceCommandUserMessage();
                string keepingTripToDestination = string.Format(
                    cortanaResourceMap.GetValue("KeepingTripToDestination", cortanaContext).ValueAsString,
                    destination);
                userMessage.DisplayMessage = userMessage.SpokenMessage = keepingTripToDestination;

                response = VoiceCommandResponse.CreateResponse(userMessage);
                await voiceServiceConnection.ReportSuccessAsync(response);
            }
        }
    }
}

消除歧义屏幕

语音命令指定的操作具有多个可能的结果时,应用服务可以要求用户提供更多信息。

下面是 Adventure Works 应用的消除歧义屏幕示例。 此示例中,用户已指示应用服务通过 Cortana 取消前往拉斯维加斯的行程。 但是,用户在不同日期有两次前往拉斯维加斯的行程,如果用户未选择所需的行程,应用服务无法完成操作。

应用服务为 Cortana 提供了消除歧义屏幕,提示用户在取消任何行程之前从匹配行程列表中进行选择

在这种情况下,Cortana 会向用户提示应用服务提供的类似问题

第二次提示时,如果用户仍然没有说出可用于识别所选内容的内容, Cortana 会第三次提示用户,其前缀为道歉。 如果用户仍然没有说出可用于识别所选内容的内容, Cortana 将停止侦听语音输入,并要求用户改为点击其中一个按钮。

消除歧义屏幕包括为操作自定义的消息、图标和包含已取消行程信息的内容磁贴。

带背景应用消除歧义屏幕的 Cortana 屏幕截图

AdventureWorksVoiceCommandService.cs 包含以下行程取消方法,该方法调用 RequestDisambiguationAsync在 Cortana 中显示消除歧义屏幕

/// <summary>
/// Provide the user with a way to identify which trip to cancel. 
/// </summary>
/// <param name="trips">The set of trips</param>
/// <param name="disambiguationMessage">The initial disambiguation message</param>
/// <param name="secondDisambiguationMessage">Repeat prompt retry message</param>
private async Task<Model.Trip> DisambiguateTrips(IEnumerable<Model.Trip> trips, string disambiguationMessage, string secondDisambiguationMessage)
{
    // Create the first prompt message.
    var userPrompt = new VoiceCommandUserMessage();
    userPrompt.DisplayMessage =
        userPrompt.SpokenMessage = disambiguationMessage;

    // Create a re-prompt message if the user responds with an out-of-grammar response.
    var userReprompt = new VoiceCommandUserMessage();
    userReprompt.DisplayMessage =
        userReprompt.SpokenMessage = secondDisambiguationMessage;

    // Create card for each item. 
    var destinationContentTiles = new List<VoiceCommandContentTile>();
    int i = 1;
    foreach (Model.Trip trip in trips)
    {
        var destinationTile = new VoiceCommandContentTile();

        destinationTile.ContentTileType = VoiceCommandContentTileType.TitleWith68x68IconAndText;
        destinationTile.Image = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///AdventureWorks.VoiceCommands/Images/GreyTile.png"));
        
        // The AppContext can be any arbitrary object.
        destinationTile.AppContext = trip;
        string dateFormat = "";
        if (trip.StartDate != null)
        {
            dateFormat = trip.StartDate.Value.ToString(dateFormatInfo.LongDatePattern);
        }
        else
        {
            // The app allows a trip to have no date.
            // However, the choices must be unique so they can be distinguished.
            // Here, we add a number to identify them.
            dateFormat = string.Format("{0}", i);
        } 

        destinationTile.Title = trip.Destination + " " + dateFormat;
        destinationTile.TextLine1 = trip.Description;

        destinationContentTiles.Add(destinationTile);
        i++;
    }

    // Cortana handles re-prompting if no valid response.
    var response = VoiceCommandResponse.CreateResponseForPrompt(userPrompt, userReprompt, destinationContentTiles);

    // If cortana is dismissed in this operation, null is returned.
    var voiceCommandDisambiguationResult = await
        voiceServiceConnection.RequestDisambiguationAsync(response);
    if (voiceCommandDisambiguationResult != null)
    {
        return (Model.Trip)voiceCommandDisambiguationResult.SelectedItem.AppContext;
    }

    return null;
}

错误屏幕

语音命令指定的操作无法完成时,应用服务可以提供错误屏幕。

下面是 Adventure Works 应用的错误屏幕示例。 此示例中,用户已指示应用服务通过 Cortana 取消前往拉斯维加斯的行程。 但是,用户没有计划前往拉斯维加斯的任何行程。

应用服务会向 Cortana 提供一个错误屏幕,其中包括为操作自定义的消息、图标和特定错误消息

调用 ReportFailureAsync 以在 Cortana 中显示错误消息

var userMessage = new VoiceCommandUserMessage();
    userMessage.DisplayMessage = userMessage.SpokenMessage = 
      "Sorry, you don't have any trips to Las Vegas";
                
    var response = VoiceCommandResponse.CreateResponse(userMessage);

    response.AppLaunchArgument = "showUpcomingTrips";
    await voiceServiceConnection.ReportFailureAsync(response);