dsh-mosaic
Mosaic 是一个轻量的 DeepSeek Harness 可视化回答插件。它把模型的回答从纯文本升级为紧凑的原生 React 组件,并保留一个受控的沙箱 HTML/JS 逃生舱,用来承载固定组件做不了的游戏、模拟器、音频、地图或自定义画布。
它是可选增强,不强制每个回答都带 UI:当指标、对比、流程或交互用组件更容易扫读时,模型可以输出 dsh-view;纯文字回答同样有效,不需要为了“用插件”而加组件。
它刻意不追求“什么都能画”:
- 主包没有 ECharts / Three.js;
flowchart、sequence、state、er、gantt、mindmap用官方 Mermaid 发行包,作为懒加载资产,只在出现这几种图时下载(原始 5.5 MB,gzip 后约 1.3 MB,带版本号可长期缓存)。 timeline、layers、quadrant、tree、funnel、venn、swimlane由插件自己排布,不依赖图引擎;节点宽度按标签文字估算,不截断成省略号。- 每个 diagram 卡片右上角有
svg/png/drawio三个导出按钮:前两个直接下载图片(颜色、字号按当前主题解析后内联,脱离页面也能看),drawio导出标准 mxGraph XML,在 draw.io 里继续编辑。 - 组件目录保持精简,字段、校验、提示词来自同一份定义。
- 普通文件/Office 只给链接,不在聊天里硬解。
- 交互默认在本地完成;只有声明了
action的组件才回模型。
预览
实际 DSH Web 页面渲染:

两个通道
正文围栏:在回答里写 dsh-view JSON。围栏标记必须正好是 dsh-view;如果模型误用 json,客户端在 assistant 行内有一层兼容识别,但不要依赖它。
```dsh-view
{"title":"Ops snapshot","items":[
{"type":"stat","label":"Uptime","value":"99.96%","delta":"+0.02%"},
{"type":"callout","content":"No incidents in the last 7 days."}
]}
```
工具卡片:模型调用 render_view,适合独立面板、表格、图表或小组件。另有 validate_view 做发出前校验。
一套视觉风格 + 可覆盖的模型 Prompt
界面只有一套轻卡片风格,颜色全部取自主题令牌(--dsw-alias-*),因此自动跟随 DSH 的浅色/深色与字号,不再有主题切换器。
插件向模型贡献一段说明可视化能力的 Prompt,默认是包内文本。
Settings → Mosaic 就是这段文本的编辑页:打开即显示当前生效的内容(默认就是内置文案),可以直接改、点「保存」,随时点「恢复默认」回到插件自带文案。保存后从下一轮模型请求开始生效,所有浏览器看到的是同一份,重启也在。两个浏览器同时编辑时,后保存的那个会收到「已在别处被修改」,页面载入最新内容而不覆盖别人的修改。
生效顺序是「你保存的覆盖 > 配置文件里的 config.prompt > 内置文本」。想在 profile 里批量固定一份基线,仍然可以在 cordis.patch.yml 给 mosaic 行加 config.prompt;留空、类型不对或超过 128 KB 会回落。
覆盖内容存在 Host 数据目录的 mosaic/prompt.json,不进 profile 的组合文件,所以升级、换浏览器都不丢。写入是原子的(临时文件 + fsync + rename,权限 0600),失败时旧内容保持原样;写不进去时页面会明确提示「本次仅在进程内生效」。文件如果损坏或读不出来,页面同样会提示,并按内置内容运行、保留原文件供你修复。
读写接口都只接受同源请求(核对浏览器的 Sec-Fetch-Site 与 Origin),写请求还要求 content-type: application/json;响应里不带 Host 的绝对路径。跨站页面既读不到也改不了这段文本。渲染出的卡片右上角另有「复制 JSON」,用于取回该块的围栏文本。
组件
- 布局:
textrowcolgridcarddividerspacer - 展示:
statbadgeprogresslisttablekeyvaluestepscalloutcodediffchartdiagram - 媒体:
imagevideoaudiopdflink - 交互:
buttoninputselectslidertabsquiz - 高级:
widget
diff 带行号和 -/+ gutter,长行自动换行。
diagram 支持 13 种图,按“读者要回答的问题”选:
| 读者要看懂 | kind | 建议规模 |
|---|---|---|
| 分支、重试、判断 | flowchart |
9 节点 / 12 边 |
| 谁在什么时候说了什么 | sequence |
5 条生命线 / 12 条消息 |
| 时间线上的事件 | timeline |
7 项 |
| 分层结构 | layers |
6 层 |
| 两轴定位(优先级、取舍) | quadrant |
12 项 |
| 父子层级、组织关系 | tree |
12 节点 / 4 层 |
| 逐级流失的转化 | funnel |
6 级 |
| 集合重叠 | venn |
3 个集合 |
| 跨角色的交接 | swimlane |
6 步 / 5 条泳道 |
| 状态与迁移 | state |
9 个状态 |
| 表结构与关系 | er |
8 个实体 |
| 排期 | gantt |
12 个任务 |
| 主题发散 | mindmap |
12 个分支 |
legend: [{label, tone}] 给颜色加说明:tone 表达含义时写,纯装饰时省略。
完整字段见 SKILL.md。
日志
插件会把自己的运行情况写进一个 JSONL 文件,出问题时优先看它:
$DSH_HOME/logs/dsh-mosaic/YYYY-MM-DD.jsonl # 未设置 DSH_HOME 时是 ~/.dsh/logs/dsh-mosaic/
- 一行一个 JSON 对象,字段固定:
ts、level(debug/info/warn/error)、event,其余按事件附带(如tool、ms、status、route、source、reason)。 - 记录什么:激活/停用、Prompt 的读取与保存(来源、字符数、是否落盘)、工具执行(名称、结果状态、耗时)、路由请求(方法、状态码、耗时)、技能被读取、以及浏览器侧上报的失败(
client.前缀:围栏解析失败、Mermaid 失败、示意图导出失败、Prompt 读写失败、Widget 动作被限流/超限丢弃)。 - 不记录什么:Prompt 正文、组件 JSON、任何文本字段的内容。字段名为
prompt/text/code/content/secret/token/password/apiKey时只写字符数;其它字符串超过 300 字符会被截断。 - 体积:单文件超过 2 MB 滚动为
<日期>.1.jsonl;超过 7 天的文件在下次启动时删除;文件权限0600。写不进去时不会影响插件,只在宿主日志里告警一次。 - 浏览器侧的失败通过同源
POST /dsh-mosaic/log上报(请求体上限 4 KB,非本机权威或跨站一律 403,客户端每分钟最多 20 条);接口在日志落盘之后才回 204,看到成功就是真的写进去了。 - 默认级别
info;想要更细可以给 mosaic 行加config.logLevel: debug(或warn减少噪音)。
排查示例:
# 只看错误
grep -h '"level":"error"' "$DSH_HOME"/logs/dsh-mosaic/*.jsonl
# 看最近一次激活和 Prompt 来源
grep -h '"event":"plugin.activate"\|"event":"prompt.hydrate"' "$DSH_HOME"/logs/dsh-mosaic/*.jsonl | tail -5
# 统计工具失败次数
grep -c '"event":"tool.failed"' "$DSH_HOME"/logs/dsh-mosaic/*.jsonl
已知边界
- 本插件的 npm 版本独立递增(当前
1.1.3),不再与 Harness 版本号对齐;兼容范围由package.json的engines.dsh声明,最近一次实测的 Harness 版本见「安装」一节。 - 围栏渲染靠扫描页面里的代码块实现:0.2.1 全系列(实测 rc.3 与 rc.5,master 同样)都没有给插件开放「渲染 fenced code block」的扩展点。工具通道
render_view+tool.call.toolview是原生做法,围栏是它的补充。 - 从 0.2.1 起代码块由 Shiki 渲染,
<code>上没有language-*类;识别靠「代码块位于 assistant 行内 + 正文能解析成合法视图 spec」这条路径,用户消息、工具结果、推理块里的代码块都被排除。 diagram与chart自带缩放/平移:卡片右上角有−/+/⟲/⤢,放大后按住图面拖动即可平移,Ctrl/⌘ + 滚轮缩放,双击复位;示意图按原始尺寸渲染,不会被压缩到卡片宽度。触屏上图表区域只拦横向拖动(touch-action: pan-y),手指上下划仍然滚动聊天页面。widget里调用window.mosaic.action()只有真实用户手势之后才会把动作发回模型:Widget 自己也在判断navigator.userActivation,但决定权在宿主——消息的 nonce 就写在 Widget 自己的文档里,模型代码能读出来直接伪造,所以父页面独立再校验一次自己文档的激活状态。确实需要自行提交的 Widget 要在节点上写actions: "programmatic";无论哪种,整个会话共享「每分钟 10 次、单次 payload 16 KB(按 UTF-8 字节算)」的额度,新开一个 Widget 不会重置。浏览器若不提供navigator.userActivation,非programmatic的 Widget 会被按「没有手势」处理,按钮不会回模型。widget是 iframe,拿不到宿主的ctx.locale,因此内部文案由调用方自己写;主题靠宿主令牌值传入。围栏是独立的 React root,切换外观不会触发它重渲染,所以 widget 会观察body[data-ds-dark-theme]这一个外观属性并在变化时重发令牌——这是插件唯一读取的宿主属性。- Mermaid 资产走包内私有路由
/dsh-mosaic/assets;没有 Web 服务时 diagram 回落到内置 SVG 布局。 - 设置页的导航图标走
settings.section.icon插槽。Harness0.2.1-rc.5的导航图标是写死的映射,没有这个插槽,Mosaic 会落到默认齿轮;升级到声明该插槽的版本(0.2.2-alpha.3起)后自动显示自己的 mark,插件不用再改。 - 自有路由(
/prompt、/log)先验 Host 必须是本机权威(loopback、监听地址,或config.trustedHosts里声明的),再验Sec-Fetch-Site与Origin。只比 Origin 和 Host 挡不住 DNS rebinding:被 rebinding 的页面这两者都等于攻击者域名,看起来完全同源。宿主把 Web 服务绑到非 loopback 地址时,请把对应主机名写进config.trustedHosts,否则这些路由会拒绝请求。 - bundle patch 只插入 Mosaic 自己这一行,不改动任何其他插件。若你还装了别的围栏类插件,两者可以对同一段围栏各显一套,按需在 profile 里关掉其中一个即可。
- 持久化只用「Host 数据目录里的一个 JSON 文件」,没有走 DSH 的 storageDomain。实测(rc.5):
storageDomain可用,但记录校验只在重新打开时执行(put不校验);single布局遇到版本变化会直接拒绝打开,compatibleVersions也不适用。对一份 Prompt 来说这些能力换不来收益,反而多一个依赖和三条失败分支。 - Prompt 编辑页是对平台形态的补充,不是平台标准做法。DSH 把 Prompt 视为 composition 配置(system-prompt 的 README 明确写着没有终端用户编辑 Prompt 的 API),标准平面是
Config+cordis.patch.yml,客户端改值应走@Remote,插件自有数据应走存储服务。这里为了「只装一个包、不引入 peer 依赖」,写入用了自建路由(Host 权威校验 + 同源校验)、持久化用了自建的mosaic/prompt.json。将来宿主提供多行配置表单或标准 Remote 时,这块应当迁移过去。
本地优先
表格排序/筛选/导出、tabs、quiz 判题、输入值、slider 拖动都在浏览器内完成。button 的 local 支持 reset、copy、open、fullscreen;只有 action 会把一条 [mosaic-action] 用户消息发回模型。
状态按「会话 + 槽位 + 内容指纹」写入 localStorage,最多保留 100 块,刷新或重开会话时恢复。界面文案(按钮、提示、诊断)走 DSH 的 Client locale 服务,内置 en 与 zh 两套。
沙箱 widget
widget.code 是 HTML 片段或完整文档,渲染在:
<iframe sandbox="allow-scripts allow-forms allow-popups" srcDoc="..."></iframe>
- 不透明源(没有
allow-same-origin),读不到宿主页面、cookie、storage。 - CSP:没有网络;脚本仅内联/eval/blob;图片和媒体只允许
data:/blob:。 - 宿主校验
event.source和每次挂载的 nonce。 - 主题通过
--mosaic-*CSS 变量注入;widget 用window.mosaic.action(name, payload)回传动作,用window.mosaic.resize()主动报高度。 - 外部 CDN 脚本会被 CSP 拦掉;需要库时把代码内联进 widget。
安装
在 DSH Plugin Manager 中安装本插件,或使用命令行:
# 发布到 npm 后(普通 dsh web profile)
dsh plugin --profile web add dsh-mosaic
# 本地开发目录
dsh plugin --profile desktop add link:/absolute/path/to/dsh-mosaic
安装后重启或刷新 DSH Web,新会话即可使用。bundle patch 只插入本插件一行,不会关闭或覆盖别人的插件。
兼容性:npm 版本独立于 DeepSeek Harness 版本,1.1.x 对应下方实测的 Harness 范围;声明式范围见 package.json 的 engines.dsh(当前 >=0.2.1-rc.1 <0.3.0-0)。
| 插件版本 | 实测通过的 DeepSeek Harness | 说明 |
|---|---|---|
1.1.3 |
0.2.1-rc.5 |
设置页改用插件自己的图标;需要 Harness 声明 settings.section.icon,rc.5 尚未提供 |
1.1.2 |
0.2.1-rc.5 |
修复示意图标签里的括号/方括号/花括号导致解析失败;补上边与消息的可读预算 |
1.1.1 |
0.2.1-rc.5 |
运行时代码与 1.1.0 相同;仅修正发布工作流的无效输入 |
1.1.0 |
0.2.1-rc.5 |
13 种示意图、PNG/SVG 导出、warnings= 可读规模提示、布局修正 |
1.0.0 |
0.2.1-rc.5(同时复核 rc.3 行为) |
首个稳定版;含 Widget 沙箱、路由权威、Prompt 并发等修正 |
0.2.1 |
0.2.1-rc.3、0.2.1-rc.5 |
上一版,npm 上的历史发布 |
Harness 升级到 0.3.x 后,请先在新版本上验证再放宽 engines.dsh,并在此表补一行。
校验与建议
validate_view 和 render_view 除了报错,还会返回 warnings= 行,指出“能渲染但读起来费劲”的地方,例如:
- 表格超过 8 行、列表超过 7 项、图表超过 12 个点或只有 1 个点;
- 同一个视图里
accent用在超过 2 个元素上; - 卡片嵌套超过 2 层;
- 某类图超过它自己的可读规模(flowchart 9 节点、sequence 5 条生命线……);
widget里引用了外部 CDN 资源(沙箱没有网络,必定加载失败)。
这些不阻塞渲染,只是给出改法。
限制
- 硬上限:80 节点、6 层嵌套、表格 40×8、图表 40 点、diagram 12 节点/20 边、widget 代码 20k。可读规模见上文的表格与
SKILL.md。 - 不读本地文件、不渲染 docx/pptx/xlsx;这些用
link卡片交给宿主打开。 - 不内置数学公式排版;公式可以放在
code或普通文本里。 - 围栏渲染依赖 DSH 当前的代码块 DOM;宿主结构变化时可能需要适配。
开发
node --test test/*.test.mjs
node --check lib/index.js
node --check lib/client.js
License
MIT
No comments yet. Be the first to write one.