Skip to content

action body 里写 ctx.record 被静默丢弃 —— 已声明字段也一样(#4271 家族) #4345

Description

@os-zhuang

现象

L2 action body 里对 ctx.record 的任何赋值都被静默丢弃 —— 已声明字段和未声明字段一样。没有任何诊断,action 照常返回成功。

defineAction({
  name: 'close_deal',
  objectName: 'crm_deal',
  type: 'script',
  body: {
    language: 'js',
    // stage 是 crm_deal 上确实声明了的字段
    source: "ctx.record.stage = 'won'; return { ok: true };",
    capabilities: ['api.write'],
  },
});

调用后 action 返回 { ok: true },记录没有任何变化。

为什么

packages/runtime/src/sandbox/body-runner.ts:

  1. buildActionSandboxContext 交给 body 的是一份普通快照,不是 proxy:
    record: unwrapProxyToPlain(actionCtx?.record),
    unwrapProxyToPlainObject.fromEntries(Object.entries(v)) —— 一个新对象。
  2. boundActionHandlerreturn result.value。hook 路径上把 VM 内改动写回宿主的 applyMutationsToInput,action 路径根本没调用

所以写进 VM 里那份快照的东西,连出 VM 的机会都没有。

为什么值得单独记一笔

这是 #4001 那一族("静默 no-op 制造假完成")的形状:作者以为自己改了记录,runtime 说成功,数据没动。

#4271 / #4305 / #4344 处理的"未知列静默不落库"不是同一个缺陷:

  • 那个是"写了个不存在的字段,已声明的字段照常落库";
  • 这个是"所有写都不落库,包括完全正确的字段名"。

正因如此,#4344(action body 写集 lint)刻意不检查 ctx.record:在那里只报未声明字段,等于暗示已声明字段会落库 —— 反而放大了这个坑。留言在 validate-action-body-writes.ts 头注释里,指回本 issue。

目前的处境

没有任何文档声称 ctx.record 可写 —— ScriptContext 里写的是 "the record loaded by the dispatcher before the action ran"(packages/runtime/src/sandbox/script-runner.ts),读的语义。content/docs/ui/actions.mdx 里的示例也都只读(ctx.recordId || (ctx.record && ctx.record.id))。

所以这不是 declared ≠ enforced —— 没有东西声明它可写。但类型上是裸的 record?: unknown,而同一个 ctxctx.input 在 hook 里确实是写回去的,作者按类比推断"record 也能写"是完全合理的。

可选方向(需要决策,不是已定方案)

  1. 只读到底 + 说清楚。 ScriptContext.record 标注只读语义,ActionSchema.body 的文档写明"要落库走 ctx.api.object(...)",再加一条 advisory lint:action body 里出现 ctx.record.<field> = … 就告警(不管字段声没声明),提示改用 ctx.api。成本最低,跟当前实现一致。
  2. 让它写回。boundActionHandler 里补 record 的 mutation 写回。这需要先想清楚:写回到哪(dispatcher 预取的那份?)、要不要落库、跟 requiresRecord: false 怎么处理、权限走谁的 context。比看上去大,而且 action 的返回值语义(result.value)本来就是显式的。
  3. 干脆不给。ctx.record 从 action 沙箱上下文里去掉,只留 ctx.recordId,让作者显式 ctx.api.object(...).findOne(...)。破坏性最大。

倾向 1 —— action 的写路径本来就是 ctx.api,ctx.record 是预取的读快照,这个模型是自洽的,缺的只是"说出来 + 在作者时拦一下"。

参考

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions