ساختن مراحل سفارشی Google Workspace Studio در Node.js در Cloud Run

۱. مقدمه

Google Workspace Studio به شما امکان می‌دهد فرایندهای کسب‌وکار را در برنامه‌های Google Workspace مثل Gmail،‏ Google Chat،‏ Google Drive، و «کاربرگ‌نگار Google» خودکارسازی کنید. وقتی گردش‌های کاری شما باید از یک برنامه وب خارجی شروع شود یا منطق سفارشی را در محیط ابری خودتان اجرا کند، می‌توانید Google Workspace Studio را با یک برافزای Google Workspace HTTP در Node.js در Cloud Run گسترش دهید:

  1. مرحله آغازگر سفارشی (workflowTrigger): با فراخوانی notifyUri راه‌انداز (یا Google Workspace Studio triggers.fire REST API)، وقتی رویداد خارجی رخ می‌دهد—مثلاً مشتری در درگاه وب داخلی درخواست پشتیبانی ارسال می‌کند—جریان Google Workspace Studio را شروع می‌کند.
  2. مرحله کنش سفارشی (workflowAction): متغیرها را از مراحل قبلی در جریان دریافت می‌کند، منطق سفارشی یا استنباط هوش مصنوعی را در سرویس 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 با یک جایگزین مقاوم دربرابر تطابق کلیدواژه طبقه‌بندی می‌کنید.

آنچه خواهید ساخت

  • برنامه وب پورتال سریع پشتیبانی و «برافزای Google Workspace»‏ HTTP ساخته‌شده با Node.js،‏ Express،‏ Firestore، و Cloud Tasks در Cloud Run.
  • یک مرحله شروع سفارشی درخواست پشتیبانی جدید (supportTicketTrigger) که اشتراک‌های جریان فعال را در مجموعه studio_triggers Firestore حفظ می‌کند، درخواست‌های ارسال‌شده را در مجموعه support_tickets Firestore ثبت می‌کند، و هر زمان که درخواستی در درگاه وب ارسال شود فعال می‌شود—ticketTitle و ticketDescription را به جریان Google Workspace Studio منتقل می‌کند.
  • کارگر تلاش مجدد Cloud Tasks (صف studio-trigger-retries و POST /api/tasks/fire-trigger) که تماس‌های راه‌انداز ناموفق را بااستفاده از پس‌گیری نمایی با جدیدترین userOAuthToken ذخیره‌شده در Firestore در صف قرار می‌دهد و دوباره امتحان می‌کند و تحویل به notifyUri را تضمین می‌کند.
  • مرحله کنش سفارشی تشخیص فوریت (urgencyDetectorStep) که فوریت بلیت را بااستفاده از Gemini 3.8 Flash (gemini-3.8-flash) در پلاتفرم Gemini Enterprise Agent (aiplatform.googleapis.com) طبقه‌بندی می‌کند و به‌طور خودکار به مطابقت کلیدواژه (urgent، blocker، critical) برمی‌گردد.

معماری

نمودار زیر نحوه تعامل «درگاه سریع پشتیبانی»، برافزای Node.js شما در Cloud Run، Cloud Firestore، Cloud Tasks (studio-trigger-retries)، Gemini 3.8 Flash، و Google Workspace Studio را از ثبت جریان تا ارائه محرک انعطاف‌پذیر و اجرای کنش نشان می‌دهد:

معماری برافزای Node.js در Google Workspace Studio با Firestore،‏ Cloud Tasks، و Gemini 3.8 Flash

آنچه خواهید آموخت

  • نحوه پیکربندی مانیفست استقرار برافزای Google Workspace‏ HTTP (deployment.json) با عناصر workflowTrigger و workflowAction.
  • How to persist Google Workspace Studio trigger subscriptions (triggerId, notifyUri, and userOAuthToken) in a studio_triggers Firestore collection and fire flows using notifyUri / triggers.fire.
  • نحوه استفاده از Cloud Tasks (@google-cloud/tasks) برای امتحان مجدد اعلان‌های راه‌انداز ناموفق با عقب‌نشینی نمایی تا زمانی که notifyUri پاسخ موفقیت 2xx را برگرداند.
  • نحوه ساختن گام سفارشی workflowAction در Node.js بااستفاده از برونداد ساختاری Gemini 3.8 Flash (gemini-3.8-flash) در پلاتفرم Gemini Enterprise Agent با کلیدواژه برگشتی.

پیش‌نیازها

  • حساب Google Workspace با دسترسی به استودیوی Google Workspace.
  • پروژه Google Cloud با صورت‌حساب فعال، که در همان سازمان Google Workspace حساب کاربری شما ایجاد شده است تا بتوانید پیاده‌سازی‌های برافزای منتشرنشده را برای آزمایش نصب کنید.

۲. راه‌اندازی پروژه Google Cloud،‏ Firestore، و Cloud Tasks

در این مرحله، صفحه رضایت OAuth پروژه‌تان را پیکربندی می‌کنید، میاناهای برنامه‌سازی کاربردی Google Cloud و Google Workspace موردنیاز را در Cloud Shell فعال می‌کنید، پایگاه داده (default) Firestore خود را تأمین می‌کنید، صف تکرار مجدد Cloud Tasks خود را ایجاد می‌کنید، و حساب سرویس مجوز برافزایتان را مقداردهی اولیه می‌کنید.

  1. کنسول Google Cloud را باز کنید، پروژه Google Cloud خود را انتخاب کنید، و روی نماد فعال‌سازی Cloud Shell در نوار پیمایش بالا کلیک کنید.
  2. صفحه رضایت OAuth را پیکربندی کنید تا Google Workspace بتواند کارخواه OAuth را برای برافزایتان فراهم کند:
    1. صفحه نمانام‌سازی پلاتفرم Google Auth را باز کنید.
    2. درصورت درخواست، روی شروع به کار کلیک کنید. (اگر صفحه رضایت شما قبلاً پیکربندی شده است، به مرحله ۳ بروید.)
    3. در بخش اطلاعات برنامه، Support Extension را برای نام برنامه وارد کنید، ایمیل پشتیبانی کاربر را انتخاب کنید، و روی بعدی کلیک کنید.
    4. در بخش مخاطب، داخلی را انتخاب کنید و روی بعدی کلیک کنید.
    5. در بخش اطلاعات تماس، نشانی ایمیلتان را وارد کنید و روی بعدی کلیک کنید.
    6. در بخش پایان، با سرویس‌های Google API: خط‌مشی داده‌های کاربر موافقت کنید، روی ادامه کلیک کنید، و روی ایجاد کلیک کنید.
  3. در Cloud Shell، پروژه فعال خود را تنظیم کنید (YOUR_PROJECT_ID را با شناسه پروژه Google Cloud خود جایگزین کنید) و میاناهای برنامه‌سازی کاربردی موردنیاز را فعال کنید:
    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
    
  4. پایگاه داده (default) Firestore و صف studio-trigger-retries Cloud 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
    

۳. ساختن برنامه 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: وب‌قلاب‌های مرحله آغازگر سفارشی بلیت پشتیبانی جدید را پیاده‌سازی می‌کند (/onConfigTrigger و /onManageTrigger).
  • action.js: وب‌قلاب‌های مرحله کنش سفارشی تشخیص فوریت (/onConfigUrgency و /onExecuteUrgency) را بااستفاده از Gemini 3.8 Flash (gemini-3.8-flash) با بازگشت تطابق کلیدواژه پیاده‌سازی می‌کند.
  • tasks.js: اعلان‌های راه‌انداز را به trigger.notifyUri (با برگشت به https://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire) ارسال می‌کند، تحویل‌های ناموفق را در صف studio-trigger-retries Cloud Tasks قرار می‌دهد، و نقطه پایانی کارگر تلاش مجدد POST /api/tasks/fire-trigger را پیاده‌سازی می‌کند.
  • portal.js: رابط کاربری وب پورتال سریع پشتیبانی (GET /) را ارائه می‌دهد و راه‌اندازهای فعال Google Workspace Studio‏ (POST /api/submit-ticket) را فعال می‌کند، با گزینه‌ای برای شبیه‌سازی یک خطای گذرا و تماشای تلاش مجدد Cloud Tasks.
  • ‫index.js: همه مسیرهای Express را به‌هم متصل می‌کند و سرور HTTP را راه‌اندازی می‌کند.

مقداردهی اولیه پروژه و package.json

  1. فهرست راهنمای پروژه را بسازید و به آن پیمایش کنید:
    mkdir -p ~/studio-support-addon && cd ~/studio-support-addon
    
  2. ‫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 و به‌روز نگه داشتن ژتون‌های OAuth کاربر ذخیره‌شده در حافظه نهان، firestore.js را ایجاد کنید:

  • studio_triggers: اشتراک‌های فعال جریان «استودیوی Google Workspace» (triggerId، notifyUri، و userOAuthToken) را ذخیره می‌کند تا درصورت راه‌اندازی مجدد محفظه Cloud Run و رویدادهای مقیاس‌بندی به صفر، حفظ شوند.
  • support_tickets: سابقه هر درخواست پشتیبانی ارسال‌شده ازطریق درگاه را ذخیره می‌کند.
  • refreshTokenInFirestore(req): هروقت «استودیوِ Google Workspace» با هریک از نقاط پایانی وب‌قلاب شما تماس می‌گیرد، authorizationEventObject.userOAuthToken کوتاه‌مدتی را اضافه می‌کند. این کمک‌کننده اسناد راه‌انداز فعال را با جدیدترین کد به‌روز می‌کند تا تماس‌های triggers.fire پس‌زمینه همچنان مجاز باقی بمانند.
cat << 'EOF' > firestore.js
import { Firestore } from '@google-cloud/firestore';

export const db = new Firestore({
  projectId: process.env.GOOGLE_CLOUD_PROJECT || undefined,
  databaseId: '(default)',
  ignoreUndefinedProperties: true,
});

export const triggersCollection = db.collection(
  process.env.FIRESTORE_TRIGGERS_COLLECTION || 'studio_triggers'
);

export const ticketsCollection = db.collection(
  process.env.FIRESTORE_TICKETS_COLLECTION || 'support_tickets'
);

// Keep cached userOAuthToken fresh across active trigger documents
export async function refreshTokenInFirestore(req) {
  const freshToken = req.body?.authorizationEventObject?.userOAuthToken;
  if (!freshToken) return;
  try {
    const snapshot = await triggersCollection.get();
    if (snapshot.empty) return;
    const batch = db.batch();
    snapshot.docs.forEach((doc) => {
      batch.set(
        doc.ref,
        { userOAuthToken: freshToken, updatedAt: new Date().toISOString() },
        { merge: true }
      );
    });
    await batch.commit();
  } catch (err) {
    console.error('Error refreshing OAuth token in Firestore:', err.message);
  }
}
EOF

‫starter.js — مرحله شروع سفارشی (workflowTrigger)

starter.js را برای پیاده‌سازی دو نقطه پایانی وب‌قلاب موردنیاز برای مرحله شروع سفارشی Google Workspace Studio ایجاد کنید:

  • onConfigTrigger (POST /onConfigTrigger): زمانی فراخوانده می‌شود که کاربر درخواست پشتیبانی جدید را به‌عنوان شروع‌کننده جاری‌سازی خود در «استودیو Google Workspace» انتخاب کند. یک کنش پیمایش 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 را برای پیاده‌سازی دو نقطه پایانی وب‌قلاب موردنیاز برای مرحله کنش سفارشی 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 با برونداد ساختاریافته (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

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 را با متغیرهای برونداد ticketTitle و ticketDescription و requestId خودتوان به نشانی وب راه‌انداز ارسال می‌کند.
  • ‫enqueueTriggerRetry(req, payload): هروقت تماس راه‌انداز اولیه ناموفق باشد یا وضعیت HTTP غیر2xx برگرداند، وظیفه HTTP را در صف studio-trigger-retries که نقطه پایانی POST /api/tasks/fire-trigger سرویس Cloud Run شما را هدف‌یابی می‌کند در صف قرار می‌دهد.
  • 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 می‌گوید که دوباره براساس خط‌مشی پس‌گرد صف شما تلاش کند.
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 — پشتیبانی از میانای کاربری Quick Portal و توزیع‌کننده notifyUri با بازگشت به Cloud Tasks

portal.js را برای ارائه پورتال وب و ارسال درخواست‌های تیکت به Google Workspace Studio با زمان‌بندی خودکار تکرار Cloud Tasks ایجاد کنید:

  • renderPortal (GET /): مجموعه studio_triggers Firestore را پُرسمان می‌کند تا نشان دهد چند راه‌انداز فعال Google Workspace Studio ثبت شده است، و فرم ارسال تیکت HTML را ارائه می‌دهد (ازجمله چارگوش انتخاب شبیه‌سازی خطای گذرا برای آزمایش کردن تلاش‌های مجدد Cloud Tasks).
  • ‫submitTicket (POST /api/submit-ticket): در همه سندهای محرک فعال در studio_triggers تکرار می‌کند و fireTriggerDirect() را فرا می‌خواند. اگر نشانی وب راه‌انداز پاسخ غیر 2xx برگرداند، خطای شبکه ایجاد کند، یا simulateFailure فعال باشد، به‌طور خودکار تکلیف بازآزمایی را بااستفاده از enqueueTriggerRetry() در studio-trigger-retries در صف قرار می‌دهد و بلیت را در مجموعه Firestore support_tickets ثبت می‌کند.
cat << 'EOF' > portal.js
import { triggersCollection, ticketsCollection } from './firestore.js';
import { fireTriggerDirect, enqueueTriggerRetry } from './tasks.js';

// Support Quick Portal UI
export async function renderPortal(req, res) {
  let activeCount = 0;
  try {
    const snapshot = await triggersCollection.get();
    activeCount = snapshot.size;
  } catch (err) {
    console.error('Error reading studio_triggers collection:', err.message);
  }

  res.status(200).send(`<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Support Quick Portal</title>
    <style>
      body { font-family: Arial, sans-serif; padding: 24px; max-width: 540px; margin: auto; color: #202124; }
      .badge { background: #e8f0fe; color: #1967d2; padding: 8px 12px; border-radius: 6px; font-size: 14px; margin-bottom: 16px; }
      label { font-weight: bold; display: block; margin-top: 16px; }
      .checkbox-label { font-weight: normal; margin-top: 14px; display: flex; align-items: center; gap: 8px; cursor: pointer; }
      .checkbox-label input { width: auto; margin: 0; }
      input, textarea { width: 100%; margin-top: 6px; padding: 10px; box-sizing: border-box; border: 1px solid #dadce0; border-radius: 4px; }
      button { background-color: #1a73e8; color: white; border: none; padding: 10px 18px; margin-top: 16px; font-weight: bold; cursor: pointer; border-radius: 4px; }
      button:hover { background-color: #1557b0; }
      #status { margin-top: 16px; font-weight: bold; }
    </style>
  </head>
  <body>
    <h2>Support Quick Portal</h2>
    <div class="badge">
      Active Google Workspace Studio triggers in Firestore (<code>studio_triggers</code>):
      <strong id="trigger-count">${activeCount}</strong>
    </div>
    <p>Submit a support ticket below to fire your Google Workspace Studio flow.</p>

    <label for="title">Ticket Title</label>
    <input type="text" id="title" placeholder="e.g. Database failure">

    <label for="description">Issue Description</label>
    <textarea id="description" rows="4" placeholder="Describe the issue..."></textarea>

    <label class="checkbox-label" for="simulateFailure">
      <input type="checkbox" id="simulateFailure">
      Simulate transient failure (enqueue in Cloud Tasks and retry notifyUri)
    </label>

    <button onclick="submitTicket()">Submit Ticket</button>
    <div id="status"></div>

    <script>
      async function submitTicket() {
        const title = document.getElementById('title').value.trim();
        const description = document.getElementById('description').value.trim();
        const simulateFailure = document.getElementById('simulateFailure').checked;
        const statusEl = document.getElementById('status');

        if (!title || !description) {
          statusEl.style.color = '#d93025';
          statusEl.textContent = 'Please fill out both fields.';
          return;
        }

        statusEl.style.color = '#188038';
        statusEl.textContent = 'Submitting ticket...';

        try {
          const res = await fetch('/api/submit-ticket', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ title, description, simulateFailure }),
          });
          const data = await res.json();
          if (data.ok) {
            if (data.tasksQueued > 0) {
              statusEl.textContent =
                'Ticket saved! Initial notifyUri call did not succeed; queued ' +
                data.tasksQueued +
                ' retry task(s) in Cloud Tasks.';
            } else {
              statusEl.textContent =
                'Ticket saved to Firestore! Fired ' +
                data.triggersFired +
                ' Google Workspace Studio trigger(s).';
            }
          } else {
            statusEl.style.color = '#d93025';
            statusEl.textContent = 'Error: ' + (data.error || 'Unknown error');
          }
        } catch (err) {
          statusEl.style.color = '#d93025';
          statusEl.textContent = 'Request failed: ' + err.message;
        }
      }
    </script>
  </body>
</html>`);
}

// Portal Form Submission -> Call notifyUri directly, or enqueue Cloud Task on failure
export async function submitTicket(req, res) {
  const title = req.body?.title || 'Untitled Ticket';
  const description = req.body?.description || '';
  const simulateFailure = Boolean(req.body?.simulateFailure);
  const results = [];
  let triggersFired = 0;
  let tasksQueued = 0;

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

      const requestId =
        'req_' + Date.now() + '_' + Math.floor(Math.random() * 1000);

      let outcome = { ok: false, status: 503 };
      if (!simulateFailure) {
        try {
          outcome = await fireTriggerDirect(trigger, {
            title,
            description,
            requestId,
          });
        } catch (fetchErr) {
          console.error(
            `Direct trigger call failed for ${trigger.triggerId}:`,
            fetchErr.message
          );
          outcome = { ok: false, status: 0, error: fetchErr.message };
        }
      }

      if (outcome.ok) {
        triggersFired++;
        results.push({
          triggerId: trigger.triggerId,
          status: outcome.status,
          queuedRetry: false,
        });
      } else {
        const taskName = await enqueueTriggerRetry(req, {
          triggerId: trigger.triggerId,
          title,
          description,
          requestId,
          simulateFailure,
        });
        tasksQueued++;
        results.push({
          triggerId: trigger.triggerId,
          status: outcome.status,
          queuedRetry: true,
          taskName,
        });
      }
    }

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

    res.status(200).json({
      ok: true,
      triggersFired,
      tasksQueued,
      results,
    });
  } catch (err) {
    console.error('Error in /api/submit-ticket:', err.message);
    res.status(500).json({ ok: false, error: err.message });
  }
}
EOF

‫index.js — نقطه ورودی سرور Express

index.js را ایجاد کنید تا مسیرهای درگاه، نقطه پایانی کارگر تلاش مجدد Cloud Tasks، و نقاط پایانی وب‌قلاب Google Workspace Studio را باهم در سرور 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

۴. مستقر کردن در Cloud Run و نصب برافزا

در این مرحله، سرویس Node.js را در Cloud Run مستقر می‌کنید، اجازه‌های IAM را برای فراخواننده «برافزاهای Google Workspace» و Firestore،‏ Cloud Tasks، و دسترسی زمان اجرای Agent Platform اعطا می‌کنید، مانیفست deployment.json را تولید می‌کنید، و استقرار برافزا را نصب می‌کنید.

  1. سرویس support-extension را با متغیرهای محیطی پروژه، مجموعه Firestore، و صف Cloud Tasks در Cloud Run مستقر کنید:
    gcloud run deploy support-extension \
      --source . \
      --region us-central1 \
      --allow-unauthenticated \
      --set-env-vars="GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project),FIRESTORE_TRIGGERS_COLLECTION=studio_triggers,FIRESTORE_TICKETS_COLLECTION=support_tickets,CLOUD_TASKS_QUEUE=studio-trigger-retries,CLOUD_TASKS_LOCATION=us-central1" \
      --quiet
    
  2. به حساب خدمات «برافزاهای Google Workspace» در سرویس Cloud Run roles/run.invoker اعطا کنید تا «استودیو Google Workspace» بتواند نقطه‌های پایانی وب‌قلاب شما را فراخوانی کند:
    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. roles/datastore.user (برای خواندن و نوشتن مجموعه‌های Firestore studio_triggers و support_tickets)، roles/cloudtasks.enqueuer (برای صف‌بندی کردن تکالیف تکرار در studio-trigger-retries)، و roles/aiplatform.user (برای فراخوانی Gemini در Gemini Enterprise Agent Platform) را به حساب سرویس زمان اجرای 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
    
  4. نشانی وب 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 را برای حسابتان ایجاد (یا جایگزین) و نصب کنید و نشانی وب «درگاه سریع پشتیبانی» را چاپ کنید:
    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}"
    

۵. مراحل سفارشی و تلاش‌های مجدد «کارهای ابری» را آزمایش کنید

  1. Google Workspace Studio را باز کنید (اگر قبلاً باز بوده است، برگه مرورگر را بازآوری کنید) و روی جریان جدید کلیک کنید.
  2. روی انتخاب شروع‌کننده > افزونه پشتیبانی > بلیت پشتیبانی جدید کلیک کنید.
  3. روی افزودن مرحله (+) > افزونه پشتیبانی > تشخیص فوریت کلیک کنید. در فیلد شرح منبع، روی + متغیرها کلیک کنید و شرح درخواست پشتیبانی را انتخاب کنید.
  4. روی افزودن مرحله (+) کلیک کنید و مرحله گپ > در Chat به من اطلاع بده را اضافه کنید که شامل متغیرهای عنوان درخواست پشتیبانی، سطح فوریت، و شرح درخواست پشتیبانی در پیام باشد.
  5. جریان را در گوشه بالا سمت راست روشن کنید. «استودیوی Google Workspace» بلافاصله نقطه پایانی /onManageTrigger شما را در Cloud Run فراخوانی می‌کند، که اشتراک راه‌انداز فعال (ازجمله triggerId، notifyUri، و userOAuthToken) را در مجموعه studio_triggers در Firestore می‌نویسد.
  6. نشانی وب «درگاه سریع پشتیبانی» (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. در Google Chat و در برگه فعالیت در Google Workspace Studio تأیید کنید که جریان شما برای هر دو بلیت با Urgency Level: High و Urgency Level: Normal اجرا شده است.
  8. آزمایش تلاش مجدد Cloud Tasks برای ارسال وقتی notifyUri ناموفق است:
    1. در درگاه سریع پشتیبانی، شبیه‌سازی خطای گذرا (صف‌بندی در Cloud Tasks و تلاش مجدد برای notifyUri) را بررسی کنید.
    2. عنوان درخواست پشتیبانی: Checkout service outage و شرح مشکل: Production payment gateway is timing out for all customers را وارد کنید، سپس روی ارسال درخواست پشتیبانی کلیک کنید.
    3. توجه داشته باشید که درگاه بلافاصله با این پیام پاسخ می‌دهد: Ticket saved! Initial notifyUri call did not succeed; queued 1 retry task(s) in Cloud Tasks.
    4. در پس‌زمینه، Cloud Tasks‏ POST /api/tasks/fire-trigger را اجرا می‌کند. اولین تلاش برای انجام تکلیف (retryCount=0) یک شکست 503 را شبیه‌سازی می‌کند، که باعث می‌شود Cloud Tasks برای 10s (--min-backoff=10s) عقب‌نشینی کند و دوباره امتحان کند (retryCount=1)، که notifyUri را فراخوانی می‌کند و موفق می‌شود. پس‌از حدود ۱۰ تا ۱۵ ثانیه، تأیید کنید که اعلان Google Chat شما دریافت می‌شود!

‫۶. مرتب‌سازی

برای جلوگیری از تحمیل هزینه‌های جاری به حساب 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

‫۷. تبریک!

بااستفاده از Firestore،‏ Cloud Tasks،‏ Cloud Run، و Gemini 3.8 Flash در پلاتفرم کارگزار Gemini Enterprise، افزونه HTTP‏ Node.js برای Google Workspace Studio ساختید، مستقر کردید، و آزمایش کردید!

آنچه پوشش داده‌ایم

  • پیکربندی مانیفست برافزای HTTP در Google Workspace Studio (deployment.json) با عناصر workflowTrigger و workflowAction.
  • اشتراک‌های Google Workspace Studio workflowTrigger در مجموعه studio_triggers Firestore ماندگار می‌شوند و ارسال‌های درگاه در support_tickets ثبت می‌شوند تا محرک‌ها از مقیاس‌بندی و استقرار مجدد Cloud Run جان سالم به‌در ببرند.
  • ارسال notifyUri (triggers.fire) با Cloud Tasks (studio-trigger-retries) تضمین می‌شود، بنابراین خطاهای اچ‌تی‌تی‌پی موقت به‌طور خودکار با «پس‌گیری نمایی» بااستفاده از جدیدترین userOAuthToken از Firestore مجدداً امتحان می‌شوند.
  • درحال ساختن مرحله workflowAction که پیام‌های پشتیبانی را با Gemini 3.8 Flash (gemini-3.8-flash) برونداد ساختاریافته در پلاتفرم Gemini Enterprise Agent طبقه‌بندی می‌کند، با یک برگشت به تطبیق کلیدواژه انعطاف‌پذیر.

مراحل بعدی