建構自訂 Workspace Studio 起始範本和動作步驟

1. 簡介

在本程式碼研究室中,您將建構可直接與 Google Workspace Studio 互動的支援快速入口。您將瞭解如何建構自訂起始步驟和自訂動作步驟,不必依賴任何第三方基礎架構,即可立即測試。

我們會使用 Google Apps Script 建立:

  • 網頁應用程式:提供簡潔的表單,供使用者手動提交支援單。
  • 自訂啟動條件:擷取這些網頁應用程式提交內容,並觸發 Workspace Studio 工作流程。
  • 自訂步驟:分析傳入工單的說明,判斷緊急程度 (「高」或「一般」)。

課程內容

  • 如何在 appsscript.json 資訊清單中設定啟動器和步驟。
  • 如何在入門版註冊生命週期中建構 notifyUri。
  • 如何使用 UrlFetchApp 從外部程式碼以程式輔助方式觸發 Workspace Studio 工作流程。
  • 如何在自訂步驟中建構輸出變數,將資料傳遞至下游。

必要條件

  • 已啟用 Google Workspace Studio 的 Google Workspace 帳戶。
  • 在網域的管理控制台中啟用「允許使用未發布 (測試) 的自訂步驟」設定 (依序前往「應用程式」>「Google Workspace」>「Workspace Studio」>「自訂步驟設定」)。
  • 熟悉 Google Apps Script。

2. 設定 Apps Script 專案

首先,請建立新的 Apps Script 專案來存放程式碼:

  1. 前往 script.google.com,然後按一下「新專案」。
  2. 將專案命名為「Support Quick Portal」。
  3. 按一下左側邊欄中的「專案設定」專案設定。
  4. 勾選「在編輯器中顯示『appsscript.json』資訊清單檔案」。
  5. 返回「編輯器」編輯者檢視畫面。

根據預設,Apps Script 專案會與隱藏的預設 Google Cloud 雲端專案建立關聯。如要呼叫 Google Workspace Studio API 並避免 PERMISSION_DENIED 錯誤,您必須將指令碼切換至標準雲端專案、啟用 API,並正確設定 Google Auth Platform。

  1. 選取或建立專案:前往 Google Cloud 控制台,然後使用頁面頂端的專案下拉式選單,選擇現有專案或建立新專案。
  2. 啟用 API:在頂端列中啟用所選專案後,開啟 API 啟用流程,然後依序點選「下一步」和「啟用」,啟用 Workspace Studio API。
  3. 設定 OAuth 品牌宣傳:開啟 Google Auth Platform 品牌宣傳頁面。
    1. 如果系統提示,請按一下「開始使用」。(如已設定,請跳到步驟 4)。
    2. 在「App Information」(應用程式資訊) 下方,輸入應用程式名稱和使用者支援電子郵件地址。點選「下一步」。
    3. 在「目標對象」下方,選取「內部」 (或「外部」,如果沒有「內部」選項),然後點選「下一步」。
    4. 提供聯絡資訊、同意資料政策,然後點按「建立」。
  4. 設定資料存取權:開啟「資料存取」頁面,然後按一下「新增或移除範圍」。
    1. 在「手動新增範圍」下方,貼上 https://www.googleapis.com/auth/workspace.studio.trigger。
    2. 依序點選「新增至表格」和「更新」,然後按一下頁面底部的「儲存」。
  5. 前往「IAM & Admin」>「Settings」(IAM 與管理 > 設定) (或 Cloud Console 資訊主頁),然後複製「Project number」(專案編號)。
  6. 返回 Apps Script 專案,然後按一下左側邊欄的「專案設定」專案設定。
  7. 在「Google Cloud Platform (GCP) 專案」部分下方,點選「變更專案」。
  8. 輸入您複製的「GCP 專案編號」,然後按一下「設定專案」。

設定資訊清單

