使用 Node.js 为 Google Chat 构建互动式意见调查应用

1. 简介

Google Chat 应用会将您的服务和资源直接导入 Google Chat 中,让用户无需离开对话窗口即可获取信息并快速采取行动。

在此 Codelab 中,您将学习如何使用 Node.js 和 Cloud Functions 构建和部署投票应用。

学习内容

  • 使用 Cloud Shell
  • 部署到 Cloud Functions
  • 使用斜杠命令和对话框获取用户输入
  • 创建互动式卡片

2. 设置和要求

创建一个 Google Cloud 项目,然后启用 Chat 应用要使用的 API 和服务

前提条件

要开发 Google Chat 应用,您需要有权访问 Google Chat 的 Google Workspace 帐号。如果您还没有 Google Workspace 帐号,请先创建一个帐号并登录,然后再继续学习此 Codelab。

自定进度的环境设置

  1. 打开 Google Cloud Console 并创建一个项目

    “选择项目”菜单“新建项目”按钮项目 ID

    请记住,项目 ID,它在所有 Google Cloud 项目中都是唯一名称(很抱歉,上述名称已被占用,您无法使用!)。它稍后将在此 Codelab 中被称为 PROJECT_ID
  1. 接下来,为了使用 Google Cloud 资源,请在 Cloud Console 中启用结算功能

此 Codelab 的运行费用应该不会太多。请务必按照此 Codelab 末尾的“清理”部分中的说明操作,了解如何关闭资源,以免产生超出本教程费用的情况。Google Cloud 的新用户有资格参与 $300USD 免费试用计划。

Google Cloud Shell

虽然 Google Cloud 可以从笔记本电脑远程操作,但在此 Codelab 中,我们将使用 Google Cloud Shell,这是一种在 Google Cloud 中运行的命令行环境。

激活 Cloud Shell

  1. 在 Cloud Console 中,点击激活 Cloud Shell Cloud Shell 图标

    菜单栏中的 Cloud Shell 图标

    首次打开 Cloud Shell 时,系统会显示一条描述性的欢迎消息。如果您看到欢迎辞,请点击继续。欢迎消息不会再显示。以下是欢迎消息:

    Cloud Shell 欢迎消息

    预配和连接到 Cloud Shell 只需片刻。连接后,您会看到 Cloud Shell 终端:

    Cloud Shell 终端

    此虚拟机加载了您需要的所有开发工具。它提供了一个持久的 5GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证。此 Codelab 中的所有工作均可通过浏览器或 Chromebook 完成。连接到 Cloud Shell 后,您应该会看到自己已通过身份验证,并且相关项目已设置为您的项目 ID。
  2. 在 Cloud Shell 中运行以下命令以确认您已通过身份验证:
    gcloud auth list
    
    如果系统提示您授权 Cloud Shell 进行 GCP API 调用,请点击授权

    命令输出
    Credentialed Accounts
    ACTIVE  ACCOUNT
    *       <my_account>@<my_domain.com>
    
    如果您的帐号默认处于选中状态,请运行以下命令:
    $ gcloud config set account <ACCOUNT>
    
  1. 确认您已选择正确的项目。在 Cloud Shell 中,运行以下命令:
    gcloud config list project
    
    命令输出
    [core]
    project = <PROJECT_ID>
    
    如果系统未返回正确的项目,您可以使用以下命令进行设置:
    gcloud config set project <PROJECT_ID>
    
    命令输出
    Updated property [core/project].
    

完成此 Codelab 后,您将使用命令行操作和修改文件。如需修改文件,您可以点击 Cloud Shell 工具栏右侧的打开编辑器,以使用 Cloud Shell 的内置代码编辑器 Cloud Shell Editor。Cloud Shell 中还提供了 Vim 和 Emacs 等热门编辑器。

3.启用 Cloud Functions、Cloud Build 和 Google Chat API

在 Cloud Shell 中,启用以下 API 和服务:

gcloud services enable \
  cloudfunctions \
  cloudbuild.googleapis.com \
  chat.googleapis.com

此操作可能需要一些时间才能完成。

完成后,系统会显示类似下面的成功消息:

Operation "operations/acf.cc11852d-40af-47ad-9d59-477a12847c9e" finished successfully.

4.创建 Chat 初始应用

初始化项目

