Statement 目录

statements 数组的每个元素都是一个语句(statement)。本文整理了 25 种语句的字段、行为与结果。所有取值位置都遵循值表达式的规则(引用、字面量、JsonLogic、Locale 映射),例外有两个:RegexpatternCachekey(参见 RegexCache)。

语句概览

分类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 语句,必须写明所处理的 Content Type ResourceFind·ResourceForEach·ResourceCountresource"Content" 时,contentType必填。没有横跨整个 SpaceContent 查询。ResourceCreate 同样要写明要创建的 Content TypeMedia 因为整个 Space 共用一份而不划定范围,按 id 指定目标的语句(ResourceRead·ResourceUpdate·ResourcePatch·ResourceDelete 以及发布·归档语句)则带有 target,因此不需要范围。

循环调用最多 3 次。 在上面的资源写入语句(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)相同,且在同一个 Script 内必须唯一。格式违规、使用保留字或重复时,保存会被拒绝。

实体引用形态

contentTypetarget 等实体引用统一为 { "sys": { "id": <值表达式> } } 一种形态。只需 sys.id,目标类型由 resource 推断(sys.typesys.targetType 均省略)。

  • contentType.sys.id 通常是字面量(例如 "ct_post")。
  • target.sys.id 通常是 { /ptr } 值表达式(运行时 resolve,例如 { /payload/sys/id })。

resource

资源类语句通过 resource: "Content" | "ContentType" | "Media" | "ServiceUser" 指定目标种类。

Content Type 只被 ResourceCount 接受。写在其他语句里则保存会被拒绝。创建或修改模板本身的工作不归 Script,而是 CMA 的职责。

ServiceUser(在产品上注册的会员)是只读的。只有读取的三种语句(ResourceRead·ResourceFind·ResourceForEach)接受这个值,写在写入语句里则保存会被拒绝(参见错误)。规则见会员目录读取

资源写入

所有写入语句都带有 propagateEvents(默认 false)。设为 true 时,该写入会触发变更事件,从而执行 Webhook 这类后续动作。默认不触发(静默的系统写入)。

ResourceCreate

创建 ContentMediaContentMedia 共享 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 filefields.file.{locale} 的值是摄取指令 { "source": <值表达式>, "encoding": "url"|"base64" }(两者均必填)。在包含文件的写入中由引擎执行摄取(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" }

ResourceUpdate

对目标 ContentMedia 的字段进行整体替换(PUT)。fields 中给出的内容会原样成为新字段,这里未列出的 field 与 locale 会被清除。若只想改动一部分,请使用 ResourcePatch

字段说明
resource"Content""Media"
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)
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

对目标 ContentMedia 的字段进行部分合并(PATCH)。只覆盖 fields 中给出的字段(及其中的 Locale),而未提及的字段与 Locale 保持不变。值形态、localeversionpublishResourceUpdate 相同。

字段说明
resource"Content""Media"
target目标({ sys: { id } },必填)。id 通常为 { /ptr }
fields要覆盖的字段。值为 Locale 映射。仅更新指定的字段与 Locale 桶(其余保持不变)。若值为字面量 null,则删除该 (field, locale)。Media file 是摄取指令(参见上文 ResourceCreate)
locale(便捷项)自动包装 fields
version(可选)与 ResourceUpdate 相同(乐观锁)
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

删除目标。DraftArchived 状态可以删除。若为 Published 或 Changed 会被拒绝,因此必须先 ResourceUnpublishMedia 在文件处理过程中会被拒绝删除。不会 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 中放入资源写入语句,则每一项都会执行该写入,从而改变状态。

这四种语句(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 可使用 regexnearwithin 运算符与全文检索(对已开启全文检索的 LongText 字段,eq 还能通过部分、近似匹配找到包含该值的项目),且可按 fields.* 排序。关闭时,这三个运算符会被拒绝,文本的 eq 为精确匹配,而 prefix 及比较、列表运算符与高级搜索无关。刚刚创建或修改的项目,需要短暂片刻(约 1 秒)才会反映到高级搜索中,因此紧接着的下一次高级搜索查询可能查不到它。由于默认是开启的,只要不把 advanced 设为 false,这个延迟就适用于所有查询。若需立即读取刚写入的项目,请按 id 使用 ResourceRead(基本存储版本,无反映延迟),或用写入返回的 sys.id 查询。

wherecreatedBy: ":self" 表示“仅限当前调用者创建的资源”。但在允许匿名调用的 ScriptanonymousCallEnabled)中不能使用它。 此时 :self 不会 resolve 为调用者,而是 resolve 为作者,等于静默地开放了作者的资源,因此这样的定义在保存时会被拒绝(参见匿名调用)。

