1. 简介
借助 Google Workspace Studio ,您可以自动执行 Gmail、Google Chat、Google 云端硬盘和 Google 表格等 Google Workspace 应用中的业务流程。如果您的工作流需要从外部 Web 应用启动,或者需要在您自己的云环境中运行自定义逻辑,您可以使用 Cloud Run 上的 Node.js 中的 HTTP Google Workspace 插件 扩展 Google Workspace Studio:
- 自定义启动器步骤 (
workflowTrigger):当外部事件发生时(例如客户在内部 Web 门户中提交工单),通过调用触发器的notifyUri(或 Google Workspace Studiotriggers.fireREST API)启动 Google Workspace Studio 流程。 - 自定义操作步骤 (
workflowAction):接收流程中前面步骤的变量,在 Node.js 服务中运行自定义逻辑或 AI 推理,并将输出变量返回给 Google Workspace Studio。
在本 Codelab 中,您将在 Cloud Run 上为 Google Workspace Studio 构建并部署完整的 Node.js HTTP 扩展程序,使用 Cloud Firestore 在容器修订版本之间持久保存有效流程触发器订阅和已提交的工单,使用 Cloud Tasks 在 notifyUri 未返回成功响应时自动重试触发器通知,并在自定义操作步骤中使用 Gemini Enterprise Agent Platform 上的 Gemini 3.8 Flash (gemini-3.8-flash) 对工单紧急程度进行分类,并提供弹性关键字匹配后备。
构建内容
- 一个 Support Quick Portal Web 应用和 HTTP Google Workspace 插件,使用 Node.js 、Express 、Firestore 和 Cloud Tasks 在 Cloud Run 上构建。
- 一个 New Support Ticket 自定义启动器步骤 (
supportTicketTrigger),用于在studio_triggersFirestore 集合中保留有效流程订阅,在support_ticketsFirestore 集合中记录已提交的工单,并在 Web 门户中提交工单时触发,将ticketTitle和ticketDescription传递到 Google Workspace Studio 流程中。 - 一个 Cloud Tasks 重试工作器 (
studio-trigger-retries队列和POST /api/tasks/fire-trigger),用于使用存储在 Firestore 中的最新userOAuthToken以指数退避算法将失败的触发器调用加入队列并重试,保证将内容发送到notifyUri。 - 一个 Detect Urgency 自定义操作步骤 (
urgencyDetectorStep),用于使用 Gemini Enterprise Agent Platform (aiplatform.googleapis.com) 上的 Gemini 3.8 Flash (gemini-3.8-flash) 对工单紧急程度进行分类,并自动回退到关键字匹配(urgent、blocker、critical)。
架构
下图说明了 Support Quick Portal、Cloud Run 上的 Node.js 插件、Cloud Firestore、Cloud Tasks (studio-trigger-retries)、Gemini 3.8 Flash 和 Google Workspace Studio 如何从流程注册到弹性触发器传送和操作执行进行交互:

