收集和处理 Google Chat 用户的信息

本指南介绍了 Google Chat 应用如何通过在卡片式界面中构建表单输入来收集和处理用户信息。

一个对话框,其中包含各种不同的 widget。
图 1:示例 Chat 应用,可打开对话框来收集联系信息。

Chat 应用会请求用户提供信息,以便在 Chat 内部或外部执行操作,包括通过以下方式:

  • 配置设置。例如,允许用户自定义通知设置,或配置 Chat 应用并将其添加到一个或多个聊天室。
  • 在其他 Google Workspace 应用中创建或更新信息。例如,让用户创建 Google 日历活动。
  • 让用户可以访问和更新其他应用或 Web 服务中的资源。例如,Chat 应用可以帮助用户 直接在 Chat 聊天室中更新支持服务工单的状态

前提条件

HTTP

接收并响应用户互动的 Google Chat 应用。如需构建一个,请完成 HTTP 快速入门。

Apps 脚本

接收并响应用户互动的 Google Chat 应用。如需构建一个,请完成 Apps 脚本快速入门。

使用卡片构建表单

为了收集信息,Chat 应用会设计表单及其输入,并将其构建到卡片中。如需向用户显示卡片,Chat 应用可以使用以下 Chat 界面:

  • 包含一张或多张卡片的消息。
  • 首页,即在与 Chat 应用的私信中,从首页标签页显示的卡片。
  • 对话框,即从消息和首页在新窗口中打开的卡片。

Chat 应用可以使用以下 widget 构建卡片:

  • 向用户请求信息的表单输入 widget。(可选)您可以向表单输入 widget 添加验证,以确保用户正确输入和设置信息格式。Chat 应用可以使用以下表单输入 widget:

    • 文本输入 (textInput),用于输入自由形式文本或建议文本。
    • 选择输入 (selectionInput) 是可选择的界面元素,例如复选框、单选按钮和下拉菜单。选择输入微件还可以根据 Google Workspace 数据(例如 Chat 聊天室)或动态数据源填充和建议内容。如需了解详情,请参阅以下部分:添加下拉菜单和添加多选菜单。
    • 日期时间选择器 (dateTimePicker),用于输入日期和时间。
  • 按钮 widget,以便用户提交他们在卡片中输入的值。用户点击该按钮后,Chat 应用便可以处理收到的信息。

在以下示例中,卡片使用文本输入、日期时间选择器和选择输入来收集联系信息:

如需查看可用于收集信息的互动式 widget 的更多示例,请参阅设计互动式卡片或对话框。

添加下拉菜单

如需自定义选择项或让用户从动态数据源中选择单个项,Chat 应用可以使用下拉菜单,这是一种 SelectionInput widget。例如,以下卡片显示了一个下拉菜单,用户可以从中动态选择联系人列表:

您可以从以下数据源填充下拉菜单中的项:

从 Google Workspace 数据源填充内容

如需从 Google Workspace 数据源(例如 Google Workspace 用户)填充内容,请在 DataSourceConfig 对象中指定 platformDataSource 字段。与其他选择输入类型不同,您需要省略 SelectionItem 对象,因为这些选择项是从 Google Workspace 动态获取的。

以下代码显示了 Google Workspace 用户的下拉菜单:

JSON

{
  "sections": [
    {
      "header": "Section Header",
      "widgets": [
        {
          "selectionInput": {
            "name": "contacts",
            "type": "DROPDOWN",
            "label": "Select contact from organization",
            "dataSourceConfigs": [
              {
                "platformDataSource": {
                  "commonDataSource": "USER"
                },
                "minCharactersTrigger": 1
              }
            ]
          }
        }
      ]
    }
  ]
}

从外部数据源填充内容

下拉菜单还可以填充来自第三方或外部数据源的内容。如需使用外部数据源,您需要在 DataSourceConfig 对象中指定 remoteDataSource 字段,该对象包含用于查询和返回数据源中内容的函数。

为了减少对外部数据源的请求,您可以在用户输入内容之前,在下拉菜单中显示建议的项。如需从外部数据源填充建议的内容,请指定静态 SelectionItem 对象。

以下代码展示了一个下拉菜单,该菜单会查询外部数据源并填充其中的项:

JSON

{
  "sections": [
    {
      "header": "Section Header",
      "widgets": [
        {
          "selectionInput": {
            "name": "crm_leads",
            "type": "DROPDOWN",
            "label": "Select CRM Lead",
            "dataSourceConfigs": [
              {
                "remoteDataSource": {
                  "function": "getCrmLeads"
                },
                "minCharactersTrigger": 2
              }
            ],
            "items": [
              {
                "text": "Suggested Lead 1",
                "value": "lead-1"
              }
            ]
          }
        }
      ]
    }
  ]
}

如需查看展示如何返回建议内容的已完成示例,请参阅建议选择内容部分。

添加多选菜单

如需自定义选择项或让用户从动态数据源中选择内容,Chat 应用可以使用多选菜单,这是一种 SelectionInput widget。例如,以下卡片显示了一个多选菜单,用户可以从中动态选择联系人列表:

您可以通过以下数据源填充多选菜单的选项:

  • Google Workspace 数据,包括用户或用户所属的 Chat 聊天室。该菜单仅填充来自同一 Google Workspace 组织的内容。
  • 外部数据源,例如关系型数据库。例如,您可以使用多选菜单帮助用户从客户关系管理 (CRM) 系统中的潜在客户列表中进行选择。

从 Google Workspace 数据源填充内容

如需使用 Google Workspace 数据源,请在 SelectionInput widget 中指定 platformDataSource 字段。与其他选择输入类型不同,您需要省略 SelectionItem 对象,因为这些选择项是从 Google Workspace 动态获取的。

以下代码展示了一个包含 Google Workspace 用户的多选菜单。 为了填充用户,选择输入会将 commonDataSource 设置为 USER:

JSON

{
  "selectionInput": {
    "name": "contacts",
    "type": "MULTI_SELECT",
    "label": "Selected contacts",
    "multiSelectMaxSelectedItems": 5,
    "multiSelectMinQueryLength": 1,
    "platformDataSource": {
      "commonDataSource": "USER"
    }
  }
}

以下代码显示了 Chat 空间的多选菜单。为了填充聊天室,选择输入指定了 hostAppDataSource 字段。多选菜单还会将 defaultToCurrentSpace 设置为 true,这会使当前聊天室成为菜单中的默认选择:

JSON

{
  "selectionInput": {
    "name": "spaces",
    "type": "MULTI_SELECT",
    "label": "Selected contacts",
    "multiSelectMaxSelectedItems": 3,
    "multiSelectMinQueryLength": 1,
    "platformDataSource": {
      "hostAppDataSource": {
        "chatDataSource": {
          "spaceDataSource": {
            "defaultToCurrentSpace": true
          }
        }
      }
    }
  }
}

从外部数据源填充内容

多选菜单还可以从第三方或外部数据源填充内容。如需使用外部数据源,您需要在 SelectionInput widget 中指定 externalDataSource 字段,该字段包含用于查询和返回数据源中内容的函数。

为了减少对外部数据源的请求,您可以在用户在菜单中输入内容之前,添加显示在多选菜单中的建议项。例如,您可以为用户填充最近搜索过的联系人。如需从外部数据源填充建议的内容,请指定静态 SelectionItem 对象。

以下代码示例展示了一个多选菜单,该菜单会查询外部数据源中的内容并填充到菜单中:

Node.js

node/chat/selection-input/index.js
selectionInput: {
  name: "contacts",
  type: "MULTI_SELECT",
  label: "Selected contacts",
  multiSelectMaxSelectedItems: 3,
  multiSelectMinQueryLength: 1,
  externalDataSource: { function: FUNCTION_URL },
  // Suggested items loaded by default.
  // The list is static here but it could be dynamic.
  items: [getSuggestedContact("3")]
}

将 FUNCTION_URL 替换为查询外部数据源的 HTTP 端点。

Python

python/chat/selection-input/main.py
'selectionInput': {
  'name': "contacts",
  'type': "MULTI_SELECT",
  'label': "Selected contacts",
  'multiSelectMaxSelectedItems': 3,
  'multiSelectMinQueryLength': 1,
  'externalDataSource': { 'function': FUNCTION_URL },
  # Suggested items loaded by default.
  # The list is static here but it could be dynamic.
  'items': [get_suggested_contact("3")]
}

将 FUNCTION_URL 替换为查询外部数据源的 HTTP 端点。

Java

java/chat/selection-input/src/main/java/com/google/chat/selectionInput/App.java
.setSelectionInput(new GoogleAppsCardV1SelectionInput()
  .setName("contacts")
  .setType("MULTI_SELECT")
  .setLabel("Selected contacts")
  .setMultiSelectMaxSelectedItems(3)
  .setMultiSelectMinQueryLength(1)
  .setExternalDataSource(new GoogleAppsCardV1Action().setFunction(FUNCTION_URL))
  // Suggested items loaded by default.
  // The list is static here but it could be dynamic.
  .setItems(List.of(getSuggestedContact("3")))))))))));

将 FUNCTION_URL 替换为查询外部数据源的 HTTP 端点。

Apps 脚本

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

apps-script/chat/selection-input/selection-input.gs
selectionInput: {
  name: "contacts",
  type: "MULTI_SELECT",
  label: "Selected contacts",
  multiSelectMaxSelectedItems: 3,
  multiSelectMinQueryLength: 1,
  externalDataSource: { function: "queryContacts" },
  // Suggested items loaded by default.
  // The list is static here but it could be dynamic.
  items: [getSuggestedContact("3")]
}

如需查看展示如何返回建议内容的已完成示例,请参阅建议选择内容部分。

从互动微件接收数据

每当用户点击按钮时,系统都会触发其 Chat 应用操作,并提供有关互动的信息。在事件载荷的 commonEventObject 中,formInputs 对象包含用户输入的任何值。

您可以从对象 event.commonEventObject.formInputs.WIDGET_NAME 中检索值,其中 WIDGET_NAME 是您为 widget 指定的 name 字段。这些值会作为微件的特定数据类型返回。

下面显示了事件对象的一部分,其中用户为每个 widget 输入了值:

{
  "commonEventObject": { "formInputs": {
    "contactName": { "stringInputs": {
      "value": ["Kai 0"]
    }},
    "contactBirthdate": { "dateInput": {
      "msSinceEpoch": 1000425600000
    }},
    "contactType": { "stringInputs": {
      "value": ["Personal"]
    }}
  }}
}

