练习 - 从Microsoft Entra受保护的 API 返回产品数据

已完成

在本练习中,将更新消息扩展以从自定义 API 检索数据。 基于用户查询从自定义 API 获取数据,并将搜索结果中的数据返回给用户。

Microsoft Teams 中基于搜索的邮件扩展返回的搜索结果的屏幕截图。

安装和配置开发代理

在本练习中,你将使用开发代理,这是一种可以模拟 API 的命令行工具。 当你想要测试应用而无需创建实际 API 时,此功能非常有用。

若要完成本练习,需要安装 最新版本的开发代理 并下载此模块的开发代理预设。

预设使用内存中数据存储(受Microsoft Entra保护)模拟 CRUD (创建、读取、更新、删除) API。 这意味着你可以像在调用需要身份验证的真实 API 一样测试应用。

若要下载预设,请在终端中运行以下命令:

devproxy preset get learn-copilot-me-plugin

获取用户查询值

创建一个方法,该方法按参数的名称获取用户查询值。

在 Visual Studio 和 ProductsPlugin 项目中:

  1. “帮助程序” 文件夹中,创建名为 MessageExtensionHelpers.cs

  2. 在 文件中,添加以下代码:

    using Microsoft.Bot.Schema.Teams;
    
    internal class MessageExtensionHelpers
    {
        internal static string GetQueryParameterValueByName(IList<MessagingExtensionParameter> parameters, string name) => parameters.FirstOrDefault(p => p.Name == name)?.Value as string ?? string.Empty;
    }
    
  3. Save your changes

接下来,更新 OnTeamsMessagingExtensionQueryAsync 方法以使用新的帮助程序方法。

  1. “搜索 ”文件夹中,打开 SearchApp.cs

  2. OnTeamsMessagingExtensionQueryAsync 方法中,替换以下代码:

    var text = query?.Parameters?[0]?.Value as string ?? string.Empty;
    

    var text = MessageExtensionHelpers.GetQueryParameterValueByName(query.Parameters, "ProductName");
    
  3. 将光标移动到 文本 变量,使用 Ctrl + RCtrl + R并将变量重命名 为名称

  4. Save your changes

OnTeamsMessagingExtensionQueryAsync 方法现在应如下所示:

protected override async Task<MessagingExtensionResponse> OnTeamsMessagingExtensionQueryAsync(ITurnContext<IInvokeActivity> turnContext, MessagingExtensionQuery query, CancellationToken cancellationToken)
{
    var userTokenClient = turnContext.TurnState.Get<UserTokenClient>();
    var tokenResponse = await AuthHelpers.GetToken(userTokenClient, query.State, turnContext.Activity.From.Id, turnContext.Activity.ChannelId, connectionName, cancellationToken);

    if (!AuthHelpers.HasToken(tokenResponse))
    {
        return await AuthHelpers.CreateAuthResponse(userTokenClient, connectionName, (Activity)turnContext.Activity, cancellationToken);
    }

    var name = MessageExtensionHelpers.GetQueryParameterValueByName(query.Parameters, "ProductName");

    var card = await File.ReadAllTextAsync(Path.Combine(".", "Resources", "card.json"), cancellationToken);
    var template = new AdaptiveCardTemplate(card);

    return new MessagingExtensionResponse
    {
        ComposeExtension = new MessagingExtensionResult
        {
            Type = "result",
            AttachmentLayout = "list",
            Attachments = [
                new MessagingExtensionAttachment
                    {
                        ContentType = AdaptiveCard.ContentType,
                        Content = JsonConvert.DeserializeObject(template.Expand(new { title = name })),
                        Preview = new ThumbnailCard { Title = name }.ToAttachment()
                    }
            ]
        }
    };
}

从自定义 API 获取数据

若要从自定义 API 获取数据,需要在请求的 Authorization 标头中发送访问令牌,并将响应反序列化到表示产品数据的模型中。

首先,创建一个表示从自定义 API 返回的产品数据的模型。

