你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

教程:在聊天应用中启用文件附件支持

聊天 SDK 在会议上下文中与 Microsoft Teams 无缝配合工作。 只有 Teams 用户才能将文件附件发送给 Azure 通信服务用户。 Azure 通信服务用户无法向 Teams 用户发送文件附件。 有关当前功能,请参阅 Teams 互操作聊天

添加文件附件支持

聊天 SDK 为每个文件附件提供 previewUrl 属性。 尤其是,previewUrl 链接到 SharePoint 上的网页,用户可在该网页上查看文件内容、编辑文件并下载文件(如果权限允许)。

此功能有一些约束条件:

  • 发件人租户的 Teams 管理员可以实施完全限制或禁用此功能的策略。 例如,Teams 管理员可以禁用某些权限(例如 Anyone),这会导致文件附件 URL (previewUrl) 不可访问。

  • 我们目前仅支持两种文件权限:

    • Anyone
    • People you choose(使用电子邮件地址选择)

    让 Teams 用户知道,所有其他权限(例如,People in your organization)均不受支持。 Teams 用户应仔细检查,确保在 Teams 客户端上上传文件后支持默认权限。

  • 不支持直接下载 URL (url)。

除了常规文件(fileAttachmentType),聊天 SDK 还提供 AttachmentTypeimage 属性。 Azure 通信服务用户可以这样附加图像,即反映了 Microsoft Teams 客户端如何在 UI 层将图像附件转换内联图像的行为。 有关详细信息,请参阅处理图像附件

Azure 通信服务用户可通过“从此设备上传”(可在 Teams 端找到)来添加图像,聊天 SDK 会返回 image 等附件。 通过“附加云文件”上传的图像会在 Teams 端被视为常规文件,因此聊天 SDK 会将此类附件返回为 file

另请注意,Azure 通信服务用户只能通过拖放来上传文件,或者通过“从此设备上传”和“附加云文件”这两个附件菜单命令进行上传。 目前不支持某些包含嵌入式媒体(例如视频剪辑、音频消息、天气卡)的消息类型。

本教程介绍如何使用用于 JavaScript 的 Azure 通信服务聊天 SDK 启用文件附件支持。

代码示例

GitHub 上找到本教程的最终代码。

先决条件

目标

  • 在消息会话中呈现文件附件。 每个文件附件卡都有一个“打开”按钮。
  • 将图像附件呈现为内联图像。

处理文件附件

对于常规文件附件,用于 JavaScript 的聊天 SDK 会返回 file 作为 ChatAttachmentType,而对于消息内联图像,则返回 image

export interface ChatMessageReceivedEvent extends BaseChatMessageEvent {
  /**
   * Content of the message.
   */
  message: string;

  /**
   * Chat message attachment.
   */
  attachments?: ChatAttachment[];
  
  ...
}

export interface ChatAttachment {
  /** Id of the attachment */
  id: string;
  /** The type of attachment. */
  attachmentType: AttachmentType;
  /** The name of the attachment content. */
  name?: string;
  /** The URL that is used to provide the original size of the inline images */
  url?: string;
  /** The URL that provides the preview of the attachment */
  previewUrl?: string;
}

/** Type of supported attachments. */
export type ChatAttachmentType = "image" | "file" | "unknown";

例如,以下 JSON 显示图像附件和文件附件的 ChatAttachment 显示效果:

"attachments": [
    {
        "id": "08a182fe-0b29-443e-8d7f-8896bc1908a2",
        "attachmentType": "file",
        "name": "business report.pdf",
        "previewUrl": "https://contoso.sharepoint.com/:u:/g/user/h8jTwB0Zl1AY"
    },
    {
        "id": "9d89acb2-c4e4-4cab-b94a-7c12a61afe30",
        "attachmentType": "image", 
        "name": "Screenshot.png",
        "url": "https://contoso.communication.azure.com/chat/threads/19:9d89acb29d89acb2@thread.v2/messages/123/images/9d89acb2-c4e4-4cab-b94a-7c12a61afe30/views/original?api-version=2023-11-15-preview",
        "previewUrl": "https://contoso.communication.azure.com/chat/threads/19:9d89acb29d89acb2@thread.v2/messages/123/images/9d89acb2-c4e4-4cab-b94a-7c12a61afe30/views/small?api-version=2023-11-15-preview"
      }
]

现在,让我们返回到在快速入门:将聊天应用加入 Teams 会议中创建的事件处理程序,并添加一些额外的逻辑来处理 attachmentType 属性为 file 的附件:

chatClient.on("chatMessageReceived", (e) => {
  console.log("Notification chatMessageReceived!");
  // Check whether the notification is intended for the current thread
  if (threadIdInput.value != e.threadId) {
     return;
  }
   
  if (e.sender.communicationUserId != userId) {
    renderReceivedMessage(e);
  } else {
    renderSentMessage(e.message);
  }
});

async function renderReceivedMessage(event) {
    messages += `<div class="container lighter"> ${event.message} </div>`;
    messagesContainer.innerHTML = messages;

    // Get the list of attachments and calls renderFileAttachments to construct a file attachment card
    var attachmentHtml = event.attachments
        .filter(attachment => attachment.attachmentType === "file")
        .map(attachment => renderFileAttachments(attachment))
        .join('');
    messagesContainer.innerHTML += attachmentHtml;
}

function renderFileAttachments(attachment) {
    var re = /(?:\.([^.]+))?$/;
    var fileExtension = re.exec(attachment.name)[1];  
    return '<div class="attachment-container">' +
        '<img class="attachment-icon" alt="attachment file icon" />' +
        '<div>' +
        '<p class="attachment-type">' + fileExtension + '</p>' +
        '<p>' + attachment.name + '</p>' +
        '<a href=' + attachment.previewUrl + ' target="_blank" rel="noreferrer">Open</a>' +
        '</div>' +
        '</div>';
}

确保为附件卡添加一些 CSS:

  /* Let's make the chat popup scrollable */
  .chat-popup {

     ...

     max-height: 650px;
     overflow-y: scroll;
}

 .attachment-container {
     overflow: hidden;
     background: #f3f2f1;
     padding: 20px;
     margin: 0;
     border-radius: 10px;
}
 .attachment-container img {
     width: 50px;
     height: 50px;
     float: left;
     margin: 0;
}
 .attachment-container p {
     font-weight: 700;
     margin: 0 5px 20px 0;
}
 .attachment-container {
     display: grid;
     grid-template-columns: 100px 1fr;
     margin-top: 5px;
}
 .attachment-icon {
     content: url("data:image/svg+xml;base64, ...");
}
 .attachment-container a {
     background-color: #dadada;
     color: black;
     font-size: 12px;
     padding: 10px;
     border: none;
     cursor: pointer;
     border-radius: 5px;
     text-align: center;
     margin-right: 10px;
     text-decoration: none;
     margin-top: 10px;
}
 .attachment-container a:hover {
     background-color: black;
     color: white;
}
 .attachment-type {
     position: absolute;
     color: black;
     border: 2px solid black;
     background-color: white;
     margin-top: 50px;
     font-family: sans-serif;
     font-weight: 400;
     padding: 2px;
     text-transform: uppercase;
     font-size: 8px;
}

这就是文件附件处理所需的全部内容。 接下来,让我们运行代码。

运行代码

Webpack 用户可以使用 webpack-dev-server 属性来构建和运行应用。 运行以下命令,在本地 Web 服务器上捆绑应用程序主机:

npx webpack-dev-server --entry ./client.js --output bundle.js --debug --devtool inline-source-map

或:

npm start

文件附件演示

  1. 打开浏览器并转到http://localhost:8080/。 输入会议 URL 和会话 ID。

  2. 从 Teams 客户端发送一些文件附件。

    显示 Teams 客户端的屏幕截图,上面显示一条已发送的消息,其中包含三个文件附件。

  3. 应会看到正在呈现的新消息以及文件附件。

    显示示例应用的屏幕截图,上面显示收到一条传入的消息,其中包含三个文件附件。

处理图像附件

