1. מבוא
Google Workspace Studio מאפשר לכם להפוך תהליכים עסקיים לאוטומטיים באפליקציות של Google Workspace כמו Gmail, Google Chat, Google Drive ו-Google Sheets. אם תהליכי העבודה שלכם צריכים להתחיל מאפליקציית אינטרנט חיצונית או להריץ לוגיקה בהתאמה אישית בסביבת הענן שלכם, אתם יכולים להרחיב את Google Workspace Studio באמצעות תוסף HTTP ל-Google Workspace ב-Node.js ב-Cloud Run:
- שלב התחלה בהתאמה אישית (
workflowTrigger): הפעלה של תהליך ב-Google Workspace Studio כשמתרחש אירוע חיצוני, כמו לקוח ששולח כרטיס תמיכה בפורטל אינטרנט פנימי, על ידי קריאה ל-notifyUriשל הטריגר (או ל-triggers.fireREST API של Google Workspace Studio). - שלב פעולה בהתאמה אישית (
workflowAction): מקבל משתנים משלבים קודמים בתהליך, מריץ לוגיקה בהתאמה אישית או הסקת AI בשירות Node.js ומחזיר משתני פלט ל-Google Workspace Studio.
ב-Codelab הזה, תבנו ותפרסו תוסף HTTP מלא של Node.js ל-Google Workspace Studio ב-Cloud Run, תשתמשו ב-Cloud Firestore כדי לשמר מינויים פעילים לטריגרים של תהליכים וכרטיסי תמיכה שנשלחו בין גרסאות קונטיינרים, תשתמשו ב-Cloud Tasks כדי לנסות שוב באופן אוטומטי לשלוח התראות על טריגרים אם notifyUri לא מחזיר תגובת הצלחה, ותסווגו את דחיפות הכרטיסים בשלב הפעולה בהתאמה אישית באמצעות Gemini 3.8 Flash (gemini-3.8-flash) ב-Gemini Enterprise Agent Platform עם חלופה גמישה של התאמה למילות מפתח.
מה תפַתחו
- אפליקציית אינטרנט של Support Quick Portal ותוסף HTTP ל-Google Workspace שנוצרו באמצעות Node.js, Express, Firestore ו-Cloud Tasks ב-Cloud Run.
- שלב התחלה מותאם אישית של כרטיס תמיכה חדש (
supportTicketTrigger) ששומר מינויים פעילים של תהליכי עבודה באוסףstudio_triggersFirestore, מתעד כרטיסים שנשלחו באוסףsupport_ticketsFirestore ומופעל בכל פעם שכרטיס נשלח בפורטל האינטרנט – ומעביר אתticketTitleו-ticketDescriptionלתהליך עבודה של Google Workspace Studio. - תהליך עבודה לניסיון חוזר ב-Cloud Tasks (תור
studio-trigger-retriesו-POST /api/tasks/fire-trigger) שמוסיף לתור ומנסה שוב קריאות לטריגר שנכשלו עם השהיה מעריכית לפני ניסיון חוזר (exponential backoff) באמצעות הגרסה העדכנית ביותר שלuserOAuthTokenשמאוחסנת ב-Firestore, ומבטיח מסירה ל-notifyUri. - שלב של פעולה מותאמת אישית Detect Urgency (זיהוי דחיפות) (
urgencyDetectorStep) שמסווג את הדחיפות של הכרטיס באמצעות Gemini 3.8 Flash (gemini-3.8-flash) ב-Gemini Enterprise Agent Platform (aiplatform.googleapis.com), עם מעבר אוטומטי לחיפוש התאמה למילות מפתח (urgent, blocker, critical).
ארכיטקטורה
בתרשים הבא מוצגות האינטראקציות בין פורטל התמיכה המהיר, תוסף Node.js ב-Cloud Run, Cloud Firestore, Cloud Tasks (studio-trigger-retries), Gemini 3.8 Flash ו-Google Workspace Studio, החל מרישום הזרימה ועד להפעלת טריגרים ולביצוע פעולות בצורה עמידה:

מה תלמדו
- איך מגדירים מניפסט פריסה של תוסף HTTP ל-Google Workspace (
deployment.json) עם רכיביworkflowTriggerו-workflowAction. - איך לשמר מינויים לטריגרים של Google Workspace Studio (
triggerId,notifyUriו-userOAuthToken) באוסףstudio_triggersFirestore ולהפעיל תהליכים באמצעותnotifyUriאוtriggers.fire. - איך משתמשים ב-Cloud Tasks (
@google-cloud/tasks) כדי לנסות שוב לשלוח התראות על טריגרים שנכשלו עם השהיה מעריכית לפני ניסיון חוזר (exponential backoff) עד ש-notifyUriמחזיר תגובת הצלחה2xx. - איך ליצור
workflowActionשלב מותאם אישית ב-Node.js באמצעות פלט מובנה של Gemini 3.8 Flash (gemini-3.8-flash) ב-Gemini Enterprise Agent Platform עם חזרה למילת מפתח.
דרישות מוקדמות
- חשבון Google Workspace עם גישה ל-Google Workspace Studio.
- פרויקט ב-Google Cloud עם חיוב מופעל, שנוצר באותו ארגון ב-Google Workspace כמו חשבון המשתמש שלכם, כדי שתוכלו להתקין פריסות של תוספים שלא פורסמו לצורך בדיקה.
2. הגדרה של פרויקט Google Cloud, Firestore ו-Cloud Tasks
בשלב הזה מגדירים את מסך ההסכמה ל-OAuth של הפרויקט, מפעילים את ממשקי ה-API הנדרשים של Google Cloud ו-Google Workspace ב-Cloud Shell, מקצים את מסד הנתונים של (default) Firestore, יוצרים את תור הניסיונות החוזרים של Cloud Tasks ומפעילים את חשבון השירות של שירות ההרשאות של התוסף.
- פותחים את מסוף Google Cloud, בוחרים את פרויקט בענן ולוחצים על הסמל Activate Cloud Shell (הפעלה של Cloud Shell) בסרגל הניווט העליון.
- מגדירים את מסך ההסכמה ל-OAuth כדי ש-Google Workspace יוכל להקצות לקוח OAuth לתוסף:
- פותחים את דף המיתוג של פלטפורמת האימות של Google.
- אם מוצגת בקשה, לוחצים על שנתחיל?. (אם מסך ההסכמה כבר מוגדר, דלגו לשלב 3).
- בקטע App Information, מזינים
Support Extensionבשדה App name, בוחרים את User support email ולוחצים על Next. - בקטע Audience, לוחצים על Internal ואז על Next.
- בקטע פרטים ליצירת קשר, מזינים את כתובת האימייל ולוחצים על הבא.
- בקטע Finish, מאשרים את המדיניות של Google בנושא נתוני משתמשים בשירותי API, לוחצים על Continue ואז על Create.
- ב-Cloud Shell, מגדירים את הפרויקט הפעיל (מחליפים את
YOUR_PROJECT_IDבמזהה הפרויקט ב-Google Cloud) ומפעילים את ממשקי ה-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 - מקצים את מסד הנתונים של
(default)Firestore ואת תור המשימות שלstudio-trigger-retriesCloud Tasks ב-us-central1(אם הם עדיין לא קיימים), ומאתחלים את חשבון השירות של התוספים ל-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. פיתוח אפליקציית Node.js באמצעות Firestore, Cloud Tasks ו-Gemini 3.8 Flash
בשלב הזה, אתם בונים את אפליקציית Node.js ב-~/studio-support-addon. כדי שהקוד יהיה מודולרי וקריא, הפרויקט מחולק לשישה קבצים ממוקדים:
-
firestore.js: מאתחל את הלקוח של Cloud Firestore (אוספיstudio_triggersו-support_tickets) ומרענן את הטוקנים של OAuth שנשמרו במטמון. -
starter.js: הטמעה של ווּבּהוקים (webhook) של שלב התחלה מותאם אישית כרטיס תמיכה חדש (/onConfigTriggerו-/onManageTrigger). -
action.js: הטמעה של ה-webhooks של שלב הפעולה בהתאמה אישית Detect Urgency (/onConfigUrgencyו-/onExecuteUrgency) באמצעות Gemini 3.8 Flash (gemini-3.8-flash) עם חלופה של התאמה למילות מפתח. -
tasks.js: שולח התראות טריגר אלtrigger.notifyUri(עם חזרה אלhttps://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire), מוסיף לתור שלstudio-trigger-retriesCloud Tasks מסירות שנכשלו ומטמיע את נקודת הקצה של ה-worker לניסיונות חוזרים שלPOST /api/tasks/fire-trigger. - <!-- : Serves the Support Quick Portal web UI () and fires active Google Workspace Studio triggers (), with an option to simulate a transient failure and watch Cloud Tasks retry. -->
portal.js: מציג את ממשק המשתמש של Support Quick Portal באינטרנט (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 - יצירת
package.jsonעםexpress,@google-cloud/firestore,@google-cloud/tasksו-@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 — שיתוף סטטוס של 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 כדי להטמיע את שתי נקודות הקצה של ה-webhook שנדרשות לשלב התחלה בהתאמה אישית ב-Google Workspace Studio:
-
onConfigTrigger(POST /onConfigTrigger): נקראת כשמשתמש בוחר באפשרות כרטיס תמיכה חדש כטריגר לתהליך שלו ב-Google Workspace Studio. הפונקציה מחזירה פעולת ניווט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 כדי להטמיע את שתי נקודות הקצה של ה-webhook שנדרשות לשלב של פעולה בהתאמה אישית ב-Google Workspace Studio:
-
onConfigUrgency(POST /onConfigUrgency): מחזיר כרטיס הגדרה עם ווידג'טtextInputשמגדיר אתhostAppDataSource.workflowDataSource.includeVariables: true, ומאפשר למשתמשים לבחור את המשתנהticketDescriptionשמופק משלב ההתחלה. -
onExecuteUrgency(POST /onExecuteUrgency): קורא את הקלט שלticketDescriptionמ-req.body.workflow.actionInvocation.inputs, מסווג את ההשפעה העסקית והדחיפות שלו באמצעות Gemini 3.8 Flash (gemini-3.8-flash) ב-Gemini Enterprise Agent Platform עם פלט מובנה (responseSchemaenum["High", "Normal"]), חוזר ל-detectKeywordUrgency()(urgent,blocker,critical) אם הקריאה למודל נכשלת, ומחזיר את משתנה הפלט שלurgencyLevelבאמצעותhostAppAction.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 — תור לניסיון חוזר של 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): המערכת משתמשת ב-trigger.notifyUriשנשמר מ-triggerCreation.notifyUriאם הוא קיים, או חוזרת ל-https://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire. -
fireTriggerDirect(trigger, payload): שולח את הבקשהPOSTלכתובת ה-URL של הטריגר עם משתני הפלטticketTitleו-ticketDescriptionועםrequestIdאידמפוטנטי. -
enqueueTriggerRetry(req, payload): הוספה של משימת HTTP לתורstudio-trigger-retriesשמטרגטת את נקודת הקצה (endpoint)POST /api/tasks/fire-triggerשל שירות Cloud Run, בכל פעם שהקריאה הראשונית לטריגר נכשלת או מחזירה סטטוס HTTP שהוא לא 2xx. -
handleFireTriggerTask(POST /api/tasks/fire-trigger): מופעל על ידי Cloud Tasks בכל ניסיון חוזר. חיפוש המסמך האחרון שלtriggerב-studio_triggersמ-Firestore (כדי לוודא שבניסיונות החוזרים תמיד נעשה שימוש ב-userOAuthTokenהעדכני ביותר), בדיקת הכותרתX-CloudTasks-TaskRetryCount(אםsimulateFailureמופעל, ההפעלה הראשונה של Cloud Task נכשלת עם503כדי שתוכלו לראות את Cloud Tasks מבצע השהיה לפני ניסיון חוזר ומצליח בניסיון החוזר#1), והחזרת200 OKרק כש-notifyUriמחזיר תגובת הצלחה של2xx. כל תגובה מסוג5xxתגרום ל-Cloud Tasks לנסות שוב בהתאם למדיניות ההשהיה לפני ניסיון חוזר (backoff) של התור.
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 כדי להציג את פורטל האינטרנט ולשלוח את הפניות ל-Google Workspace Studio עם תזמון אוטומטי של ניסיונות חוזרים ב-Cloud Tasks:
-
renderPortal(GET /): שאילתה באוסףstudio_triggersFirestore כדי להציג כמה טריגרים פעילים של Google Workspace Studio רשומים, והצגת טופס שליחת הכרטיס HTML (כולל תיבת סימון Simulate transient failure (הדמיית כשל זמני) כדי לבדוק ניסיונות חוזרים של Cloud Tasks). -
submitTicket(POST /api/submit-ticket): עובר על כל מסמכי הטריגר הפעילים ב-studio_triggersוקורא ל-fireTriggerDirect(). אם כתובת ה-URL של הטריגר מחזירה תשובה שהיא לא 2xx, מקפיצה שגיאה בחיבור לרשת או שהאפשרותsimulateFailureמופעלת, היא מוסיפה באופן אוטומטי משימה של ניסיון חוזר לתור ב-studio-trigger-retriesבאמצעותenqueueTriggerRetry()ומתעדת את הכרטיס באוסףsupport_ticketsב-Firestore.
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 כדי לחבר את נתיבי הפורטל, נקודת הקצה של ה-worker לניסיון חוזר של Cloud Tasks ונקודות הקצה של ה-webhook של Google Workspace Studio לשרת Express HTTP שמאזין ב-PORT (ברירת המחדל היא 8080 ב-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. פריסה ב-Cloud Run והתקנת התוסף
בשלב הזה פורסים את שירות Node.js ב-Cloud Run, נותנים הרשאות IAM למפעיל של התוספים ל-Google Workspace ולגישה בזמן הריצה של Firestore, של Cloud Tasks ושל Agent Platform, יוצרים את המניפסט של deployment.json ומתקינים את פריסת התוסף.
- פורסים את שירות
support-extensionב-Cloud Run עם משתני הסביבה של הפרויקט, האוסף ב-Firestore ותור משימות של 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 - נותנים את ההרשאה
roles/run.invokerבשירות Cloud Run לחשבון השירות של Google Workspace Add-ons, כדי ש-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 - צריך להעניק את ההרשאות
roles/datastore.user(לקריאה וכתיבה של האוספיםstudio_triggersו-support_ticketsב-Firestore),roles/cloudtasks.enqueuer(להוספת משימות חוזרות לתור ב-studio-trigger-retries) ו-roles/aiplatform.user(להפעלת Gemini בפלטפורמת הסוכנים של Gemini Enterprise) לחשבון השירות של זמן הריצה ב-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 - מאחזרים את כתובת ה-URL של 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 בחשבון, ומדפיסים את כתובת ה-URL של 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 (תוסף תמיכה) > Detect Urgency (זיהוי דחיפות). בשדה תיאור המקור, לוחצים על + משתנים ובוחרים באפשרות תיאור הכרטיס. - לוחצים על הוספת שלב (
+) ומוסיפים שלב של צ'אט > התראה בצ'אט שכולל את המשתנים שם הכרטיס, רמת הדחיפות ותיאור הכרטיס בהודעה. - בפינה השמאלית העליונה, מעבירים את המתג של התהליך למצב מופעל. Google Workspace Studio קורא מיד לנקודת הקצה (endpoint) של
/onManageTriggerב-Cloud Run, שכותבת את המינוי הפעיל לטריגר (כוללtriggerId,notifyUriו-userOAuthToken) לאוסףstudio_triggersב-Firestore. - פותחים את כתובת ה-URL של Support Quick Portal (
https://support-extension-...run.app) בכרטיסייה חדשה, מוודאים שהפורטל מציג טריגר פעיל של1ב-Firestore, ושולחים שני כרטיסי תמיכה כדי לבדוק את הסיווג של Gemini 3.8 Flash (gemini-3.8-flash):- כרטיס עם השפעה גבוהה (שם הכרטיס:
London office lockout, תיאור הבעיה: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. - פנייה שגרתית (שם הפנייה:
Conference room adapters, תיאור הבעיה:Could we order two extra HDMI adapters for the third-floor conference room next month?):gemini-3.8-flashמסווג את הפנייה הזו כדחופה ברמהNormal.
- כרטיס עם השפעה גבוהה (שם הכרטיס:
- כדי לוודא שהתהליך של רצף הפעולות פעל עבור שני הכרטיסים עם
Urgency Level: Highו-Urgency Level: Normal, אפשר לבדוק את הכרטיסייה פעילות ב-Google Workspace Studio וב-Google Chat. - כדי לבדוק את ניסיון המסירה הנוסף של Cloud Tasks כש
notifyUriנכשל:- בפורטל התמיכה המהיר, מסמנים את התיבה Simulate transient failure (enqueue in Cloud Tasks and retry notifyUri).
- מזינים את שם הפנייה:
Checkout service outageואת תיאור הבעיה:Production payment gateway is timing out for all customers, ולוחצים על שליחת הפנייה. - שימו לב שהפורטל מגיב באופן מיידי עם:
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. מעולה!
יצרתם, פרסתם ובדקתם תוסף HTTP ב-Node.js ל-Google Workspace Studio באמצעות Firestore, Cloud Tasks, Cloud Run ו-Gemini 3.8 Flash ב-Gemini Enterprise Agent Platform.
מה למדנו
- הגדרת מניפסט של תוסף HTTP ל-Google Workspace Studio (
deployment.json) עם רכיביworkflowTriggerו-workflowAction. - שמירת מינויים ל-Google Workspace Studio
workflowTriggerבאוסףstudio_triggersFirestore ורישום של שליחות בפורטל ב-support_tickets, כדי שהטריגרים ימשיכו לפעול גם אחרי שינוי קנה מידה ופריסה מחדש של Cloud Run. - הבטחת מסירה של
notifyUri(triggers.fire) באמצעות Cloud Tasks (studio-trigger-retries), כך שמתבצע ניסיון חוזר באופן אוטומטי במקרה של כשלים זמניים ב-HTTP, עם השהיה מעריכית לפני ניסיון חוזר (exponential backoff) באמצעות ה-userOAuthTokenהעדכני מ-Firestore. - יצירת שלב
workflowActionשמסווג כרטיסי תמיכה באמצעות Gemini 3.8 Flash (gemini-3.8-flash) עם פלט מובנה ב-Gemini Enterprise Agent Platform, עם חזרה למצב קודם של התאמת מילות מפתח.