Skip to content
dsh.fish
Bundle

dsh-plugin-show-image

Render local image files inline in the DSH conversation via a global show_image tool.

Source
justhalfbit
License
MIT
Updated
Updated 15 hours ago

Readme

# dsh-plugin-show-image

中文 | [English](README.en.md)

[DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) 的会话内图片渲染插件:
对话模型调用 `show_image` 工具即可在浏览器会话界面中**内联显示本地图片**。
图片字节通过按需 HTTP 路由传输,不进入会话日志或模型上下文——轻量、无膨胀。

## 特性

- 🖼 **内联图片渲染**:在对话流中直接显示本地图片,无需离开会话界面
- 🔍 **点击放大**:缩略图可点击,打开全屏 lightbox 浮层查看大图;点击任意位置或按 Escape 关闭
- 🌐 **HTTP 按需传输**:图片字节走专用 HTTP 路由(`GET /show-image?path=…`),不经 base64 编码、不进会话日志
- 📦 **零依赖零构建**:纯手写 JavaScript,无 npm 依赖、无构建步骤、无 TypeScript
- 🔒 **安全守卫**:8MB 文件大小上限、扩展名白名单(防原型链污染)、不支持的格式直接拒绝
- 🧹 **完全可逆**:所有副作用(工具注册、HTTP 路由、卡片渲染器)绑定在插件 fiber 上,卸载即清理

## 支持格式

`png` · `jpg` / `jpeg` · `gif` · `webp` · `svg` · `bmp` · `avif` · `ico`

## 安装

前置:已安装 [DSH](https://github.com/deepseek-ai/deepseek-harness) 且 `pnpm` 在 PATH 上。

```sh
# 从 GitHub 安装(零构建步骤,无需 allowBuilds 配置)
dsh plugin --profile web add github:justhalfbit/dsh-plugin-show-image

# 重启 dsh web 生效
```

`web` 是 `dsh web`(浏览器界面)对应的 profile 名。
`dsh plugin add` 会自动把包写入 profile 依赖并追加到 `dsh.profile.bundles`,无需手工编辑。

卸载:`dsh plugin --profile web remove dsh-plugin-show-image`,重启生效。

本地开发安装:克隆本仓库后 `dsh plugin --profile web add /绝对路径/dsh-plugin-show-image`。

> 兼容性:针对 DSH `0.1.1-rc.x` 开发;rc 阶段上游 API 可能变动。

### 界面支持

| 运行形态 | 工具 + 路由(Host 半) | 卡片渲染(Client 半) |
|---|---|---|
| `dsh web`(浏览器 GUI) | ✅ | ✅ |
| `tui` / `headless` | ❌ 依赖 `webServer`,无此服务时整行不激活 | ❌ |

这是有意为之:在无渲染面的终端里注册一个"假装能显示图片"的工具只会误导模型。

## 使用方式

安装并重启后,所有会话(任何 preset)都自动获得 `show_image` 工具。直接对 agent 说:

```
把 /path/to/image.png 显示出来
```

agent 会调用 `show_image` 工具,图片即内联渲染在会话卡片中。点击图片可全屏查看。

> **提示**:建议使用绝对路径。相对路径会按 DSH 进程的工作目录解析,而非会话工作目录。

## 工作方式

### 架构

插件由两个半体组成,通过 HTTP 路由松耦合:

```
┌─ Host 半 (lib/index.js) ──────────────────────────────┐
│                                                        │
│  show_image 工具                                        │
│    模型调用 → execute() → fs.readBytes()                │
│    → 返回 {path, mime, bytes} 元数据(不含图片字节)      │
│                                                        │
│  /show-image 路由                                       │
│    浏览器 GET → handler() → 按需返回图片字节              │
│                                                        │
└────────────────────────────────────────────────────────┘
         ↕ HTTP (图片字节按需传输)
┌─ Client 半 (lib/client.js) ────────────────────────────┐
│                                                        │
│  ImageCard 组件                                         │
│    注册在 tool.call.toolview[key="show_image"]          │
│    <img src="/show-image?path=…"> 内联渲染               │
│    点击 → DOM lightbox 浮层(挂载到 document.body)       │
│                                                        │
└────────────────────────────────────────────────────────┘
```

### 一次调用的完整链路

1. **模型决策**:agent 循环把 `show_image` 工具 schema 编入请求,模型决定调用并返回 `{path}`
2. **工具执行**:Host 端 `execute()` 用 `fs` 服务读取文件元数据,返回 `{path, mime, bytes}` — 图片字节**不进日志**
3. **卡片渲染**:会话事件同步到浏览器,React 渲染 `ImageCard`,输出 `<img src="/show-image?path=…">`
4. **按需取图**:浏览器 `<img>` 标签自动发起 HTTP GET,命中 `/show-image` 路由,Handler 返回图片字节

三个调用方(agent 循环、React 渲染器、HTTP 服务器)互不感知,靠会话日志和 URL 松耦合衔接。

### Lightbox 实现

点击缩略图后,`useEffect` 直接在 `document.body` 上创建 DOM 浮层(而非 React 树内渲染),
避免任何祖先 CSS(`transform` / `filter` / `overflow`)对 `position: fixed` 的裁剪。
同时锁定页面滚动(`overflow: hidden`),Escape 键关闭,组件卸载时自动清理所有副作用。

## 文件结构

```
dsh-plugin-show-image/
├── package.json          # 包声明:dsh.bundle.patch + dsh.client + exports
├── cordis.patch.yml      # 组合补丁:一行 insert,把本包挂进 host 组合
├── lib/
│   ├── index.js          # Host 半:show_image 工具 + /show-image HTTP 路由
│   └── client.js         # Client 半:ImageCard 组件 + lightbox + slot 注册
├── LICENSE               # MIT
└── README.md
```

## 设计决策

**为什么图片走 HTTP 路由而不是 base64 RPC?**
base64 编码膨胀 33%,且整段数据必须通过 JSON 序列化/反序列化。HTTP 路由让浏览器直接获取原始字节,
体积更小、传输更快。

**为什么挂 host 平面?**
`show_image` 应该对所有会话可见(全局工具),而非仅限某个 preset。host 层的 `tools.register()` 注册为全局工具,
所有会话(不分 preset)都能看到。同时 `webServer` 服务仅在 host 层可用。

**为什么 `webServer` 缺失时整行不激活?**
`inject: ['tools', 'fs', 'webServer']` 声明了硬依赖。在 TUI/headless profile 中 `webServer` 不存在,
Cordis 会让该行进入 waiting 状态而永不激活——这比注册一个无法渲染的假工具更诚实。

**为什么 lightbox 用原生 DOM 而不是 React?**
React 渲染的元素受制于 DOM 树中祖先节点的 CSS 属性。`position: fixed` 会被祖先的 `transform`、`filter`
等属性降级为相对定位。直接挂载到 `document.body` 上的 DOM 元素不受此限制,保证 lightbox 始终全屏覆盖。

## 已知限制

- 仅适用于 `dsh web`(浏览器 GUI)profile,TUI / headless 环境下不可用
- 相对路径按 DSH 进程工作目录解析,而非会话工作目录(工具描述中已提示模型优先使用绝对路径)
- 本机任何进程可通过 GUI 端口读取本地图片文件,与 DSH GUI 既有的本地信任模型一致

## 开发

本插件零依赖零构建。修改源码后重启 `dsh web` 即可生效(本地链接安装不需要重新 `dsh plugin add`)。

## License

[MIT](LICENSE)

Install

dsh plugin --profile web add github:justhalfbit/dsh-plugin-show-image

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source