首先,您将创建和部署简单的“Hello world”应用。聊天应用是响应 https 请求并使用 JSON 载荷进行响应的网络服务。对于此应用,您将使用 Node.js 和 Cloud Functions

Cloud Shell 中,新建名为 poll-app 的目录并导航到该目录:

mkdir ~/poll-app
cd ~/poll-app

此 Codelab 的所有剩余工作以及您要创建的文件都将位于此目录中。

初始化 Node.js 项目:

npm init

NPM 会询问有关项目配置的多个问题,例如名称和版本。对于每个问题,请按 ENTER 接受默认值。默认入口点是一个名为 index.js 的文件,我们将在下面创建该文件。

创建 Chat 应用后端

开始创建应用的时候到了。创建名为 index.js 且包含以下内容的文件:

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  if (event.type === 'MESSAGE') {
    reply = {
        text: `Hello ${event.user.displayName}`
    };
  }
  res.json(reply)
}

应用不会执行太多操作,但没关系。您稍后将添加更多功能。

部署应用

要部署“Hello world”应用,您需要部署 Cloud Functions 函数,在 Google Cloud Console 中配置 Chat 应用,然后向该应用发送测试消息以验证部署。

部署 Cloud Functions 函数

如需部署“Hello world”应用的 Cloud Functions 函数,请输入以下命令:

gcloud functions deploy app --trigger-http --security-level=secure-always --allow-unauthenticated --runtime nodejs14

完成后,输出应如下所示:

availableMemoryMb: 256
buildId: 993b2ca9-2719-40af-86e4-42c8e4563a4b
buildName: projects/595241540133/locations/us-central1/builds/993b2ca9-2719-40af-86e4-42c8e4563a4b
entryPoint: app
httpsTrigger:
  securityLevel: SECURE_ALWAYS
  url: https://us-central1-poll-app-codelab.cloudfunctions.net/app
ingressSettings: ALLOW_ALL
labels:
  deployment-tool: cli-gcloud
name: projects/poll-app-codelab/locations/us-central1/functions/app
runtime: nodejs14
serviceAccountEmail: poll-app-codelab@appspot.gserviceaccount.com
sourceUploadUrl: https://storage.googleapis.com/gcf-upload-us-central1-66a01777-67f0-46d7-a941-079c24414822/94057943-2b7c-4b4c-9a21-bb3acffc84c6.zip
status: ACTIVE
timeout: 60s
updateTime: '2021-09-17T19:30:33.694Z'
versionId: '1'

请记下 httpsTrigger.url 属性中已部署函数的网址。您将在接下来的步骤中使用此变量。

配置应用

要配置应用,请转到Chat 配置页面(API 和服务>数字面板>Hangouts Chat API >配置)。

  1. 应用名称中,输入“PollCodelab”。
  2. 头像网址中输入 https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png
  3. Description 中,输入“Poll app for codelab”。
  4. 功能部分,选择应用可以直接发送消息应用可由多位用户在聊天室中工作
  5. 连接设置下,选择应用网址,然后粘贴 Cloud Functions 函数的网址(上一部分中的 httpsTrigger.url 属性)。
  6. 权限下方,选择您网域中的特定用户和群组,然后输入您的电子邮件地址。
  7. 点击保存

该应用现在可以发送消息了。

测试应用

在继续操作之前,请将应用添加到 Google Chat 聊天室,以检查该应用是否正常运行。

  1. 转到 Google Chat
  2. 在“聊天”旁边,依次点击 + > 查找应用
  3. 在搜索中输入“PollCodelab”。
  4. 点击聊天
  5. 要向应用发送消息,请输入“Hello”,然后按 Enter 键。

应用应该以简短的问候消息进行响应。

现在已经有了基本的框架,是时候将其变成更实用的东西了!

5. 构建投票功能

应用运作方式简要概览

该应用由两个主要部分组成:

  1. 一个斜杠命令,显示用于配置意见调查的对话框。
  2. 用于投票和查看结果的互动式卡片。

该应用还需要某种状态来存储投票配置和结果。这可以使用 Firestore 或任何数据库来完成,也可以将状态存储在应用消息中。由于此应用旨在供团队快速开展非正式投票活动,因此将状态存储在应用消息中非常适合此用例。

应用的数据模型(以 TypeScript 表示)如下:

interface Poll {
  /* Question/topic of poll */
  topic: string;
  /** User that submitted the poll */
  author: {
    /** Unique resource name of user */
    name: string;
    /** Display name */
    displayName: string;
  };
  /** Available choices to present to users */
  choices: string[];
  /** Map of user ids to the index of their selected choice */
  votes: { [key: string]: number };
}

除了主题或问题和选项列表之外,状态还包括作者 ID、姓名以及记录的投票。为防止用户多次投票,投票会存储为用户 ID 到他们所选索引的索引的映射。

当然,有许多不同的方法,但这提供了一个在空间内快速轮询的良好起点。

实现轮询配置命令

要允许用户发起和配置投票活动,请设置用于打开对话框斜杠命令。此过程包含多个步骤:

  1. 注册用于发起投票的斜杠命令。
  2. 创建用于设置投票活动的对话框。
  3. 让应用识别并处理斜杠命令。
  4. 创建互动式卡片,方便在投票活动中进行投票。
  5. 实现可让应用运行投票的代码。
  6. 重新部署 Cloud Functions 函数。

注册斜杠命令

要注册斜杠命令,请返回控制台中的 Chat 配置页面(API 和服务 > 信息中心 > Hangouts Chat API) > 配置)。

  1. 斜杠命令下,点击添加斜杠命令
  2. 名称中,输入“/poll”
  3. 命令 ID 中,输入“1”
  4. 说明中,输入“发起投票”。
  5. 选择打开对话框
  6. 点击完成
  7. 点击保存

应用现在可以识别 /poll 命令,并打开一个对话框。接下来,我们来配置该对话框。

以对话框的形式创建配置表单

斜杠命令用于打开一个对话框来配置投票主题和可能的选项。新建一个名为 config-form.js 的文件,其中包含以下内容:

/** Upper bounds on number of choices to present. */
const MAX_NUM_OF_OPTIONS = 5;

/**
 * Build widget with instructions on how to use form.
 *
 * @returns {object} card widget
 */
function helpText() {
  return {
    textParagraph: {
      text: 'Enter the poll topic and up to 5 choices in the poll. Blank options will be omitted.',
    },
  };
}

/**
 * Build the text input for a choice.
 *
 * @param {number} index - Index to identify the choice
 * @param {string|undefined} value - Initial value to render (optional)
 * @returns {object} card widget
 */
function optionInput(index, value) {
  return {
    textInput: {
      label: `Option ${index + 1}`,
      type: 'SINGLE_LINE',
      name: `option${index}`,
      value: value || '',
    },
  };
}

/**
 * Build the text input for the poll topic.
 *
 * @param {string|undefined} topic - Initial value to render (optional)
 * @returns {object} card widget
 */
function topicInput(topic) {
  return {
    textInput: {
      label: 'Topic',
      type: 'MULTIPLE_LINE',
      name: 'topic',
      value: topic || '',
    },
  };
}

/**
 * Build the buttons/actions for the form.
 *
 * @returns {object} card widget
 */
function buttons() {
  return {
    buttonList: {
      buttons: [
        {
          text: 'Submit',
          onClick: {
            action: {
              function: 'start_poll',
            },
          },
        },
      ],
    },
  };
}

/**
 * Build the configuration form.
 *
 * @param {object} options - Initial state to render with form
 * @param {string|undefined} options.topic - Topic of poll (optional)
 * @param {string[]|undefined} options.choices - Text of choices to display to users (optional)
 * @returns {object} card
 */
function buildConfigurationForm(options) {
  const widgets = [];
  widgets.push(helpText());
  widgets.push(topicInput(options.topic));
  for (let i = 0; i < MAX_NUM_OF_OPTIONS; ++i) {
    const choice = options?.choices?.[i];
    widgets.push(optionInput(i, choice));
  }
  widgets.push(buttons());

  // Assemble the card
  return {
    sections: [
      {
        widgets,
      },
    ],
  };
}

exports.MAX_NUM_OF_OPTIONS = MAX_NUM_OF_OPTIONS;
exports.buildConfigurationForm = buildConfigurationForm;

此代码会生成一个对话框表单,供用户设置意见调查。它还导出一个问题可以拥有的选项数上限的常量。最好将界面标记构建为无状态函数,并将任何状态作为参数传入。这有助于重复使用,之后此卡片将在不同的上下文中呈现。

此实现还将卡片分解为更小的单元或组件。虽然并非强制性要求,但该技术是一种最佳做法,因为它在构建复杂接口时往往更易于阅读和维护。

