Skip to content

[Bug] 侧边对话等第三方 markdown 表面里的 dsh-ui 围栏永不渲染:isGenericGenuiFence 硬依赖宿主 assistant 行(L375) #354

Open

Description

@Faye-Night

环境

项 值
@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 都没触发,说明它在接管判定阶段就被拒,而不是解析/挂载失败)。

复现

  1. 安装 genui 0.13.0 + better-sidebar 0.28.0,宿主 0.2.1-rc.2。
  2. 主会话里发一条含 dsh-ui 围栏的助手回复 → 渲染为卡片(基线正常)。
  3. 打开右侧栏「侧边对话」页签,向该线程提问,让它的助手回复里包含同一份 ```dsh-ui 围栏。
  4. 观察:侧边对话里是带「代码块」横幅的 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 偶然拦截。

关联

Activity

  1. sunxiuguo commented on Oct 21, 2026

    @sunxiuguo
    Contributor

    /claim

    先认领本 issue 的复现与接入方案对齐,不把“放宽 DOM 守卫”直接当修复,也不先引入新公共协议。

    基于 GenUI 7301d8a8 / BetterSidebar 83faf709 核过源码:已有 /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。

  2. taekchef commented on Oct 22, 2026

    @taekchef
    Collaborator

    @Faye-Night 感谢你的反馈和 @sunxiuguo 的实现。进度更新(2026-10-23):GenUI 侧的 #360 已合入 main,使用显式接入方式渲染侧聊,并将操作限定到所属子会话。PR 的 rc.2 Web 双插件检查已通过,使用的是合成会话和本地回放,尚未完成原 Windows Desktop 环境验收。

    这项能力尚未包含在正式版 0.14.0;配套的 Better Sidebar #1033 仍未合并,当前存在合并冲突。需要先完成配套 PR 的合并和双方发布,再复测原环境里的侧聊渲染、状态隔离与操作归属。本 issue 继续保留。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    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