# dsh-vision-fallback

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

为 [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) 提供**全局静默视觉增强**：你继续在会话顶部直接选择真实主模型（如 `deepseek-v4-flash`），聊天框里拖入、粘贴、引用的图片会自动交给固定视觉模型，转成事实性文字观察后作为隐藏上下文交给主模型。界面始终显示你的原始图片——不新增"视觉回退"模型分组、不切换模型、不调用额外工具。

兼容当前 DSH STORE 版本窗口：`0.1.2-alpha.3`、`0.1.2-alpha.4`、`0.1.2-alpha.5`。MCP / ACP 的持久图片附件，以及 PTC Mode 转发的嵌套图片，都会沿用同一套视觉桥处理。插件启用时会保留图片能力声明；否则聊天框会在图片到达视觉桥之前直接提示“模型不支持图片输入”。

## 为什么需要

- DeepSeek V4 Flash/Pro 等强编码模型是**纯文本**的：聊天框发图会直接报"模型不支持图片输入"。
- 已有的"视觉工具"类插件需要你把图存成文件再调 `see_image(path)`——笨重，而且主模型依然收不到聊天框附件。
- 本插件在请求层架桥：**聊天框发图就像原生支持一样**，无论你在模型选择器里选哪个主模型。

## 工作方式

```
拖图/粘贴 ──► 聊天附件（UI 始终显示原图）
        │
        ▼
agent/pre-step ──► 图片 + 当前问题 + 最近上下文
        │                    │
        │                    ▼
        │          固定视觉模型（OpenAI 兼容 /chat/completions）
        │                    │  事实性文字观察
        │                    ▼
        └──► model-only surface replacement ──► 主模型（纯文本）
```

1. 插件覆盖发送前的图片能力检查，让文本主模型也能接收带图消息。
2. `agent/pre-step` 检测本轮图片，把图片、最新用户问题和最近对话上下文发给配置的视觉模型。
3. 你的原始图片作为正常聊天附件保留在界面。
4. **仅模型可见的 surface replacement** 把图片换成视觉观察后再交给主模型。
5. 切换主模型（DeepSeek、Kimi、MiniMax……）不会改变固定视觉模型。

### MCP / ACP / PTC 图片

DSH `0.1.0-rc.7` 会把 MCP、ACP 和 PTC 产生的图片保存为持久附件，再以核心 `image` 内容块交给后续链路。本插件会递归处理普通消息、`tool-result` 和多层嵌套 `tool-result` 中的图片，并保持图片与文字的原始顺序：

- MCP / ACP 持久附件：按 `attachmentId` 读取，不依赖临时文件路径；
- PTC 嵌套图片：子调用返回的图片也会转换为视觉观察；
- 多张图片：逐张调用视觉模型，观察结果对应原始出现顺序；
- 原始界面：只替换模型视图，聊天界面仍保留原图。

插件的图片能力声明是必要的入口兼容层。它不是说主模型真的具备视觉推理能力，而是允许图片先进入 DSH，再由视觉桥转成主模型可用的文字上下文。

### DSH 兼容性证据

2026-09-03 使用 Node.js `24.16.0` 验证，每个版本均使用独立的一次性 `DSH_HOME`：

| DSH 版本 | 本地路径安装 | `--dump-config` | 带认证冷启动 | 卸载 |
| --- | --- | --- | --- | --- |
| `0.1.2-alpha.3` | 通过 | 通过 | HTTP 200 | 通过 |
| `0.1.2-alpha.4` | 通过 | 通过 | HTTP 200 | 通过 |
| `0.1.2-alpha.5` | 通过 | 通过 | HTTP 200 | 通过 |

Profile 操作使用官方 CLI：`plugin --profile web add -w <local-path>` 和 `remove -w dsh-vision-fallback`。新版 DSH 会深冻结 `llm/stream` 请求，插件不再原地修改它；上下文压缩会复制请求并执行一次带重入保护的嵌套分发。

## 安装

### npm / 本地检出

```sh
# npm 发布后（或本地检出目录）
dsh plugin --profile web add dsh-vision-fallback
# 或：dsh plugin --profile web add /path/to/dsh-vision-fallback
```

### 从源码

```sh
git clone https://github.com/1HelloMan1/dsh-vision-fallback.git
cd dsh-vision-fallback
pnpm install --config.minimumReleaseAge=0   # 预发布 peer 依赖可能需绕过发布年龄策略
pnpm test                                    # node --test test/*.test.mjs
dsh plugin --profile web add "$PWD"
```

然后验证并重启：

```sh
dsh --profile web --dump-config   # 应出现 "# == dsh-vision-fallback" 层
# 重启 `dsh web`（patch/bundle 层不支持热加载）
```

> 插件注册在 host 平面，headless / TUI 等其他 profile 同样兼容。

## 配置

两种方式都实时生效（保存后无需重启）：

### Web 设置页

打开 DSH Web **设置 → 视觉增强**，只暴露这些项：

