使用回调创建人机协同工作流

本教程介绍了如何创建一个翻译工作流,以等待您的输入(人机协同),并将 Firestore 数据库、两个 Cloud Functions 函数、Cloud Translation API 以及使用要实时更新的 Firebase SDK 的网页关联起来。

借助 Workflows,您可以支持回调端点(或 webhook),用于等待 HTTP 请求到达该端点,并在稍后继续执行该工作流。在这种情况下,工作流会等待您的输入拒绝或验证某些文本的翻译,但也可以等待外部进程。如需了解详情,请参阅等待使用回调

架构

本教程会创建一个 Web 应用,以便允许您执行以下操作:

  1. 在翻译网页上,输入您希望从英语翻译成法语的文本。点击翻译
  2. 在网页中调用一个 Cloud Functions 函数来启动工作流的执行。要翻译的文本将作为参数传递给函数和工作流。
  3. 该文本保存在 Cloud Firestore 数据库中。调用 Cloud Translation API。返回的译文存储在数据库中。使用 Firebase 托管部署 Web 应用,并实时更新以显示翻译后的文本。
  4. 工作流的 create_callback 步骤会创建一个回调端点网址,此网址也保存在 Firestore 数据库中。此时,网页会同时显示验证拒绝按钮。
  5. 工作流现已暂停,并等待对回调端点网址的显式 HTTP POST 请求。
  6. 您可以决定是验证还是拒绝翻译。点击一个按钮会调用第二个 Cloud Functions 函数,而该 Cloud Functions 函数又会调用由工作流创建的回调端点,并传递审批状态。工作流恢复执行,并将审批状态 truefalse 保存在 Firestore 数据库中。

下图简要展示了此过程:

使用回调的工作流

目标

  • 部署 Web 应用。
  • 创建 Firestore 数据库以存储翻译请求。
  • 部署 Cloud Functions 函数以运行工作流。
  • 部署并运行工作流以编排整个过程。

费用

本教程使用 Google Cloud 的以下收费组件:

请使用价格计算器根据您的预计用量来估算费用。

Google Cloud 新用户可能有资格申请免费试用

准备工作

如果您的组织将限制应用于您的 Google Cloud 环境,则本文档中的某些步骤可能无法正常工作。在这种情况下,您可能无法完成创建公共 IP 地址或服务帐号密钥等任务。如果您发出的请求会返回有关限制条件的错误,请参阅如何在受限的 Google Cloud 环境中开发应用

  1. 登录您的 Google Cloud 帐号。如果您是 Google Cloud 新手,请创建一个帐号来评估我们的产品在实际场景中的表现。新客户还可获享 $300 赠金,用于运行、测试和部署工作负载。
  2. 在 Google Cloud Console 的项目选择器页面上,选择或创建一个 Google Cloud 项目。

    转到“项目选择器”

  3. 确保您的 Cloud 项目已启用结算功能。 了解如何确认您的项目是否已启用结算功能

  4. 启用 App Engine, Cloud Build, Cloud Functions, Firestore, Translation, Workflows API。

    启用 API

  5. 在 Google Cloud Console 的项目选择器页面上,选择或创建一个 Google Cloud 项目。

    转到“项目选择器”

  6. 确保您的 Cloud 项目已启用结算功能。 了解如何确认您的项目是否已启用结算功能

  7. 启用 App Engine, Cloud Build, Cloud Functions, Firestore, Translation, Workflows API。

    启用 API

  8. 通过请求将您的项目添加到 Google Cloud 工作流的非公开预览版,确保您有权访问此功能。
  9. 安装并初始化 Cloud SDK。
  10. 更新 gcloud 组件:
    gcloud components update
  11. 使用您的帐号登录:
    gcloud auth login
  12. 设置本教程中使用的默认位置:
    export REGION=REGION
    gcloud config set workflows/location ${REGION}
    

    REGION 替换为您选择的受支持的 Workflows 位置

部署第一个 Cloud Functions 函数

