# DSH-dseyes 👁️ — Free eyes for DeepSeek Harness

[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![DSH](https://img.shields.io/badge/DeepSeek%20Harness-plugin-blueviolet)](https://github.com/deepseek-ai/deepseek-harness)
[![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](package.json)

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

**DSH-dseyes** gives text-only DeepSeek models a **native image-attachment experience** in [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness):

Paste or drop an image into the Web GUI composer, and it behaves like a normal AI chat:
1. The image shows up as a **thumbnail attachment** (kept in history, click to zoom);
2. Before the request reaches DeepSeek, the host reads the image with the **free Zhipu GLM-4V-Flash** vision model (`glm-4v-flash` → `glm-4.6v-flash` → `glm-4.1v-thinking-flash` automatic fallback chain);
3. DeepSeek receives the text description and answers based on the image content.

No extra commands, no manual conversion, no model changes.

> **How it works**: DSH's `session.prompt` image admission rejects images for text-only models (the DeepSeek adapter declares `inputModalities: ["text"]` → `MODEL_DOES_NOT_SUPPORT_IMAGES`). DSH-dseyes does two things on the host:
> 1. **Admits the image** — patches `llm.resolveModelInfo` so the routed model claims image input, letting the image into the session (thumbnail, history and zoom all work natively);
> 2. **Substitutes before dispatch** — patches `llm.streamWithRegistration` (the single choke point behind both `llm.stream` and `prepareCall().stream`), replacing every image content block with a GLM description. DeepSeek only ever receives text, while the session keeps the real image.

## Features

- 🖼️ **Native upload UX**: paste/drop image → thumbnail attachment → send → DeepSeek understands it.
- 🆓 **Free**: `glm-4v-flash` is a free Zhipu model — register and use.
- 🔒 **Key never reaches the browser**: the GLM key lives in env / secrets file, host-side only.
- 🔁 **Automatic fallback chain**: `glm-4.6v-flash` → `glm-4.1v-thinking-flash` on failure.
- 🗂️ **Images preserved**: originals stay in the session (thumbnail + click-to-zoom); descriptions feed the model.
- ⚡ **Result cache**: reuses the first description for repeated images across turns.
- 🩺 **Self-check**: `GET /dsh-dseyes/diag` reports key presence and Zhipu connectivity.

## Install

### Prerequisite: a free Zhipu GLM API key

1. Register/login at [open.bigmodel.cn](https://open.bigmodel.cn);
2. **API Keys** → create one (format `id.secret`; `glm-4v-flash` is free, no payment required).

Configure the key (either of these, priority top-down):

```sh
# Option 1: environment variable
set GLM_API_KEY=your-key          # current shell
setx GLM_API_KEY "your-key"       # permanent (Windows user env)

# Option 2: secrets file (shared with dsh-media-skills; takes effect immediately)
# Create %USERPROFILE%\.dsh\secrets\media-tools.env with one line:
GLM_API_KEY=your-key
```

> `ZHIPU_API_KEY` is treated as an alias of `GLM_API_KEY`; on Windows the plugin also reads `HKCU\Environment\GLM_API_KEY` live.

### Install the plugin

```sh
# Once published to npm:
dsh plugin --profile web add dsh-dseyes

# Or local development:
cd %USERPROFILE%\.dsh\profiles\web
pnpm add "file:D:\path\to\DSH-dseyes"
# then append "dsh-dseyes" to dsh.profile.bundles in package.json
```

Then **restart the DSH Web GUI** (fully quit and reopen).

## Usage

1. **Paste** (Ctrl+V) or **drop** an image into the chat input — a thumbnail rail appears above the input.
2. Type a question (optional) and press send.
3. DeepSeek understands the image and answers.

The original image stays in the history (click to zoom).

## Real-world example

Paste an anime illustration and ask "What's in this image?":

```
🖼️ [image attachment thumbnail] What's in this image? Answer briefly.

🤖 DeepSeek:
According to the image description, this is a pixel-art anime illustration of a
cute girl with blue twin tails, big eyes, eating a yellow food (possibly ice
cream or cake). She's centered in the frame, dressed in white and black with
small golden accents and a little whale pattern on her clothes.
```

> The image itself is kept as an attachment in the conversation history
> (click to zoom); DeepSeek answers from the host-side GLM-4V-Flash
> description — you never see the reading step.

## Troubleshooting

If reading an image fails (e.g. no key configured), the message DeepSeek
receives contains a `[图片内容无法识别：原因]` placeholder and the conversation
continues. To self-check key presence and Zhipu connectivity, open in your
browser:

```
http://127.0.0.1:<dsh-port>/dsh-dseyes/diag
```

Example: `{"ok":true,"diag":{"keyFound":true,"apiStatus":200,"apiOk":true}}`

## How it works

```
paste / drop image
      │  (Web GUI native: thumbnail attachment)
      ▼
session.prompt (with image part)
      │  ┌────────────────────────────────────────────┐
      │  │ DSH-dseyes (host half)                     │
      │  │ ① patch llm.resolveModelInfo              │
      │  │    → model claims image input → admission  │
      │  │    → image saved into session              │
      │  │ ② patch llm.streamWithRegistration        │
      │  │    → image blocks replaced with GLM text   │
      │  └────────────────────────────────────────────┘
      ▼
DeepSeek receives plain text → answers about the image
```

## Development

```sh
npm install        # or pnpm install (rollup dev dependency)
npm run build      # produces lib/index.js
npm run watch      # rebuild on change
```

## Configuration

| Variable | Description |
|---|---|
| `GLM_API_KEY` / `ZHIPU_API_KEY` | Zhipu key (required) |
| `~/.dsh/secrets/media-tools.env` | Optional secrets file, shared with dsh-media-skills |

## Privacy

- Images are sent only to Zhipu's official API (open.bigmodel.cn) for recognition.
- The key is never committed, never sent to the browser.
- Descriptions are used only in the model request; nothing is uploaded elsewhere.

## License

MIT
