← Back to home@EnhydraLutristus

dsh-math-tex

DSH plugin: keeps streamed answers free of red (broken) KaTeX regions - validates every maths region with the client's own renderer before it is written

Stars
0
Language
JavaScript
Created
Oct 15, 2026
Updated
Oct 16, 2026

Introduction

dsh-math-tex

让 DSH 的数学回答始终可读:给模型一套排版规则,再在文本落盘之前拦截并修好每一个会被渲染成红字的公式区域。

这是一个 host 侧插件,没有客户端部分——渲染器改不了,所以修复发生在文本还属于我们的最后一刻。


它解决什么问题

DSH 的对话渲染用 KaTeX 渲染 Markdown 数学,只要遇到它解析不了的东西,就把原始源码用 KaTeX 的错误色 #cc0000 直接吐到屏幕上。没有崩溃,也没有日志:一个位置不对的 $$ 足以让整篇推导变成"一片血红",而用户只会看到一片红字,不知道哪一步错了、也不知道原因。

前端是 app.asar 里的一个整体 bundle,给 KaTeX 传的选项是空的(没有 errorColor、没有 maxExpand、没有 trust),所以事后无法配置。文本一旦落盘,屏幕上和会话记录里就都是坏的。

dsh-math-tex 因此在文本落盘之前动手。

两道防线

机制作用
1systemPrompt.section事前规则:行间公式单独成段、$$ 内只有公式、标题不粘公式……让模型少犯这个错。默认只注入给已经出现过数学的会话(promptRules: auto)
2llm/stream waterfall兜底:用客户端同版本的 KaTeX 逐个区域判定,把会渲染成红色的区域改写掉,然后才落盘

第 2 道是关键:llm/stream 是每一次流式模型调用外面的 waterfall,改写过的 chunk 会同时进入持久化的消息体、实时前端帧和上下文压缩,所以屏幕和记录一起修好。

判定不是正则,是问渲染器

直觉上的启发式(公式太长、嵌套太深、用了重型宏)经实测两个方向都会错:300 字符的公式、8 层 \underbrace、\substack、公式里的中文都渲染正常,而一个 99 字符的公式却死了。

更麻烦的是,"渲染器抛没抛异常"也不是判据:

输入第 1 段调用(严格)第 2 段调用(宽松)读者看到
\foobar{x}抛错不抛错,把 \foobar 用 #cc0000 画出来红字
x_1_2(双下标)抛错不抛错红字
\htmlClass{red}{x}成功—红字

第三行是最阴的一种:严格调用完全成功,输出里却带错误色。只看异常的实现会把它判成健康。 所以判定看的是渲染产物里有没有错误色。

修复原则:不猜数学

一个区域会被渲染成红色时,按顺序尝试:

  1. 重新排版——公式本身没问题、只是框架错了(例如 $$ 开在段落中间),改成规范的 $$\n…\n$$ 块;
  2. 定点施救——只在两种有确定语义的情况下动:删掉渲染器不认识的命令的反斜杠(\foobar → foobar),或给重复的算子重新分组(x_1_2 → x_{1_2})。改完必须**重新渲染验证"严格通过且无红色"**才接受,所以永远不会出现"改一下只是把错误挪了个位置"的循环;
  3. 降级——修不了的(括号不闭合、\begin 没有 \end)原样给出源码:块级区域包成代码围栏,行内区域只去掉定界符(围栏是多行结构,用在行内会把一条表格行切成三段)。

数学永远不会被删掉。 最坏情况是读者看到公式源码(可复制、可恢复),而不是红字。猜结构性损坏等于替作者编造数学,这件事不做。

安装

node tools/install.mjs              # 复制到 profile 并注册 bundle
node tools/install.mjs --dry-run    # 只说会改什么
node tools/install.mjs --uninstall  # 从 bundle 列表移除

装完必须开新会话:bundle 栈是会话的 runner 进程启动时读的,当前会话不会重新加载。

手工挂载也可以:把本包放进 <profile>/node_modules/,再把名字加进 profile package.json 的 dsh.profile.bundles。

配置

在 profile 的 cordis.patch.yml 里按 id 设置:

- id: dsh-math-tex
  name: dsh-math-tex
  config:
    repair: auto
    logFile: C:/tmp/math-tex.log
选项默认说明
enabledtruefalse 时两道防线都不挂载
promptRulesautoauto 只给已经出现过数学的会话注入 / full 每个会话都注入完整七条 / minimal 只留防红字的两条 / off 不注入
repairautoauto 改写 / check-only 只记日志不改写 / off 完全放行
tailBuffer15最少扣住多少个字符,避免完成记号被逐字泄漏
maxHold600单个未闭合区域最多等多少字符,超了就原样放行(宁可不管,不可卡住)
logFile''诊断日志路径;留空则只走 host logger
katexPath''指定 KaTeX 位置,默认自动按 profile 解析
macros{}传给渲染器的宏,与客户端行为保持一致

