vizcb-codeblock-visualizer
DeepSeek Harness 可视化插件:把模型回答中的 svg` / html / ````mermaid 代码块渲染为消息底部的内嵌图表卡片;并把 ````present-files` 指令渲染为工作区文件预览面板(HTML/图片/文本 + 文件卡片)。mermaid 为宿主端渲染(mermaid + svgdom,运行在独立 worker_threads,不污染宿主进程),运行时零网络外联(无 CDN、无 iframe 脚本);依赖由安装时自动拉取(仓库不内置 node_modules)。
效果预览
vizcb-codeblock-visualizer 效果
功能
- SVG 卡片:宿主端 DOMPurify 消毒(剥 script / 事件属性 / javascript: 链接)+ 客户端宽松校验(与渲染路径一致),轻微 XML 瑕疵也能正常渲染
- HTML 卡片:空 sandbox iframe(默认禁脚本;
htmlAllowScripts 可开 iframe 内脚本)
- Mermaid 卡片:宿主端渲染(mermaid + svgdom → SVG,独立 worker_threads 隔离),24 种方言别名(flowchart/graph/sequenceDiagram…);深色主题下文字/连线/箭头按宿主色板提亮(文字
#E5E7EB、连线 #4F8CFF),节点文字超出时自动扩宽矩形 + viewBox 自适应;同一图的并发请求合并、结果内存缓存
- 图注标题:自动提取代码块上方的标题行
- 交互:复制源码 / 全屏灯箱放大(Esc、点背景、按钮关闭)/ 保存导出(原生对话框自选位置与格式:PNG / SVG;HTML 导出源文件)
- 失败可见化:未渲染时显示原因通知条;mermaid 渲染错误显示具体原因
- 可预览文件(present-files):模型在回复末尾输出 ````present-files
块列出最终成品文件 → 第一行自动打开预览面板(HTML 走sandbox="allow-scripts"iframe,图片大图,css/js/文本源码),其余排成文件卡片点击切换;**mtime 轮询**:文件被改写后预览自动整体刷新;卡片支持**复制完整路径**、**在系统文件管理器中显示**(宿主explorer /select);路径先 resolve + 包含性校验(防 ../越界),经随机 token 前缀暴露,单文件超previewMaxBytes 拒绝;用户手动关闭预览后本次会话不再自动弹开;hideSourceBlocks` 隐藏渲染过的源码围栏,只留渲染结果(源码仍可复制;渲染失败/被禁用的图不会被误藏,源码保留)
- 呈现层(v1.7.0):源码围栏与渲染卡片不同框——检测到成对闭合围栏的同一轮提交里隐藏源码并顶出占位骨架,成品淡入出现;卡片按语言预留占位高度,减少跳位;
prefers-reduced-motion 下无动画
- 工程化:配置化(8 块/条、64KB/块、重试阶梯、画布底色、mermaid 深色模式、预览上限等)、per-seq 缓存(可重试失败不缓存,重试真实重取)、递增退避重试(400/800/2000ms 阶梯,默认 6s 放弃并还原源码)、请求体限制 + 每会话限流 + mermaid 全局限流、
/vizcb/debug 自检、启动日志 [vizcb] mounted vX、node --test 24 用例 + GitHub Actions CI
安装
前提:目标 profile 为标准 DSH web 应用栈(含 dsh-web-app 的 turnTail 槽位,以及 systemPrompt / webServer / sessionQuery 服务)。桌面版 2.0.4 已验证;独立 dsh web(web profile)v1.4.1 实测可用。
方式 A —— 一键安装脚本(推荐)
# 1. 获取源码(git clone 本仓库 / 下载 zip)
# 2. 在仓库根目录运行(会自动复制插件 + 写入 bundles + 缺依赖时 npm install):
powershell -ExecutionPolicy Bypass -File install-vizcb.ps1
# 其他 profile:
powershell -ExecutionPolicy Bypass -File install-vizcb.ps1 -ProfileName web
# 3. 重启 DeepSeek Harness
方式 B —— 手动复制
- 把
vizcb-codeblock-visualizer 目录复制到目标 profile 的 node_modules/
- 在 profile 的
package.json 的 dsh.profile.bundles 追加 "vizcb-codeblock-visualizer"
- (若未带 node_modules)在插件目录执行
npm install
- 重启
方式 C —— 作为 npm/git 依赖安装
在目标 profile 的 package.json 中把插件加入依赖并注册 bundle,然后执行 pnpm install(桌面版自带 pnpm,或直接用 dsh plugin 命令转发):
// ~/.dsh/profiles/<name>/package.json
{
"dependencies": {
"vizcb-codeblock-visualizer": "github:Reseezhang/vizcb-codeblock-visualizer"
},
"dsh": {
"profile": {
"bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "vizcb-codeblock-visualizer"]
}
}
}
或者用 dsh CLI 一行安装依赖(随后仍需把 "vizcb-codeblock-visualizer" 追加进上述 bundles 数组):
dsh plugin --profile <name> add github:Reseezhang/vizcb-codeblock-visualizer
依赖(mermaid/jsdom/dompurify/svgdom)由 npm/pnpm 从 registry 自动拉取,仓库不内置 node_modules。
配置
默认值见 lib/index.js 的 DEFAULTS。在 profile 级 cordis.patch.yml 按 id 覆盖:
- id: vizcb-codeblock-visualizer
config:
maxBlocks: 8 # 每条消息最多渲染块数
maxBlockChars: 65536 # 单块最大字符数
retryDelaysMs: [400, 800, 2000] # 空结果重试的递增退避阶梯(第 N 次失败取第 N 项,末位兜底)
retryGiveUpMs: 6000 # 放弃等待并还原源码的时限(ms);超时后仍按末位间隔探测
retryDelayMs: 2000 # 【兼容旧配置】显式配置它即按固定间隔重试,退避阶梯失效
minSvgHeight: 120 # SVG 无 viewBox 时的兜底高度
mermaidEnabled: true # 是否渲染 mermaid
mermaidTextColor: "#E5E7EB" # mermaid 深色主题文字/标签颜色(对齐宿主色板)
mermaidLineColor: "#4F8CFF" # mermaid 连线/箭头/生命线颜色
canvasBg: "#0F172A" # 卡片图区 / HTML / PNG 导出的画布底色
mermaidDark: null # mermaid 主题:null=自动检测(data-theme/prefers-color-scheme)、true/false=强制
htmlAllowScripts: false # HTML iframe 是否允许脚本(安全权衡,默认关)
debugLog: false # 请求级调试日志(开启写 os.tmpdir()/vizcb-debug.log)
mermaidRateMax: 30 # mermaid 渲染全局限流(滑动窗口内最大次数)
previewEnabled: true # 是否启用 present-files 文件预览
workspaceRoot: "" # 工作区根目录;留空 = sandboxPolicy.workspaceRoot → 进程 cwd
previewMaxBytes: 5242880 # 单文件预览上限(超过提示用浏览器打开)
previewPollMs: 3000 # 文件变化轮询间隔(ms)
maxFiles: 12 # 一次展示最多卡片数
hideSourceBlocks: true # 隐藏本插件渲染过的源码围栏(svg/html/mermaid/present-files),只留渲染结果
卸载 / 回滚
删掉 bundles 条目 + 删除插件目录 + 重启。
文件结构
vizcb-codeblock-visualizer/
├── package.json # dsh.bundle.patch + dsh.client 声明 + files 白名单
├── cordis.patch.yml # 挂载行 + 配置说明
├── README.md
├── DEVELOPMENT.md # 完整开发历程与踩坑记录
├── LICENSE # MIT
├── install-vizcb.ps1 # 一键安装脚本(按 files 白名单复制)
├── test/
│ └── index.test.mjs # node --test 冒烟测试
├── .github/workflows/ci.yml
└── lib/
├── index.js # Host:提示词 section + read-turn / mermaid.svg / p/<token> 预览 / reveal / debug 路由 + worker 管理(不碰 jsdom)
├── client.js # Client:turnTail 渲染 + 灯箱 + 保存导出 + 预览面板(__ModuleLoader__ 格式)
├── mermaid-worker.mjs # Worker:mermaid + svgdom 隔离渲染 + 渲染收尾(增强/自适应/消毒)
├── dom-worker.mjs # Worker:```svg 块的宿主端消毒(与 mermaid 渲染分线程,互不排队)
├── svg-dom.mjs # jsdom + DOMPurify + 节点自适应 + 配色增强(只允许被 worker import)
└── label-metrics.mjs # 纯文本度量(零依赖,主线程与 worker 共用)
版本历史
-
1.7.5 修「胶囊/菱形里的文字压出图形」(v1.7.4 的遗留):后处理只认 rect.label-container,而 ([..]) 胶囊是 <g class="label-container"><path>、{..} 菱形是 <polygon class="label-container">,两者被直接跳过 —— mermaid 在 svgdom 下按「单行」算图形尺寸,两行标签就从底部(实测下溢 4.5px)和两侧(实测左右各溢 4~5px)压出去。现在:① 从 d / points 反推图形包围盒(jsdom 没有布局引擎,只能几何推算)② 矩形仍直接改宽高 ③ 路径/多边形按需非等比缩放(上限 1.6×),超出部分压字号兜住,并对描边加 vector-effect="non-scaling-stroke" 免得边框跟着变粗 ④ 行盒一律改成绝对像素并居中到图形中心。实测三个图形四边均为负值(文字完全在框内),state/class/ER 仍不受影响。POLISH_VERSION 递增到 2 让老缓存失效
-
1.7.4 mermaid 版式优化(纯增量,不动渲染引擎):多行标签此前在 SVG 里每行字号字重完全一致,自动生成的图因此显得「平」;而 svgdom 下字体测量不准,mermaid 按「换行前」的行数算子块居中与框高,实渲染折成更多行后行与行会叠在一起、末行压出方框。现在在 worker 里同一趟 jsdom 解析中做版式后处理:① 节点标签首行 → 主标题(14px/500)、其余行 → 副标题(11.5px/0.72)② 按真实行数用像素重算行盒,并把整块文字在框内垂直居中(只补高不居中会让文字贴住框顶)③ 节点方框补 rx=6 圆角 ④ 边标签去灰底 ⑤ 连线统一 1.4px。渲染参数同时补 CJK 字体栈(Microsoft YaHei/苹方)与 14px 字号、curve: basis、间距 45/55、padding 10。只对 flowchart 生效(state/class/ER 的 g.node 结构不同,按固定行盒重排会把标题顶到框上沿);mermaidPolish: false 会把渲染参数、配色增强、后处理整包退回改动前行为,可直接做 A/B 回滚(逐字节等价已验证)。缓存 key 补上配色与 POLISH_VERSION,避免「改了配置旧图不变」。
-
1.7.3 修「源码围栏一直不消失」(hideSourceBlocks 在 DSH 0.2.0-rc.2 上从未生效过——三处宿主假设都不成立,任一处单独就足以让功能失效):① 语言标签类名是 CSS Modules 哈希名:旧代码查 [data-code-block-banner] .infostring,而真机是 _infostring_7gxqk_45,永远匹配不到(实测映射见 DEVELOPMENT 教训 14)② 即使选中横幅,横幅也不显示语言:宿主 CodeToolbar 的标签是 isLang(lang) ? lang : codeLabel,isLang 基于 shiki 语法集,svg/mermaid/present-files 都不在其中,只显示通用标签「代码块」(js 能显示正因为它在集合里)—— 语言在真机 DOM 上根本读不到 ③ turnTail 槽位与助手消息节点是兄弟(各自是 div[data-chat-turn] flowItem),旧实现「向上找最近的含代码祖先」会越过消息停在会话滚动列:fences 混进其它回合的块 → 与「本轮解析出的块」按序号对账必然错位(targets 恒空),命中数还会撞上 ≤24 守卫直接放弃(真机一个 turn 有 116 个流节点)。现在:语言标签按 [class*="infostring"], [class*="language"] 子串匹配;读不到语言时按正文特征识别(^<svg/mermaid 关键字…);确认阶段改用宿主解析出的 code 做归一化内容对账(与顺序、语言标签无关,唯一命中才认,多命中要求语言一致,否则宁可少藏);作用域按 data-chat-turn 收窄到本轮。顺带修掉一个反向 bug:原「正文正则扫 ```」兜底会把讲解 mermaid 语法的普通代码块误判成图表块并藏掉用户代码。另:客户端改动不需要重启( 每 500ms 轮询每个客户端 bundle 的 mtime/ctime/size,变更后经 SSE 推送、浏览器热重载该模块)
完整踩坑历程见 DEVELOPMENT.md。