dsh-visualizer
一个不改动 DSH 源码的外部插件:让模型在对话流里边生成边渲染可视化内容——流式 SVG/HTML 组件,以及结构化图表(ChartSpec → echarts)。
这是什么
在 DSH(DeepSeek Harness)的对话里,让模型直接生成可视内容,安全地渲染到对话流中:
- 结构化图表——
visualize工具的spec,用 echarts 渲染(折线 / 柱状 / 面积 / 饼图 / 散点)。 - SVG / HTML 组件——模型写了
svg /html 围栏,逐 token 流式渲染到沙箱 iframe;也可用visualize的widget参数整件交付。
它复用 DSH 已有的 assistant/chunk 与 tool/call + tool/result 事件,不改 DSH 源码。
特性
- 两类产物、三条交付路径:
visualize(spec)出图表;正文svg /html 围栏流式出组件;visualize(widget)出完整组件。 - 流式渲染:复用已有
assistant/chunk事件拿到逐 token 输出,组件随文本流逐帧更新。 - 双端校验:host
execute与客户端折叠共用同一套纯函数解析器(chartspec/widget),模型漂移不会静默通过。 - 安全隔离:组件代码原样插入
sandbox=""iframe + CSPdefault-src 'none',无 sanitizer 可绕过。 - 渲染体验:图表主题采样
--dsw-alias-*令牌;SVG 按固有宽高比自适应高度;卡片带「适应 / 1.5× / 2×」缩放与状态徽标(生成中 / 已截断 / 完成)。 - 优雅降级:任何校验 / 渲染失败都不留空白、不抛错——退回普通代码块或 JSON 卡。
- 纯函数、可测试:核心逻辑均为无 DSH 依赖的纯模块,97 个单测在 Node 里独立跑。
效果一览
真实对话截图(DSH Web 客户端内渲染):

结构化图表 ·
visualize(spec) → echarts(主题跟随 DSH)