此 Cloud Functions 函数启动工作流的执行。要翻译的文本将作为参数传递给函数和工作流。

  1. 创建一个名为 callback-translation 的目录,其中包含名为 invokeTranslationWorkflowtranslationCallbackCallpublic 的子目录:

    mkdir -p ~/callback-translation/{invokeTranslationWorkflow,translationCallbackCall,public}
    
  2. 切换到 invokeTranslationWorkflow 目录:

    cd ~/callback-translation/invokeTranslationWorkflow
    
  3. 创建一个文件名为 index.js 且包含以下 Node.js 代码的文本文件:

    const cors = require('cors')({origin: true});
    const {ExecutionsClient} = require('@google-cloud/workflows');
    const client = new ExecutionsClient();
    
    exports.invokeTranslationWorkflow = async (req, res) => {
      cors(req, res, async () => {
        const text = req.body.text;
        console.log(`Translation request for "${text}"`);
    
        const PROJECT_ID = process.env.PROJECT_ID;
        const CLOUD_REGION = process.env.CLOUD_REGION;
        const WORKFLOW_NAME = process.env.WORKFLOW_NAME;
    
        const execResponse = await client.createExecution({
          parent: client.workflowPath(PROJECT_ID, CLOUD_REGION, WORKFLOW_NAME),
          execution: {
            argument: JSON.stringify({text})
          }
        });
        console.log(`Translation workflow execution request: ${JSON.stringify(execResponse)}`);
    
        const execName = execResponse[0].name;
        console.log(`Created translation workflow execution: ${execName}`);
    
        res.set('Access-Control-Allow-Origin', '*');
        res.status(200).json({executionId: execName});
      });
    };
  4. 创建一个文件名为 package.json 且包含以下 npm 元数据的文本文件:

    {
      "name": "launch-translation-workflow",
      "version": "0.0.1",
      "dependencies": {
        "@google-cloud/workflows": "^1.2.5",
        "cors": "^2.8.5"
      }
    }
    
  5. 使用 HTTP 触发器部署函数,并允许未经身份验证的访问:

    gcloud functions deploy invokeTranslationWorkflow \
    --region=${REGION} \
    --runtime nodejs14 \
    --entry-point=invokeTranslationWorkflow \
    --set-env-vars PROJECT_ID=${GOOGLE_CLOUD_PROJECT},CLOUD_REGION=${REGION},WORKFLOW_NAME=translation_validation \
    --trigger-http \
    --allow-unauthenticated
    

    部署该函数可能需要几分钟的时间。或者,您也可以使用 Cloud Console 中的 Cloud Functions 界面来部署该函数。

  6. 部署该函数后,您可以确认 httpsTrigger.url 属性:

    gcloud functions describe invokeTranslationWorkflow
    

    记下返回的网址,以便在后续步骤中使用。

部署第二个 Cloud Functions 函数

此 Cloud Functions 函数会向工作流创建的回调端点发出 HTTP POST 请求,并传递审批状态以反映翻译已验证还是被拒绝。

  1. 切换到 translationCallbackCall 目录:

    cd ../translationCallbackCall
    
  2. 创建一个文件名为 index.js 且包含以下 Node.js 代码的文本文件:

    const cors = require('cors')({origin: true});
    const fetch = require('node-fetch');
    
    exports.translationCallbackCall = async (req, res) => {
      cors(req, res, async () => {
        res.set('Access-Control-Allow-Origin', '*');
    
        const {url, approved} = req.body;
        console.log("Approved? ", approved);
        console.log("URL = ", url);
        const {GoogleAuth} = require('google-auth-library');
        const auth = new GoogleAuth();
        const token = await auth.getAccessToken();
        console.log("Token", token);
    
        try {
          const resp = await fetch(url, {
              method: 'POST',
              headers: {
                  'accept': 'application/json',
                  'content-type': 'application/json',
                  'authorization': `Bearer ${token}`
              },
              body: JSON.stringify({ approved })
          });
          console.log("Response = ", JSON.stringify(resp));
    
          const result = await resp.json();
          console.log("Outcome = ", JSON.stringify(result));
    
          res.status(200).json({status: 'OK'});
        } catch(e) {
          console.error(e);
    
          res.status(200).json({status: 'error'});
        }
      });
    };
  3. 创建一个文件名为 package.json 且包含以下 npm 元数据的文本文件:

    {
      "name": "approve-translation-workflow",
      "version": "0.0.1",
      "dependencies": {
        "cors": "^2.8.5",
        "node-fetch": "^2.6.1",
        "google-auth-library": "^7.1.1"
      }
    }
    
  4. 使用 HTTP 触发器部署函数,并允许未经身份验证的访问:

    gcloud functions deploy translationCallbackCall \
    --region=${REGION} \
    --runtime nodejs14 \
    --entry-point=translationCallbackCall \
    --trigger-http \
    --allow-unauthenticated
    

    部署该函数可能需要几分钟的时间。或者,您也可以使用 Cloud Console 中的 Cloud Functions 界面来部署该函数。

  5. 部署该函数后,您可以确认 httpsTrigger.url 属性:

    gcloud functions describe translationCallbackCall
    

    记下返回的网址,以便在后续步骤中使用。

