Zoom Team Chat 表单提交开发实战:Chatbot API 表单卡片、chat_message.submit Webhook 与后端校验
【免费下载链接】knowledge-work-pluginsOpen source repository of plugins primarily intended for knowledge workers to use in Claude Cowork项目地址: https://gitcode.com/GitHub_Trending/kn/knowledge-work-plugins
导读
在 Zoom Team Chat 的 Chatbot API 中,消息卡片(Message Card)不仅能展示信息,还能内嵌表单收集用户输入——这是构建审批、数据采集、多步交互机器人最常用的能力之一。本文以仓库内 表单提交指南 为核心骨架,结合 消息卡片组件参考、Webhook 事件参考 与 Chatbot 完整示例 等文档,带你从发送一张带表单字段的卡片开始,到接收chat_message.submitWebhook、服务端校验输入、再以更新后的卡片或确认消息完成闭环。读完你将掌握 Zoom Team Chat 表单功能的完整开发链路,以及服务端校验类型、将提交文本视为不可信输入等关键安全实践。
一、认识表单提交:卡片内表单 + Webhook 回传
在 Zoom Team Chat 的 Chatbot API 中,交互的核心模型是消息卡片:机器人通过POST https://api.zoom.us/v2/im/chat/messages发送带有交互组件的卡片,用户在卡片上填写或选择,Zoom 再以 Webhook 把结果回传给我们的服务端。
表单提交(Form Submissions)正是这一模型中的典型场景。表单提交指南 给出的核心模式是三步闭环:
- 发送一张带表单字段的卡片(Send a card with form fields);
- 接收
chat_message.submitWebhook(Receivechat_message.submitwebhook); - 校验输入并以更新后的卡片或确认消息回复(Validate inputs and respond with an updated card or confirmation message)。
用一张流程图表示:
机器人发送表单卡片 → 用户在 Zoom 内填写/选择 → 提交 ↓ Zoom 推送 chat_message.submit Webhook 到 Bot Endpoint URL ↓ 服务端校验输入(类型、范围、必填) → 回复确认消息或更新后的卡片需要注意:这一能力属于Chatbot API(机器人身份),而非 Team Chat API(用户身份)。从 技能总入口 SKILL.md 的选型表可以看到,机器人消息使用 Client Credentials(client_credentials)授权,端点族为/v2/im/chat/messages,并且"Create messages with buttons/forms"与"Handle user interactions"都明确指向 Chatbot API。
二、用消息卡片构建表单
表单由消息卡片中的交互组件组成。根据 消息卡片组件参考,与表单直接相关的组件有三种:
2.1form_field:文本输入框
{ "type": "form_field", "editable": true, "text": "Enter your name" }editable:是否允许用户编辑;text:输入框的提示/占位文本。
2.2dropdown:下拉选择菜单
{ "type": "dropdown", "select_items": [ { "text": "Option 1", "value": "opt1" }, { "text": "Option 2", "value": "opt2" } ] }下拉菜单适合让用户从固定列表中选择,例如选择频道、成员或某个枚举值。参考 下拉选择示例,其典型用途是"从固定列表中选择",而选择结果同样通过 Webhook 回传。
2.3date_picker:日期选择器
日期选择器用于收集日期型输入(如请假开始日、截止日期),是 SKILL.md 消息卡片组件表中的明确组件,与form_field、dropdown同属交互组件。
2.4 组合成一张完整的表单卡片
消息卡片的整体结构是content.head(可选标题)+content.body(组件数组)。下面是一张组合了文本输入、下拉选择与日期选择的"访客登记"表单卡片示例:
{ "content": { "head": { "text": "访客登记", "sub_head": { "text": "请填写以下信息" } }, "body": [ { "type": "message", "text": "请填写访客信息并提交:" }, { "type": "form_field", "editable": true, "text": "访客姓名" }, { "type": "form_field", "editable": true, "text": "来访事由" }, { "type": "dropdown", "select_items": [ { "text": "内部会议", "value": "meeting" }, { "text": "面试", "value": "interview" }, { "text": "供应商", "value": "vendor" } ] }, { "type": "date_picker" } ] } }提示:卡片中的组件必须符合 消息卡片组件参考 的 JSON 结构。消息卡片结构说明 特别提醒:许多"卡片没渲染出来"的问题,其实只是 payload 的 JSON 结构不合法——发送前务必校验你的 payload。
2.5 组件限制(来自 消息卡片组件参考)
| 组件 | 限制 |
|---|---|
| 消息文本 | 4,096 字符 |
| 按钮文本 | 40 字符 |
| 字段 key/value | 各 256 字符 |
| 下拉选项 | 100 个 |
| 单条消息按钮 | 5 个 |
这些限制在设计表单时需要提前考虑,避免字段过多或文本超长。
三、订阅并接收chat_message.submitWebhook
3.1 事件总览
根据 Webhook 事件参考 与 Webhook 架构指南,Chatbot API 常见事件包括:
| 事件 | 触发时机 |
|---|---|
endpoint.url_validation | 配置/更换 Bot Endpoint URL(仅设置阶段) |
bot_installed | 机器人被添加到账户 |
bot_notification | 用户给机器人发消息或使用斜杠命令 |
interactive_message_actions | 用户点击卡片按钮 |
chat_message.submit | 用户提交表单 |
app_deauthorized | 机器人被移除/应用被取消授权 |
本文主角就是chat_message.submit。
3.2 验证 Webhook 签名
无论处理哪种事件,第一步都是验签。根据 Webhook 架构指南,Zoom 的每个 Webhook 请求都带有两个关键请求头:
{ 'x-zm-signature': 'v0=abc123...', // 用于校验的签名 'x-zm-request-timestamp': '1234567890', // Unix 时间戳 'content-type': 'application/json' }验签算法为 HMAC-SHA256,验签实现(来自 Chatbot 完整示例 的utils/validation.js):
// utils/validation.js const crypto = require('crypto'); /** * 验证 Zoom Webhook 签名 */ function verifyZoomWebhookSignature(req) { const signature = req.headers['x-zm-signature']; const timestamp = req.headers['x-zm-request-timestamp']; if (!signature || !timestamp) { throw new Error('Missing signature headers'); } const message = `v0:${timestamp}:${JSON.stringify(req.body)}`; const hash = crypto .createHmac('sha256', process.env.ZOOM_VERIFICATION_TOKEN) .update(message) .digest('hex'); if (signature !== `v0=${hash}`) { throw new Error('Invalid webhook signature'); } return true; }验签的意义在于:不验签的话,任何人都可以向你的 Bot Endpoint URL 伪造 Webhook,可能触发未授权操作、造成拒绝服务,或导致敏感数据被访问。这一点同样被 安全最佳实践 列为第一条要求。
3.3 Webhook 处理器骨架
表单提交与按钮点击、斜杠命令共用同一个 Webhook 入口,用event字段路由分发。参考 Webhook 架构指南 的处理器骨架:
app.post('/webhook', (req, res) => { try { // 第 1 步:验证签名 verifyZoomWebhookSignature(req); // 第 2 步:取出事件与 payload const { event, payload } = req.body; // 第 3 步:按事件类型分发 switch (event) { case 'endpoint.url_validation': return handleUrlValidation(req, res); case 'bot_installed': return handleBotInstalled(payload, res); case 'bot_notification': return handleBotNotification(payload, res); case 'interactive_message_actions': return handleButtonClick(payload, res); case 'chat_message.submit': return handleFormSubmit(payload, res); // ← 表单提交走这里 case 'app_deauthorized': return handleBotUninstalled(payload, res); default: console.log('Unsupported event:', event); return res.status(200).json({ success: true }); } } catch (error) { if (error.message.includes('signature')) { return res.status(401).json({ error: 'Invalid webhook signature' }); } return res.status(500).json({ error: error.message }); } });事件路由的推荐实践是:对未知事件也返回
200并记录日志,而不是直接崩溃(参考 Webhook 架构指南 的最佳实践)。
四、服务端处理表单提交
4.1 核心校验要求(原文档关键内容)
表单提交指南 明确了两条服务端处理铁律:
- Always validate types (dates, numbers) server-side—— 必须在服务端校验字段类型(日期、数字等);
- Treat submitted text as untrusted input—— 将提交的文本当作不可信输入处理。
这是因为卡片表单的输入从用户产生、经 Zoom 回传,中间任何环节都不能保证数据格式正确、内容安全。客户端(卡片)的限制只是体验层面的约束,真正的安全边界在服务端。
4.2 处理器实现:校验 + 回复
收到chat_message.submit后,处理器需要:解析 payload → 逐字段校验类型与取值范围 → 按结果回复确认消息或更新后的卡片。参考 Chatbot 完整示例 中的工具函数,处理器可以这样写:
// routes/webhook.js(节选) const { verifyZoomWebhookSignature } = require('../utils/validation'); const { sendChatbotMessage, sendTextMessage } = require('../utils/chatbot'); /** * 处理表单提交(chat_message.submit) */ async function handleFormSubmit(payload, res) { const { toJid, accountId, userName } = payload; const formData = payload.formData || {}; // 表单字段的实际位置以 Zoom 官方事件说明为准 console.log(`${userName} 提交了表单`); // 立即返回 200,避免 Webhook 超时(Zoom 期望 3 秒内响应) res.status(200).json({ success: true }); try { // —— 第 1 步:服务端校验 —— // 1) 必填校验 if (!formData.name || !String(formData.name).trim()) { await sendTextMessage(toJid, accountId, '❌ 提交失败:访客姓名为必填项'); return; } // 2) 类型校验:日期必须是合法日期 const visitDate = formData.visit_date; if (visitDate && Number.isNaN(Date.parse(visitDate))) { await sendTextMessage(toJid, accountId, '❌ 提交失败:日期格式不合法'); return; } // 3) 范围/长度校验:文本长度上限 if (String(formData.name).length > 256) { await sendTextMessage(toJid, accountId, '❌ 提交失败:姓名字段过长'); return; } // —— 第 2 步:通过校验,回复确认消息 —— await sendChatbotMessage(toJid, accountId, { head: { text: '✅ 登记成功' }, body: [ { type: 'fields', items: [ { key: '姓名', value: String(formData.name) }, { key: '日期', value: visitDate || '未指定' }, { key: '状态', value: '待审批' } ] } ] }); } catch (error) { console.error('Error processing form submit:', error); } }说明:上述
formData字段的取法仅作演示,实际 payload 结构请以 Zoom 官方 Chatbot 事件文档为准;本仓库中的 Webhook 事件参考 也提示要"仔细解析 payload 并按事件类型与 action 值路由"。
4.3 快速响应:先返回 200,再异步处理
根据 Webhook 架构指南,Zoom 期望在 3 秒内收到 200 响应。因此推荐"立即响应、异步处理"的模式:
// ✅ 推荐:立即响应,再异步处理 app.post('/webhook', (req, res) => { verifyZoomWebhookSignature(req); res.status(200).json({ success: true }); // 先回 200 processFormSubmitAsync(req.body); // 异步处理表单 }); // ❌ 不推荐:同步阻塞在慢操作上,可能超时 app.post('/webhook', async (req, res) => { await slowDatabaseWrite(); // 可能拖到超时 res.status(200).json({ success: true }); });五、把提交文本当作不可信输入:安全与校验清单
结合 安全最佳实践 与 Chatbot 完整示例 的utils/validation.js,处理表单提交时应建立如下防线:
5.1 文本清洗(sanitize)
Chatbot 完整示例 提供了一个可复用的清洗函数:限制 4096 字符、移除控制字符,防止异常内容进入下游:
/** * 清洗消息(4096 字符上限) */ function sanitizeMessage(message) { if (typeof message !== 'string') return ''; return message .trim() .replace(/[\x00-\x1F\x7F]/g, '') // 移除控制字符 .substring(0, 4096); // 截断到 4096 字符 }5.2 类型与格式校验
- 日期:用
Date.parse()或专门的日期解析库校验,拒绝非法格式; - 数字:确认是合法数值且在业务允许范围内(如金额、数量);
- 枚举值:下拉菜单提交的值应与卡片中定义的
select_items白名单比对,拒绝未知值; - JID:如果需要用提交内容拼接发送目标,校验 JID 格式
user@domain/channel@domain:
function isValidJID(jid) { if (typeof jid !== 'string' || !jid.trim()) return false; return /^[^@\s]+@[^@\s]+$/.test(jid); }5.3 避免记录敏感信息
安全最佳实践 与 多步工作流示例 都提醒:不要在日志中记录 PII(个人身份信息);日志中记录 request ID / correlation ID 即可。
5.4 Webhook 可能重复投递
多步工作流示例 明确指出:Webhook 可能被投递不止一次,如果事件中带有 ID,应据其去重,避免表单被重复处理(例如重复创建工单)。
5.5 凭据放在环境变量里
环境变量参考 给出了标准化的.env键位,其中与表单提交 Webhook 直接相关的是:
| 变量 | 是否必需 | 用途 | 获取位置 |
|---|---|---|---|
ZOOM_CLIENT_ID | 是 | 应用 OAuth 身份 | Marketplace → App Credentials |
ZOOM_CLIENT_SECRET | 是 | OAuth 换取 token | Marketplace → App Credentials |
ZOOM_BOT_JID | Chatbot 流程 | 机器人标识 | Team Chat 应用/机器人配置 |
ZOOM_SECRET_TOKEN | 推荐 | 事件/Webhook 签名验证 | Marketplace → Event Subscriptions → Secret Token |
ZOOM_VERIFICATION_TOKEN | 仅旧版 | 旧式验证路径 | Marketplace 旧版字段 |
注意:该文档明确建议优先使用
ZOOM_SECRET_TOKEN进行签名验证,ZOOM_VERIFICATION_TOKEN是旧应用的遗留字段。仓库示例代码(如utils/validation.js)为兼容旧版使用了ZOOM_VERIFICATION_TOKEN,新项目建议按上述推荐迁移。无论哪个 token,都不能硬编码进代码。
六、进阶:多步表单工作流与状态持久化
当表单不止一步时,就进入了状态化机器人场景。多步工作流示例 给出的模式是:
- 发送带按钮/表单的卡片(第 1 步);
- 用户交互后,更新存储的状态,并回复第 2 步的卡片;
- 重复直到流程完成。
这与表单提交天然契合:例如"第一步填姓名 → 第二步选日期 → 第三步确认"。状态可以存在内存,但生产环境建议落库。数据库集成示例 给出的建议表结构:
installations(account_id,bot_jid,created_at)——记录机器人安装关系;users(zoom_jid,internal_user_id)——把 Zoom 用户映射到内部系统用户;workflows(workflow_id,status,payload_json)——保存多步流程的中间状态。
七、本地联调与部署
7.1 用 ngrok 打通本地开发
参考 Chatbot 完整示例 的本地测试流程:
# 安装 ngrok npm install -g ngrok # 启动本地服务(假设监听 4000 端口) node server.js # 新开终端,暴露本地端口 ngrok http 4000然后把 ngrok 提供的 HTTPS 地址填到 Zoom Marketplace 的Features → Team Chat Subscription → Bot Endpoint URL(例如https://abc123.ngrok.io/webhook)。保存时 Zoom 会发送endpoint.url_validation请求,服务端需返回:
function handleUrlValidation(req, res) { const { plainToken } = req.body.payload; const encryptedToken = crypto .createHmac('sha256', process.env.ZOOM_VERIFICATION_TOKEN) .update(plainToken) .digest('hex'); return res.status(200).json({ plainToken, encryptedToken }); }校验通过后 Marketplace 会显示绿色对勾。
7.2 验证表单提交是否打通
- 在 Zoom Team Chat 中触发机器人发送带表单字段的卡片;
- 填写并提交后,观察服务端是否收到
chat_message.submit; - 确认回复的消息(确认卡片或文本)正确送达。
如果收不到 Webhook,Webhook 架构指南 的排查表给出了常见原因:Bot Endpoint URL 与服务器不一致、未返回plainToken + encryptedToken、响应超时(应在 3 秒内返回 200)等。
7.3 生产部署要点
- 生产环境必须是HTTPS 且公网可访问的端点;
- 在 Marketplace 中把 Bot Endpoint URL 更新为生产地址;
- 生产环境变量与开发环境分离(环境变量参考);
- 在 Webhook 端点前加限流(安全最佳实践)。
相关仓库文档索引
- 表单提交指南(本文核心文档)
- 技能总入口 SKILL.md
- 消息卡片组件参考
- Webhook 事件参考
- Webhook 架构指南
- Chatbot 完整示例
- 按钮动作示例
- 下拉选择示例
- 多步工作流示例
- 数据库集成示例
- 安全最佳实践
- 环境变量参考
【免费下载链接】knowledge-work-pluginsOpen source repository of plugins primarily intended for knowledge workers to use in Claude Cowork项目地址: https://gitcode.com/GitHub_Trending/kn/knowledge-work-plugins
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考