为了接收数据,您的 Chat 应用会处理事件对象,以获取用户在微件中输入的值。下表显示了如何获取给定表单输入 widget 的值。对于每个 widget,该表都会显示 widget 接受的数据类型、值在事件对象中的存储位置以及示例值。

表单输入微件 输入数据类型 事件对象中的输入值 示例值
textInput stringInputs event.commonEventObject.formInputs.contactName.stringInputs.value[0] Kai O
selectionInput stringInputs 如需获取第一个或唯一的值,请使用 event.commonEventObject.formInputs.contactType.stringInputs.value[0] Personal
dateTimePicker 仅接受日期。 dateInput event.commonEventObject.formInputs.contactBirthdate.dateInput.msSinceEpoch。 1000425600000

Chat 应用收到数据后,可以执行以下任一操作:

  • 对于包含多选菜单的卡片,请根据用户在菜单中输入的内容填充或建议选项。
  • 将数据转移到另一张卡片,以便用户查看其信息或继续填写表单的下一部分。
  • 回复用户,确认用户已成功填写表单。

建议选择项

如果卡片包含多选菜单或从外部数据源填充内容的下拉菜单,Chat 应用可以根据用户在菜单中输入的内容返回建议的内容。例如,如果用户开始输入 Atl,而菜单中填充的是美国城市,则 Chat 应用可以在用户输入完毕之前自动建议 Atlanta。Chat 应用最多可以建议 100 项。

如需在选择输入中建议和动态填充内容,卡片上的 SelectionInput widget 必须指定一个查询外部数据源的函数。对于多选菜单,您需要指定 externalDataSource 字段。对于下拉菜单,您可以在 DataSourceConfig 对象中指定 remoteDataSource 字段。

您还可以配置用户在菜单返回建议之前输入的字符数。对于多选菜单,请设置 multiSelectMinQueryLength 字段。对于下拉菜单,请在 DataSourceConfig 中设置 min_characters_trigger 字段。

如需返回建议的商品,该函数必须执行以下操作:

  1. 处理事件对象,当用户在菜单中输入内容时,Chat 应用会收到该对象。
  2. 从事件对象中获取用户输入的值,该值在 event.commonEventObject.parameters["autocomplete_widget_query"] 字段中表示。
  3. 使用用户输入值查询数据源,以获取一个或多个 SelectionItems 建议给用户。
  4. 通过返回带有 modifyCard 对象的 RenderActions 操作来返回建议的商品。

以下代码示例展示了 Chat 应用如何在卡片的多选菜单中动态建议选项。当用户在菜单中输入内容时,微件的 externalDataSource 字段中提供的函数或端点会查询外部数据源,并建议用户可以选择的项目。

Node.js

node/chat/selection-input/index.js
/**
 * Web app that responds to events sent from a Google Chat space.
 *
 * @param {Object} req Request sent from Google Chat space
 * @param {Object} res Response to send back
 */
app.post('/', async (req, res) => {
  // Stores the Google Chat event
  const chatEvent = req.body.chat;

  // Handle user interaction with multiselect.
  if(chatEvent.widgetUpdatedPayload) {
    return res.json(queryContacts(req.body));
  }

  // Replies with a card that contains the multiselect menu.
  return res.json({ hostAppDataAction: { chatDataAction: { createMessageAction: { message: {
    cardsV2: [{
      cardId: "contactSelector",
      card: { sections:[{ widgets: [{
        selectionInput: {
          name: "contacts",
          type: "MULTI_SELECT",
          label: "Selected contacts",
          multiSelectMaxSelectedItems: 3,
          multiSelectMinQueryLength: 1,
          externalDataSource: { function: FUNCTION_URL },
          // Suggested items loaded by default.
          // The list is static here but it could be dynamic.
          items: [getSuggestedContact("3")]
        }
      }]}]}
    }]
  }}}}});
});

/**
 * Get contact suggestions based on text typed by users.
 *
 * @param {Object} event the event object that contains the user's query
 * @return {Object} suggestions
 */
function queryContacts(event) {
  const query = event.commonEventObject.parameters["autocomplete_widget_query"];
  return { action: { modifyOperations: [{ updateWidget: { selectionInputWidgetSuggestions: { suggestions: [
    // The list is static here but it could be dynamic.
    getSuggestedContact("1"), getSuggestedContact("2"), getSuggestedContact("3"), getSuggestedContact("4"), getSuggestedContact("5")
  // Only return items based on the query from the user.
  ].filter(e => !query || e.text.includes(query)) }}}]}};
}

/**
 * Generate a suggested contact given an ID.
 *
 * @param {String} id The ID of the contact to return.
 * @return {Object} The contact formatted as a selection item in the menu.
 */
function getSuggestedContact(id) {
  return {
    value: id,
    startIconUri: "https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png",
    text: "Contact " + id
  };
}

将 FUNCTION_URL 替换为查询外部数据源的 HTTP 端点。

Python

