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"(必填)
contentTypeContent要创建的 Content Type({ sys: { id } })。resource 为 Content 时必填
fields通用字段映射 { "<field>": { "<locale>": 值 } }。每个填充的字段都必须包含默认 Locale 桶。Content 的键遵循 Content Type 定义,Media 的键是固定的(title、description、file)
locale通用(便捷项)提供后会将 fields 的各个值自动包装为 { <locale>: 值 }
publish通用写入后发布(在 CDA/ACDA 上可见)。默认 true
  • Media file: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")。缺省时为平台默认顺序
fromCurrent(默认,最新草稿)或 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-Typebody 形态发出的 body
application/json任何形态JSON
application/x-www-form-urlencoded对象、数组order[id]=A-2481&order[amount]=34000
text/plain标量值原样
其他(text/xml 等)任何形态JSON

以下是因无法以所声明格式承载而被更正的组合。

声明的 Content-Typebody 形态实际发出的 Content-Type发出的 body
application/x-www-form-urlencoded标量text/plain;charset=UTF-8值原样
text/plain对象、数组application/jsonJSON

这两行说明的是搭配错位时请求会以何种形式发出,而不是取得预期格式的方法。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 按以下顺序确定。

  1. 若在 headers 中指定了 Content-Type,则原样使用该值。body 序列化中的更正不适用。
  2. 未指定或其值为空时,使用文件 URL 响应的 Content-Type。
  3. 若也没有,则使用 application/octet-stream。

multipartBody:把文件与文本作为表单发送

以 multipart/form-data 发送。数组的一个元素就是一个 part,part 会按数组顺序发送。数组必须有 1 个以上的元素,空数组在保存时会被拒绝。Content-Type 始终为 multipart/form-data(边界字符串自动生成),写在 headers 中的 Content-Type 会被忽略。

part 字段说明
nameform 字段名(必填)。它不是值表达式,而是字面量。同一名称使用多次时,各自作为独立的 part 发出
value文本 part 的内容(值表达式)。结果为空时按空值发送
url获取文件 part 字节的 URL(值表达式)。例如 Media 文件的 url
fileName写入 part 的 Content-Disposition 的文件名(值表达式)。只能用于 url part
contentTypepart 的 Content-Type(值表达式)
  • value 与 url 必须恰好写其中一个。 两者都写或都缺时,保存会被拒绝。在 value part 上写 fileName,保存也会被拒绝。
  • 省略 fileName 时,会解码 url 的最后一个路径片段来使用(不含查询字符串)。该片段为空或含有控制字符时,以 file 发送。
  • 省略 contentType 时,value part 不带 Content-Type 发出,url part 使用文件 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>/... } 指向其内部。
  • 如果传入的已经是解析过的值,则原样绑定。 当 value resolve 出的结果不是字符串时,就视为它不是需要解析的文本,直接原样放入该值。
  • 已解析文本中的 { /pointer } 不会再次解释。 即使来自外部的字符串包含 { /payload/... } 这样的表达式,也不会被替换成值,而是保留为字符串。
  • null 要区分两种情况。 待解析的文本若是 null 这一个词,属于正常,结果也是 null。但如果 value 指向的位置为空、根本没有值,就没有可解析的内容,因此失败。
  • 失败:value resolve 后无值或只有空白时,以及文本不是 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存放摘要的名称(必填)
algorithmMD5、SHA1、SHA256、SHA384、SHA512(必填)。MD5 是为了再现要求它的旧方案而存在的,不是新建签名时该选的值
value要做摘要的消息(值表达式,必填)
encoding结果的编码。Hex(默认)、HexUpper、Base64、Base64Url
  • 没有 secret 字段。 在 value 里该方案摆放密钥的位置写上 { /secrets/<key> }。各方案把密钥放在前、后、中间的位置各不相同,而这样写就能表达任何位置。密钥要事先在 secrets 中声明。
  • 结果:字符串。与对方发来的码比较时,写作 { "==": [ "{ /<name> }", "{ /headers/... }" ] }。这个比较与 Signature 的 constant-time 比较不同,是普通的相等比较。
  • value resolve 后没有值或只有空白时,则为失败(因为它是作者自己的表达式)。
  • 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,真与假遵循真值判定的规则。

字段说明
conditionJsonLogic(求值为 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 秒基础预算就是实际上限。

字段说明
overforeach: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 后再继续。分支之间不可相互引用(若存在依赖,请改为顺序排列)。

字段说明
branchesStatement[][]。每个元素为一个分支(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": [ /* 始终执行 */ ] }