要查看其构建的完整 JSON 的示例,请在卡片构建工具中查看。

处理斜杠命令

斜杠命令在发送到应用后显示为 MESSAGE 事件。更新 index.js 以通过 MESSAGE 事件检查是否存在斜杠命令,并使用对话框进行响应。将 index.js 替换为以下内容:

const { buildConfigurationForm, MAX_NUM_OF_OPTIONS } = require('./config-form');

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  // Dispatch slash and action events
  if (event.type === 'MESSAGE') {
    const message = event.message;
    if (message.slashCommand?.commandId === '1') {
      reply = showConfigurationForm(event);
    }
  } else if (event.type === 'CARD_CLICKED') {
    if (event.action?.actionMethodName === 'start_poll') {
      reply = await startPoll(event);
    }
  }
  res.json(reply);
}

/**
 * Handles the slash command to display the config form.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function showConfigurationForm(event) {
  // Seed the topic with any text after the slash command
  const topic = event.message?.argumentText?.trim();
  const dialog = buildConfigurationForm({
    topic,
    choices: [],
  });
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        dialog: {
          body: dialog,
        },
      },
    },
  };
}

/**
 * Handle the custom start_poll action.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function startPoll(event) {
  // Not fully implemented yet -- just close the dialog
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        actionStatus: {
          statusCode: 'OK',
          userFacingMessage: 'Poll started.',
        },
      },
    },
  }
}

应用现在会在调用 /poll 命令时显示对话框。通过从 Cloud Shell 中重复使用 Cloud Functions 函数来测试交互。

gcloud functions deploy app --trigger-http --security-level=secure-always

部署 Cloud Functions 函数后,使用 /poll 命令向应用发送消息,以测试斜杠命令和对话框。对话框会发送一个包含自定义操作 start_pollCARD_CLICKED 事件。该事件会在更新后的入口点中进行处理,并在其中调用 startPoll 方法。目前,已对 startPoll 方法进行存根以仅关闭对话框。在下一部分中,您将实现投票功能,并将所有部分关联在一起。

实现投票卡片

如需实现应用的投票部分,请首先定义一个互动式卡片,为用户提供投票选项。

实现投票界面

创建一个名为 vote-card.js 的文件,其中包含以下内容:

/**
 * Creates a small progress bar to show percent of votes for an option. Since
 * width is limited, the percentage is scaled to 20 steps (5% increments).
 *
 * @param {number} voteCount - Number of votes for this option
 * @param {number} totalVotes - Total votes cast in the poll
 * @returns {string} Text snippet with bar and vote totals
 */
function progressBarText(voteCount, totalVotes) {
  if (voteCount === 0 || totalVotes === 0) {
    return '';
  }

  // For progress bar, calculate share of votes and scale it
  const percentage = (voteCount * 100) / totalVotes;
  const progress = Math.round((percentage / 100) * 20);
  return '▀'.repeat(progress);
}

/**
 * Builds a line in the card for a single choice, including
 * the current totals and voting action.
 *
 * @param {number} index - Index to identify the choice
 * @param {string|undefined} value - Text of the choice
 * @param {number} voteCount - Current number of votes cast for this item
 * @param {number} totalVotes - Total votes cast in poll
 * @param {string} state - Serialized state to send in events
 * @returns {object} card widget
 */
function choice(index, text, voteCount, totalVotes, state) {
  const progressBar = progressBarText(voteCount, totalVotes);
  return {
    keyValue: {
      bottomLabel: `${progressBar} ${voteCount}`,
      content: text,
      button: {
        textButton: {
          text: 'vote',
          onClick: {
            action: {
              actionMethodName: 'vote',
              parameters: [
                {
                  key: 'state',
                  value: state,
                },
                {
                  key: 'index',
                  value: index.toString(10),
                },
              ],
            },
          },
        },
      },
    },
  };
}

/**
 * Builds the card header including the question and author details.
 *
 * @param {string} topic - Topic of the poll
 * @param {string} author - Display name of user that created the poll
 * @returns {object} card widget
 */
function header(topic, author) {
  return {
    title: topic,
    subtitle: `Posted by ${author}`,
    imageUrl:
      'https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png',
    imageStyle: 'AVATAR',
  };
}

