来源
发现自 #3842 实施(渲染器侧 disabled 的「已声明」判定补 !== '')的同族扫面。不在 #3842 的修法面(PM 已裁其范围为 DeclaredActionsBar + action:button 两处渲染落点),故独立成单,未认领。
机理
packages/core/src/actions/ActionRunner.ts:666 的执行门:
if (action.disabled != null && action.disabled !== false) {
let isDisabled = false;
try { isDisabled = this.evaluator.evaluateCondition(action.disabled as never); } catch { isDisabled = false; }
if (isDisabled) return { success: false, error: 'Action is disabled' };
}
两点都与渲染器侧不同:
- 入门判定是
!= null && !== false,没有 !== '' —— 空谓词进门;
- 把 原始值交给
evaluateCondition,而不是先过 toPredicateInput。evaluateCondition 对空/纯空白/空 source 的语义是「没有条件 → true」,而这里 true 意味着禁用。
于是任何「空谓词」拼法都被判成「已禁用」并拦掉执行。
实测(worktree @ origin/main = aca561a77fd0dc793ea76a7b46cd037e1731eb93,一次性探针,未提交)
new ActionRunner({}) + registerHandler('probe'),只改 disabled 一个键:
disabled '' | handler ran: false | result: {"success":false,"error":"Action is disabled"}
disabled ' ' | handler ran: false | result: {"success":false,"error":"Action is disabled"}
disabled {dialect:'cel',source:''}| handler ran: false | result: {"success":false,"error":"Action is disabled"}
disabled absent | handler ran: true | result: {"success":true}
disabled false | handler ran: true | result: {"success":true}
handler 一次没跑 —— 不是「点了没反应」,是执行被门拦下并回了一个 Action is disabled 错误。
与渲染器侧的分歧(#3314 同一类)
同一个 disabled 值,两条路给出相反答案(渲染器侧数据取自同一批探针,PR 落地后):
disabled |
渲染器(修完 #3842 后) |
ActionRunner.execute |
'' |
可点 |
拦 |
' '(纯空白) |
可点('${ }' 求值为 false) |
拦 |
{dialect:'cel', source:''} |
置灰(#3842 残留,另单) |
拦 |
false / 未声明 |
可点 |
放行 |
所以 #3842 修完之后的用户可见行为是:disabled: '' 的服务端声明动作按钮能点了,点下去收到 Action is disabled 报错。比原来「永久置灰、无任何解释」是改善(至少有了消息),但这半边不修,该动作仍然执行不了。渲染器与执行入口对同一谓词不同意,正是 #3314 已经付过一次代价的形状。
影响
修法建议(未裁)
契约优先:「是否声明了 disabled 门」这个问题在仓里已有唯一定义(hasDeclaredVisibilityGate,packages/components/src/renderers/action/visibility-gate.ts,!= null && !== ''),而「谓词如何归一」也已有唯一定义(toPredicateInput,packages/core/src/evaluator/predicateInput.ts)。这处执行门两个都没用。
方向:入门判定改问「已声明」,并把值过 toPredicateInput 后再求值,使执行侧与渲染侧逐一同意。注意 hasDeclaredVisibilityGate 目前住在 components,而这里是 core(依赖方向相反)—— 定义大概要下沉到 core(与 toPredicateInput 同层)才谈得上复用,那是跨包动作,请先裁。⚠️ 现有 catch { isDisabled = false }(求值失败不误拦)的姿态应保留。
Related: #3842(渲染器侧同族,PR: claude/issue-3842-disabled-declared-gate)、#3492(不变量出处)、#3314(渲染路径与引擎路径判定分歧的先例)、#1885(disabled 首次接线)。未认领。
来源
发现自 #3842 实施(渲染器侧
disabled的「已声明」判定补!== '')的同族扫面。不在 #3842 的修法面(PM 已裁其范围为DeclaredActionsBar+action:button两处渲染落点),故独立成单,未认领。机理
packages/core/src/actions/ActionRunner.ts:666的执行门:两点都与渲染器侧不同:
!= null && !== false,没有!== ''—— 空谓词进门;evaluateCondition,而不是先过toPredicateInput。evaluateCondition对空/纯空白/空source的语义是「没有条件 →true」,而这里true意味着禁用。于是任何「空谓词」拼法都被判成「已禁用」并拦掉执行。
实测(worktree @
origin/main=aca561a77fd0dc793ea76a7b46cd037e1731eb93,一次性探针,未提交)new ActionRunner({})+registerHandler('probe'),只改disabled一个键:handler 一次没跑 —— 不是「点了没反应」,是执行被门拦下并回了一个
Action is disabled错误。与渲染器侧的分歧(#3314 同一类)
同一个
disabled值,两条路给出相反答案(渲染器侧数据取自同一批探针,PR 落地后):disabled''' '(纯空白)'${ }'求值为 false){dialect:'cel', source:''}false/ 未声明所以 #3842 修完之后的用户可见行为是:
disabled: ''的服务端声明动作按钮能点了,点下去收到Action is disabled报错。比原来「永久置灰、无任何解释」是改善(至少有了消息),但这半边不修,该动作仍然执行不了。渲染器与执行入口对同一谓词不同意,正是 #3314 已经付过一次代价的形状。影响
source)。ActionRunner.execute的动作,不限于某个渲染面 —— 这是 core 的公共执行入口。disabled的「已声明」判定用!= null,disabled: ''把按钮永久置灰(#3492 同族的另一半 predicate,探针实证) #3842 相反的方向也存在:' '在渲染器侧可点、在执行侧被拦,任何用「按钮能点」推断「动作能跑」的人都会被误导。修法建议(未裁)
契约优先:「是否声明了
disabled门」这个问题在仓里已有唯一定义(hasDeclaredVisibilityGate,packages/components/src/renderers/action/visibility-gate.ts,!= null && !== ''),而「谓词如何归一」也已有唯一定义(toPredicateInput,packages/core/src/evaluator/predicateInput.ts)。这处执行门两个都没用。方向:入门判定改问「已声明」,并把值过⚠️ 现有
toPredicateInput后再求值,使执行侧与渲染侧逐一同意。注意hasDeclaredVisibilityGate目前住在components,而这里是core(依赖方向相反)—— 定义大概要下沉到core(与toPredicateInput同层)才谈得上复用,那是跨包动作,请先裁。catch { isDisabled = false }(求值失败不误拦)的姿态应保留。Related: #3842(渲染器侧同族,PR:
claude/issue-3842-disabled-declared-gate)、#3492(不变量出处)、#3314(渲染路径与引擎路径判定分歧的先例)、#1885(disabled首次接线)。未认领。