预览链接

为了防止用户在 Google Chat 中分享链接时切换上下文,您的 Chat 应用可以通过在消息中附加卡片预览链接,从而提供更多信息,并让用户能够直接在 Google Chat 中执行操作。

例如,假设有一个 Google Chat 聊天室,其中包含一家公司的所有客户服务人员,以及一个名为 Case-y 的 Chat 应用。客服人员经常在 Chat 聊天室中分享客户服务支持请求的链接,而每次分享后,同事都必须打开支持请求链接,才能查看分配者、状态和主题等详细信息。同样,如果某人想要获得支持请求的所有权或更改状态,则需要打开链接。

借助链接预览功能,聊天室的常驻 Chat 应用 Case-y 会在用户分享支持请求链接时附加一个卡片,其中会显示分配者、状态和主题。借助卡片上的按钮,客服人员可以直接在聊天会话中接管支持请求并更改状态。

当用户向消息中添加链接时,系统会显示一个条状标签,告知用户 Chat 应用可能会预览该链接。

指示 Chat 应用可能会预览链接的条状标签

发送消息后,系统会将链接发送到 Chat 应用,然后 Chat 应用会生成卡片并将其附加到用户的消息中。

聊天应用通过将卡片附加到消息来预览链接

除了链接之外,该卡片还会提供有关链接的更多信息,包括按钮等交互元素。您的 Chat 应用可以根据用户互动(例如按钮点击)更新所附的卡片。

如果用户不希望 Chat 应用通过在消息中附加卡片来预览其链接,则可以点击预览条状标签上的 来阻止预览。用户可以随时点击移除预览来移除所附的卡片。

前提条件

Node.js

启用了交互功能的 Google Chat 应用。如需使用 HTTP 服务创建交互式 Chat 应用,请完成此快速入门

Python

启用了交互功能的 Google Chat 应用。如需使用 HTTP 服务创建交互式 Chat 应用,请完成此快速入门

Java

启用了交互功能的 Google Chat 应用。如需使用 HTTP 服务创建交互式 Chat 应用,请完成此快速入门

Apps 脚本

启用了交互功能的 Google Chat 应用。如需在 Apps 脚本中创建交互式 Chat 应用,请完成此快速入门

在 Google Cloud 控制台中的 Chat 应用配置页面上,将特定链接(例如 example.comsupport.example.comsupport.example.com/cases/)注册为网址模式,以便 Chat 应用可以预览它们。

“链接预览”配置菜单

  1. 打开 Google Cloud Console
  2. 点击“Google Cloud”旁边的向下箭头 ,然后打开 Chat 应用的项目。
  3. 在搜索字段中输入 Google Chat API,然后点击 Google Chat API
  4. 依次点击管理 > 配置
  5. 在“链接预览”下,添加或修改网址模式。
    1. 如需为新网址格式配置链接预览,请点击添加网址格式
    2. 如需修改现有网址模式的配置,请点击下拉箭头
  6. Host pattern(主机模式)字段中,输入网址模式的网域。Chat 应用会预览指向此网域的链接。

    如需让 Chat 应用预览特定子网域(例如 subdomain.example.com)的链接,请添加该子网域。

    如需让 Chat 应用预览整个网域的链接,请将星号 (*) 作为子网域指定为通配符字符。例如,*.example.comsubdomain.example.comany.number.of.subdomains.example.com 匹配。

  7. 路径前缀字段中,输入要附加到主机模式网域的路径。

    如需匹配主机模式网域中的所有网址,请将路径前缀留空。

    例如,如果主机模式为 support.example.com,如需匹配托管在 support.example.com/cases/ 的支持请求的网址,请输入 cases/

  8. 点击完成

  9. 点击保存

现在,每当有人在包含 Chat 应用的 Chat 聊天室中向消息中添加与链接预览网址模式匹配的链接时,您的应用都会预览该链接。

为给定链接配置链接预览功能后,Chat 应用便可通过为链接附加更多信息来识别和预览该链接。

在包含 Chat 应用的 Chat 聊天室中,当某位用户的消息包含与链接预览网址格式匹配的链接时,您的 Chat 应用会收到 MESSAGE 互动事件。互动事件的 JSON 载荷包含 matchedUrl 字段:

JSON

message: {
  matchedUrl: {
    url: "https://support.example.com/cases/case123"
  },
  ... // other message attributes redacted
}

通过检查 MESSAGE 事件载荷中是否存在 matchedUrl 字段,您的 Chat 应用可以向消息中添加预览链接的信息。Chat 应用可以使用基本文本消息进行回复,也可以附加卡片。

使用短信回复

对于基本回复,Chat 应用可以通过简单的文本消息回复链接来预览链接。以下示例会附加一条消息,重复与链接预览网址模式匹配的链接网址。

Node.js

node/preview-link/index.js
// Reply with a text message for URLs of the subdomain "text"
if (event.message.matchedUrl.url.includes("text.example.com")) {
  return {
    text: 'event.message.matchedUrl.url: ' + event.message.matchedUrl.url
  };
}

Python

python/preview-link/main.py
# Reply with a text message for URLs of the subdomain "text"
if 'text.example.com' in event.get('message').get('matchedUrl').get('url'):
  return {
    'text': 'event.message.matchedUrl.url: ' +
            event.get('message').get('matchedUrl').get('url')
  }

Java

java/preview-link/src/main/java/com/google/chat/preview/App.java
// Reply with a text message for URLs of the subdomain "text"
if (event.at("/message/matchedUrl/url").asText().contains("text.example.com")) {
  return new Message().setText("event.message.matchedUrl.url: " +
    event.at("/message/matchedUrl/url").asText());
}

Apps 脚本

apps-script/preview-link/preview-link.gs
// Reply with a text message for URLs of the subdomain "text"
if (event.message.matchedUrl.url.includes("text.example.com")) {
  return {
    text: 'event.message.matchedUrl.url: ' + event.message.matchedUrl.url
  };
}

如需将卡片附加到预览的链接,请返回一个类型为 UPDATE_USER_MESSAGE_CARDSActionResponse。此示例会附加基本卡片。

聊天应用通过将卡片附加到消息来预览链接

Node.js

node/preview-link/index.js
// Attach a card to the message for URLs of the subdomain "support"
if (event.message.matchedUrl.url.includes("support.example.com")) {
  // A hard-coded card is used in this example. In a real-life scenario,
  // the case information would be fetched and used to build the card.
  return {
    actionResponse: { type: 'UPDATE_USER_MESSAGE_CARDS' },
    cardsV2: [{
      cardId: 'attachCard',
      card: {
        header: {
          title: 'Example Customer Service Case',
          subtitle: 'Case basics',
        },
        sections: [{ widgets: [
          { decoratedText: { topLabel: 'Case ID', text: 'case123'}},
          { decoratedText: { topLabel: 'Assignee', text: 'Charlie'}},
          { decoratedText: { topLabel: 'Status', text: 'Open'}},
          { decoratedText: { topLabel: 'Subject', text: 'It won\'t turn on...' }},
          { buttonList: { buttons: [{
            text: 'OPEN CASE',
            onClick: { openLink: {
              url: 'https://support.example.com/orders/case123'
            }},
          }, {
            text: 'RESOLVE CASE',
            onClick: { openLink: {
              url: 'https://support.example.com/orders/case123?resolved=y',
            }},
          }, {
            text: 'ASSIGN TO ME',
            onClick: { action: { function: 'assign'}}
          }]}}
        ]}]
      }
    }]
  };
}

Python

