Statement 目录
statements 数组的每个元素都是一个语句(statement)。本文整理了 25 种语句的字段、行为与结果。所有取值位置都遵循值表达式的规则(引用、字面量、JsonLogic、Locale 映射),例外有两个:Regex 的 pattern 与 Cache 的 key(参见 Regex 与 Cache)。
语句概览
| 分类 | 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·ResourceCount在resource为"Content"时,contentType为必填。没有横跨整个 Space 的 Content 查询。ResourceCreate同样要写明要创建的 Content Type。Media 因为整个 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 内必须唯一。格式违规、使用保留字或重复时,保存会被拒绝。
实体引用形态
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
资源类语句通过 resource: "Content" | "ContentType" | "Media" | "ServiceUser" 指定目标种类。
Content Type 只被 ResourceCount 接受。写在其他语句里则保存会被拒绝。创建或修改模板本身的工作不归 Script,而是 CMA 的职责。
ServiceUser(在产品上注册的会员)是只读的。只有读取的三种语句(ResourceRead·ResourceFind·ResourceForEach)接受这个值,写在写入语句里则保存会被拒绝(参见错误)。规则见会员目录读取。
资源写入
所有写入语句都带有 propagateEvents(默认 false)。设为 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" }(两者均必填)。在包含文件的写入中由引擎执行摄取(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
对目标 Content 或 Media 的字段进行整体替换(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
对目标 Content 或 Media 的字段进行部分合并(PATCH)。只覆盖 fields 中给出的字段(及其中的 Locale),而未提及的字段与 Locale 保持不变。值形态、locale、version、publish 与 ResourceUpdate 相同。
| 字段 | 说明 |
|---|---|
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
删除目标。仅 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 中放入资源写入语句,则每一项都会执行该写入,从而改变状态。
这四种语句(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 传给下一条语句这类流程。以下规则为三种语句共通。
- 只能读取。
ResourceCreate·ResourceUpdate·ResourcePatch·ResourceDelete以及发布·归档语句都不接受"ServiceUser",这样的定义会在保存时被拒绝。这不是加上权限就能打开的事情,而是 Script 中根本没有变更会员的途径,因此它不是权限错误,而是作为写错的语句被拒绝。 - 作者必须拥有会员目录权限才能保存。 它不像 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。它不是用来构造一个作为值使用的集合,而是用来对每一项执行操作的语句。适用于批量发布草稿、批量修改符合条件的 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末尾标记完成,则重跑时会从剩余项继续)。 - 在时间预算中按乘法计算。 这条语句所声明的时间,是
onEach所声明时间乘以处理项目数(limit,未写时为 10,000)的结果(时间预算)。作为拥有子项的复合 statement,它自身不计入外部调用 leaf 预算,被计入的是onEach内的外部调用语句。 onEach与其他语句一样,可以放入外部调用(Http·EmailSend)或 Media 文件摄取(与Loop的body相同)。把资源查询结果对每一项处理一次,正是该语句存在的理由。
// 找出所有 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遍历计数。 遍历会把时间预算按项目数相乘来占用(时间预算),而且在仍有剩余匹配时触及平台上限就会失败。只需要计数时,这条语句一次就能完成。 - 没有
order与limit。统计不需要顺序,而且匹配到的都会被计入。
// 统计这篇帖子下有多少条评论
{ "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 以何种格式发出
放入 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" }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 发送,因此请写标记而非纯文本(换行会变成空格,< 会被解释为标签)。被插值的值表达式结果会被 HTML 转义 |
replyTo | (可选)Reply-To 头(值表达式)。可以与发件人不同(例如以 no-reply 发送,但回信到支持地址) |
timeoutMs | (可选,1 以上)本次发送的超时时间(ms)。缺省时为平台默认值,超过上限的值在保存时会被拒绝 |
- 收件人合计最多 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" }变量
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时,保存会被拒绝。因为下面的数量上限在循环内部起不到任何限制作用。每轮循环都会写入一份数据,于是定义里写下的语句数与实际用到的缓存键数就对不上了。 - 每个定义最多 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>/... }指向其内部。 - 如果传入的已经是解析过的值,则原样绑定。 当
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 }" }签名校验与文本处理
这些语句用来确认支付服务商通过 Webhook 发来的签名,并解开该签名被打包进来的字符串。三者都不是外部调用而是计算,因此不计入外部调用限额,在时间预算中也没有可声明的时间;它们不带数据位置,因此与 $ 前缀规则 无关。把三者组合起来的完整示例见 Cookbook 的 Webhook 签名校验。
这三种语句都对 resolve 后的值的长度设有上限。它不是表达式本身的长度,而是该表达式所指向的值的长度({ /rawPayload } 这十六个字符指向的可能是数十 KB),超过时执行会失败,可用 Try 处理。具体数值汇总在值长度上限。
Signature
确认收到的签名码是否与用 secret 生成的码一致,并把这个答案作为 Boolean 值绑定。支付服务商(PG·MoR)通过 Webhook 发来的签名就用这条语句校验。
| 字段 | 说明 |
|---|---|
name | 存放校验结果的名称(必填)。{ /<name> } 为 true 或 false。校验完却不使用结果就等于没有校验,因此不能省略 |
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所声明的编码时,则为失败。这三者之中只有这一项是作者自己的输入。失败消息里不会带上secret与value。
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 | 存放摘要的名称(必填) |
algorithm | MD5·SHA1·SHA256·SHA384·SHA512(必填)。MD5 是为了再现要求它的旧方案而存在的,不是新建签名时该选的值 |
value | 要做摘要的消息(值表达式,必填) |
encoding | 结果的编码。Hex(默认)·HexUpper·Base64·Base64Url |
- 没有
secret字段。 各方案把密钥放在前、后、中间的位置各不相同,因此把密钥直接写在value里更能表达所有位置。 - 结果:字符串。与对方发来的码比较时,写作
{ "==": [ "{ /<name> }", "{ /headers/... }" ] }。这个比较与Signature的 constant-time 比较不同,是普通的相等比较。 valueresolve 后没有值或只有空白时,则为失败(因为它是作者自己的表达式)。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)时执行会失败 |
- 结果:
Match为Boolean,Capture为数组或null。数组的索引0是整个匹配,1开始才是捕获组,未参与匹配的组为null(不是空字符串。空字符串意味着匹配到了)。模式没有出现时,Capture不是空数组而是null。 - 两种模式问的都是“模式是否出现在某处”。 若要求整段文本与模式相同,请用
^…$固定。这样安排是为了让先用Match检查、再用Capture取出的两条语句不会给出彼此不同的答案。 pattern是这个引擎中不是值表达式的两个字段之一(另一个是Cache的key)。如果原样执行来自请求的模式,调用方就能挑选被执行的式子,而正则的回溯会让它成为拒绝服务的手段。因此模式内部的{ /pointer }也不会被换成值,而是逐字成为模式的一部分。- 模式会在执行开始时对整个定义编译一次。即使它位于
Loop或ResourceForEach内部,也不会每次迭代重新编译;无法使用的模式会在第一条语句做任何事之前就失败(可用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 文件摄取,外部调用语句在执行时每次迭代都会实际发起调用。每个定义的外部调用最大数量限制照常适用。
在时间预算中按乘法计算。 这条语句所声明的时间,是 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[][]。每个元素为一个分支(语句数组) |
{ "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 也用该语句表达。在
If的then中放入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": [ /* 始终执行 */ ] }相关文档
- 值表达式:上述所有字段遵循的取值规则。
- 执行语义、约束与安全:执行顺序、错误、静态约束与安全。
- Cookbook:组合这些语句的完整示例。
- Script 概述:顶层结构与一次执行可用的时间。