| 字段 | 默认值 | 说明 |
|---|---|---|
| 启用 | `true` | 总开关 |
| 视觉模型 | `mimo-v2.5` | OpenAI 兼容 `model` |
| API 地址 | `https://opencode.ai/zen/go/v1` | 插件自动拼 `/chat/completions` |
| 凭据引用 | `OPENCODE_GO_API_KEY` | 从 DSH 凭证系统解析（不写入环境变量） |
| 输出上限 / 超时 / 图片上限 | `1536` / `60000` / `15MB` | 视觉请求限制；超过图片上限会失败 |
| 最近上下文 | `includeRecentContext: true`、`contextMessages: 6`、`contextMaxChars: 6000` | 附带多少最近对话给视觉模型 |
| 提示词 | （中文详细分析） | 分析指令；用户问题自动追加 |
| 结果标注 | `true` | 观察前加 `【视觉观察：<model>】` |

### settings.yaml

```yaml
vision-fallback:
  enabled: true
  model: mimo-v2.5
  baseURL: https://opencode.ai/zen/go/v1
  apiKeyRef: OPENCODE_GO_API_KEY
  maxTokens: 1536
  timeoutMs: 60000
  maxBytes: 15728640
  includeRecentContext: true
  contextMessages: 6
  contextMaxChars: 6000
  prompt: "请分析这张图片..."
  tagResult: true
```

API key 通过 DSH **凭证系统**（`~/.dsh/.credentials.yaml`）解析，`process.env[apiKeyRef]` 作为兜底——插件不会把 key 写入 shell 环境文件。

## 安全与隐私

- 配置路由仅限本机回环 + 同源校验，请求体限大小并做 schema 校验。
- 视觉模型**没有工具权限、没有 system prompt、没有执行权限**——只拿到图片、问题和最近文字上下文。
- 观察结果以 model-only surface replacement 交付，界面中的原始图片永不被改写。
- 图片读取走 DSH 附件服务（遵守沙箱与观察策略）；视觉请求携带官方 `attributionHeaders()`。

## 用量记录（usage.jsonl）

开启 `recordUsage` 后，每次真实视觉调用（成功或失败）追加一行 JSON 到
`<dshHome>/vision-fallback/usage.jsonl`（可在设置页改 `usageLogPath`），供 usage-dashboard 统计。
字段：

| 字段 | 含义 |
| --- | --- |
| `ts` | 调用发起时刻（毫秒时间戳） |
| `durationMs` | 本次调用响应耗时（毫秒） |
| `kind` | 固定 `"vision"` |
| `status` | `"ok"` 成功 / `"error"` 失败 |
| `model` | 视觉模型名 |
| `inputTokens` / `outputTokens` | 输入 / 输出 token 数 |
| `cacheReadTokens` | 命中缓存读取的 token 数 |
| `error` | 失败时的错误信息（仅失败条目） |
| `imageName` / `mediaType` / `imageBytes` | 图片文件名 / 媒体类型 / 字节数 |
| `imageIndex` / `imageTotal` | 该图在本次请求中的第几张 / 总张数 |

复用已记忆的观察结果（observations.json）不会产生新条目——该文件统计的是真实外部视觉调用次数。

### 观察缓存语义

观察结果按**图片在某次会话中的出现位置**（会话 id + 消息 id）记录：

- 同一图片在**同一消息位置**被重复处理（重启恢复、重放、压缩）→ 直接复用，不重新识别；
- 同一图片在**会话的新位置**再次出现（新轮次、主模型"再仔细看看"）→ **重新识别**，生成贴合当前上下文的新观察；
- 不同会话之间不共享观察结果。

缓存上限 256 条（LRU 淘汰），失败结果不入缓存。

### 关于图片压缩

当前版本的“压缩”指**上下文压缩时复用已经得到的视觉观察**，不会重新识图；插件暂时不会对图片文件做缩放、转码或质量压缩。图片超过 `maxBytes` 时会直接报告失败，不会静默改变原图。后续如增加真正的图片压缩，会单独记录压缩前后字节数和媒体类型。

## 默认视觉路由

- 模型：`mimo-v2.5` · 端点：`https://opencode.ai/zen/go/v1/chat/completions` · 凭据：`OPENCODE_GO_API_KEY`

任何 OpenAI 兼容视觉端点都可用（智谱 GLM-4V-Flash、SiliconFlow Qwen-VL、vLLM、Ollama……），在设置页改 `model`、`baseURL`、`apiKeyRef` 即可。

## 与 OpenCode 方案的关系

OpenCode 社区的 `opencode-see-image` 把 `filePath` 和针对当前任务的 `question` 交给固定视觉模型，再把文字结果返回当前主模型。DSH 比 OpenCode 多了一层发送前的图片能力校验，因此这里同时覆盖该校验，并用 DSH 官方的 `agent/pre-step` 与 model-only surface replacement 保持界面静默。

## 开发

```sh
pnpm test    # node --test test/*.test.mjs
```

目录结构：

```
dsh-vision-fallback/
├── package.json        # dsh.bundle + dsh.client 声明
├── cordis.patch.yml    # 插入 vision-fallback 行
├── lib/index.js        # host 插件（pre-step 桥、配置路由、控制器）
├── lib/client.js       # Web 设置页（"视觉增强"）
└── test/               # 单元测试
```

## 许可证

MIT
