值表达式 (Value Expressions)

Script 中,凡是需要值的位置(URL、请求 body、字段值、条件、筛选值、目标 id 等)都是下面三种形态之一。例外只有两个:RegexpatternCachekey 只能写成字面量,其中的 { /pointer } 不会被替换为值。本文档说明这三种形态、值来自何处(上下文根),以及 WEEGLOO 数据特有的 Locale 映射规则。Statement 目录中的所有字段都遵循此规则。

三种形态

形态规则示例
引用 (reference)在上下文中解析字符串内的 { /json-pointer }"{ /payload/fields/title }"
字面量 (literal)不含 { /ptr } 的值(字符串、数字、布尔值、对象、数组)。原样使用。"draft", 42, true, { "a": 1 }
运算与条件 (JsonLogic)以单个运算符作为键的对象。操作数本身又是值表达式(引用、字面量、嵌套)。运算符是否需要加 $,取决于所在位置。{ "$+": [ "{ /vars/n }", 1 ] }

这三种形态可以嵌套:在 JsonLogic 操作数中放入引用,把引用的结果再放入运算,即可这样组合。

数据位置与表达式位置:何时加 $

同一份 JSON 会因所在位置不同而被解读成不同的含义。区分的标准是该位置的键归谁所有fields 的键是 Content Type 的字段 id,Http.body 的键是对方 API 的 schema,因此在这类位置上,catin 必须是字段名,而不是运算符。

位置对应字段解读方式
数据位置fieldsResourceCreateResourceUpdateResourcePatch)、Http.bodyReturn.valueSetVar.valueCache.valueCache.defaultValue不带 $ 的键始终是字段名。要使用运算就加上 $
表达式位置If.conditionLoop.whileversion整个值就是表达式。运算符写成 cat$cat 都可以。
模板位置其余全部(urlmethodheaders[].valuelocaleorderovertarget.sys.idEmailSend 的各个字段,以及 Signature·Hash·Regex 的值字段)因为是字符串,所以只能放入 { /pointer }
仅字面量Regex.patternCache.key不是值表达式。写在 Regex.pattern 中的 { /pointer } 不会被替换,而会成为 pattern 的一部分。

规则只有两条。

  1. 在数据位置,不带 $ 的键始终是字段名。 要使用运算,就给运算符加上 $
  2. 一旦通过 $ 进入表达式,其内部就全都是表达式。 嵌套的运算符不需要 $(加上也可以)。

拿不准时,请给所有运算符都加上 $ 在任何位置都是正确的。

// 数据位置:cat 是 Content Type 的字段名(不是拼接运算)
"fields": { "cat": { "en-US": "hello" } }
 
// 在数据位置做计算:只在边界加 $,其内部保持原样
"fields": { "tier": { "en-US": { "$if": [ { ">=": [ "{ /p/score }", 700 ] }, "gold", "silver" ] } } }
 
// 表达式位置:原样书写
"condition": { "and": [ { "<": [ "{ /a/body/risk }", 0.5 ] }, { ">=": [ "{ /b/body/score }", 700 ] } ] }

需要以 $ 开头的字段名时:$$

像 JSON Schema 的 $ref$schema 那样,键本身确实需要以 $ 开头时,请$ 写两次"$$ref" 表示数据键 $ref。只有最前面的一个 $ 会被剥掉($$$ref$$ref),而且只对生效(值里面的 $ 保持原样)。

"body": { "$$ref": "#/components/schemas/Item", "topK": { "$min": [ "{ /payload/fields/k }", 50 ] } }

会被拒绝的两种情况

以下两种情况不会被悄悄解读为另一种含义,而是会作为错误被拒绝。

  • $ 键与同一对象中的其他键并存时会报错。运算必须是该对象的唯一键,同级数据往外挪一层即可。
  • 无法识别的 $会报错。$catt 并不是名为 $catt 的字段。$ 命名空间是为运算符保留的。

在表达式位置,运算符名与同级键并存同样会报错({ "and": […], "or": […] })。那里没有“这是数据”这一层解读,而且所有对象都判定为真,若放任不管,条件就会悄悄变成恒为真。

引用:{ /json-pointer }

在花括号内放入 RFC 6901 JSON Pointer(必须以 / 开头)。花括号周围允许有空格({ /a/b }{/a/b} 相同)。

单一指针与混合模板:类型规则

  • 当整个字符串是单一指针时,会保持该值的原始类型(数字则数字,对象则对象,数组则数组)。
  • 与字面量文本混合时,会拼接(concatenation)为字符串
