Repository navigation
[Bug] 侧边对话等第三方 markdown 表面里的 dsh-ui 围栏永不渲染:isGenericGenuiFence 硬依赖宿主 assistant 行(L375) #354
Description
环境
| 项 | 值 |
|---|---|
@changfenhuang/dsh-genui |
0.13.0(~/.dsh/profiles/desktop/node_modules/@changfenhuang/dsh-genui) |
dsh-better-sidebar |
0.28.0(~/.dsh/profiles/desktop/node_modules/dsh-better-sidebar) |
| DSH 宿主 | 0.2.1-rc.2(dsh --version) |
| 平台 | Windows,Web GUI |
| 通道 | fence-channel=dom(宿主无 fence-registry 扩展点,见 #275) |
症状
主会话正文里的 ```dsh-ui 围栏正常渲染为组件;同样的围栏出现在 dsh-better-sidebar 的「侧边对话」页签的助手回复里时,永远停留在原始 JSON 代码块:不渲染、无红色诊断条、控制台无任何 [dsh-genui] 告警(连 warnOnce 都没触发,说明它在接管判定阶段就被拒,而不是解析/挂载失败)。
复现
- 安装 genui 0.13.0 + better-sidebar 0.28.0,宿主 0.2.1-rc.2。
- 主会话里发一条含
dsh-ui围栏的助手回复 → 渲染为卡片(基线正常)。 - 打开右侧栏「侧边对话」页签,向该线程提问,让它的助手回复里包含同一份
```dsh-ui围栏。 - 观察:侧边对话里是带「代码块」横幅的 JSON 代码块;主会话里同样的围栏是卡片。
根因:接管链上有两条硬契约,只有宿主主会话行能满足
src/client/dom-fence.tsx(上游 main,行号以当前 main 为准)里,一个候选块要被接管,必须拿到 language === 'dsh-ui' | 'svg',或 genericGenui === true:
// L1168-1169
const genericGenui = language === undefined && settled && isGenericGenuiFence(block, raw)
if (language !== 'dsh-ui' && language !== 'svg' && !genericGenui) return侧边对话的三条取值路径全断:
① DOM 横幅 domLanguageOf(L312-317) — 宿主的 CodeBlock 横幅对高亮器不认识的语言显示本地化通用词(R_(e) ? e : i.codeLabel,中文即「代码块」),不会显示 dsh-ui。KNOWN_LANGUAGE_LABELS(L291)不含「代码块」→ 返回 null。与 #275 / #258 是同一根因。
② ChatSnapshot 来源 sourceLanguageOf(L648-657) — L649 要求
const row = block.closest<HTMLElement>(`${ASSISTANT_FLOW_ROW}[data-chat-node-key]`)
if (row === null || row.dataset.chatGroupPart === 'reasoning') return undefined侧边对话是 better-sidebar 用宿主 MarkdownText 自渲染的独立对话,DOM 里既没有 data-chat-flow-kind 也没有 data-chat-node-key(对 dsh-better-sidebar/lib/*.js 全量检索,两者命中数均为 0),它也不在主会话的 ChatSnapshot 里 → 必 undefined。
③ 内容兜底 isGenericGenuiFence(L373-397) — 第一行就是硬前提:
const row = block.closest<HTMLElement>(ASSISTANT_FLOW_ROW) // L374
if (row === null || row.dataset.chatGroupPart === 'reasoning') return false // L375侧边对话没有 [data-chat-flow-kind="assistant-step"] 祖先 → 正文再合规也直接 false。
于是 L1169 的闸门拒绝接管。
关键:这不是「选择器没匹配到」
codeBlockSurfaces(L78-84)靠.md-code-block+[data-code-block-content]定位;侧边对话的代码块由宿主MarkdownText产出,与主会话同形。better-sidebar 的SideChatView.renderRow对assistant行是jsx(MarkdownText, { ...markdownTextProps(rewrite(row.text), labels) })。surfaceRejection(L448-458)只排 genui 自己的容器,SURFACE_EXCLUDES(L120)不含任何侧栏标记 → 候选能被正常发现并enqueueCandidate,最终卡在语言判定上。
一处不一致,可能是线索
SURFACE_EXCLUDES(L120)与 TAG_TEXT_EXCLUDES(L123)不对称:
const SURFACE_EXCLUDES = `.${CONTAINER_CLASS}, .${DIAGNOSTIC_CLASS}, [data-genui-svg-fence]` // L120
const TAG_TEXT_EXCLUDES = `... [data-sidebar-chat], [data-sidebar-right-session], [data-sidebar-right-panel], [data-panel-conversation], [data-plugin-panel]` // L123后者显式排除侧栏/插件面板,前者一个都没有。而 better-sidebar 的侧边对话正是通过 sidebar.right.pane.tab 注册、渲染在宿主右侧栏容器内(宿主 SidebarPanel 在该容器上打出 data-sidebar-right-session / data-sidebar-right-panel)。
所以:
- 如果「侧栏里的围栏不该被接管」是有意策略,那
SURFACE_EXCLUDES应补上同批标记 —— 现在只靠 L375 偶然拦住,而且拦得过头:它拦掉的是所有没有宿主 assistant 行的合法表面,不只是侧栏。 - 如果希望支持,L375 就不能作为硬前提。
无论取哪边,现状都是「靠一条为别的原因写的守卫偶然拦截第三方 markdown 表面」。
为什么这是 genui 侧的问题
- 侧边对话的 DOM 是宿主
MarkdownText产出的,与主会话代码块同形;better-sidebar 没有义务知道 genui 的存在。 - 拒绝发生在 genui 自己的
isGenericGenuiFence,而理由(「没有宿主 assistant 行」)与「这段 markdown 到底是不是dsh-ui围栏」无关。 - genui 已经为「宿主 DOM 漂移」准备了两条兜底(
sourceLanguageOf、isGenericGenuiFence),但两条都绑在同一种宿主行上 —— 第三方 markdown 表面是这条设计的盲区。
建议修法(代价从低到高)
A. 放宽 isGenericGenuiFence 的行前提(最小改动)
把 L374-375 降级为「有 assistant 行时额外排除 reasoning / 无行时不做要求」,其余原样复用 L378-396 的现有内容校验([data-code-block-banner] 存在、domLanguageOf(block) === null、正文经 tier-1 修复后 validateCanonicalGenuiSpec 通过、normalize 往返一致)。主会话行为不变,第三方表面获得兜底。
B. 提供可注入的表面契约(更稳)
接受一个显式标记(例如 [data-genui-surface],或复用 better-sidebar 已在用的 data-dsh-md-surface),打了标记的容器内允许按内容识别接管;同时给 sourceLanguageOf 增加一条不依赖 ChatSnapshot 的来源(容器上的 data-genui-language)。这样第三方插件能主动声明。
⚠️ 不要建议第三方伪造data-chat-flow-kind/data-chat-node-key:那会让 genui 去主会话 ChatSnapshot 按 nodeKey 查错行,比不渲染更糟。
C. 长期:宿主提供 markdown fence renderer registry,替代 DOM 观察通道(#275 已记录该扩展点消失)。
二级隐患:即便渲染打通,交互会发到错误的会话
sendActionForBlock(L1151-1158)恒用sessionIdOf()→sourceSessionOf(L742-754)→resolveViewedSessionId(src/client/session-resolver.ts,取retainedBy.mainView > 0的会话)。侧边对话的子会话不是主视图 → 解析结果是当前主会话,[genui-action]会被转发给主会话。contextOf(L1030-1047)同理:无data-chat-anchor-key时 key 降级为dom:unknown:N(L1039),多个侧边对话线程会撞同一批 key。
建议 A/B 一并处理:至少让被接管的块携带「所属会话」的来源,或对该类表面禁用需要回传的交互组件。
期望
侧边对话(以及任何第三方 markdown 表面)里的 dsh-ui 围栏能与主会话一致地渲染;若决定不支持,请在 SURFACE_EXCLUDES 显式排除并在 README 写明边界,而不是靠 L375 偶然拦截。
关联
- DSH 0.2.1-alpha.1:
dsh-ui围栏不再渲染(fence-registry 扩展点已不存在;DOM 通道靠横幅字样识别,而宿主横幅对非高亮语言显示「代码块」) #275 — 同一根因:宿主横幅对非高亮语言显示通用词 dsh-uifences never render when the host locale is not English (generic code-banner label is treated as a real language) #258 —domLanguageOf判定反转,正是为这个场景做的- dsh-ui 围栏静默不渲染(无任何 [dsh-genui] 日志;硬刷新后仍不渲染) #313 — 静默不渲染(本例连
warnOnce都不触发,比 dsh-ui 围栏静默不渲染(无任何 [dsh-genui] 日志;硬刷新后仍不渲染) #313 更早被拒)
Activity
sunxiuguo commentedon Oct 21, 2026
/claim
先认领本 issue 的复现与接入方案对齐,不把“放宽 DOM 守卫”直接当修复,也不先引入新公共协议。
基于 GenUI
7301d8a8/ BetterSidebar83faf709核过源码:已有/embed能复用渲染器;侧边对话动作应使用子threadId,不能回落到主会话。转录中同一事件可有多个文本块,因此还需保留原 content block index,避免状态串到另一块。我做了一个仅合成数据的隔离消费者:主会话和两个子会话放相同围栏,验证动作回调、重渲染/重挂载、身份与内容变化、无身份拒绝接管,最终 20 项测试通过。独立复核曾发现并修正该消费者自己的身份校验遗漏。这里用的是 React/jsdom 和现有 UI primitives 0.2.1-rc.1,回调是 spy;还没完成 rc.2 真实双插件宿主验收,不能据此说 #354 已修好。
/embed是编译期入口,不是现成跨插件服务;BetterSidebar 的接入规则要求跨插件交互走ctx.betterSidebar。想先确认:维护者更倾向 GenUI 提供显式 opt-in 的 surface 适配,还是在 BetterSidebar 服务中增加受控的 sidechat 渲染扩展点?方向确认后再界定最小接入范围与验证门槛,保持 source/thread/文本块/围栏身份隔离、明确动作回调及无能力时原 Markdown 回退,不伪造主会话 DOM 属性。AI 辅助分析与测试,结果已独立复核;尚未提交生产代码或 PR。
taekchef commentedon Oct 22, 2026
@Faye-Night 感谢你的反馈和 @sunxiuguo 的实现。进度更新(2026-10-23):GenUI 侧的 #360 已合入 main,使用显式接入方式渲染侧聊,并将操作限定到所属子会话。PR 的 rc.2 Web 双插件检查已通过,使用的是合成会话和本地回放,尚未完成原 Windows Desktop 环境验收。
这项能力尚未包含在正式版 0.14.0;配套的 Better Sidebar #1033 仍未合并,当前存在合并冲突。需要先完成配套 PR 的合并和双方发布,再复测原环境里的侧聊渲染、状态隔离与操作归属。本 issue 继续保留。