使用 Google Apps 脚本 API 执行函数

Apps 脚本 API 提供了一个 scripts.run 方法,用于 远程执行指定的 Google Apps 脚本函数。您可以在调用应用中使用此方法,以远程运行脚本项目中的某个函数并接收响应。

要求

在调用应用可以使用 scripts.run 方法之前,您 必须:

  • 将脚本项目部署为可执行的 API。您可以根据需要部署、取消部署和重新部署项目。

  • 为执行提供适当范围的 OAuth 令牌。此 OAuth 令牌 必须涵盖脚本使用的所有 范围, 而不仅仅是被调用函数使用的范围。如需查看 授权范围 的完整列表,请参阅方法参考。

  • 确保脚本和调用应用的 OAuth2 客户端共享一个共同的 Google Cloud 项目。Cloud 项目必须是 标准 Cloud 项目; 为 Apps 脚本项目创建的默认项目 不足以满足要求。您可以使用新的标准版云项目,也可以使用现有的标准版云项目。

  • 在云项目中 **启用 Google Apps 脚本 API** 。

scripts.run 方法

The scripts.run 方法 需要以下信息:

您可以选择将脚本配置为在开发模式下执行。 此模式使用脚本项目的最新保存版本(而不是最新部署的版本)执行。 为此,请在 devMode布尔值在 请求正文 设置为 true。只有脚本的所有者才能在开发模式下执行脚本。

处理参数数据类型

使用 Apps 脚本 API scripts.run 方法 通常涉及将数据作为函数 参数发送到 Apps 脚本,并以函数返回值形式获取数据。该 API 只能接受和返回具有基本类型的值:字符串、数组、对象、数字和布尔值。API 无法将 Document或 Sheet等更复杂的 Apps 脚本对象传递到脚本项目或 从脚本项目传递。

如果您的调用应用是用 Java 等强类型语言编写的,它会以与这些基本类型对应的通用对象列表或数组的形式传入参数。在许多情况下,您可以自动应用类型转换。例如,接受数字参数的函数可以接受 Java Double、Integer 或 Long 对象作为参数,而无需进行额外处理。

当 API 返回函数响应时,您通常需要将返回值转换为正确的类型,然后才能使用它。以下是一些基于 Java 的示例:

  • API 返回给 Java 应用的数字以 java.math.BigDecimal 对象形式到达,可能需要转换为 Double 或 int 类型。
  • 如果 Apps 脚本函数返回字符串数组,a Java 应用会将响应转换为 List<String> 对象:

    List<String> mylist = (List<String>)(op.getResponse().get("result"));
    
  • 如果您想返回 Bytes 数组,请在 Apps 脚本函数中将该数组编码为 base64 字符串,然后返回该字符串:

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

以下示例代码段说明了解释 API 响应的 方法。

一般过程

如需使用 Apps 脚本 API 执行 Apps 脚本函数,请按以下步骤操作:

第 1 步:设置通用云项目

您的脚本和调用应用都需要共享同一个云项目。此云项目可以是现有项目,也可以是为此目的创建的新项目。准备好云项目后,您 必须切换脚本项目以使用该项目。

第 2 步:将脚本部署为可执行的 API

  1. 打开包含要使用的函数的 Apps 脚本项目。
  2. 依次点击右上角的部署 > 新建部署。
  3. 在打开的对话框中,依次点击启用部署类型 > 可执行的 API。
  4. 在“谁有权访问”下拉菜单中,选择允许 使用 Apps 脚本 API 调用脚本函数的用户。
  5. 点击部署 。

第 3 步:配置调用应用

调用应用必须先启用 Apps 脚本 API 并建立 OAuth 凭据,然后才能使用。您必须有权访问云项目才能执行此操作。

  1. 配置调用应用和 脚本正在使用的云项目:
    1. 在云项目中启用 Apps 脚本 API 。
    2. 配置 OAuth 权限请求页面。
    3. 创建 OAuth 凭据。
  2. 打开脚本项目,然后点击左侧的概览 。
  3. 在项目 OAuth 范围 下,记录 脚本所需的所有范围。
  4. 在调用应用代码中,为 API 调用生成脚本 OAuth 访问令牌 。这不是 API 本身使用的令牌,而是脚本在执行时需要的令牌 。它应使用 Cloud 项目客户端 ID 和您记录的脚本范围构建。

    Google 客户端库可以帮助 构建此令牌并处理应用的 OAuth,通常 允许您使用脚本 范围构建更高级别的“凭据”对象。如需查看从范围列表构建凭据对象的示例,请参阅 Apps 脚本 API 快速入门 。

第 4 步:发出 scripts.run 请求

配置调用应用后,您可以发出 scripts.run 调用:

  1. 使用部署 ID、函数名称和任何必需的参数构建 API 请求 。
  2. 发出 scripts.run 调用,并在标头中添加您构建的脚本 OAuth 令牌(如果使用 基本 POST 请求),或使用您使用脚本 范围构建的凭据对象。
  3. 允许脚本完成执行。脚本的执行时间最长可达 6 分钟,因此您的应用应允许此时间。
  4. 完成后,脚本函数可能会返回一个值,如果该值是受支持的类型,API 会将其返回给应用。

您可以在以下部分中找到 API 调用的scripts.run示例。

如需刷新访问令牌,请在 scripts.run API 请求之前添加以下代码段:

if (credential.getExpiresInSeconds() <= 360) {
  credential.refreshToken();
}

API 请求示例

以下示例展示了如何使用各种语言发出 Apps 脚本 API 执行请求,调用 Apps 脚本函数以输出用户根目录中的文件夹列表。必须在标有 ENTER_YOUR_DEPLOYMENT_ID_HERE 的位置指定包含执行函数的 Apps 脚本项目的部署 ID。这些示例 依赖于 Google API 客户端库。

目标脚本

此脚本中的函数使用 Drive API。

您必须在托管脚本的 项目中启用 Drive API。

此外,调用应用必须发送包含以下 Drive 范围的 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


import {GoogleAuth} from 'google-auth-library';
import {google} from 'googleapis';

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

  // Authenticate with Google and get an authorized client.
  // TODO (developer): Use an appropriate auth mechanism for your app.
  const auth = new GoogleAuth({
    scopes: 'https://www.googleapis.com/auth/drive',
  });

  // Create a new Apps Script API client.
  const script = google.script({version: 'v1', auth});

  const resp = await script.scripts.run({
    auth,
    requestBody: {
      // The name of the function to call in the Apps Script project.
      function: 'getFoldersUnderRoot',
    },
    scriptId,
  });

  if (resp.data.error?.details?.[0]) {
    // The API executed, but the script returned an error.
    // Extract the error details.
    const error = resp.data.error.details[0];
    console.log(`Script error message: ${error.errorMessage}`);
    console.log('Script error stacktrace:');

    if (error.scriptStackTraceElements) {
      // Log the stack trace.
      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 script executed successfully.
    // The structure of the response depends on the Apps Script function's return value.
    const folderSet = resp.data.response ?? {};
    if (Object.keys(folderSet).length === 0) {
      console.log('No folders returned!');
    } else {
      console.log('Folders under your root folder:');
      Object.keys(folderSet).forEach((id) => {
        console.log('\t%s (%s)', folderSet[id], id);
      });
    }
  }
}

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 脚本 API 具有以下限制:

  1. 通用云项目 。被调用的脚本和调用应用必须共享一个云项目。Cloud 项目必须是 标准 Cloud 项目; 为 Apps 脚本项目创建的默认项目不足以满足要求。

  2. 基本参数和返回值类型 。API 无法将 Apps 脚本专用对象(例如 文档、 Blob、 日历、 云端硬盘文件等)传递给 应用或从应用返回。只能传递和返回字符串、数组、对象、数字和布尔值等基本类型。

  3. OAuth 范围 。API 只能执行至少具有一个必需范围的脚本。这意味着,您无法使用 API 调用不需要授权一项或多项服务的脚本。

  4. 无触发器 。API 无法创建 Apps 脚本 触发器。