图像附件需要与标准 file 附件采用不同的处理方式。 图像附件的 image 属性为 attachmentType,这要求有通信令牌才能检索预览图像或全尺寸图像。

在继续之前,请完成演示如何在聊天应用中启用内联图像支持的教程。 此教程介绍如何提取请求标中需要通信令牌的图像。 收到图像 blob 后,需要创建指向此 blob 的 ObjectUrl 属性。 然后,将此 URL 注入到每个内联图像的 src 属性中。

现在,你已经熟悉了内联图像的工作方式,可以像常规内联图像一样呈现图像附件。

首先,每当有图像附件时,都向消息内容注入 image 标记:

async function renderReceivedMessage(event) {
    messages += `<div class="container lighter"> ${event.message} </div>`;
    messagesContainer.innerHTML = messages;
    console.log(event);
    // Filter out inline images from attachments
    const imageAttachments = event.attachments?.filter(
        (attachment) =>
        attachment.attachmentType === "image" && !messages.includes(attachment.id)
    );
    // Inject image tag for all image attachments
    var imageAttachmentHtml =
        imageAttachments
        .map((attachment) => renderImageAttachments(attachment))
        .join("") ?? "";
    messagesContainer.innerHTML += imageAttachmentHtml;

    // Get list of attachments and calls renderFileAttachments to construct a file attachment card
    var attachmentHtml =
        event.attachments
        ?.filter((attachment) => attachment.attachmentType === "file")
        .map((attachment) => renderFileAttachments(attachment))
        .join("") ?? "";
    messagesContainer.innerHTML += attachmentHtml;

    // Fetch and render preview images
    fetchPreviewImages(imageAttachments);
}

function renderImageAttachments(attachment) {
    return `<img alt="image" src="" itemscope="png" id="${attachment.id}" style="max-width: 100px">`
}

现在,让我们借用教程:启用内联图像支持中的 fetchPreviewImages(),并按原样使用,不做任何更改:

function fetchPreviewImages(attachments) {
    if (!attachments.length > 0) {
        return;
    }
    Promise.all(
        attachments.map(async (attachment) => {
            const response = await fetch(attachment.previewUrl, {
                method: 'GET',
                headers: {
                    'Authorization': 'Bearer ' + tokenString,
                },
            });
            return {
                id: attachment.id,
                content: await response.blob(),
            };
        }),
    ).then((result) => {
        result.forEach((imageRef) => {
            const urlCreator = window.URL || window.webkitURL;
            const url = urlCreator.createObjectURL(imageRef.content);
            document.getElementById(imageRef.id).src = url;
        });
    }).catch((e) => {
        console.log('error fetching preview images');
    });
}

此函数需要 tokenString 属性,因此需要在 init() 中开始的全局副本,如以下代码片段所示:

var tokenString = '';

async function init() {
    ...
    const {
        token,
        expiresOn
    } = tokenResponse;
    
    tokenString = token;
    ...
}

现在,你已获得图像附件支持。 继续运行代码,亲眼看看效果。

图像附件演示

  1. 从 Teams 客户端发送一些图像附件。

    显示 Teams 客户端的屏幕截图,上面显示一个“发送”框,并且上传了一个图像附件。

  2. 发送图像附件后,请注意,它在 Teams 客户端变成了内联图像。

    显示 Teams 客户端的屏幕截图,其中显示一条带有图像附件的消息发送给了另一个参与者。

  3. 返回到示例应用,并确保呈现相同的图像。

    显示示例应用的屏幕截图,其中显示传入的一条消息中带有呈现的内联图像。

本教程介绍如何使用用于 C# 的 Azure 通信服务聊天 SDK 启用文件附件支持。

本教程介绍如何执行下列操作:

  • 处理文件附件。
  • 处理图像附件。

先决条件

示例代码

GitHub 上找到本教程的最终代码。

处理文件附件

对于常规文件附件,用于 C# 的聊天 SDK 返回的 ChatAttachmentType 属性为 file,而对于内联图像,则返回 image

public readonly partial struct ChatAttachmentType : IEquatable<ChatAttachmentType>
{
        private const string ImageValue = "image";
        private const string FileValue = "file";
        /// <summary> image. </summary>
        public static ChatAttachmentType Image { get; } = new ChatAttachmentType(ImageValue);
        /// <summary> file. </summary>
        public static ChatAttachmentType File { get; } = new ChatAttachmentType(FileValue);
}


例如,以下 JSON 显示在从服务器端接收请求时,为图像附件和文件附件显示的 ChatAttachment

"attachments": [
    {
        "id": "08a182fe-0b29-443e-8d7f-8896bc1908a2",
        "attachmentType": "file",
        "name": "business report.pdf",
        "previewUrl": "https://contoso.sharepoint.com/:u:/g/user/h8jTwB0Zl1AY"
    },
    {
        "id": "9d89acb2-c4e4-4cab-b94a-7c12a61afe30",
        "attachmentType": "image", 
        "name": "Screenshot.png",
        "url": "https://contoso.communication.azure.com/chat/threads/19:9d89acb29d89acb2@thread.v2/messages/123/images/9d89acb2-c4e4-4cab-b94a-7c12a61afe30/views/original?api-version=2023-11-15-preview",
        "previewUrl": "https://contoso.communication.azure.com/chat/threads/19:9d89acb29d89acb2@thread.v2/messages/123/images/9d89acb2-c4e4-4cab-b94a-7c12a61afe30/views/small?api-version=2023-11-15-preview"
      }
]

接下来,返回到之前在快速入门中创建的事件处理程序,并添加一些额外的逻辑来处理 ChatAttachmentType 属性为 file 的附件:


await foreach (ChatMessage message in allMessages)
{
    // Get message attachments that are of type 'file'
    IEnumerable<ChatAttachment> fileAttachments = message.Content.Attachments.Where(x => x.AttachmentType == ChatAttachmentType.File);
    var chatAttachmentFileUris = new List<Uri>();
    foreach (var file in fileAttachments) 
    {
        chatAttachmentFileUris.Add(file.PreviewUri);
    }

    // Build message list
    if (message.Type == ChatMessageType.Html || message.Type == ChatMessageType.Text)
    {
        textMessages++;
        var userPrefix = message.Sender.Equals(currentUser) ? "[you]:" : "";
        var strippedMessage = StripHtml(message.Content.Message);
      


        var chatAttachments = fileAttachments.Count() > 0 ? "[Attachments]:\n" + string.Join(",\n", chatAttachmentFileUris) : "";
        messageList.Add(long.Parse(message.SequenceId), $"{userPrefix}{strippedMessage}\n{chatAttachments}");
    }
}

具体而言,对于每个文件附件,获取 previewUrl 属性并构造 for loop 中的 URL 列表。 然后,将字符串与聊天消息内容一起嵌入。

处理图像附件

需要使用与标准 file 附件不同的方式处理图像附件。 图像附件的 ChatAttachmentType 属性为 image,这要求有通信令牌才能检索预览图像或全尺寸图像。

在继续之前,请完成启用内联映像支持教程。 为了标识图像附件,需要确定消息内容是否包含附件中的相同图像 ID。

bool isImageAttachment = message.Content.Message.Contains(x.Id);

如果此标志为 true,则应该应用内联图像逻辑来呈现它:

IEnumerable<ChatAttachment> imageAttachments = message.Content.Attachments.Where(x => x.AttachmentType == ChatAttachmentType.Image);
// Fetch image and render
var chatAttachmentImageUris = new List<Uri>();
foreach (ChatAttachment imageAttachment in imageAttachments)
{
    client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", communicationTokenCredential.GetToken().Token);
    var response = await client.GetAsync(imageAttachment.PreviewUri);
    var randomAccessStream = await response.Content.ReadAsStreamAsync();
    await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
    {
        var bitmapImage = new BitmapImage();
        await bitmapImage.SetSourceAsync(randomAccessStream.AsRandomAccessStream());
        InlineImage.Source = bitmapImage;
    });
    chatAttachmentImageUris.Add(imageAttachment.PreviewUri);
}

现在,应用支持图像附件。

后续步骤