现象
平台版本 @objectstack/*@17.0.0-rc.6。
记录详情页页头动作的 visible CEL 无法表达「只有本记录的某个责任人才看得见这颗按钮」这个最常见的诉求。以一个 lookup 字段 manager 为例:
- 能跑通的写法(
os.user.id == record.manager)对本人也恒为 false —— 按钮谁都看不见; - 直觉写法(
os.user.id == record.manager.id)求值抛错,而该处 catch 返回 true —— 按钮不是消失,而是对所有人显示,且无任何告警。
两条路都不通,且失败方向恰好相反:一条恒关,一条恒开。
复现与实测
每组重启服务,两个账号对照。manager 是 lookup(sys_user);账号 A = 本记录的 manager,账号 B = 非 manager 但有读权。
| 谓词 | A | B | 结论 |
|---|
os.user.id != null | 显示 | — | os.user 已绑定 |
os.user.id == 'A 的 uid 字面量'(直接写死 A 的 uid) | 显示 | — | os.user.id 即会话用户 id,左侧正常 |
os.user.id == record.manager | 隐藏 | 隐藏 | 对本人也恒 false(干净求值成假) |
os.user.id == record.manager.id | 显示 | 显示 | 求值抛错 → fail-open,两人都可见 |
排除项:
- 同一条记录经 REST 取回时,
manager 是与会话 uid 逐字相同的裸字符串 —— 已断言 String(uid) === String(rec.manager) 为 true。所以「值不相等」不成立,页头作用域下 record.manager 的取值形态与 REST 不一致; - 已排除「无读权导致 expand 静默降级成裸 id」:账号 B 能 200 读到 A 的用户记录,
?$expand=manager 仍返回裸串。
fail-open 是代码级事实,不是推断 —— console bundle(RecordDetailView-*.js)中该处为:
try{returnn.evaluateCondition(e.visible)}catch{returntrue}谓词一旦抛错,按钮对全体显示,console 里没有任何输出。
下游项目已在自己仓库记录了完整实测过程:steedos-labs/os-project-titanwind-ehr#874(私有仓,此处仅作实测记录索引)。
影响
页头动作是「只有责任人可见」这类权限收敛最自然的落点,而这条路径当前没有任何可用写法。更糟的是错误写法的失败方向是 fail-open:作者写完看到按钮在,会认为谓词生效了 —— 实际上它对所有人都在。静默使得这类问题按检视无法发现。
期望
- 取值形态对齐:页头作用域下 lookup 字段的取值与 REST 一致,或提供一个稳定的
record.某lookup字段.id 取法,并写进文档; - 不要静默 fail-open:
visible 求值异常改为 fail-closed,或至少在 console 输出告警。
与既有裁决 / 相邻单的关系(非重复)
现象
平台版本
@objectstack/*@17.0.0-rc.6。记录详情页页头动作的
visibleCEL 无法表达「只有本记录的某个责任人才看得见这颗按钮」这个最常见的诉求。以一个 lookup 字段manager为例:os.user.id == record.manager)对本人也恒为 false —— 按钮谁都看不见;os.user.id == record.manager.id)求值抛错,而该处catch返回true—— 按钮不是消失,而是对所有人显示,且无任何告警。两条路都不通,且失败方向恰好相反:一条恒关,一条恒开。
复现与实测
每组重启服务,两个账号对照。
manager是lookup(sys_user);账号 A = 本记录的 manager,账号 B = 非 manager 但有读权。os.user.id != nullos.user已绑定os.user.id == 'A 的 uid 字面量'(直接写死 A 的 uid)os.user.id即会话用户 id,左侧正常os.user.id == record.manageros.user.id == record.manager.id排除项:
manager是与会话 uid 逐字相同的裸字符串 —— 已断言String(uid) === String(rec.manager)为 true。所以「值不相等」不成立,页头作用域下record.manager的取值形态与 REST 不一致;?$expand=manager仍返回裸串。fail-open 是代码级事实,不是推断 —— console bundle(
RecordDetailView-*.js)中该处为:谓词一旦抛错,按钮对全体显示,console 里没有任何输出。
下游项目已在自己仓库记录了完整实测过程:steedos-labs/os-project-titanwind-ehr#874(私有仓,此处仅作实测记录索引)。
影响
页头动作是「只有责任人可见」这类权限收敛最自然的落点,而这条路径当前没有任何可用写法。更糟的是错误写法的失败方向是 fail-open:作者写完看到按钮在,会认为谓词生效了 —— 实际上它对所有人都在。静默使得这类问题按检视无法发现。
期望
record.某lookup字段.id取法,并写进文档;visible求值异常改为 fail-closed,或至少在 console 输出告警。与既有裁决 / 相邻单的关系(非重复)
visibleWhen/visibleOn) fail OPEN and silently — a broken predicate is indistinguishable from no predicate #5149(已迁至 Conditional-visibility predicates (visibleWhen/visibleOn) fail OPEN and silently — a broken predicate is indistinguishable from no predicate objectui#4051)处理的是evalFieldPredicate这条 funnel —— 字段级 / section 级 / page 组件级的visibleWhen。其 2026-08-06 裁决明确:fail-open 或 fail-closed 都可以裁,静默不可以,并已在 rc.5 给该 funnel 加上每谓词一次的实名 warn。visible,走的是RecordDetailView里的evaluateCondition,上面那段catch { return true }说明 Conditional-visibility predicates (visibleWhen/visibleOn) fail OPEN and silently — a broken predicate is indistinguishable from no predicate #5149 的响亮化没有覆盖到这里 —— 按既有裁决的口径,这一处仍然处在「不可以」的状态。visibleWhen/visibleOn) fail OPEN and silently — a broken predicate is indistinguishable from no predicate #5149 完全没有的一半:lookup 字段在该作用域下的取值形态问题(表格第 3、4 行),这半边与 fail-open 无关,单独修 fail-open 也不能让谓词写得出来。