使用回呼建立人機迴圈工作流程


本教學課程說明如何建立翻譯工作流程,等待您輸入內容 (即迴路中的人員),並連結 Firestore 資料庫、兩個 Cloud Run 函式、Cloud Translation API,以及使用 Firebase SDK 即時更新的網頁。

使用 Workflows 時,您可以支援回呼端點 (或 Webhook),等待 HTTP 要求抵達該端點,稍後再繼續執行工作流程。在本例中,工作流程會等待您輸入內容,以拒絕或驗證部分文字的翻譯,但工作流程也可能等待外部程序。詳情請參閱「使用回呼等待」。

架構

本教學課程會建立一個網頁應用程式,讓您執行下列操作:

  1. 在翻譯網頁上,輸入要從英文翻譯成法文的文字。按一下 [翻譯]
  2. 網頁會呼叫 Cloud Run 函式,啟動工作流程的執行作業。要翻譯的文字會以參數形式傳遞至函式和工作流程。
  3. 文字會儲存在 Cloud Firestore 資料庫中。呼叫 Cloud Translation API。傳回的翻譯內容會儲存在資料庫中。網頁應用程式會使用 Firebase Hosting 部署,並即時更新以顯示翻譯文字。
  4. 工作流程中的 create_callback 步驟會建立回呼端點 URL,並儲存在 Firestore 資料庫中。網頁現在會顯示「驗證」和「拒絕」按鈕。
  5. 工作流程現在已暫停,並等待對回呼端點網址發出的明確 HTTP POST 要求。
  6. 您可以決定是否要驗證或拒絕翻譯。點選按鈕會呼叫第二個 Cloud Run 函式,該函式接著會呼叫工作流程建立的回呼端點,並傳遞核准狀態。工作流程會繼續執行,並在 Firestore 資料庫中儲存「已核准」true或「已拒絕」false的狀態。

下圖概略說明這個程序:

含回呼的工作流程

目標

  • 部署網頁應用程式。
  • 建立 Firestore 資料庫來儲存翻譯要求。
  • 部署 Cloud Run 函式來執行工作流程。
  • 部署及執行工作流程,自動化整個流程。

費用

在本文件中,您會使用 Google Cloud的下列計費元件:

如要根據預測用量估算費用,請使用 Pricing Calculator

初次使用 Google Cloud 的使用者可能符合免費試用資格。

事前準備

