Google Apps Script API 提供了一种 scripts.run
方法,用于远程执行指定的 Apps 脚本函数。您可以在调用应用中使用此方法,以远程运行脚本项目中的某个函数并接收响应。
要求
调用应用必须满足以下要求,才能使用 scripts.run
方法。您必须:
将脚本项目部署为可执行的 API。您可以根据需要部署、取消部署和重新部署项目。
为执行提供适当范围的 OAuth 令牌。 此 OAuth 令牌必须涵盖脚本使用的所有范围,而不仅仅是所调用函数使用的范围。如需查看完整的授权范围列表,请参阅方法参考。
确保脚本和调用应用的 OAuth2 客户端共享一个共同的 Google Cloud 项目。 Cloud 项目必须是标准 Cloud 项目;为 Apps 脚本项目创建的默认项目不够。您可以使用新的标准 Cloud 项目,也可以使用现有项目。
在 Cloud 项目中启用 Google Apps 脚本 API。
scripts.run
方法
scripts.run
方法需要密钥识别信息才能运行:
- 脚本项目的 ID。
- 要执行的函数的名称。
- 函数所需的参数列表(如有)。
您可以选择将脚本配置为在开发模式下执行。此模式会使用脚本项目的最新保存版本(而非最新部署版本)来执行。为此,请将请求正文中的 devMode
布尔值设置为 true
。只有脚本的所有者才能在开发模式下执行脚本。
处理形参数据类型
使用 Apps 脚本 API scripts.run
方法通常涉及将数据作为函数参数发送到 Apps 脚本,以及将数据作为函数返回值返回。该 API 只能接受和返回基本类型的值:字符串、数组、对象、数字和布尔值。这些类型与 JavaScript 中的基本类型类似。更复杂的 Apps 脚本对象(例如 Document 或 Sheet)无法通过 API 传递到脚本项目或从脚本项目传递出来。
如果您的调用应用是用强类型语言(例如 Java)编写的,则会以与这些基本类型对应的泛型对象列表或数组的形式传入参数。在许多情况下,您可以自动应用简单的类型转换。例如,如果某个函数接受数字形参,则无需额外处理,即可将 Java Double
、Integer
或 Long
对象作为形参传递给该函数。
当 API 返回函数响应时,您通常需要先将返回值强制转换为正确的类型,然后才能使用该值。以下是一些基于 Java 的示例:
- API 返回给 Java 应用的数字以
java.math.BigDecimal
对象的形式到达,可能需要根据需要转换为Doubles
或int
类型。 如果 Apps 脚本函数返回一个字符串数组,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 步:设置通用 Cloud 项目
脚本和调用应用需要共享同一 Cloud 项目。此 Cloud 项目可以是现有项目,也可以是为此目的创建的新项目。创建 Cloud 项目后,您必须将脚本项目切换为使用该项目。
第 2 步:将脚本部署为可执行的 API
- 打开包含要使用的函数的 Apps 脚本项目。
- 依次点击右上角的部署 > 新部署。
- 在随即打开的对话框中,依次点击“启用部署类型”图标
> API 可执行文件。
- 在“谁有权访问”下拉菜单中,选择允许使用 Apps 脚本 API 调用脚本函数的用户。
- 点击部署。
第 3 步:配置通话应用
调用应用必须先启用 Apps Script API 并建立 OAuth 凭据,然后才能使用该 API。您必须有权访问 Cloud 项目才能执行此操作。
- 配置调用应用和脚本所使用的 Cloud 项目。 为此,您可以按以下步骤操作:
- 打开脚本项目,然后点击左侧的概览图标
。
- 在项目 OAuth 范围下,记录脚本所需的所有范围。
在调用应用代码中,为 API 调用生成脚本 OAuth 访问令牌。这不是 API 本身使用的令牌,而是脚本在执行时需要的令牌。它应使用您记录的 Cloud 项目客户端 ID 和脚本范围构建。
Google 客户端库可极大地帮助您构建此令牌并为应用处理 OAuth,通常允许您使用脚本范围构建更高级别的“凭据”对象。如需查看如何从范围列表构建凭据对象的示例,请参阅 Apps 脚本 API 快速入门。
第 4 步:发出 script.run
请求
配置好调用应用后,您就可以进行 scripts.run
调用了。每次 API 调用都包含以下步骤:
- 使用脚本 ID、函数名称和任何必需的参数构建 API 请求。
- 进行
scripts.run
调用,并在标头中包含您构建的脚本 OAuth 令牌(如果使用基本POST
请求),否则请使用您使用脚本范围构建的凭据对象。 - 等待脚本完成执行。脚本最多可占用 6 分钟的执行时间,因此您的应用应允许此时间。
- 完成时,脚本函数可能会返回值,如果该值属于支持的类型,API 会将其返回给应用。
您可以在下方找到 script.run
API 调用的示例。
API 请求示例
以下示例展示了如何使用各种语言发出 Apps 脚本 API 执行请求,调用 Apps 脚本函数以打印出用户根目录中的文件夹列表。必须在标有 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 存在以下限制:
一个普通的 Cloud 项目。被调用的脚本和调用应用必须共享一个 Cloud 项目。Cloud 项目必须是标准 Cloud 项目;为 Apps 脚本项目创建的默认项目不够。标准 Cloud 项目可以是新项目,也可以是现有项目。
基本参数和返回类型。API 无法将特定于 Apps 脚本的对象(例如 Documents、Blobs、Calendars、Drive Files 等)传递或返回给应用。只能传递和返回字符串、数组、对象、数字和布尔值等基本类型。
OAuth 范围。该 API 只能执行至少具有一个必需范围的脚本。这意味着您无法使用该 API 调用不需要一项或多项服务授权的脚本。
无触发器。该 API 无法创建 Apps 脚本触发器。