Google Forms 和 Apps Script
Sheets 插件在您点击生成时生成文档。如果想在无人点击的情况下生成文档,例如为每条表单回复生成一份证书,或在某行被勾选时生成一份报价单,可以用一小段 Apps Script 把数据发送给 AutomaDoc 文档 API,再按您的需要处理文件:保存到 Drive、通过邮件发送,或把链接写入表格。
脚本在您的 Google 账户中运行,遵循 Google 对 Apps Script 的常规规则。AutomaDoc 只能看到脚本发送的 JSON。
- 一个模板。 任何 AutomaDoc 模板都可以:脚本发送的 JSON 就是记录,因此不会使用模板自身的数据源。模板的占位符以脚本发送的键命名(参见名称)。
- 模板 ID。 在企业门户中打开模板,打开该模板的菜单,选择复制模板 ID。
- 一个 API 密钥。 可以是公司 API 密钥(由管理员在企业门户的 API 密钥下创建,以
amk_开头,只显示一次),如果是飞书多维表格模板,也可以用该模板自己的密钥(只能打开该模板)。密钥的作用范围越小越好:任何能编辑脚本的人都能看到它。
示例:为每条表单回复生成一份文档
Section titled “示例:为每条表单回复生成一份文档”此脚本绑定到表单。对于每条回复,它会生成一份 PDF,保存到 Drive 文件夹中,并通过邮件发送给填写人。
- 在 Google Forms 中打开表单,打开 ⋮ 菜单,选择 Apps Script。
- 在 Apps Script 编辑器中打开项目设置,添加一个名为
AUTOMADOC_API_KEY的脚本属性,值为您的密钥。这样密钥就不会出现在代码中。 - 用下面的脚本替换
Code.gs的内容,并填写TEMPLATE_ID和FOLDER_ID(Drive 文件夹地址中/folders/之后的部分)。 - 在函数列表中选择
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”页脚。