python/preview-link/main.py
# Attach a card to the message for URLs of the subdomain "support"
if 'support.example.com' in event.get('message').get('matchedUrl').get('url'):
  # A hard-coded card is used in this example. In a real-life scenario,
  # the case information would be fetched and used to build the card.
  return {
    'actionResponse': { 'type': 'UPDATE_USER_MESSAGE_CARDS' },
    'cardsV2': [{
      'cardId': 'attachCard',
      'card': {
        'header': {
          'title': 'Example Customer Service Case',
          'subtitle': 'Case basics',
        },
        'sections': [{ 'widgets': [
          { 'decoratedText': { 'topLabel': 'Case ID', 'text': 'case123'}},
          { 'decoratedText': { 'topLabel': 'Assignee', 'text': 'Charlie'}},
          { 'decoratedText': { 'topLabel': 'Status', 'text': 'Open'}},
          { 'decoratedText': { 'topLabel': 'Subject', 'text': 'It won\'t turn on...' }},
          { 'buttonList': { 'buttons': [{
            'text': 'OPEN CASE',
            'onClick': { 'openLink': {
              'url': 'https://support.example.com/orders/case123'
            }},
          }, {
            'text': 'RESOLVE CASE',
            'onClick': { 'openLink': {
              'url': 'https://support.example.com/orders/case123?resolved=y',
            }},
          }, {
            'text': 'ASSIGN TO ME',
            'onClick': { 'action': { 'function': 'assign'}}
          }]}}
        ]}]
      }
    }]
  }

Java

java/preview-link/src/main/java/com/google/chat/preview/App.java
// Attach a card to the message for URLs of the subdomain "support"
if (event.at("/message/matchedUrl/url").asText().contains("support.example.com")) {
  // A hard-coded card is used in this example. In a real-life scenario,
  // the case information would be fetched and used to build the card.
  return new Message()
    .setActionResponse(new ActionResponse()
      .setType("UPDATE_USER_MESSAGE_CARDS"))
    .setCardsV2(List.of(new CardWithId()
      .setCardId("attachCard")
      .setCard(new GoogleAppsCardV1Card()
        .setHeader(new GoogleAppsCardV1CardHeader()
          .setTitle("Example Customer Service Case")
          .setSubtitle("Case basics"))
        .setSections(List.of(new GoogleAppsCardV1Section().setWidgets(List.of(
          new GoogleAppsCardV1Widget().setDecoratedText(new GoogleAppsCardV1DecoratedText()
            .setTopLabel("Case ID")
            .setText("case123")),
          new GoogleAppsCardV1Widget().setDecoratedText(new GoogleAppsCardV1DecoratedText()
            .setTopLabel("Assignee")
            .setText("Charlie")),
          new GoogleAppsCardV1Widget().setDecoratedText(new GoogleAppsCardV1DecoratedText()
            .setTopLabel("Status")
            .setText("Open")),
          new GoogleAppsCardV1Widget().setDecoratedText(new GoogleAppsCardV1DecoratedText()
            .setTopLabel("Subject")
            .setText("It won't turn on...")),
          new GoogleAppsCardV1Widget()
            .setButtonList(new GoogleAppsCardV1ButtonList().setButtons(List.of(
              new GoogleAppsCardV1Button()
                .setText("OPEN CASE")
                .setOnClick(new GoogleAppsCardV1OnClick()
                  .setOpenLink(new GoogleAppsCardV1OpenLink()
                    .setUrl("https://support.example.com/orders/case123"))),
              new GoogleAppsCardV1Button()
                .setText("RESOLVE CASE")
                .setOnClick(new GoogleAppsCardV1OnClick()
                  .setOpenLink(new GoogleAppsCardV1OpenLink()
                    .setUrl("https://support.example.com/orders/case123?resolved=y"))),
              new GoogleAppsCardV1Button()
                .setText("ASSIGN TO ME")
                .setOnClick(new GoogleAppsCardV1OnClick()
                  .setAction(new GoogleAppsCardV1Action().setFunction("assign")))))))))))));
}

Apps 脚本

此示例通过返回卡片 JSON 来发送卡片消息。您还可以使用 Apps 脚本卡片服务

apps-script/preview-link/preview-link.gs
// Attach a card to the message for URLs of the subdomain "support"
if (event.message.matchedUrl.url.includes("support.example.com")) {
  // A hard-coded card is used in this example. In a real-life scenario,
  // the case information would be fetched and used to build the card.
  return {
    actionResponse: { type: 'UPDATE_USER_MESSAGE_CARDS' },
    cardsV2: [{
      cardId: 'attachCard',
      card: {
        header: {
          title: 'Example Customer Service Case',
          subtitle: 'Case basics',
        },
        sections: [{ widgets: [
          { decoratedText: { topLabel: 'Case ID', text: 'case123'}},
          { decoratedText: { topLabel: 'Assignee', text: 'Charlie'}},
          { decoratedText: { topLabel: 'Status', text: 'Open'}},
          { decoratedText: { topLabel: 'Subject', text: 'It won\'t turn on...' }},
          { buttonList: { buttons: [{
            text: 'OPEN CASE',
            onClick: { openLink: {
              url: 'https://support.example.com/orders/case123'
            }},
          }, {
            text: 'RESOLVE CASE',
            onClick: { openLink: {
              url: 'https://support.example.com/orders/case123?resolved=y',
            }},
          }, {
            text: 'ASSIGN TO ME',
            onClick: { action: { function: 'assign'}}
          }]}}
        ]}]
      }
    }]
  };
}

当用户与链接预览卡片互动(例如点击卡片上的按钮)时,您的 Chat 应用可以更新该卡片。

如需更新该卡片,您的 Chat 应用必须处理 CARD_CLICKED 互动事件,并根据发送包含链接预览的消息的用户返回 actionResponse

  • 如果消息是由用户发送的,请将 actionResponse.type 设置为 UPDATE_USER_MESSAGE_CARDS
  • 如果是 Chat 应用发送了消息,请将 actionResponse.type 设为 UPDATE_MESSAGE

如需确定发送者是谁,您可以使用互动事件的 message.sender.type 字段,查看发件人是 HUMAN 用户还是 BOT

以下示例展示了 Chat 应用如何在用户点击分配给我按钮时更新链接预览,具体方法是更新卡片的负责人字段并停用该按钮。

聊天应用中,预览了附加到消息中的卡片更新版的链接

Node.js

node/preview-link/index.js
/**
 * Updates a card that was attached to a message with a previewed link.
 *
 * @param {Object} event The event object from Chat.
 *
 * @return {Object} Response from the Chat app. Either a new card attached to
 * the message with the previewed link, or an update to an existing card.
 */
function onCardClick(event) {
  // To respond to the correct button, checks the button's actionMethodName.
  if (event.action.actionMethodName === 'assign') {
    // A hard-coded card is used in this example. In a real-life scenario,
    // an actual assign action would be performed before building the card.

    // Checks whether the message event originated from a human or a Chat app
    // and sets actionResponse.type to "UPDATE_USER_MESSAGE_CARDS if human or
    // "UPDATE_MESSAGE" if Chat app.
    const actionResponseType = event.message.sender.type === 'HUMAN' ?
      'UPDATE_USER_MESSAGE_CARDS' :
      'UPDATE_MESSAGE';

    // Returns the updated card that displays "You" for the assignee
    // and that disables the button.
    return {
      actionResponse: { type: actionResponseType },
      cardsV2: [{
        cardId: 'attachCard',
        card: {
          header: {
            title: 'Example Customer Service Case',
            subtitle: 'Case basics',
          },
          sections: [{ widgets: [
            { decoratedText: { topLabel: 'Case ID', text: 'case123'}},
            // The assignee is now "You"
            { decoratedText: { topLabel: 'Assignee', text: 'You'}},
            { decoratedText: { topLabel: 'Status', text: 'Open'}},
            { decoratedText: { topLabel: 'Subject', text: 'It won\'t turn on...' }},
            { buttonList: { buttons: [{
              text: 'OPEN CASE',
              onClick: { openLink: {
                url: 'https://support.example.com/orders/case123'
              }},
            }, {
              text: 'RESOLVE CASE',
              onClick: { openLink: {
                url: 'https://support.example.com/orders/case123?resolved=y',
              }},
            }, {
              text: 'ASSIGN TO ME',
              // The button is now disabled
              disabled: true,
              onClick: { action: { function: 'assign'}}
            }]}}
          ]}]
        }
      }]
    };
  }
}

Python

python/preview-link/main.py
def on_card_click(event: dict) -> dict:
  """Updates a card that was attached to a message with a previewed link."""
  # To respond to the correct button, checks the button's actionMethodName.
  if 'assign' == event.get('action').get('actionMethodName'):
    # A hard-coded card is used in this example. In a real-life scenario,
    # an actual assign action would be performed before building the card.

    # Checks whether the message event originated from a human or a Chat app
    # and sets actionResponse.type to "UPDATE_USER_MESSAGE_CARDS if human or
    # "UPDATE_MESSAGE" if Chat app.
    actionResponseType = 'UPDATE_USER_MESSAGE_CARDS' if \
      event.get('message').get('sender').get('type') == 'HUMAN' else \
      'UPDATE_MESSAGE'

    # Returns the updated card that displays "You" for the assignee
    # and that disables the button.
    return {
      'actionResponse': { 'type': actionResponseType },
      'cardsV2': [{
        'cardId': 'attachCard',
        'card': {
          'header': {
            'title': 'Example Customer Service Case',
            'subtitle': 'Case basics',
          },
          'sections': [{ 'widgets': [
            { 'decoratedText': { 'topLabel': 'Case ID', 'text': 'case123'}},
            # The assignee is now "You"
            { 'decoratedText': { 'topLabel': 'Assignee', 'text': 'You'}},
            { 'decoratedText': { 'topLabel': 'Status', 'text': 'Open'}},
            { 'decoratedText': { 'topLabel': 'Subject', 'text': 'It won\'t turn on...' }},
            { 'buttonList': { 'buttons': [{
              'text': 'OPEN CASE',
              'onClick': { 'openLink': {
                'url': 'https://support.example.com/orders/case123'
              }},
            }, {
              'text': 'RESOLVE CASE',
              'onClick': { 'openLink': {
                'url': 'https://support.example.com/orders/case123?resolved=y',
              }},
            }, {
              'text': 'ASSIGN TO ME',
              # The button is now disabled
              'disabled': True,
              'onClick': { 'action': { 'function': 'assign'}}
            }]}}
          ]}]
        }
      }]
    }

Java

java/preview-link/src/main/java/com/google/chat/preview/App.java
// Updates a card that was attached to a message with a previewed link.
Message onCardClick(JsonNode event) {
  // To respond to the correct button, checks the button's actionMethodName.
  if (event.at("/action/actionMethodName").asText().equals("assign")) {
    // A hard-coded card is used in this example. In a real-life scenario,
    // an actual assign action would be performed before building the card.

    // Checks whether the message event originated from a human or a Chat app
    // and sets actionResponse.type to "UPDATE_USER_MESSAGE_CARDS if human or
    // "UPDATE_MESSAGE" if Chat app.
    String actionResponseType =
      event.at("/message/sender/type").asText().equals("HUMAN")
      ? "UPDATE_USER_MESSAGE_CARDS" : "UPDATE_MESSAGE";

    // Returns the updated card that displays "You" for the assignee
    // and that disables the button.
    return new Message()
    .setActionResponse(new ActionResponse()
      .setType(actionResponseType))
    .setCardsV2(List.of(new CardWithId()
      .setCardId("attachCard")
      .setCard(new GoogleAppsCardV1Card()
        .setHeader(new GoogleAppsCardV1CardHeader()
          .setTitle("Example Customer Service Case")
          .setSubtitle("Case basics"))
        .setSections(List.of(new GoogleAppsCardV1Section().setWidgets(List.of(
          new GoogleAppsCardV1Widget().setDecoratedText(new GoogleAppsCardV1DecoratedText()
            .setTopLabel("Case ID")
            .setText("case123")),
          new GoogleAppsCardV1Widget().setDecoratedText(new GoogleAppsCardV1DecoratedText()
            .setTopLabel("Assignee")
            // The assignee is now "You"
            .setText("You")),
          new GoogleAppsCardV1Widget().setDecoratedText(new GoogleAppsCardV1DecoratedText()
            .setTopLabel("Status")
            .setText("Open")),
          new GoogleAppsCardV1Widget().setDecoratedText(new GoogleAppsCardV1DecoratedText()
            .setTopLabel("Subject")
            .setText("It won't turn on...")),
          new GoogleAppsCardV1Widget()
            .setButtonList(new GoogleAppsCardV1ButtonList().setButtons(List.of(
              new GoogleAppsCardV1Button()
                .setText("OPEN CASE")
                .setOnClick(new GoogleAppsCardV1OnClick()
                  .setOpenLink(new GoogleAppsCardV1OpenLink()
                    .setUrl("https://support.example.com/orders/case123"))),
              new GoogleAppsCardV1Button()
                .setText("RESOLVE CASE")
                .setOnClick(new GoogleAppsCardV1OnClick()
                  .setOpenLink(new GoogleAppsCardV1OpenLink()
                    .setUrl("https://support.example.com/orders/case123?resolved=y"))),
              new GoogleAppsCardV1Button()
                .setText("ASSIGN TO ME")
                // The button is now disabled
                .setDisabled(true)
                .setOnClick(new GoogleAppsCardV1OnClick()
                  .setAction(new GoogleAppsCardV1Action().setFunction("assign")))))))))))));
  }
  return null;
}