python/chat/selection-input/main.py
@app.route('/', methods=['POST'])
def post() -> Mapping[str, Any]:
  """Handle requests from Google Chat

  Returns:
      Mapping[str, Any]: The response
  """
  # Stores the Google Chat event
  chatEvent = request.get_json().get('chat')

  # Handle user interaction with multiselect.
  if chatEvent.get('widgetUpdatedPayload') is not None:
    return json.jsonify(query_contacts(request.get_json()))

  # Replies with a card that contains the multiselect menu.
  return json.jsonify({ 'hostAppDataAction': { 'chatDataAction': { 'createMessageAction': {
    'message': { 'cardsV2': [{
      'cardId': "contactSelector",
      'card': { 'sections':[{ 'widgets': [{
        'selectionInput': {
          'name': "contacts",
          'type': "MULTI_SELECT",
          'label': "Selected contacts",
          'multiSelectMaxSelectedItems': 3,
          'multiSelectMinQueryLength': 1,
          'externalDataSource': { 'function': FUNCTION_URL },
          # Suggested items loaded by default.
          # The list is static here but it could be dynamic.
          'items': [get_suggested_contact("3")]
        }
      }]}]}
    }]}
  }}}})


def query_contacts(event: dict) -> dict:
  """Get contact suggestions based on text typed by users.

  Args:
      event (Mapping[str, Any]): The event object that contains the user's query

  Returns:
      Mapping[str, Any]: The response with contact suggestions.
  """
  query = event.get("commonEventObject").get("parameters").get("autocomplete_widget_query")
  return { 'action': { 'modifyOperations': [{ 'updateWidget': { 'selectionInputWidgetSuggestions': { 'suggestions': list(
    filter(lambda e: query is None or query in e["text"], [
      # The list is static here but it could be dynamic.
      get_suggested_contact("1"), get_suggested_contact("2"), get_suggested_contact("3"), get_suggested_contact("4"), get_suggested_contact("5")
    # Only return items based on the query from the user
    ])
  )}}}]}}


def get_suggested_contact(id: str) -> dict:
  """Generate a suggested contact given an ID.

  Args:
      id (str): The ID of the contact to return.

  Returns:
      Mapping[str, Any]: The contact formatted as a selection item in the menu.
  """
  return {
    'value': id,
    'startIconUri': "https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png",
    'text': "Contact " + id
  }

将 FUNCTION_URL 替换为查询外部数据源的 HTTP 端点。

Java

java/chat/selection-input/src/main/java/com/google/chat/selectionInput/App.java
@SpringBootApplication
@RestController
// Web app that responds to events sent from a Google Chat space.
public class App {
  private static final String FUNCTION_URL = "your-function-url";

  public static void main(String[] args) {
    SpringApplication.run(App.class, args);
  }

  /**
   * Handle requests from Google Chat
   * 
   * @param event the event object sent by Google Chat
   * @return The response to be sent back to Google Chat
   */
  @PostMapping("/")
  @ResponseBody
  public GenericJson onEvent(@RequestBody JsonNode event) throws Exception {
    // Stores the Google Chat event
    JsonNode chatEvent = event.at("/chat");

    // Handle user interaction with multiselect.
    if (!chatEvent.at("/widgetUpdatedPayload").isEmpty()) {
      return queryContacts(event);
    }

    // Replies with a card that contains the multiselect menu.
    Message message = new Message().setCardsV2(List.of(new CardWithId()
      .setCardId("contactSelector")
      .setCard(new GoogleAppsCardV1Card()
        .setSections(List.of(new GoogleAppsCardV1Section().setWidgets(List.of(new GoogleAppsCardV1Widget()
          .setSelectionInput(new GoogleAppsCardV1SelectionInput()
            .setName("contacts")
            .setType("MULTI_SELECT")
            .setLabel("Selected contacts")
            .setMultiSelectMaxSelectedItems(3)
            .setMultiSelectMinQueryLength(1)
            .setExternalDataSource(new GoogleAppsCardV1Action().setFunction(FUNCTION_URL))
            // Suggested items loaded by default.
            // The list is static here but it could be dynamic.
            .setItems(List.of(getSuggestedContact("3")))))))))));

    return new GenericJson() {{
      put("hostAppDataAction", new GenericJson() {{
        put("chatDataAction", new GenericJson() {{
          put("createMessageAction", new GenericJson() {{
            put("message", message);
          }});
        }});
      }});
    }};
  }

  /**
   * Get contact suggestions based on text typed by users.
   *
   * @param event the event object that contains the user's query.
   * @return The response with contact suggestions.
   */
  GenericJson queryContacts(JsonNode event) throws Exception {
    String query = event.at("/commonEventObject/parameters/autocomplete_widget_query").asText();
    List<GoogleAppsCardV1SelectionItem> suggestions = List.of(
      // The list is static here but it could be dynamic.
      getSuggestedContact("1"), getSuggestedContact("2"), getSuggestedContact("3"), getSuggestedContact("4"), getSuggestedContact("5")
    // Only return items based on the query from the user
    ).stream().filter(e -> query == null || e.getText().indexOf(query) > -1).toList();

    return new GenericJson() {{
      put("action", new GenericJson() {{
        put("modifyOperations", List.of(new GenericJson() {{
          put("updateWidget", new GenericJson() {{
            put("selectionInputWidgetSuggestions", new GenericJson() {{
              put("suggestions", suggestions);
            }});
          }});
        }}));
      }});
    }};
  }

  /**
   * Generate a suggested contact given an ID.
   * 
   * @param id The ID of the contact to return.
   * @return The contact formatted as a selection item in the menu.
   */
  GoogleAppsCardV1SelectionItem getSuggestedContact(String id) {
    return new GoogleAppsCardV1SelectionItem()
      .setValue(id)
      .setStartIconUri("https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png")
      .setText("Contact " + id);
  }
}

将 FUNCTION_URL 替换为查询外部数据源的 HTTP 端点。

