Statement 目录
statements 数组的每个元素都是一个 Statement。本文整理了 25 种 Statement 的字段、行为与结果。所有取值位置都遵循值表达式的规则(引用、字面量、JsonLogic、Locale 映射),例外有三个:Regex 的 pattern、Cache 的 key 与 PdfWatermark 的 color(参见 Regex、Cache 与文件转换)。
Statement 概览
| 分类 | type | 一句话概要 |
|---|---|---|
| 资源写入 | ResourceCreate | 创建 Content/Media(可选择发布) |
ResourceUpdate | 整体替换 Content/Media 字段(未提供的 field、locale 会被删除) | |
ResourcePatch | 部分合并 Content/Media 字段(仅指定的 field、locale;字面量 null 表示删除) | |
ResourceDelete | 删除(仅 Draft、Archived;若为 Published 需先 unpublish) | |
ResourcePublish / ResourceUnpublish | 发布 / 取消发布 | |
ResourceArchive / ResourceUnarchive | 归档 / 取消归档 | |
| 资源读取 | ResourceRead | 按 id 单条查询 |
ResourceFind | 按过滤条件返回首个匹配的单条(无则为 null) | |
ResourceForEach | 在内部遍历符合过滤条件的资源,对每一项执行 onEach | |
ResourceCount | 只统计符合过滤条件的条数(不读取项目) | |
| 外部 | Http | 外部 HTTP 调用({ status, body })。可以将文件作为正文发送 |
EmailSend | 通过已注册的 EmailAccount 发送 1 封邮件 | |
| 变量 | SetVar | 声明/更新 script 作用域变量 |
| 缓存 | Cache | 读取/写入/删除仅属于该 Script 的短生命周期缓存 |
| 值解析 | ParseJson | 将 JSON 文本解析为值(对象、数组、标量)并绑定 |
| 签名与文本 | Signature | 校验收到的签名码与用密钥生成的码是否一致(Boolean) |
Hash | 计算无密钥的摘要(字符串) | |
Regex | 应用正则。匹配与否(Boolean)或捕获组(数组) | |
| 控制流 | If | 条件分支 |
Loop | 循环(foreach / while / counted) | |
Parallel | 分支并发执行 | |
Return | 返回结果并提前结束 | |
Try | 异常处理(catch/finally) |
不通过 id 指定目标而处理 Content 的 Statement,必须写明 Content Type。
ResourceFind、ResourceForEach、ResourceCount在resource为"Content"时,contentType为必填。没有横跨整个 Space 的 Content 查询。ResourceCreate同样要写明要创建的 Content Type。Media 因为整个 Space 共用一份而不划定范围,按 id 指定目标的 Statement(ResourceRead、ResourceUpdate、ResourcePatch、ResourceDelete以及发布、归档 Statement)则带有target,因此不需要范围。
循环调用最多 3 次。 在上面的资源写入 Statement(
ResourceCreate、ResourceUpdate、ResourcePublish等)上开启propagateEvents(默认为关闭)后,它们会触发变更事件,而这些事件可能通过 Webhook 再次运行 Script。这样接续下去的链条(Script → 事件 → Webhook → Script → …)最多只延续 3 次,超过后会自动中断,以防止无限循环。
公共字段
{ "type": "<StatementType>", "name": "<可选,script 内唯一>", /* ...各类型专属字段... */ }type:判别符。取上表中的某个值(必填)。name:可选。指定后,结果会以/<name>绑定到上下文,后续 Statement 可通过{ /<name>/... }引用。若不使用结果则可省略。- 绑定名称规则:
name是直接叠加到上下文根上的键,因此在保存时会被校验。它只能使用英文字母、数字、_、-(须可用作 JSON Pointer 键,因此其他字符以及空名称都会被拒绝),不能与保留根(payload、rawPayload、headers、vars、error、now、secrets)相同,且在同一个 Script 内必须唯一。格式违规、使用保留字或重复时,保存会被拒绝。
实体引用形态
contentType、target 等实体引用统一为 { "sys": { "id": <值表达式> } } 一种形态。只需 sys.id,目标类型由 resource 推断(sys.type 与 sys.targetType 均省略)。
contentType.sys.id通常是字面量(例如"ct_post")。target.sys.id通常是{ /ptr }值表达式(运行时 resolve,例如{ /payload/sys/id })。
resource
资源类 Statement 通过 resource: "Content" | "ContentType" | "Media" | "ServiceUser" 指定目标种类。
Content Type 只被 ResourceCount 接受。写在其他 Statement 里则保存会被拒绝。创建或修改模板本身的工作不归 Script,而是 CMA 的职责。
接受 ServiceUser(在产品上注册的会员)的 Statement,是读取的三种 Statement(ResourceRead、ResourceFind、ResourceForEach)与 ResourceUpdate、ResourcePatch。后两者能够写入的字段只有 roleOverride 与 enableLogin 这两个。写在其余的写入 Statement 里则保存会被拒绝(参见错误)。规则见处理会员目录。
资源写入
所有写入 Statement 都带有 propagateEvents(默认 false),但写入 ServiceUser 的 ResourceUpdate、ResourcePatch 不接受这个字段。设为 true 时,该写入会触发变更事件,从而执行 Webhook 这类后续动作。默认不触发(静默的系统写入)。
ResourceCreate
创建 Content 或 Media。Content 与 Media 共享 fields 模型,值为 Locale 映射。
| 字段 | 目标 | 说明 |
|---|---|---|
resource | 通用 | "Content" 或 "Media"(必填) |
contentType | Content | 要创建的 Content Type({ sys: { id } })。resource 为 Content 时必填 |
fields | 通用 | 字段映射 { "<field>": { "<locale>": 值 } }。每个填充的字段都必须包含默认 Locale 桶。Content 的键遵循 Content Type 定义,Media 的键是固定的(title、description、file) |
locale | 通用 | (便捷项)提供后会将 fields 的各个值自动包装为 { <locale>: 值 } |
publish | 通用 | 写入后发布(在 CDA/ACDA 上可见)。默认 true |
Mediafile:fields.file.{locale}的值是摄取指令{ "source": <值表达式>, "encoding": "url"|"base64", "transformations": [ … ] }。source与encoding为必填,transformations只在需要于保存前修改所接收的文件时填写(文件转换)。在包含文件的写入中由引擎执行摄取(url则下载,base64则先解码,然后上传并处理)。这项摄取没有可声明的时间,因此从 30 秒基础预算中支出(时间预算),并且不计入外部调用限额。也可以创建不含文件(fileless)的 Media。若publish:true但没有文件或处理未完成,则会在发布阶段报错;若publish:false则保持Draft。- 结果(
name绑定):创建出的资源。{ /<name>/sys/id }、{ /<name>/fields/<field>/<locale> }。
// Content
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
"fields": { "title": { "en-US": "{ /payload/fields/title }" } }, "publish": true, "name": "post" }
// Media。file 是摄取指令
{ "type": "ResourceCreate", "resource": "Media",
"fields": {
"title": { "en-US": "{ /payload/fields/prompt }" },
"file": { "en-US": { "source": "{ /gen/body/data/0/url }", "encoding": "url" } }
}, "name": "img" }文件转换(transformations)
销售电子书时,有时希望在 PDF 的每一页上印上买家的邮箱,再把副本交给对方。在摄取指令中写入 transformations,就可以在保存之前修改所接收的文件。支持的转换只有 PdfWatermark 一种。
| 摄取指令字段 | 必填 | 说明 |
|---|---|---|
transformations | 可选 | 转换对象的数组。省略或为空数组时,所接收的文件不做修改,直接放入 Media。不是数组时,Script 保存会被拒绝 |
- 按数组顺序应用。 前一个转换的结果成为下一个转换的输入,最后的结果成为 Media 的文件。
ResourceCreate、ResourceUpdate、ResourcePatch的摄取指令中都可以写入。source所指向的原始文件不会被修改。 若要在 Space 中已有的文件上印文字,先用ResourceRead读取该 Media,再把{ /<name>/fields/file/<locale>/url }放入source,创建新的 Media。- 转换也是摄取的一部分。 所耗费的时间从 30 秒基础预算中支出(时间预算)。
PdfWatermark:在 PDF 的每一页上印上文字
| 字段 | 必填 | 说明 |
|---|---|---|
type | 必填 | "PdfWatermark"。为其他值时,Script 保存会被拒绝 |
text | 必填 | 要印的文字。为字符串,可以混用 { /pointer } 引用。不接受 JsonLogic 运算对象。必须是由可用字符组成的单行文字,长度为 1~64 个字符 |
color | 可选 | 文字颜色。写成 #RRGGBB(6 位十六进制),省略时为 #808080(灰色)。只接受字面量,不接受值表达式;格式不符时,Script 保存会被拒绝 |
- 表中没有的字段会被忽略。无法指定文字大小、位置与透明度。
- 印在每一页上。 一行文字沿着穿过页面中心的对角线(从左下到右上)放置。
- 以 30% 的不透明度叠印在正文之上。 文字下方的原有内容仍然可以正常阅读。
- 文字大小按每一页自动确定。 以文字占对角线长度 80% 的大小印出,但不超过页面短边的 15%。短文字印得大,长文字印得小,都不会超出页面。
- 字体是 PDF 标准字体 Helvetica。
- 是否为 PDF 根据文件内容判断,而不是根据文件名或扩展名。
// statements 数组的一部分。在所购电子书 PDF 上印上买家邮箱与订单号,生成新的 Media
{ "type": "ResourceRead", "resource": "Media",
"target": { "sys": { "id": "{ /payload/fields/ebookMediaId }" } }, "name": "ebook" },
{ "type": "ResourceCreate", "resource": "Media", "locale": "en-US", "publish": false,
"fields": {
"title": "{ /ebook/fields/title/en-US }",
"file": {
"source": "{ /ebook/fields/file/en-US/url }", "encoding": "url",
"transformations": [
{ "type": "PdfWatermark", "text": "{ /payload/fields/email } · { /payload/fields/orderId }", "color": "#B22222" }
]
}
}, "name": "stamped" }这是为某一位买家准备的副本,因此将 publish 设为 false,使其不会通过 CDA/ACDA 传递。若省略,则按默认值 true 发布。
PdfWatermark 文字的限制
text 中只能使用 PDF 标准字体能够绘制的字符(WinAnsi 字符集)。
| 类别 | 字符 |
|---|---|
| ASCII 可打印字符 | 空格、英文大小写字母、数字,以及 @ . # { } 等基本符号 |
| Latin-1 字符 | é ü ñ ß · © ° 等西欧字符与符号。不包括不换行空格(U+00A0)与软连字符(U+00AD) |
| 附加符号 | € ‚ ƒ „ … † ‡ ˆ ‰ Š ‹ Œ Ž ‘ ’ “ ” • ˜ ™ š › œ ž Ÿ,以及两种破折号(U+2013、U+2014) |
除此之外的字符都不能使用。表中没有的拉丁扩展字符(如 ł、ő、ș)、非拉丁字母的文字、表情符号,以及换行符与制表符都属于此类。
保存 Script 时,会按原样检查所写的 text。含有 { /pointer } 的 text,其解析后的值要到执行时才能知道,因此会在执行时再次检查解析后的值。保存时出现的错误码见错误。
| 限制 | 字面量 text | 含有 { /pointer } 的 text |
|---|---|---|
| 由可用字符组成的一行 | Script 保存被拒绝 | 保存时检查所写的整个字符串(包括指针路径),Script 保存被拒绝。解析后的值在执行时再次检查,以 status 400 失败 |
| 1~64 个字符 | Script 保存被拒绝 | 保存时不检查。执行时按解析后的值的长度检查,以 status 400 失败 |
- 请计算好固定文字的长度再编写。
"Purchased by { /payload/fields/email } · Order { /payload/fields/orderId }"中模板之外的固定文字为 22 个字符,因此留给邮箱与订单号的长度合计为 42 个字符。若传入比这更长的实际值,Script 能够保存,但该次执行会失败。 - 请印标识符,而不是姓名。 含有表中没有的字符的买家姓名会在执行时失败。由 ASCII 组成的邮箱或订单号则可以印出。
文件转换失败的情况
以下情况下该 Statement 会失败。与其他运行时失败一样,可以用 Try/catch 处理。转换发生在写入 Media 之前,因此失败时不会创建新的 Media,ResourceUpdate、ResourcePatch 的目标也不会被修改。
| 情况 | 结果 |
|---|---|
source 的文件不是 PDF | 以 status 422 失败 |
| 无法读取 PDF,或 PDF 已加密(包括打开时不要求输入密码、仅限制了权限的 PDF) | 以 status 422 失败 |
解析后的 text 为空或只有空白 | 以 status 400 失败 |
解析后的 text 超出 1~64 个字符的范围,或含有不可用的字符 | 以 status 400 失败 |
ResourceUpdate
对目标 Content、Media、ServiceUser 的字段进行整体替换(PUT)。fields 中给出的内容会原样成为新字段,这里未列出的 field 与 locale 会被清除。若只想改动一部分,请使用 ResourcePatch。
| 字段 | 说明 |
|---|---|
resource | "Content"、"Media"、"ServiceUser"。ServiceUser 上可写的字段与约束见处理会员目录 |
target | 目标({ sys: { id } },必填)。id 通常为 { /ptr } |
fields | 写入的全部字段。值为 Locale 映射。因为是整体替换,这里未列出的 field 与 locale 会被移除。Media file 是摄取指令(参见上文 ResourceCreate)。列出的文件总是重新摄取,未提供的 locale 文件会被删除 |
locale | (便捷项)自动包装 fields |
version | (可选)值表达式(Int)。乐观锁。提供后,仅当与目标当前的 sys.version 一致时才更新;不一致则以版本冲突错误 abort(可用 Try catch)。省略时不做检查(last-write-wins)。ServiceUser 不接受该字段 |
publish | 更新后 republish。默认 true |
在 Media 上若仅为修改 metadata 而使用 Update,会因缺少 file 而导致文件全部被删除(因为是整体替换)。部分变更请务必使用 ResourcePatch。
{ "type": "ResourceUpdate", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
"fields": { "title": { "en-US": "Hello", "ko-KR": "안녕" }, "status": { "en-US": "published" } } }ResourcePatch
对目标 Content、Media、ServiceUser 的字段进行部分合并(PATCH)。只覆盖 fields 中给出的字段(及其中的 Locale),而未提及的字段与 Locale 保持不变。值形态、locale、version、publish 与 ResourceUpdate 相同。
| 字段 | 说明 |
|---|---|
resource | "Content"、"Media"、"ServiceUser"。ServiceUser 上可写的字段与约束见处理会员目录 |
target | 目标({ sys: { id } },必填)。id 通常为 { /ptr } |
fields | 要覆盖的字段。值为 Locale 映射。仅更新指定的字段与 Locale 桶(其余保持不变)。若值为字面量 null,则删除该 (field, locale)。Media file 是摄取指令(参见上文 ResourceCreate) |
locale | (便捷项)自动包装 fields |
version | (可选)与 ResourceUpdate 相同(乐观锁)。ServiceUser 不接受该字段 |
publish | 更新后 republish。默认 true |
- 删除特定 Locale 或文件:将值设为字面量
null。例如"title": { "fr-FR": null }(删除 fr-FR 标题)、"file": { "en-US": null }(删除 en-US 文件)。值表达式在运行时求值为 null 并不会删除,而是报错(只有字面量 null 才会删除)。 - 若对 Media
file给出摄取指令,则会替换该 locale 的文件。不提供文件则保持不变。
// 仅将 viewCount(en-US) +1。title、其他 locale 等其余内容保持不变
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
"fields": { "viewCount": { "en-US": { "$+": [ "{ /payload/fields/viewCount }", 1 ] } } } }ResourceDelete
删除目标。仅 Draft 与 Archived 状态可以删除。若为 Published 或 Changed 会被拒绝,因此必须先 ResourceUnpublish。Media 在文件处理过程中会被拒绝删除。不会 auto-unpublish(与 CMA/ACMA 相同)。
| 字段 | 说明 |
|---|---|
resource | "Content" 或 "Media" |
target | 目标({ sys: { id } },必填) |
{ "type": "ResourceDelete", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } }ResourcePublish, ResourceUnpublish, ResourceArchive, ResourceUnarchive
独立控制目标的发布与归档状态。四者的字段完全相同。各操作的 status 前提条件与 CMA/ACMA 相同。ResourcePublish 不能在 Archived 状态下执行,并且文件处理必须已经完成。ResourceUnpublish 只能在 Published、Changed 状态下执行,ResourceArchive 只能在 Draft 状态下执行,ResourceUnarchive 只能在 Archived 状态下执行。
| 字段 | 说明 |
|---|---|
resource | "Content" 或 "Media" |
target | 目标({ sys: { id } },必填) |
version | (可选)值表达式(Int)。乐观锁。提供后仅当与当前 sys.version 一致时才执行 |
{ "type": "ResourcePublish", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } }
{ "type": "ResourceUnpublish", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } }
{ "type": "ResourceArchive", "resource": "Media", "target": { "sys": { "id": "{ /m/sys/id }" } } }资源读取
ResourceRead、ResourceFind 读取资源并绑定为值,ResourceCount 则只统计条数。三者都不改变状态(没有 propagateEvents)。ResourceForEach 的查询本身也是读取,但若在 onEach 中放入资源写入 Statement,则每一项都会执行该写入,从而改变状态。
这四种 Statement(ResourceRead、ResourceFind、ResourceForEach、ResourceCount)都通过 from(默认 Current)来决定读取哪一份存储版本。Current 是内容工作室所看到的最新草稿(CMA/ACMA 读取的值),Published 是发布快照(CDA/ACDA 所传递的、最后一次发布时刻的值)。ServiceUser 不是会被发布的资源,因此只接受 Current(参见处理会员目录)。
ResourceFind、ResourceForEach、ResourceCount 在此基础上还可以通过 advanced(默认 true)开启或关闭高级搜索(Advanced Search)。不写时即为开启。 它仅限 Content,因此在 Media、ServiceUser 读取中会被忽略。开启时,where 可使用 regex、near、within 运算符与全文检索(对已开启全文检索的 LongText 字段,eq 还能通过部分、近似匹配找到包含该值的项目),且可按 fields.* 排序。关闭时,这三个运算符会被拒绝,文本的 eq 为精确匹配,而 prefix 及比较、列表运算符与高级搜索无关。刚刚创建或修改的项目,需要短暂片刻(约 1 秒)才会反映到高级搜索中,因此紧接着的下一次高级搜索查询可能查不到它。由于默认是开启的,只要不把 advanced 设为 false,这个延迟就适用于所有查询。若需立即读取刚写入的项目,请按 id 使用 ResourceRead(基本存储版本,无反映延迟),或用写入返回的 sys.id 查询。
where 的 createdBy: ":self" 表示“仅限当前调用者创建的资源”。但在允许匿名调用的 Script(anonymousCallEnabled)中不能使用它。 此时 :self 不会 resolve 为调用者,而是 resolve 为作者,等于静默地开放了作者的资源,因此这样的定义在保存时会被拒绝(参见匿名调用)。
在 where 和 order 中,内容字段要写成 fields.<field>(仅凭字段名本身无法识别)。fields.<field> 的 Space 默认 Locale 会被自动应用,因此无需直接附加 Locale。下方示例中的 fields.status、fields.slug 就是默认 Locale 查询。只有在需要查询特定(非默认)Locale 时,才用 fields.<field>.<locale>(例如 fields.title.ko-KR)明确指定。sys.*(如 sys.createdAt)与 createdBy(:self)则不加 fields.,直接书写。详细规则见值表达式的 where 与 order 中的 Locale。
处理会员目录 (ServiceUser)
ResourceRead、ResourceFind、ResourceForEach 可以在 resource 中接受 "ServiceUser",从而读取该 Space 的会员目录(ResourceCount 不接受,参见下方 ResourceCount)。它适用于确认某笔订单的主人是谁,或者用邮箱找到会员、把其 sys.id 传给下一条 Statement 这类流程。以下规则适用于处理会员目录的五种 Statement。
- 可以修改的字段只有两个。
ResourceUpdate、ResourcePatch接受"ServiceUser",可以改动roleOverride与enableLogin。在ResourcePatch中把roleOverride设为null,则会解除已指定的角色。ResourceCreate、ResourceDelete以及发布、归档 Statement 都不接受"ServiceUser",这样的定义会在保存时被拒绝。这不是加上权限就能打开的事情,而是 Script 无法创建或删除会员,因此它不是权限错误,而是作为写错的 Statement 被拒绝。 - 会员没有
version与propagateEvents。 写入其中任何一个都会导致执行失败。ResourceUpdate会整体替换可写的字段,因此enableLogin为必填,而且不写roleOverride就会解除已指定的角色。若只想改动其中一个,请使用ResourcePatch,此时这两个字段中至少要写一个。 - 作者必须拥有会员目录权限才能保存。 它不像 Content、Media 那样用权限映射来校验,而是看作者的 SpaceRole
settings中是否有SETTING_SERVICE_LOGIN(或SETTING_ALL)。因为会员目录在其他所有路径上也都是由 Space 设置来管辖的资源。若没有,保存会被拒绝(参见安全模型)。 from只接受Current。会员不是会被发布的资源,因此给出Published会导致执行失败。contentType与advanced会被忽略。 会员目录不按 Content Type 划分(整个 Space 共用一份),高级搜索也仅限 Content。where的sys.email只接受精确匹配类运算符(eq、ne、in、nin)。会员的地址是加密存储的,因此顺序比较或prefix没有意义。给出其他运算符时不会静默返回 0 条,而是执行失败。- 结果就是 ServiceUser 资源本身。可以像
{ /<name>/sys/id }、{ /<name>/nickname }这样引用。其结构见 ServiceUser 参考。给找到的会员发邮件时,不要把地址取出来,而是把该sys.id传给EmailSend的toServiceUser(引擎会在发送前才 resolve 地址,因此会员的地址不会进入 Script 变量空间)。
// 用邮箱查找一位会员。若无则为 null
{ "type": "ResourceFind", "resource": "ServiceUser",
"where": { "sys.email": { "eq": "{ /payload/fields/email }" } }, "name": "member" }ResourceRead
按 id 单条查询(get-by-id)。结果会将整个资源绑定到名称上。
| 字段 | 说明 |
|---|---|
resource | "Content"、"Media"、"ServiceUser" |
target | 目标({ sys: { id } })。id 为值表达式 |
from | (可选)Current(默认,最新草稿)或 Published(发布快照)。ServiceUser 只能是 Current |
- 结果:绑定的就是资源本身。指定了
name后,可通过{ /<name>/sys/id }、{ /<name>/fields/<field>/<locale> }直接引用(下面示例中"name": "order",即{ /order/sys/id })。它不是列表,因此不经过数组索引。 - 目标不存在时报错。可用
Try包裹处理。
{ "type": "ResourceRead", "resource": "Content",
"target": { "sys": { "id": "{ /payload/fields/orderId }" } }, "name": "order" }ResourceFind
读取按过滤条件匹配的首个单条。若无则为 null。用于以唯一业务键(slug、email、sku)查找单条记录时。
| 字段 | 说明 |
|---|---|
resource | "Content"、"Media"、"ServiceUser" |
contentType | 检索范围 Content Type({ sys: { id } })。Content 时必填。在 Media、ServiceUser 中被忽略 |
where | 过滤条件({ "<field>": { "<op>": <值> } })。可用运算符遵循运算符列表(regex/near/within 需要 advanced)。支持 createdBy: ":self"。ServiceUser 的 sys.email 只能用 eq、ne、in、nin(参见处理会员目录) |
order | 在多个匹配时决定“第一个”的排序(例如 "-sys.createdAt") |
from | (可选)Current(默认,最新草稿)或 Published(发布快照)。ServiceUser 只能是 Current |
advanced | (可选)通过高级搜索(Advanced Search)执行。仅限 Content(Media、ServiceUser 忽略)。默认 true。参见上文 资源读取 说明 |
- 结果:将首个匹配的资源绑定到
name上。可通过{ /<name>/fields/<field>/<locale> }直接引用。若无则为null,因此可用{ "==": [ "{ /<name> }", null ] }判断是否存在来分支(find-then-upsert 的典型做法)。
{ "type": "ResourceFind", "resource": "Content", "contentType": { "sys": { "id": "ct_article" } },
"where": { "fields.slug": { "eq": "{ /payload/fields/slug }" } }, "name": "found" }ResourceForEach
在内部遍历符合过滤条件的资源,对每一项执行 onEach。它不是用来构造一个作为值使用的集合,而是用来对每一项执行操作的 Statement。适用于批量发布草稿、批量修改符合条件的 Content、把每一项向外发送、同步等反复性工作。只读取一条时,请使用 ResourceRead(id)或 ResourceFind(过滤条件)。
| 字段 | 说明 |
|---|---|
resource | "Content"、"Media"、"ServiceUser"(必填) |
contentType | 遍历范围 Content Type({ sys: { id } })。Content 时必填。在 Media、ServiceUser 中被忽略 |
where | 过滤条件({ "<field>": { "<op>": <值> } })。语义与 ResourceFind 的 where 相同(ServiceUser 的 sys.email 约束也一样)。可用运算符遵循运算符列表(regex/near/within 需要 advanced)。支持 createdBy: ":self" |
order | 排序(例如 "sys.createdAt,sys.id")。缺省时为平台默认顺序 |
from | Current(默认,最新草稿)或 Published(发布快照)。ServiceUser 只能是 Current |
advanced | 通过高级搜索(Advanced Search)遍历。仅限 Content(Media、ServiceUser 忽略)。默认 true。参见上文 资源读取 说明 |
limit | (可选,1 以上)处理总数上限(不是页大小)。缺省时遍历到平台上限(10,000 条)为止 |
name | (可选)用于绑定当前项的名称。每次迭代都会重新绑定,可在 onEach 内通过 { /<name> } 引用(与 Loop 的 name 生命周期相同;遍历结束后最后一项仍保留绑定)。若不引用项目则省略 |
onEach | 对每一项执行的子 Statement 数组(必填) |
- 不绑定集合(是
foreach而非map)。 既没有{ items, next }也没有 cursor。它不是把遍历结果作为值返回,而是对每一项执行onEach。若需要列表,请用SetVar自行收集。只需要条数时,请使用ResourceCount。 - 即使没有
limit也不是无限遍历。 缺省时遍历到平台上限(10,000 条)为止,若在仍有剩余匹配时触及该上限则失败(以免在留有未处理项的情况下报告成功)。反之,到达声明的limit是有意的停止,属于正常结束。超过上限的limit在保存时会被拒绝。 - 没有 cursor。 跑完即成功,中途中断(超出挂钟时间、配额,或
onEach出现未处理的失败)即失败,错误会指明在哪一项、因何失败。恢复由作者用自己的数据表达(把where设为“未处理”,并在onEach末尾标记完成,则重跑时会从剩余项继续)。 - 在时间预算中按乘法计算。 这条 Statement 所声明的时间,是
onEach所声明时间乘以处理项目数(limit,未写时为 10,000)的结果(时间预算)。作为拥有子项的复合 Statement,它自身不计入外部调用 leaf 预算,被计入的是onEach内的外部调用 Statement。 onEach与其他 Statement 一样,可以放入外部调用(Http、EmailSend)或 Media 文件摄取(与Loop的body相同)。把资源查询结果对每一项处理一次,正是该 Statement 存在的理由。
// 找出所有 draft 状态的帖子,对每一项进行发布
{ "type": "ResourceForEach", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
"where": { "fields.status": { "eq": "draft" } }, "order": "sys.createdAt,sys.id",
"from": "Current", "advanced": false, "name": "post",
"onEach": [
{ "type": "ResourcePublish", "resource": "Content", "target": { "sys": { "id": "{ /post/sys/id }" } } }
] }ResourceCount
只统计符合过滤条件的条数。 它不会读取项目,因此适用于需要的是数量而不是列表的场合。确认剩余库存、判断相同的值是否已经存在、检查是否超过了限额,都用在这里。
| 字段 | 说明 |
|---|---|
resource | "Content" 或 "ContentType"(必填)。Media、ServiceUser 无法统计,这样写则保存会被拒绝 |
contentType | 统计范围 Content Type({ sys: { id } })。Content 时必填。统计 Content Type 时会被忽略(整个 Space 共用一份) |
where | 过滤条件。语义与 ResourceFind 的 where 相同。匹配到的项目会全部计入 |
from | (可选)Current(默认,最新草稿)或 Published(发布快照) |
advanced | (可选)通过高级搜索(Advanced Search)执行。仅限 Content(统计 Content Type 时忽略)。默认 true。参见上文 资源读取 说明 |
name | (可选)用于绑定条数的名称 |
- 结果:将匹配到的条数绑定到
name上。通过{ /<name> }引用,用于比较与分支。 - 不返回项目。 若需要项目,请使用
ResourceFind(首个匹配的单条)或ResourceForEach(对每一项执行)。 - 不要为了取得条数而用
ResourceForEach遍历计数。 遍历会把时间预算按项目数相乘来占用(时间预算),而且在仍有剩余匹配时触及平台上限就会失败。只需要计数时,这条 Statement 一次就能完成。 - 没有
order与limit。统计不需要顺序,而且匹配到的都会被计入。
// 统计这篇帖子下有多少条评论
{ "type": "ResourceCount", "resource": "Content", "contentType": { "sys": { "id": "ct_comment" } },
"where": { "fields.postId": { "eq": "{ /payload/sys/id }" } }, "name": "commentCount" }外部
Http
调用外部 HTTP。它是外部调用,因此会计入各套餐的外部调用限额。时间预算中会加上 timeoutMs,发送文件时还会按每个文件一并加上下载时间。指定了 retry 时,该合计会再乘以尝试次数(1 + retry)。
| 字段 | 说明 |
|---|---|
method | "GET"、"POST"、"PUT"、"PATCH"、"DELETE" |
url | 目标 URL(值表达式,可插入 { /ptr }) |
headers | [{ "key", "value" }]。value 为值表达式。API 密钥这类机密值不要直接写入,而是在 secrets 中声明后以 { /secrets/<key> } 读取(例如 "Bearer { /secrets/LLM_API_KEY }")。在此放入 Content-Type 时,body 会按该格式序列化(见下文) |
body | 请求 body(值表达式或 JSON)。以何种格式发出,由 Content-Type 请求头决定 |
urlBody | 作为请求 body 发送的文件的 URL(值表达式)。平台会从该 URL 获取文件,并原样发送其字节(将文件作为正文发送) |
multipartBody | 以 multipart/form-data 发送的 part 数组(1 个以上)。可以混合文本 part 与文件 part(将文件作为正文发送) |
timeoutMs | 本次调用的超时时间(ms) |
retry | 响应 status 大于等于 400 时的重试次数。默认 0,上限为 2 |
ignoreStatusCode | (重试结束后的)最终 status 大于等于 400 时是否将本次调用视为失败。默认 false 时会按失败处理,成为 Try/catch 的对象。为 true 时则不视为失败,而是将 { status, body } 原样绑定(由调用方自行按 status 分支) |
responseType | 以何种形式接收响应正文。"Json"(默认)会解析为对象或数组,"Text" 则按字符串接收 |
body、urlBody、multipartBody三者只能使用其一。 写了两个以上时,保存会被拒绝。三者都没有时,不带正文发送。- 结果:
{ status, body }。指定了name后,可用{ /<name>/status }、{ /<name>/body/... }。body的形态由responseType决定。 responseType只作用于成功响应。 status 大于等于 400 的响应正文,无论声明为何都会作为诊断信息绑定(是 JSON 时为解析后的值,否则为字符串)。- 声明为
"Json"但正文不是 JSON 时,本次调用会失败(Try/catch的对象)。不返回 JSON 的 API 请用"Text"接收,需要当作值使用时再用ParseJson解析。 "Text"按响应Content-Type的 charset 解码,没有 charset 时视为 UTF-8。正文为空时,两种情况下body都是null。- 响应大小上限:响应 body 最大为 10MiB。超过时本次调用会以异常失败,可像其他运行时失败一样用
Try/catch处理(这是基于大小的失败,因此不会被ignoreStatusCode忽略)。
{ "type": "Http", "method": "POST", "url": "https://api.llm.com/v1/gen",
"headers": [ { "key": "Authorization", "value": "Bearer { /secrets/LLM_API_KEY }" } ],
"body": { "prompt": "{ /payload/fields/prompt }" }, "timeoutMs": 15000, "retry": 1,
"responseType": "Json", "name": "resp" }body 以何种格式发出
放入 headers 的 Content-Type 决定 body 的序列化格式。比较时忽略大小写以及 ;charset=… 这类参数,只看前面的部分。没有该请求头或其值为空时,按 application/json 发送。该请求头只在存在 body 时才附加,因此没有 body 时,填写的请求头会原样发出。同一个键放入多次时只采用第一个值,并合并为一个。
无法以所声明格式承载的 body,会更正为能够承载的格式后发送。 请求头与实际 body 说法不一致的情况不会发生。
以下是按声明值原样发出的组合。
声明的 Content-Type | body 形态 | 发出的 body |
|---|---|---|
application/json | 任何形态 | JSON |
application/x-www-form-urlencoded | 对象、数组 | order[id]=A-2481&order[amount]=34000 |
text/plain | 标量 | 值原样 |
其他(text/xml 等) | 任何形态 | JSON |
以下是因无法以所声明格式承载而被更正的组合。
声明的 Content-Type | body 形态 | 实际发出的 Content-Type | 发出的 body |
|---|---|---|---|
application/x-www-form-urlencoded | 标量 | text/plain;charset=UTF-8 | 值原样 |
text/plain | 对象、数组 | application/json | JSON |
这两行说明的是搭配错位时请求会以何种形式发出,而不是取得预期格式的方法。body 由值表达式组装时,可能会随执行时的 payload 而变成标量,此时这种更正会静默发生,不会报错。若接收方不接受该格式,请按意图修改 body 的形态或 Content-Type 中的一处。
form-urlencoded 会把对象展开为方括号键,把数组展开为索引。
body | 展开后的键与值 |
|---|---|
{ "order": { "id": "A-2481", "amount": 34000 } } | order[id]=A-2481&order[amount]=34000 |
{ "tags": ["outerwear", "winter"] } | tags[0]=outerwear&tags[1]=winter |
{ "items": [{ "sku": "TUMBLER-500" }] } | items[0][sku]=TUMBLER-500 |
{ "memo": null } | memo= |
键与值会以 UTF-8 做百分号编码后发出。上表为展示键的结构而采用了解码后的形态。即使值中含有 & 或 +,也不会被误认为键值对分隔符或空格,而是原样传递。
把嵌套展开为方括号键的写法是广泛使用的惯例,并不是该格式本身的规范。 请确认接收方是否会把 order[id] 还原为嵌套对象;若不会还原,请用扁平的键来构建 body。
{ "type": "Http", "method": "POST", "url": "https://api.example.com/oauth/token",
"headers": [ { "key": "Content-Type", "value": "application/x-www-form-urlencoded" } ],
"body": { "grant_type": "client_credentials", "client_id": "{ /vars/clientId }" },
"name": "token" }将文件作为正文发送
有时需要把上传到 Media 的文件转交给外部 API。文件的字节无法放进值表达式。取而代之,只要指定文件所在的 URL,平台就会从该 URL 获取文件,并作为请求正文发送。要把一个文件作为整个正文发送,请使用 urlBody;要把文件与文本字段一起作为表单发送,请使用 multipartBody。
urlBody:把一个文件作为整个正文发送
把从指定 URL 获取的文件不经转换、原样作为请求正文发送。它适合以 PUT 把文件上传到上传 URL 的 API。下面是读取合同 PDF(Media)并发送到电子签名服务上传 URL 的示例。
// statements 数组的一部分
{ "type": "ResourceRead", "resource": "Media",
"target": { "sys": { "id": "{ /payload/fields/contractMediaId }" } }, "name": "contract" },
{ "type": "Http", "method": "PUT",
"url": "https://api.esign.example/v1/envelopes/{ /payload/fields/envelopeId }/document",
"headers": [ { "key": "Authorization", "value": "Bearer { /secrets/ESIGN_API_KEY }" },
{ "key": "Content-Type", "value": "application/pdf" } ],
"urlBody": "{ /contract/fields/file/en-US/url }", "name": "upload" }请求的 Content-Type 按以下顺序确定。
- 若在
headers中指定了Content-Type,则原样使用该值。body 序列化中的更正不适用。 - 未指定或其值为空时,使用文件 URL 响应的
Content-Type。 - 若也没有,则使用
application/octet-stream。
multipartBody:把文件与文本作为表单发送
以 multipart/form-data 发送。数组的一个元素就是一个 part,part 会按数组顺序发送。数组必须有 1 个以上的元素,空数组在保存时会被拒绝。Content-Type 始终为 multipart/form-data(边界字符串自动生成),写在 headers 中的 Content-Type 会被忽略。
| part 字段 | 说明 |
|---|---|
name | form 字段名(必填)。它不是值表达式,而是字面量。同一名称使用多次时,各自作为独立的 part 发出 |
value | 文本 part 的内容(值表达式)。结果为空时按空值发送 |
url | 获取文件 part 字节的 URL(值表达式)。例如 Media 文件的 url |
fileName | 写入 part 的 Content-Disposition 的文件名(值表达式)。只能用于 url part |
contentType | part 的 Content-Type(值表达式) |
value与url必须恰好写其中一个。 两者都写或都缺时,保存会被拒绝。在valuepart 上写fileName,保存也会被拒绝。- 省略
fileName时,会解码url的最后一个路径片段来使用(不含查询字符串)。该片段为空或含有控制字符时,以file发送。 - 省略
contentType时,valuepart 不带Content-Type发出,urlpart 使用文件 URL 响应的Content-Type(没有则为application/octet-stream)。指定了不符合媒体类型格式的值时,执行时该Http会以 status 400 失败。 fileName与contentType的值表达式结果中含有换行符时,该Http会在发送请求之前以 status 400 失败。
下面是把商品照片(Media)与分析选项一起发送给图像分析服务的示例。image part 没有写 fileName,因此 Media 文件 URL 的最后一个片段(例如 tumbler.png)会作为文件名发出。
// statements 数组的一部分
{ "type": "ResourceRead", "resource": "Media",
"target": { "sys": { "id": "{ /payload/fields/photoMediaId }" } }, "name": "photo" },
{ "type": "Http", "method": "POST", "url": "https://api.vision.example/v2/analyze",
"headers": [ { "key": "x-api-key", "value": "{ /secrets/VISION_API_KEY }" } ],
"multipartBody": [
{ "name": "image", "url": "{ /photo/fields/file/en-US/url }" },
{ "name": "language", "value": "ko" },
{ "name": "features", "value": "labels" },
{ "name": "features", "value": "text" }
], "timeoutMs": 20000, "name": "analysis" }发送文件的 Http 所产生的失败也分为两种。 超时按 status 504 响应处理,因此适用 retry 与 ignoreStatusCode。下表中与文件相关的失败,两者都不适用。Http 失败的整体分类整理在 Http 失败的两种方式中。
ignoreStatusCode: true 时,超时会将 { "status": 504, "body": "timed out after …ms" } 绑定为结果。
一次请求必须在 timeoutMs 内完成,等待文件 URL 响应的时间也包含在内。发送文件时,限制时间会延长,延长量为以每秒 10,000,000 字节的速度发送该文件所需的时间。延长的时间不会超过套餐的下载时间乘以文件数的值。不过,若文件 URL 不告知文件大小,则只会按实际已发送的量延长,因此传输缓慢或中途停滞时,可能会更早超时。
下列情况下该 Http 会立即失败。不会通过 retry 重试,即使指定了 ignoreStatusCode: true 也按失败处理。与其他运行时失败一样,可以用 Try/catch 处理。
| 情况 | 结果 |
|---|---|
| 要获取的文件大于套餐的下载限额 | 以 status 413 失败。若文件 URL 预先告知大小,则在获取前失败;若不告知,则在获取过程中超过限额的那一刻失败 |
| 文件 URL 响应了非 2xx 的 status(包括重定向),或无法连接到文件 URL | 以 status 502 失败 |
part 的 contentType 不符合媒体类型格式 | 以 status 400 失败 |
part 的 fileName、contentType 值中含有换行符 | 以 status 400 失败。在发送请求之前失败 |
- 下载限额按每个文件 URL 分别适用。
multipartBody中有多个文件 part 时,每个文件分别适用限额。 - 发送的字节(含文件)会计入定价方案中数据传输的“外部调用”。
EmailSend
通过已注册的 EmailAccount 发送1 封邮件。它接收的字段只有能直接映射到 SMTP/MIME 的那些。没有模板 id、预约发送、按服务商的扩展(若需要这类功能,请用 Http 直接调用相应邮件服务的 API)。发件人(发出地址)不在此处指定,而来自 account 所指向的 EmailAccount。
| 字段 | 说明 |
|---|---|
account | 要发送的 EmailAccount 引用({ sys: { id } },必填)。通常是字面量 id。若以值表达式给出,则在发送时才 resolve,因此保存时无法校验 |
to | 收件人地址(值表达式)。to 与 toServiceUser 只能用其中之一 |
toServiceUser | 用 ServiceUser 引用指定收件人({ sys: { id } },其 sys.id 可为值表达式)。引擎会在发送前才 resolve 地址,因此会员的地址不会进入 Script 变量空间 |
cc | 抄送收件地址数组(值表达式) |
bcc | 密送收件地址数组(值表达式) |
subject | 主题(值表达式,必填) |
body | 正文(值表达式)。始终以 text/html 发送,因此请写标记而非纯文本(换行会变成空格,< 会被解释为标签)。被插值的值表达式结果,只要 autoEscape 不为 false,就会被 HTML 转义。body 与 templateBody 只能用其中之一 |
templateBody | 用模板生成正文。形如 { source, data },与 body 只能用其中之一。参见下面的用模板生成正文 |
autoEscape | (可选,默认 true)是否对插入正文的值做 HTML 转义。对象是 body 的 { /pointer } 结果与 templateBody.data 的值。设为 false 时,值中所含的标记会原样渲染。作者亲手写的标记与 templateBody.source 模板本来就不在对象范围内,头字段也不受影响 |
replyTo | (可选)Reply-To 头(值表达式)。可以与发件人不同(例如以 no-reply 发送,但回信到支持地址) |
timeoutMs | (可选,1 以上)本次发送的超时时间(ms)。缺省时为平台默认值,超过上限的值在保存时会被拒绝 |
- 正文用
body或templateBody之一来写。 两个都写或都留空时,保存会被拒绝。 - 调用者传来的值默认会按文字渲染。 开启
autoEscape时,值中像<a>这样的标记会显示为文字而不是标签。这是防止他人传来的值在别人的邮件中植入链接或标签的装置。只有在需要保留标记时(例如管理员亲手写的新闻稿正文),才设为false。 - 头字段中不能放入含换行的值。
subject、replyTo与所有地址都属此列。出现换行时执行会失败。正文中换行是普通内容,因此不作检查。 - 收件人合计最多 50 人。
to(1 人)、cc、bcc全部合并计数(SMTP 信封中不区分 cc/bcc,全部作为收件人发出,因此按合计计数)。超过时在保存、执行中会被拒绝。要发给很多人,请用ResourceForEach+EmailSend每项发 1 封。 - 不绑定结果。 成功仅表示“服务商已接收该邮件”,没有可返回的值,因此不接受
name。也不重试(邮件是非幂等的,含糊失败后重试会造成重复发送,因此不遵循Http的retry)。失败会被 throw,由Try的catch处理。 - 它是外部调用。 会计入各套餐的外部调用限额,在时间预算中按
timeoutMs(未写时为 10 秒)算一次(因为不重试,所以不像Http那样乘以次数)。可在ResourceForEach的onEach内使用(多条发送的标准形态)。
{ "type": "EmailSend", "account": { "sys": { "id": "eml_orders" } },
"to": "{ /order/fields/email/en-US }",
"subject": "订单受理完成 (订单号 { /order/sys/id })",
"body": "<p>您的订单已受理。配送开始后我们会再次通知您。</p>",
"replyTo": "support@my-shop.example" }用模板生成正文(templateBody)
若把邮件正文直接写在 Script 定义里,每次改文案都要修改 Script。templateBody 让你把正文模板放在 Content 中,只填充其中的空位,于是文案在内容工作室里改,Script 保持不动。
| 键 | 说明 |
|---|---|
source | 指向存放模板之处的单个指针(必填)。它必须指向本 Script 的 ResourceRead 或 ResourceFind 读取到的 Content,且该指针前后不能写其他字符。值必须是字符串,因此多语言 Field 要连同区域一起写(/tpl/fields/html/en-US) |
data | 用来填充模板空位的值(可选)。键名即空位名。因为是数据位置,要使用运算就给运算符加上 $($cat),名称以 $ 开头的空位写成 $$(参见数据位置与表达式位置) |
模板中的空位写作 { /<键> }。该空位读取的只有 data,无法从模板直接读取 /payload 或 /secrets。指向 data 中不存在的键的空位会渲染为空字符。值为 null 的键也一样。这与 body 中指向 null 的指针会打印出文字 null 相反,在两种写法之间切换时请留意。
模板中若出现 { 后面跟着 / 的位置(中间有空格也一样,例如 CSS 的 a{/*注释*/}),会被当作空位读取,因此请写成 \{。
有三处会通过保存却在执行时中断。source 解析成对象时(多语言 Field 漏写了区域)会失败。source 为空值时会失败(ResourceFind 没有找到,或读取模板的 Statement 排在 EmailSend 之后)。把空位全部填完后的结果只剩空白时也会失败。
source 指向的模板本身不会被转义。转义的对象是 data 的值。因此,若想把调用者传来的 HTML 原样放进邮件,就把该值放进 data 并把 autoEscape 设为 false。
{ "type": "ResourceRead", "resource": "Content", "name": "tpl",
"target": { "sys": { "id": "cnt_mail_shipping" } } },
{ "type": "EmailSend", "account": { "sys": { "id": "eml_orders" } },
"toServiceUser": { "sys": { "id": "{ /order/fields/buyer/en-US/sys/id }" } },
"subject": "您的订单已开始配送",
"templateBody": {
"source": "{ /tpl/fields/html/en-US }",
"data": { "name": "{ /order/fields/buyerName/en-US }",
"tracking": "{ /order/fields/trackingNo/en-US }" } } }模板(cnt_mail_shipping 的 html Field)是这样的。
<p>{ /name },您订购的商品已开始配送。</p>
<p>运单号:{ /tracking }</p>变量
SetVar
声明或更新 script 作用域的可变变量。通过 { /vars/<var> } 引用(JsonLogic 本身没有变量声明,因此作为 Statement 提供)。
| 字段 | 说明 |
|---|---|
var | 变量名。通过 { /vars/<var> } 引用 |
value | 值表达式。可引用自身以进行累加 |
{ "type": "SetVar", "var": "total", "value": 0 }
{ "type": "SetVar", "var": "total", "value": { "$+": [ "{ /vars/total }", "{ /row/qty }" ] } } // 累加
{ "type": "SetVar", "var": "ids", "value": { "$merge": [ "{ /vars/ids }", [ "{ /row/sys/id }" ] ] } } // 收集数组缓存
Cache
读写仅属于该 Script 的短生命周期缓存。像外部调用结果这类每次都重新取回太可惜的值,可以在这里存放几秒,供下一次调用复用。它不是外部调用,因此不计入每个定义的外部调用数量,在时间预算中也没有可声明的时间。
| 字段 | 说明 |
|---|---|
action | "Set"(写入)、"Get"(读取)、"Delete"(删除)三者之一(必填) |
key | 缓存键(Cache Key)(必填)。它不是值表达式,而是字面量(参见下文)。最长 128 个字符,超过则保存被拒绝 |
value | 要保存的值(仅 Set) |
ttl | 缓存的存活时间(仅 Set,秒)。取值在 1 到 30 之间,省略时为 5 |
defaultValue | 没有缓存数据时供 Get 绑定的值(仅 Get)。省略时为 null |
name | 存放结果的名称。Get 为必填(读到的值无处可去,就没有读取的理由)。Set 绑定所保存的值,Delete 绑定是否执行了删除,二者都是可选 |
- 只写与该操作相对应的字段。 在
Get上写ttl,或在Set上写defaultValue,保存都会被拒绝。 - 不存在与已过期不作区分。 两者都会绑定
defaultValue。存进去的值本身是null时也一样。 - 保存范围仅限该 Script 一个。 同一 Space 内的其他 Script 即使使用相同的缓存键,也看不到彼此的数据。修改或删除该 Script 后,它的数据会全部消失。
key是字面量。 如果让请求传来的缓存键去挑选数据,调用方就能决定读取什么数据,而按会员各存一份的 Script 就会把某个会员的值交给另一个会员。因此key中若含有{ /pointer },它既不会被换成值,也不会被逐字使用。保存本身就会被拒绝。- 不能放在循环里。
Loop或ResourceForEach的块内出现Cache时,保存会被拒绝。因为下面的数量上限在循环内部起不到任何限制作用。每轮循环都会写入一份数据,于是定义里写下的 Statement 数与实际用到的缓存键数就对不上了。 - 每个定义最多 5 个(含嵌套,与操作种类无关合并计数)。超过时保存会被拒绝。
- 保存的值最大为 10,240 字节(10KiB)。 超过时该 Statement 失败(status 422)。与其他运行时失败一样,可以用
Try/catch就地处理。
// 将汇率复用 30 秒。
{ "type": "Cache", "action": "Get", "name": "cached", "key": "rates" }
// 若已存有值,就不做外部调用直接返回
{ "type": "If", "condition": { "!!": [ "{ /cached }" ] },
"then": [ { "type": "Return", "value": "{ /cached }" } ] }
{ "type": "Http", "name": "fetched", "method": "GET", "url": "https://api.example.com/rates" }
{ "type": "Cache", "action": "Set", "key": "rates", "value": "{ /fetched/body }", "ttl": 30 }
{ "type": "Return", "value": "{ /fetched/body }" }
// 在过期之前丢弃已存的值
{ "type": "Cache", "action": "Delete", "key": "rates" }值解析
ParseJson
将 JSON 文本解析为它所表示的值,并绑定到一个名称上。用于处理以 responseType: "Text" 从 Http 接收到的正文、随 payload 传入的 JSON 字符串,或以字符串形式存放在字段中的 JSON。它不是外部调用,因此不计入外部调用限额,在时间预算中也没有可声明的时间。
| 字段 | 说明 |
|---|---|
name | 存放解析结果的名称(必填)。在其他 Statement 中是可选的,但这里是必填。除了绑定结果之外它不做别的事,因此没有名称就成了毫无作用的 Statement |
value | 要解析的 JSON 文本(值表达式,必填)。可以像 { /resp/body } 那样指向前一步的值,也可以直接把 JSON 文本按字面量写出(字面量中的 { 不会被解释为 { 指针 } 模板) |
- 结果:解析后的值本身。对象仍是对象,数组仍是数组,
42或"a"这样的单个值也会被解析。之后用{ /<name>/... }指向其内部。 - 如果传入的已经是解析过的值,则原样绑定。 当
valueresolve 出的结果不是字符串时,就视为它不是需要解析的文本,直接原样放入该值。 - 已解析文本中的
{ /pointer }不会再次解释。 即使来自外部的字符串包含{ /payload/... }这样的表达式,也不会被替换成值,而是保留为字符串。 null要区分两种情况。 待解析的文本若是null这一个词,属于正常,结果也是null。但如果value指向的位置为空、根本没有值,就没有可解析的内容,因此失败。- 失败:
valueresolve 后无值或只有空白时,以及文本不是 JSON 时。与其他运行时失败一样用Try/catch处理,错误消息中会带上试图解析的文本。 - 它在每个定义的 Statement 总数中计为 1 个,但与外部调用上限和
SetVar上限无关。
// 1) 不返回 JSON 的 API:以 Text 接收后解析
{ "type": "Http", "method": "GET", "url": "https://api.partner.example/v1/quote",
"responseType": "Text", "name": "resp" },
{ "type": "ParseJson", "name": "quote", "value": "{ /resp/body }" },
// 2) 解析随 payload 传入的 JSON 字符串
{ "type": "ParseJson", "name": "spec", "value": "{ /payload/fields/specJson }" }签名校验与文本处理
这些 Statement 用来确认支付服务商通过 Webhook 发来的签名,并解开该签名被打包进来的字符串。三者都不是外部调用而是计算,因此不计入外部调用限额,在时间预算中也没有可声明的时间;它们不带数据位置,因此与 $ 前缀规则 无关。把三者组合起来的完整示例见 Cookbook 的 Webhook 签名校验。
这三种 Statement 都对 resolve 后的值的长度设有上限。它不是表达式本身的长度,而是该表达式所指向的值的长度({ /rawPayload } 这十六个字符指向的可能是数十 KB),超过时执行会失败,可用 Try 处理。具体数值汇总在值长度上限。
Signature
确认收到的签名码是否与用 secret 生成的码一致,并把这个答案作为 Boolean 值绑定。支付服务商(PG、MoR)通过 Webhook 发来的签名就用这条 Statement 校验。
| 字段 | 说明 |
|---|---|
name | 存放校验结果的名称(必填)。{ /<name> } 为 true 或 false。校验完却不使用结果就等于没有校验,因此不能省略 |
algorithm | 生成码所用的哈希(必填)。SHA1、SHA256、SHA384、SHA512 |
secret | 与对方共享的密钥(值表达式,必填)。密钥在 secrets 中声明,并写成 { /secrets/<key> } |
secretEncoding | 说明 secret 所指向的密钥字符串采用哪种编码。Utf8(默认,文本密钥)、Hex、Base64。把以 hex 或 base64 发放的密钥当作文本填写,它就成了另一个密钥,虽然会生成看似像样的码,但永远不会匹配 |
value | 要计算码的消息(值表达式,必填)。它必须与对方签名的字节逐字相同,因此通常是 { /rawPayload },或者在其前面拼上服务商在请求头里一并发来的时间戳 |
expected | 调用方发来的码(值表达式,必填)。例如 { /headers/x-signature } |
- 结果:
Boolean。之后可以把{ /<name> }直接写进If的条件。 value要用/rawPayload,而不是已解析的/payload。 把解析后的 payload 再变回字符串时,空白、数字表示、转义都会被规范化,从而无法还原成对方签名时的字节(参见上下文根)。- 没有用于指定输出编码的字段。
algorithm固定了码的字节长度,而相同字节长度的 hex 与 base64 在字符串长度上不会重叠,因此即使对方不告知它用了哪一种,引擎也能还原出字节。hex 的大小写,以及 base64 与 base64url(包括有无 padding),也出于同样的道理不作区分。 - 是失败还是
false,取决于那个值由谁给出。expected缺失或码不一致时,结果只是false,并不是失败。因为把请求头缺失与码不一致分别告知,就等于告诉发送方是哪一边错了。value为空时,会按空消息计算。空正文同样是签名对象。secret缺失,或它不是secretEncoding所声明的编码时,则为失败。这三者之中只有这一项是作者自己的输入。失败消息里不会带上secret与value。
value的上限是 65,536 个字符(以 resolve 后的值为准)。这是按实际服务商发来的 Webhook 正文大小设定的值。- 比较会以 constant-time 判定两个值是否相同。前面几个字节是否对上,不会通过响应时间泄露出去。
- 签名密钥不要直接写在
secret中。 请在定义的secrets中声明,并写成"{ /secrets/<key> }"。secrets的值由 WEEGLOO 加密并安全地存储。secretEncoding表示存放在secrets中的密钥字符串采用哪种编码。
// 对整个正文签名的服务商
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
"secret": "{ /secrets/WEBHOOK_SIGNING_SECRET }", "value": "{ /rawPayload }",
"expected": "{ /headers/x-webhook-signature }" }
// 以 base64 发放密钥的服务商(在 secrets 中原样存入 base64 字符串)
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
"secret": "{ /secrets/WEBHOOK_SIGNING_KEY }", "secretEncoding": "Base64",
"value": "{ /rawPayload }", "expected": "{ /headers/webhook-signature }" }Hash
把 value 做摘要,并以 encoding 指定的编码作为字符串绑定。它不是 HMAC,而是用于再现“把若干字段与密钥拼接起来计算 SHA256”这类签名方案。
| 字段 | 说明 |
|---|---|
name | 存放摘要的名称(必填) |
algorithm | MD5、SHA1、SHA256、SHA384、SHA512(必填)。MD5 是为了再现要求它的旧方案而存在的,不是新建签名时该选的值 |
value | 要做摘要的消息(值表达式,必填) |
encoding | 结果的编码。Hex(默认)、HexUpper、Base64、Base64Url |
- 没有
secret字段。 在value里该方案摆放密钥的位置写上{ /secrets/<key> }。各方案把密钥放在前、后、中间的位置各不相同,而这样写就能表达任何位置。密钥要事先在secrets中声明。 - 结果:字符串。与对方发来的码比较时,写作
{ "==": [ "{ /<name> }", "{ /headers/... }" ] }。这个比较与Signature的 constant-time 比较不同,是普通的相等比较。 valueresolve 后没有值或只有空白时,则为失败(因为它是作者自己的表达式)。value的上限是 128 个字符。 它是放置拼接起来的若干字段的位置,因此比Signature窄得多。若需要以整个 Webhook 正文为对象计算,请使用Signature。
// 把 SHA256(订单号 + 金额 + MERCHANT_KEY) 输出为大写 hex
{ "type": "Hash", "name": "expectedSign", "algorithm": "SHA256", "encoding": "HexUpper",
"value": "{ /payload/orderId }{ /payload/amount }{ /secrets/MERCHANT_KEY }" }Regex
把 pattern 应用到 value 上,并绑定 mode 所要求的东西。值表达式里没有切分字符串的手段(只有做拼接的 cat 与看是否包含的 in),因此像 t=…,v1=… 这样多个值被打包在一个请求头里时,就用这条 Statement 来解开。
| 字段 | 说明 |
|---|---|
name | 存放结果的名称(必填)。Capture 通过 { /<name>/1 } 指向其元素 |
mode | "Match" 把是否匹配绑定为 Boolean,"Capture" 把首个匹配绑定为数组(必填) |
pattern | 正则(必填)。它不是值表达式,而是字面量(参见下文)。标志写在模式内部,例如 (?i)。最长 128 个字符,超过则保存被拒绝 |
value | 要应用模式的文本(值表达式,必填)。resolve 后的值超过 10,240 个字符(10KiB)时执行会失败 |
- 结果:
Match为Boolean,Capture为数组或null。数组的索引0是整个匹配,1开始才是捕获组,未参与匹配的组为null(不是空字符串。空字符串意味着匹配到了)。模式没有出现时,Capture不是空数组而是null。 - 两种模式问的都是“模式是否出现在某处”。 若要求整段文本与模式相同,请用
^…$固定。这样安排是为了让先用Match检查、再用Capture取出的两条 Statement 不会给出彼此不同的答案。 pattern是这个引擎中不是值表达式的三个字段之一(其余两个是Cache的key与PdfWatermark的color)。如果原样执行来自请求的模式,调用方就能挑选被执行的式子,而正则的回溯会让它成为拒绝服务的手段。因此模式内部的{ /pointer }也不会被换成值,而是逐字成为模式的一部分。- 模式会在执行开始时对整个定义编译一次。即使它位于
Loop或ResourceForEach内部,也不会每次迭代重新编译;无法使用的模式会在第一条 Statement 做任何事之前就失败(可用Try处理)。
// 解开 "t=1492774577,v1=<64 字符 hex>",得到 { /sig/1 } = 时间戳、{ /sig/2 } = 签名码
{ "type": "Regex", "name": "sig", "mode": "Capture",
"pattern": "^t=(\\d+),v1=([0-9a-f]{64})$", "value": "{ /headers/x-provider-signature }" }
// 仅校验格式
{ "type": "Regex", "name": "isOrderId", "mode": "Match",
"pattern": "^ORD-\\d{8}-\\d{4}$", "value": "{ /payload/orderId }" }控制流
If
条件分支。condition 是 JsonLogic,真与假遵循真值判定的规则。
| 字段 | 说明 |
|---|---|
condition | JsonLogic(求值为 boolean) |
then | 为真时执行的 Statement 数组 |
else | (可选)为假时执行的 Statement 数组 |
{ "type": "If",
"condition": { "<": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] },
"then": [ { "type": "Return", "value": { "ok": false, "reason": "insufficient credit" } } ],
"else": [ /* ... */ ] }Loop
循环。选择一种模式:over(foreach)、while(条件)、for(计数)。无论哪种模式,引擎都会强制施加循环上限(防止无限循环)。上限用 maxIterations 声明,未写时适用平台上限。body 内也可以放入外部调用(Http、EmailSend)与 Media 文件摄取,外部调用 Statement 在执行时每次迭代都会实际发起调用。每个定义的外部调用最大数量限制照常适用。
在时间预算中按乘法计算。 这条 Statement 所声明的时间,是 body 所声明时间乘以 maxIterations(未写时为 10,000)的结果(时间预算)。若 body 中没有外部调用,声明时间为 0,因此 30 秒基础预算就是实际上限。
| 字段 | 说明 |
|---|---|
over | foreach:resolve 为数组的值表达式 |
while | 条件:JsonLogic(为真时持续循环) |
for | 计数:{ "from", "to", "step"? }。从 from 到 to(含两端),step 默认 1 |
maxIterations | 最大循环次数(可选)。未写时适用平台上限 10,000,比它更大的值在保存时会被拒绝 |
name | (可选)用于绑定当前项(foreach)或索引(while、for)的名称({ /<name> }) |
body | 循环体 Statement 数组 |
// foreach
{ "type": "Loop", "over": "{ /payload/fields/items }", "name": "item", "maxIterations": 100,
"body": [ { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_item" } },
"fields": { "name": { "en-US": "{ /item/name }" } } } ] }
// while
{ "type": "Loop", "while": "{ /vars/hasMore }", "maxIterations": 1000, "body": [ /* ... */ ] }
// counted (1..10 step 2)
{ "type": "Loop", "for": { "from": 1, "to": 10, "step": 2 }, "name": "i", "maxIterations": 100, "body": [ /* ... */ ] }Parallel
并发执行各分支,join 后再继续。分支之间不可相互引用(若存在依赖,请改为顺序排列)。
| 字段 | 说明 |
|---|---|
branches | Statement[][]。每个元素为一个分支(Statement 数组) |
{ "type": "Parallel", "branches": [
[ { "type": "Http", "method": "GET", "url": "https://api.a.com/x", "name": "a" } ],
[ { "type": "Http", "method": "GET", "url": "https://api.b.com/y", "name": "b" } ]
] }Return
相当于一般编程中的 return。 Script 的结果返回给调用方,并在该处正常结束。
| 字段 | 说明 |
|---|---|
value | (可选)要返回的值表达式 |
isError | 默认 false。为 true 时,value 会作为响应的 error 输出(否则为 return) |
statusCode | 响应状态码。默认 200 |
- 若未到达
Return,则没有返回值。要给出结果,请显式指定value。 - 它是正常结束,而非异常或 throw,因此不属于
catch的对象(即使在Try内,也会结束整个 Script,但仍会执行finally)。 - guard 也用该 Statement 表达。在
If的then中放入Return,就会在违反条件时返回值,之后不再执行后续 Statement。这是Return的众多用法之一。
{ "type": "Return", "value": { "orderId": "{ /order/sys/id }", "status": "paid" }, "statusCode": 201 }
{ "type": "Return", "value": { "reason": "payment failed" }, "isError": true, "statusCode": 402 }Try
异常处理。
| 字段 | 说明 |
|---|---|
body | 要尝试执行的 Statement 数组 |
catch | (可选)body 失败时执行。在 /error 中暴露 { message }(不会记载是在哪条 Statement 上失败的) |
finally | (可选)无论成功或失败始终执行 |
- 若
catch处理了错误,则 Script 不会中断。只有没有catch的失败才会中断 Script(包括补偿尝试)。 - 什么算“失败”,以及补偿(compensation)的限制,详见执行语义、约束与安全。
{ "type": "Try",
"body": [ { "type": "Http", "method": "POST", "url": "https://primary.api/gen", "name": "resp" },
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_result" } },
"fields": { "text": { "en-US": "{ /resp/body/text }" } } } ],
"catch": [ { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_result" } },
"fields": { "text": { "en-US": "Generation failed" }, "error": { "en-US": "{ /error/message }" } } } ],
"finally": [ /* 始终执行 */ ] }相关文档
- 值表达式:上述所有字段遵循的取值规则。
- 执行语义、约束与安全:执行顺序、错误、静态约束与安全。
- Cookbook:组合这些 Statement 的完整示例。
- Script 概述:顶层结构与一次执行可用的时间。
