本文档介绍了如何构建一个启动器,让您的应用或服务能够在发生事件时通知 Google Workspace Studio 并启动流程执行。在 API 中,启动器称为 workflowTriggers。
启动器用于启动流程,而步骤是构成流程的任务序列中的单个任务。通过构建启动器,您可以让用户设置自动流程,以响应来自您的应用或服务的实时事件。
构建启动器涉及在插件清单文件中声明启动器,并在 Google Apps 脚本中实现生命周期回调,或者通过向 Google Workspace Studio API 端点发布载荷来触发启动器。
前提条件和 OAuth 授权
如需与 Workspace Studio API 端点通信,您的应用或服务必须使用 OAuth 2.0 进行身份验证。应用必须在授权期间向用户请求以下专用 OAuth 范围:
https://www.googleapis.com/auth/workspace.studio.trigger
此范围授权应用调用 Workspace Studio API 并触发用户为此启动方式配置的流程。
离线访问和刷新令牌
由于启动器会在外部服务中发生事件时异步通知 Workspace Studio(这可能发生在用户配置流程后的数小时、数天或数月),因此您的服务在调用 API 端点时必须提供有效的 OAuth 2.0 访问令牌。
Google 在插件事件对象(例如在启动器配置或生命周期回调请求期间)中提供的访问令牌有效期较短,仅为 1 小时。这不足以在未来异步触发启动器事件。为了能够随时调用 Workspace Studio API,您的服务需要使用离线刷新令牌来按需生成新的访问令牌。
您处理授权和获取刷新令牌的方式取决于插件运行时:
HTTP 加购项(替代运行时):对于 HTTP 加购项,您的后端服务必须实现一个独立于内置加购项授权的单独 OAuth 2.0 授权流程,以请求离线访问权限 (
access_type=offline) 并接收刷新令牌。当用户在 Workspace Studio 中配置启动方式时,您可以通过显示登录或授权卡片来提示用户授权此连接。如需详细了解如何返回授权卡片和处理 OAuth 流程,请参阅将 Google Workspace 插件与第三方服务相关联(将 Google Workspace 视为您要关联的第三方服务)。
您的后端服务必须安全地存储刷新令牌(例如,在服务的数据库中与
triggerId一起存储),并在每次发生事件时使用该令牌检索新的访问令牌,然后再向启动器的notifyUri或triggers.fireAPI 端点发送请求。Google Apps 脚本插件:基于 Google Apps 脚本且使用预定(时间驱动型)触发器轮询事件的插件可以跳过实现独立的 OAuth 流程。由于定时触发器直接在 Google Apps 脚本运行时环境中运行,因此 Google Apps 脚本会使用清单中声明的范围自动管理和刷新 OAuth 令牌。
在清单文件中定义启动器
如需定义启动器,请将其添加到插件清单文件 (appsscript.json) 的 addOns.studio.flows.workflowElements 块中。Apps 脚本和 HTTP 运行时(备选运行时)都需要此配置。将元素配置为 workflowTrigger,而不是 workflowAction(用于定义步骤)。如需了解详情,请参阅 Google Workspace 加载项的清单结构。
在 workflowTrigger 代码块内,指定:
inputs:用户在配置卡片上配置的变量(例如项目名称、资源过滤条件等)。outputs:启动器可返回给流程中下游步骤的变量。onConfigFunction:用于显示用户配置界面的回调函数的名称。onManageFunction:由 Google 调用以处理初始订阅创建和删除的回调函数的名称。
以下代码示例展示了事件启动器的示例清单定义:
JSON
{
"timeZone": "America/Los_Angeles",
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"addOns": {
"common": {
"name": "Trigger App",
"logoUrl": "https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/start/default/24px.svg",
"useLocaleFromApp": true
},
"studio": {
"flows": {
"workflowElements": [
{
"id": "triggerDemo",
"state": "ACTIVE",
"name": "Event Trigger",
"description": "Fires when a event occurs in the app.",
"workflowTrigger": {
"inputs": [
{
"id": "projectId",
"description": "The project identifier to watch.",
"cardinality": "SINGLE",
"dataType": {
"basicType": "STRING"
}
}
],
"outputs": [
{
"id": "eventName",
"description": "The name of the triggered event.",
"cardinality": "SINGLE",
"dataType": {
"basicType": "STRING"
}
},
{
"id": "eventMessage",
"description": "Detailed event message description.",
"cardinality": "SINGLE",
"dataType": {
"basicType": "STRING"
}
}
],
"onConfigFunction": "onConfigTrigger",
"onManageFunction": "onManageTrigger"
}
}
]
}
}
}
}
处理初始订阅生命周期
当用户配置并启用包含您的初始流程的流程时,或者当流程被停用或删除时,Google 会使用清单中声明的 onManageFunction 回调函数来调用您的插件。
生命周期事件对象
回调函数会接收包含操作上下文的 WorkflowEventObject。首先,这包括:
触发器创建 (
event.workflow.triggerCreation):在发布或启用流程时触发。triggerId:用于标识相应启动器注册实例的唯一 UUID 字符串。notifyUri:与此初始注册相关联的唯一 REST API 端点网址(例如https://workspacestudio.googleapis.com/v1/triggers/YOUR_TRIGGER_ID:fire)。inputs:用户通过卡片配置的变量输入。
触发器删除 (
event.workflow.triggerDeletion):当启动器从流程中移除,或者当整个流程被停用或删除时触发。triggerId:要清理的订阅实例的唯一 UUID 字符串。
Alternate Runtimes (HTTP API) 订阅生命周期
对于使用替代运行时构建的插件,订阅生命周期通知会通过 HTTP POST 请求发送到插件的已配置 HTTP 端点网址,并使用 onManageFunction 回调函数指定的操作名称。载荷与 WorkflowEventObject 的 JSON 表示形式一致。
如需详细了解替代运行时,请参阅使用 HTTP 端点构建 Google Workspace 插件。
在 Apps 脚本中实现生命周期回调
以下 Apps 脚本示例展示了如何配置用户界面卡片、使用 onManageTrigger 处理订阅生命周期事件,以及在发生事件时将启动器请求发回给 Google。
Apps 脚本
/**
* Generates and returns the user configuration card to collect inputs.
*/
function onConfigTrigger() {
const projectInput = CardService.newTextInput()
.setFieldName("projectId")
.setTitle("Project ID")
.setHint("Enter the project identifier to watch");
const section = CardService.newCardSection()
.setHeader("Configure Event Trigger")
.addWidget(projectInput);
const card = CardService.newCardBuilder()
.addSection(section)
.build();
return card;
}
/**
* Handles subscription lifecycle events sent from Google Workspace Studio.
*
* @param {Object} event The Workspace Studio event object.
*/
function onManageTrigger(event) {
const triggerCreation = event.workflow.triggerCreation;
const triggerDeletion = event.workflow.triggerDeletion;
if (triggerCreation) {
const triggerId = triggerCreation.triggerId;
const notifyUri = triggerCreation.notifyUri;
const inputs = triggerCreation.inputs;
// Extract input values configured by the user.
const projectId = inputs["projectId"].stringValues[0];
// TODO: Save triggerId, notifyUri, and projectId in your database/service.
// Your backend service listens for events related to 'projectId'
// and calls notifyUri when those events occur.
console.log("Trigger subscription created: " + triggerId +
", Notify URI: " + notifyUri +
", Match Project: " + projectId);
} else if (triggerDeletion) {
const triggerId = triggerDeletion.triggerId;
// TODO: Remove references to triggerId from your database and stop
// sending future event notifications to the associated notifyUri.
console.log("Trigger subscription deleted: " + triggerId);
}
}
/**
* Mock function showing how your backend service fires the trigger.
* This logic runs on your service when a watched event occurs.
*
* @param {string} notifyUri The stored notifyUri associated with the trigger.
* @param {string} triggerId The stored triggerId.
* @param {string} userAccessToken The OAuth 2.0 access token for the user
* (obtained using your stored refresh token).
*/
function simulateEventFire(notifyUri, triggerId, userAccessToken) {
// A unique UUID version 4 is recommended as the requestId for idempotency.
const requestId = Utilities.getUuid();
const payload = {
"name": "triggers/" + triggerId,
"outputs": {
"eventName": { "stringValues": ["EventOccurred"] },
"eventMessage": { "stringValues": ["Hello from the service!"] }
},
"requestId": requestId
};
const options = {
"method": "POST",
"contentType": "application/json",
"headers": {
"Authorization": "Bearer " + userAccessToken
},
"payload": JSON.stringify(payload),
"muteHttpExceptions": true
};
const response = UrlFetchApp.fetch(notifyUri, options);
const responseCode = response.getResponseCode();
if (responseCode === 200) {
console.log("Trigger successfully fired!");
} else if (responseCode === 404) {
// 404 means the trigger registration is invalid or deleted.
console.log("Trigger not found. Stop sending events for this trigger.");
// TODO: Clean up the trigger from your backend database.
} else if (responseCode === 429 || responseCode >= 500) {
console.log("Temporary error (" + responseCode + "). Retry using exponential backoff.");
} else {
console.log("Failed to fire trigger. HTTP Code: " + responseCode + " - " + response.getContentText());
}
}
使用 Workspace Studio API
您可以使用 Workspace Studio API (workspacestudio.googleapis.com) 以程序化方式将启动方式事件通知给 Google。
端点位于基本路径 https://workspacestudio.googleapis.com/v1 下。
通知启动器事件
使用 triggers.fire 方法触发启动器,以启动流程的执行。
- HTTP 方法:
POST - 路径:
/v1/triggers/{triggerId}:fire(其中{triggerId}是在创建触发器订阅期间检索到的唯一标识符) - OAuth 范围:
https://www.googleapis.com/auth/workspace.studio.trigger
以下代码示例展示了如何在请求中触发启动器。
请求
{
"name": "triggers/TRIGGER_ID",
"outputs": {
"eventName": {
"stringValues": [
"EventOccurred"
]
},
"eventMessage": {
"stringValues": [
"Hello from the service!"
]
}
},
"log": {
"textFormatElements": [
{
"text": "An event occurred in the app."
}
]
},
"requestId": "UNIQUE_REQUEST_ID"
}
name(字符串,必需):启动器的资源名称,格式为triggers/{triggerId}。outputs(映射,可选):表示事件数据的初始输出变量的映射。每个值都是支持类型化列表(例如stringValues、booleanValues、integerValues)的VariableData对象。log(对象,可选):在 Workspace Studio 执行活动日志中显示的TextFormat标记表示法。requestId(字符串,可选):一个唯一标识符(建议使用 UUID v4),最多包含 36 个 ASCII 字符,用于确保在重试时 API 具有幂等性。
答案
如果成功,响应将返回一个空的 JSON 对象 {}。
Workspace Studio API 配额
发送到 workspacestudio.googleapis.com 服务的流量受到限制,以防止系统过载、鼓励合理使用资源并保护整体 Google Workspace 性能。
系统会强制执行以下配额:
| 配额类型 | 配额 |
|---|---|
| 每个项目每分钟 | 1,000 个入门版请求 |
| 每位用户每分钟 | 100 个入门版请求 |
配额类型包括:
- 每个项目每分钟:将单个开发者的 Google Cloud 项目中触发的启动器事件的累计数量限制为每分钟 1,000 个请求,适用于运行其启动器的所有用户。
- 每位用户每分钟:将任何单个最终用户在给定 Cloud 项目中的累积启动器调用次数限制为每分钟 100 个请求。
处理基于时间的配额错误
如果超出这些配额,API 会返回 HTTP 429 Too Many Requests(或 429 Resource Exhausted)错误代码,指明速率配额已超出。
如需解决这些错误,您的代码应捕获异常并使用截断的指数退避算法策略。指数退避算法会重试失败的请求,并在每次尝试之间逐步增加延迟时间,包括随机抖动(在每次迭代时重新计算随机延迟时间),以防止多个客户端同时同步并重试:
- 向 Workspace Studio API 发出请求。
- 如果请求失败并显示
429错误,请等待1 second + random_number_milliseconds,然后重试。 - 如果再次失败,请等待
2 seconds + random_number_milliseconds,然后重试。 - 如果再次失败,请等待
4 seconds + random_number_milliseconds,然后重试。 - 继续此循环,将延迟时间加倍,直至达到
maximum_backoff阈值(通常为 32 或 64 秒)。 - 达到退避时长上限后,使用该恒定延迟进行重试,直至达到重试次数上限,然后停止并记录错误。
最佳做法
在设计和实现启动器时,请考虑以下最佳实践:
发出单个事件,而不是批量列表
设计启动器时,请确保它能针对每个不同的事件(例如更新单条记录、发布新消息或分配任务)分别发出一个事件,而不是发出包含一批或一个列表的单个事件:
- 与内置启动器的一致性:在 Workspace Studio 中,内置的 Google Workspace 启动器(例如在 Gmail 中收到电子邮件或用户加入 Google Chat 中的聊天室)由单个事件触发。发出单项事件与此行为保持一致,并为所有初始配置的用户提供一致且可预测的体验。
- 更简单的流程配置:流程中的下游步骤通常一次处理一个项目。通过发出单项事件,用户可以直接映射变量,而无需添加复杂的步骤来迭代数组或解析列表。
- 单独处理轮询和批量更改:如果您的后端服务轮询外部 API,并在单个轮询间隔期间检测到多个已更改的项,请为每个项触发单独的启动器事件,而不是将它们捆绑到一个批量事件中。
- 管理事件速率和配额:由于为多个更改的项目触发单个事件可能会导致请求突然激增,因此请确保您的服务保持在 Workspace Studio API 配额(例如每位用户每分钟 100 个请求的限制)范围内。如果轮询周期产生大量项(例如,超过 100 条更改记录),请在一段时间内调整或限制事件分派,以避免
429 Too Many Requests错误。
核心行为和极端情形
集成启动器时,开发者必须处理特定的错误行为和运行时功能:
- 不支持测试运行:Workspace Studio 不支持新手版测试运行。
- 幂等性和重放预防:虽然不是严格要求,但您应在 HTTP 或 Apps 脚本载荷中包含唯一的
requestId(例如 UUID)。提供requestId可确保幂等性,因为这样一来,API 就能检测并忽略重复的通知,从而防止流程针对单个事件运行多次。 已停用和重新启用的流程:当包含启动器的流程在 Workspace Studio 中停用时,Google 会向您的
onManageFunction回调发送triggerDeletion生命周期事件。此外,对关联的FireTrigger方法的任何调用都会返回404 Not Found错误返回代码 (Requested entity was not found.)。您的服务应通过停止向相应启动器实例 ID 传送未来的事件通知来对404错误做出反应。如果用户稍后重新启用该流程,Google 会通过调用您的
onManageFunction回调来启动新的订阅生命周期,并提供包含新triggerId和notifyUri的新triggerCreation事件。之前的triggerId已永久停用,不会重新启用,因此您的服务不应轮询或检查旧触发器实例是否已重新启用。如需了解详情,请参阅处理初始订阅生命周期。幂等订阅删除:您的
onManageFunction回调函数必须以幂等方式处理来自 Google 的启动器删除请求。如果 Google 针对同一triggerId多次调用删除钩子(例如,在因临时连接丢失而重试期间),该函数应成功返回。流程配额:除了 Workspace Studio API 配额之外,用户流程还受其他内部配额控制。高频循环或过多的事件量可能会超出安全阈值,导致系统自动停用流程。