- Home
- Plugin
- Strumenti e capacità
- dsh-reply-visual
dsh-reply-visual
yfwu2020/dsh-reply-visual
Aggiunge un pulsante diagramma sotto ogni risposta completata nella UI web DSH. Il modello configurato crea una pagina HTML autonoma con SVG incorporato, mostrata in un iframe isolato nella barra laterale destra. Offre viste diagramma/sorgente/originale, cronologia per sessione dalla cache su disco senza richiamare il modello, ridisegno e salvataggio .html.
Installazione
dsh plugin --profile web add github:yfwu2020/dsh-reply-visualREADME
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 之后——插在它之前会把文档推进怪异模式)、 滚轮手感、橡皮筋、可选底部留白。 - 侧栏宽度只有 320
720px,而模型习惯按 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。 - 侧栏宽度窄(320
720px)时工具栏会换成 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
Plugin correlati
archify (deepseek-harness)
tt-a1i/archify
WeKnora (dsh-weknora)
tencent/weknora
next-ai-draw-io (dsh-plugin)
dayuanjiang/next-ai-draw-io
weknora
tencent/weknora



