Google Apps 脚本 API 提供了可远程执行指定的 Apps 脚本函数的 scripts.run
方法。您可以在调用应用中使用此方法,远程运行某个脚本项目中的函数并接收响应。
要求
您必须满足以下要求,调用应用才能使用 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 脚本,并以函数返回值的形式获取数据。该 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 Script API 执行 Apps 脚本函数的一般流程:
第 1 步:设置通用 Cloud 项目
您的脚本和调用方应用需要共享同一个 Cloud 项目。此 Cloud 项目可以是现有项目,也可以是为此创建的新项目。有了 Cloud 项目后,您必须将脚本项目切换为使用该项目。
第 2 步:将脚本部署为 API 可执行文件
- 打开包含您要使用的函数的 Apps 脚本项目。
- 在右上角,依次点击部署 > 新建部署。
- 在打开的对话框中,依次点击“启用部署类型”
> API 可执行文件。
- 在“有权使用的人”下拉菜单中,选择允许使用 Apps Script API 调用脚本函数的用户。
- 点击部署。
第 3 步:配置发起调用的应用
发起调用的应用必须先启用 Apps Script API 并建立 OAuth 凭据,然后才能使用它。您必须拥有对 Cloud 项目的访问权限才能执行此操作。
- 配置发出调用的应用和脚本正在使用的 Cloud 项目。 为此,您可以按照以下步骤操作:
- 打开脚本项目,然后点击左侧的概览
。
- 在项目 OAuth 范围下,记录脚本需要的所有范围。
在调用方应用代码中,为 API 调用生成一个脚本 OAuth 访问令牌。这不是 API 本身使用的令牌,而是脚本在执行时需要使用的令牌。它应使用您记录的 Cloud 项目客户端 ID 和脚本范围进行构建。
Google 客户端库可以极大地帮助您构建此令牌和处理应用的 OAuth,这通常可让您改用脚本范围构建更高层级的“凭据”对象。如需查看根据范围列表构建凭据对象的示例,请参阅 Apps Script API 快速入门。
第 4 步:发出 script.run
请求
配置调用方应用后,您可以进行 scripts.run
调用。每次 API 调用都包含以下步骤:
- 使用脚本 ID、函数名称和任何必需参数构建 API 请求。
- 进行
scripts.run
调用并添加您在标头中构建的脚本 OAuth 令牌(如果使用基本POST
请求),或者使用您通过脚本范围构建的凭据对象。 - 允许脚本完成执行。脚本的执行时间最长可达 6 分钟,因此您的应用应考虑到这一点。
- 完成后,脚本函数可能会返回一个值,如果该值是受支持的类型,则 API 会将其传送给应用。
您可以在下文中找到 script.run
API 调用示例。
API 请求示例
以下示例展示了如何使用各种语言发出 Apps Script 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 脚本的对象(例如文档、Blob、日历、云端硬盘文件等)传递或返回给应用。只能传递和返回字符串、数组、对象、数字和布尔值等基本类型。
OAuth 范围。API 只能执行至少具有一个必需范围的脚本。这意味着您无法使用 API 调用不需要一项或多项服务授权的脚本。
无触发器。该 API 无法创建 Apps 脚本触发器。