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 脚本对象(例如 Document 或 Sheet)无法通过 API 传入或传出脚本项目。
如果调用应用是使用 Java 等强类型语言编写的,则会将参数作为与这些基本类型对应的泛型对象的列表或数组传入。在许多情况下,您可以自动应用简单类型的转化。例如,接受数字形参的函数可以将 Java Double
、Integer
或 Long
对象作为形参,而无需额外处理。
当 API 返回函数响应时,您通常需要先将返回值转换为正确的类型,然后才能使用该值。下面是一些基于 Java 的示例:
- API 向 Java 应用返回的数字以
java.math.BigDecimal
对象的形式传入,并且可能需要根据需要转换为Doubles
或int
类型。 如果 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
- 打开包含要使用的函数的 Apps 脚本项目。
- 在右上角,依次点击部署 > 新建部署。
- 在随即打开的对话框中,依次点击“启用部署类型” > API 可执行文件。
- 在“有权访问的用户”下拉菜单中,选择允许使用 Apps Script API 调用脚本函数的用户。
- 点击部署。
第 3 步:配置调用应用
调用应用必须先启用 Apps Script API 并建立 OAuth 凭据,然后才能使用该 API。您必须有权访问相应 Cloud 项目才能执行此操作。
- 配置调用应用和脚本正在使用的 Cloud 项目。 为此,您可以按以下步骤操作:
- 打开脚本项目,然后点击左侧的概览 。
- 在“Project Oauth scopes”(项目 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 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 存在一些限制:
一个常见的 Cloud 项目。被调用的脚本和调用应用必须共用一个 Cloud 项目。Cloud 项目必须是标准 Cloud 项目;为 Apps 脚本项目创建的默认项目不够用。标准 Cloud 项目可以是新项目,也可以是现有项目。
基本参数和返回类型。该 API 无法向应用传递或返回 Apps Script 专用对象(例如文档、Blob、日历、云端硬盘文件等)。只能传递和返回字符串、数组、对象、数字和布尔值等基本类型。
OAuth 范围。该 API 只能执行至少具有一个必需作用域的脚本。这意味着,您无法使用该 API 调用不需要一项或多项服务授权的脚本。
无触发器。该 API 无法创建 Apps 脚本触发器。