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。
自定进度的环境设置
- 打开 Google Cloud 控制台,然后创建一个项目。
请记住项目 ID,它在所有 Google Cloud 项目中都是唯一的名称(上述名称已被占用,您无法使用,抱歉!)。它稍后将在此 Codelab 中被称为PROJECT_ID
。
- 接下来,为了使用 Google Cloud 资源,请在 Cloud 控制台中启用结算功能。
运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。请务必按照本 Codelab 末尾的“清理”部分中的所有说明操作,该部分介绍了如何关停资源,以免产生超出本教程范围的结算费用。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。
Google Cloud Shell
虽然 Google Cloud 可以从笔记本电脑远程操作,但在此 Codelab 中,我们将使用 Google Cloud Shell,这是一个在 Google Cloud 中运行的命令行环境。
激活 Cloud Shell
- 在 Cloud Console 中,点击激活 Cloud Shell
。
首次打开 Cloud Shell 时,系统会显示一条描述性欢迎消息。如果您看到欢迎信息,请点击继续。欢迎消息不会再次显示。欢迎消息如下:
预配和连接到 Cloud Shell 只需花几分钟时间。连接后,您会看到 Cloud Shell 终端:
这个虚拟机已加载了您需要的所有开发工具。它提供了一个持久的 5GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证。您在此 Codelab 中执行的所有操作都可以使用浏览器或 Chromebook 完成。连接到 Cloud Shell 后,您应该会看到自己已通过身份验证,并且相关项目已设置为您的项目 ID。 - 在 Cloud Shell 中运行以下命令以确认您已通过身份验证:
如果系统提示您授权 Cloud Shell 进行 GCP API 调用,请点击授权。gcloud auth list
命令输出 如果您的账号未默认选中,请运行以下命令:Credentialed Accounts ACTIVE ACCOUNT * <my_account>@<my_domain.com>
$ gcloud config set account <ACCOUNT>
- 确认您已选择正确的项目。在 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 编辑器。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. 创建初始聊天应用
初始化项目
首先,您将创建和部署一个简单的“Hello World”应用。Chat 应用属于 Web 服务,用于响应 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
的文件,我们将在下一步中创建该文件。
创建聊天应用后端
现在开始创建应用。创建一个名为 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 控制台中配置 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
属性中已部署函数的网址。您将在下一步中用到此网址。
配置应用
如需配置此应用,请转到 Cloud 控制台中的 Chat 配置页面
- 取消选中将此 Chat 扩展应用作为 Workspace 插件构建,然后点击停用进行确认。
- 在应用名称中,输入“PollCodelab”。
- 在头像网址中,输入
https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png
。 - 在说明中,输入“Codelab 的投票应用”。
- 在功能下,选择接收一对一消息和加入聊天室和群组对话。
- 在连接设置下,选择 HTTP 端点网址,然后粘贴 Cloud Functions 函数的网址(上一部分中的
httpsTrigger.url
属性)。 - 在权限下方,选择您网域中的特定人员和群组,然后输入您的电子邮件地址。
- 点击保存。
该应用现在可以发送消息了。
测试应用
在继续操作之前,请在 Google Chat 中将应用添加到聊天室,以检查其是否正常运行。
- 转到 Google Chat。
- 在 Chat 旁边,点击 + > 查找应用。
- 在搜索中输入“PollCodelab”。
- 点击 Chat。
- 如需向应用发送消息,请输入“Hello”,然后按 Enter 键。
应用应以简短的问候消息进行响应。
现在,我们已经拥有了一个基本的框架,可以将其转变为更实用的应用!
5. 构建投票功能
应用运作方式的简要概览
该应用包含两个主要部分:
- 一条斜杠命令,显示用于配置投票的对话框。
- 用于投票和查看结果的互动式卡片。
应用还需要一些状态来存储投票配置和结果。这可以通过 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 与所选索引对应的形式存储。
当然,有许多不同的方法,但此方法为在聊天室中进行快速投票提供了一个很好的起点。
实现投票配置命令
如需允许用户发起和配置投票,请设置一个可打开对话框的斜杠命令。此过程分为多个步骤:
- 注册用于启动投票的斜杠命令。
- 创建用于设置投票的对话框。
- 让应用识别并处理斜杠命令。
- 创建有助于在投票中进行投票的互动式卡片。
- 实现可让应用开展投票活动的代码。
- 重新部署 Cloud Functions 函数。
注册斜杠命令
要注册斜杠命令,请返回控制台中的 Chat 配置页面(API 和服务 > 信息中心 > Hangouts Chat API > 配置)。
- 在 Slash 命令下,点击添加新的 Slash 命令。
- 在名称中,输入“/poll”
- 在 Command id 中,输入“1”
- 在说明中,输入“发起投票”。
- 选择打开对话框。
- 点击完成。
- 点击保存。
应用现在可以识别 /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 示例,请在 Card Builder 工具中查看。
处理斜杠命令
当斜杠命令发送到应用时,会显示为 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_poll
的 CARD_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 示例。
实现投票操作
投票卡片包含每个选项的按钮。选项的索引和投票的序列化状态会附加到该按钮。应用会接收包含操作 vote
的 CARD_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
方法。
不过,这里有个问题。
提交投票配置后,应用需要执行两项操作:
- 关闭对话框。
- 向聊天室发布包含投票卡的新的消息。
很遗憾,直接回复 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 和管理 > 管理资源。
- 在项目列表中,选择您的项目,然后点击删除。
- 在对话框中输入项目 ID,然后点击关停以删除项目。
了解详情
如需详细了解如何开发 Chat 应用,请参阅:
如需详细了解如何在 Google Cloud 控制台中进行开发,请参阅: