跳转到内容

Google Forms 和 Apps Script

Sheets 插件在您点击生成时生成文档。如果想在无人点击的情况下生成文档,例如为每条表单回复生成一份证书,或在某行被勾选时生成一份报价单,可以用一小段 Apps Script 把数据发送给 AutomaDoc 文档 API,再按您的需要处理文件:保存到 Drive、通过邮件发送,或把链接写入表格。

脚本在您的 Google 账户中运行,遵循 Google 对 Apps Script 的常规规则。AutomaDoc 只能看到脚本发送的 JSON。

  1. 一个模板。 任何 AutomaDoc 模板都可以:脚本发送的 JSON 就是记录,因此不会使用模板自身的数据源。模板的占位符以脚本发送的键命名(参见名称)。
  2. 模板 ID。 在企业门户中打开模板,打开该模板的菜单,选择复制模板 ID。
  3. 一个 API 密钥。 可以是公司 API 密钥(由管理员在企业门户的 API 密钥下创建,以 amk_ 开头,只显示一次),如果是飞书多维表格模板,也可以用该模板自己的密钥(只能打开该模板)。密钥的作用范围越小越好:任何能编辑脚本的人都能看到它。

示例:为每条表单回复生成一份文档

Section titled “示例:为每条表单回复生成一份文档”

此脚本绑定到表单。对于每条回复,它会生成一份 PDF,保存到 Drive 文件夹中,并通过邮件发送给填写人。

  1. 在 Google Forms 中打开表单,打开 ⋮ 菜单,选择 Apps Script。
  2. 在 Apps Script 编辑器中打开项目设置,添加一个名为 AUTOMADOC_API_KEY 的脚本属性,值为您的密钥。这样密钥就不会出现在代码中。
  3. 用下面的脚本替换 Code.gs 的内容,并填写 TEMPLATE_ID 和 FOLDER_ID(Drive 文件夹地址中 /folders/ 之后的部分)。
  4. 在函数列表中选择 setUp,点击一次运行。Google 会请您授权脚本连接外部服务、使用您的 Drive,并以您的身份发送邮件。

从此以后,每次提交都会运行 handleSubmit。它的运行记录和错误列在编辑器的执行下。

const AUTOMADOC_API = 'https://api.automadoc.cc';
const TEMPLATE_ID = 'PASTE-THE-TEMPLATE-ID';
const FOLDER_ID = 'PASTE-THE-DRIVE-FOLDER-ID';
/** Run once: creates the trigger that calls handleSubmit on every response. */
function setUp() {
ScriptApp.newTrigger('handleSubmit')
.forForm(FormApp.getActiveForm())
.onFormSubmit()
.create();
}
function handleSubmit(e) {
const response = e.response;
// One key per question: its title is the placeholder's name.
const data = {};
response.getItemResponses().forEach(function (itemResponse) {
data[itemResponse.getItem().getTitle()] = answerOf(itemResponse.getResponse());
});
data['Submitted'] = Utilities.formatDate(
response.getTimestamp(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm');
const doc = createDocument(data);
const file = DriveApp.getFolderById(FOLDER_ID).createFile(downloadFile(doc));
console.log('Saved ' + file.getName() + ': ' + file.getUrl());
// Needs "Collect email addresses" in the form's settings.
const email = response.getRespondentEmail();
if (email) {
MailApp.sendEmail({
to: email,
subject: doc.filename,
body: 'Thank you. Your document is attached.',
attachments: [file.getBlob()],
});
}
}
/** Grid questions answer with a list of lists, which the API refuses: one text instead. */
function answerOf(value) {
if (Array.isArray(value) && value.some(Array.isArray)) {
return value.map(function (row) { return [].concat(row).join(', '); }).join('; ');
}
return value;
}
/** Fills the template and waits until the document is finished. */
function createDocument(data) {
let doc = callApi(AUTOMADOC_API + '/api/v1/templates/' + TEMPLATE_ID + '/documents?wait=20', {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({ data: data, format: 'pdf' }),
});
// Not finished within the 20 seconds: read its status every 5 seconds, for up to 4 minutes.
for (let i = 0; doc.status === 'processing' && i < 48; i++) {
Utilities.sleep(5000);
doc = callApi(doc.statusUrl);
}
if (doc.status !== 'completed') {
throw new Error(doc.error || 'The document is still being made: ' + doc.statusUrl);
}
if (doc.warnings) console.log(doc.warnings.join('\n'));
return doc;
}
/** The finished file, as a blob named like the document. */
function downloadFile(doc) {
const response = UrlFetchApp.fetch(doc.statusUrl + '/file', {
headers: { 'X-Webhook-API-Key': apiKey() },
muteHttpExceptions: true,
});
if (response.getResponseCode() !== 200) {
throw new Error('Download failed: ' + response.getContentText());
}
return response.getBlob().setName(doc.filename);
}
function callApi(url, options) {
options = options || {};
options.headers = { 'X-Webhook-API-Key': apiKey() };
options.muteHttpExceptions = true;
const response = UrlFetchApp.fetch(url, options);
const body = JSON.parse(response.getContentText());
if (response.getResponseCode() >= 400) throw new Error(body.code + ': ' + body.error);
return body;
}
function apiKey() {
return PropertiesService.getScriptProperties().getProperty('AUTOMADOC_API_KEY');
}

每次调用会发生什么:

  • POST /api/v1/templates/{templateId}/documents?wait=20 发送 { "data": …, "format": "pdf" }。带上 wait 时,文档完成后(200)或 20 秒后(202,仍为 processing)才返回响应。format 也可以是 "native"(模板本身的 Word、Excel 或 PowerPoint 格式)或 "png",filename 可为本次调用设置文件名格式。
  • GET /api/v1/documents/{generationId}(响应中的 statusUrl)读取其 status:processing、completed 或 failed,原因在 error 中。已完成的文档带有 filename 和 downloadUrl。
  • GET /api/v1/documents/{generationId}/file 使用同一个密钥返回文件本身。

如果您只需要链接,可以省略 downloadFile:doc.downloadUrl 无需密钥即可打开文件,有效期与贵公司保留生成文档的时间相同(如果文档永久保留,则为 30 天)。每次读取状态都会得到一个新的链接。

示例:在表格中勾选复选框时生成文档

Section titled “示例:在表格中勾选复选框时生成文档”

同样的辅助函数也可以在电子表格中使用。打开表格,选择扩展程序 → Apps Script,添加脚本属性,粘贴上面的辅助函数(createDocument、downloadFile、callApi、apiKey 以及三个常量),并用下面的代码替换 setUp 和 handleSubmit。

这里的工作表名为 Orders。第 1 行存放列名;名为 Generate 的复选框列用于触发生成,名为 Document 的列用于接收文档链接。

const SHEET_NAME = 'Orders';
/** Run once: creates the trigger that calls handleEdit on every edit. */
function setUp() {
ScriptApp.newTrigger('handleEdit')
.forSpreadsheet(SpreadsheetApp.getActive())
.onEdit()
.create();
}
function handleEdit(e) {
const sheet = e.range.getSheet();
// Only a single checkbox ticked on a data row of the right tab
if (sheet.getName() !== SHEET_NAME || e.range.getRow() < 2 || e.value !== 'TRUE') return;
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getDisplayValues()[0];
if (headers[e.range.getColumn() - 1] !== 'Generate') return;
const row = e.range.getRow();
const values = sheet.getRange(row, 1, 1, headers.length).getDisplayValues()[0];
const data = {};
headers.forEach(function (header, i) {
if (header && header !== 'Generate' && header !== 'Document') data[header] = values[i];
});
const doc = createDocument(data);
const file = DriveApp.getFolderById(FOLDER_ID).createFile(downloadFile(doc));
const linkColumn = headers.indexOf('Document') + 1;
if (linkColumn > 0) sheet.getRange(row, linkColumn).setValue(file.getUrl());
}

触发器必须是 setUp 创建的可安装触发器。名为 onEdit 的函数会作为简单触发器运行,而 Google 不允许简单触发器调用外部服务。

getDisplayValues() 按表格中显示的样子发送每个单元格,因此数字会以表格格式化后的文本形式到达。如果要对某列使用 number、currency 或 sum 等过滤器,请改为发送该列 getValues() 的值。

每个键都会像列名或字段名一样变成占位符名称:空格、点号和其他符号会变成 _,任何语言的字母和数字都会保留,并且区分大小写。

问题或列 占位符
Full name {Full_name}
Invoice No. {Invoice_No_}
Submitted(由脚本添加) {Submitted}

不含任何字母或数字的标题会被拒绝(DATA_INVALID_NAME),变成同一名称的两个标题也会被拒绝(DATA_NAME_COLLISION)。复选框问题的答案是一个列表,打印时以逗号连接。

以文本形式发送且不带时区的日期,例如 2026-10-17 09:30,会按模板的时区读取,因此 date 过滤器可以重新格式化它们。

图片必须是公开的网址(https:// 或 http://),并发送到模板中有占位图片的位置。通过表单问题上传的文件是表单所有者 Drive 中的私有文件:AutomaDoc 无法获取它们,它们的答案也只是文件的 ID。请不要在模板中使用这类问题。

每个模板的请求数 每分钟 60 个
每个公司的文档数 每分钟 600 份,所有自动化合计
每份文档的状态查询 每分钟 60 次;文件下载另有 60 次
wait 最多 25 秒
请求正文 1 MB
单个值的字符数 100,000

被拒绝时会返回 code 和 error,上面的脚本会停止运行,并在执行日志中记录两者。RATE_LIMITED(429)表示一分钟内调用过多:可能同时收到大量回复的表单应捕获该错误,稍后再试。API 参考列出了所有代码。

Google 会在单次脚本运行 6 分钟后将其停止,因此脚本在 4 分钟后就不再读取状态。此时仍在生成的文档不会丢失:错误中的 statusUrl 仍然可以查询。

以这种方式生成的文档属于自动生成的文档。它们先计入 Business 席位的自动化额度,再计入每月免费额度,然后从您的文档包中扣除;三者都用完后,API 会返回 QUOTA_EXCEEDED。在免费版中,除非贵公司持有购买的文档包,否则这些文档会带有“Generated with AutomaDoc”页脚。