whereorder 中,内容字段要写成 fields.<field>(仅凭字段名本身无法识别)。fields.<field>Space 默认 Locale 会被自动应用,因此无需直接附加 Locale。下方示例中的 fields.statusfields.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 传给下一条语句这类流程。以下规则为三种语句共通。

  • 只能读取。 ResourceCreate·ResourceUpdate·ResourcePatch·ResourceDelete 以及发布·归档语句都不接受 "ServiceUser",这样的定义会在保存时被拒绝。这不是加上权限就能打开的事情,而是 Script 中根本没有变更会员的途径,因此它不是权限错误,而是作为写错的语句被拒绝。
  • 作者必须拥有会员目录权限才能保存。 它不像 Content·Media 那样用权限映射来校验,而是看作者的 SpaceRole settings 中是否有 SETTING_SERVICE_LOGIN(或 SETTING_ALL)。因为会员目录在其他所有路径上也都是由 Space 设置来管辖的资源。若没有,保存会被拒绝(参见安全模型)。
  • from 只接受 Current。会员不是会被发布的资源,因此给出 Published 会导致执行失败。
  • contentTypeadvanced 会被忽略。 会员目录不按 Content Type 划分(整个 Space 共用一份),高级搜索也仅限 Content
  • wheresys.email 只接受精确匹配类运算符eq·ne·in·nin)。会员的地址是加密存储的,因此顺序比较或 prefix 没有意义。给出其他运算符时不会静默返回 0 条,而是执行失败
  • 结果就是 ServiceUser 资源本身。可以像 { /<name>/sys/id }{ /<name>/nickname } 这样引用。其结构见 ServiceUser 参考。给找到的会员发邮件时,不要把地址取出来,而是把该 sys.id 传给 EmailSendtoServiceUser(引擎会在发送前才 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"ServiceUsersys.email 只能用 eq·ne·in·nin(参见会员目录读取
order在多个匹配时决定“第一个”的排序(例如 "-sys.createdAt"
from(可选)Current(默认,最新草稿)或 Published(发布快照)。ServiceUser 只能是 Current
advanced(可选)通过高级搜索(Advanced Search)执行。仅限 ContentMedia·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。它不是用来构造一个作为值使用的集合,而是用来对每一项执行操作的语句。适用于批量发布草稿、批量修改符合条件的 Content、把每一项向外发送·同步等反复性工作。只读取一条时,请使用 ResourceRead(id)或 ResourceFind(过滤条件)。

字段说明
resource"Content"·"Media"·"ServiceUser"(必填)
contentType遍历范围 Content Type{ sys: { id } })。Content 时必填。在 Media·ServiceUser 中被忽略
where过滤条件({ "<field>": { "<op>": <值> } })。语义与 ResourceFindwhere 相同(ServiceUsersys.email 约束也一样)。可用运算符遵循运算符列表regex/near/within 需要 advanced)。支持 createdBy: ":self"
order排序(例如 "sys.createdAt,sys.id")。缺省时为平台默认顺序
fromCurrent(默认,最新草稿)或 Published(发布快照)。ServiceUser 只能是 Current
advanced通过高级搜索(Advanced Search)遍历。仅限 ContentMedia·ServiceUser 忽略)。默认 true。参见上文 资源读取 说明
limit(可选,1 以上)处理总数上限(不是页大小)。缺省时遍历到平台上限(10,000 条)为止
name(可选)用于绑定当前项的名称。每次迭代都会重新绑定,可在 onEach 内通过 { /<name> } 引用(与 Loopname 生命周期相同;遍历结束后最后一项仍保留绑定)。若不引用项目则省略
onEach对每一项执行的子 statement 数组(必填)
  • 不绑定集合(是 foreach 而非 map)。 既没有 { items, next } 也没有 cursor。它不是把遍历结果作为值返回,而是对每一项执行 onEach。若需要列表,请用 SetVar 自行收集。只需要条数时,请使用 ResourceCount
  • 即使没有 limit 也不是无限遍历。 缺省时遍历到平台上限(10,000 条)为止,若在仍有剩余匹配时触及该上限则失败(以免在留有未处理项的情况下报告成功)。反之,到达声明的 limit 是有意的停止,属于正常结束。超过上限的 limit 在保存时会被拒绝。
  • 没有 cursor。 跑完即成功,中途中断(超出挂钟时间·配额,或 onEach 出现未处理的失败)即失败,错误会指明在哪一项、因何失败。恢复由作者用自己的数据表达(把 where 设为“未处理”,并在 onEach 末尾标记完成,则重跑时会从剩余项继续)。
  • 在时间预算中按乘法计算。 这条语句所声明的时间,是 onEach 所声明时间乘以处理项目数(limit,未写时为 10,000)的结果(时间预算)。作为拥有子项的复合 statement,它自身不计入外部调用 leaf 预算,被计入的是 onEach 内的外部调用语句。
  • onEach 与其他语句一样,可以放入外部调用(Http·EmailSend)或 Media 文件摄取(与 Loopbody 相同)。把资源查询结果对每一项处理一次,正是该语句存在的理由。
