dsh-math-tex
让 DSH 的数学回答始终可读:给模型一套排版规则,再在文本落盘之前拦截并修好每一个会被渲染成红字的公式区域。
这是一个 host 侧插件,没有客户端部分——渲染器改不了,所以修复发生在文本还属于我们的最后一刻。
它解决什么问题
DSH 的对话渲染用 KaTeX 渲染 Markdown 数学,只要遇到它解析不了的东西,就把原始源码用 KaTeX 的错误色 #cc0000 直接吐到屏幕上。没有崩溃,也没有日志:一个位置不对的 $$ 足以让整篇推导变成"一片血红",而用户只会看到一片红字,不知道哪一步错了、也不知道原因。
前端是 app.asar 里的一个整体 bundle,给 KaTeX 传的选项是空的(没有 errorColor、没有 maxExpand、没有 trust),所以事后无法配置。文本一旦落盘,屏幕上和会话记录里就都是坏的。
dsh-math-tex 因此在文本落盘之前动手。
两道防线
| 机制 | 作用 |
|---|
| 1 | systemPrompt.section | 事前规则:行间公式单独成段、$$ 内只有公式、标题不粘公式……让模型少犯这个错。默认只注入给已经出现过数学的会话(promptRules: auto) |
| 2 | llm/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} | 成功 | — | 红字 |
第三行是最阴的一种:严格调用完全成功,输出里却带错误色。只看异常的实现会把它判成健康。 所以判定看的是渲染产物里有没有错误色。
修复原则:不猜数学
一个区域会被渲染成红色时,按顺序尝试:
- 重新排版——公式本身没问题、只是框架错了(例如
$$ 开在段落中间),改成规范的 $$\n…\n$$ 块;
- 定点施救——只在两种有确定语义的情况下动:删掉渲染器不认识的命令的反斜杠(
\foobar → foobar),或给重复的算子重新分组(x_1_2 → x_{1_2})。改完必须**重新渲染验证"严格通过且无红色"**才接受,所以永远不会出现"改一下只是把错误挪了个位置"的循环;
- 源码围栏——修不了的(括号不闭合、
\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
| 选项 | 默认 | 说明 |
|---|
enabled | true | false 时两道防线都不挂载 |
promptRules | auto | auto 只给已经出现过数学的会话注入 / full 每个会话都注入完整七条 / minimal 只留防红字的两条 / off 不注入 |
repair | auto | auto 改写 / check-only 只记日志不改写 / off 完全放行 |
tailBuffer | 15 | 最少扣住多少个字符,避免完成记号被逐字泄漏 |
maxHold | 600 | 单个未闭合区域最多等多少字符,超了就原样放行(宁可不管,不可卡住) |
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 | 判定与区域切分是否和渲染器一致;提示词段是否按会话决定 | 60 |
red-render-test | 三类红字是否都能抓到 | 48 |
e2e-test | 真实事故文本端到端是否可读 | 39 |
wire-test | 流式输出是否等于整篇修复 | 97 |
fuzz-test | 该性质在 236 种任意切分下是否成立 | 236 |
config-test | 每个文档化选项是否真的有效 | 32 |
smoke-test | 包是否按宿主的加载方式加载 | 23 |
installed-test | profile 里那份安装副本(不是源码目录)能否加载并自修 | 11 |
regression-test | 已修缺陷不再回归(用例逐字引自真实会话) | 23 |
live-path-test | 验收:装好的那份 + profile 的真实配置 + 宿主的挂载路径,坏答案是否真的变成干净文本 | 28 |
合计 10 个文件、594 条断言,约 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/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