יצירת שלבים מותאמים אישית ב-Google Workspace Studio ב-Node.js ב-Cloud Run

1. מבוא

Google Workspace Studio מאפשר לכם להפוך תהליכים עסקיים לאוטומטיים באפליקציות של Google Workspace כמו Gmail,‏ Google Chat,‏ Google Drive ו-Google Sheets. אם אתם צריכים להפעיל תהליכי עבודה מאפליקציית אינטרנט חיצונית או להריץ לוגיקה מותאמת אישית בסביבת הענן שלכם, אתם יכולים להרחיב את Google Workspace Studio באמצעות תוסף ל-Google Workspace מסוג HTTP ב-Node.js ב-Cloud Run:

  1. שלב התחלתי מותאם אישית (workflowTrigger): מתחיל תהליך עבודה ב-Google Workspace Studio כשמתרחש אירוע חיצוני – כמו לקוח ששולח כרטיס תמיכה בפורטל אינטרנט פנימי – על ידי קריאה ל-Google Workspace Studio triggers.fire API בארכיטקטורת REST.
  2. שלב פעולה בהתאמה אישית (workflowAction): מקבל משתנים משלבים קודמים ברצף הפעולות, מפעיל לוגיקה בהתאמה אישית או הסקת מסקנות מ-AI בשירות Node.js ומחזיר משתני פלט ל-Google Workspace Studio.

ב-Codelab הזה נסביר איך ליצור ולפרוס תוסף HTTP מלא של Node.js ל-Google Workspace Studio ב-Cloud Run, איך להשתמש ב-Cloud Firestore כדי לשמור מינויים פעילים להפעלת תהליכים וכרטיסים שנשלחו בגרסאות שונות של קונטיינרים, ואיך לסווג את הדחיפות של כרטיסים בשלב של פעולה מותאמת אישית באמצעות Gemini 3.8 Flash ‏ (gemini-3.8-flash) ב-Gemini Enterprise Agent Platform עם חזרה חסינה למצב קודם של התאמת מילות מפתח.

מה תפַתחו

  • אפליקציית אינטרנט של פורטל תמיכה מהיר ותוסף HTTP ל-Google Workspace שנבנו באמצעות Node.js,‏ Express ו-Firestore ב-Cloud Run.
  • שלב התחלתי מותאם אישית של New Support Ticket (supportTicketTrigger) ששומר מינויים פעילים לרצף פעולות באוסף studio_triggers Firestore, רושם ביומן כרטיסים שנשלחו באוסף support_tickets Firestore ומופעל בכל פעם שנשלח כרטיס בפורטל האינטרנט – מעביר את ticketTitle ו-ticketDescription לרצף פעולות ב-Google Workspace Studio.
  • שלב של פעולה מותאמת אישית Detect Urgency (זיהוי דחיפות) (urgencyDetectorStep) שמסווג את הדחיפות של הכרטיס באמצעות Gemini 3.8 Flash (gemini-3.8-flash) ב-Gemini Enterprise Agent Platform (aiplatform.googleapis.com), עם מעבר אוטומטי לחיפוש התאמה למילות מפתח (urgent, ‏ blocker, ‏ critical).

מה תלמדו

  • איך מגדירים קובץ מניפסט לפריסת תוסף HTTP ל-Google Workspace‏ (deployment.json) עם רכיבי workflowTrigger ו-workflowAction.
  • איך לשמר מינויים להפעלת טריגרים של Google Workspace Studio‏ (triggerId,‏ notifyUri ו-userOAuthToken) בstudio_triggersאוסף Firestore ולהפעיל תהליכי עבודה באמצעות triggers.fire API בארכיטקטורת REST.
  • איך ליצור 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

בשלב הזה, מגדירים את מסך ההסכמה ל-OAuth של הפרויקט, מפעילים את Google Cloud APIs ו-Google Workspace APIs הנדרשים ב-Cloud Shell, מקצים את מסד הנתונים של (default) Firestore ומאתחלים את חשבון השירות של שירות ההרשאות של התוסף.

  1. פותחים את מסוף Google Cloud, בוחרים את פרויקט בענן ולוחצים על הסמל Activate Cloud Shell (הפעלה של Cloud Shell) בסרגל הניווט העליון.
  2. מגדירים את מסך ההסכמה ל-OAuth כדי ש-Google Workspace יוכל להקצות לקוח OAuth לתוסף:
    1. פותחים את דף המיתוג של פלטפורמת האימות של Google.
    2. אם מתבקשים, לוחצים על שנתחיל?. (אם מסך ההסכמה כבר מוגדר, אפשר לדלג לשלב 3).
    3. בקטע App Information, מזינים Support Extension בשדה App name, בוחרים את User support email ולוחצים על Next.
    4. בקטע Audience, לוחצים על Internal ואז על Next.
    5. בקטע פרטים ליצירת קשר, מזינים את כתובת האימייל ולוחצים על הבא.
    6. בקטע Finish, מסכימים למדיניות של Google בנושא נתוני משתמשים בשירותי API, לוחצים על Continue ואז על Create.
  3. ב-Cloud Shell, מגדירים את הפרויקט הפעיל (מחליפים את YOUR_PROJECT_ID במזהה הפרויקט ב-Google Cloud) ומפעילים את ממשקי ה-API הנדרשים:
    gcloud config set project YOUR_PROJECT_ID
    gcloud services enable \
      aiplatform.googleapis.com \
      firestore.googleapis.com \
      run.googleapis.com \
      cloudbuild.googleapis.com \
      artifactregistry.googleapis.com \
      appsmarket-component.googleapis.com \
      gsuiteaddons.googleapis.com \
      workspacestudio.googleapis.com
    
  4. מקצים את מסד הנתונים של (default) Firestore ב-us-central1 (אם הוא עדיין לא קיים) ומפעילים את חשבון השירות של 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 workspace-add-ons get-authorization
    

3. פיתוח אפליקציית Node.js עם Firestore ו-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: הטמעה של ווּבּהוּקים (/onConfigUrgency ו-/onExecuteUrgency) של שלבי פעולה מותאמים אישית של זיהוי דחיפות באמצעות Gemini 3.8 Flash (gemini-3.8-flash) עם חזרה למצב קודם של התאמת מילות מפתח.
  • ‫portal.js: מציג את ממשק המשתמש האינטרנטי של פורטל התמיכה המהיר (GET /) ומפעיל טריגרים פעילים של Google Workspace Studio (POST /api/submit-ticket).
  • ‫index.js: מחבר את כל המסלולים של Express ומתחיל את שרת ה-HTTP.

מא初始化 את הפרויקט ו-package.json

  1. יוצרים ספריית פרויקט ועוברים אליה:
    mkdir -p ~/studio-support-addon && cd ~/studio-support-addon
    
  2. יצירת package.json באמצעות express,‏ @google-cloud/firestore ו-@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/genai": "^1.0.0",
        "express": "^4.21.2"
      }
    }
    EOF
    

‫firestore.js — שיתוף סטטוס של Firestore ורענון טוקן OAuth

כדי לנהל את שתי הקולקציות של Firestore ולשמור על טריות של אסימוני OAuth של משתמשים שנשמרו במטמון, צריך ליצור firestore.js:

  • ‫studio_triggers: מאחסן מינויים פעילים ל-Google Workspace Studio Flow (‏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): מופעל כשמשתמש בוחר באפשרות New Support Ticket (כרטיס תמיכה חדש) כהתחלה של התהליך ב-Google Workspace Studio. הפונקציה מחזירה פעולת ניווט pushCard שמתארת את הסימן לתחילת הפעולה.
  • ‫onManageTrigger (POST /onManageTrigger): מופעלת כשמשתמש מפעיל (req.body.workflow.triggerCreation) רצף פעולות או משבית אותו או מוחק אותו (req.body.workflow.triggerDeletion). שומרת או מוחקת את מסמך המינוי באוסף studio_triggers Firestore.
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 עם פלט מובנה (responseSchema enum ["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

‫portal.js — תמיכה בממשק המשתמש של פורטל התמיכה המהיר וב-triggers.fire dispatcher

ליצור portal.js כדי להפעיל את פורטל האינטרנט ולשלוח בקשות לתמיכה ל-Google Workspace Studio:

  • ‫renderPortal (GET /): השאילתה מחפשת באוסף studio_triggers Firestore כדי להציג כמה טריגרים פעילים של Google Workspace Studio רשומים, ומציגה את טופס שליחת הכרטיס ב-HTML.
  • ‫submitTicket (POST /api/submit-ticket): חוזר על הפעולה בכל מסמכי הטריגר הפעילים ב-studio_triggers, מפעיל את נקודת הקצה של Google Workspace Studio‏ triggers.fire (https://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire) עם משתני הפלט ticketTitle ו-ticketDescription ומזהה ייחודי requestId, ושומר את הכרטיס באוסף support_tickets Firestore.
cat << 'EOF' > portal.js
import { triggersCollection, ticketsCollection } from './firestore.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; }
      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>

    <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 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 }),
          });
          const data = await res.json();
          if (data.ok) {
            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 -> Fire Active Google Workspace Studio Triggers
export async function submitTicket(req, res) {
  const title = req.body?.title || 'Untitled Ticket';
  const description = req.body?.description || '';
  const results = [];

  try {
    const snapshot = await triggersCollection.get();
    for (const doc of snapshot.docs) {
      const trigger = doc.data();
      if (!trigger.triggerId || !trigger.userOAuthToken) continue;

      const response = await fetch(
        `https://workspacestudio.googleapis.com/v1/triggers/${encodeURIComponent(trigger.triggerId)}:fire`,
        {
          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:
              'req_' + Date.now() + '_' + Math.floor(Math.random() * 1000),
          }),
        }
      );
      results.push({ triggerId: trigger.triggerId, status: response.status });
    }

    await ticketsCollection.add({
      title,
      description,
      triggersFired: results.length,
      createdAt: new Date().toISOString(),
    });

    res.status(200).json({ ok: true, triggersFired: results.length, 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 כדי לחבר את נתיבי הפורטל ואת נקודות הקצה של ה-Webhook ב-Google Workspace Studio לשרת HTTP של Express שמקשיב ל-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';

const app = express();
app.use(express.json());

// Support Quick Portal routes
app.get('/', renderPortal);
app.post('/api/submit-ticket', submitTicket);

// 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, וגישה לזמן הריצה של Agent Platform, יוצרים את קובץ המניפסט deployment.json ומתקינים את פריסת התוסף.

  1. פורסים את שירות support-extension ב-Cloud Run עם משתני הסביבה של הפרויקט ואוסף Firestore:
    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" \
      --quiet
    
  2. נותנים את ההרשאה 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
    
  3. מקצים לחשבון השירות של זמן הריצה של Cloud Run את ההרשאות roles/datastore.user (לקריאה וכתיבה של האוספים studio_triggers ו-support_tickets ב-Firestore) ו-roles/aiplatform.user (להתקשרות עם Gemini בפלטפורמת הסוכנים של Gemini Enterprise):
    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/aiplatform.user" \
      --condition=None \
      --quiet
    
  4. מאחזרים את כתובת ה-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
    
  5. יוצרים (או מחליפים) ומתקינים את פריסת התוסף של 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. בדיקת השלבים המותאמים אישית ב-Google Workspace Studio

  1. פותחים את Google Workspace Studio (אם הכרטיסייה בדפדפן כבר פתוחה, מרעננים אותה) ולוחצים על New Flow (תהליך חדש).
  2. לוחצים על בחירת תבנית > תוסף תמיכה > כרטיס תמיכה חדש.
  3. לוחצים על הוספת שלב (+) > Support Extension (תוסף תמיכה) > Detect Urgency (זיהוי דחיפות). בשדה תיאור המקור, לוחצים על + משתנים ובוחרים באפשרות תיאור הכרטיס.
  4. לוחצים על הוספת שלב (+) ומוסיפים שלב של Chat > הודעה ב-Chat שכולל את המשתנים שם הכרטיס, רמת הדחיפות ותיאור הכרטיס בהודעה.
  5. מעבירים את המתג של התהליך למצב On (מופעל) בפינה השמאלית העליונה. ‫Google Workspace Studio קורא באופן מיידי לנקודת הקצה /onManageTrigger ב-Cloud Run, שכותבת את המינוי הפעיל לטריגר לאוסף studio_triggers ב-Firestore.
  6. פותחים את כתובת ה-URL של פורטל התמיכה המהיר (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.
  7. כדי לוודא שהתהליך של רצף הפעולות פעל עבור שני הכרטיסים עם Urgency Level: High ו-Urgency Level: Normal, אפשר לבדוק את הכרטיסייה פעילות ב-Google Workspace Studio וב-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 firestore databases delete --database="(default)" --quiet

7. מעולה!

יצרתם, פרסתם ובדקתם תוסף HTTP של Node.js ל-Google Workspace Studio באמצעות Firestore,‏ Cloud Run ו-Gemini 3.8 Flash ב-Gemini Enterprise Agent Platform.

מה כיסינו

  • הגדרת מניפסט של תוסף HTTP ל-Google Workspace Studio ‏ (deployment.json) עם רכיבי workflowTrigger ו-workflowAction.
  • שמירת מינויים ל-Google Workspace Studio workflowTrigger באוסף studio_triggers Firestore ורישום של שליחות בפורטל ב-support_tickets, כדי שהטריגרים ימשיכו לפעול גם אחרי שינוי קנה מידה ופריסה מחדש של Cloud Run.
  • יצירת שלב workflowAction שמסווג כרטיסי תמיכה באמצעות Gemini 3.8 Flash (gemini-3.8-flash) עם פלט מובנה ב-Gemini Enterprise Agent Platform, עם חזרה למצב קודם של התאמת מילות מפתח.

השלבים הבאים