在 Visual Studio 和 ProductsPlugin 项目中:

  1. 创建名为 Models 的文件夹

  2. “模型”文件夹中,创建名为 Product.cs 的新文件

  3. 在 文件中,添加以下代码:

    using System.Text.Json.Serialization;
    
    internal class Product
    {
        [JsonPropertyName("productId")]
        public int Id { get; set; }
        [JsonPropertyName("imageUrl")]
        public string ImageUrl { get; set; }
        [JsonPropertyName("name")]
        public string Name { get; set; }
        [JsonPropertyName("category")]
        public string Category { get; set; }
        [JsonPropertyName("callVolume")]
        public int CallVolume { get; set; }
        [JsonPropertyName("releaseDate")]
        public string ReleaseDate { get; set; }
    }
    
  4. Save your changes

接下来,创建一个服务类,用于从自定义 API 检索产品数据。

  1. 创建名为“服务”的文件夹

  2. “服务” 文件夹中,创建一个名为 ProductService.cs

  3. 在 文件中,添加以下代码:

    using System.Net.Http.Headers;
    
    internal class ProductsService
    {
        private readonly HttpClient _httpClient;
        private readonly string _baseUri = "https://api.contoso.com/v1/";
    
        internal ProductsService(string token)
        {
            _httpClient = new HttpClient();
            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        }
    
        internal async Task<Product[]> GetProductsByNameAsync(string name)
        {
            var response = await _httpClient.GetAsync($"{_baseUri}products?name={name}");
            response.EnsureSuccessStatusCode();
            var jsonString = await response.Content.ReadAsStringAsync();
            return System.Text.Json.JsonSerializer.Deserialize<Product[]>(jsonString);
        }
    }
    
  4. Save your changes

ProductsService 类包含从自定义 API 获取产品数据的方法。 类构造函数采用访问令牌作为参数,并使用 Authorization 标头中的访问令牌设置 HttpClient 实例。

接下来,更新 OnTeamsMessagingExtensionQueryAsync 方法,以使用 ProductsService 类从自定义 API 获取产品数据。

  1. “搜索 ”文件夹中,打开 SearchApp.cs

  2. OnTeamsMessagingExtensionQueryAsync 方法中,在 name 变量声明后添加以下代码,以从自定义 API 获取产品数据:

    var productService = new ProductsService(tokenResponse.Token);
    var products = await productService.GetProductsByNameAsync(name);
    
  3. Save your changes

创建搜索结果

现在,你已拥有产品数据,可以将其包含在返回给用户的搜索结果中。

首先,让我们更新现有的自适应卡片模板以显示产品信息。

在 Visual Studio 和 ProductsPlugin 项目中继续操作:

  1. “资源”文件夹中,将卡.json重命名为Product.json

  2. “资源” 文件夹中,创建一个名为 Product.data.json 的新文件。 此文件包含 Visual Studio 用于生成自适应卡片模板预览的示例数据。

  3. 在 文件中,添加以下 JSON:

    {
      "callVolume": 36,
      "category": "Enterprise",
      "imageUrl": "https://raw.githubusercontent.com/SharePoint/sp-dev-provisioning-templates/master/tenant/productsupport/source/Product%20Imagery/Contoso4.png",
      "name": "Contoso Quad",
      "productId": 1,
      "releaseDate": "2019-02-09"
    }
    
  4. Save your changes

  5. “资源” 文件夹中,打开 Product.json

  6. 在 文件中,将内容替换为以下 JSON:

    {
      "type": "AdaptiveCard",
      "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
      "version": "1.5",
      "body": [
        {
          "type": "TextBlock",
          "text": "${name}",
          "wrap": true,
          "style": "heading"
        },
        {
          "type": "TextBlock",
          "text": "${category}",
          "wrap": true
        },
        {
          "type": "Container",
          "items": [
            {
              "type": "Image",
              "url": "${imageUrl}",
              "altText": "${name}"
            }
          ],
          "minHeight": "350px",
          "verticalContentAlignment": "Center",
          "horizontalAlignment": "Center"
        },
        {
          "type": "FactSet",
          "facts": [
            {
              "title": "Call Volume",
              "value": "${formatNumber(callVolume,0)}"
            },
            {
              "title": "Release Date",
              "value": "${formatDateTime(releaseDate,'dd/MM/yyyy')}"
            }
          ]
        }
      ]
    }
    
  7. Save your changes

自适应卡片模板使用绑定表达式来显示产品信息。 ${name}${category}${imageUrl}${callVolume}${releaseDate} 表达式将替换为产品数据中的相应值。 formatNumberformatDateTime 模板函数用于将 callVolumereleaseDate 值分别设置为数字和日期。

请花点时间了解 Visual Studio 中的自适应卡片预览。 预览显示将产品数据绑定到模板时自适应卡片模板的外观。 它使用 Product.data.json 文件中的示例数据来生成预览。

接下来,更新应用清单中的 validDomains 属性以包含 raw.githubusercontent.com 域,以便自适应卡片模板中的图像可以在 Microsoft Teams 中显示。

在 TeamsApp 项目中:

  1. appPackage 文件夹中,打开 manifest.json

  2. 在 文件中,将 GitHub 域添加到 validDomains 属性:

      "validDomains": [
        "token.botframework.com",
        "raw.githubusercontent.com",
        "${{BOT_DOMAIN}}"
      ],
    
  3. Save your changes

接下来,更新 OnTeamsMessagingExtensionQueryAsync 方法,以创建包含产品信息的附件列表。

在 ProductsPlugin 项目中:

  1. “搜索 ”文件夹中,打开 SearchApp.cs

  2. 卡.json更新为 Product.json,以反映文件名中的更改。 替换以下代码:

    var card = await File.ReadAllTextAsync(Path.Combine(".", "Resources", "card.json"), cancellationToken);
    

    var card = await File.ReadAllTextAsync(Path.Combine(".", "Resources", "Product.json"), cancellationToken);
    
  3. 模板 变量声明后添加以下代码,以创建附件列表:

     var attachments = products.Select(product =>
     {
         var content = template.Expand(product);
    
         return new MessagingExtensionAttachment
         {
             ContentType = AdaptiveCard.ContentType,
             Content = JsonConvert.DeserializeObject(content),
             Preview = new ThumbnailCard
             {
                 Title = product.Name,
                 Subtitle = product.Category,
                 Images = [new() { Url = product.ImageUrl }]
             }.ToAttachment()
         };
     }).ToList();
    
  4. 更新 return 语句以包含 attachments 变量:

    return new MessagingExtensionResponse
    {
        ComposeExtension = new MessagingExtensionResult
        {
            Type = "result",
            AttachmentLayout = "list",
            Attachments = attachments
        }
    };
    
  5. 保存更改

创建和更新资源

一切准备就绪后,运行 准备 Teams 应用依赖项 过程以创建新资源并更新现有资源。

在 Visual Studio 中继续:

  1. 解决方案资源管理器中,右键单击 TeamsApp 项目
  2. 展开“Teams 工具包”菜单,选择“准备 Teams 应用依赖项
  3. “Microsoft 365 帐户”对话框中,选择“继续
  4. 在“预配”对话框中,选择“预配
  5. “Teams 工具包警告”对话框中,选择“预配
  6. “Teams 工具包信息 ”对话框中,选择交叉图标以关闭对话框

运行和调试

预配资源后,启动调试会话以测试消息扩展。

首先,启动开发代理以模拟自定义 API。

  1. 打开终端窗口

  2. 运行以下命令以启动开发代理:

    devproxy --config-file "~appFolder/presets/learn-copilot-me-plugin/products-api-config.json"
    
  3. 如果出现提示,请接受证书警告

注意

当开发代理运行时,它充当系统范围的代理。

接下来,在 Visual Studio 中启动调试会话:

  1. 若要启动新的调试会话,请按 F5 或从工具栏中选择“ 启动

  2. 等待浏览器窗口打开,应用安装对话框显示在 Microsoft Teams Web 客户端中。 如果出现提示,请输入Microsoft 365 帐户凭据。

  3. 在应用安装对话框中,选择 “添加”

  4. 打开新的或现有的Microsoft Teams 聊天

  5. 在邮件撰写区域中,选择 + 以打开应用选取器

  6. 在应用列表中,选择 “Contoso 产品 ”以打开消息扩展

  7. 在文本框中,输入 mark8

  8. 等待搜索完成并显示结果

    Microsoft Teams 中基于搜索的邮件扩展返回的搜索结果的屏幕截图。

  9. 在结果列表中,选择搜索结果以将卡嵌入撰写邮件框中

返回到 Visual Studio 并从工具栏中选择“ 停止 ”,或按 Shift + F5 停止调试会话。 此外,使用 Ctrl + C 关闭开发代理。