在 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 应用显示语音命令请求的完成屏幕,以显示即将前往伦敦的行程。
语音命令是在 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 说出的文本。
进度屏幕
如果应用服务调用 ReportSuccessAsync 的时间超过 500 毫秒,Cortana 会为用户提供进度屏幕。 将显示应用图标,必须同时提供 GUI 和 TTS 进度字符串来表明正在积极处理任务。
Cortana 显示进度屏幕的时间最长为 5 秒。 5 秒后,Cortana 会向用户显示一条错误消息,并且应用服务将关闭。 如果应用服务需要 5 秒以上才能完成操作,它可以继续使用进度屏幕更新 Cortana 应用。
下面是 Adventure Works 应用的进度屏幕示例。 此示例中,用户取消了前往拉斯维加斯的行程。 进度屏幕包括为操作自定义的消息、图标和包含已取消行程信息的内容磁贴。
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 将停止侦听语音输入,并要求用户改为点击其中一个按钮。
确认屏幕包括为操作自定义的消息、图标和包含已取消行程信息的内容磁贴。
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 将停止侦听语音输入,并要求用户改为点击其中一个按钮。
消除歧义屏幕包括为操作自定义的消息、图标和包含已取消行程信息的内容磁贴。
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);