Skip to content

[Bug] genui 与 python 代码框共存时渲染错误,仅保留 genui #13

Description

@Danieltoraji

Bug 报告

标题

[Bug] genui 与 python 代码框共存时渲染错误,仅保留 genui

严重程度

Medium —— 不影响单代码块使用,但在同一提示词内混用 genui 与普通代码块(如 python)时渲染结果错误,影响提示词模板复用。

环境

  • 组件:DeepSeek Harness Web GUI(http://127.0.0.1:3080)/ dsh-ui 渲染器
  • 插件:@omdsh-dev/dsh-genui 0.8.3(经 github:omdsh-dev/dsh-genui 安装)
  • 宿主:无 fence-registry 扩展点的 DSH 构建(插件因此走 DOM 渲染通道)
  • 触发方式:任意同时包含 dsh-ui 围栏与 python 围栏的提示词

复现步骤

  1. 输入一段提示词,内容为「简单重复下述话语」。
  2. 提示词内依次包含:
    • 一个 ```dsh-ui 围栏(内含 mermaid + table 组件);
    • 一个 ```python 代码块(如 @dataclass 类型提示代码)。
  3. 观察模型回复 / 界面渲染结果。

期望行为

dsh-ui 与 python 两个围栏应各自独立渲染:dsh-ui 呈现交互 UI,python 呈现代码块,两者同时存在且互不影响。

实际行为

当 genui(dsh-ui)与 python 代码框同时存在时,渲染错误——只留下 genui 渲染后的内容,python 代码框丢失或未渲染。

复现示例(最小可复现)

提示词

简单重复下述话语。

{"title":"从二值掩膜到中心线的正确流水线","gap":10,"items":[
  {"type":"mermaid","code":"flowchart LR\n    A[\"二值掩膜\"] --> B[\"① 连通域/轮廓分割\"]"},
  {"type":"table","columns":["步骤","做什么"],"rows":[["① 连通域分割","拆分成独立组件"]]}
]}
@dataclass
class LineSegment:
    points: list
    orientation: str

预期输出

  • [UI] dsh-ui 交互卡片正常渲染
  • [代码] python 代码块正常渲染

实际输出

  • [UI] dsh-ui 正常渲染
  • [代码] python 代码块缺失 ✗

触发条件(补充)

只有「一个 ```dsh-ui 围栏 + 任意第二个代码块(不限于 python,java / ts / bash 等皆可)同处一条消息」时才触发;消息里只有一个 dsh-ui 围栏时不会复现。根因是围栏发现的兜底逻辑误判,与具体语言无关。

根因分析

(以下代码分析我看不懂,只能放在这里供您参考。谢谢!)

宿主无 fence-registry 时,插件走 DOM 通道:靠 MutationObserver 观察会话 DOM,把带 dsh-ui 标签的代码块原位替换成 GenUI。问题出在围栏发现函数——lib/client.js 中的 zn / Rn / Pn(对应 src/client 里 DOM 通道的发现逻辑)。

宿主把这条消息渲染成大致如下 DOM:

<div class="markdown">              <!-- MarkdownText 根容器 -->
  <p>简单重复下述话语。</p>
  <div class="md-code-block">       <!-- dsh-ui 围栏 -->
    <div class="banner">…infostring 文本 = "dsh-ui"…</div>
    <pre>{"title":…}</pre>
  </div>
  <div class="md-code-block">       <!-- python 围栏 -->
    <div class="banner">…infostring 文本 = "python"…</div>
    <pre>@dataclass…</pre>
  </div>
</div>

围栏发现 zn() 分两步:

// 第一步:直接收集已知类名代码块
for (let r of e.querySelectorAll(jn))   // jn = ".md-code-block, .code-block, .code-block-small"
  (r.parentElement === null || r.parentElement.closest(jn) === null) && ( n.push(r) );

// 第二步(兜底):遍历所有 <pre>,向上回溯找 dsh-ui 围栏包裹层
for (let r of e.querySelectorAll(`pre`)) {
  let i = Rn(r, e);   // 从 <pre> 向上最多 4 层,找 Pn() === "dsh-ui" 的祖先
   n.push(i) 
}

判定函数 Pn(e) 用「元素里有一个 dsh-ui 叶子、且不在【第一个】<pre> 内」来认定 e 是 dsh-ui 围栏:

function Pn(e) {
  let t = e.querySelector(`pre`);     // e 里的【第一个】<pre>
  for (let n of e.querySelectorAll(`*`))
    if (n.childElementCount === 0 && n.textContent === `dsh-ui` && !(t !== null && t.contains(n)))
      return `dsh-ui`;
  return null;
}

对 python 的 <pre> 执行 Rn 向上回溯时:

  1. 第 0 层 = python 的 .md-code-blockPn 看到 banner 是 "python",返回 null;
  2. 第 1 层 = .markdown 根容器:Pn 先取「第一个 <pre>」(= dsh-ui 围栏的 <pre>),再找 dsh-ui 叶子——dsh-ui 围栏的 .infostring 正好是 "dsh-ui" 且不在该 <pre> 内 → Pn 返回 "dsh-ui"
  3. 于是 Rn 返回 .markdown 根容器,兜底循环把整条消息容器当成「围栏」加入列表。

随后挂载函数 c(root) 对这个根容器执行:

e.style.display = `none`;   // 隐藏【整条消息】
e.after(f);                 // 在整条消息后插入 GenUI

于是整条消息被 display:none,只剩插入的 GenUI。

一句话:Pn 用「第一个 <pre> + 不在该 <pre> 内的 dsh-ui 叶子」来判定,当 e 是包含多个代码块的容器时,嵌套代码块的 dsh-ui 标签被误当成 e 自己的标签;Rn 又向上回溯到了共享容器。

修复方案

推荐两个都改;方案一即可修掉本 issue,方案二是对判定谓词本身的加固。

方案一(最小改动):兜底循环跳过「已由第一步选择器命中」的 <pre>,避免对已识别代码块内的 <pre> 再做向上回溯:

for (let r of e.querySelectorAll(`pre`)) {
  if (r.closest(jn) !== null) continue;   // ← 新增:已由第一步处理,跳过
  let i = Rn(r, e);
  
}

方案二(加固)Pn 只在「dsh-ui 标签属于 e 自己的 banner、而非嵌套代码块的 banner」时才认定是围栏:

function Pn(e) {
  let t = e.querySelector(`pre`);
  for (let n of e.querySelectorAll(`*`)) {
    if (n.childElementCount !== 0 || n.textContent !== `dsh-ui`) continue;
    if (t !== null && t.contains(n)) continue;     // 标签在正文里,不算 banner
    let block = n.closest(jn);
    if (block !== null && block !== e) continue;   // ← 新增:标签属于嵌套的另一个代码块
    return `dsh-ui`;
  }
  return null;
}

附注

  • 本 bug 只在 DOM 通道存在。新版 DSH 提供 fence-registry 扩展点时,插件走 Registry 通道、围栏进宿主流式渲染管线,没有这条「DOM 向上回溯误判」路径——这解释了为什么部分环境复现、部分不复现。
  • 建议在源码 src/client 中修复后发版,而不是直接改已安装的 lib/client.js(git 依赖一更新即被覆盖)。

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