// 找出所有 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过滤条件。语义与 ResourceFindwhere 相同。匹配到的项目会全部计入
from(可选)Current(默认,最新草稿)或 Published(发布快照)
advanced(可选)通过高级搜索(Advanced Search)执行。仅限 Content(统计 Content Type 时忽略)。默认 true。参见上文 资源读取 说明
name(可选)用于绑定条数的名称
  • 结果:将匹配到的条数绑定到 name 上。通过 { /<name> } 引用,用于比较与分支。
  • 不返回项目。 若需要项目,请使用 ResourceFind(首个匹配的单条)或 ResourceForEach(对每一项执行)。
  • 不要为了取得条数而用 ResourceForEach 遍历计数。 遍历会把时间预算按项目数相乘来占用(时间预算),而且在仍有剩余匹配时触及平台上限就会失败。只需要计数时,这条语句一次就能完成。
  • 没有 orderlimit。统计不需要顺序,而且匹配到的都会被计入。
// 统计这篇帖子下有多少条评论
{ "type": "ResourceCount", "resource": "Content", "contentType": { "sys": { "id": "ct_comment" } },
  "where": { "fields.postId": { "eq": "{ /payload/sys/id }" } }, "name": "commentCount" }

外部

Http

调用外部 HTTP。它是外部调用,因此会计入各套餐的外部调用限额,在时间预算中则按 timeoutMs(未写时为 30 秒)×(1 + retry)计算。