"{ /payload/fields/count }"                 // 若为数字值则保持数字原样(例如 42)
"{ /payload/fields/tags }"                  // 若为数组则保持数组原样
"page-{ /payload/fields/n }-of-10"          // 字符串拼接 → "page-42-of-10"
"Bearer { /payload/fields/token }"          // 字符串拼接 → "Bearer abc123"

缺失值

  • 当路径不存在或值为空时,单一指针会作为 null 处理混合模板会作为空字符串处理

上下文根:值从何而来

{ /pointer } 的顶层段是下面七种之一。

内容
/payload调用时传入的 JSON payload(输入)。例如:{ /payload/fields/email }
/rawPayload同一个输入,但保留为调用方发送的 body 字符串本身(解析之前)。例如:{ /rawPayload }
/headers调用时传入的请求 HTTP 头。键为小写,每个名称对应单个值。例如:{ /headers/authorization }
/now执行开始的时刻。{ /now/seconds }·{ /now/millis }·{ /now/iso }
/<name>带有 name 的前置 statement 的结果。例如:{ /order/sys/id }
/vars/<name>SetVar 声明的 script-scoped 可变变量。例如:{ /vars/total }
/error仅在 Trycatch 块内使用。即捕获到的错误 { message }。例如:{ /error/message }

/<name> 之外的六个名称(payload·rawPayload·headers·now·vars·error)是保留名,不能用作 statement 的 name。用了同名会覆盖那个根,因此在保存时就会被拒绝(公共字段的绑定名规则)。

/rawPayload:发送时的原始 body

/payload 是解析后的值,/rawPayload同一个 body 的原文字符串。两者指向同一样东西,却并不相同:把解析后的值重新变成字符串时,空白、数字写法、转义、重复键都会被规范化,无法回到发送时的字节。

因此,在发送的字节之上计算出来的值只能用 /rawPayload 处理。 最典型的就是支付服务商 Webhook 的签名校验(Signature)。平时取值引用则用 /payload

调用 body 只接受 JSON 对象。body 为空视为没有 body;不是 JSON 对象时(损坏的 JSON、数组、标量、字面量 null)不执行,并予以拒绝(参见错误)。

/now:执行开始的时刻

/now 以三种形态保存本次执行开始的时刻。

指针
{ /now/seconds }epoch 秒(整数)
{ /now/millis }epoch 毫秒(整数)
{ /now/iso }sys.createdAt 相同的平台时刻表示字符串(UTC)
  • 一次执行只有一个时刻。 它不是读取时钟的 statement,而是在执行开始时埋入的值,所以两个语句不会看到不同的值。Parallel 的每个分支也继承同一个时刻。因为不是语句,也不计入 statement 数量。
  • 没有选择时区的字段。 epoch 值在任何地方都是同一个数,iso 则是 UTC 表示。
  • 用于校验 Webhook 的 replay window(签名中携带的时间戳距现在多少秒以内)。时间戳通常以字符串传入,但算术运算会将其转换为数字,因此可以直接比较。
// 签名中携带的时间戳是否在 5 分钟(300 秒)以内
{ "<": [ { "-": [ "{ /now/seconds }", "{ /sig/1 }" ] }, 300 ] }

statement 结果的形态

带有 name 的 statement,其结果形态因类型而异。

statement结果形态引用示例
Http{ status, body }{ /resp/status }, { /resp/body/choices/0/message/content }
ResourceCreateResourceRead(单条)、ResourceFind(单条)资源本身{ /post/sys/id }, { /post/fields/title/en-US }
ResourceForEach(遍历中)name当前项 = 资源本身。仅在 onEach 内引用{ /post/sys/id }, { /post/fields/title/en-US }
ResourceCount匹配的条数(整数){ /commentCount }
ParseJson解析后的值本身(对象·数组·标量){ /quote/items/0/price }
SignatureBoolean(是否通过校验){ /verified }
Hash字符串(按所声明表示形式的摘要){ /expectedSign }
RegexMatchBooleanCapture 为数组(0=整个匹配,1 起为捕获组),没有匹配时为 null{ /isOrderId }, { /sig/1 }
  • ResourceFind 在没有匹配时绑定 null。用 { "==": [ "{ /found }", null ] } 来分支判断是否存在。
  • ResourceRead(单条)在目标不存在时会报错(可用 Try 处理)。详情参见 Statement 目录的资源读取
  • 读取 ServiceUser 时,结果就是会员资源本身({ /member/sys/id })。与 Content·Media 不同,其字段不是 Locale 映射,而是值本身。规则参见读取会员目录