開啟新顯示的 appsscript.json 檔案。將其內容替換為下列程式碼。這份資訊清單會明確列出必要的 OAuth 範圍,並在 studio.flows.workflowElements 區塊下,將啟動條件定義為 workflowTrigger,動作定義為 workflowAction。

{
  "timeZone": "America/Los_Angeles",
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "webapp": {
     "executeAs": "USER_ACCESSING",
     "access": "MYSELF"
  },
  "oauthScopes": [
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/workspace.studio.trigger",
    "https://www.googleapis.com/auth/script.storage",
    "https://www.googleapis.com/auth/script.locale"
  ],
  "urlFetchWhitelist": [
    "https://workspacestudio.googleapis.com/"
  ],
  "addOns": {
    "common": {
      "name": "Support Extension",
      "logoUrl": "https://www.gstatic.com/images/branding/productlogos/buganizer/v1/192px.svg",
      "useLocaleFromApp": true
    },
    "studio": {
      "flows": {
        "workflowElements": [
          {
            "id": "supportTicketTrigger",
            "state": "ACTIVE",
            "name": "New Support Ticket",
            "description": "Fires when someone submits a ticket via the Web App.",
            "workflowTrigger": {
              "inputs": [],
              "outputs": [
                {
                  "id": "ticketTitle",
                  "description": "The title of the ticket",
                  "cardinality": "SINGLE",
                  "dataType": { "basicType": "STRING" }
                },
                {
                  "id": "ticketDescription",
                  "description": "The full description of the issue",
                  "cardinality": "SINGLE",
                  "dataType": { "basicType": "STRING" }
                }
              ],
              "onConfigFunction": "onConfigTrigger",
              "onManageFunction": "onManageTrigger"
            }
          },
          {
            "id": "urgencyDetectorStep",
            "state": "ACTIVE",
            "name": "Detect Urgency",
            "description": "Scans the ticket description to detect High or Normal urgency.",
            "workflowAction": {
              "inputs": [
                {
                  "id": "ticketDescription",
                  "description": "The ticket description",
                  "cardinality": "SINGLE",
                  "dataType": { "basicType": "STRING" }
                }
              ],
              "outputs": [
                {
                  "id": "urgencyLevel",
                  "description": "The urgency High/Normal",
                  "cardinality": "SINGLE",
                  "dataType": { "basicType": "STRING" }
                }
              ],
              "onConfigFunction": "onConfigUrgency",
              "onExecuteFunction": "onExecuteUrgency"
            }
          }
        ]
      }
    }
  }
}

3. 建構網頁應用程式 UI

為了提供觸發啟動器的視覺化測試方式,不必使用終端機工具或第三方 Webhook,我們將建構 Apps Script 網頁應用程式。

在 Apps Script 指令碼編輯器中,按一下「檔案」旁的 新增檔案「新增檔案」,然後選取「HTML」。將其命名為 index.html (系統會自動新增 .html 擴充功能)。