部署工作流

工作流由一系列使用 Workflows 语法描述的步骤组成,该语法可以采用 YAML 或 JSON 格式编写。这是工作流的定义。创建工作流后,可以进行部署,使其可以执行。

  1. 切换到 callback-translation 目录:

    cd ..
    
  2. 创建一个文件名为 translation-validation.yaml 且包含以下内容的文本文件:

    main:
        params: [translation_request]
        steps:
            - log_request:
                call: sys.log
                args:
                    text: ${translation_request}
            - vars:
                assign:
                    - exec_id: ${sys.get_env("GOOGLE_CLOUD_WORKFLOW_EXECUTION_ID")}
                    - text_to_translate: ${translation_request.text}
                    - database_root: ${"projects/" + sys.get_env("GOOGLE_CLOUD_PROJECT_ID") + "/databases/(default)/documents/translations/"}
            - log_translation_request:
                call: sys.log
                args:
                    text: ${text_to_translate}
    
            - store_translation_request:
                call: googleapis.firestore.v1.projects.databases.documents.patch
                args:
                    name: ${database_root + exec_id}
                    updateMask:
                        fieldPaths: ['text']
                    body:
                        fields:
                            text:
                                stringValue: ${text_to_translate}
                result: store_translation_request_result
    
            - translate:
                call: googleapis.translate.v2.translations.translate
                args:
                    query:
                        q: ${text_to_translate}
                        target: "FR"
                        format: "text"
                        source: "EN"
                result: translation_result
            - assign_translation:
                assign:
                    - translation: ${translation_result.data.translations[0].translatedText}
            - log_translation_result:
                call: sys.log
                args:
                    text: ${translation}
    
            - store_translated_text:
                call: googleapis.firestore.v1.projects.databases.documents.patch
                args:
                    name: ${database_root + exec_id}
                    updateMask:
                        fieldPaths: ['translation']
                    body:
                        fields:
                            translation:
                                stringValue: ${translation}
                result: store_translation_request_result
    
            - create_callback:
                call: events.create_callback_endpoint
                args:
                    http_callback_method: "POST"
                result: callback_details
            - log_callback_details:
                call: sys.log
                args:
                    text: ${callback_details}
    
            - store_callback_details:
                call: googleapis.firestore.v1.projects.databases.documents.patch
                args:
                    name: ${database_root + exec_id}
                    updateMask:
                        fieldPaths: ['callback']
                    body:
                        fields:
                            callback:
                                stringValue: ${callback_details.url}
                result: store_callback_details_result
    
            - await_callback:
                call: events.await_callback
                args:
                    callback: ${callback_details}
                    timeout: 3600
                result: callback_request
            - assign_approval:
                assign:
                    - approved: ${callback_request.http_request.body.approved}
    
            - store_approval:
                call: googleapis.firestore.v1.projects.databases.documents.patch
                args:
                    name: ${database_root + exec_id}
                    updateMask:
                        fieldPaths: ['approved']
                    body:
                        fields:
                            approved:
                                booleanValue: ${approved}
                result: store_approval_result
    
            - return_outcome:
                return:
                    text: ${text_to_translate}
                    translation: ${translation}
                    approved: ${approved}
  3. 创建工作流后,您可以对其进行部署,但不要执行工作流

    gcloud workflows deploy translation_validation --source=translation-validation.yaml
    

创建您的 Web 应用