/**
 * Builds the configuration form.
 *
 * @param {object} poll - Current state of poll
 * @param {object} poll.author - User that submitted the poll
 * @param {string} poll.topic - Topic of poll
 * @param {string[]} poll.choices - Text of choices to display to users
 * @param {object} poll.votes - Map of cast votes keyed by user ids
 * @returns {object} card
 */
function buildVoteCard(poll) {
  const widgets = [];
  const state = JSON.stringify(poll);
  const totalVotes = Object.keys(poll.votes).length;

  for (let i = 0; i < poll.choices.length; ++i) {
    // Count votes for this choice
    const votes = Object.values(poll.votes).reduce((sum, vote) => {
      if (vote === i) {
        return sum + 1;
      }
      return sum;
    }, 0);
    widgets.push(choice(i, poll.choices[i], votes, totalVotes, state));
  }

  return {
    header: header(poll.topic, poll.author.displayName),
    sections: [
      {
        widgets,
      },
    ],
  };
}

exports.buildVoteCard = buildVoteCard;

实现方式与对话框类似,但互动卡片的标记与对话框略有不同。和以前一样,您可以在卡片制作工具中查看生成的 JSON 示例。

实现投票操作

投票卡片包含一个选项。该选择的索引和投票的序列化状态会附加到该按钮。应用收到包含操作 voteCARD_CLICKED,以及作为参数附加到按钮上的所有数据。

使用以下代码更新 index.js

const { buildConfigurationForm, MAX_NUM_OF_OPTIONS } = require('./config-form');
const { buildVoteCard } = require('./vote-card');

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  // Dispatch slash and action events
  if (event.type === 'MESSAGE') {
    const message = event.message;
    if (message.slashCommand?.commandId === '1') {
      reply = showConfigurationForm(event);
    }
  } else if (event.type === 'CARD_CLICKED') {
    if (event.action?.actionMethodName === 'start_poll') {
      reply = await startPoll(event);
    } else if (event.action?.actionMethodName === 'vote') {
        reply = recordVote(event);
    }
  }
  res.json(reply);
}

