构建自定义 Workspace Studio 启动器和操作步骤

1. 简介

在此 Codelab 中,您将构建一个直接与 Google Workspace Studio 交互的互动式支持快速门户。您将学习如何构建自定义起始步骤和自定义操作步骤,以便立即进行测试,而无需依赖任何第三方基础设施。

我们将使用 Google Apps 脚本来创建:

  • 一个Web 应用,其中包含一个简洁的表单,供用户手动提交支持服务工单。
  • 一种自定义启动器,用于捕获这些 Web 应用提交内容并触发 Workspace Studio 流程。
  • 一个自定义步骤,用于分析传入工单的说明,以确定紧急程度(“高”或“正常”)。

学习内容

  • 如何在 appsscript.json 清单中配置初始状态和步骤。
  • 如何在启动器注册生命周期内构建 notifyUri。
  • 如何使用 UrlFetchApp 从外部代码以编程方式触发 Workspace Studio 流程。
  • 如何在自定义步骤中构建输出变量,以便将数据传递到下游。

前提条件

  • 已启用 Google Workspace Studio 的 Google Workspace 账号。
  • 在网域的管理控制台中启用允许使用未发布的(测试)自定义步骤设置(位于应用 > Google Workspace > Workspace Studio > 自定义步骤设置下)。
  • 熟悉 Google Apps 脚本。

2. 设置 Apps 脚本项目

首先,我们来创建一个新的 Apps 脚本项目来存放代码:

  1. 前往 script.google.com,然后点击新项目。
  2. 将项目命名为 Support Quick Portal。
  3. 点击左侧边栏中的 项目设置 项目设置。
  4. 选中在编辑器中显示 appsscript.json 清单文件复选框。
  5. 返回到编辑者 编辑器视图。

默认情况下,Apps 脚本项目与一个隐藏的默认 Google Cloud 项目相关联。如需调用 Google Workspace Studio API 并避免 PERMISSION_DENIED 错误,您必须将脚本切换为标准云项目,启用该 API,并正确配置 Google Auth 平台。

  1. 选择或创建项目:前往 Google Cloud 控制台,然后使用页面顶部的项目下拉菜单选择现有项目或创建新项目。
  2. 启用 API:在顶部栏中激活所选项目后,打开 API 启用流程,然后依次点击下一步和启用 Workspace Studio API。
  3. 配置 OAuth 品牌设置:打开 Google Auth Platform 品牌设置页面。
    1. 如果系统提示,请点击开始。(如果已配置,请跳至第 4 步)。
    2. 在应用信息下,输入应用名称和用户支持电子邮件地址。点击下一步。
    3. 在受众群体下,选择内部(如果没有“内部”,则选择外部),然后点击下一步。
    4. 提供联系信息,同意数据政策,然后点击创建。
  4. 配置数据访问权限:打开“数据访问权限”页面,然后点击添加或移除范围。
    1. 在手动添加范围下,粘贴 https://www.googleapis.com/auth/workspace.studio.trigger。
    2. 依次点击添加到表格和更新,最后点击页面底部的保存。
  5. 前往 IAM 和管理 > 设置(或 Cloud 控制台信息中心),然后复制项目编号。
  6. 返回您的 Apps 脚本项目,然后点击左侧边栏中的项目设置 项目设置。
  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. 构建 Web 应用界面

为了提供一种直观且可测试的方式来触发我们的启动器,而无需使用终端工具或第三方 Webhook,我们将构建一个 Apps 脚本 Web 应用。

在 Apps 脚本编辑器中,点击文件旁边的 添加文件 添加文件,然后选择 HTML。将其命名为 index.html(系统会自动添加 .html 扩展名)。

粘贴以下简化的界面代码:

<!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 应用、起始逻辑和操作步骤逻辑分别放在三个不同的脚本文件中。

WebApp.gs

将编辑器中的默认 Code.gs 文件重命名为 WebApp.gs,删除样板 myFunction,然后将以下代码块复制并粘贴到其中。此文件用于处理 Web 应用托管:

/** --- 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 云项目,因此必须针对新项目明确授权脚本的范围(例如 UrlFetchApp 使用的 script.external_request)。

  1. 在 Apps 脚本编辑器中,选择文件下的 WebApp.gs,然后从顶部工具栏下拉菜单中选择 doGet 函数。
  2. 点击运行。
  3. 系统会显示“需要授权”提示。点击查看权限。
  4. 选择您的账号并授权脚本。如果您看到“未经验证的应用”警告,请点击高级 -> 前往“支持快速门户”以绕过该警告。
  5. 等待执行日志显示执行完成,这表示脚本现已获得完全授权。

第 2 步:测试扩展程序和 Web 应用

由于您的 appsscript.json 同时定义了插件和 Web 应用,因此您可以同时配置这两者。

  1. 在 Apps 脚本右上角,依次点击部署 > 测试部署。
  2. 点击选择类型旁边的 启用部署类型 启用部署类型,确保已勾选 Google Workspace 加载项,然后点击安装。
  3. 再次点击 启用部署类型 Enable deployment types,确保选中 Web app,然后复制 URL(以 /dev 结尾)。这是您构建的 Support Quick Portal Web 应用的网址。
  4. 点击完成。您可能需要刷新浏览器,然后 Workspace Studio 才能注册该插件。

第 3 步:配置工作室流程

  1. 在 studio.workspace.google.com 中打开 Google Workspace Studio。
  2. 点击左侧的 添加工具 Add tool(添加工具),然后选择 Flow(流程)。
  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. 点击提交。等待“工单已成功转送”消息。
  4. 查看您的 Workspace Studio 活动日志、Gmail 收件箱或 Google Chat(具体取决于您选择的操作)。您应该会看到一条标记为“高”优先级的消息或电子邮件,该消息或电子邮件已使用您的自定义 Apps 脚本元素进行原生集成。

6. 清理

为避免工作区和 Google Cloud 账号杂乱无章,您可以清理在本 Codelab 中创建的资源。

  1. 删除 Workspace Studio Flow:
    • 打开 studio.workspace.google.com。
    • 找到你的Support Ticket Router节奏。
    • 点击该应用旁边的三点状菜单,然后选择删除。
  2. 删除 Apps 脚本项目:
    • 前往 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 应用(或传入的网络钩子)的触发位置,将这些原则与 Jira、Salesforce 或 Zendesk 等实际 SaaS 平台相关联。