发现于 #3808 的实施过程(那一单给 record:related_list.add 补 inputs 声明时,apps/console 的 record-reach 探针按声明自动生成 fixture,type: 'object' 的通用样本 {} 直接把这个块炸了 —— 探针的报错原文见下)。围栏外,#3808 的 PR 只把 fixture 补成 spec 合法值(缺 picker 的 {} 本来就不是任何作者能发布的元数据),没有动渲染器。未认领,交 PM triage。
机制
packages/plugin-detail/src/RelatedList.tsx 对同一条路径有两套取法(objectui origin/main @ c85268256):
:378 const pickerObject = add?.picker?.object; // 可选链
:390 const pickerDisplayField = add?.picker?.labelField || 'name'; // 可选链
...
:1293 {add && dataSource && ( // 只判了 add 真值
:1299 objectName={add.picker.object} // 裸取 → 抛
:1293 的门只问「add 是不是真值」,{} 是真值。于是 :1299 抛 TypeError: Cannot read properties of undefined (reading 'object'),SchemaRenderer 捕获后把整个块换成错误卡:
Component "record:related_list" failed to render
Cannot read properties of undefined (reading 'object')
:724(add.picker.valueField)也是裸取,但它在 handleAddRecords 回调里、:723 有 if (!add || …) return 前置,只在用户真的选完记录后才会走到 —— 不是渲染期崩溃点。唯一的渲染期崩溃点是 :1299。
可达性(为什么这不是纯 dormant)
前提是 add 存在而 add.picker 不存在,这是 spec 拒绝的形状(picker 在 RecordRelatedListProps.add 里是必填,实测 safeParse({... add: { label: 'Add' } }) 报 invalid_type on add.picker)。所以只有不合规元数据能触发。
但渲染器拿到的是原始 schema,链路上没有任何一处对它做 Zod 解析:
所以一个作者(尤其 AI 作者)写下
{ "type": "record:related_list", "objectName": "sys_user_position",
"relationshipField": "user", "columns": ["position"],
"add": { "label": "指派职位" } }
—— 忘了 picker,或者以为 picker 可选 —— 得到的不是「Add 按钮没出现」,而是整段相关列表消失,换成一张红卡,报错文案里没有 picker 三个字。#3808 给 add 补的 description 现在会把 shape 写在 manifest 里,这降低了写错的概率,但没有改变写错之后的后果。
修法的取舍(所以只记录,不在 #3808 里顺手改)
按 AGENTS.md #0.1,「消费端加宽容」是被明确反对的方向,所以 :1299 直接改成 add?.picker?.object 需要想清楚它属于哪一类:
- (a) 一致化守卫:
:1293 的门从 add && 收紧成 add?.picker?.object &&,让 :1299 的裸取变成不可达,而不是给它加可选链。这不是"接受不合规元数据"——它是不渲染一个配置不全的 Add affordance,和 :378/:390 已有的写法同向,也不产生第二种方言(缺 picker 的 add 仍然什么都不做)。我倾向这一条。
- (b) 显式诊断:缺
picker 时渲染 designer placeholder / console.warn,点名 add.picker 缺失。比静默更好,但要决定用哪套诊断通道(这个渲染器已有 record:related_list — missing objectName 的 placeholder 先例,见 renderers/record-related-list.tsx)。
- (c) 在产出端拦住:让写页面元数据的路径真的跑一次 spec 解析,这是最正的方向,但范围远超本单(牵动 save-gate /
os build),而且不改变已在野的坏元数据的渲染后果。
(a) 与 (b) 不互斥,(b) 是 (a) 的加强版。三条都不是纯实现细节 —— (b) 定的是诊断契约,(c) 定的是校验时机。
顺带:同族的裸取只此一处
grep -n "add\.picker" packages/plugin-detail/src/RelatedList.tsx 只有 :724 与 :1299 两处裸取,其余全部可选链;:724 有前置 return 且不在渲染路径。所以这是一个点的修复,不是一片。
参考位置
关联:#3808、#3809(同一道门"只比键名"的另一处盲区)、#3831(add.picker.filter 未接线)、AGENTS.md #0.1
发现于 #3808 的实施过程(那一单给
record:related_list.add补inputs声明时,apps/console的 record-reach 探针按声明自动生成 fixture,type: 'object'的通用样本{}直接把这个块炸了 —— 探针的报错原文见下)。围栏外,#3808 的 PR 只把 fixture 补成 spec 合法值(缺picker的{}本来就不是任何作者能发布的元数据),没有动渲染器。未认领,交 PM triage。机制
packages/plugin-detail/src/RelatedList.tsx对同一条路径有两套取法(objectuiorigin/main@c85268256)::1293的门只问「add是不是真值」,{}是真值。于是:1299抛TypeError: Cannot read properties of undefined (reading 'object'),SchemaRenderer捕获后把整个块换成错误卡::724(add.picker.valueField)也是裸取,但它在handleAddRecords回调里、:723有if (!add || …) return前置,只在用户真的选完记录后才会走到 —— 不是渲染期崩溃点。唯一的渲染期崩溃点是:1299。可达性(为什么这不是纯 dormant)
前提是
add存在而add.picker不存在,这是 spec 拒绝的形状(picker在RecordRelatedListProps.add里是必填,实测safeParse({... add: { label: 'Add' } })报invalid_typeonadd.picker)。所以只有不合规元数据能触发。但渲染器拿到的是原始 schema,链路上没有任何一处对它做 Zod 解析:
sdui-parser的 manifest 门只比顶层键名与粗类型,add是个object、{}也是object—— 零诊断(这正是 parity 门的反方向没推到全仓:7 个 block 共 15 个 spec 已声明的顶层键没有任何inputs发布,其中 3 个渲染器实读(record:details.hideFields/record:related_list.relationshipValueField/element:text_input.defaultValue) #3808 落的那道门在 LIMIT 段落里写明看不见成员形状的原因);validateComponentProps是 advisory;record:related_list的渲染器自己不校验。所以一个作者(尤其 AI 作者)写下
—— 忘了
picker,或者以为picker可选 —— 得到的不是「Add 按钮没出现」,而是整段相关列表消失,换成一张红卡,报错文案里没有picker三个字。#3808 给add补的 description 现在会把 shape 写在 manifest 里,这降低了写错的概率,但没有改变写错之后的后果。修法的取舍(所以只记录,不在 #3808 里顺手改)
按 AGENTS.md #0.1,「消费端加宽容」是被明确反对的方向,所以
:1299直接改成add?.picker?.object需要想清楚它属于哪一类::1293的门从add &&收紧成add?.picker?.object &&,让:1299的裸取变成不可达,而不是给它加可选链。这不是"接受不合规元数据"——它是不渲染一个配置不全的 Add affordance,和:378/:390已有的写法同向,也不产生第二种方言(缺picker的add仍然什么都不做)。我倾向这一条。picker时渲染 designer placeholder / console.warn,点名add.picker缺失。比静默更好,但要决定用哪套诊断通道(这个渲染器已有record:related_list — missing objectName的 placeholder 先例,见renderers/record-related-list.tsx)。os build),而且不改变已在野的坏元数据的渲染后果。(a) 与 (b) 不互斥,(b) 是 (a) 的加强版。三条都不是纯实现细节 —— (b) 定的是诊断契约,(c) 定的是校验时机。
顺带:同族的裸取只此一处
grep -n "add\.picker" packages/plugin-detail/src/RelatedList.tsx只有:724与:1299两处裸取,其余全部可选链;:724有前置 return 且不在渲染路径。所以这是一个点的修复,不是一片。参考位置
packages/plugin-detail/src/RelatedList.tsx:1293-1299(崩溃点)、:378/:390(同一路径的可选链写法)、:723-724(回调里的裸取,有前置 return)packages/plugin-detail/src/renderers/record-related-list.tsx——missing objectName的 placeholder 先例((b) 路线的现成形状)apps/console/src/__tests__/record-block-record-reach.test.tsx—— 按声明生成 fixture 的探针;parity 门的反方向没推到全仓:7 个 block 共 15 个 spec 已声明的顶层键没有任何inputs发布,其中 3 个渲染器实读(record:details.hideFields/record:related_list.relationshipValueField/element:text_input.defaultValue) #3808 把它的add样本补成了 spec 合法的{ picker: { object } },并在注释里指向本单关联:#3808、#3809(同一道门"只比键名"的另一处盲区)、#3831(
add.picker.filter未接线)、AGENTS.md #0.1