创建一个调用 Cloud Functions 函数的 Web 应用,以启动工作流的执行。网页会实时更新,提供翻译请求的结果。

  1. 切换到 public 目录:

    cd public
    
  2. 创建一个文件名为 index.html 且包含以下 HTML 标记的文本文件。(在稍后的步骤中,您将修改 index.html 文件并添加 Firebase SDK 脚本。)

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">
    
        <title>Frenglish translation — Feature Workflows callbacks</title>
    
        <link rel="stylesheet"
            href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.42/dist/themes/base.css">
        <script type="module"
            src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.42/dist/shoelace.js"></script>
        <link rel="stylesheet" href="./style.css">
    </head>
    
    <body>
        <h1>Translate from English to French</h1>
    
        <sl-form class="form-overview">
            <sl-textarea id="text" placeholder="The quick brown fox jumps over the lazy dog."
                label="English text to translate"></sl-textarea>
            <p></p>
            <sl-button id="translateBtn" type="primary">Translate</sl-button>
            <p></p>
            <sl-alert id="translation" type="primary">
                Le rapide renard brun saute au dessus du chien paresseux.
            </sl-alert>
            <p></p>
            <div id="buttonRow" style="display: none;">
                <sl-button id="validateBtn" type="success">Validate</sl-button>
                <sl-button id="rejectBtn" type="danger">Reject</sl-button>
            </div>
            <p></p>
            <sl-alert id="validationAlert" type="success">
                <sl-icon slot="icon" name="check2-circle"></sl-icon>
                <strong>The translation has been validated</strong><br>
                Glad that you liked our translation! We'll save it in our database.
            </sl-alert>
            <sl-alert id="rejectionAlert" type="danger">
                <sl-icon slot="icon" name="exclamation-octagon"></sl-icon>
                <strong>The translation has been rejected</strong><br>
                A pity the translation isn't good! We'll do better next time!
            </sl-alert>
            <p></p>
            <sl-button id="newBtn" style="display: none;" type="primary">New translation</sl-button>
        </sl-form>
    
        <script src="https://www.gstatic.com/firebasejs/8.6.3/firebase-app.js"></script>
        <script src="https://www.gstatic.com/firebasejs/8.6.3/firebase-firestore.js"></script>
    
        <script>
            var firebaseConfig = {
                apiKey: "XXXX",
                authDomain: "XXXX",
                projectId: "XXXX",
                storageBucket: "XXXX",
                messagingSenderId: "XXXX",
                appId: "XXXX",
                measurementId: "XXXX"
            };
            // Initialize Firebase
            firebase.initializeApp(firebaseConfig);
        </script>
        <script src="./script.js" type="module"></script>
    </body>
    
    </html>
    
  3. 创建一个文件名为 script.js 且包含以下 JavaScript 代码的文本文件:

    document.addEventListener("DOMContentLoaded", async function (event) {
        const textArea = document.getElementById("text");
        textArea.focus();
    
        const newBtn = document.getElementById("newBtn");
        newBtn.addEventListener("sl-focus", event => {
            event.target.blur();
            window.location.reload();
        });
    
        const translationAlert = document.getElementById("translation");
        const buttonRow = document.getElementById("buttonRow");
    
        var callbackUrl = "";
    
        const validationAlert = document.getElementById("validationAlert");
        const rejectionAlert = document.getElementById("rejectionAlert");
        const validateBtn = document.getElementById("validateBtn");
        const rejectBtn = document.getElementById("rejectBtn");
    
        const translateBtn = document.getElementById("translateBtn");
        translateBtn.addEventListener("sl-focus", async event => {
            event.target.disabled = true;
            event.target.loading = true;
            textArea.disabled = true;
    
            console.log("Text to translate = ", textArea.value);
    
            const fnUrl = UPDATE_ME;
    
            try {
                console.log("Calling workflow executor function...");
                const resp = await fetch(fnUrl, {
                    method: "POST",
                    headers: {
                        "accept": "application/json",
                        "content-type": "application/json"
                    },
                    body: JSON.stringify({ text: textArea.value })
                });
                const executionResp = await resp.json();
                const executionId = executionResp.executionId.slice(-36);
                console.log("Execution ID = ", executionId);
    
                const db = firebase.firestore();
                const translationDoc = db.collection("translations").doc(executionId);
    
                var translationReceived = false;
                var callbackReceived =  false;
                var approvalReceived = false;
                translationDoc.onSnapshot((doc) => {
                    console.log("Firestore update", doc.data());
                    if (doc.data()) {
                        if ("translation" in doc.data()) {
                            if (!translationReceived) {
                                console.log("Translation = ", doc.data().translation);
                                translationReceived = true;
                                translationAlert.innerText = doc.data().translation;
                                translationAlert.open = true;
                            }
                        }
                        if ("callback" in doc.data()) {
                            if (!callbackReceived) {
                                console.log("Callback URL = ", doc.data().callback);
                                callbackReceived = true;
                                callbackUrl = doc.data().callback;
                                buttonRow.style.display = "block";
                            }
                        }
                        if ("approved" in doc.data()) {
                            if (!approvalReceived) {
                                const approved = doc.data().approved;
                                console.log("Approval received = ", approved);
                                if (approved) {
                                    validationAlert.open = true;
                                    buttonRow.style.display = "none";
                                    newBtn.style.display = "inline-block";
                                } else {
                                    rejectionAlert.open = true;
                                    buttonRow.style.display = "none";
                                    newBtn.style.display = "inline-block";
                                }
                                approvalReceived = true;
                            }
                        }
                    }
                });
            } catch (e) {
                console.log(e);
            }
            event.target.loading = false;
        });
    
        validateBtn.addEventListener("sl-focus", async event => {
            validateBtn.disabled = true;
            rejectBtn.disabled = true;
            validateBtn.loading = true;
            validateBtn.blur();
    
            // call callback
            await callCallbackUrl(callbackUrl, true);
        });
    
        rejectBtn.addEventListener("sl-focus", async event => {
            rejectBtn.disabled = true;
            validateBtn.disabled = true;
            rejectBtn.loading = true;
            rejectBtn.blur();
    
            // call callback
            await callCallbackUrl(callbackUrl, false);
        });
    
    });
    
    async function callCallbackUrl(url, approved) {
        console.log("Calling callback URL with status = ", approved);
    
        const fnUrl = UPDATE_ME;
        try {
            const resp = await fetch(fnUrl, {
                method: "POST",
                headers: {
                    "accept": "application/json",
                    "content-type": "application/json"
                },
                body: JSON.stringify({ url, approved })
            });
            const result = await resp.json();
            console.log("Callback answer = ", result);
        } catch(e) {
            console.log(e);
        }
    }
  4. 修改 script.js 文件,将 UPDATE_ME 占位符替换为您之前记下的 Cloud Functions 函数网址。

    1. translateBtn.addEventListener 方法中,将 const fnUrl = UPDATE_ME; 替换为:

      const fnUrl = "https://REGION-PROJECT_ID.cloudfunctions.net/invokeTranslationWorkflow";

    2. callCallbackUrl 函数中,将 const fnUrl = UPDATE_ME; 替换为:

      const fnUrl = "https://REGION-PROJECT_ID.cloudfunctions.net/translationCallbackCall";

  5. 创建一个文件名为 style.css 且包含以下级联样式的文本文件:

    * {
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
    }
    
    body {
        margin: 20px;
    }
    
    h1, h2, h3, h4 {
        color: #0ea5e9;
    }
    

将 Firebase 添加到您的网页应用

在本教程中,HTML 页面、JavaScript 脚本和 CSS 样式表使用 Firebase Hosting 部署为静态资源,但它们可以托管在任何位置,并在您自己的机器上本地提供以进行测试。

创建 Firebase 项目

您需要先创建一个要关联到您的应用的 Firebase 项目,然后才能将 Firebase 添加到您的应用。

  1. Firebase 控制台中,点击创建项目,然后从下拉菜单中选择现有的 Google Cloud 项目,以将 Firebase 资源添加到该项目中。

  2. 点击继续,直到您看到添加 Firebase 的选项。

    跳过为您的项目设置 Google Analytics(分析)的步骤。

  3. 点击添加 Firebase

Firebase 会自动为您的 Firebase 项目预配资源。完成此过程后,您将进入 Firebase 控制台中您的项目的概览页面。

在 Firebase 中注册您的应用

有了 Firebase 项目后,您就可以将自己的 Web 应用添加到其中了。

  1. 在项目概览页面的中心,点击 Web 图标 (</>) 以启动设置工作流。

  2. 输入您的应用的别名。

    只有您能在 Firebase 控制台中看到。

  3. 暂时跳过设置 Firebase Hosting 的步骤。

  4. 点击注册应用并转到该控制台。

启用 Cloud Firestore

Web 应用使用 Cloud Firestore 接收和保存数据。您需要启用 Cloud Firestore。

  1. 在 Firebase 控制台的构建部分中,点击 Firestore 数据库

    (您可能需要先展开左侧导航窗格以查看构建部分。)

  2. 在 Cloud Firestore 窗格中,点击创建数据库

  3. 选择以测试模式开始,并使用如下所示的安全规则:

    rules_version = '2';
    service cloud.firestore {
    match /databases/{database}/documents {
      match /{document=**} {
        allow read, write;
      }
    }
    }
    
  4. 在阅读有关安全规则的免责声明后,点击下一步

  5. 设置 Cloud Firestore 数据的存储位置。您可以接受默认值,也可以选择您附近的区域。

  6. 点击启用以预配 Firestore。

添加 Firebase SDK 并初始化 Firebase

Firebase 为大多数 Firebase 产品提供 JavaScript 库。在使用 Firebase Hosting 之前,您必须将 Firebase SDK 添加到您的 Web 应用。

  1. 要在您的应用中初始化 Firebase,您需要提供应用的 Firebase 项目配置。
    1. 在 Firebase 控制台中,转到您的项目设置
    2. 您的应用窗格中,选择您的应用。
    3. SDK 设置和配置窗格中,如需从 CDN 加载 Firebase SDK 库,请选择 CDN
    4. 将代码段复制到 <body> 标记底部的 index.html 文件中,替换 XXXX 占位符值。
  2. 安装 Firebase JavaScript SDK。

    1. 如果您还没有 package.json 文件,请从 callback-translation 目录中运行以下命令来创建一个:

      npm init
    2. 运行以下命令,安装 firebase npm 软件包并将其保存到 package.json 文件:

      npm install --save firebase

初始化和部署您的项目

如需将本地项目文件关联到 Firebase 项目,您必须初始化您的项目。然后,您可以部署您的 Web 应用。

  1. callback-translation 目录运行以下命令:

    firebase init
  2. 选择 Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys 选项。

  3. 选择使用现有项目并输入您的项目 ID。

  4. 接受 public 作为默认公共根目录。

  5. 选择配置单页应用。

  6. 跳过使用 GitHub 设置自动构建和部署的步骤。

  7. File public/index.html already exists. Overwrite? 提示符处输入

  8. 切换到 public 目录:

    cd public
  9. public 目录中,运行以下命令以将您的项目部署到网站:

    firebase deploy --only hosting

在本地测试 Web 应用

借助 Firebase Hosting,您可以在本地查看和测试更改并与模拟后端项目资源交互。使用 firebase serve 时,您的应用会与托管内容和配置的模拟后端进行交互,但与所有其他项目资源的实际后端交互。在本教程中,您可以使用 firebase serve,但在进行范围更广的测试时不建议使用。

  1. public 目录运行以下命令:

    firebase serve
  2. 使用返回的本地网址(通常为 http://localhost:5000)打开您的 Web 应用。

  3. 输入一些英文文本,然后点击翻译

    此时应显示法语文本的翻译。

  4. 您现在可以点击验证拒绝

    在 Firestore 数据库中,您可以验证内容。它应该类似于以下内容:

    approved: true
    callback: "https://workflowexecutions.googleapis.com/v1/projects/26811016474/locations/us-central1/workflows/translation_validation/executions/68bfce75-5f62-445f-9cd5-eda23e6fa693/callbacks/72851c97-6bb2-45e3-9816-1e3dcc610662_1a16697f-6d90-478d-9736-33190bbe222b"
    text: "The quick brown fox jumps over the lazy dog."
    translation: "Le renard brun rapide saute par-dessus le chien paresseux."
    

    approved 状态为 truefalse,具体取决于您是验证还是拒绝翻译。

恭喜!您已创建了一个使用 Workflows 回调的人机协同翻译工作流。

清理

如果您为本教程创建了一个新项目,请删除项目。 如果您使用的是现有项目,希望保留此项目且不保留本教程中添加的任何更改,请删除为教程创建的资源

删除项目

为了避免产生费用,最简单的方法是删除您为本教程创建的项目。

要删除项目,请执行以下操作:

  1. 在 Cloud Console 中,转到管理资源页面。

    转到“管理资源”

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关闭以删除项目。

删除教程资源

  1. 移除您在教程设置过程中添加的 gcloud 默认配置:

    gcloud config unset workflows/location
    
  2. 删除在本教程中创建的工作流:

    gcloud workflows delete WORKFLOW_NAME
    
  3. 删除您在本教程中创建的 Cloud Functions 函数:

    gcloud functions delete FUNCTION_NAME
    

    您也可以通过 Google Cloud Console 删除 Cloud Functions 函数。

后续步骤