Apps 脚本

此示例通过返回卡片 JSON 来发送卡片消息。您还可以使用 Apps 脚本卡片服务

apps-script/preview-link/preview-link.gs
/**
 * Updates a card that was attached to a message with a previewed link.
 *
 * @param {Object} event The event object from Chat.
 *
 * @return {Object} Response from the Chat app. Either a new card attached to
 * the message with the previewed link, or an update to an existing card.
 */
function onCardClick(event) {
  // To respond to the correct button, checks the button's actionMethodName.
  if (event.action.actionMethodName === 'assign') {
    // A hard-coded card is used in this example. In a real-life scenario,
    // an actual assign action would be performed before building the card.

    // Checks whether the message event originated from a human or a Chat app
    // and sets actionResponse.type to "UPDATE_USER_MESSAGE_CARDS if human or
    // "UPDATE_MESSAGE" if Chat app.
    const actionResponseType = event.message.sender.type === 'HUMAN' ?
      'UPDATE_USER_MESSAGE_CARDS' :
      'UPDATE_MESSAGE';

    // Returns the updated card that displays "You" for the assignee
    // and that disables the button.
    return {
      actionResponse: { type: actionResponseType },
      cardsV2: [{
        cardId: 'attachCard',
        card: {
          header: {
            title: 'Example Customer Service Case',
            subtitle: 'Case basics',
          },
          sections: [{ widgets: [
            { decoratedText: { topLabel: 'Case ID', text: 'case123'}},
            // The assignee is now "You"
            { decoratedText: { topLabel: 'Assignee', text: 'You'}},
            { decoratedText: { topLabel: 'Status', text: 'Open'}},
            { decoratedText: { topLabel: 'Subject', text: 'It won\'t turn on...' }},
            { buttonList: { buttons: [{
              text: 'OPEN CASE',
              onClick: { openLink: {
                url: 'https://support.example.com/orders/case123'
              }},
            }, {
              text: 'RESOLVE CASE',
              onClick: { openLink: {
                url: 'https://support.example.com/orders/case123?resolved=y',
              }},
            }, {
              text: 'ASSIGN TO ME',
              // The button is now disabled
              disabled: true,
              onClick: { action: { function: 'assign'}}
            }]}}
          ]}]
        }
      }]
    };
  }
}

限制和注意事项

为 Chat 应用配置链接预览时,请注意以下限制和注意事项:

  • 每个 Chat 应用最多支持 5 个网址格式的链接预览。
  • Chat 应用会针对每条消息预览一个链接。如果单个消息中包含多个可预览的链接,系统只会预览第一个可预览的链接。
  • 聊天应用仅预览以 https:// 开头的链接,因此 https://support.example.com/cases/ 会预览,但 support.example.com/cases/ 不会。
  • 除非消息包含要发送到 Chat 应用的其他信息(例如斜杠命令),否则只有链接网址会通过链接预览发送到 Chat 应用。
  • 如果用户发布链接,则只有在用户与链接预览卡片互动(例如点击按钮)时,Chat 应用才能更新链接预览卡片。您无法对 Message 资源调用 Chat API 的 update() 方法以异步更新用户的消息。
  • Chat 应用必须向聊天室中的所有人预览链接,因此消息必须省略 privateMessageViewer 字段。

在实现链接预览时,您可能需要通过读取应用的日志来调试 Chat 应用。如需读取日志,请访问 Google Cloud 控制台中的日志浏览器