组件 ·
visualize(widget) → 沙箱 iframe
更多渲染示例(由插件自身代码渲染生成):
左:柱状图 · 右:SVG 组件 · 下左:HTML 组件 · 下右:流式渲染(```svg 围栏逐 token 边生成边更新)
安装
从 GitHub 直接安装(推荐,构建产物已入库):
dsh plugin --profile web add github:Moses14159/dsh-visualizer
也可以先克隆、再从本地路径安装:
git clone https://github.com/Moses14159/dsh-visualizer.git
dsh plugin --profile web add /path/to/dsh-visualizer
- 从 Git 安装的插件会在安装时通过
prepare脚本构建。pnpm 为安全起见会拦截构建脚本,若它提示授权,把对应的allowBuilds键加到 profile 的pnpm-workspace.yaml后再执行即可。- 待
dsh-visualizer发布到 npm 后,也可以用裸名安装:dsh plugin --profile web add dsh-visualizer。- 需要本机已安装 DSH(
deepseek-harness)并能启动dsh web。依赖 DSH 的@deepseek-ai/dsh-client-runtime、@deepseek-ai/dsh-llm、@deepseek-ai/dsh-tools(见peerDependencies)。
用法
安装后,在对话里告诉模型即可:
- 说「用 visualize 画个图」→ 模型调用
visualize工具传spec; - 说「写一个 SVG 徽章 / HTML 组件」→ 模型在正文直接流式输出
svg /html 围栏,生成过程中即可看到逐帧渲染; - 模型也可用
visualize传widget参数整件交付(经 host 校验、持久化、可重放)。
工具载荷
visualize 二选一:
// 结构化图表
{ "spec": {
"kind": "bar", // bar | line | area | pie | scatter
"title": "深圳 · 未来 7 天气温",
"xAxis": ["周六", "周日", "周一", "周二", "周三", "周四", "周五"],
"yName": "°C",
"series": [{ "name": "最高温", "data": [32, 32, 30, 31, 29, 31, 32] }]
} }
// SVG / HTML 组件
{ "widget": { "kind": "svg", "code": "<svg ...>…</svg>", "title": "卡片标题" } }
流式围栏
```svg
<svg width="360" height="200" viewBox="0 0 360 200" xmlns="…">
…逐 token 边生成边渲染…
</svg>
```
示例
在对话里直接这样说,就能看到效果(这些图取自真实对话)。
生成一张天气卡片
帮我生成一张深圳现在的天气卡片
模型会调用 visualize 传 widget(HTML),渲染成沙箱组件卡片:

visualize(widget) · 沙箱 iframe 渲染的 HTML 卡片
画一个折线图
用 visualize 画一张北京明天 24 小时的气温变化图
模型会传 spec(line),用 echarts 渲染:

visualize(spec) · echarts 渲染(主题跟随 DSH)
边生成边渲染(流式)
写一个深圳实时天气的 SVG 卡片
模型会在正文写 ```svg 围栏,边生成边渲染:

正文
```svg 围栏 · 逐 token 流式渲染
一次要多种
同时画一张柱状图、一张饼图,再写一个天气卡片
模型会多次调用 visualize,分别产出图表与组件,在对话流里排开。
💡 提示:以上示例要求模型已加载
visualize工具(安装插件后即注册)。若模型没有主动用该工具,直接描述你想要的内容即可,它会优先调用visualize。
架构
两条交付路径
| 产物 | 触发 | 会话事件 | 折叠 | 渲染 |
|---|---|---|---|---|
| 结构化图表 | visualize(spec) |
tool/call + tool/result | visualizer-chart |
echarts |
| 组件(整件交付) | visualize(widget) |
tool/call + tool/result | visualizer-widget |
沙箱 iframe |
| 组件(流式) | 正文 svg / html |
assistant/chunk |
visualizer-widget(逐帧更新) |
沙箱 iframe |
为什么这个形态可行(源码级事实)
assistant/chunk是已存在的会话事件家族:agent-loop 把每个StreamChunk落盘为{ turn, step, chunk },Web 客户端的流式文本正是折叠这些事件得来的——插件复用同一事件流即可拿到逐 token 输出,无需新增 host 侧事件家族。conversationEvents是 cordisService,外部插件可 inject;visualizer-widget与内置assistant-step并行折叠同一批assistant/chunk事件,互不干扰。conversation.chat.node是 keyed slot(replaceRisk: 'none'),外部插件按字符串名注册{ key: 'visualizer-chart' | 'visualizer-widget' }即增量贡献。ChatNodeViewProps/ConversationNodeDefinition/ChatNodeDataMap都是纯类型,构建期擦除,不触发客户端 bundle 的纯度门。- 确定性:
match只读当前事件;同一 Context 每个事件携带或独立推导同一稳定 id(step:<turn>:<step>/widget:<callId>);update按日志seq折叠,可重放。
安全边界
| 层 | 处理 |
|---|---|
| 模型 → spec / widget | host execute 用 parseChartSpec / parseWidgetSpec 校验;非法载荷拒绝执行(工具报错) |
| 正文流 → widget | 客户端 WidgetScanner 只认行首 svg / html 围栏;组件代码不做标记校验(流式中任意字节都可能是合法前缀),安全边界在渲染侧 |
| session log → 客户端 | Definition 的 update/fallback 对结果文本再次解析;assistant/message 全文是冷回放的恢复源 |
| 渲染(组件) | 双层隔离:iframe sandbox=""(禁脚本/同源/表单/弹窗/导航,opaque origin)+ srcdoc 注入 CSP default-src 'none';代码原样插入,无 sanitizer 可绕过 |
| 渲染(图表) | ChartSpec 是纯数据 → echarts setOption;无 HTML/SVG 注入面 |
任何校验 / 渲染失败都降级(不渲染该节点 / 围栏仍以普通代码块显示 / JSON 卡),绝不让对话流出现空白行或抛错中断。
载荷上限
- 图表:≤ 8 个 series、≤ 500 个点、标签 ≤ 120 字符;
- 组件:单件 ≤ 128 KB(UTF-8)、每节点 ≤ 12 件 / 总计 ≤ 512 KB,超限截断或省略并显示「已截断」徽标。
开发
pnpm install
pnpm test # 纯函数单测(97 用例)
pnpm typecheck # tsc --noEmit
pnpm build # tsdown:host + replay + 两渠道 client bundle
pnpm render-demo # 重新生成 docs/ 里的演示图(需本机 Chrome)
- 纯函数模块(
chartspec/widget/to-echarts/to-iframe/svg-geometry/ 两个 fold)不 import DSH、不触达 DOM,因此能在 Node 里独立测试。 - 客户端聚合入口是
src/client/index.tsx;host 工具入口是src/index.ts。
已知边界
- 图表仍是「整图出现」:ChartSpec 经工具调用交付,没有逐 token 流式图表(工具参数本身不是流式的);围栏组件才是流式路径。
- 围栏原文与组件卡并存:模型写出的代码块仍会按普通 markdown 代码块渲染(作为「源码」视图),组件卡在流中增量渲染——两者不互斥。
- 组件是静态的:sandbox 禁脚本,交互式组件(按钮逻辑、动画脚本)不会执行;需要交互时请用图表或让组件纯展示。
- 不包含 mermaid:v1 只支持 svg/html 两种围栏,mermaid 图请走普通代码块或后续版本。
- 围栏必须在行首(≤ 3 空格缩进),行内
svg 不是围栏;围栏内容里的单独成行会闭合围栏(CommonMark 语义)。 - echarts 按需
import('echarts')内联进客户端 bundle(registry 路由只服务单文件,暂不能 code-split)。
兼容性
- Node:
>= 20;DSH:通过外部插件机制加载(profile bundle patch)。 - peer 依赖:
@deepseek-ai/cordis、@deepseek-ai/dsh-client-runtime、@deepseek-ai/dsh-client-ui-conversation、@deepseek-ai/dsh-llm、@deepseek-ai/dsh-tools、react、react-dom。
许可
MIT · Copyright (c) 2026 Moses14159
No comments yet. Be the first to write one.