运算与条件:JsonLogic

需要计算或条件时,使用 jsonlogic.com 规范中的运算符对象。

  • 数据访问统一使用 { /ptr } 引用,而非原生的 var(dot-path)写法。引擎先解析操作数中的指针,再应用运算符。
  • 运算符必须是该对象的唯一键。在数据位置,只有带 $ 的键才是运算;在表达式位置,无论带不带 $ 都是运算(参见数据位置与表达式位置)。

运算符表

表中的名称是运算符 token。在数据位置使用时要在前面加 $cat 写作 $cat)。在表达式位置两种写法都可以。

分类运算符含义与示例
条件if(别名 ?:{ "if": [条件, 真值, 条件2, 真值2, …, 默认值] }。取第一个为真的条件对应的值;若都不满足,则取最后的默认值。
逻辑andor短路求值。and 返回第一个 falsy(或最后一个),or 返回第一个 truthy(或最后一个),并以其值本身返回。
逻辑! (not)、!! (to-bool){ "!": x } 是对 truthy 取反,{ "!!": x } 是判断是否 truthy。存在性检查常用 !!
相等==!=宽松比较(强制转换为数字后比较。"1"==1 为真)。
相等===!==严格比较(连类型也比较)。
比较<<=>>=可链式{ "<": [1,2,3] }1<2 AND 2<3。无法数字化(NaN)时为 false。
算术+所有操作数之和。
算术-一个操作数时取负,两个操作数时做减法。
算术*/%乘、除、取余。
聚合minmax操作数的最小值、最大值。
字符串cat把所有操作数拼接成字符串。
包含in{ "in": [needle, haystack] }。haystack 是字符串时判断子字符串,是集合时判断元素包含。
数组merge把多个数组或值扁平化为一个数组(用于累积收集)。
日期date{ "date": [值, 输出单位] }。把值规范化为可比较的时刻。输出单位为 millis(默认)、secondsisoday。参见日期规范化

不支持数组遍历运算符(mapfilterreduceallsomenone)。Script 通过 Loop 遍历数组(参见 Statement 目录的 Loop)。从列表中只挑出符合日期条件的项,也不是遍历要做的事,而是读取语句要做的事。给 ResourceFindResourceForEachwhere 加上条件,服务器就会筛选后返回(可用的运算符见运算符列表)。

数字转换与示例

数字转换规则如下。数字保持原样,true 转为 1,false 转为 0,字符串会被解析(无法解析时计算会失败),null 转为 0。

日期字符串不是数字。 "2026-10-03" 无法被解析为数字,因此比较运算符不会报错,而是始终返回 false。要比较日期,请先用 date 规范化。

下面的片段以表达式位置为准。要放进数据位置(fieldsHttp.bodyReturn.valueSetVar.value)时,请给最外层运算符加上 $,内部的操作数保持原样。

{ "-":  [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] }   // 余额 - 费用
{ "<":  [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] }   // 余额 < 费用 → boolean
{ "and": [ { "<": [ "{ /a/body/risk }", 0.5 ] }, { ">=": [ "{ /b/body/score }", 700 ] } ] }
{ "cat": [ "id-", "{ /payload/sys/id }" ] }                                    // "id-<uuid>"
{ "!!": "{ /found/sys/id }" }                                                  // 存在则为 true
{ "$merge": [ "{ /vars/ids }", [ "{ /row/sys/id }" ] ] }                       // 数组累积:SetVar.value 是数据位置,所以加 $
{ "if": [ "{ /payload/fields/next }", "{ /payload/fields/next }", "END" ] }     // next 存在则取 next,没有则取 "END"

日期规范化 (date)

比较运算符会先把操作数转换为数字,再进行比较。日期字符串不是数字,因此这样的比较不会报错,而是始终得到 false。换成 == 也解决不了。两边都不是数字时会直接比较文本,于是把同一时刻写成不同形式的 "2026-10-03""2026-10-03T00:00:00.000Z" 就会成为不同的值。日期请在比较之前用 date 规范化。

{ "date": [ , 输出单位 ] }      // 输出单位可以省略
{ "date": "2026-10-03" }          // 只传一个值时可以不写数组

没有 beforeafterequal 这样的专用运算符。规范化后的值是数字,因此直接使用现有的比较、算术、聚合运算符即可。

想要判定的内容使用的表达式
a 早于 b{ "<": [ { "date": a }, { "date": b } ] }
a 晚于 b{ ">": [ { "date": a }, { "date": b } ] }
同一时刻{ "==": [ { "date": a }, { "date": b } ] }
同一天(忽略时刻){ "==": [ { "date": [a, "day"] }, { "date": [b, "day"] } ] }
在 from 与 to 之间{ "<=": [ { "date": from }, { "date": x }, { "date": to } ] }(链式比较)
一周之后{ "date": [ { "+": [ { "date": x }, 604800000 ] }, "iso" ] }
两个日期相差的天数{ "/": [ { "-": [ { "date": a }, { "date": b } ] }, 86400000 ] }
多个日期中最早的一个{ "min": [ { "date": a }, { "date": b } ] }

算术运算的结果又是毫秒数字,因此可以再放进 date 一次,以 isoday 输出(上表中的“一周之后”)。

// 表达式位置:优惠券是否在有效期内。三个值的写法可以互不相同
{ "<=": [
  { "date": "{ /coupon/fields/startsAt/en-US }" },
  { "date": "{ /now/iso }" },
  { "date": "{ /coupon/fields/endsAt/en-US }" }
] }
 
// 表达式位置:HTTP Date 头是否在距现在 5 分钟(300 秒)以内
{ "<": [ { "-": [ "{ /now/seconds }", { "date": [ "{ /headers/date }", "seconds" ] } ] }, 300 ] }

可读取的输入

下面这些值都会被读取为同一时刻。

格式示例
ISO-8601、RFC 33392026-10-03T00:00:00Z, 2026-10-03T00:00:00.000Z, 2026-10-03T09:00:00+09:00
省略了秒或小数部分的时刻2026-10-03T00:00
T 的位置放了空格的时刻2026-10-03 00:00:00
仅日期(按 UTC 零点读取)2026-10-03
RFC 1123(HTTP Date 头的表示形式)Sat, 03 Oct 2026 00:00:00 GMT
epoch 数字与数字字符串1790985600, 1790985600000, "1790985600"
  • 没有 offset 时按 UTC 读取。 offset 可以写成 +09:00+0900+09Z,这些写法都接受。
  • 解析是严格的。 即使位数正确,只要是实际不存在的日期(2026-13-45),也会失败。
  • epoch 按绝对值的大小来判断单位。 小于 100,000,000,000 时为秒,大于或等于该值时为毫秒。因此无论放入 { /now/seconds } 还是 { /now/millis },都会被各自正确读取。
  • 认定为 epoch 的范围是绝对值大于或等于 100,000,000 且小于 100,000,000,000,000。 因为要按大小来判断单位,所以在两端都设了界限。超出该范围的数字不会被读成 1970 年,而是会失败。不带分隔符的日期 20261003、年份 2026,以及表示没有值而传入的 0 都属于这种情况。

输出单位

第二个操作数决定输出形态。单位名称不区分大小写。

结果使用场景
省略、millisepoch 毫秒(数字)比较与算术
secondsepoch 秒(数字)。不足一秒的部分会被舍弃接受 epoch 秒的外部 API
iso2026-10-03T00:00:00.000Z写入 ContentDate 字段
day2026-10-03(以 UTC 为准)同一天的比较、界面显示

给出列表中没有的名称时会失败,错误消息会列出可用的名称。

iso 输出与写入 Date 字段

ContentDate 字段在写入时只接受 yyyy-MM-ddTHH:mm:ss[.小数点]Z 这一种格式。T、秒和结尾的 Z 都必须有,小数部分可以写也可以不写,值按 UTC 读取。因此,把通过 payload 收到的 2026-10-032026-10-03T09:00:00+09:00 原样放进去,会因为值无效而被拒绝。dateiso 输出正好就是这种格式,所以在把收到的日期写入字段之前,先让它经过一次 date

// 数据位置:把 payload 中的 "2026-10-03" 写入优惠券的到期日
"fields": { "endsAt": { "en-US": { "$date": [ "{ /payload/fields/endsAt }", "iso" ] } } }

无法读取的值

下面三种情况会让该语句失败(status 400)。这是执行过程中发生的失败,因此可以用 Trycatch 局部处理。

  • 第一个操作数缺失,或者引用没有找到值。
  • 无法把值读取为日期。空字符串、只有空白的字符串、不是日期的字符串、不存在的日期、布尔值、对象,以及超出认定范围的数字都属于这种情况。
  • 输出单位的名称不在列表中。

值缺失时不返回 null,这是有意设计的契约。null 在数字转换中会变成 0,从而被当作 1970 年来比较,于是缺少日期的检查不是失败,而是结果被反转。让超出有效期的优惠券通过,比让执行停下来更糟。

真假判定 (Truthiness)

ifandor!!! 以及 If.conditionLoop.while 按以下规则判定真假。

  • falsynullfalse、数字 0、空字符串 ""、空集合(空数组)。
  • truthy:其余全部(非 0 的数字、非空的字符串和数组、所有对象)。

键也可以引用

fields 这样的映射,其也支持 { /ptr } 引用。键会在运行时解析。

"fields": { "{ /payload/fields/fieldName }": { "en-US": "{ /payload/fields/fieldValue }" } }

如果两个键解析为相同的值,就会发生冲突并导致引擎报错。

Locale 映射:ContentMedia 特有的规则

WEEGLOO ContentMedia 的每个字段都不是单一值,而是按 Locale 划分的映射(例如 balance{ "en-US": 1, "ko-KR": 10 })。因此读写时必须连同 Locale 一起处理。Media 同样,其 titledescription(标量)、file(摄取指令)都是 Locale 映射。像 /payload 或 HTTP 响应这类并非 ContentMedia 的 JSON 则与此规则无关(保持 schema 所定义的结构,是标量就是标量)。

读取

  • 要取得标量,需Locale 一起指定:{ /<name>/fields/<field>/<locale> }(例如 { /post/fields/title/en-US })。
  • 不带 Locale 时,{ /<name>/fields/<field> } 会返回 Locale 映射的整个对象
  • localized:false 字段只存在于默认 Locale 存储桶中,因此用该默认 Locale 代码来读取。

写入 (ResourceCreate、ResourceUpdate、ResourcePatch 的 fields)

值是Locale 划分的映射 { "<locale>": <标量值表达式> }。与读取对称。

"fields": {
  "title":  { "en-US": "Hello", "ko-KR": "안녕" },   // 多个 Locale 就并列存储桶
  "status": { "en-US": "paid" }
}
  • ResourceCreate 必须在所有要填充的字段中都包含 Space 默认 Locale 存储桶(default-locale 规则)。
  • ResourceUpdate 是整体替换fields 中不存在的字段和 Locale 会被移除(包括 file)。
  • ResourcePatch 只更新指定的字段和存储桶(其余字段和 Locale 保持不变)。
  • 用字面量 null 删除:当值为字面量 null 时,会删除该 (field, locale) 存储桶(这是在 Patch 中清空特定 Locale 的标准做法)。""(空字符串)不是删除,而是设置为空值。值表达式({ /ptr })在运行时求值为 null 时不是删除,而是报错(不会静默吞掉 payload 的缺失)。删除只适用于字面量 null
  • Mediafile:值不是标量,而是摄取指令 { "source": …, "encoding": "url"|"base64" }。摄取实际上做了什么,在 Statement 目录的 ResourceCreate 中讲解。
  • localized:false 字段只放入默认 Locale 存储桶
  • Locale 代码(映射的键)也可以用 { /ptr } 引用(参见上文 键也可以引用)。用于创建动态 Locale

locale 便利字段

ResourceCreateResourceUpdateResourcePatch 中提供 locale 时,引擎会自动把 fields 的每个值包装成 { <locale>: 值 } 存储桶。 也就是说只需提供标量即可。

// 下面两个是等价的
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
  "locale": "en-US", "fields": { "title": "Hello" } }
 
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
  "fields": { "title": { "en-US": "Hello" } } }

如果在提供 locale 的同时又在值中嵌套了 Locale 映射({ "en-US": … }),就会变成 { <locale>: { "en-US": … } } 这样的双重嵌套(属于作者失误)。请统一为:用 locale 时只写标量,不用时只写显式的 Locale 映射。

where 与 order 中的 Locale

  • whereorder 中,fields.XSpace 默认 Locale 会被引擎自动应用(与 CMA 查询一致)。
  • 要指定特定的 Locale,请显式写成 fields.X.<locale>
"where": { "fields.slug": { "eq": "{ /payload/fields/slug }" } }   // 默认 Locale 的 slug
"where": { "fields.title.ko-KR": { "prefix": "안" } }              // 特定 Locale

错误

以下是违反值表达式规则时出现的错误码。这些检查在保存时进行;违反定义的其他静态约束的错误码在执行语义、约束与安全的错误中,调用时出现的错误码在端点的错误中。所有资源共通的错误码,请参见通用错误

错误码条件
WGL400056在数据位置上,$ 运算键与同一对象中的其他键并存。
WGL400055在数据位置上写了未被定义为运算符的 $ 键。