Apps 脚本

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

apps-script/chat/selection-input/selection-input.gs
/**
* Responds to a Message trigger in Google Chat.
*
* @param {Object} event the event object from Google Chat
* @return {Object} Response from the Chat app.
*/
function onMessage(event) {
  // Replies with a card that contains the multiselect menu.
  return { hostAppDataAction: { chatDataAction: { createMessageAction: { message: {
    cardsV2: [{
      cardId: "contactSelector",
      card: { sections:[{ widgets: [{
        selectionInput: {
          name: "contacts",
          type: "MULTI_SELECT",
          label: "Selected contacts",
          multiSelectMaxSelectedItems: 3,
          multiSelectMinQueryLength: 1,
          externalDataSource: { function: "queryContacts" },
          // Suggested items loaded by default.
          // The list is static here but it could be dynamic.
          items: [getSuggestedContact("3")]
        }
      }]}]}
    }]
  }}}}};
}

/**
* Get contact suggestions based on text typed by users.
*
* @param {Object} event the event object that contains the user's query
* @return {Object} suggestions
*/
function queryContacts(event) {
  const query = event.commonEventObject.parameters["autocomplete_widget_query"];
  return { action: { modifyOperations: [{ updateWidget: { selectionInputWidgetSuggestions: { suggestions: [
    // The list is static here but it could be dynamic.
    getSuggestedContact("1"), getSuggestedContact("2"), getSuggestedContact("3"), getSuggestedContact("4"), getSuggestedContact("5")
  // Only return items based on the query from the user.
  ].filter(e => !query || e.text.includes(query)) }}}]}};
}

/**
* Generate a suggested contact given an ID.
*
* @param {String} id The ID of the contact to return.
* @return {Object} The contact formatted as a selection item in the menu.
*/
function getSuggestedContact(id) {
  return {
    value: id,
    startIconUri: "https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png",
    text: "Contact " + id
  };
}

将数据转移到另一张卡

用户通过卡片提交信息后,您可能需要返回其他卡片,以执行以下任一操作:

  • 通过创建不同的部分,帮助用户填写较长的表单。
  • 让用户预览并确认初始卡片中的信息,以便他们在提交之前检查自己的回答。
  • 动态填充表单的其余部分。例如,为了提示用户创建预约,Chat 应用可以显示一个初始卡片,请求预约原因,然后根据预约类型填充另一个提供可用时间的卡片。

如需传输从初始卡片输入的数据,您可以使用 actionParameters 构建 button widget,其中包含 widget 的 name 和用户输入的值,如以下示例所示:

Node.js

node/chat/contact-form-app/index.js
{ buttonList: { buttons: [{
  text: "SUBMIT",
  onClick: { action: {
    function: FUNCTION_URL,
    parameters: [
      { key: "actionName", value: "submitDialog" },
      // Pass input values as parameters for last dialog step (submission)
      { key: "contactName", value: name },
      { key: "contactBirthdate", value: birthdate },
      { key: "contactType", value: type }
    ]
  }}
}]}}

将 FUNCTION_URL 替换为处理按钮点击的 HTTP 端点。

Python

python/chat/contact-form-app/main.py
{ 'buttonList': { 'buttons': [{
  'text': "SUBMIT",
  'onClick': { 'action': {
    'function': FUNCTION_URL,
    'parameters': [
      { 'key': "actionName", 'value': "submitDialog" },
      # Pass input values as parameters for last dialog step (submission)
      { 'key': "contactName", 'value': name },
      { 'key': "contactBirthdate", 'value': birthdate },
      { 'key': "contactType", 'value': type }
    ]
  }}
}]}}

将 FUNCTION_URL 替换为处理按钮点击的 HTTP 端点。

Java

java/chat/contact-form-app/src/main/java/com/google/chat/contact/App.java
new GoogleAppsCardV1Widget().setButtonList(new GoogleAppsCardV1ButtonList().setButtons(List.of(
  new GoogleAppsCardV1Button()
    .setText("SUBMIT")
    .setOnClick(new GoogleAppsCardV1OnClick().setAction(new GoogleAppsCardV1Action()
      .setFunction(FUNCTION_URL)
      .setParameters(List.of(
        new GoogleAppsCardV1ActionParameter().setKey("actionName").setValue("submitDialog"),
        // Pass input values as parameters for last dialog step (submission)
        new GoogleAppsCardV1ActionParameter().setKey("contactName").setValue(name),
        new GoogleAppsCardV1ActionParameter().setKey("contactBirthdate").setValue(birthdate),
        new GoogleAppsCardV1ActionParameter().setKey("contactType").setValue(type))))))))))));

将 FUNCTION_URL 替换为处理按钮点击的 HTTP 端点。

Apps 脚本

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

apps-script/chat/contact-form-app/Code.gs
{ buttonList: { buttons: [{
  text: "SUBMIT",
  onClick: { action: {
    function: "submitDialog",
    // Pass input values as parameters for last dialog step (submission)
    parameters: [
      { key: "contactName", value: name },
      { key: "contactBirthdate", value: birthdate },
      { key: "contactType", value: type }
    ]
  }}
}]}}

当用户点击该按钮时,您的 Chat 应用会收到一个事件对象,您可以从中接收数据。

回复表单提交

收到来自卡片消息或对话框的数据后,Chat 应用会通过确认收到或返回错误来做出响应。

