สร้างขั้นตอน Google Workspace Studio ที่กำหนดเองใน Node.js บน Cloud Run

1. บทนำ

Google Workspace Studio ช่วยให้คุณปรับกระบวนการทางธุรกิจให้เป็นระบบอัตโนมัติในแอปพลิเคชัน Google Workspace เช่น Gmail, Google Chat, Google ไดรฟ์ และ Google ชีต เมื่อเวิร์กโฟลว์ต้องเริ่มต้นจากเว็บแอปพลิเคชันภายนอกหรือเรียกใช้ตรรกะที่กำหนดเองในสภาพแวดล้อมระบบคลาวด์ของคุณเอง คุณสามารถขยาย Google Workspace Studio ด้วย ส่วนเสริมของ Google Workspace HTTP ใน Node.js บน Cloud Run ได้ดังนี้

  1. ขั้นตอนเริ่มต้นที่กำหนดเอง (workflowTrigger): เริ่มโฟลว์ Google Workspace Studio เมื่อเกิดเหตุการณ์ภายนอก เช่น ลูกค้าส่งตั๋วในเว็บพอร์ทัลภายใน โดยการเรียกใช้ notifyUri ของทริกเกอร์ (หรือ REST API triggers.fire ของ Google Workspace Studio)
  2. ขั้นตอนการดำเนินการที่กำหนดเอง (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) บน แพลตฟอร์ม Agent ของ Gemini Enterprise พร้อมการสำรองข้อมูลการจับคู่คีย์เวิร์ดที่ยืดหยุ่น

สิ่งที่คุณจะได้สร้าง

  • เว็บแอป พอร์ทัลการสนับสนุนด่วน และส่วนเสริมของ Google Workspace HTTP ที่สร้างด้วย Node.js, Express, Firestore และ Cloud Tasks บน Cloud Run
  • ขั้นตอนเริ่มต้นที่กำหนดเอง คำขอแจ้งปัญหาใหม่ (supportTicketTrigger) ที่เก็บข้อมูลการสมัครใช้บริการโฟลว์ที่ใช้งานอยู่ในคอลเล็กชัน studio_triggers ของ Firestore บันทึกคำขอแจ้งปัญหาที่ส่งมาในคอลเล็กชัน support_tickets ของ Firestore และเริ่มทำงานเมื่อมีการส่งคำขอแจ้งปัญหาในเว็บพอร์ทัล โดยส่ง ticketTitle และ ticketDescription ไปยังโฟลว์ Google Workspace Studio
  • Worker ลองใหม่ของ Cloud Tasks (studio-trigger-retries คิวและ POST /api/tasks/fire-trigger) ที่จัดคิวและลองเรียกใช้ทริกเกอร์ที่ล้มเหลวอีกครั้งด้วย Exponential Backoff โดยใช้ userOAuthToken ล่าสุดที่จัดเก็บไว้ใน Firestore ซึ่งรับประกันการส่งไปยัง notifyUri
  • ขั้นตอนการดำเนินการที่กำหนดเอง ตรวจหาความเร่งด่วน (urgencyDetectorStep) ที่จัดประเภทความเร่งด่วนของตั๋วโดยใช้ Gemini 3.8 Flash (gemini-3.8-flash) บน แพลตฟอร์ม Agent ของ Gemini Enterprise (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

สิ่งที่คุณจะได้เรียนรู้

  • วิธีกำหนดค่าไฟล์ Manifest การติดตั้งใช้งานส่วนเสริมของ Google Workspace HTTP (deployment.json) ด้วยองค์ประกอบ workflowTrigger และ workflowAction
  • วิธีเก็บข้อมูลการสมัครใช้บริการทริกเกอร์ Google Workspace Studio (triggerId, notifyUri และ userOAuthToken) ไว้ในคอลเล็กชัน studio_triggers ของ Firestore และเริ่มโฟลว์โดยใช้ notifyUri / triggers.fire
  • วิธีใช้ Cloud Tasks (@google-cloud/tasks) เพื่อลองส่งการแจ้งเตือนทริกเกอร์ที่ล้มเหลวอีกครั้งด้วย Exponential Backoff จนกว่า notifyUri จะแสดงผลการตอบกลับที่สำเร็จ 2xx
  • วิธีสร้างขั้นตอนที่กำหนดเอง workflowAction ใน Node.js โดยใช้เอาต์พุตที่มีโครงสร้างของ Gemini 3.8 Flash (gemini-3.8-flash) บนแพลตฟอร์ม Agent ของ Gemini Enterprise พร้อมการสำรองข้อมูลคีย์เวิร์ด

ข้อกำหนดเบื้องต้น

  • บัญชี Google Workspace ที่มีสิทธิ์เข้าถึง Google Workspace Studio
  • โปรเจ็กต์ Google Cloud ที่เปิดใช้การเรียกเก็บเงินแล้ว ซึ่งสร้างขึ้นภายในองค์กร Google Workspace เดียวกับบัญชีผู้ใช้เพื่อให้คุณติดตั้งใช้งานส่วนเสริมที่ยังไม่ได้เผยแพร่เพื่อทำการทดสอบได้

2. ตั้งค่าโปรเจ็กต์ที่อยู่ในระบบคลาวด์ของ Google, Firestore และ Cloud Tasks

ในขั้นตอนนี้ คุณจะกำหนดค่าหน้าจอขอความยินยอม OAuth ของโปรเจ็กต์ เปิดใช้ Google Cloud และ Google Workspace API ที่จำเป็นใน Cloud Shell จัดเตรียมฐานข้อมูล (default) Firestore สร้างคิวลองใหม่ของ Cloud Tasks และเริ่มต้นบัญชีบริการการให้สิทธิ์ส่วนเสริม

  1. เปิดคอนโซล Google Cloud เลือกโปรเจ็กต์ที่อยู่ในระบบคลาวด์ Google Cloud แล้วคลิกไอคอนเปิดใช้งาน Cloud Shell ในแถบการนำทางด้านบน
  2. กำหนดค่าหน้าจอขอความยินยอม OAuth เพื่อให้ Google Workspace จัดเตรียมไคลเอ็นต์ OAuth สำหรับส่วนเสริมของคุณได้ โดยทำดังนี้
    1. เปิดหน้าการสร้างแบรนด์แพลตฟอร์มการตรวจสอบสิทธิ์ของ Google
    2. หากได้รับข้อความแจ้ง ให้คลิกเริ่มต้นใช้งาน (หากกำหนดค่าหน้าจอขอความยินยอมไว้แล้ว ให้ข้ามไปยังขั้นตอนที่ 3)
    3. ในส่วนข้อมูลแอป ให้ป้อน Support Extension สำหรับชื่อแอป เลือกอีเมลสนับสนุนสำหรับผู้ใช้ แล้วคลิกถัดไป
    4. ในส่วนกลุ่มเป้าหมาย ให้เลือกภายใน แล้วคลิกถัดไป
    5. ในส่วนข้อมูลติดต่อ ให้ป้อนอีเมลแล้วคลิกถัดไป
    6. ในส่วนเสร็จสิ้น ให้ยอมรับบริการ Google API: นโยบายข้อมูลผู้ใช้ คลิกต่อไป แล้วคลิกสร้าง
  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 \
      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
    

3. สร้างแอป Node.js ด้วย Firestore, Cloud Tasks และ Gemini 3.8 Flash

ในขั้นตอนนี้ คุณจะสร้างแอปพลิเคชัน Node.js ใน ~/studio-support-addon เราจะจัดระเบียบโปรเจ็กต์เป็น 6 ไฟล์ที่มุ่งเน้นเพื่อให้โค้ดเป็นแบบโมดูลาร์และอ่านง่าย ดังนี้

  • 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 และใช้ปลายทาง Worker ลองใหม่ POST /api/tasks/fire-trigger
  • portal.js: แสดง UI ของเว็บ พอร์ทัลการสนับสนุนด่วน (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.js เพื่อจัดการคอลเล็กชัน Firestore 2 รายการและเก็บโทเค็น OAuth ของผู้ใช้ที่แคชไว้ให้เป็นปัจจุบันอยู่เสมอ โดยทำดังนี้

  • studio_triggers: จัดเก็บข้อมูลการสมัครใช้บริการโฟลว์ Google Workspace Studio ที่ใช้งานอยู่ (triggerId, notifyUri และ userOAuthToken) เพื่อให้ข้อมูลยังคงอยู่ได้แม้จะมีการรีสตาร์ทคอนเทนเนอร์ Cloud Run และเหตุการณ์การลดขนาดเหลือศูนย์
  • support_tickets: จัดเก็บบันทึกของคำขอแจ้งปัญหาแต่ละรายการที่ส่งผ่านพอร์ทัล
  • refreshTokenInFirestore(req): เมื่อใดก็ตามที่ Google Workspace Studio เรียกปลายทางเว็บฮุกใดก็ตามของคุณ ระบบจะรวม authorizationEventObject.userOAuthToken ที่มีอายุสั้นไว้ด้วย ฟังก์ชัน Helper นี้จะอัปเดตเอกสารทริกเกอร์ที่ใช้งานอยู่ด้วยโทเค็นล่าสุดเพื่อให้การเรียกใช้ 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 เพื่อใช้ปลายทางเว็บฮุก 2 รายการที่ขั้นตอนเริ่มต้นที่กำหนดเองของ Google Workspace Studio กำหนดไว้ โดยทำดังนี้

  • onConfigTrigger (POST /onConfigTrigger): เรียกใช้เมื่อผู้ใช้เลือกคำขอแจ้งปัญหาใหม่ เป็นตัวเริ่มต้นของโฟลว์ใน 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 เพื่อใช้ปลายทางเว็บฮุค 2 รายการที่ขั้นตอนการกระทำที่กำหนดเองของ 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) บน แพลตฟอร์ม Agent ของ Gemini Enterprise พร้อม เอาต์พุตที่มีโครงสร้าง (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 \- คิวลองใหม่และปลายทาง Worker ของ 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 ที่ idempotent
  • enqueueTriggerRetry(req, payload): จัดคิวงาน HTTP ในคิว studio-trigger-retries ที่กำหนดเป้าหมายเป็นปลายทาง 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 Tasks ครั้งแรกไม่สำเร็จด้วย 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 \- UI ของพอร์ทัลการสนับสนุนด่วนและตัวส่ง notifyUri พร้อมการสำรองข้อมูล Cloud Tasks

สร้าง portal.js เพื่อแสดงพอร์ทัลเว็บและส่งการส่งตั๋วไปยัง Google Workspace Studio พร้อมการกำหนดเวลาลองใหม่ของ Cloud Tasks โดยอัตโนมัติ โดยทำดังนี้

  • renderPortal (GET /): ค้นหาคอลเล็กชัน studio_triggers ของ Firestore เพื่อแสดงจำนวนทริกเกอร์ Google Workspace Studio ที่ใช้งานอยู่ที่ลงทะเบียนไว้ และแสดงแบบฟอร์มการส่งตั๋ว HTML (รวมถึงช่องทำเครื่องหมายจำลองความล้มเหลวชั่วคราว (จัดคิวใน Cloud Tasks และลอง notifyUri อีกครั้ง) เพื่อทดสอบการลองใหม่ของ 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 และปลายทางเว็บฮุกของ 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';
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 สร้างไฟล์ Manifest deployment.json และติดตั้งใช้งานส่วนเสริม

  1. ติดตั้งใช้งานบริการ 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
    
  2. ให้บทบาท roles/run.invoker ในบริการ Cloud Run แก่บัญชีบริการส่วนเสริมของ Google Workspace เพื่อให้ Google Workspace Studio เรียกปลายทางเว็บฮุกของคุณได้ โดยทำดังนี้
    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 (สำหรับการอ่านและเขียนคอลเล็กชัน studio_triggers และ support_tickets ของ Firestore), roles/cloudtasks.enqueuer (สำหรับการจัดคิวงานลองใหม่ลงใน studio-trigger-retries) และ roles/aiplatform.user (สำหรับการเรียกใช้ Gemini บนแพลตฟอร์ม Agent ของ 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
    
  4. เรียก URL ของ Cloud Run ที่ติดตั้งใช้งานแล้วและสร้างไฟล์ Manifest 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 ของพอร์ทัลการสนับสนุนด่วน โดยทำดังนี้
    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

  1. เปิด Google Workspace Studio (รีเฟรชแท็บเบราว์เซอร์หากเปิดอยู่แล้ว) แล้วคลิก โฟลว์ใหม่
  2. คลิกเลือกตัวเริ่มต้น > ส่วนขยายการสนับสนุน > คำขอแจ้งปัญหาใหม่
  3. คลิกเพิ่มขั้นตอน (+) > ส่วนขยายการสนับสนุน > ตรวจหาความเร่งด่วน ในช่องคำอธิบายแหล่งที่มา ให้คลิก + ตัวแปร แล้วเลือกคำอธิบายตั๋ว
  4. คลิกเพิ่มขั้นตอน (+) แล้วเพิ่มขั้นตอนChat > แจ้งเตือนฉันใน Chat ที่มีตัวแปรชื่อตั๋ว ระดับความเร่งด่วน และคำอธิบายตั๋ว ในข้อความ
  5. สลับโฟลว์เป็นเปิด ที่มุมขวาบน Google Workspace Studio จะเรียกปลายทาง /onManageTrigger ของคุณใน Cloud Run ทันที ซึ่งจะเขียนข้อมูลการสมัครใช้บริการทริกเกอร์ที่ใช้งานอยู่ (รวมถึง triggerId, notifyUri และ userOAuthToken) ลงในคอลเล็กชัน studio_triggers ใน Firestore
  6. เปิด URL ของพอร์ทัลการสนับสนุนด่วน (https://support-extension-...run.app) ในแท็บใหม่ ยืนยันว่าพอร์ทัลแสดงทริกเกอร์ที่ใช้งานอยู่ 1 รายการใน Firestore และส่งตั๋ว 2 รายการเพื่อทดสอบการจัดประเภทของ 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 ว่าโฟลว์ทำงานสำหรับตั๋วทั้ง 2 รายการที่มี 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 และสำเร็จ หลังจากผ่านไปประมาณ 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 บน แพลตฟอร์ม Agent ของ Gemini Enterprise แล้ว

สิ่งที่เราได้กล่าวถึง

  • การกำหนดค่าไฟล์ Manifest ส่วนเสริม HTTP ของ Google Workspace Studio (deployment.json) ด้วยองค์ประกอบ workflowTrigger และ workflowAction
  • การเก็บข้อมูลการสมัครใช้บริการ workflowTrigger ของ Google Workspace Studio ไว้ในคอลเล็กชัน studio_triggers ของ Firestore และบันทึกการส่งพอร์ทัลไว้ใน 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) บน แพลตฟอร์ม Agent ของ Gemini Enterprise พร้อมการสำรองข้อมูลการทำงานของคีย์เวิร์ดที่ยืดหยุ่น

ขั้นตอนถัดไป