字段说明
method"GET""POST""PUT""PATCH""DELETE"
url目标 URL(值表达式,可插入 { /ptr }
headers[{ "key", "value", "secret"? }]value 为值表达式。secret:true 的请求头被视为 CMA(管理员)专用,不会向最终用户暴露,且仅在发送前才解密。在此放入 Content-Type 时,body 会按该格式序列化(见下文
body请求 body(值表达式或 JSON)。以何种格式发出,由 Content-Type 请求头决定
timeoutMs本次调用的超时时间(ms)
retry响应 status 大于等于 400 时的重试次数。默认 0,上限为 2
ignoreStatusCode(重试结束后的)最终 status 大于等于 400 时是否将本次调用视为失败。默认 false 时会按失败处理,成为 Try/catch 的对象。为 true 时则不视为失败,而是将 { status, body } 原样绑定(由调用方自行按 status 分支)
responseType以何种形式接收响应正文。"Json"(默认)会解析为对象或数组,"Text" 则按字符串接收
  • 结果{ 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 sk-...", "secret": true } ],
  "body": { "prompt": "{ /payload/fields/prompt }" }, "timeoutMs": 15000, "retry": 1,
  "responseType": "Json", "name": "resp" }

body 以何种格式发出

放入 headersContent-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" }

EmailSend

通过已注册的 EmailAccount 发送1 封邮件。它接收的字段只有能直接映射到 SMTP/MIME 的那些。没有模板 id、预约发送、按服务商的扩展(若需要这类功能,请用 Http 直接调用相应邮件服务的 API)。发件人(发出地址)不在此处指定,而来自 account 所指向的 EmailAccount

字段说明
account要发送的 EmailAccount 引用({ sys: { id } },必填)。通常是字面量 id。若以值表达式给出,则在发送时才 resolve,因此保存时无法校验
to收件人地址(值表达式)。totoServiceUser 只能用其中之一
toServiceUserServiceUser 引用指定收件人({ sys: { id } },其 sys.id 可为值表达式)。引擎会在发送前才 resolve 地址,因此会员的地址不会进入 Script 变量空间
cc抄送收件地址数组(值表达式)
bcc密送收件地址数组(值表达式)
subject主题(值表达式,必填)
body正文(值表达式,必填)。始终以 text/html 发送,因此请写标记而非纯文本(换行会变成空格,< 会被解释为标签)。被插值的值表达式结果会被 HTML 转义
replyTo(可选)Reply-To 头(值表达式)。可以与发件人不同(例如以 no-reply 发送,但回信到支持地址)
timeoutMs(可选,1 以上)本次发送的超时时间(ms)。缺省时为平台默认值,超过上限的值在保存时会被拒绝
  • 收件人合计最多 50 人to(1 人)、ccbcc 全部合并计数(SMTP 信封中不区分 cc/bcc,全部作为收件人发出,因此按合计计数)。超过时在保存·执行中会被拒绝。要发给很多人,请用 ResourceForEach + EmailSend 每项发 1 封
  • 不绑定结果。 成功仅表示“服务商已接收该邮件”,没有可返回的值,因此不接受 name也不重试(邮件是非幂等的,含糊失败后重试会造成重复发送,因此不遵循 Httpretry)。失败会被 throw,由 Trycatch 处理。
  • 它是外部调用。 会计入各套餐的外部调用限额,在时间预算中按 timeoutMs(未写时为 10 秒)算一次(因为不重试,所以不像 Http 那样乘以次数)。可在 ResourceForEachonEach 内使用(多条发送的标准形态)。
{ "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" }

变量

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 },它既不会被换成值,也不会被逐字使用。保存本身就会被拒绝。
  • 不能放在循环里。 LoopResourceForEach 的块内出现 Cache 时,保存会被拒绝。因为下面的数量上限在循环内部起不到任何限制作用。每轮循环都会写入一份数据,于是定义里写下的语句数与实际用到的缓存键数就对不上了。
  • 每个定义最多 5 个(含嵌套,与操作种类无关合并计数)。超过时保存会被拒绝。
  • 保存的值最大为 10,240 字节(10KiB)。 超过时该语句失败(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存放解析结果的名称(必填)。在其他语句中是可选的,但这里是必填。除了绑定结果之外它不做别的事,因此没有名称就成了毫无作用的语句
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 }" }

签名校验与文本处理

这些语句用来确认支付服务商通过 Webhook 发来的签名,并解开该签名被打包进来的字符串。三者都不是外部调用而是计算,因此不计入外部调用限额,在时间预算中也没有可声明的时间;它们不带数据位置,因此与 $ 前缀规则 无关。把三者组合起来的完整示例见 Cookbook 的 Webhook 签名校验

这三种语句都对 resolve 后的值的长度设有上限。它不是表达式本身的长度,而是该表达式所指向的值的长度({ /rawPayload } 这十六个字符指向的可能是数十 KB),超过时执行会失败,可用 Try 处理。具体数值汇总在值长度上限

Signature

确认收到的签名码是否与用 secret 生成的码一致,并把这个答案作为 Boolean 值绑定。支付服务商(PG·MoR)通过 Webhook 发来的签名就用这条语句校验。

字段说明
name存放校验结果的名称(必填)。{ /<name> }truefalse。校验完却不使用结果就等于没有校验,因此不能省略
algorithm生成码所用的哈希(必填)。SHA1·SHA256·SHA384·SHA512
secret与对方共享的密钥(值表达式,必填)
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 所声明的编码时,则为失败。这三者之中只有这一项是作者自己的输入。失败消息里不会带上 secretvalue
  • value 的上限是 65,536 个字符(以 resolve 后的值为准)。这是按实际服务商发来的 Webhook 正文大小设定的值。
  • 比较会以 constant-time 判定两个值是否相同。前面几个字节是否对上,不会通过响应时间泄露出去。
  • secret 不会被加密存储。Http 请求头的 secret: true(加密存储后在发送前才解密)不同,它会原样留在定义里,因此对能够读取该 Script 的角色来说这个值是可见的。会员(ServiceUser)无法读取 Script 定义(创作与查询仅限 CMA)。
// 对整个正文签名的服务商
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
  "secret": "whsec_9f2c1b7ae4", "value": "{ /rawPayload }",
  "expected": "{ /headers/x-webhook-signature }" }
 