在以下示例中,Chat 应用发送了一条短信,确认已成功收到从卡片消息提交的表单。

Node.js

node/chat/contact-form-app/index.js
return { hostAppDataAction: { chatDataAction: { createMessageAction: { message: {
  text: "✅ " + event.commonEventObject.parameters["contactName"] + " has been added to your contacts."
}}}}};

Python

python/chat/contact-form-app/main.py
return { 'hostAppDataAction': { 'chatDataAction': { 'createMessageAction': { 'message': {
  'text': "✅ " + event.get('commonEventObject').get('parameters')["contactName"] + " has been added to your contacts."
}}}}}

Java

java/chat/contact-form-app/src/main/java/com/google/chat/contact/App.java
return new GenericJson() {{
  put("hostAppDataAction", new GenericJson() {{
    put("chatDataAction", new GenericJson() {{
      put("createMessageAction", new GenericJson() {{
        put("message", new Message()
          .setText( "✅ " + event.at("/commonEventObject/parameters/contactName").asText() +
                    " has been added to your contacts."));
      }});
    }});
  }});
}};

Apps 脚本

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

apps-script/chat/contact-form-app/Code.gs
return { hostAppDataAction: { chatDataAction: { createMessageAction: { message: {
  text: "✅ " + event.commonEventObject.parameters["contactName"] + " has been added to your contacts."
}}}}};

如需处理和关闭对话框,您需要返回一个 RenderActions 对象,该对象用于指定您是要发送确认消息、更新原始消息或卡片,还是仅关闭对话框。如需了解相关步骤,请参阅关闭对话框。

问题排查

本部分介绍了在 Chat 中与对话框互动时,针对特定错误代码和运行时行为的问题排查步骤。

对话互动返回“调用插件时出现未指明的错误”

与对话框互动时,如果您看到错误日志中包含消息“Unspecified error invoking the add-on.”(调用插件时出现未指明的错误)和代码 13,通常表示发生了内部错误,或者 Chat 应用的 HTTP 端点未能处理请求或返回有效响应。

如需排查此错误,请执行以下操作:

  • 检查 HTTP 端点的日志,看看是否有任何未处理的异常或崩溃。
  • 验证您的端点是否会在 30 秒内响应请求。如果端点运行时间超过 30 秒,Chat 将无法处理响应,并且互动会失败。如需了解详情,请参阅速率限制和最佳实践。
  • 确保您的端点返回有效响应。对于对话框提交,端点必须返回采用正确 JSON 格式的 RenderActions 对象。如果响应格式不正确或不包含必填字段,对话框互动可能会失败。

当 Google Chat 应用或卡片返回错误时,Chat 界面会显示一条消息,提示“出了点问题”。 或“无法处理您的请求”。有时,Chat 界面不会显示任何出错提示,但 Chat 应用或卡片会产生意外结果;例如,卡片消息可能不会显示。

虽然 Chat 界面中可能不会显示出错提示,但如果为 Chat 应用启用了日志记录,则可以使用描述性出错提示和日志数据来帮助您修复错误。如需查看、调试和修复错误方面的帮助,请参阅排查和修复 Google Chat 错误。

非插件的 Chat 扩展应用:读取用户在卡片上输入的表单数据

以下文档适用于非 Google Workspace 插件的 Chat 应用。如需迁移非插件的 Chat 扩展应用,请参阅将 Google Chat 扩展应用转换为 Google Workspace 插件。

使用卡片构建表单

如需查看非插件的 Chat 应用示例,该应用使用包含文本输入、日期时间选择器和选择输入的联系表单,请参阅以下代码:

Node.js

node/contact-form-app/index.js
/**
 * The section of the contact card that contains the form input widgets. Used in a dialog and card message.
 * To add and preview widgets, use the Card Builder: https://addons.gsuite.google.com/uikit/builder
 */
const CONTACT_FORM_WIDGETS = [
  {
    "textInput": {
      "name": "contactName",
      "label": "First and last name",
      "type": "SINGLE_LINE"
    }
  },
  {
    "dateTimePicker": {
      "name": "contactBirthdate",
      "label": "Birthdate",
      "type": "DATE_ONLY"
    }
  },
  {
    "selectionInput": {
      "name": "contactType",
      "label": "Contact type",
      "type": "RADIO_BUTTON",
      "items": [
        {
          "text": "Work",
          "value": "Work",
          "selected": false
        },
        {
          "text": "Personal",
          "value": "Personal",
          "selected": false
        }
      ]
    }
  }
];

Python

python/contact-form-app/main.py
# The section of the contact card that contains the form input widgets. Used in a dialog and card message.
# To add and preview widgets, use the Card Builder: https://addons.gsuite.google.com/uikit/builder
CONTACT_FORM_WIDGETS = [
  {
    "textInput": {
      "name": "contactName",
      "label": "First and last name",
      "type": "SINGLE_LINE"
    }
  },
  {
    "dateTimePicker": {
      "name": "contactBirthdate",
      "label": "Birthdate",
      "type": "DATE_ONLY"
    }
  },
  {
    "selectionInput": {
      "name": "contactType",
      "label": "Contact type",
      "type": "RADIO_BUTTON",
      "items": [
        {
          "text": "Work",
          "value": "Work",
          "selected": False
        },
        {
          "text": "Personal",
          "value": "Personal",
          "selected": False
        }
      ]
    }
  }
]

Java

java/contact-form-app/src/main/java/com/google/chat/contact/App.java
// The section of the contact card that contains the form input widgets. Used in a dialog and card message.
// To add and preview widgets, use the Card Builder: https://addons.gsuite.google.com/uikit/builder
final static private List<GoogleAppsCardV1Widget> CONTACT_FORM_WIDGETS = List.of(
  new GoogleAppsCardV1Widget().setTextInput(new GoogleAppsCardV1TextInput()
    .setName("contactName")
    .setLabel("First and last name")
    .setType("SINGLE_LINE")),
  new GoogleAppsCardV1Widget().setDateTimePicker(new GoogleAppsCardV1DateTimePicker()
    .setName("contactBirthdate")
    .setLabel("Birthdate")
    .setType("DATE_ONLY")),
  new GoogleAppsCardV1Widget().setSelectionInput(new GoogleAppsCardV1SelectionInput()
    .setName("contactType")
    .setLabel("Contact type")
    .setType("RADIO_BUTTON")
    .setItems(List.of(
      new GoogleAppsCardV1SelectionItem()
        .setText("Work")
        .setValue("Work")
        .setSelected(false),
      new GoogleAppsCardV1SelectionItem()
        .setText("Personal")
        .setValue("Personal")
        .setSelected(false)))));

Apps 脚本

apps-script/contact-form-app/contactForm.gs
/**
 * The section of the contact card that contains the form input widgets. Used in a dialog and card message.
 * To add and preview widgets, use the Card Builder: https://addons.gsuite.google.com/uikit/builder
 */
const CONTACT_FORM_WIDGETS = [
  {
    "textInput": {
      "name": "contactName",
      "label": "First and last name",
      "type": "SINGLE_LINE"
    }
  },
  {
    "dateTimePicker": {
      "name": "contactBirthdate",
      "label": "Birthdate",
      "type": "DATE_ONLY"
    }
  },
  {
    "selectionInput": {
      "name": "contactType",
      "label": "Contact type",
      "type": "RADIO_BUTTON",
      "items": [
        {
          "text": "Work",
          "value": "Work",
          "selected": false
        },
        {
          "text": "Personal",
          "value": "Personal",
          "selected": false
        }
      ]
    }
  }
];

从互动微件接收数据

每当用户点击按钮时,非插件的 Chat 应用都会收到互动事件,具体取决于按钮的位置:

  • 如果按钮位于消息或对话框中,则非插件的 Chat 应用会收到包含互动相关信息的 CARD_CLICKED 互动事件。CARD_CLICKED 互动事件的载荷包含一个 common.formInputs 对象 (event.common.formInputs),其中包含用户输入的任何值。

    您可以从对象 common.formInputs.WIDGET_NAME 中检索值,其中 WIDGET_NAME 是您为 widget 指定的 name 字段。这些值会以微件的特定数据类型(表示为 Inputs 对象)返回。

    以下示例显示了 CARD_CLICKED 互动事件的一部分,其中用户为每个 widget 输入了值:

    HTTP

    {
      "type": "CARD_CLICKED",
      "common": { "formInputs": {
        "contactName": { "stringInputs": {
          "value": ["Kai 0"]
        }},
        "contactBirthdate": { "dateInput": {
          "msSinceEpoch": 1000425600000
        }},
        "contactType": { "stringInputs": {
          "value": ["Personal"]
        }}
      }}
    }
    

    Apps 脚本

    {
      "type": "CARD_CLICKED",
      "common": { "formInputs": {
        "contactName": { "": { "stringInputs": {
          "value": ["Kai 0"]
        }}},
        "contactBirthdate": { "": { "dateInput": {
          "msSinceEpoch": 1000425600000
        }}},
          "contactType": { "": { "stringInputs": {
          "value": ["Personal"]
        }}}
      }}
    }
    
  • 如果按钮位于首页,则非插件的 Chat 应用会收到 SUBMIT_FORM 互动事件。互动事件的载荷包含一个 commonEventObject.formInputs 对象 (event.commonEventObject.formInputs),其中包含用户输入的任何值。

    您可以从对象 commonEventObject.formInputs.WIDGET_NAME 中检索值,其中 WIDGET_NAME 是您为 widget 指定的 name 字段。这些值会作为微件的特定数据类型(表示为 Inputs 对象)返回。

    以下示例显示了 SUBMIT_FORM 互动事件的一部分,其中用户为每个 widget 输入了值:

    HTTP

    {
      "type": "SUBMIT_FORM",
      "commonEventObject": { "formInputs": {
        "contactName": { "stringInputs": {
          "value": ["Kai 0"]
        }},
        "contactBirthdate": { "dateInput": {
          "msSinceEpoch": 1000425600000
        }},
        "contactType": { "stringInputs": {
          "value": ["Personal"]
        }}
      }}
    }
    

    Apps 脚本

    {
      "type": "SUBMIT_FORM",
      "commonEventObject": { "formInputs": {
        "contactName": { "": { "stringInputs": {
          "value": ["Kai 0"]
        }}},
        "contactBirthdate": { "": { "dateInput": {
          "msSinceEpoch": 1000425600000
        }}},
          "contactType": { "": { "stringInputs": {
          "value": ["Personal"]
        }}}
      }}
    }
    

为了接收数据,非插件的 Chat 应用会处理互动事件,以获取用户输入到 widget 中的值。下表展示了如何获取给定表单输入 widget 的值。对于每个 widget,该表显示了 widget 接受的数据类型、值在互动事件中的存储位置以及示例值。

