@crosery/dsh-viewer
Model-facing display_file tool that renders 36 file extensions inline in the web UI: images with a click-to-zoom lightbox, seekable video, audio, PDF, Office and OpenDocument files converted to PDF on the host via LibreOffice, and local HTML in a sandboxed frame; bytes stream from a signed HTTP rout
Install
dsh plugin --profile web add @crosery/dsh-viewer
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✗ |
| 2 list | — |
| 3 mount | — |
| 4 boot | — |
dsh: initialized profile web at /home/node/.dsh/profiles/webFour stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
<div align="center">
<img src="assets/logo.png" alt="dsh-viewer" width="132">
@crosery/dsh-viewer
万物皆可渲染。 一个 display_file 工具,把图片、视频、音频、PDF、Office 文档和本地网页直接渲染进 DeepSeek Harness 的对话流——是真的能看、能播、能拖进度条,不是一个文件名加一个字节数。
[English](README.md) · [中文](README.zh.md) · MIT



</div>
支持的格式:6 类 36 种扩展名
类别 · 渲染元素 · 扩展名
图片 · <img> + 点击开灯箱 · png jpg jpeg webp gif svg avif bmp ico apng
视频 · <video controls>,可拖进度条 · mp4 m4v webm ogv mov
音频 · <audio controls> · mp3 m4a aac wav flac ogg oga opus
PDF · 内嵌阅读器 · pdf
文档 · Host 转 PDF 后内嵌 · docx doc rtf odt xlsx xls ods pptx ppt odp
网页 · sandbox <iframe> · html htm
其余格式仍然给一张带类型、大小和打开链接的卡片——没有任何一种类别是渲染不出东西的。
视觉路由上,png/jpg/jpeg/webp/gif 同时进入模型上下文:一次调用既给你看,也让模型看见。其余只给你看,卡片会如实标注。
截图
图片、视频、音频,一个回合内全部渲染:
Word / Excel / PowerPoint,Host 转换后内嵌:
PDF 与本地网页,实时渲染:
安装
dsh plugin --profile web add @crosery/dsh-viewer
然后把包名加进 profile package.json 的 dsh.profile.bundles,并重启 profile——bundle 成员变更不走热重载。
仓库里直接提交了构建产物,所以这条命令不需要任何构建步骤,也不需要你在 profile 的 allowBuilds 里批准任何脚本。想要锁定版本安装的话,每次 release 也附带了同样的 tarball:
dsh plugin --profile web add https://github.com/Crosery/dsh-viewer/releases/latest/download/dsh-viewer.tgz
桌面版:打开插件 → 添加插件,粘贴上面那条 release 压缩包地址。dsh 命令行拒绝操作桌面 profile(profile "desktop" is managed exclusively by the Electron application),所以这是唯一的入口。在那里升级已装插件需要重启应用。
harness 0.1.7(包括桌面版)需要 0.2.0 或更新的版本。 从 0.1.7 起 harness 会自己检查插件的 peer 范围,而 v0.1.1 的范围止于 0.1.6 以下:安装会被拒绝,已经装好的副本在启动时被悄无声息地跳过——对话照常,但什么文件都不会显示。harness 0.0.1 与 0.1.0-rc.2 – rc.7 需要 0.2.1 或更新的版本:更早的范围从 0.1.0-rc.8 起算。那些版本自己不检查范围,但 npm 和 pnpm 会。
Office 渲染需要一个转换器。从 harness 0.1.6-alpha.2 起(包括桌面版),harness 自带的内置转换器就能处理 doc docx xls xlsx ppt pptx,什么都不用装。rtf 和三种 OpenDocument 格式,以及更早版本上的所有文档格式,仍需要 LibreOffice(PATH 上的 soffice,或 macOS 的应用包):
brew install --cask libreoffice
没有转换器也不影响其余格式,文档卡片会明确说明缺什么。
转好的 PDF 缓存在 $DSH_HOME/.dsh-viewer-cache/,所有 profile 共用,所以一份文档每改一次只转一次,而不是每显示一次转一次。缓存有上限:30 天没用过的产物会被删掉,超过 256 份或 512 MiB 时先删最久没用的。卡片加载它的 PDF 也算一次使用。
它解决什么
dsh 出厂只有 read_image:它存在的目的是把图片塞进模型上下文,所以
- 路由模型不声明
image输入时,它直接拒绝; - 只认 PNG/JPEG/WebP/GIF;
- 视频、音频、PDF、网页完全没有入口;
- 而且在 harness 0.1.2 及以前,内置 Web 客户端不给它画卡片——
read_image落在通用工具行上,人在屏幕前看到的只有一行Read image /path/to/a.png。(0.1.3 起 harness 自带了read_image视图,本插件在这个 key 上的卡片会主动让位。)
这个插件反过来:它存在的目的是把文件放到用户屏幕上。所以纯文本路由不是拒绝理由,非图片媒体也不是。
· read_image(出厂) · display_file(本插件)
目的 · 进模型上下文 · 进用户视野
纯文本路由 · 拒绝 · 正常显示,只是模型看不见
图片 · PNG/JPEG/WebP/GIF · 上述 + SVG/AVIF/BMP/ICO/APNG
视频 / 音频 · 无 · MP4/WebM/MOV/OGV,MP3/WAV/FLAC/OGG/M4A/Opus
PDF / 网页 · 无 · 内嵌 iframe
Office 全家桶 · 无 · docx/doc/rtf/odt、xlsx/xls/ods、pptx/ppt/odp(Host 转 PDF 后内嵌)
Web 卡片 · 0.1.2 及以前无(通用行);0.1.3 起有 · 有(在 harness 没有 read_image 视图的版本上,本插件也给它画卡片)
两条字节通道
一个卡片可能从两个地方拿到字节,优先级如下。
签名资源路由(/crosery/dsh-viewer/asset)——Host 直接流式吐字节。这是唯一能承载视频/音频/PDF/网页的通道,也是唯一支持 Range 请求的通道,而 Range 正是 <video> 能出现进度条、Safari 肯开始播放的前提。
持久化附件——ctx.attachments 那条老通道。只有图片,但它在两种情况下不可替代:文件系统后端不暴露本地路径(远程 workspace)时,以及渲染出厂 read_image 结果时(那份结果只有附件,没有 URL)。
两条通道不是冗余:视觉路由上的 PNG 仍然要走附件,因为那是它进入模型上下文的唯一方式。0.1.7 起持久化图片改由对话自带的加载器读取,同一会话里每张图只读一次。
桌面版里,视频和音频从 Host 的回环地址(__DSH_TRANSPORT__.streamBaseUrl)加载,而不是窗口的 dsh-app: 源:应用的协议转发会去掉 Content-Length,Chromium 于是把媒体 URL 的第一次加载当成不可拖动的流。资源路由按签名授权,所以除了源以外什么都不变。
资源路由的安全边界
路由从不接受浏览器给的路径。工具执行时用一个每 harness 一份的密钥对已解析的绝对路径做 HMAC,路径和 MAC 一起放进 URL;路由只在 MAC 验过之后才认那个路径。
- 密钥:32 字节随机数,
$DSH_HOME/.dsh-viewer-asset-key,0600,首次使用时生成。自包含的签名而不是进程内 token 表,是因为卡片必须活过重启:从会话日志重放出来的卡片手上只有几个月前铸的那个 URL。唯一的例外是转换过的文档:它的 URL 指向缓存里的 PDF,那份 PDF 被淘汰之后,卡片里的 frame 会得到404,直到display_file重新转换这个文件。 - 篡改路径、换密钥、去掉签名、改用 padded base64 拼写——全部 404,且「没签名」和「签了但文件不存在」返回同一个状态码,探测者无法从状态码学到文件是否存在。
text/html和image/svg+xml带 CSP 响应头下发(前者sandbox,后者default-src 'none')。同源直接导航到资源 URL 时,这两种文档否则会以应用的 origin 执行脚本。所有响应带nosniff。- 字节走
createReadStream(processPath)而不是ctx.fs.readBytes——后者会把整个文件读进内存,而两小时的视频正是这插件存在的理由。
Office 全家桶怎么转
目标格式永远是 PDF。纯前端方案(docx-preview / exceljs + 表格网格)过不了本插件的约束:浏览器半边是 lazy-CJS bundle,模块表只答应 shell 基线,所有依赖都得内联,而 PPTX 根本没有可内联的免费渲染器。一个转换器产出一种格式,也意味着卡片只有一条文档代码路径。
- 转换器:LibreOffice headless。PATH 上的
soffice/libreoffice,或 macOS 的/Applications/LibreOffice.app/...;都没有时卡片如实说「需要安装 LibreOffice」并给出brew install --cask libreoffice,不失败整个调用。 - 每次调用私有 profile(
-env:UserInstallation=file://…)。LibreOffice 多个实例共用用户 profile 目录会互相破坏;更要命的是桌面上已经开着 LibreOffice 时,headless 调用会立刻退出且不产出任何文件——这是「明明成功了却没有 PDF」最常见的原因。 - 串行队列。私有 profile 解决了互相破坏,但没解决成本:几个 LibreOffice 冷启动同时跑会拖垮笔记本。重复由缓存吸收。
- 缓存键含 LibreOffice 版本 + 源文件 path/mtime/size。同样的字节经过新版 LibreOffice 是不同的产物,键里不带版本,升级后就会命中陈旧渲染而且看不出来。不对文件内容做摘要:几百 MB 的演示文稿不该为了「查一下转过没有」被读两遍。
- harness 内置转换器的缓存键是它的配置(字体、回退字体、图片分辨率等)加源文件的 path/版本/大小,而不是它的
generation:上游每个转换器实例都用randomUUID()生成generation,拿它做键等于每次重启都是冷缓存,缓存永远命中不了、只会越长越大。配置变了就是新产物,重启不是。它看不到的是配置不变、随 harness 升级换了引擎的情况;这时的产物仍是同一份字节的忠实渲染,会像其他产物一样按时淘汰。 - 产物写在
$DSH_HOME/.dsh-viewer-cache/,先以临时名写在缓存目录里,再 rename 就位——读者要么看不到产物,要么看到完整的,不会拿到半个 PDF 喂给阅读器。临时文件必须和产物在同一目录:rename不能跨文件系统,LibreOffice 的输出目录在系统临时目录下,而 Linux 上/tmp常是单独挂载的 tmpfs,直接从那里 rename 会报EXDEV。所以 LibreOffice 的 PDF 是先复制进缓存目录再 rename。 - **缓存有界,按最近
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::6,最后更新 2026-11-06