// 以 base64 发放密钥的服务商
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
  "secret": "aGVsbG8td2VlZ2xvbw==", "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 里更能表达所有位置。
  • 结果:字符串。与对方发来的码比较时,写作 { "==": [ "{ /<name> }", "{ /headers/... }" ] }。这个比较与 Signature 的 constant-time 比较不同,是普通的相等比较。
  • value resolve 后没有值或只有空白时,则为失败(因为它是作者自己的表达式)。
  • value 的上限是 128 个字符。 它是放置拼接起来的若干字段的位置,因此比 Signature 窄得多。若需要以整个 Webhook 正文为对象计算,请使用 Signature
// 把 SHA256(订单号 + 金额 + merchantKey) 输出为大写 hex
{ "type": "Hash", "name": "expectedSign", "algorithm": "SHA256", "encoding": "HexUpper",
  "value": "{ /payload/orderId }{ /payload/amount }9f2c1b7ae4" }

Regex

pattern 应用到 value 上,并绑定 mode 所要求的东西。值表达式里没有切分字符串的手段(只有做拼接的 cat 与看是否包含的 in),因此像 t=…,v1=… 这样多个值被打包在一个请求头里时,就用这条语句来解开。

字段说明
name存放结果的名称(必填)。Capture 通过 { /<name>/1 } 指向其元素
mode"Match" 把是否匹配绑定为 Boolean"Capture" 把首个匹配绑定为数组(必填)
pattern正则(必填)。它不是值表达式,而是字面量(参见下文)。标志写在模式内部,例如 (?i)。最长 128 个字符,超过则保存被拒绝
value要应用模式的文本(值表达式,必填)。resolve 后的值超过 10,240 个字符(10KiB)时执行会失败
  • 结果MatchBooleanCapture 为数组或 null。数组的索引 0 是整个匹配,1 开始才是捕获组,未参与匹配的组为 null(不是空字符串。空字符串意味着匹配到了)。模式没有出现时,Capture 不是空数组而是 null
  • 两种模式问的都是“模式是否出现在某处”。 若要求整段文本与模式相同,请用 ^…$ 固定。这样安排是为了让先用 Match 检查、再用 Capture 取出的两条语句不会给出彼此不同的答案。
  • pattern 是这个引擎中不是值表达式的两个字段之一(另一个是 Cachekey)。如果原样执行来自请求的模式,调用方就能挑选被执行的式子,而正则的回溯会让它成为拒绝服务的手段。因此模式内部的 { /pointer } 也不会被换成值,而是逐字成为模式的一部分。
  • 模式会在执行开始时对整个定义编译一次。即使它位于 LoopResourceForEach 内部,也不会每次迭代重新编译;无法使用的模式会在第一条语句做任何事之前就失败(可用 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 文件摄取,外部调用语句在执行时每次迭代都会实际发起调用。每个定义的外部调用最大数量限制照常适用。

在时间预算中按乘法计算。 这条语句所声明的时间,是 body 所声明时间乘以 maxIterations(未写时为 10,000)的结果(时间预算)。body 中没有外部调用,声明时间为 0,因此 30 秒基础预算就是实际上限。

字段说明
overforeach:resolve 为数组的值表达式
while条件:JsonLogic(为真时持续循环)
for计数:{ "from", "to", "step"? }。从 fromto含两端),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[][]。每个元素为一个分支(语句数组)
{ "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 也用该语句表达。在 Ifthen 中放入 Return,就会在违反条件时返回值,之后不再执行后续语句。这是 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 }(不会记载是在哪条语句上失败的)
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": [ /* 始终执行 */ ] }