透過 Apps Script API 執行函式

Google Apps Script API 提供 scripts.run 方法,可遠端執行指定的 Apps Script 函式。您可以在呼叫應用程式中使用這個方法,透過遠端方式執行其中一個指令碼專案中的函式,並接收回應。

需求條件

呼叫應用程式必須符合下列規定,才能使用 scripts.run 方法。您必須

  • 將指令碼專案部署為 API 可執行檔。您可以視需要部署、取消部署及重新部署專案。

  • 為執行作業提供適當範圍的 OAuth 權杖。這個 OAuth 權杖必須涵蓋指令碼使用的所有範圍,而非僅涵蓋呼叫函式使用的範圍。請參閱方法參考資料中的授權範圍完整清單。

  • 請確認指令碼和呼叫應用程式的 OAuth2 用戶端共用相同的 Google Cloud 專案。Cloud 專案必須是 標準 Cloud 專案;為 Apps Script 專案建立的預設專案不夠用。您可以使用新的標準 Cloud 專案,也可以使用現有專案。

  • 在 Cloud 專案中啟用 Google Apps Script API

scripts.run 方法

scripts.run 方法需要關鍵識別資訊才能執行:

您可以選擇將指令碼設為在開發模式下執行。這個模式會使用最近儲存的指令碼專案版本執行,而不是最近部署的版本。方法是將要求主體中的 devMode 布林值設為 true。只有指令碼擁有者可以在開發模式下執行指令碼。

處理參數資料類型

使用 Apps Script API scripts.run 方法時,通常會將資料以函式參數的形式傳送至 Apps Script,並將資料以函式傳回值的形式傳回。API 只能使用及傳回基本類型的值:字串、陣列、物件、數字和布林值。這些類型與 JavaScript 中的基本類型類似。較複雜的 Apps Script 物件 (例如 DocumentSheet) 無法透過 API 傳入或傳出指令碼專案。

如果呼叫應用程式是使用 Java 等強型別語言編寫,則會將參數傳遞為與這些基本類型相對應的泛型物件清單或陣列。在許多情況下,您可以自動套用簡單的類型轉換。舉例來說,如果函式會採用數字參數,則可將 Java DoubleIntegerLong 物件做為參數,而不需要額外處理。

當 API 傳回函式回應時,您通常需要先將傳回的值轉換為正確的類型,才能使用該值。以下是一些以 Java 為基礎的範例:

  • API 傳回至 Java 應用程式的數字會以 java.math.BigDecimal 物件形式傳送,可能需要視需要轉換為 Doublesint 類型。
  • 如果 Apps Script 函式傳回字串陣列,Java 應用程式會將回應轉換為 List<String> 物件:

    List<String> mylist = (List<String>)(op.getResponse().get("result"));
    
  • 如果您想傳回 Bytes 陣列,建議您在 Apps Script 函式中將陣列編碼為 Base64 字串,然後改為傳回該字串:

    return Utilities.base64Encode(myByteArray); // returns a String.
    

以下程式碼範例說明如何解讀 API 回應。

一般程序

以下說明使用 Apps Script API 執行 Apps Script 函式的一般程序:

步驟 1:設定通用 Cloud 專案

您的指令碼和呼叫應用程式都必須共用相同的 Cloud 專案。這個 Cloud 專案可以是現有專案,也可以是為此目的建立的新專案。建立 Cloud 專案後,請務必切換指令碼專案以便使用

步驟 2:以 API 可執行檔形式部署指令碼

  1. 開啟含有要使用的函式的 Apps Script 專案。
  2. 依序按一下右上方的「部署」>「新部署作業」
  3. 在隨即開啟的對話方塊中,依序按一下「啟用部署類型」 >「API 可執行檔」
  4. 在「有權存取」下拉式選單中,選取可使用 Apps Script API 呼叫指令碼函式的使用者。
  5. 按一下「部署」

步驟 3:設定通話應用程式