学习内容
- 如何使用
workflowTrigger和workflowAction元素配置 HTTP Google Workspace 插件部署清单 (deployment.json)。 - 如何在
studio_triggersFirestore 集合中持久保存 Google Workspace Studio 触发器订阅(triggerId、notifyUri和userOAuthToken),以及如何使用notifyUri/triggers.fire触发流程。 - 如何使用 Cloud Tasks (
@google-cloud/tasks) 以指数退避算法重试失败的触发器通知,直到notifyUri返回2xx成功响应。 - 如何使用 Gemini Enterprise Agent Platform 上的 Gemini 3.8 Flash (
gemini-3.8-flash) 结构化输出(并提供关键字回退)在 Node.js 中构建workflowAction自定义步骤。
前提条件
- 一个 Google Workspace 账号,并且可以访问 Google Workspace Studio。
- 一个已启用结算功能的 Google Cloud 项目 ,该项目是在与您的用户账号相同的 Google Workspace 组织内创建的,因此您可以安装未发布的插件部署以进行测试。
2. 设置 Google Cloud 云项目、Firestore 和 Cloud Tasks
在此步骤中,您将配置项目的 OAuth 权限请求页面,在 Cloud Shell 中启用所需的 Google Cloud 和 Google Workspace API,预配 (default) Firestore 数据库,创建 Cloud Tasks 重试队列,并初始化插件授权服务账号。
- 打开 Google Cloud 控制台,选择您的云项目,然后点击顶部导航栏中的 激活 Cloud Shell 图标。
- 配置 OAuth 权限请求页面 ,以便 Google Workspace 可以为您的插件预配 OAuth 客户端:
- 打开 Google Auth Platform 品牌页面。
- 如果系统提示,请点击开始使用 。(如果您的权限请求页面已配置,请跳至第 3 步。)
- 在应用信息 下,为应用名称 输入
Support Extension,选择您的用户支持电子邮件地址 ,然后点击下一步 。 - 在目标对象 下,选择内部 ,然后点击下一步 。
- 在联系信息 下,输入您的电子邮件地址,然后点击下一步 。
- 在完成 下,同意 Google API 服务:用户数据政策 ,点击继续 ,然后点击创建 。
- 在 Cloud Shell 中,设置您的活跃项目(将
YOUR_PROJECT_ID替换为您的 Google Cloud 项目 ID),并启用所需的 API:gcloud config set project YOUR_PROJECT_ID gcloud services enable \ aiplatform.googleapis.com \ firestore.googleapis.com \ cloudtasks.googleapis.com \ run.googleapis.com \ cloudbuild.googleapis.com \ artifactregistry.googleapis.com \ appsmarket-component.googleapis.com \ gsuiteaddons.googleapis.com \ workspacestudio.googleapis.com - 在
us-central1中预配(default)Firestore 数据库和studio-trigger-retriesCloud Tasks 队列(如果它们尚不存在),并初始化您的 Google Workspace 插件服务账号:gcloud firestore databases describe --database="(default)" >/dev/null 2>&1 || \ gcloud firestore databases create \ --database="(default)" \ --location=us-central1 \ --type=firestore-native \ --quiet gcloud tasks queues describe studio-trigger-retries --location=us-central1 >/dev/null 2>&1 || \ gcloud tasks queues create studio-trigger-retries \ --location=us-central1 \ --max-attempts=5 \ --min-backoff=10s \ --max-backoff=300s \ --max-doublings=4 gcloud workspace-add-ons get-authorization
3. 使用 Firestore、Cloud Tasks 和 Gemini 3.8 Flash 构建 Node.js 应用
在此步骤中,您将在 ~/studio-support-addon 中构建 Node.js 应用。为了使代码模块化且易于阅读,您将项目组织为六个重点文件:
firestore.js:初始化 Cloud Firestore 客户端(studio_triggers和support_tickets集合),并刷新缓存的 OAuth 令牌。starter.js:实现 New Support Ticket 自定义启动器步骤 Webhook(/onConfigTrigger和/onManageTrigger)。action.js:使用 Gemini 3.8 Flash (gemini-3.8-flash) 实现 Detect Urgency 自定义操作步骤 Webhook(/onConfigUrgency和/onExecuteUrgency),并提供关键字匹配回退。tasks.js:将触发器通知分派到trigger.notifyUri(回退到https://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire),将失败的传送加入studio-trigger-retriesCloud Tasks 队列,并实现POST /api/tasks/fire-trigger重试工作器端点。portal.js:提供 Support Quick Portal Web 界面 (GET /) 并触发有效的 Google Workspace Studio 触发器 (POST /api/submit-ticket),并提供模拟暂时性故障和观看 Cloud Tasks 重试的选项。index.js:将所有 Express 路由连接在一起并启动 HTTP 服务器。
初始化项目和 package.json
- 创建项目目录并导航至该目录:
mkdir -p ~/studio-support-addon && cd ~/studio-support-addon - 使用
express、@google-cloud/firestore、@google-cloud/tasks和@google/genai创建package.json:cat << 'EOF' > package.json { "name": "studio-support-addon", "version": "1.0.0", "type": "module", "main": "index.js", "scripts": { "start": "node index.js" }, "dependencies": { "@google-cloud/firestore": "^7.11.0", "@google-cloud/tasks": "^5.6.0", "@google/genai": "^1.0.0", "express": "^4.21.2" } } EOF
firestore.js - 共享 Firestore 状态和 OAuth 令牌刷新
创建 firestore.js 以管理您的两个 Firestore 集合,并保持缓存的用户 OAuth 令牌处于最新状态:
studio_triggers:存储有效的 Google Workspace Studio 流程订阅(triggerId、notifyUri和userOAuthToken),以便它们在 Cloud Run 容器重启和缩减至零事件后仍然存在。support_tickets:存储通过门户提交的每个支持服务工单的记录。refreshTokenInFirestore(req):每当 Google Workspace Studio 调用您的任何 Webhook 端点时,它都会包含一个生命周期较短的authorizationEventObject.userOAuthToken。此帮助程序会使用最新令牌更新有效的触发器文档,以便后台triggers.fire调用保持授权状态。
cat << 'EOF' > firestore.js
import { Firestore } from '@google-cloud/firestore';
export const db = new Firestore({
projectId: process.env.GOOGLE_CLOUD_PROJECT || undefined,
databaseId: '(default)',
ignoreUndefinedProperties: true,
});
export const triggersCollection = db.collection(
process.env.FIRESTORE_TRIGGERS_COLLECTION || 'studio_triggers'
);
export const ticketsCollection = db.collection(
process.env.FIRESTORE_TICKETS_COLLECTION || 'support_tickets'
);
// Keep cached userOAuthToken fresh across active trigger documents
export async function refreshTokenInFirestore(req) {
const freshToken = req.body?.authorizationEventObject?.userOAuthToken;
if (!freshToken) return;
try {
const snapshot = await triggersCollection.get();
if (snapshot.empty) return;
const batch = db.batch();
snapshot.docs.forEach((doc) => {
batch.set(
doc.ref,
{ userOAuthToken: freshToken, updatedAt: new Date().toISOString() },
{ merge: true }
);
});
await batch.commit();
} catch (err) {
console.error('Error refreshing OAuth token in Firestore:', err.message);
}
}
EOF
starter.js - 自定义启动器步骤 (workflowTrigger)
创建 starter.js 以实现 Google Workspace Studio 自定义启动器步骤所需的两个 Webhook 端点:
onConfigTrigger(POST /onConfigTrigger):当用户在 Google Workspace Studio 中选择 New Support Ticket 作为其流程的启动器时调用。返回描述启动器的pushCard导航操作。onManageTrigger(POST /onManageTrigger):当用户将流程开启 (req.body.workflow.triggerCreation) 或将其关闭 / 删除 (req.body.workflow.triggerDeletion) 时调用。在studio_triggersFirestore 集合中保存或删除订阅文档。
cat << 'EOF' > starter.js
import { triggersCollection, refreshTokenInFirestore } from './firestore.js';
// Custom Starter Configuration Card
export async function onConfigTrigger(req, res) {
await refreshTokenInFirestore(req);
res.status(200).json({
action: {
navigations: [
{
pushCard: {
sections: [
{
header: 'Support Ticket Listener',
widgets: [
{
textParagraph: {
text: 'This starter listens for new tickets submitted through the Support Quick Portal and stores subscriptions in Firestore.',
},
},
],
},
],
},
},
],
},
});
}
// Custom Starter Subscription Lifecycle (Firestore studio_triggers collection)
export async function onManageTrigger(req, res) {
try {
const triggerCreation = req.body?.workflow?.triggerCreation;
const triggerDeletion = req.body?.workflow?.triggerDeletion;
const userOAuthToken =
req.body?.authorizationEventObject?.userOAuthToken || null;
if (triggerCreation?.triggerId) {
const triggerId = String(triggerCreation.triggerId);
const docId = encodeURIComponent(triggerId);
await triggersCollection.doc(docId).set(
{
triggerId,
notifyUri: triggerCreation.notifyUri || null,
...(userOAuthToken ? { userOAuthToken } : {}),
updatedAt: new Date().toISOString(),
},
{ merge: true }
);
}
if (triggerDeletion?.triggerId) {
const triggerId = String(triggerDeletion.triggerId);
const docId = encodeURIComponent(triggerId);
await triggersCollection.doc(docId).delete();
}
if (!triggerCreation && !triggerDeletion && userOAuthToken) {
await refreshTokenInFirestore(req);
}
} catch (err) {
console.error('Error in /onManageTrigger:', err.message);
}
res.status(200).json({});
}
EOF
action.js - 自定义操作步骤 (workflowAction),使用 Gemini 3.8 Flash
创建 action.js 以实现 Google Workspace Studio 自定义操作步骤所需的两个 Webhook 端点:
onConfigUrgency(POST /onConfigUrgency):返回一个配置卡片,其中包含一个textInputwidget,用于设置hostAppDataSource.workflowDataSource.includeVariables: true,允许用户选择启动器步骤输出的ticketDescription变量。onExecuteUrgency(POST /onExecuteUrgency):从req.body.workflow.actionInvocation.inputs读取ticketDescription输入,使用 Gemini Enterprise Agent Platform 上的 Gemini 3.8 Flash (gemini-3.8-flash) 对其业务影响和紧急程度进行分类,并提供 结构化输出 (responseSchema枚举["High", "Normal"]),如果模型调用失败,则回退到detectKeywordUrgency()(urgent、blocker、critical),并使用hostAppAction.workflowAction.returnOutputVariablesAction返回urgencyLevel输出变量。
cat << 'EOF' > action.js
import { GoogleGenAI, Type } from '@google/genai';
import { refreshTokenInFirestore } from './firestore.js';
// Initialize Google GenAI SDK for the Gemini Enterprise Agent Platform
const ai = new GoogleGenAI({
vertexai: true,
project: process.env.GOOGLE_CLOUD_PROJECT,
location: 'global',
});
// Fallback keyword classifier if the Gemini API call is unavailable
export function detectKeywordUrgency(description) {
const lower = (description || '').toLowerCase();
return lower.includes('urgent') ||
lower.includes('blocker') ||
lower.includes('critical')
? 'High'
: 'Normal';
}
// Custom Action Step Configuration Card
export async function onConfigUrgency(req, res) {
await refreshTokenInFirestore(req);
res.status(200).json({
action: {
navigations: [
{
pushCard: {
sections: [
{
header: 'Urgency Detector Step',
widgets: [
{
textInput: {
name: 'ticketDescription',
label: 'Source Description',
hintText:
'Select the description variable outputted by the starter',
hostAppDataSource: {
workflowDataSource: {
includeVariables: true,
},
},
},
},
],
},
],
},
},
],
},
});
}
// Custom Action Step Execution: Gemini 3.8 Flash with keyword fallback
export async function onExecuteUrgency(req, res) {
await refreshTokenInFirestore(req);
const inputs = req.body?.workflow?.actionInvocation?.inputs || {};
const description = inputs.ticketDescription?.stringValues?.[0] || '';
let urgencyLevel = detectKeywordUrgency(description);
try {
const response = await ai.models.generateContent({
model: 'gemini-3.8-flash',
contents: `Evaluate the business impact and time sensitivity of this support ticket description and classify its urgency:\n\n${description}`,
config: {
responseMimeType: 'text/x.enum',
responseSchema: {
type: Type.STRING,
enum: ['High', 'Normal'],
},
},
});
const candidate = (response.text || '').trim();
if (candidate === 'High' || candidate === 'Normal') {
urgencyLevel = candidate;
}
} catch (err) {
console.error('Gemini fallback to keyword matching:', err.message);
}
res.status(200).json({
hostAppAction: {
workflowAction: {
returnOutputVariablesAction: {
variables: {
urgencyLevel: {
stringValues: [urgencyLevel],
},
},
},
},
},
});
}
EOF
tasks.js - Cloud Tasks 重试队列和工作器端点
创建 tasks.js 以将触发器调用分派到 trigger.notifyUri(回退到 https://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire),将失败的调用加入 studio-trigger-retries Cloud Tasks 队列,并在 POST /api/tasks/fire-trigger 处理重试:
resolveTriggerUrl(trigger):使用triggerCreation.notifyUri中保留的trigger.notifyUri(如果存在),否则回退到https://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire。fireTriggerDirect(trigger, payload):使用ticketTitle和ticketDescription输出变量以及幂等的requestId将POST请求发送到触发器网址。enqueueTriggerRetry(req, payload):每当初始触发器调用失败或返回非 2xx HTTP 状态时,在以 Cloud Run 服务的POST /api/tasks/fire-trigger端点为目标的studio-trigger-retries队列中加入 HTTP 任务。handleFireTriggerTask(POST /api/tasks/fire-trigger):由 Cloud Tasks 在每次重试尝试时调用。在 Firestore 中查找studio_triggers中的最新trigger文档(确保重试始终使用最新的userOAuthToken),检查X-CloudTasks-TaskRetryCount标头(如果启用了simulateFailure,则第一次 Cloud Tasks 执行会失败并返回503,以便您可以观看 Cloud Tasks 退避并成功重试#1),并且仅当notifyUri返回2xx成功响应时才返回200 OK。任何5xx响应都会告知 Cloud Tasks 根据队列的退避政策再次重试。
cat << 'EOF' > tasks.js
import { CloudTasksClient } from '@google-cloud/tasks';
import { triggersCollection } from './firestore.js';
const tasksClient = new CloudTasksClient();
const PROJECT_ID = process.env.GOOGLE_CLOUD_PROJECT;
const LOCATION = process.env.CLOUD_TASKS_LOCATION || 'us-central1';
const QUEUE = process.env.CLOUD_TASKS_QUEUE || 'studio-trigger-retries';
// Prefer trigger.notifyUri from triggerCreation, falling back to triggers.fire
export function resolveTriggerUrl(trigger) {
return (
trigger.notifyUri ||
`https://workspacestudio.googleapis.com/v1/triggers/${encodeURIComponent(trigger.triggerId)}:fire`
);
}
// Call the Google Workspace Studio trigger URL directly
export async function fireTriggerDirect(
trigger,
{ title, description, requestId }
) {
const targetUrl = resolveTriggerUrl(trigger);
const response = await fetch(targetUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${trigger.userOAuthToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'triggers/' + trigger.triggerId,
outputs: {
ticketTitle: { stringValues: [title] },
ticketDescription: { stringValues: [description] },
},
requestId,
}),
});
return { ok: response.ok, status: response.status, targetUrl };
}
// Enqueue a retry task in Cloud Tasks targeting POST /api/tasks/fire-trigger
export async function enqueueTriggerRetry(req, payload) {
const parent = tasksClient.queuePath(PROJECT_ID, LOCATION, QUEUE);
const baseUrl = process.env.SERVICE_URL || `https://${req.get('host')}`;
const workerUrl = `${baseUrl}/api/tasks/fire-trigger`;
const task = {
httpRequest: {
httpMethod: 'POST',
url: workerUrl,
headers: { 'Content-Type': 'application/json' },
body: Buffer.from(JSON.stringify(payload)).toString('base64'),
},
};
const [response] = await tasksClient.createTask({ parent, task });
return response.name;
}
// Cloud Tasks worker endpoint: retries calling notifyUri using fresh Firestore state
export async function handleFireTriggerTask(req, res) {
const { triggerId, title, description, requestId, simulateFailure } =
req.body || {};
const retryCount = Number(req.get('X-CloudTasks-TaskRetryCount') || 0);
if (!triggerId) {
return res.status(200).json({ skipped: true, reason: 'Missing triggerId' });
}
try {
const docId = encodeURIComponent(String(triggerId));
const doc = await triggersCollection.doc(docId).get();
if (!doc.exists) {
return res
.status(200)
.json({ skipped: true, reason: 'Trigger no longer active' });
}
const trigger = doc.data();
if (!trigger.userOAuthToken) {
return res
.status(503)
.json({ ok: false, error: 'Missing userOAuthToken in Firestore' });
}
// When simulateFailure is enabled, fail the 1st Cloud Task attempt (retryCount === 0)
// so learners can observe Cloud Tasks back off and succeed on retry #1.
if (simulateFailure && retryCount < 1) {
console.warn(
`[Cloud Tasks] Simulated transient failure on initial task attempt (retryCount=${retryCount}) for trigger ${triggerId}`
);
return res
.status(503)
.json({ ok: false, simulated: true, retryCount, triggerId });
}
const outcome = await fireTriggerDirect(trigger, {
title,
description,
requestId,
});
if (outcome.ok) {
console.log(
`[Cloud Tasks] Successfully delivered trigger ${triggerId} to ${outcome.targetUrl} on retryCount=${retryCount}`
);
return res
.status(200)
.json({ ok: true, status: outcome.status, retryCount });
}
console.error(
`[Cloud Tasks] Non-2xx response (${outcome.status}) from ${outcome.targetUrl} on retryCount=${retryCount}`
);
return res
.status(502)
.json({ ok: false, status: outcome.status, retryCount });
} catch (err) {
console.error(
`[Cloud Tasks] Error firing trigger ${triggerId} on retryCount=${retryCount}:`,
err.message
);
return res
.status(503)
.json({ ok: false, error: err.message, retryCount });
}
}
EOF
portal.js - Support Quick Portal 界面和 notifyUri 分派器,提供 Cloud Tasks 回退
创建 portal.js 以提供 Web 门户,并将工单提交分派到 Google Workspace Studio,并自动安排 Cloud Tasks 重试:
renderPortal(GET /):查询studio_triggersFirestore 集合以显示已注册的有效 Google Workspace Studio 触发器的数量,并提供 HTML 工单提交表单(包括用于测试 Cloud Tasks 重试的模拟暂时性故障 复选框)。submitTicket(POST /api/submit-ticket):遍历studio_triggers中的所有有效触发器文档,并调用fireTriggerDirect()。如果触发器网址返回非 2xx 响应、抛出网络连接错误或启用了simulateFailure,则它会自动使用enqueueTriggerRetry()在studio-trigger-retries中加入重试任务,并在support_ticketsFirestore 集合中记录工单。
cat << 'EOF' > portal.js
import { triggersCollection, ticketsCollection } from './firestore.js';
import { fireTriggerDirect, enqueueTriggerRetry } from './tasks.js';
// Support Quick Portal UI
export async function renderPortal(req, res) {
let activeCount = 0;
try {
const snapshot = await triggersCollection.get();
activeCount = snapshot.size;
} catch (err) {
console.error('Error reading studio_triggers collection:', err.message);
}
res.status(200).send(`<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Support Quick Portal</title>
<style>
body { font-family: Arial, sans-serif; padding: 24px; max-width: 540px; margin: auto; color: #202124; }
.badge { background: #e8f0fe; color: #1967d2; padding: 8px 12px; border-radius: 6px; font-size: 14px; margin-bottom: 16px; }
label { font-weight: bold; display: block; margin-top: 16px; }
.checkbox-label { font-weight: normal; margin-top: 14px; display: flex; align-items: center; gap: 8px; cursor: pointer; }
.checkbox-label input { width: auto; margin: 0; }
input, textarea { width: 100%; margin-top: 6px; padding: 10px; box-sizing: border-box; border: 1px solid #dadce0; border-radius: 4px; }
button { background-color: #1a73e8; color: white; border: none; padding: 10px 18px; margin-top: 16px; font-weight: bold; cursor: pointer; border-radius: 4px; }
button:hover { background-color: #1557b0; }
#status { margin-top: 16px; font-weight: bold; }
</style>
</head>
<body>
<h2>Support Quick Portal</h2>
<div class="badge">
Active Google Workspace Studio triggers in Firestore (<code>studio_triggers</code>):
<strong id="trigger-count">${activeCount}</strong>
</div>
<p>Submit a support ticket below to fire your Google Workspace Studio flow.</p>
<label for="title">Ticket Title</label>
<input type="text" id="title" placeholder="e.g. Database failure">
<label for="description">Issue Description</label>
<textarea id="description" rows="4" placeholder="Describe the issue..."></textarea>
<label class="checkbox-label" for="simulateFailure">
<input type="checkbox" id="simulateFailure">
Simulate transient failure (enqueue in Cloud Tasks and retry notifyUri)
</label>
<button onclick="submitTicket()">Submit Ticket</button>
<div id="status"></div>
<script>
async function submitTicket() {
const title = document.getElementById('title').value.trim();
const description = document.getElementById('description').value.trim();
const simulateFailure = document.getElementById('simulateFailure').checked;
const statusEl = document.getElementById('status');
if (!title || !description) {
statusEl.style.color = '#d93025';
statusEl.textContent = 'Please fill out both fields.';
return;
}
statusEl.style.color = '#188038';
statusEl.textContent = 'Submitting ticket...';
try {
const res = await fetch('/api/submit-ticket', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, description, simulateFailure }),
});
const data = await res.json();
if (data.ok) {
if (data.tasksQueued > 0) {
statusEl.textContent =
'Ticket saved! Initial notifyUri call did not succeed; queued ' +
data.tasksQueued +
' retry task(s) in Cloud Tasks.';
} else {
statusEl.textContent =
'Ticket saved to Firestore! Fired ' +
data.triggersFired +
' Google Workspace Studio trigger(s).';
}
} else {
statusEl.style.color = '#d93025';
statusEl.textContent = 'Error: ' + (data.error || 'Unknown error');
}
} catch (err) {
statusEl.style.color = '#d93025';
statusEl.textContent = 'Request failed: ' + err.message;
}
}
</script>
</body>
</html>`);
}
// Portal Form Submission -> Call notifyUri directly, or enqueue Cloud Task on failure
export async function submitTicket(req, res) {
const title = req.body?.title || 'Untitled Ticket';
const description = req.body?.description || '';
const simulateFailure = Boolean(req.body?.simulateFailure);
const results = [];
let triggersFired = 0;
let tasksQueued = 0;
try {
const snapshot = await triggersCollection.get();
for (const doc of snapshot.docs) {
const trigger = doc.data();
if (!trigger.triggerId || !trigger.userOAuthToken) continue;
const requestId =
'req_' + Date.now() + '_' + Math.floor(Math.random() * 1000);
let outcome = { ok: false, status: 503 };
if (!simulateFailure) {
try {
outcome = await fireTriggerDirect(trigger, {
title,
description,
requestId,
});
} catch (fetchErr) {
console.error(
`Direct trigger call failed for ${trigger.triggerId}:`,
fetchErr.message
);
outcome = { ok: false, status: 0, error: fetchErr.message };
}
}
if (outcome.ok) {
triggersFired++;
results.push({
triggerId: trigger.triggerId,
status: outcome.status,
queuedRetry: false,
});
} else {
const taskName = await enqueueTriggerRetry(req, {
triggerId: trigger.triggerId,
title,
description,
requestId,
simulateFailure,
});
tasksQueued++;
results.push({
triggerId: trigger.triggerId,
status: outcome.status,
queuedRetry: true,
taskName,
});
}
}
await ticketsCollection.add({
title,
description,
triggersFired,
tasksQueued,
createdAt: new Date().toISOString(),
});
res.status(200).json({
ok: true,
triggersFired,
tasksQueued,
results,
});
} catch (err) {
console.error('Error in /api/submit-ticket:', err.message);
res.status(500).json({ ok: false, error: err.message });
}
}
EOF
index.js - Express 服务器入口点
创建 index.js 以将门户路由、Cloud Tasks 重试工作器端点和 Google Workspace Studio Webhook 端点连接到在 PORT(Cloud Run 上的默认值为 8080)上监听的 Express HTTP 服务器:
cat << 'EOF' > index.js
import express from 'express';
import { onConfigTrigger, onManageTrigger } from './starter.js';
import { onConfigUrgency, onExecuteUrgency } from './action.js';
import { renderPortal, submitTicket } from './portal.js';
import { handleFireTriggerTask } from './tasks.js';
const app = express();
app.use(express.json());
// Support Quick Portal routes
app.get('/', renderPortal);
app.post('/api/submit-ticket', submitTicket);
// Cloud Tasks retry worker route
app.post('/api/tasks/fire-trigger', handleFireTriggerTask);
// Google Workspace Studio Custom Starter Step webhooks
app.post('/onConfigTrigger', onConfigTrigger);
app.post('/onManageTrigger', onManageTrigger);
// Google Workspace Studio Custom Action Step webhooks
app.post('/onConfigUrgency', onConfigUrgency);
app.post('/onExecuteUrgency', onExecuteUrgency);
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Support Extension listening on port ${PORT}`);
});
EOF
4. 部署到 Cloud Run 并安装插件
在此步骤中,您将 Node.js 服务部署到 Cloud Run,授予 Google Workspace 插件调用方和 Firestore、Cloud Tasks 和 Agent Platform 运行时访问权限的 IAM 权限,生成 deployment.json 清单,并安装插件部署。
- 使用项目、Firestore 集合和 Cloud Tasks 任务队列环境变量将
support-extension服务部署到 Cloud Run:gcloud run deploy support-extension \ --source . \ --region us-central1 \ --allow-unauthenticated \ --set-env-vars="GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project),FIRESTORE_TRIGGERS_COLLECTION=studio_triggers,FIRESTORE_TICKETS_COLLECTION=support_tickets,CLOUD_TASKS_QUEUE=studio-trigger-retries,CLOUD_TASKS_LOCATION=us-central1" \ --quiet - 向 Google Workspace 插件服务账号授予 Cloud Run 服务上的
roles/run.invoker,以便 Google Workspace Studio 可以调用您的 Webhook 端点:ADDON_SA=$(gcloud workspace-add-ons get-authorization --format="value(serviceAccountEmail)") gcloud run services add-iam-policy-binding support-extension \ --region=us-central1 \ --member="serviceAccount:${ADDON_SA}" \ --role="roles/run.invoker" \ --quiet - 向 Cloud Run 运行时服务账号授予
roles/datastore.user(用于读取和写入studio_triggers和support_ticketsFirestore 集合)、roles/cloudtasks.enqueuer(用于将重试任务加入studio-trigger-retries)和roles/aiplatform.user(用于在 Gemini Enterprise Agent Platform 上调用 Gemini):PROJECT_ID=$(gcloud config get-value project) PROJECT_NUMBER=$(gcloud projects describe "${PROJECT_ID}" --format="value(projectNumber)") RUNTIME_SA=$(gcloud run services describe support-extension --region=us-central1 --format="value(spec.template.spec.serviceAccountName)") RUNTIME_SA=${RUNTIME_SA:-"${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"} gcloud projects add-iam-policy-binding "${PROJECT_ID}" \ --member="serviceAccount:${RUNTIME_SA}" \ --role="roles/datastore.user" \ --condition=None \ --quiet gcloud projects add-iam-policy-binding "${PROJECT_ID}" \ --member="serviceAccount:${RUNTIME_SA}" \ --role="roles/cloudtasks.enqueuer" \ --condition=None \ --quiet gcloud projects add-iam-policy-binding "${PROJECT_ID}" \ --member="serviceAccount:${RUNTIME_SA}" \ --role="roles/aiplatform.user" \ --condition=None \ --quiet - 检索已部署的 Cloud Run 网址,并生成
deployment.json清单,用于定义supportTicketTrigger(workflowTrigger) 和urgencyDetectorStep(workflowAction) 元素:SERVICE_URL=$(gcloud run services describe support-extension --region=us-central1 --format="value(status.url)") cat << EOF > deployment.json { "oauthScopes": [ "https://www.googleapis.com/auth/workspace.studio.trigger", "https://www.googleapis.com/auth/userinfo.email" ], "addOns": { "common": { "name": "Support Extension", "logoUrl": "https://www.gstatic.com/images/branding/productlogos/buganizer/v1/192px.svg" }, "studio": { "flows": { "workflowElements": [ { "id": "supportTicketTrigger", "state": "ACTIVE", "name": "New Support Ticket", "description": "Fires when someone submits a ticket in the Support Quick Portal.", "workflowTrigger": { "inputs": [], "outputs": [ { "id": "ticketTitle", "description": "Ticket Title", "cardinality": "SINGLE", "dataType": { "basicType": "STRING" } }, { "id": "ticketDescription", "description": "Ticket Description", "cardinality": "SINGLE", "dataType": { "basicType": "STRING" } } ], "onConfigFunction": "${SERVICE_URL}/onConfigTrigger", "onManageFunction": "${SERVICE_URL}/onManageTrigger" } }, { "id": "urgencyDetectorStep", "state": "ACTIVE", "name": "Detect Urgency", "description": "Evaluates the ticket description to detect High or Normal urgency.", "workflowAction": { "inputs": [ { "id": "ticketDescription", "description": "Description to scan", "cardinality": "SINGLE", "dataType": { "basicType": "STRING" } } ], "outputs": [ { "id": "urgencyLevel", "description": "Detected Urgency (High / Normal)", "cardinality": "SINGLE", "dataType": { "basicType": "STRING" } } ], "onConfigFunction": "${SERVICE_URL}/onConfigUrgency", "onExecuteFunction": "${SERVICE_URL}/onExecuteUrgency" } } ] } }, "httpOptions": { "authorizationHeader": "SYSTEM_ID_TOKEN" } } } EOF - 为您的账号创建(或替换)并安装 Google Workspace 插件部署,并输出 Support Quick Portal 网址:
gcloud workspace-add-ons deployments create support-extension --deployment-file=deployment.json || \ gcloud workspace-add-ons deployments replace support-extension --deployment-file=deployment.json gcloud workspace-add-ons deployments install support-extension echo "Support Quick Portal URL: ${SERVICE_URL}"
5. 测试自定义步骤和 Cloud Tasks 重试
- 打开 Google Workspace Studio(如果浏览器标签页已打开,请刷新该标签页),然后点击 新建流程。
- 依次点击选择启动器 > Support Extension > New Support Ticket 。
- 依次点击添加步骤 (
+) > Support Extension > Detect Urgency 。在来源说明 字段中,点击 + 变量 ,然后选择 Ticket Description 。 - 点击添加步骤 (
+),然后添加一个 Chat > Notify me in Chat 步骤,该步骤在消息中包含 Ticket Title、Urgency Level 和 Ticket Description 变量。 - 在右上角将流程切换为开启 。Google Workspace Studio 会立即在 Cloud Run 上调用
/onManageTrigger端点,该端点会将有效触发器订阅(包括triggerId、notifyUri和userOAuthToken)写入 Firestore 中的studio_triggers集合。 - 在新标签页中打开 Support Quick Portal 网址 (
https://support-extension-...run.app),确认门户在 Firestore 中显示1个有效触发器,然后提交两个工单以测试 Gemini 3.8 Flash (gemini-3.8-flash) 分类:- 高影响工单(Ticket Title:
London office lockout,Issue Description:All 500 employees in the London office are locked out of their workstations 20 minutes before the board presentation.):即使没有urgent或blocker等硬编码关键字,gemini-3.8-flash也会将此工单分类为High紧急程度。 - 常规工单 (Ticket Title:
Conference room adapters,Issue Description:Could we order two extra HDMI adapters for the third-floor conference room next month?):gemini-3.8-flash会将此工单分类为Normal紧急程度。
- 高影响工单(Ticket Title:
- 在 Google Chat 和 Google Workspace Studio Activity 标签页中验证,您的流程是否针对这两个工单运行,并且
Urgency Level: High和Urgency Level: Normal。 - 在
notifyUri失败时测试 Cloud Tasks 重试传送 :- 在 Support Quick Portal 中,选中 模拟暂时性故障(在 Cloud Tasks 中加入队列并重试 notifyUri)。
- 输入 Ticket Title:
Checkout service outage和 Issue Description:Production payment gateway is timing out for all customers,然后点击 Submit Ticket。 - 请注意,门户会立即响应:
Ticket saved! Initial notifyUri call did not succeed; queued 1 retry task(s) in Cloud Tasks. - 在后台,Cloud Tasks 执行
POST /api/tasks/fire-trigger。第一个任务尝试 (retryCount=0) 模拟503失败,导致 Cloud Tasks 退避10s(--min-backoff=10s) 并重试 (retryCount=1),从而调用notifyUri并成功。大约 10-15 秒后,验证您的 Google Chat 通知是否已送达!
6. 清理
为避免持续产生 Google Cloud 费用,请在 Cloud Shell 中移除已部署的资源:
gcloud workspace-add-ons deployments uninstall support-extension
gcloud workspace-add-ons deployments delete support-extension --quiet
gcloud run services delete support-extension --region=us-central1 --quiet
gcloud tasks queues delete studio-trigger-retries --location=us-central1 --quiet
gcloud firestore databases delete --database="(default)" --quiet
7. 恭喜!
您已使用 Firestore 、Cloud Tasks 、Cloud Run 和 Gemini Enterprise Agent Platform 上的 Gemini 3.8 Flash 构建、部署和测试了 Google Workspace Studio 的 Node.js HTTP 扩展程序!
所学内容
- 使用
workflowTrigger和workflowAction元素配置 Google Workspace Studio HTTP 插件清单 (deployment.json)。 - 在
studio_triggersFirestore 集合中保留 Google Workspace StudioworkflowTrigger订阅,并在support_tickets中记录门户提交,以便触发器在 Cloud Run 扩缩和重新部署后仍然存在。 - 使用 Cloud Tasks (
studio-trigger-retries) 保证notifyUri(triggers.fire) 传送,以便使用 Firestore 中的最新userOAuthToken以指数退避算法自动重试暂时性 HTTP 失败。 - 构建一个
workflowAction步骤,该步骤使用 Gemini Enterprise Agent Platform 上的 Gemini 3.8 Flash (gemini-3.8-flash) 结构化输出对支持工单进行分类,并提供弹性关键字匹配回退。