貼上下列簡化的 UI 程式碼:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      body { font-family: Arial, sans-serif; padding: 20px; max-width: 500px; margin: auto; }
      label { font-weight: bold; display: block; margin-top: 15px; }
      input, textarea { width: 100%; margin-top: 5px; padding: 10px; box-sizing: border-box; }
      button { background-color: #1a73e8; color: white; border: none; padding: 10px 15px; margin-top: 15px; font-weight: bold; cursor: pointer; border-radius: 4px; }
      button:hover { background-color: #1557b0; }
      #status { margin-top: 15px; color: green; font-weight: bold; }
    </style>
  </head>
  <body>
    <h2>Support Quick Portal</h2>
    <p>Submit your issue below to trigger the Workspace Studio flow.</p>

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

    <label for="description">Issue Description</label>
    <textarea id="description" rows="4" placeholder="Explain your issue (use words like 'urgent' to test the step!)..."></textarea>

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

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

        if (!title || !description) {
           statusEl.style.color = "red";
           statusEl.innerText = "Please fill out both fields.";
           return;
        }

        statusEl.style.color = "green";
        statusEl.innerText = "Submitting ticket...";

        google.script.run
          .withSuccessHandler(function(response) {
            statusEl.innerText = response;
          })
          .withFailureHandler(function(error) {
            statusEl.style.color = 'red';
            statusEl.innerText = 'Error: ' + error.message;
          })
          .submitTicketToStudio(title, description);
      }
    </script>
  </body>
</html>

4. 建構後端指令碼

現在來整合所有內容。為確保程式碼可讀性,我們會將 Web 應用程式、Starter 邏輯和動作步驟邏輯分成三個不同的指令碼檔案。

WebApp.gs

在編輯器中將預設的 Code.gs 檔案重新命名為 WebApp.gs,刪除樣板 myFunction,然後複製下列程式碼區塊並貼入其中。這個檔案會處理網頁應用程式代管作業:

/** --- WEB APP HOSTING --- **/

/**
 * Serves the HTML UI when users visit the web app URL.
 */
function doGet() {
  return HtmlService.createHtmlOutputFromFile('index')
      .setTitle('Support Quick Portal')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

/**
 * Called by the Web App form. It calls the Workspace Studio API.
 */
function submitTicketToStudio(title, description) {
  const props = PropertiesService.getUserProperties();
  const notifyUri = props.getProperty('notifyUri');
  const triggerId = props.getProperty('triggerId');

  if (!notifyUri || !triggerId) {
    throw new Error('Trigger URL missing! Make sure the flow is enabled in Workspace Studio.');
  }

  // Use ScriptApp.getOAuthToken() (requires the workspace.studio.trigger scope in manifest)
  const token = ScriptApp.getOAuthToken();
  const requestId = Utilities.getUuid();

  // Package our outputs mapped in appsscript.json
  const payload = {
    "name": "triggers/" + triggerId,
    "outputs": {
      "ticketTitle": { "stringValues": [title] },
      "ticketDescription": { "stringValues": [description] }
    },
    "requestId": requestId
  };

  const options = {
    "method": "POST",
    "contentType": "application/json",
    "headers": { "Authorization": "Bearer " + token },
    "payload": JSON.stringify(payload),
    "muteHttpExceptions": true
  };

  // Fire!
  const response = UrlFetchApp.fetch(notifyUri, options);
  if (response.getResponseCode() !== 200) {
    console.error(response.getContentText());
    throw new Error('Workspace Studio API Error: ' + response.getResponseCode());
  }
  return 'Ticket successfully routed to Workspace Studio!';
}

Starter.gs

接著,將游標懸停在「檔案」上,按一下「新增檔案」新增檔案,選取「指令碼」,然後將其命名為 Starter.gs。複製下列程式碼區塊並貼到該檔案中。這個檔案會處理起始範本設定和 Workspace Studio 生命週期管理:

/** --- STARTER LOGIC --- **/

/**
 * Workspace Studio calls this to display the config UI when the user adds the starter.
 * We just return a static message since there are no properties to configure.
 */
function onConfigTrigger() {
  const section = CardService.newCardSection()
      .setHeader("Configure Web App Starter")
      .addWidget(CardService.newTextParagraph().setText("Ready to go! Once you enable this flow, deploy your Apps Script project as a Web App to submit tickets."));

  return CardService.newCardBuilder().addSection(section).build();
}

/**
 * Workspace Studio posts data here to manage the lifecycle (enable/disable the trigger).
 * Critically, we must construct or save the `notifyUri` and `triggerId` to call it later.
 */
function onManageTrigger(event) {
  const triggerCreation = event.workflow.triggerCreation;
  const triggerDeletion = event.workflow.triggerDeletion;

  const props = PropertiesService.getUserProperties();

  if (triggerCreation) {
    // Save the unique URL and ID when the flow is enabled
    // Note: notifyUri might be omitted, so fallback to constructing it dynamically
    const notifyUri = triggerCreation.notifyUri || `https://workspacestudio.googleapis.com/v1/triggers/${triggerCreation.triggerId}:fire`;
    props.setProperty('notifyUri', notifyUri);
    props.setProperty('triggerId', triggerCreation.triggerId);
  } else if (triggerDeletion) {
    // Cleanup if the user deletes the flow
    props.deleteProperty('notifyUri');
    props.deleteProperty('triggerId');
  }
}

Action.gs

接著,將游標懸停在「檔案」上,按一下「新增檔案」新增檔案,選取「指令碼」,然後將其命名為 Action.gs。複製下列「緊急程度步驟」邏輯並貼到其中:

/** --- ACTION STEP LOGIC --- **/

/**
 * Workspace Studio configuration card for the Step.
 * We prompt the user to bind a variable to our "ticketDescription" input.
 */
function onConfigUrgency() {
  const input = CardService.newTextInput()
      .setFieldName("ticketDescription")
      .setTitle("Source Description")
      .setHint('Select the description variable outputted by the starter')
      .setHostAppDataSource(
        CardService.newHostAppDataSource().setWorkflowDataSource(CardService.newWorkflowDataSource().setIncludeVariables(true))
      );

  const section = CardService.newCardSection()
      .setHeader("Urgency Detector Step")
      .addWidget(input);

  return CardService.newCardBuilder().addSection(section).build();
}

/**
 * The synchronous logic executed during the Step.
 */
function onExecuteUrgency(event) {
  // Read mapped inputs
  let description = event.workflow.actionInvocation.inputs["ticketDescription"].stringValues[0];
  if (!description) description = "";

  const urgentKeywords = ['urgent', 'crash', 'broken', 'down', 'fire'];
  let urgencyLevel = 'Normal';

  if (urgentKeywords.some(keyword => description.toLowerCase().includes(keyword))) {
    urgencyLevel = 'High';
  }

  // Package outputs correctly
  const variableDataMap = {
    "urgencyLevel": AddOnsResponseService.newVariableData().addStringValue(urgencyLevel)
  };

  return outputVariables(variableDataMap);
}

/**
 * Helper strictly enforcing the expected Step object return type.
 */
function outputVariables(variableDataMap) {
  const workflowAction = AddOnsResponseService.newReturnOutputVariablesAction()
      .setVariableDataMap(variableDataMap);
  const hostAppAction = AddOnsResponseService.newHostAppAction()
      .setWorkflowAction(workflowAction);
  return AddOnsResponseService.newRenderActionBuilder()
      .setHostAppAction(hostAppAction)
      .build();
}

5. 部署及測試

程式碼編寫完成後,請部署及設定專案。

步驟 1:重新授權指令碼

由於您連結了新的標準 Google Cloud 專案,因此必須針對新專案明確授權指令碼的範圍 (例如 script.external_request 使用的 UrlFetchApp)。

  1. 在 Apps Script 指令碼編輯器中,選取「檔案」下方的 WebApp.gs,然後從頂端工具列下拉式選單中選取 doGet 函式。
  2. 按一下「執行」。
  3. 系統會顯示「需要授權」提示。按一下「Review permissions」(查看權限)。
  4. 選擇帳戶並授權指令碼。如果看到「未經驗證的應用程式」警告,請依序點選「進階」->「前往支援快速入口」來略過警告。
  5. 等待「執行記錄」顯示「執行完成」,確認指令碼已獲得完整授權。

步驟 2:測試擴充功能和網頁應用程式

由於 appsscript.json 同時定義外掛程式和 Web 應用程式,因此您可以同時設定兩者。

  1. 在 Apps Script 編輯器中,依序點選右上角的「部署」>「測試部署作業」。
  2. 按一下「選取類型」旁的 啟用部署類型「啟用部署類型」,確認已勾選「Google Workspace 外掛程式」,然後按一下「安裝」。
  3. 再次點按「啟用部署類型」啟用部署類型,確認已勾選「網頁應用程式」,然後複製「網址」 (結尾為 /dev)。這是您建立的「支援快速入口」網頁應用程式網址。
  4. 按一下「完成」,您可能需要重新整理瀏覽器,Workspace Studio 才會註冊外掛程式。

步驟 3:設定 Studio Flow

  1. 前往 studio.workspace.google.com 開啟 Google Workspace Studio。
  2. 按一下左側的「新增工具」圖示 新增工具,然後選取「流程」。
  3. 如要設定啟動器,請點選「選擇啟動器」,然後依序選取已安裝的「支援擴充功能」 >「建立新的支援單」。如果系統顯示「需要權限」訊息,請按一下「授予權限」,允許外掛程式存取資料。
  4. 接著,在「動作」下方點選「選擇步驟」,捲動至「支援擴充功能」,然後選取「偵測緊急程度」步驟。
  5. 在「來源說明」中,按一下「變數」圖示 新增,然後依序選取「步驟 1:新支援單」 >「問題的完整說明」圖示 ticketDescription。
  6. 再次點按「選擇步驟」,然後選取內建步驟,例如「Gmail」>「傳送電子郵件」或「Chat」>「在 Chat 中通知我」。
    • 如果選擇 Gmail:
      • 收件者:輸入自己的電子郵件地址。
      • 主旨:支援單 ticketTitle 優先順序為「urgencyLevel」
      • 訊息: ticketDescription
    • 如果選擇透過即時通訊聯絡:
      • 訊息:票證 ticketTitle 優先順序為 urgencyLevel。ticketDescription
  7. 啟用工作流程。如果系統提示重新命名流程,請輸入名稱 (例如 Support Ticket Router) 並儲存。這會觸發 onManageTrigger 生命週期,儲存 notifyUri。

步驟 4:執行模擬

  1. 在新的瀏覽器分頁中,開啟您稍早在本頁面步驟 2 複製的 Web 應用程式網址 (結尾為 /dev)。
  2. 填寫表單。請務必在說明中加入觸發關鍵字 (例如「緊急」或「當機」)。
  3. 按一下「提交」。等待系統顯示「Ticket successfully routed」(支援單已成功轉送) 訊息。
  4. 查看 Workspace Studio 活動記錄、Gmail 收件匣或 Google Chat (視您選擇的動作而定)。您應該會看到以自訂 Apps Script 元素原生整合的「緊急」訊息或電子郵件。

6. 清除

為避免工作區和 Google Cloud 帳戶雜亂無章,您可以清理在本 Codelab 建立的資源。

  1. 刪除 Workspace Studio 工作流程:
    • 前往 studio.workspace.google.com。
    • 掌握Support Ticket Router敘事節奏。
    • 按一下旁邊的三點選單,然後選取「刪除」。
  2. 刪除 Apps Script 專案:
    • 前往 script.google.com。
    • 找出 Support Quick Portal 專案。
    • 按一下三點圖示選單,然後選取「移除」。
  3. 關閉 Google Cloud 專案:
    • 前往 Google Cloud 控制台。
    • 確認頂端下拉式選單中已選取新專案。
    • 依序前往「IAM 與管理」>「設定」,然後按一下「關閉」。

7. 結語

您已成功為 Google Workspace Studio 自訂擴充功能建構完整的生態系統,完全不需依賴任何外部第三方訂閱項目。

課程內容:

  • 搭配使用 workflowTrigger 和 onManageFunction,正確擷取及處理 Google 的生命週期事件。
  • 使用 ScriptApp.getOAuthToken() 對 workspacestudio.googleapis.com API 端點驗證後端要求。
  • 使用 workflowAction 同步處理輸入變數,並產生下游輸出內容。

現在,您可以修改 Web 應用程式 (或傳入的 Webhook) 的觸發位置,將這些原則水平擴展至 Jira、Salesforce 或 Zendesk 等實際的 SaaS 平台。