呼叫應用程式必須啟用 Apps Script API,並建立 OAuth 憑證,才能使用此 API。您必須具備 Cloud 專案的存取權,才能執行這項操作。

  1. 設定呼叫應用程式和指令碼使用的 Cloud 專案。您可以按照下列步驟操作:
    1. 在 Cloud 專案中啟用 Apps Script API
    2. 設定 OAuth 同意畫面
    3. 建立 OAuth 憑證
  2. 開啟指令碼專案,然後按一下左側的「總覽」圖示
  3. 在「專案 OAuth 範圍」下方,記錄指令碼所需的所有範圍。
  4. 在呼叫應用程式程式碼中,為 API 呼叫產生指令碼 OAuth 存取權杖。這不是 API 本身使用的符記,而是指令碼執行時所需的符記。您應使用 Cloud 專案客戶端 ID 和您記錄的指令碼範圍建立此專案。

    Google 用戶端程式庫可大幅協助您建構此權杖,並為應用程式處理 OAuth,通常會讓您使用指令碼範圍建構較高層級的「憑證」物件。如要參考從範圍清單建立憑證物件的範例,請參閱 Apps Script API 快速入門

步驟 4:提出 script.run 要求

設定呼叫應用程式後,您就可以撥打 scripts.run 電話。每個 API 呼叫都包含下列步驟:

  1. 使用指令碼 ID、函式名稱和任何必要參數,建立 API 要求
  2. 請發出 scripts.run 呼叫,並在標頭中加入您建立的指令碼 OAuth 權杖 (如果使用基本 POST 要求),或使用您使用指令碼範圍建立的憑證物件。
  3. 讓指令碼完成執行。指令碼最多可執行六分鐘,因此應用程式應允許這項操作。
  4. 完成後,指令碼函式可能會傳回值,如果值是支援的類型,API 就會將該值傳回應用程式。

如需 script.run API 呼叫的示例,請參閱下方說明。

API 要求範例

以下範例說明如何以各種語言建立 Apps Script API 執行要求,並呼叫 Apps Script 函式,列印使用者根目錄中的資料夾清單。請務必在 ENTER_YOUR_SCRIPT_ID_HERE 所指示的位置,指定含有執行函式的 Apps Script 專案的腳本 ID。這些範例會依據各自的語言使用 Google API 用戶端程式庫

目標指令碼

這個指令碼中的函式會使用 Drive API。

您必須在代管指令碼的專案中啟用雲端硬碟 API

此外,呼叫應用程式必須傳送 OAuth 憑證,其中包含下列雲端硬碟範圍:

  • https://www.googleapis.com/auth/drive

此處的範例應用程式會使用 Google 用戶端程式庫,透過這個範圍建構 OAuth 憑證物件。

/**
 * Return the set of folder names contained in the user's root folder as an
 * object (with folder IDs as keys).
 * @return {Object} A set of folder names keyed by folder ID.
 */
function getFoldersUnderRoot() {
  const root = DriveApp.getRootFolder();
  const folders = root.getFolders();
  const folderSet = {};
  while (folders.hasNext()) {
    const folder = folders.next();
    folderSet[folder.getId()] = folder.getName();
  }
  return folderSet;
}

Java


/**
 * Create a HttpRequestInitializer from the given one, except set
 * the HTTP read timeout to be longer than the default (to allow
 * called scripts time to execute).
 *
 * @param {HttpRequestInitializer} requestInitializer the initializer
 *                                 to copy and adjust; typically a Credential object.
 * @return an initializer with an extended read timeout.
 */
private static HttpRequestInitializer setHttpTimeout(
    final HttpRequestInitializer requestInitializer) {
  return new HttpRequestInitializer() {
    @Override
    public void initialize(HttpRequest httpRequest) throws IOException {
      requestInitializer.initialize(httpRequest);
      // This allows the API to call (and avoid timing out on)
      // functions that take up to 6 minutes to complete (the maximum
      // allowed script run time), plus a little overhead.
      httpRequest.setReadTimeout(380000);
    }
  };
}

/**
 * Build and return an authorized Script client service.
 *
 * @param {Credential} credential an authorized Credential object
 * @return an authorized Script client service
 */
public static Script getScriptService() throws IOException {
  Credential credential = authorize();
  return new Script.Builder(
      HTTP_TRANSPORT, JSON_FACTORY, setHttpTimeout(credential))
      .setApplicationName(APPLICATION_NAME)
      .build();
}

/**
 * Interpret an error response returned by the API and return a String
 * summary.
 *
 * @param {Operation} op the Operation returning an error response
 * @return summary of error response, or null if Operation returned no
 * error
 */
public static String getScriptError(Operation op) {
  if (op.getError() == null) {
    return null;
  }

  // Extract the first (and only) set of error details and cast as a Map.
  // The values of this map are the script's 'errorMessage' and
  // 'errorType', and an array of stack trace elements (which also need to
  // be cast as Maps).
  Map<String, Object> detail = op.getError().getDetails().get(0);
  List<Map<String, Object>> stacktrace =
      (List<Map<String, Object>>) detail.get("scriptStackTraceElements");

  java.lang.StringBuilder sb =
      new StringBuilder("\nScript error message: ");
  sb.append(detail.get("errorMessage"));
  sb.append("\nScript error type: ");
  sb.append(detail.get("errorType"));

  if (stacktrace != null) {
    // There may not be a stacktrace if the script didn't start
    // executing.
    sb.append("\nScript error stacktrace:");
    for (Map<String, Object> elem : stacktrace) {
      sb.append("\n  ");
      sb.append(elem.get("function"));
      sb.append(":");
      sb.append(elem.get("lineNumber"));
    }
  }
  sb.append("\n");
  return sb.toString();
}

public static void main(String[] args) throws IOException {
  // ID of the script to call. Acquire this from the Apps Script editor,
  // under Publish > Deploy as API executable.
  String scriptId = "ENTER_YOUR_SCRIPT_ID_HERE";
  Script service = getScriptService();

  // Create an execution request object.
  ExecutionRequest request = new ExecutionRequest()
      .setFunction("getFoldersUnderRoot");

  try {
    // Make the API request.
    Operation op =
        service.scripts().run(scriptId, request).execute();

    // Print results of request.
    if (op.getError() != null) {
      // The API executed, but the script returned an error.
      System.out.println(getScriptError(op));
    } else {
      // The result provided by the API needs to be cast into
      // the correct type, based upon what types the Apps
      // Script function returns. Here, the function returns
      // an Apps Script Object with String keys and values,
      // so must be cast into a Java Map (folderSet).
      Map<String, String> folderSet =
          (Map<String, String>) (op.getResponse().get("result"));
      if (folderSet.size() == 0) {
        System.out.println("No folders returned!");
      } else {
        System.out.println("Folders under your root folder:");
        for (String id : folderSet.keySet()) {
          System.out.printf(
              "\t%s (%s)\n", folderSet.get(id), id);
        }
      }
    }
  } catch (GoogleJsonResponseException e) {
    // The API encountered a problem before the script was called.
    e.printStackTrace(System.out);
  }
}

JavaScript

/**
 * Load the API and make an API call.  Display the results on the screen.
 */
function callScriptFunction() {
  const scriptId = '<ENTER_YOUR_SCRIPT_ID_HERE>';

  // Call the Apps Script API run method
  //   'scriptId' is the URL parameter that states what script to run
  //   'resource' describes the run request body (with the function name
  //              to execute)
  try {
    gapi.client.script.scripts.run({
      'scriptId': scriptId,
      'resource': {
        'function': 'getFoldersUnderRoot',
      },
    }).then(function(resp) {
      const result = resp.result;
      if (result.error && result.error.status) {
        // The API encountered a problem before the script
        // started executing.
        appendPre('Error calling API:');
        appendPre(JSON.stringify(result, null, 2));
      } else if (result.error) {
        // The API executed, but the script returned an error.

        // Extract the first (and only) set of error details.
        // The values of this object are the script's 'errorMessage' and
        // 'errorType', and an array of stack trace elements.
        const error = result.error.details[0];
        appendPre('Script error message: ' + error.errorMessage);

        if (error.scriptStackTraceElements) {
          // There may not be a stacktrace if the script didn't start
          // executing.
          appendPre('Script error stacktrace:');
          for (let i = 0; i < error.scriptStackTraceElements.length; i++) {
            const trace = error.scriptStackTraceElements[i];
            appendPre('\t' + trace.function + ':' + trace.lineNumber);
          }
        }
      } else {
        // The structure of the result will depend upon what the Apps
        // Script function returns. Here, the function returns an Apps
        // Script Object with String keys and values, and so the result
        // is treated as a JavaScript object (folderSet).

        const folderSet = result.response.result;
        if (Object.keys(folderSet).length == 0) {
          appendPre('No folders returned!');
        } else {
          appendPre('Folders under your root folder:');
          Object.keys(folderSet).forEach(function(id) {
            appendPre('\t' + folderSet[id] + ' (' + id + ')');
          });
        }
      }
    });
  } catch (err) {
    document.getElementById('content').innerText = err.message;
    return;
  }
}

Node.js

/**
 * Call an Apps Script function to list the folders in the user's root Drive
 * folder.
 *
 */
async function callAppsScript() {
  const scriptId = '1xGOh6wCm7hlIVSVPKm0y_dL-YqetspS5DEVmMzaxd_6AAvI-_u8DSgBT';

  const {GoogleAuth} = require('google-auth-library');
  const {google} = require('googleapis');

  // Get credentials and build service
  // TODO (developer) - Use appropriate auth mechanism for your app
  const auth = new GoogleAuth({
    scopes: 'https://www.googleapis.com/auth/drive',
  });
  const script = google.script({version: 'v1', auth});

  try {
    // Make the API request. The request object is included here as 'resource'.
    const resp = await script.scripts.run({
      auth: auth,
      resource: {
        function: 'getFoldersUnderRoot',
      },
      scriptId: scriptId,
    });
    if (resp.error) {
      // The API executed, but the script returned an error.

      // Extract the first (and only) set of error details. The values of this
      // object are the script's 'errorMessage' and 'errorType', and an array
      // of stack trace elements.
      const error = resp.error.details[0];
      console.log('Script error message: ' + error.errorMessage);
      console.log('Script error stacktrace:');

      if (error.scriptStackTraceElements) {
        // There may not be a stacktrace if the script didn't start executing.
        for (let i = 0; i < error.scriptStackTraceElements.length; i++) {
          const trace = error.scriptStackTraceElements[i];
          console.log('\t%s: %s', trace.function, trace.lineNumber);
        }
      }
    } else {
      // The structure of the result will depend upon what the Apps Script
      // function returns. Here, the function returns an Apps Script Object
      // with String keys and values, and so the result is treated as a
      // Node.js object (folderSet).
      const folderSet = resp.response.result;
      if (Object.keys(folderSet).length == 0) {
        console.log('No folders returned!');
      } else {
        console.log('Folders under your root folder:');
        Object.keys(folderSet).forEach(function(id) {
          console.log('\t%s (%s)', folderSet[id], id);
        });
      }
    }
  } catch (err) {
    // TODO(developer) - Handle error
    throw err;
  }
}

Python

import google.auth
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError


def main():
  """Runs the sample."""
  # pylint: disable=maybe-no-member
  script_id = "1VFBDoJFy6yb9z7-luOwRv3fCmeNOzILPnR4QVmR0bGJ7gQ3QMPpCW-yt"

  creds, _ = google.auth.default()
  service = build("script", "v1", credentials=creds)

  # Create an execution request object.
  request = {"function": "getFoldersUnderRoot"}

  try:
    # Make the API request.
    response = service.scripts().run(scriptId=script_id, body=request).execute()
    if "error" in response:
      # The API executed, but the script returned an error.
      # Extract the first (and only) set of error details. The values of
      # this object are the script's 'errorMessage' and 'errorType', and
      # a list of stack trace elements.
      error = response["error"]["details"][0]
      print(f"Script error message: {0}.{format(error['errorMessage'])}")

      if "scriptStackTraceElements" in error:
        # There may not be a stacktrace if the script didn't start
        # executing.
        print("Script error stacktrace:")
        for trace in error["scriptStackTraceElements"]:
          print(f"\t{0}: {1}.{format(trace['function'], trace['lineNumber'])}")
    else:
      # The structure of the result depends upon what the Apps Script
      # function returns. Here, the function returns an Apps Script
      # Object with String keys and values, and so the result is
      # treated as a Python dictionary (folder_set).
      folder_set = response["response"].get("result", {})
      if not folder_set:
        print("No folders returned!")
      else:
        print("Folders under your root folder:")
        for folder_id, folder in folder_set.items():
          print(f"\t{0} ({1}).{format(folder, folder_id)}")

  except HttpError as error:
    # The API encountered a problem before the script started executing.
    print(f"An error occurred: {error}")
    print(error.content)


if __name__ == "__main__":
  main()

限制

Apps Script API 有幾項限制:

  1. 一般 Cloud 專案。被呼叫的指令碼和呼叫應用程式必須共用一個 Cloud 專案。Cloud 專案必須是標準 Cloud 專案;為 Apps Script 專案建立的預設專案不夠用。標準 Cloud 專案可以是新專案或現有專案。

  2. 基本參數和傳回類型。API 無法將 Apps Script 專屬物件 (例如 文件Blob日曆雲端硬碟檔案等) 傳遞或傳回至應用程式。您只能傳遞及傳回字串、陣列、物件、數字和布林值等基本類型。

  3. OAuth 範圍。API 只能執行至少包含一個必要範圍的指令碼。也就是說,您無法使用 API 呼叫不需要一或多項服務授權的腳本。

  4. 沒有觸發條件:API 無法建立 Apps Script 觸發條件