loyalchiiina/dsh-chat-image-lightbox

loyalchiiina/dsh-chat-image-lightbox

聊天图片全屏查看器:点击任意图片打开原始分辨率大图,支持缩放(滚轮、按钮、快捷键)、拖拽平移、左右翻页、文件名显示、下载保存,以及键盘方向键与移动端滑动切换。输入框旁一个「对话图显控制」面板,可一键把整个会话的图收成可调列数(1–12,自动记忆)的缩略图网格,或整块隐藏并收掉占位、随时还原;图片只原地加样式,绝不搬移或重挂节点。宿主侧 display_image 工具支持本地路径或目录(TIFF/HEIC 自动转码,下载仍保存原文件)、网络直链、以及 Bing、花瓣、搜狗的关键词搜图(按抽样可加载率选源,被防盗链拦截的自动转存,坏链按字节校验剔除),返回可直接粘贴的 Markdown;并在 system prompt 常驻一条投递规则,要求发出的图必须写进回复正文(![](…)),因为折叠的工具行读者看不到。

UI 增强chat-imagedeepseek-harnessgallery

DSH Chat Image Lightbox 🖼

### 🛡 v1.8.9 — 工具行折叠 ≠ 已发送:铁律升级为「必须复述 + 计数自检」 / Collapsed tool row ≠ delivered
实测事故(2026-10-21):插件 20 张图(10 种格式)全部交付成功,但模型把图全留在 display_image 的工具结果里,
正文只写了一张格式对照表 → 用户回「一张没看见啊」。原因是 1.7.8~1.8.8 的铁律只写了「不得只依赖工具行」,
可以被「正文写了字但没写图」绕过。
1.8.9 起铁律升级为 6 条硬约束 + 1 条收尾自检:必须把工具返回的 `` 原样复述进正文、
表格/结论/清单都不算图、发前数一遍正文里 `![` 的数量必须等于声称的张数。
Opt out with config.injectImageLaw: false.
Real incident (2026-10-21): all 20 images were delivered fine, but the model left every one of them inside the
collapsed tool result and only wrote a format table in the body — the user replied "not a single one visible".
Since 1.8.9 the rule demands verbatim echoing of the returned markdown into the reply body, forbids passing a
table off as an image, and requires a pre-send count of ![ occurrences.
### 🛠 v1.7.5 — 发图片失败已修复 / Image-sending bug fixed
如果你在 DSH 里发图片失败(报 prompt rejected + ERR_DLOPEN_FAILED + session/agent-busy,纯文字正常),
那是 1.7.4 及更早的已知问题,1.7.5 已修复。升级 + 重启 DSH 即可:
```sh
pnpm add dsh-chat-image-lightbox@latest
`
原因:本插件旧版把 sharp 精确锁在 0.34.4,与 DSH 内核的 ^0.35.3 冲突,两个版本争抢同名 libvips-42.dll。
详见 Release v1.7.5 · 排查见 ⑥ 发图失败 与 Troubleshooting ⑥。
If sending an image fails with those errors (plain text works fine), it is a known issue in 1.7.4 and earlier, fixed in 1.7.5. Upgrade and restart DSH. Cause: the old version pinned sharp to 0.34.4, conflicting with the kernel's ^0.35.3 over the same-named libvips-42.dll.

对话里的图,值得被认真看。

Images in chat deserve a proper look.

English | 中文

📦 npm 包名 / npm package name:dsh-chat-image-lightbox(无 scope / unscoped)
旧包名 @loyalchiiina/dsh-chat-image-lightbox 已被取代,历史版本已标记弃用。
The legacy scoped name @loyalchiiina/dsh-chat-image-lightbox is superseded; its older versions are deprecated.
dsh plugin --profile desktop add dsh-chat-image-lightbox
当前版本 / Current version:1.8.9
适用内核 / Requires:本包声明 engines.dsh: ">=0.1.7-0 =0.1.7-0 <0.3.0"(带世代上界,因为 peer 是运行时门禁,不匹配会被宿主直接禁用);本版实测验证于 DSH NEXT 2.0.19-next,客户端组件 @deepseek-ai/dsh-client-ui-tool 实测 0.2.1-rc.2。
This package declares engines.dsh: ">=0.1.7-0 =0.1.7-0 <0.3.0" (bounded by generation, because peers are a runtime gate: a mismatch makes the host disable the plugin outright); this release was verified end to end on DSH NEXT 2.0.19-next, with @deepseek-ai/dsh-client-ui-tool measured at 0.2.1-rc.2.
⚠️ 升级到 1.7.3 前请注意 / Before upgrading to 1.7.3
1.7.2 起 移除了「跳图」功能(该功能在多次迭代中不够稳定,已整体删除,界面上不再出现 ⌖ 按钮)。
Since 1.7.2 the jump-to-image feature was removed — it proved unreliable across iterations and has been deleted outright; no ⌖ button appears anymore.

效果预览 · Screenshots

灯箱查看 · Full-screen lightbox

点开正文里任意一张图即全屏灯箱:原图按屏幕高度自适应,右下角标注当前是第几张(如「图4」)。
右侧竖排工具栏自下而上依次是:✕ 关闭、⬇ 下载、‹ › 上一张 / 下一张、复制、在文件夹中显示、
缩略图网格、隐藏图片、− + 1×(缩小 / 放大 / 复位比例)、↑ ← ↺ → ↓(上移 / 左旋 / 复位 / 右旋 / 下移),
底部显示当前缩放百分比。

切换图片 · Paging between images in the same message

同一条消息里的多张图可以连续翻页(截图里是 30 张的第 21 张)。红色箭头指的是右侧的 ‹ › 两颗按钮,
也可以用键盘方向键翻页;翻页后计数(如「21 / 30」)与缩放比例同步刷新。

缩略图分列 · Thumbnail grid with adjustable columns

3 列 · 3 columns

4 列 · 4 columns

5 列 · 5 columns

一键把整个会话的图片收成网格;面板里的「缩略图显示列数」可设 1–12 列,输入即生效并保存为默认值。
每格高度统一、object-fit: contain 居中,所以竖图横图混排也不会参差不齐;网格之后的正文从最左侧开始,不会被挤到中间。

隐藏 / 展开 · Hide / restore

一键隐藏所有图片,并把它占的空间完全收掉(不会留下空白带);再点一下原样恢复。
One click hides every image and closes up the space it occupied (no blank band left); another click restores everything.

功能总览 · At a glance(中英对照 / Bilingual)

灯箱查看 · Lightbox viewer

中文 · English 点击任意对话图片进入全屏灯箱 · Click any chat image for a full-screen lightbox 滚轮缩放、拖拽平移 · Wheel zoom, drag to pan 工具栏缩放按钮 ⊕ ⊖ 与 1× 复位 · Toolbar ⊕ / ⊖ zoom buttons with 1× reset 方向键平移视角(按住连续移动) · D-pad moves the view (press-and-hold repeats) 左右翻页:按钮 / 键盘 / 触屏滑动 · Prev/next via toolbar, keyboard or swipe 下载原图(自动识别扩展名) · Download original (extension auto-detected) 复制图片文件绝对路径 · Copy the image's absolute local path 在文件夹中显示(资源管理器定位) · Reveal in folder (file manager locates the file)

会话级图片控制 · Conversation-wide controls

工具行里的 「对话图显控制」 按钮打开面板,面板内含三项:

The "对话图显控制" button in the composer row opens a panel with three controls:

中文 · English 缩略图:一键把整个会话的图片收成网格 · Thumbnail: collapse every image in the conversation into a grid 缩略图显示列数:1–12 可调,改过即存为默认值 · Columns per row: 1–12, and your value is saved as the default 隐藏:一键隐藏 / 再点恢复,不留空白 · Hide: hide / restore all images, leaving no blank space

列数说明:默认 3 列。设置后会记住,下次启动仍用该值;改回其他值即覆盖默认。
Columns: 3 by default. The value is remembered across restarts; changing it again simply replaces the stored default.

格式与清晰度 · Formats & fidelity

中文 · English TIFF/TIF、HEIC 自动转换显示 · TIFF/TIF, HEIC auto-transcode for display TIFF/PNG/BMP 无损原像素(逐像素零误差) · TIFF/PNG/BMP lossless, native pixels (bit-exact) 2 亿像素大图流畅显示 · 200-megapixel images display smoothly 11 种格式实测支持(JPG/PNG/WebP/GIF/AVIF/SVG/BMP…) · 11 formats verified (JPG/PNG/WebP/GIF/AVIF/SVG/BMP…) 网图被防盗链拦截时自动转存本地显示 · Hotlink-protected remote images are auto-saved locally

宿主侧工具 · Host-side tool (display_image)

中文 · English 关键词搜网图(Bing/花瓣/搜狗 3 源回退,按抽样可加载率选源) · Keyword image search (Bing / Huaban / Sogou fallback, source chosen by sampled loadability) 搜图自动过滤广告与他人面孔 · Auto-filters adverts & other celebrities 本地文件/目录直读(TIFF/HEIC 转码) · Read local files/dirs (TIFF/HEIC transcoded) 网图落地保存到本地目录(save_to) · Save remote images to a local folder (save_to) 能直连就直连,被拦截才自动转存 · Direct CDN when it works, local copy only when blocked 代理自动探活(无需手配) · Auto proxy discovery (TCP probe)

隐私与安全 · Privacy & safety

中文 · English 一切本地服务仅回环(loopback-only) · All local services are loopback-only 打开文件夹用参数化调用,绝不拼接 shell 命令 · "Show in folder" spawns argv, never a shell string 保存说明不回显本机路径 · No local paths echoed in confirmations 只作用于对话区图片,不碰侧栏/头像 · Only touches conversation images, never the sidebar or avatar

English

A DeepSeek Harness plugin that displays images inline in the chat with a lightbox overlay — zoom, download (save-as dialog), and prev/next navigation.

Pictures from your agent sit tiny inside a bubble — to see detail you have to save them and open elsewhere. With this plugin, one click opens any chat image full-screen: zoom, drag-pan, download, and flip through the others in the same message.

Features

  • Inline display: Images in AI responses render directly in the chat (via markdown ``)
  • Click to zoom: Click any image to open a full-screen lightbox; clicking the enlarged image zooms in again, and clicking while already zoomed closes the lightbox (the zoomed image covers the backdrop, so this guarantees the close target is reachable). Esc or clicking the backdrop also closes it.
  • Scroll to zoom & pan: When zoomed in, use the mouse wheel to zoom further and drag to pan around the image
  • Download: Click the download button ⬇ to save the image (triggers browser save-as dialog for same-origin images; filename is sanitized and the extension is derived from the image type)
  • Navigation: Left/right arrow buttons, keyboard arrows, or swipe left/right on touch devices to switch between multiple images
  • Caption: The viewer shows the current image's filename/caption
  • Accessible: The lightbox is a proper role="dialog" with aria-modal, button aria-labels, and focus management (focus returns to the trigger on close)
  • Smooth: Adjacent images are prefetched so navigation feels instant
  • Close: Click backdrop, press Escape, or click ✕ to close
  • ZCode-style rendering: chat images are styled in place — never moved, resized or reparented — so DSH's React virtual list is never fought with (no more vanished/duplicated images or console removeChild errors)
  • Capped, not cropped: every chat image renders with its aspect ratio preserved, capped at min(460px, 64vh) height (the ZCode look); charts and diagrams stay fully readable
  • Responsive image row: when one message contains multiple images in one container, they lay out as a responsive CSS-grid row — via a class on the container only, without moving any nodes
  • HD original on open: clicking any image opens the lightbox with the full-resolution original image — zooming in shows the real high-resolution file
  • Clean UI: The lightbox toolbar (download / close), prev/next arrows, counter and caption are hidden whenever the lightbox is closed — they only appear while you are actually viewing an image, so they never clutter the chat layout
  • Zoom / pan pad: the lightbox also has explicit controls for what the gestures do — ⊖ ⊕ 1× for zoom with a live percentage readout, and a d-pad for moving the view (press and hold to keep moving). Wheel, drag and +/-/Shift+arrow all still work
  • Conversation-wide image controls in one popover: a single compact 「对话图显控制」 trigger sits next to the access-mode control and slides out a panel with the thumbnail switch, the hide switch and the column field — no row of buttons cluttering the composer
  • Thumbnail grid with configurable columns: one click collapses every image in the conversation into a grid of small tiles. The 缩略图显示列数 field sets the columns per row (1–12); the value you enter is saved and reused on the next launch
  • Hide / show: one click hides every image and the space it occupied closes up completely (no blank band left behind); another click brings them back intact
  • Only conversation images: the plugin deliberately ignores pictures outside the message list — the signed-in account avatar and the sidebar glyphs are ordinary `` elements too, and they are left untouched (fixed in 1.7.2)
  • Safe by construction: the thumbnail look is applied with classes and inline styles only. No node is ever moved, removed or reparented — an earlier build that did so made DSH's conversation slot throw DOMException and render the transcript empty
  • Auto-enhance: MutationObserver automatically enhances new images added to the chat, and re-adopts images whose class a React re-render overwrote
  • Bring images into the chat (`display_image` tool, host side): one tool takes a local path (file or directory — TIFF/HEIC transcoded, optional downscale), a remote url, or a query (keyword image search: Bing first, then Huaban and Sogou — each source is gated by a sampled loadability check, so a source that has results but no loadable pictures is skipped) and returns ready-to-paste Markdown. Any file on this machine can be served in place via /images/?abs= (loopback-only), no copying into the gallery root required
  • Hotlink fallback (1.7.2): a search result can answer 200 and still fail in the browser, because some CDNs (Huaban via Tencent EdgeOne) reject any request whose Referer is not their own domain — the picture then shows as "无法预览". The tool now probes each URL the way a browser would and only re-saves the ones that would be blocked, so working links stay direct and nothing is downloaded unnecessarily
  • Source-health gate (1.8.1): "the API answered with results" used to be read as "the source is usable". Huaban's search API keeps answering 200 with twenty pins while every one of its image URLs returns 567 from Tencent EdgeOne — so a search returned five pictures that all rendered as "图片无法预览", and because the first source "had results" the search never fell through to another one. Each source is now sampled (4 URLs, probed the way a browser would); below a 30% loadability ratio the source is skipped with the reason recorded, and if no source clears the bar the best one is used with that stated plainly in the note. Source order is now the one that measurably works (Bing → Huaban → Sogou; Sogou now answers anonymous callers with forbid, which is reported as "source unavailable" instead of being silently read as "no pictures found")
  • Concurrent rescue (1.8.1): rescuing blocked images was serial — ten pictures took 18.4s. It is now four lanes wide (result order preserved, one failure does not cancel the rest), and URLs already probed by the health sample are not probed a second time
  • Failures are stated (1.8.1): a failed rescue used to vanish into an empty catch (e) {}, leaving the user with broken thumbnails and a silent log. The reason is now recorded on rescueError and surfaced in the note ("N images were blocked by the origin and could not be rescued"), so "no image came through" is at least visible
  • The auto-rescue cache no longer grows forever (1.8.1): ~/.dsh/image-gallery-auto was write-only (measured: 129 files / 187 MB). It is now checked before every rescue and pruned oldest-first past 500 files or 512 MB (a file that cannot be deleted does not abort the sweep)
  • Survives a Junction install (1.8.4): the plugin is a directory Junction into local-plugins in all three profiles. Node resolves require from the module's real path, so createRequire(import.meta.url)("sharp") walked up from local-plugins/ while sharp/undici live in profiles//node_modules — the link cut them out of the chain. Consequence: the startup line said sharp: unavailable and every TIFF/HEIC rendered as "图片无法预览", and undici was unreachable too. resolveDep() now searches several roots (module path / process.execPath / EXEPATH / cwd, eight levels up each) and all three dependencies go through it
  • Direct really means direct (1.8.4): with NODE_USE_ENV_PROXY=1, Node's global fetch applies HTTPS_PROXY by itself — omitting dispatcher does not mean "no proxy". Hosts that only answer direct requests (duitang) were therefore reported as "probe failed" and good images were dropped as broken. Direct fetches now use node:https with an explicit Agent (ignoring env proxies entirely, 5 redirect hops max); getBytes() tries runtime fetch first and falls back to direct on transport failure, treating an HTTP status as the answer rather than grounds for a second route
  • The body no longer points at a file the browser cannot decode (1.8.5) — once 1.8.4 fixed

sharp, TIFFs displayed correctly in the tool row but still showed "图片无法预览" in the reply body, because those two paths are served by different handlers. A body image written as ` is rewritten by the Host into api/file?path=…, and serveFile() there does nothing but mime.lookup(extension) and stream the **original bytes** through — no transcoding at all. Chromium has no TIFF/HEIC decoder, so the fails. Every local picture is now transcoded to a browser-renderable copy before delivery (lossless sources → PNG, others → WebP q92); the body names that copy while localPath` keeps the original. If the result still exceeds the Host's per-image limit (default 20 MB — a 413 which also renders as "图片无法预览"), the width is reduced by 0.7 and retried, five rounds at most. Files whose extension says image but whose magic bytes say otherwise (a saved CDN error page) are dropped before delivery with the reason stated ("后缀是 .jpg,内容却不是图片") instead of a vague transcode failure

  • The candidate pool is four times the ask, so dead entries leave spares (1.8.8)

Asking for three pictures of 「喜羊羊」produced none at all — not because the source lacked them, but because Bing ranked hotlink-hostile hosts first: the top three hits were bkimg.cdn.bcebos.com and i0.hdslb.com, all HTTP 403, while the old code took exactly want hits and treated one failure as a failed query. The pool is now want × 4 (capped at 40), downloaded in batches of eight, stopping as soon as the requested count is proven good: it neither downloads forty files to deliver three, nor lets three 403s destroy the query. Measured: 「喜羊羊」 went from "0 pictures + an error" to "3 pictures in 8 s"; twelve 「刘亦菲」 pictures went from 17.5 s to 7.9 s.

  • A 403 gets one retry with the host's own Referer (1.8.8)

Hotlink protection asks "does this request look like it came from my own site", so no single Referer is correct for every host. Measured over 36 real candidates: the Huaban Referer fetched 32, each host's own Referer fetched 32, trying both fetched 33 — i1.hdslb.com only answers its own Referer (403 → 200, 1.5 MB), while gd-hbimg.huaban.com is the reverse (its own Referer gets blocked by Tencent EdgeOne with 567). A 403/401/567 now triggers one retry with the same-origin Referer; if that also fails the picture is skipped and the reason is stated in note.

  • Search no longer ships unaudited links (1.8.7): "4 of 35 loadable" describes the source's overall quality, not whether THIS picture will render. Every candidate is now downloaded and magic-byte checked before delivery; dead links (sinaimg 404 NoSuchKey) and 307s onto another image host (renwuji → static.xsite.com.cn) are dropped, and the note reports how many failed and why.
  • Body numbering continues across batches (1.8.7): display_image takes a new start argument. Pass "last number + 1" for every batch after the first, and the reply numbers 图1..图N instead of restarting at 图1.
  • Real HEIC never worked before (1.8.6): the only .heic sample on this machine was actually a JPEG (FFD8FF leading bytes — the extension lied), so "HEIC verified" was false; sharp's bundled libheif is built without an HEVC decoder, so it reads the container fine and throws the moment pixels are requested (Support for this compression format has not been built in: HEVC). On Windows the plugin now falls back to the system WIC decoder (-EncodedCommand, so no path ever crosses a command line); measured a genuine 1440x960 HEIC → 520x347 PNG in 532 ms. New test/probe-heic-real.mjs (24 checks) ships a real HEVC fixture and asserts the fixture itself is not a mislabelled JPEG.
  • Trust the bytes, not the `200` or the content-type (1.8.4) — one "successfully rescued"

cache file was actually a CDN error page (HTTP 200, content-type: image/jpeg, body starting `). It was served as image/jpeg`, the browser failed to decode it, and the result was "图片无法预览". Download, probe and serve now all verify the magic bytes (JPEG/PNG/GIF/WEBP/TIFF/BMP/ISOBMFF) and refuse anything that is not an image — a file that lies about itself gets a 415 with the reason instead of a silent broken picture.

  • Filenames containing `%` now resolve (1.8.4) — ?abs= was percent-decoded twice, so