/**
 * Handles the slash command to display the config form.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function showConfigurationForm(event) {
  // Seed the topic with any text after the slash command
  const topic = event.message?.argumentText?.trim();
  const dialog = buildConfigurationForm({
    topic,
    choices: [],
  });
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        dialog: {
          body: dialog,
        },
      },
    },
  };
}

/**
 * Handle the custom start_poll action.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function startPoll(event) {
  // Not fully implemented yet -- just close the dialog
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        actionStatus: {
          statusCode: 'OK',
          userFacingMessage: 'Poll started.',
        },
      },
    },
  }
}

/**
 * Handle the custom vote action. Updates the state to record
 * the user's vote then rerenders the card.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function recordVote(event) {
  const parameters = event.common?.parameters;

  const choice = parseInt(parameters['index']);
  const userId = event.user.name;
  const state = JSON.parse(parameters['state']);

  // Add or update the user's selected option
  state.votes[userId] = choice;

  const card = buildVoteCard(state);
  return {
    thread: event.message.thread,
    actionResponse: {
      type: 'UPDATE_MESSAGE',
    },
    cards: [card],
  }
}

recordVote 方法会解析存储的状态并使用用户的投票进行更新,然后重新渲染卡片。每次更新时,投票结果都会进行序列化,并随卡片一起存储。

连接各组件

应用即将完成。随着斜杠命令和投票的实施,唯一剩下的工作就是完成 startPoll 方法。

不过,有个问题。

提交投票配置后,该应用需要执行两项操作:

  1. 关闭对话框。
  2. 使用投票卡片在聊天室中发布新消息。

很遗憾,HTTP 请求的直接回复只能执行一项请求,并且这必须是第一个请求。 要发布投票卡,应用必须使用 Chat API 异步创建新消息。

添加客户端库

运行以下命令以更新应用的依赖项,以包含 Node.js 版 Google API 客户端。

npm install --save googleapis

发起投票

index.js 更新为以下最终版本:

const { buildConfigurationForm, MAX_NUM_OF_OPTIONS } = require('./config-form');
const { buildVoteCard } = require('./vote-card');
const {google} = require('googleapis');

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  // Dispatch slash and action events
  if (event.type === 'MESSAGE') {
    const message = event.message;
    if (message.slashCommand?.commandId === '1') {
      reply = showConfigurationForm(event);
    }
  } else if (event.type === 'CARD_CLICKED') {
    if (event.action?.actionMethodName === 'start_poll') {
      reply = await startPoll(event);
    } else if (event.action?.actionMethodName === 'vote') {
        reply = recordVote(event);
    }
  }
  res.json(reply);
}

/**
 * Handles the slash command to display the config form.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function showConfigurationForm(event) {
  // Seed the topic with any text after the slash command
  const topic = event.message?.argumentText?.trim();
  const dialog = buildConfigurationForm({
    topic,
    choices: [],
  });
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        dialog: {
          body: dialog,
        },
      },
    },
  };
}

/**
 * Handle the custom start_poll action.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
async function startPoll(event) {
  // Get the form values
  const formValues = event.common?.formInputs;
  const topic = formValues?.['topic']?.stringInputs.value[0]?.trim();
  const choices = [];
  for (let i = 0; i < MAX_NUM_OF_OPTIONS; ++i) {
    const choice = formValues?.[`option${i}`]?.stringInputs.value[0]?.trim();
    if (choice) {
      choices.push(choice);
    }
  }

  if (!topic || choices.length === 0) {
    // Incomplete form submitted, rerender
    const dialog = buildConfigurationForm({
      topic,
      choices,
    });
    return {
      actionResponse: {
        type: 'DIALOG',
        dialogAction: {
          dialog: {
            body: dialog,
          },
        },
      },
    };
  }

  // Valid configuration, build the voting card to display
  // in the space
  const pollCard = buildVoteCard({
    topic: topic,
    author: event.user,
    choices: choices,
    votes: {},
  });
  const message = {
    cards: [pollCard],
  };
  const request = {
    parent: event.space.name,
    requestBody: message,
  };
  // Use default credentials (service account)
  const credentials = new google.auth.GoogleAuth({
    scopes: ['https://www.googleapis.com/auth/chat.bot'],
  });
  const chatApi = google.chat({
    version: 'v1',
    auth: credentials,
  });
  await chatApi.spaces.messages.create(request);

  // Close dialog
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        actionStatus: {
          statusCode: 'OK',
          userFacingMessage: 'Poll started.',
        },
      },
    },
  };
}

/**
 * Handle the custom vote action. Updates the state to record
 * the user's vote then rerenders the card.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function recordVote(event) {
  const parameters = event.common?.parameters;

  const choice = parseInt(parameters['index']);
  const userId = event.user.name;
  const state = JSON.parse(parameters['state']);

  // Add or update the user's selected option
  state.votes[userId] = choice;

  const card = buildVoteCard(state);
  return {
    thread: event.message.thread,
    actionResponse: {
      type: 'UPDATE_MESSAGE',
    },
    cards: [card],
  }
}

重新部署函数:

gcloud functions deploy app --trigger-http --security-level=secure-always

您现在应该能够完整使用该应用。尝试调用 /poll 命令可以提供问题和多个选项。提交后,系统会显示投票卡片。

投票,看看会发生什么。

当然,投票活动并不完全有用,所以请邀请一些朋友或同事试一试吧!

6.恭喜

恭喜!您已成功使用 Cloud Functions 构建和部署 Google Chat 应用。尽管此 Codelab 介绍了构建应用的许多核心概念,但还有很多需要探索。请参阅下面的资源,并记得清理您的项目以避免产生额外费用。

其他活动

如果您想更深入地探索 Chat 平台和此应用,可以尝试执行以下操作:

  • 如果用 @ 提及该应用,会发生什么情况?请尝试更新应用以改善行为。
  • 在卡片中,对投票空间中的投票状态进行序列化可以处理较小的空间,但有限制。请尝试切换到更好的选项。
  • 如果作者想修改投票活动或不再接受新投票,该怎么办?您将如何实现这些功能?
  • 应用端点尚不安全。请尝试添加一些验证,以确保请求来自 Google Chat。

以上只是改进应用的一些不同方式。尽情发挥您的想象力吧!

清理

为避免系统因本教程中使用的资源向您的 Google Cloud Platform 帐号收取费用,请执行以下操作:

  • 在 Cloud Console 中,转到管理资源页面。点击左上角的 菜单菜单图标 > IAM 和管理 > 管理资源
  1. 在项目列表中,选择您的项目,然后点击删除
  2. 在对话框中输入项目 ID,然后点击关停以删除项目。

了解详情

如需详细了解如何开发 Chat 应用,请参阅:

如需详细了解如何在 Google Cloud Console 中进行开发,请参阅: