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

1. บทนำ

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

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

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

  • เว็บแอป Support Quick Portal และส่วนเสริม HTTP ของ Google Workspace ที่สร้างด้วย Node.js, Express และ Firestore ใน Cloud Run
  • ขั้นตอนเริ่มต้นที่กำหนดเองของคำขอแจ้งปัญหาใหม่ (supportTicketTrigger) ที่คงการสมัครใช้บริการโฟลว์ที่ใช้งานอยู่ไว้ในคอลเล็กชัน studio_triggers Firestore บันทึกคำขอแจ้งปัญหาที่ส่งแล้วในคอลเล็กชัน support_tickets Firestore และทริกเกอร์ทุกครั้งที่มีการส่งคำขอแจ้งปัญหาในเว็บพอร์ทัล โดยส่ง ticketTitle และ ticketDescription ไปยังโฟลว์ Google Workspace Studio
  • ขั้นตอนการกระทำที่กำหนดเอง ตรวจหาความเร่งด่วน (urgencyDetectorStep) ที่จัดประเภทความเร่งด่วนของคำขอแจ้งปัญหาโดยใช้ Gemini 3.8 Flash (gemini-3.8-flash) ในแพลตฟอร์ม Agent ของ Gemini Enterprise (aiplatform.googleapis.com) โดยจะเปลี่ยนไปใช้การทำงานของคีย์เวิร์ดโดยอัตโนมัติ (urgent, blocker, critical)

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

  • วิธีกำหนดค่าไฟล์ Manifest การติดตั้งใช้งานส่วนเสริม HTTP ของ Google Workspace (deployment.json) ด้วยองค์ประกอบ workflowTrigger และ workflowAction
  • วิธีบันทึกการสมัครรับข้อมูลทริกเกอร์ Google Workspace Studio (triggerId, notifyUri และ userOAuthToken) ในstudio_triggersคอลเล็กชัน Firestore และเรียกใช้โฟลว์โดยใช้ triggers.fire REST API
  • วิธีสร้าง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

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

  1. เปิด Google Cloud Console เลือกโปรเจ็กต์ 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 \
      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 ส่วนเสริม โดยทำดังนี้
    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 คุณจัดระเบียบโปรเจ็กต์เป็น 5 ไฟล์ที่มุ่งเน้นเพื่อให้โค้ดเป็นโมดูลและอ่านได้

  • firestore.js: เริ่มต้นไคลเอ็นต์ Cloud Firestore (คอลเล็กชัน studio_triggers และ support_tickets) และรีเฟรชโทเค็น OAuth ที่แคชไว้
  • starter.js: ใช้เว็บฮุกขั้นตอนเริ่มต้นที่กำหนดเองของคำขอแจ้งปัญหาใหม่ (/onConfigTrigger และ /onManageTrigger)
  • action.js: ใช้เว็บฮุกของขั้นตอนการกระทำที่กำหนดเอง Detect Urgency (/onConfigUrgency และ /onExecuteUrgency) โดยใช้ Gemini 3.8 Flash (gemini-3.8-flash) พร้อมการสำรองข้อมูลการทำงานของคีย์เวิร์ด
  • portal.js: แสดง UI เว็บของพอร์ทัลด่วนสำหรับการสนับสนุน (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.js เพื่อจัดการคอลเล็กชัน Firestore 2 รายการและรักษาโทเค็น OAuth ของผู้ใช้ที่แคชไว้ให้เป็นข้อมูลล่าสุด

  • studio_triggers: จัดเก็บการสมัครใช้บริการโฟลว์ Google Workspace Studio ที่ใช้งานอยู่ (triggerId, notifyUri และ userOAuthToken) เพื่อให้การสมัครใช้บริการยังคงอยู่เมื่อมีการรีสตาร์ทคอนเทนเนอร์ Cloud Run และเหตุการณ์การลดขนาดเหลือศูนย์
  • support_tickets: จัดเก็บบันทึกของคำขอแจ้งปัญหาแต่ละรายการที่ส่งผ่านพอร์ทัล
  • refreshTokenInFirestore(req): เมื่อใดก็ตามที่ Google Workspace Studio เรียกใช้ปลายทางของ Webhook ระบบจะรวม authorizationEventObject.userOAuthToken ที่มีอายุสั้น ตัวช่วยนี้จะอัปเดตเอกสารทริกเกอร์ที่ใช้งานอยู่ด้วยโทเค็นล่าสุดเพื่อให้การเรียกใช้ triggers.fire ในเบื้องหลังยังคงได้รับอนุญาต
cat << 'EOF' > firestore.js
import { Firestore } from '@google-cloud/firestore';

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

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

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

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

starter.js — ขั้นตอนเริ่มต้นที่กำหนดเอง (workflowTrigger)

สร้าง starter.js เพื่อใช้ปลายทางของ Webhook 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) ในแพลตฟอร์มเอเจนต์ 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

portal.js - รองรับ UI ของ Quick Portal และโปรแกรมส่งต่อ triggers.fire

สร้าง 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, สร้างไฟล์ Manifest 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 ส่วนเสริม เพื่อให้ 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. ให้สิทธิ์ roles/datastore.user (สำหรับการอ่านและเขียนคอลเล็กชัน studio_triggers และ support_tickets ใน Firestore) และ 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/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. ทดสอบขั้นตอนที่กำหนดเองใน Google Workspace Studio

  1. เปิด Google Workspace Studio (รีเฟรชแท็บเบราว์เซอร์หากเปิดอยู่แล้ว) แล้วคลิกโฟลว์ใหม่
  2. คลิกเลือกแพ็กเกจเริ่มต้น > ส่วนขยายการสนับสนุน > คำขอแจ้งปัญหาใหม่
  3. คลิกเพิ่มขั้นตอน (+) > ส่วนขยายการสนับสนุน > ตรวจหาความเร่งด่วน ในช่องคำอธิบายแหล่งที่มา ให้คลิก + ตัวแปร แล้วเลือกคำอธิบายตั๋ว
  4. คลิกเพิ่มขั้นตอน (+) แล้วเพิ่มขั้นตอน Chat > แจ้งเตือนฉันใน Chat ที่มีตัวแปรชื่อของคำขอ ระดับความเร่งด่วน และคำอธิบายของคำขอในข้อความ
  5. สลับโฟลว์เป็นเปิดที่มุมขวาบน Google Workspace Studio จะเรียกใช้/onManageTriggerปลายทางใน Cloud Run ทันที ซึ่งจะเขียนการสมัครใช้บริการทริกเกอร์ที่ใช้งานอยู่ไปยังคอลเล็กชัน 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

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

สิ่งที่เราได้พูดถึงไปแล้ว

  • การกำหนดค่าไฟล์ Manifest ของส่วนเสริม 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) เอาต์พุตที่มีโครงสร้างในแพลตฟอร์ม Agent ของ Gemini Enterprise โดยมีตัวเลือกสำรองที่ยืดหยุ่นในการจับคู่คีย์เวิร์ด

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