未知键报错而不是静默忽略——写错的选项看起来像生效了,比没有这个选项更糟。

日志能证明它真的在工作

oracle: katex 0.16.47
prompt section "math-output-format" registered (auto)
llm/stream hooked model=deepseek-flash mode=auto
prompt rules now apply to session session-8c6a99cc-…
guarded stream: 214 text delta(s) in, 231 out

最后一行是真实回合的判据:in 必须远大于 1(自测喂的假流是 1)。

测试

npm test
文件测什么断言
guard-test判定与区域切分是否和渲染器一致;修复不许破坏周围的 Markdown 结构;释放点不许落进未闭合的行内代码71
red-render-test三类红字是否都能抓到;回显是否从正确的账本读71
e2e-test真实事故文本端到端是否可读39
wire-test流式输出是否等于整篇修复103
fuzz-test该性质在 236 种任意切分下是否成立236
config-test每个文档化选项是否真的有效32
smoke-test包是否按宿主的加载方式加载23
installed-testprofile 里那份安装副本(不是源码目录)能否加载并自修11
regression-test已修缺陷不再回归(用例逐字引自真实会话)23
live-path-test验收:装好的那份 + profile 的真实配置 + 宿主的挂载路径,坏答案是否真的变成干净文本28

合计 10 个文件、632 条断言,约 4 秒,不需要网络。

最有说服力的是 fuzz-test:它要求真实事故文本的任意切分(1 字符一帧到 1600 字符一帧,外加随机切分)最终都重组为逐字相同的整篇修复结果。

live-path-test 是唯一一个把"装好的副本、profile 的配置、宿主的挂载路径、真实的增量流"接起来测的文件。它每个用例跑两遍:关掉修复时缺陷必须原样到达读者,开着时必须消失——前一遍是后一遍的前提,否则一个根本没坏的用例也会"通过"。

regression-test 是唯一断言"输出来的文本渲染后没有红色"的文件——其余文件盯的是判定与结构,它是最后一道"判定对了、但改完仍然红"的防线。把修复回退掉,它会红 4 条。

关于 test/fixtures/real-broken.txt

这份夹具复刻的是事故文本的结构(同一批缺陷形态:段落中间的 $$ 配错对、## 粘在行间公式里、未闭合的 \begin、结尾的代码围栏),内容已替换为与具体题目无关的通用推导,因此它测的是缺陷形态而不是任何一次具体事故。

想直接看一份文本是怎么被判定的:

node tools/inspect-fixture.mjs                    # 默认检查上面这份夹具
node tools/inspect-fixture.mjs path/to/text.md    # 换成任何一份文本

它会打印区域数量、被标记的区域及其原因、修复动作,以及修复后是否还有区域会被渲染成红色。

排查真实会话时有这几件工具:

node tools/find-red-session.mjs                   # 扫本机所有会话,列出还会渲染成红色的回答
node tools/strip-sentinel.mjs --list              # 列出正文里还留着哨兵的记录
node tools/strip-sentinel.mjs --id <前缀> --yes   # 把那些哨兵清掉(逐帧改写,改完自验,失败自动回滚)
node tools/inspect-session.mjs <session.jsonl>    # 单条会话逐条消息:区域、判定、守卫会怎么修
node tools/probe-region.mjs 'd\alpha=0'           # 把候选写法逐个送进渲染器,看哪种被接受

注意:记录里存的是修复前的原文——守卫改的是往外发的流,不改落盘的模型文本。 所以"记录里还有坏公式"不代表屏幕上还有红字,判断要看同一次流的出口。

设计边界

  • 渲染器不可改:这是设计前提,不是让步。前端是打包好的 bundle,没有数学相关的 slot 可以替换。
  • 只做 host 侧:唯一能同时影响屏幕、会话记录和压缩的拦截点在 llm/stream。
  • 宁可不管,不可卡住:所有等待都有界(maxHold),超限即原样放行——那正是没有插件时的行为,因此安全。
  • 不改写非数学文本:不含 $ 和 \ 的文本逐字节直通。

文档

  • STATUS.md——现在装了什么、证明了什么、还差什么(交接用)。
  • docs/——判据的来源:FINDINGS.md(到底坏在哪)、KATEX-MEASUREMENTS.md(逐个用例的实测数字)、DESIGN.md(方案怎么定的、否掉了什么)。

License

MIT