貴機構定義的安全性限制,可能會導致您無法完成下列步驟。如需疑難排解資訊,請參閱「在受限的 Google Cloud 環境中開發應用程式」。

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. Install the Google Cloud CLI.

  3. 如果您使用外部識別資訊提供者 (IdP),請先 使用聯合身分登入 gcloud CLI

  4. 如要初始化 gcloud CLI,請執行下列指令:

    gcloud init
  5. Create or select a Google Cloud project.

    • Create a Google Cloud project:

      gcloud projects create PROJECT_ID

      Replace PROJECT_ID with a name for the Google Cloud project you are creating.

    • Select the Google Cloud project that you created:

      gcloud config set project PROJECT_ID

      Replace PROJECT_ID with your Google Cloud project name.

  6. Verify that billing is enabled for your Google Cloud project.

  7. Enable the App Engine, Cloud Build, Cloud Run functions, Firestore, Translation, and Workflows APIs:

    gcloud services enable appengine.googleapis.com cloudbuild.googleapis.com cloudfunctions.googleapis.com firestore.googleapis.com translate.googleapis.com workflows.googleapis.com
  8. Install the Google Cloud CLI.

  9. 如果您使用外部識別資訊提供者 (IdP),請先 使用聯合身分登入 gcloud CLI

  10. 如要初始化 gcloud CLI,請執行下列指令:

    gcloud init
  11. Create or select a Google Cloud project.

    • Create a Google Cloud project:

      gcloud projects create PROJECT_ID

      Replace PROJECT_ID with a name for the Google Cloud project you are creating.

    • Select the Google Cloud project that you created:

      gcloud config set project PROJECT_ID

      Replace PROJECT_ID with your Google Cloud project name.

  12. Verify that billing is enabled for your Google Cloud project.

  13. Enable the App Engine, Cloud Build, Cloud Run functions, Firestore, Translation, and Workflows APIs:

    gcloud services enable appengine.googleapis.com cloudbuild.googleapis.com cloudfunctions.googleapis.com firestore.googleapis.com translate.googleapis.com workflows.googleapis.com
  14. 更新 Google Cloud CLI 元件:
    gcloud components update
  15. 登入帳戶:
    gcloud auth login
  16. 設定本教學課程中使用的專案 ID 和預設位置:
    export GOOGLE_CLOUD_PROJECT=PROJECT_ID
    export REGION=REGION
    gcloud config set workflows/location ${REGION}

    更改下列內容:

    • PROJECT_ID:您的 Google Cloud 專案 ID。您可以在 Google Cloud 控制台的「歡迎」頁面找到專案 ID。
    • REGION:您選擇的支援 Workflows 的位置

  17. 部署第一個 Cloud Run 函式

    這個 Cloud Run 函式會啟動工作流程的執行作業。要翻譯的文字會以參數形式傳遞至函式和工作流程。

    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

      部署函式可能需要幾分鐘的時間。或者,您也可以在 Google Cloud 控制台使用 Cloud Run 函式介面部署函式。

    6. 部署函式後,您可以確認 httpsTrigger.url 屬性:

      gcloud functions describe invokeTranslationWorkflow

      記下傳回的網址,以便在後續步驟中使用。

    部署第二個 Cloud Run 函式

    這項 Cloud Run 函式會對工作流程建立的回呼端點發出 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

      部署函式可能需要幾分鐘的時間。或者,您也可以在 Google Cloud 控制台使用 Cloud Run 函式介面部署函式。

    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

    建立網頁應用程式

    建立網頁應用程式,呼叫啟動工作流程執行的 Cloud 函式。網頁會即時更新,顯示翻譯要求結果。

    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 Run 函式網址。

      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 的選項。

    3. 略過為專案設定 Google Analytics。

    4. 按一下「新增 Firebase」

    Firebase 會自動為 Firebase 專案佈建資源。程序完成後,系統會將您帶往 Firebase 控制台的專案總覽頁面。

    向 Firebase 註冊應用程式

    建立 Firebase 專案後,你可以加入網頁應用程式。

    1. 在專案總覽頁面中間,按一下「Web」圖示 (</>) 啟動設定工作流程。

    2. 輸入應用程式的暱稱。

      只有您能在 Firebase 控制台中看到這項資訊。

    3. 暫時略過設定 Firebase 託管。

    4. 按一下「Register app」,然後繼續前往主控台。

    啟用 Cloud Firestore

    網頁應用程式會使用 Cloud Firestore 接收及儲存資料。您需要啟用 Cloud Firestore。

    1. 在 Firebase 控制台的「Build」專區中,按一下「Firestore Database」

      (您可能需要先展開左側導覽窗格,才能看到「建構」部分)。

    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 前,請先在 Web 應用程式中加入 Firebase SDK。

    1. 如要在應用程式中初始化 Firebase,請提供應用程式的 Firebase 專案設定。
      1. 在 Firebase 控制台中,前往「專案設定」
      2. 在「您的應用程式」窗格中,選取您的應用程式。
      3. 在「SDK setup and configuration」(SDK 設定和設定) 窗格中,如要從 CDN 載入 Firebase SDK 程式庫,請選取「CDN」
      4. 將程式碼片段複製到 index.html 檔案的 <body> 代碼底部,並取代 XXXX 預留位置值。
    2. 安裝 Firebase JavaScript SDK。

      1. 如果您尚未建立 package.json 檔案,請從 callback-translation 目錄執行下列指令來建立檔案:

        npm init
      2. 執行下列指令,安裝 firebase npm 套件並儲存至 package.json 檔案:

        npm install firebase

    初始化及部署專案

    如要將本機專案檔案連結至 Firebase 專案,請先初始化專案。接著即可部署網頁應用程式。

    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? 提示中輸入「No」

    8. 切換至 public 目錄:

      cd public
    9. public 目錄中執行下列指令,將專案部署至網站:

      firebase deploy --only hosting

    在本機測試網頁應用程式

    Firebase 託管服務可讓您在本機查看及測試變更,並與模擬的後端專案資源互動。使用 firebase serve 時,應用程式會與代管內容和設定的模擬後端互動,但所有其他專案資源則會與實際後端互動。在本教學課程中,您可以使用 firebase serve,但進行更廣泛的測試時,不建議使用這項工具。

    1. public 目錄執行下列指令:

      firebase serve
    2. 在傳回的本機網址 (通常為 http://localhost:5000) 開啟網頁應用程式。

    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. In the Google Cloud console, go to the Manage resources page.

      Go to Manage resources

    2. In the project list, select the project that you want to delete, and then click Delete.
    3. In the dialog, type the project ID, and then click Shut down to delete the project.

    刪除教學課程資源

    1. 移除您在教學課程設定期間新增的 gcloud CLI 預設設定:

      gcloud config unset workflows/location
    2. 刪除在本教學課程中建立的工作流程:

      gcloud workflows delete WORKFLOW_NAME
    3. 刪除您在本教學課程中建立的 Cloud Run 函式:

      gcloud functions delete FUNCTION_NAME

      您也可以從 Google Cloud 控制台刪除 Cloud Run 函式。

    後續步驟