表单输入微件 输入数据类型 互动事件的输入值 示例值
textInput stringInputs event.common.formInputs.contactName.stringInputs.value[0] Kai O
selectionInput stringInputs 如需获取第一个或唯一的值,请使用 event.common.formInputs.contactType.stringInputs.value[0] Personal
dateTimePicker 仅接受日期。 dateInput event.common.formInputs.contactBirthdate.dateInput.msSinceEpoch。 1000425600000

将数据转移到另一张卡

如需从非插件的 Chat 应用中的初始卡片传输数据输入,请使用包含 widget 的 name 和用户输入的值的 actionParameters 构建 button widget,如以下示例所示:

Node.js

node/contact-form-app/index.js
buttonList: { buttons: [{
  text: "Submit",
  onClick: { action: {
    function: "submitForm",
    parameters: [{
      key: "contactName", value: name }, {
      key: "contactBirthdate", value: birthdate }, {
      key: "contactType", value: type
    }]
  }}
}]}

Python

python/contact-form-app/main.py
'buttonList': { 'buttons': [{
  'text': "Submit",
  'onClick': { 'action': {
    'function': "submitForm",
    'parameters': [{
      'key': "contactName", 'value': name }, {
      'key': "contactBirthdate", 'value': birthdate }, {
      'key': "contactType", 'value': type
    }]
  }}
}]}

Java

java/contact-form-app/src/main/java/com/google/chat/contact/App.java
new GoogleAppsCardV1Widget().setButtonList(new GoogleAppsCardV1ButtonList().setButtons(List.of(new GoogleAppsCardV1Button()
  .setText("Submit")
  .setOnClick(new GoogleAppsCardV1OnClick().setAction(new GoogleAppsCardV1Action()
    .setFunction("submitForm")
    .setParameters(List.of(
      new GoogleAppsCardV1ActionParameter().setKey("contactName").setValue(name),
      new GoogleAppsCardV1ActionParameter().setKey("contactBirthdate").setValue(birthdate),
      new GoogleAppsCardV1ActionParameter().setKey("contactType").setValue(type))))))))));

Apps 脚本

apps-script/contact-form-app/main.gs
buttonList: { buttons: [{
  text: "Submit",
  onClick: { action: {
    function: "submitForm",
    parameters: [{
      key: "contactName", value: name }, {
      key: "contactBirthdate", value: birthdate }, {
      key: "contactType", value: type
    }]
  }}
}]}

当用户点击该按钮时,非插件的 Chat 应用会收到 CARD_CLICKED 互动事件,您可以从中接收数据。

回复表单提交

在以下示例中,非插件的 Chat 应用会发送一条短信,确认已成功收到从对话框或卡片消息提交的表单:

Node.js

node/contact-form-app/index.js
const contactName = event.common.parameters["contactName"];
// Checks to make sure the user entered a contact name.
// If no name value detected, returns an error message.
const errorMessage = "Don't forget to name your new contact!";
if (!contactName && event.dialogEventType === "SUBMIT_DIALOG") {
  return { actionResponse: {
    type: "DIALOG",
    dialogAction: { actionStatus: {
      statusCode: "INVALID_ARGUMENT",
      userFacingMessage: errorMessage
    }}
  }};
}

Python

python/contact-form-app/main.py
contact_name = event.get('common').get('parameters')["contactName"]
# Checks to make sure the user entered a contact name.
# If no name value detected, returns an error message.
error_message = "Don't forget to name your new contact!"
if contact_name == "" and "SUBMIT_DIALOG" == event.get('dialogEventType'):
  return { 'actionResponse': {
    'type': "DIALOG",
    'dialogAction': { 'actionStatus': {
      'statusCode': "INVALID_ARGUMENT",
      'userFacingMessage': error_message
    }}
  }}

Java

java/contact-form-app/src/main/java/com/google/chat/contact/App.java
String contactName = event.at("/common/parameters/contactName").asText();
// Checks to make sure the user entered a contact name.
// If no name value detected, returns an error message.
String errorMessage = "Don't forget to name your new contact!";
if (contactName.isEmpty() && event.at("/dialogEventType") != null && "SUBMIT_DIALOG".equals(event.at("/dialogEventType").asText())) {
  return new Message().setActionResponse(new ActionResponse()
    .setType("DIALOG")
    .setDialogAction(new DialogAction().setActionStatus(new ActionStatus()
      .setStatusCode("INVALID_ARGUMENT")
      .setUserFacingMessage(errorMessage))));
}

Apps 脚本

apps-script/contact-form-app/main.gs
const contactName = event.common.parameters["contactName"];
// Checks to make sure the user entered a contact name.
// If no name value detected, returns an error message.
const errorMessage = "Don't forget to name your new contact!";
if (!contactName && event.dialogEventType === "SUBMIT_DIALOG") {
  return { actionResponse: {
    type: "DIALOG",
    dialogAction: { actionStatus: {
      statusCode: "INVALID_ARGUMENT",
      userFacingMessage: errorMessage
    }}
  }};
}

如需在非插件的 Chat 应用中处理和关闭对话框,请返回一个 ActionResponse 对象,该对象指定您是要发送确认消息、更新原始消息或卡片,还是仅关闭对话框。如需了解相关步骤,请参阅关闭对话框。