1. Giới thiệu
Google Workspace Studio cho phép bạn tự động hoá các quy trình kinh doanh trên các ứng dụng của Google Workspace như Gmail, Google Chat, Google Drive và Google Trang tính. Khi quy trình công việc của bạn cần bắt đầu từ một ứng dụng web bên ngoài hoặc chạy logic tuỳ chỉnh trong môi trường đám mây của riêng bạn, bạn có thể mở rộng Google Workspace Studio bằng một Tiện ích bổ sung Google Workspace HTTP trong Node.js trên Cloud Run:
- Bước bắt đầu tuỳ chỉnh (
workflowTrigger): Bắt đầu một quy trình trong Google Workspace Studio khi một sự kiện bên ngoài xảy ra (chẳng hạn như khách hàng gửi phiếu yêu cầu hỗ trợ trong một cổng thông tin nội bộ trên web) bằng cách gọinotifyUricủa trình kích hoạt (hoặc API RESTtriggers.firecủa Google Workspace Studio). - Bước hành động tuỳ chỉnh (
workflowAction): Nhận các biến từ các bước trước đó trong quy trình, chạy logic tuỳ chỉnh hoặc suy luận AI trong dịch vụ Node.js của bạn và trả về các biến đầu ra cho Google Workspace Studio.
Trong lớp học lập trình này, bạn sẽ xây dựng và triển khai một tiện ích HTTP Node.js hoàn chỉnh cho Google Workspace Studio trên Cloud Run, sử dụng Cloud Firestore để duy trì các gói thuê bao trình kích hoạt luồng đang hoạt động và phiếu yêu cầu hỗ trợ đã gửi trên các bản sửa đổi vùng chứa, sử dụng Cloud Tasks để tự động thử lại thông báo trình kích hoạt nếu notifyUri không trả về phản hồi thành công, đồng thời phân loại mức độ khẩn cấp của phiếu yêu cầu hỗ trợ trong bước thao tác tuỳ chỉnh bằng Gemini 3.8 Flash (gemini-3.8-flash) trên Nền tảng tác nhân Gemini Enterprise với phương án dự phòng so khớp từ khoá linh hoạt.
Sản phẩm bạn sẽ tạo ra
- Một ứng dụng web Support Quick Portal và Tiện ích bổ sung HTTP của Google Workspace được tạo bằng Node.js, Express, Firestore và Cloud Tasks trên Cloud Run.
- Bước bắt đầu tuỳ chỉnh Phiếu yêu cầu hỗ trợ mới (
supportTicketTrigger) duy trì các lượt đăng ký quy trình đang hoạt động trong một bộ sưu tậpstudio_triggersFirestore, ghi nhật ký các phiếu yêu cầu đã gửi trong một bộ sưu tậpsupport_ticketsFirestore và kích hoạt bất cứ khi nào một phiếu yêu cầu được gửi trong cổng thông tin web – truyềnticketTitlevàticketDescriptionvào một quy trình Google Workspace Studio. - Trình chạy thử lại của Cloud Tasks (hàng đợi
studio-trigger-retriesvàPOST /api/tasks/fire-trigger) sẽ đưa vào hàng đợi và thử lại các lệnh gọi điều kiện kích hoạt không thành công bằng thuật toán thời gian đợi luỹ thừa, sử dụnguserOAuthTokenmới nhất được lưu trữ trong Firestore, đảm bảo phân phối đếnnotifyUri. - Một bước thao tác tuỳ chỉnh Phát hiện mức độ khẩn cấp (
urgencyDetectorStep) giúp phân loại mức độ khẩn cấp của phiếu yêu cầu bằng cách sử dụng Gemini 3.8 Flash (gemini-3.8-flash) trên Nền tảng tác nhân Gemini Enterprise (aiplatform.googleapis.com), với tính năng tự động quay lại chế độ so khớp từ khoá (urgent,blocker,critical).
Kiến trúc
Biểu đồ sau đây minh hoạ cách Cổng hỗ trợ nhanh, tiện ích bổ sung Node.js của bạn trên Cloud Run, Cloud Firestore, Cloud Tasks (studio-trigger-retries), Gemini 3.8 Flash và Google Workspace Studio tương tác từ quá trình đăng ký quy trình cho đến việc phân phối trình kích hoạt linh hoạt và thực thi hành động:

Kiến thức bạn sẽ học được
- Cách định cấu hình tệp kê khai triển khai Tiện ích bổ sung Google Workspace qua HTTP (
deployment.json) bằng các phần tửworkflowTriggervàworkflowAction. - Cách duy trì các lượt đăng ký sự kiện kích hoạt Google Workspace Studio (
triggerId,notifyUrivàuserOAuthToken) trong một bộ sưu tậpstudio_triggersFirestore và kích hoạt các luồng bằngnotifyUri/triggers.fire. - Cách sử dụng Cloud Tasks (
@google-cloud/tasks) để thử gửi lại thông báo kích hoạt không thành công bằng thuật toán đợi luỹ tuyến cho đến khinotifyUritrả về phản hồi thành công2xx. - Cách tạo một bước tuỳ chỉnh
workflowActiontrong Node.js bằng đầu ra có cấu trúc Gemini 3.8 Flash (gemini-3.8-flash) trên Nền tảng Tác nhân Gemini Enterprise với một từ khoá dự phòng.
Điều kiện tiên quyết
- Tài khoản Google Workspace có quyền truy cập vào Google Workspace Studio.
- Một dự án Google Cloud đã bật tính năng thanh toán, được tạo trong cùng một tổ chức Google Workspace với tài khoản người dùng của bạn để bạn có thể cài đặt các bản triển khai tiện ích bổ sung chưa xuất bản nhằm mục đích thử nghiệm.
2. Thiết lập dự án trên đám mây của Google Cloud, Firestore và Cloud Tasks
Trong bước này, bạn sẽ định cấu hình màn hình xin phép bằng OAuth của dự án, bật các API Google Cloud và Google Workspace cần thiết trong Cloud Shell, cấp cơ sở dữ liệu (default) Firestore, tạo hàng đợi thử lại Cloud Tasks và khởi chạy tài khoản dịch vụ uỷ quyền của tiện ích bổ sung.
- Mở Google Cloud Console, chọn dự án trên đám mây của bạn rồi nhấp vào biểu tượng Kích hoạt Cloud Shell trong thanh điều hướng trên cùng.
- Định cấu hình màn hình xin phép bằng OAuth để Google Workspace có thể cung cấp một ứng dụng OAuth cho tiện ích bổ sung của bạn:
- Mở trang Thương hiệu của Nền tảng xác thực của Google.
- Nếu được nhắc, hãy nhấp vào Bắt đầu. (Nếu màn hình xin phép của bạn đã được định cấu hình, hãy chuyển sang bước 3.)
- Trong phần Thông tin ứng dụng, hãy nhập
Support Extensioncho Tên ứng dụng, chọn Email hỗ trợ người dùng rồi nhấp vào Tiếp theo. - Trong phần Đối tượng, hãy chọn Nội bộ rồi nhấp vào Tiếp theo.
- Trong phần Thông tin liên hệ, hãy nhập địa chỉ email của bạn rồi nhấp vào Tiếp theo.
- Trong phần Hoàn tất, hãy đồng ý với Chính sách dữ liệu người dùng của dịch vụ API của Google, nhấp vào Tiếp tục rồi nhấp vào Tạo.
- Trong Cloud Shell, hãy đặt dự án đang hoạt động (thay thế
YOUR_PROJECT_IDbằng mã dự án Google Cloud của bạn) và bật các API bắt buộc: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 - Cung cấp cơ sở dữ liệu
(default)Firestore và hàng đợistudio-trigger-retriesCloud Tasks trongus-central1(nếu chưa có) rồi khởi động tài khoản dịch vụ Google Workspace Add-ons: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. Tạo ứng dụng Node.js bằng Firestore, Cloud Tasks và Gemini 3.8 Flash
Trong bước này, bạn sẽ tạo ứng dụng Node.js trong ~/studio-support-addon. Để mã có tính mô-đun và dễ đọc, bạn sắp xếp dự án thành 6 tệp tập trung:
firestore.js: Khởi chạy ứng dụng Cloud Firestore (bộ sưu tậpstudio_triggersvàsupport_tickets) và làm mới mã thông báo OAuth đã lưu vào bộ nhớ đệm.starter.js: Triển khai webhook bước khởi đầu tuỳ chỉnh Phiếu yêu cầu hỗ trợ mới (/onConfigTriggervà/onManageTrigger).action.js: Triển khai webhook bước thao tác tuỳ chỉnh Phát hiện mức độ khẩn cấp (/onConfigUrgencyvà/onExecuteUrgency) bằng Gemini 3.8 Flash (gemini-3.8-flash) với phương án dự phòng so khớp từ khoá.tasks.js: Gửi thông báo kích hoạt đếntrigger.notifyUri(quay lạihttps://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire), đưa các lần gửi không thành công vào hàng đợistudio-trigger-retriesCloud Tasks và triển khai điểm cuối của trình thực thi thử lạiPOST /api/tasks/fire-trigger.portal.js: Phục vụ giao diện người dùng web Cổng hỗ trợ nhanh (GET /) và kích hoạt các trình kích hoạt Google Workspace Studio đang hoạt động (POST /api/submit-ticket), với tuỳ chọn mô phỏng lỗi tạm thời và xem Cloud Tasks thử lại.index.js: Kết nối tất cả các tuyến đường Express với nhau và khởi động máy chủ HTTP.
Khởi động dự án và package.json
- Tạo một thư mục dự án rồi chuyển đến thư mục đó:
mkdir -p ~/studio-support-addon && cd ~/studio-support-addon - Tạo
package.jsonbằngexpress,@google-cloud/firestore,@google-cloud/tasksvà@google/genai: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 – Trạng thái Firestore được chia sẻ và làm mới mã thông báo OAuth
Tạo firestore.js để quản lý hai tập hợp Firestore và luôn cập nhật mã thông báo OAuth của người dùng đã lưu vào bộ nhớ đệm:
studio_triggers: Lưu trữ các gói thuê bao luồng Google Workspace Studio đang hoạt động (triggerId,notifyUrivàuserOAuthToken) để các gói thuê bao này vẫn hoạt động khi vùng chứa Cloud Run khởi động lại và các sự kiện điều chỉnh về 0.support_tickets: Lưu trữ bản ghi của từng phiếu yêu cầu hỗ trợ được gửi qua cổng thông tin.refreshTokenInFirestore(req): Bất cứ khi nào Google Workspace Studio gọi bất kỳ điểm cuối webhook nào của bạn, hệ thống sẽ bao gồm mộtauthorizationEventObject.userOAuthTokencó thời hạn ngắn. Trợ giúp này sẽ cập nhật mã thông báo mới nhất cho các tài liệu kích hoạt đang hoạt động để các lệnh gọitriggers.fireở chế độ nền vẫn được uỷ quyền.
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 — Bước bắt đầu tuỳ chỉnh (workflowTrigger)
Tạo starter.js để triển khai hai điểm cuối webhook mà một bước khởi đầu tuỳ chỉnh của Google Workspace Studio yêu cầu:
onConfigTrigger(POST /onConfigTrigger): Được gọi khi người dùng chọn Phiếu yêu cầu hỗ trợ mới làm điểm bắt đầu của quy trình trong Google Workspace Studio. Trả về một thao tác điều hướngpushCardmô tả thành phần khởi động.onManageTrigger(POST /onManageTrigger): Được gọi khi người dùng Bật (req.body.workflow.triggerCreation) hoặc Tắt / xoá (req.body.workflow.triggerDeletion) một luồng. Lưu hoặc xoá tài liệu đăng ký trong bộ sưu tậpstudio_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 — Bước hành động tuỳ chỉnh (workflowAction) với Gemini 3.8 Flash
Tạo action.js để triển khai hai điểm cuối webhook mà một bước thao tác tuỳ chỉnh của Google Workspace Studio yêu cầu:
onConfigUrgency(POST /onConfigUrgency): Trả về một thẻ cấu hình có tiện íchtextInputđặthostAppDataSource.workflowDataSource.includeVariables: true, cho phép người dùng chọn biếnticketDescriptiondo bước khởi đầu xuất ra.onExecuteUrgency(POST /onExecuteUrgency): Đọc dữ liệu đầu vàoticketDescriptiontừreq.body.workflow.actionInvocation.inputs, phân loại mức độ tác động và mức độ khẩn cấp của dữ liệu đó đối với doanh nghiệp bằng cách sử dụng Gemini 3.8 Flash (gemini-3.8-flash) trên Nền tảng tác nhân Gemini Enterprise với đầu ra có cấu trúc (responseSchemaenum["High", "Normal"]), quay lạidetectKeywordUrgency()(urgent,blocker,critical) nếu lệnh gọi mô hình không thành công và trả về biến đầu raurgencyLevelbằnghostAppAction.workflowAction.returnOutputVariablesAction.
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 – Hàng đợi thử lại và điểm cuối của worker Cloud Tasks
Tạo tasks.js để gửi lệnh gọi điều kiện kích hoạt đến trigger.notifyUri (quay lại https://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire), đưa các lệnh gọi không thành công vào hàng đợi Cloud Tasks studio-trigger-retries và xử lý các lần thử lại tại POST /api/tasks/fire-trigger:
resolveTriggerUrl(trigger): Sử dụngtrigger.notifyUriđược duy trì từtriggerCreation.notifyUrikhi có hoặc quay lạihttps://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire.fireTriggerDirect(trigger, payload): Gửi yêu cầuPOSTđến URL của điều kiện kích hoạt bằng các biến đầu raticketTitlevàticketDescription, cũng như mộtrequestIdcó tính chất luỹ đẳng.enqueueTriggerRetry(req, payload): Đưa một tác vụ HTTP vào hàng đợistudio-trigger-retriesnhắm đến điểm cuốiPOST /api/tasks/fire-triggercủa dịch vụ Cloud Run bất cứ khi nào lệnh gọi kích hoạt ban đầu không thành công hoặc trả về trạng thái HTTP không phải 2xx.handleFireTriggerTask(POST /api/tasks/fire-trigger): Được Cloud Tasks gọi trong mỗi lần thử lại. Tra cứu tài liệutriggermới nhất trongstudio_triggerstừ Firestore (đảm bảo các lần thử lại luôn sử dụnguserOAuthTokenmới nhất), kiểm tra tiêu đềX-CloudTasks-TaskRetryCount(nếusimulateFailuređược bật, thì tiêu đề này sẽ khiến lần thực thi Cloud Task đầu tiên thất bại với503để bạn có thể xem Cloud Tasks giảm tải và thành công khi thử lại#1) và chỉ trả về200 OKkhinotifyUritrả về phản hồi thành công2xx. Mọi phản hồi5xxđều yêu cầu Cloud Tasks thử lại theo chính sách thời gian đợi của hàng đợi.
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 — Hỗ trợ giao diện người dùng Cổng thông tin nhanh và trình điều phối notifyUri có phương án dự phòng Cloud Tasks
Tạo portal.js để phân phát cổng thông tin trên web và gửi yêu cầu tạo phiếu yêu cầu đến Google Workspace Studio bằng tính năng tự động lập lịch thử lại Cloud Tasks:
renderPortal(GET /): Truy vấn tập hợpstudio_triggersFirestore để hiển thị số lượng trình kích hoạt Google Workspace Studio đang hoạt động đã được đăng ký và phân phát biểu mẫu gửi phiếu yêu cầu hỗ trợ HTML (bao gồm cả hộp đánh dấu Mô phỏng lỗi tạm thời để kiểm tra các lần thử lại của Cloud Tasks).submitTicket(POST /api/submit-ticket): Lặp lại tất cả tài liệu về điều kiện kích hoạt đang hoạt động trongstudio_triggersvà gọifireTriggerDirect(). Nếu URL kích hoạt trả về phản hồi không phải 2xx, gửi lỗi mạng hoặc đã bậtsimulateFailure, thì URL đó sẽ tự động đưa một tác vụ thử lại vào hàng đợi trongstudio-trigger-retriesbằng cách sử dụngenqueueTriggerRetry()và ghi nhật ký phiếu yêu cầu hỗ trợ trong tập hợpsupport_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 – Điểm truy cập vào máy chủ Express
Tạo index.js để kết nối các tuyến cổng, điểm cuối của trình thực thi thử lại Cloud Tasks và điểm cuối webhook của Google Workspace Studio với nhau vào một máy chủ HTTP Express đang nghe trên PORT (mặc định là 8080 trên Cloud Run):
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. Triển khai lên Cloud Run và cài đặt tiện ích bổ sung
Trong bước này, bạn sẽ triển khai dịch vụ Node.js lên Cloud Run, cấp quyền IAM cho trình gọi Tiện ích bổ sung của Google Workspace và quyền truy cập vào thời gian chạy Firestore, Cloud Tasks và Agent Platform, tạo tệp kê khai deployment.json và cài đặt việc triển khai tiện ích bổ sung.
- Triển khai dịch vụ
support-extensionlên Cloud Run bằng dự án, tập hợp Firestore và các biến môi trường hàng đợi tác vụ Cloud Tasks: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 - Cấp
roles/run.invokertrên dịch vụ Cloud Run cho tài khoản dịch vụ Tiện ích bổ sung của Google Workspace để Google Workspace Studio có thể gọi các điểm cuối webhook của bạn: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 - Cấp
roles/datastore.user(để đọc và ghi các tập hợpstudio_triggersvàsupport_ticketsFirestore),roles/cloudtasks.enqueuer(để xếp hàng các tác vụ thử lại vàostudio-trigger-retries) vàroles/aiplatform.user(để gọi Gemini trên Gemini Enterprise Agent Platform) cho tài khoản dịch vụ thời gian chạy Cloud Run: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 - Truy xuất URL Cloud Run đã triển khai và tạo tệp kê khai
deployment.jsonxác định các phần tửsupportTicketTrigger(workflowTrigger) và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 - Tạo (hoặc thay thế) và cài đặt bản triển khai Tiện ích bổ sung Google Workspace cho tài khoản của bạn, đồng thời in URL của Cổng hỗ trợ nhanh:
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. Kiểm tra các bước tuỳ chỉnh và số lần thử lại của Cloud Tasks
- Mở Google Workspace Studio (làm mới thẻ trình duyệt nếu thẻ này đã mở) rồi nhấp vào Quy trình mới.
- Nhấp vào Chọn điều kiện khởi động > Tiện ích hỗ trợ > Phiếu yêu cầu hỗ trợ mới.
- Nhấp vào Thêm bước (
+) > Tiện ích hỗ trợ > Phát hiện tính khẩn cấp. Trong trường Mô tả nguồn, hãy nhấp vào + Biến rồi chọn Mô tả vé. - Nhấp vào Thêm bước (
+) rồi thêm bước Chat (Trò chuyện) > Notify me in Chat (Thông báo cho tôi trong Chat) có các biến Ticket Title (Tiêu đề phiếu yêu cầu), Urgency Level (Mức độ khẩn cấp) và Ticket Description (Nội dung mô tả phiếu yêu cầu) trong thông báo. - Bật quy trình ở góc trên cùng bên phải. Google Workspace Studio ngay lập tức gọi điểm cuối
/onManageTriggercủa bạn trên Cloud Run. Điểm cuối này sẽ ghi gói thuê bao trình kích hoạt đang hoạt động (bao gồmtriggerId,notifyUrivàuserOAuthToken) vào tập hợpstudio_triggerstrong Firestore. - Mở URL của Cổng hỗ trợ nhanh (
https://support-extension-...run.app) trong một thẻ mới, xác nhận rằng cổng này hiển thị1điều kiện kích hoạt đang hoạt động trong Firestore và gửi hai phiếu yêu cầu hỗ trợ để kiểm tra việc phân loại của Gemini 3.8 Flash (gemini-3.8-flash):- Phiếu yêu cầu hỗ trợ có mức độ tác động cao (Tiêu đề phiếu yêu cầu hỗ trợ:
London office lockout, Nội dung mô tả vấn đề:All 500 employees in the London office are locked out of their workstations 20 minutes before the board presentation.): Ngay cả khi không có từ khoá được cố định giá trị trong mã nhưurgenthoặcblocker,gemini-3.8-flashvẫn phân loại phiếu yêu cầu hỗ trợ này là có mức độ khẩn cấpHigh. - Phiếu yêu cầu hỗ trợ thông thường (Tiêu đề phiếu yêu cầu hỗ trợ:
Conference room adapters, Nội dung mô tả vấn đề:Could we order two extra HDMI adapters for the third-floor conference room next month?):gemini-3.8-flashphân loại phiếu yêu cầu hỗ trợ này làNormalkhẩn cấp.
- Phiếu yêu cầu hỗ trợ có mức độ tác động cao (Tiêu đề phiếu yêu cầu hỗ trợ:
- Kiểm tra trong Google Chat và thẻ Hoạt động của Google Workspace Studio để xác minh rằng quy trình của bạn đã chạy cho cả hai yêu cầu hỗ trợ có
Urgency Level: HighvàUrgency Level: Normal. - Kiểm thử Cloud Tasks retry delivery (Thử lại việc phân phối Cloud Tasks) khi
notifyUrikhông thành công:- Trong Support Quick Portal (Cổng hỗ trợ nhanh), hãy chọn Simulate transient failure (enqueue in Cloud Tasks and retry notifyUri) (Mô phỏng lỗi tạm thời (đưa vào hàng đợi trong Cloud Tasks và thử lại notifyUri)).
- Nhập Tiêu đề phiếu yêu cầu hỗ trợ:
Checkout service outagevà Nội dung mô tả vấn đề:Production payment gateway is timing out for all customers, sau đó nhấp vào Gửi phiếu yêu cầu hỗ trợ. - Lưu ý rằng cổng thông tin sẽ phản hồi ngay lập tức bằng:
Ticket saved! Initial notifyUri call did not succeed; queued 1 retry task(s) in Cloud Tasks. - Ở chế độ nền, Cloud Tasks thực thi
POST /api/tasks/fire-trigger. Lần thử tác vụ đầu tiên (retryCount=0) mô phỏng lỗi503, khiến Cloud Tasks lùi lại trong10s(--min-backoff=10s) và thử lại (retryCount=1), gọinotifyUrivà thành công. Sau khoảng 10 đến 15 giây, hãy kiểm tra để đảm bảo bạn nhận được thông báo trên Google Chat!
6. Dọn dẹp
Để tránh bị tính phí liên tục cho tài khoản Google Cloud, hãy xoá các tài nguyên đã triển khai trong 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. Xin chúc mừng!
Bạn đã xây dựng, triển khai và thử nghiệm một tiện ích HTTP Node.js cho Google Workspace Studio bằng Firestore, Cloud Tasks, Cloud Run và Gemini 3.8 Flash trên Gemini Enterprise Agent Platform!
Nội dung đã đề cập
- Định cấu hình tệp kê khai Tiện ích bổ sung HTTP của Google Workspace Studio (
deployment.json) bằng các phần tửworkflowTriggervàworkflowAction. - Duy trì các gói thuê bao Google Workspace Studio
workflowTriggertrong tập hợpstudio_triggersFirestore và ghi lại các lượt gửi trên cổng thông tin trongsupport_ticketsđể các sự kiện kích hoạt vẫn hoạt động khi Cloud Run mở rộng quy mô và triển khai lại. - Đảm bảo phân phối
notifyUri(triggers.fire) bằng Cloud Tasks (studio-trigger-retries) để các lỗi HTTP tạm thời được tự động thử lại bằng thuật toán thời gian đợi luỹ thừa bằng cách sử dụnguserOAuthTokenmới nhất từ Firestore. - Xây dựng bước
workflowActionphân loại phiếu yêu cầu hỗ trợ bằng đầu ra có cấu trúc Gemini 3.8 Flash (gemini-3.8-flash) trên Nền tảng Tác nhân Gemini Enterprise, với cơ chế dự phòng khớp từ khoá linh hoạt.