执行语义、约束与安全

最后更新:2026年7月21日

本文梳理 Script运行时如何工作(顺序、事务、错误、锁),保存时受到哪些静态约束,以及安全模型。语法参见 Statement 目录值表达式,实战组合参见菜谱

执行顺序与模式

  • statements 从上到下顺序执行。到达 Return 时在该处终止。
  • Sync 在处理请求的路径上执行,Async 在后台执行。 这只是执行位置的区分,无论哪一种,结果都是 Return 值(调用响应形态参见 Script 概述的请求与响应执行模式)。
  • 从能力(capability)到模式:如果 statement 树中含有 ExternalIoHttp 外部调用)、MediaIngestMedia 文件摄取。fields.file{ source, encoding },url 与 base64 通用)、LongRunning(大量 Loop 等)中的任意一个,executionMode 就会强制为 Async。这三者是相互独立的能力,在下面的静态约束中的计数对象各不相同。

执行语义

Guard(前置条件)

没有专用的 guard 语句。用 Ifthen:[Return] 表达。条件违反时返回结果,之后的 statement 不再执行(当然也可以有不带 guard 的 Script)。

{ "type": "If", "condition": { "<": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] },
  "then": [ { "type": "Return", "value": { "ok": false, "reason": "insufficient credit" }, "statusCode": 402 } ] }

无事务与 best-effort 补偿

Script 不是事务。 失败时引擎会尝试对目前已完成的操作进行补偿(compensation)并返回错误原因,但存在以下限制(设计上接受的取舍)。

  • 撤销删除会创建新的 sys.id,因此原本指向它的引用会失效(创建的回滚很容易,修改则需要 before-image)。
  • 外部副作用Http)不可逆(已发出的调用与计费无法撤销)。
  • 进程崩溃时可能残留未补偿状态(orphan)。

如果确实需要原子性,请由用户用 Script 亲自补偿,或者把不可逆的操作(外部调用等)放到最后。“能连锁执行却无法回滚、看上去却很安全”的顺序最危险。

乐观锁

update/patch 竞争通过 ResourceUpdateResourcePatchversion 来收窄。给出 version(值表达式,Int)后,只有当它与目标当前的 sys.version 一致时才更新,不一致则以版本冲突错误 abort(可用 Try/catch 局部处理)。省略时不做检查,采用 last-write-wins。通常先用 ResourceReadResourcePageRead 读取,再传入其 sys.version(参见菜谱中的乐观锁 CAS)。

基于 origin 的写入

写入始终反映到 origin(draft),delivery(CDA/ACDA)的暴露由 publish 控制(ResourceCreate/ResourceUpdate/ResourcePatchpublish,或 ResourcePublish/ResourceUnpublish)。

什么算失败

  • 真正的失败是 statement 运行时错误:Http 的最终 status 大于等于 400(4xx·5xx;ignoreStatusCode: true 时不算失败)或超时、响应 body 超过 10MiB、资源操作失败(对象不存在、版本冲突、不支持的运算等)。这类失败会由引擎 abort 并补偿,可用 Try/catch/finally 局部处理。
  • Return 不是错误,而是正常的提前终止。它不是 catch 的对象(没有用户 throw 的概念)。
  • catch 内通过 /error 引用 { message, statement }

无服务端聚合

没有专用于 count、sum、group-by 的服务端操作。需要通过遍历 ResourcePageRead 并用 SetVar/JsonLogic 计算,因此受限于 fetch 大小和 maxIterations(不适合数百万条的聚合)。

无等待与延迟

Script 中没有 Delay 语句。Script 在请求路径(Sync)或后台(Async)中只执行一次就结束,不会在内部等待或轮询直到外部 job 完成(Async 结果是另一回事。调用方用 202 收到的 requestId 轮询以获取 Return 值)。

静态约束(保存时校验)

以下会在保存(创建/修改)Script 的时刻检查。违反时保存会被拒绝(不是运行时,而是在编写时失败)。

约束默认值
存在外部 I/O 时 executionModeAsync不适用
禁止Loop body 内进行 Http 外部调用与 Media 文件摄取不适用
每个定义最多 Http 外部调用3(maxExternalIo
每个定义最多 SetVar(含嵌套)5(maxSetVar
每个定义全部 statement 最多(含嵌套)15(maxStatements
Http.retry 上限2(maxHttpRetry

上限可通过服务器配置(weegloo.core.script.*)调整(上表为默认值)。

Media 文件摄取属于 MediaIngest 能力,与 Http 外部调用(ExternalIo)不同,不计入 maxExternalIo(3)的上限。 不过 Async 强制和 Loop body 禁止会与 Http 完全相同地适用。

时间预算(运行时)

模式默认预算
Sync10 秒(syncTimeoutMs
Async60 秒(asyncTimeoutMs

各套餐的数量上限

ScriptBillable 资源,每个 Organization 的数量按套餐限制。

PlanScript 数量
Free3
Basic10
Pro50
Enterprise无限制

达到上限后,新建 Script 会被拒绝(与其他 Billable 资源同一路径)。

安全模型

secret 请求头

Http.headerssecret:true 的项目仅限 CMA(管理员),不会暴露给最终用户(ServiceUser),且仅在发送前才解密。像 LLM API 密钥这样的机密放在这里(即使被打包为 App Bundle,secret 值也会被掩码,不会离开原始 space)。

执行身份与授权

  • 执行身份:执行期间所有资源操作都以调用 /execute 的用户身份执行。被创建或修改的资源的 createdBy/updatedBy 即为调用方,createdBy: ":self" 作用域也以调用方为基准解析。
  • 授权边界有两个,而且运行时不会对每个 statement 重新检查资源权限。
    1. 编写时(保存):保存 Script 时,会检查作者是否确实拥有这些 statement 所写入的资源与动作权限。只要缺少任意一个,保存就会被拒绝(WGL403015)。也就是说,包含无权限操作的 Script 从一开始就无法保存。
    2. 调用时(/execute:只检查调用方的 Script Execute 权限。没有权限则返回 403。通过后,各 statement 的资源权限在运行时不再确认即执行。这与编程中的函数执行权限方式相同。只要有执行函数的权限,其内部各个操作的权限就不会再被询问。
  • 所有权作用域where 过滤器中的 createdBy: ":self" 表示“仅限当前调用方创建的对象”(例如只查询我自己的钱包)。
  • 委托的权限(编写注意):把上述两个边界合起来看,Script 的执行等同于受委托作者的权限来执行。调用方只需拥有 Execute 一项即可,Script 内的 statement 会在作者保存时被授权的范围内照常执行。因此,调用方自己无法完成的资源操作也可能通过 Script 发生。授予作者的权限即是该 Script 的影响范围,所以要谨慎设定 Script 中包含的动作。

摘要检查清单

保存前请确认以下几点。

  • 存在外部调用(Http)或 Media 文件摄取时,executionMode"Async"
  • 没有把外部调用放进 Loop body 内。
  • 外部调用不超过 3 个、SetVar 不超过 5 个、全部 statement 不超过 15 个。
  • secret 值仅通过 Http.headerssecret:true 放入。
  • 尽量把不可逆的操作(外部调用)放到后面。
  • 如果担心 update/patch 竞争,就使用 ResourceUpdateResourcePatchversion
  • 若要返回结果,已明确指定 Return.value