dsh-reply-visual
Verified@yfwu2020/dsh-reply-visual · v0.2.0 · MIT · Web UI
DSH 回复可视化(图解):把 AI 回复变成立即看得懂的单页图解——模型写单文件 HTML + 内联 SVG,右侧栏沙箱 iframe 渲染
Install
dsh plugin add @yfwu2020/dsh-reply-visual Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-reply-visual · 回复可视化(图解)
在 DSH Web 界面里,把一条 AI 回复变成一页看得懂的图解: 每条定稿回复下面多一个「图解」按钮 → 点一下,右侧栏打开「图解」页—— 模型直接产出单文件 HTML(内联 SVG),在沙箱 iframe 里渲染。
它解决什么问题
长回复难读。一段三千字的分析里,真正的结构(三个层次、两条分支、一个取舍)是空间关系, 而 Markdown 只能把它摊成一条竖线——你得在脑子里重新拼一遍。
这个插件把那条回复交给模型,让模型画一页:摘要卡、流程图、时间线、对照矩阵、结构树、 示意图……画成什么样完全由模型决定。插件只做两件事:把回复交给模型,和给一个沙箱。
它和"让模型输出 Mermaid"的区别在于:没有图表 DSL、没有内置渲染器、没有图型白名单。 模型直接写一页 HTML + 内联 SVG,所以能画的东西只受表达能力限制,不受图库限制。
长什么样
页头(模型/档位/耗时/字节)· 三段切换「图解 / 源码 / 原文」· 「历史」「重画」「字号」「复制源码」「保存 .html」。

![]() |
![]() |
| 深浅两套配色(页面自己带一套,跟随系统) | 历史:本会话画过的页,按对话顺序排,点了直接回放 |
三个视图各管一件事:
| 视图 | 内容 |
|---|---|
| 图解 | 流式期间是进度卡(已画多少字 / 秒表),整页到齐后换成模型画的页面 |
| 源码 | 模型产出的 HTML 原文(可复制)。页面不完整时这是唯一出口 |
| 原文 | 这条回复本身,用 DSH 自带的 MarkdownText 渲染——不调模型、不截断,永远可用 |
「原文」是兜底,也是「图看不懂时回去看原话」的去处。
等待期:20~70 秒不是空白
生成一页要 20~70 秒(长回复实测 57s / 64s)。这段时间侧栏分三段,各管一段、不重复表达:
![]() |
![]() |
| ① 理解中(还没有产出) 声波——只表达"在活动",不确定态不假装有进度 |
② 绘制中 → ③ 收尾 线稿按字数生长;画完后末尾浮出闪烁光标 |
状态行给真实数字(已画字数 + 秒表),动画只负责"还在长"——两者分工。 不显示百分比:总量未知,报百分比就是编的。
历史与翻页
同一会话里画过的页都能回去,三个入口殊途同归:
| 入口 | 定位到 |
|---|---|
| 每条 AI 回复下面的「图解」 | 就是你点的那一条 |
| 会话头部的「图解」图标 | 本会话上次画的那一条 |
| 工具栏的「历史 N ▾」 | 本会话任意一条(下拉列表) |
列表按对话顺序排(最早的一轮在最上面,和聊天记录同向),不是按"什么时候画的"——
重画一条旧回复只会刷新它的时间,位置不动。位置的依据是会话日志里那条
assistant/message 的序号(seq,会话内单调递增),它天然等于"这段对话的第几步";
定位不到的旧条目退回按生成时间排,并统一排在最后。
- 本会话:一条平铺的列表,不分组(只有一段对话,给分组头是噪音)。行里给时刻、 相对时间、字节数,以及第几轮。
- 全部会话:按会话分组,会话之间"最近活跃的在前",组内仍是对话正序;分组头是 会话标题(host 从会话日志折出来的,取不到就显示会话号前 8 位),右侧给 "几条 · 最近什么时候画的",当前会话标「本会话」。
筛选框、↑↓ 选择、↵ 打开、Esc 关闭、「全部会话」开关都在。
「页面自己的标题优先」——显示的是产出 HTML 里的 <title>/<h1>/<h2>,
不是回复首行那句话片段。
翻页贴在页面右缘(▲ N/M ▼),鼠标悬停才显形:序列和下拉同一个顺序
(对话正序),到头循环,只有 ≥2 条时才出现。切页不闪骨架屏(保留当前页,只转一个小圈)。
回放走
/result,token 命中即返回——一次模型都不调。
安装
# 从 npm(推荐)
dsh plugin --profile web add @yfwu2020/dsh-reply-visual
# 或从 GitHub Release 的 tgz(同一个产物,不走 npm)
dsh plugin --profile web add /path/to/yfwu2020-dsh-reply-visual-0.2.0.tgz
# 或从本地目录(开发用:改完不用重装,只重建)
dsh plugin --profile web add link:/path/to/dsh-reply-visual
它在 DSH 里落成三处:
| 位置 | 内容 |
|---|---|
~/.dsh/profiles/web/package.json |
"@yfwu2020/dsh-reply-visual": "<版本或 link:路径>" |
~/.dsh/profiles/web/node_modules/@yfwu2020/dsh-reply-visual |
软链 → 插件目录 |
profile 的 dsh.profile.bundles |
包名(按包名引用,与路径无关) |
⚠️ 装完要重建进程:host 半是新的 bundle,改完需要重启
dsh web(或走你现有的热重载通道), 之后硬刷新浏览器(Cmd/Ctrl+Shift+R)。client 半之后的改动会被client-hmr就地替换,不用刷新。
要求:DSH 运行时 >=0.1.7-rc(见 package.json 的 peerDependencies)。
0.1.7-rc 起 createSystemMessage 去掉了插件署名参数,更旧的运行时上行为会不一致。
另外 npm 上这些包的 latest 还停在更旧的 0.0.1-rc.x,缺 WebServer 等 API,装上会起不来。
配置项
改 profile 的 cordis.patch.yml 里 id: reply-visual 那一行的 config:。
模型与生成
| 键 | 默认 | 说明 |
|---|---|---|
provider / model |
空 | 固定模型路由;留空 = 跟随「设置 → 默认模型」 |
reasoningEffort |
low |
画图靠长输出、不靠深推理;要更讲究可调 high |
temperature |
0.4 |
画图要一点自由度;别调到 1 以上 |
maxTokens |
0 |
单页输出上限。默认 0 = 不传这个参数,交给模型/供应商自己的上限(早先默认 8000 会把页面切在 SVG 中间) |
timeoutMs |
180000 |
单次生成超时 |
maxSourceChars |
24000 |
原文上限,超出取头 60% + 尾 40%(结论在尾部,不能砍尾) |
页面渲染与手感
| 键 | 默认 | 说明 |
|---|---|---|
allowScripts |
true |
iframe 是否放脚本;false = sandbox="" 纯静态 |
wheelSmooth |
true |
滚轮走"跟手 + 惯性"(触摸手感);false = 中间段落交回原生滚轮,只在边界做橡皮筋 |
wheelSmoothFactor |
0.22 |
惯性逼近系数(0.02~0.8);越小越"拖沓",越大越"利落" |
elastic |
true |
橡皮筋总开关(prefers-reduced-motion 下自动关) |
elasticMax |
140 |
橡皮筋最大可拉距离(像素);0 = 关闭 |
elasticResist |
0.5 |
橡皮筋阻尼(0.05~1,越小越"沉");曲线非线性,越拉越沉 |
elasticStiffness |
0.18 |
回弹刚度(0.005~0.3,越小越软);配 elasticDamping 0.52 时不过冲 |
elasticDamping |
0.52 |
回弹阻尼(0.4~0.98);调高会重新开始过冲、弹两下 |
elasticTop |
false |
顶端是否也做橡皮筋(默认关:到顶继续拉没有过拉效果) |
elasticMaxHoldMs |
10000 |
橡皮筋硬上限(0 = 不限):到点强制回弹,继续拉伸不重置 |
elasticGestureGapMs |
120 |
两次滚轮/触摸之间超过它就当作"新的一次操作" → 立刻弹回 |
bottomGap |
0 |
页面底部留白像素(默认关):让最后一行避开右下角胶囊;观感是死空白,所以改用 elastic 解决 |
maxPageBytes |
2000000 |
「页面偏大」的提示阈值(不是拒绝线;只有 >16MB 才拒绝渲染) |
缓存
| 键 | 默认 | 说明 |
|---|---|---|
resultCacheTtlMs |
0 |
回放的时间判据:默认 0 = 不限时(文件在就回放);设正数才会到点强制重新生成 |
maxCacheEntries |
200 |
条数上限:result/ 与 prepared/ 各自保留最新这么多 |
cacheMaxBytes |
33554432 |
缓存总字节预算(32MB):超了从最老的开始删 |
cacheMaxAgeMs |
0 |
按龄清理(0 = 不限龄) |
maxRequestsPerMinute |
20 |
本地限流 |
自定义这两份规范
| 键 | 默认 | 说明 |
|---|---|---|
skillPath |
包内 skills/visual-page/SKILL.md |
可视化规范(做什么 + 环境是什么样) |
designPath |
包内 skills/page-design/SKILL.md |
审美参考(怎么做得好看);填 off = 不注入,留空 = 用包内那份 |
缓存会一直涨吗
不会无界增长——生成成功后自动清扫,启动后 5 秒也会扫一次。四道闸门:
| 闸门 | 默认 | 清什么 |
|---|---|---|
| 按条数 | 200 | result/ 与 prepared/ 各自只留最新 200 份 |
| 按总字节 | 32MB | 整个缓存超预算时从最老的删起 |
| 按龄 | 关 | 想自动过期就设 cacheMaxAgeMs(比如 7 天) |
| 临时文件 | 1 小时 | 写盘中途退出留下的 .tmp 残留 |
内存另有独立一道闸门(maxCacheEntries + 64MB 字节预算):被挤出内存的只是副本,
磁盘上还在,再点开会从磁盘回读。
什么情况下会重新生成(这是实际用起来最关心的一条):
| 动作 | 会不会重新生成 |
|---|---|
| 关掉标签页再点「图解」(同一条回复) | 不会,直接回放 |
| 切换会话再切回来 | 不会 |
| 刷新浏览器 / 重启 dsh | 标签会消失(右侧栏布局是内存态),但点会话头的「图解」即可找回;在原来那条回复上再点也不会重新生成 |
| 隔了几小时/几天再点 | 不会(resultCacheTtlMs 默认不限时) |
| 升级了插件(改了规范) | 不会——token 只由内容决定,回放时会提示"可点重画用新规范重生成" |
| 缓存被三道闸门挤掉 | 会 |
| 你点「重画」 | 会(唯一主动重新生成的方式) |
随时体检:GET /reply-visual/api/ping 报告路由、档位、缓存目录/文件数/字节、上次清扫删了几个:
curl -s http://127.0.0.1:3080/reply-visual/api/ping | python3 -m json.tool
想立刻清零:缓存在 ~/.dsh/reply-visual/,直接删目录即可(下次用到会重建):
rm -rf ~/.dsh/reply-visual
注意区分:这只删缓存。你点过「保存 .html」落到工作区
reply-visual/的文件是你的文件, 插件不碰、也不会自动清。
安全边界(刻意这么设计)
模型写的页面跑在 sandbox="allow-scripts allow-forms allow-modals allow-popups" 的 iframe 里,
永不加 allow-same-origin:于是它跑在不透明源里——能算、能画、能发请求,
但读不到本页的 DOM / Cookie / localStorage,也不能导航顶层窗口。
- 页面只经 Blob URL 进 iframe,永不作为同源顶层文档打开。「保存 .html」落下的是模型原页 (未打补丁),要看得走 DSH 自带的文件预览沙箱。
- 脚本是能跑的,而且是全的(真机实测,同一个 sandbox 串):内联 classic script、
setTimeout、requestAnimationFrame、element.animate()、canvas 2d +toDataURL、svg.getTotalLength()、 PointerEvent +setPointerCapture、ResizeObserver/IntersectionObserver、<details>、:has()/ 容器查询 /color-mix/accent-color全部可用。 被挡的只有存储与剪贴板:localStorage/sessionStorage/document.cookie/indexedDB.open一律SecurityError,navigator.clipboard是undefined(不透明源的必然结果,不是配置问题)。 - 显示时才给页面注入几个补丁(保存在
.html里的仍是模型原页):<meta viewport>+ 极小 reset(插在 doctype 之后——插在它之前会把文档推进怪异模式)、 滚轮手感、橡皮筋、可选底部留白。 - 侧栏宽度只有 320720px,而模型习惯按 10001200px 排版,所以这两针补丁是必需的。
- 原文只发给会话同款模型路由,不新增外部出口。
改补丁时注意:页面自己要用
position:fixed/position:sticky放常驻控件(固定头、浮动图例、 返回顶部)。这类纯 CSS 语义离线桩测不出来(桩里没有布局引擎),所以除了代码形状断言之外, 另有一个真机检查npm run check:patch(需要 Chrome,找不到就 SKIP)。
开发
git clone https://github.com/yfwu2020/dsh-reply-visual.git && cd dsh-reply-visual
npm install # 装 devDependencies(构建需要的 @deepseek-ai/* 类型)
npm run build # 探运行时 → 链 @deepseek-ai/@types → tsc → 拷 client
npm test # 503 条断言,不联网、不花钱
npm run check:patch # 7 条真机断言(需要 Chrome)
npm run watch # 监听 src:改 client 立即重建(client-hmr 自动换掉运行中的插件)
目录:
src/index.ts host 半(Config / 6 条路由 / SSE / 提示词 / 校验 / 缓存 / 保存)
src/client/index.js 客户端半(手写 ModuleLoader bundle,无打包器)
skills/visual-page/SKILL.md 可视化规范:做什么 + 环境什么样(**运行时读取,改文案不用重新构建**)
skills/page-design/SKILL.md 审美参考:怎么做得好看(运行时读取,可替换/可关)
scripts/build.sh 探运行时 → 链 @deepseek-ai/@types → tsc → node --check → 拷 client
scripts/watch.sh 开发监听
scripts/test-*.mjs 离线测试(split / validate / route / llm / client)
scripts/check-display-patch.mjs 真机检查:fixed/sticky/横向溢出/橡皮筋(需要 Chrome)
scripts/gen-readme-shots.mjs 生成 README 截图(抠真实 CSS 渲染,见下)
PROMPTS.md 提示词与调参笔记:发出去的是什么、为什么这么写、坑在哪
测试
| 脚本 | 断言 | 盯住什么 |
|---|---|---|
test-split.mjs |
23 | 页面抽取与切分:标记跨 chunk、无标记、页前后废话、逐字符投喂、截到 </html>、标题抽取 |
test-validate.mjs |
55 | 能不能渲染的每一档、原文兜底抽取、页面标题抽取(畸形 HTML / 实体 / 截断)、slug/越界防护 |
test-route.mjs |
85 | 全链 / 缓存回放不调模型 / 在飞去重广播 / 限流 / 断开中止上游 / save 落盘 / /recent 带 pageTitle / 对话位置(客户端给的优先、没给回日志里查、脏值当没给、跨会话带会话标题且单会话失败被隔离) |
test-llm.mjs |
26 | 模型的 9 种"乱写"姿势:废话、截断、空输出、报错、外链、iframe… |
test-client.mjs |
335 | 注册(工厂 / apply / 4 个服务 / 动作行 / 会话头入口 / 侧栏 page key)/ 自画图标的几何与无障碍(外框、图元数、aria-label)/ 采集多步 turn(含"问题按 seq 找"的回归)/ 一条流只建一个 Blob / 沙箱不带 allow-same-origin / params 丢失恢复 / 历史浮层:按对话顺序排、会话分组、分组头标题与当前会话标记 / 翻页:对话正序、循环、不闪骨架屏 / 等待动画三段(含光标延迟出现、高度跟行、橙色)/ 样式表与渲染对得上(用了没定义、定义了没用) |
合计 524 条,全部离线(不打网络、不调模型);另有 npm run check:patch 的 7 条真机断言。
调试钩子:window.__dshReplyVisual(collectReply / normalizeClientHtml / parseSseBlocks /
originalCache / counters{blobs,iframes});host 侧 GET /reply-visual/api/ping 一眼看到
当前路由、档位、缓存条数、在飞请求数。
改 README 的截图
node scripts/gen-readme-shots.mjs # 全部场景 → assets/*.png
node scripts/gen-readme-shots.mjs waiting # 只出一张
DSH_CHROME=/path/to/chrome node scripts/gen-readme-shots.mjs
它不另写一套样式:从 src/client/index.js 里 injectCss() 的样式数组原样抠出 CSS
(抠不到就报错,不静默退回),再按 render() / waitStage() 的真实 DOM 结构拼舞台。
所以样式漂移时图会跟着变,不会出现"图好看但和插件不一样"。
assets/demo/diagram.html 是 panel 场景里当演示内容的那一页(一张由本插件产出的真实图解)。
自定义:改规范,不改代码
模型的自由度是故意放开的,收窄或换风格都改这两份 Markdown,改完不用重新构建:
| 想要 | 改哪里 |
|---|---|
| 图更好看 / 更讲究 | reasoningEffort: high(默认 low) |
| 页面更长更全 | maxTokens 调到 12000~16000(配合 timeoutMs) |
| 更快出图 | reasoningEffort: off;maxTokens 降到 5000 |
| 更收敛的风格(比如只允许某几种图) | 直接改 skills/visual-page/SKILL.md——现在是"自由发挥"版 |
| 换一套审美 | 改 skills/page-design/SKILL.md,或把 designPath 指到你自己的 Markdown |
| 觉得太"设计"了 | designPath: off,只留主体规范 |
| 完全禁脚本 | allowScripts: false(沙箱退化成 sandbox="",纯静态页面) |
| 换语言 | 规范里补一句"页面语言跟随原文" |
| 换模型 | provider/model,或在设置里换「默认模型」 |
skills/visual-page/SKILL.md 只讲两件事:这一页要达成什么(让人一眼看懂),以及运行环境是什么样
(沙箱 iframe / 约 320~720px 宽 / 通常没有外网 / 读者手上永远有原文可对照)。
没有格式要求,没有必须遵守的规则 —— 形式、篇幅、配色、层级、要不要动效,都由模型自己决定。
skills/page-design/SKILL.md 讲"怎么做得好看",注入时明确写着"这是参考,不是规则"。
两份分开是有意的:可以单独关掉、换成你自己的审美文件,而不动主体规范。
审美参考的思路来自 Anthropic 公开的
frontend-design技能 与社区反 slop 实践,按本插件场景逐条改写——那些规范面向品牌落地页,原样照搬会翻车 (沙箱无外网,引 Google Fonts / 动效库会静默回退)。
边界与失败行为
原则:不替模型决定"不给看"。除了"页面是空的"和"大到浏览器画不动(>16MB)", 一切都照常展示,有问题只在底部提示一句。
| 情况 | 行为 |
|---|---|
| 回复没有正文 / 还在生成 | 不渲染按钮 |
模型先写废话 / 不写 <<<PAGE 标记 |
页面照样被找到;页面前后的说明不会进 iframe |
| 模型只写了说明、没给页面 | 明确原因 + 「重画」+「以原文显示」,不给空白页 |
输出被截断(没有 </html>) |
照常展示已到达的内容 + 提示具体原因(达到模型上限被截断 / 超时或连接中断 / 模型提前结束)。不自动续写 |
页面里嵌了 <iframe> |
照常展示 + 提示在沙箱里可能显示不出来 |
页面偏大(超 maxPageBytes) |
照常展示 + 提示"滚动可能有点卡";只有到 16MB 才拒绝 |
外链 / @import / 标签未闭合 / 代码没保留 |
照常展示 + 黄条提示(逐条写清) |
| 模型报错 / 无可用路由 / 触发限流 | 明确原因 + 「重画」「以原文显示」,绝不空面板 |
| 原文超限 | 头 60% + 尾 40% + 省略标记;页头「原文已截断」;「原文」档仍是本地全量 |
| 关闭标签 / 切会话 | 中止在飞的 SSE 与模型流;最后一个订阅者走了才 abort(不掐断别人) |
| 浏览器刷新 | 回放该会话上一条;缓存过期则给 /recent 列表 |
| 保存 .html | 取不到会话 cwd → 409;标题经 slug 净化 + 越界校验,只落在 <cwd>/reply-visual/ |
已知限制
- 一个回复不是一张单独标签:原生页类型一个 pane 只能有一个同 kind 的页,所以是「随点随切」。
要多页并存需要注册
dsh-resource://reply-visual/**协议 + resource provider。 - 任意图由模型用内联 SVG 画,插件不内置 Mermaid 运行时(7MB 懒加载块 + 打包步骤); 需要更专业的图(时序/甘特)时模型仍然可以直接画。
- 页面与插件主题不共用:模型页面自己带配色(规范要求深浅两套 + CSS 变量),
字号档通过 CSS
zoom缩放 iframe。 - 历史列表一次最多 200 条:客户端要
limit=200,host 再按maxCacheEntries夹一次。 超过 200 页要搜"滚回那条回复再点「图解」",或调大maxCacheEntries。 - 侧栏宽度窄(320720px)时工具栏会换成 23 行,这是有意的取舍。
发布(维护者)
发布走 Trusted Publishing(GitHub OIDC),不需要任何长期 npm token:
# 1) 改 package.json 的 version(例如 0.2.0)
# 2) 打同名 annotated tag 推上去
git tag -a v0.2.0 -m "..." && git push origin v0.2.0
.github/workflows/publish.yml 会自动:校验 tag 与 version 一致 → npm install → 构建 →
跑全量测试 → 类型检查 → npm publish --provenance → 打 tgz 并建 GitHub Release。
npm 侧只需配一次:包设置 → Trusted Publisher → GitHub Actions,
填 yfwu2020 / dsh-reply-visual / publish.yml,并勾选允许 npm publish
(默认只允许 npm stage publish)。
devDependencies里的@deepseek-ai/*钉在0.1.7-rc.2(与插件开发所依据的运行时一致)。 升级运行时时要同步改这里——用latest会编译失败(缺WebServer等 API)。
License
MIT



