使用 Apps Script API 执行函数

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

要求

调用应用必须满足以下要求,才能使用 scripts.run 方法。您必须

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

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

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

  • 在 Cloud 项目中启用 Google Apps Script API

scripts.run 方法

scripts.run 方法需要密钥标识信息才能运行:

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

处理参数数据类型

使用 Apps Script API scripts.run 方法通常涉及将数据作为函数参数发送到 Apps Script,并将数据作为函数返回值接收回来。该 API 只能接受和返回基本类型的值:字符串、数组、对象、数字和布尔值。这些类型与 JavaScript 中的基本类型类似。更复杂的 Apps 脚本对象(例如 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 脚本项目。
  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. 在“Project Oauth scopes”(项目 OAuth 范围)下,记录脚本所需的所有范围。
  4. 在调用应用代码中,为 API 调用生成脚本 OAuth 访问令牌。这不是 API 本身使用的令牌,而是脚本在执行时所需的令牌。该脚本应使用 Cloud 项目客户端 ID 和您记录的脚本作用域构建。

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

第 4 步:发出 script.run 请求

配置调用应用后,您就可以进行 scripts.run 调用了。每个 API 调用都包含以下步骤:

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

您可以在下方找到 script.run API 调用示例

API 请求示例

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

目标脚本

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

您必须在托管脚本的项目中启用 Drive 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 脚本项目创建的默认项目不够用。标准 Cloud 项目可以是新项目,也可以是现有项目。

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

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

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