Skip to content
dsh.fish
Skill

dsh-multimodal-skill

通过三方多模态 API 为纯文本 LLM 提供图像理解与文档理解能力(OCR、表格、公式、 版面解析、PDF/扫描件→Markdown、截图/照片/图表问答)。使用场景:用户粘贴或给出 图片/PDF 路径时解读内容、从文档提取文字与表格、对截图提问、发票/论文/票据解析。 触发词:图片理解、OCR、文档解析、截图内容、PDF 提取、表格识别、公式识别、票据识别、 扫描件、image、vision。实现为 Python 标准库 CLI(urllib/json/base64),零三方依赖。

Source
v587d
stars
1 stars
License
MIT
Updated
Updated yesterday

Readme

# dsh-multimodal-skill · 慧眼

<a id="top"></a>

**🌐 中文** · **[English README](#english)**

> **给纯文本 LLM 一双慧眼。** 一个 DeepSeek Harness(DSH)原生 skill + 零依赖 Python CLI,
> 为 **DeepSeek** 等纯文本模型补上**图像理解**与**文档解析**(OCR、表格、公式、PDF → Markdown),
> 使用免费额度优先的三方多模态 API,国内网络直连、无需代理。
>
> **核心特色 —— 🔄 内容寻址缓存**:相同文件 + 相同问题只调一次 API(sha256 指纹、磁盘落盘、
> 跨会话共享、TTL/LRU 自动淘汰),**避免重复请求、浪费免费额度**;再叠加零依赖
> (纯 Python 标准库)与自愈 provider 链(模型下线 / 限流 / 坏 key 自动切换),开箱即用。

[![许可证](https://img.shields.io/badge/%E8%AE%B8%E5%8F%AF%E8%AF%81-MIT-blue)](LICENSE)
[![Python](https://img.shields.io/badge/Python-3.8%2B-3776AB)](https://www.python.org/)
[![零依赖](https://img.shields.io/badge/%E9%9B%B6%E4%BE%9D%E8%B5%96-%E6%A0%87%E5%87%86%E5%BA%93-brightgreen)](scripts/mm_cli.py)
[![缓存](https://img.shields.io/badge/%E7%BC%93%E5%AD%98-%E5%86%85%E5%AE%B9%E5%AF%BB%E5%9D%80-28a745)](#缓存)
[![DSH](https://img.shields.io/badge/DSH-%E5%8E%9F%E7%94%9F%20skill-4B32C3)](https://github.com/deepseek-ai/dsh)

## 慧眼

《慧眼》,灵感源自 1993 年的流行歌曲《雾里看花》。
作者在使用 DeepSeek 的过程中,仿佛听到它发出这样的呼唤:“借我借我一双慧眼吧,让我把这纷扰看得清清楚楚……”。
如今有了“慧眼-skill”,我终于可以对 DeepSeek 说:“帮我把这张截图看清楚”——
它会自动调用 `mm_cli.py`,把 OCR/解析结果读回上下文,再回答你。

<!-- Demo (work in progress): -->
<p align="left">
  <img src="assets/demo.gif" alt="dsh-multimodal-skill demo" width="1000" />
</p>

## 为什么需要这个 skill?

**DeepSeek(以及许多编码模型)是纯文本的——它们看不见。** 当你把截图、PDF 或图表
交给 agent 时,模型无法理解。这个 skill 就是那座缺失的桥:

```
截图 / PDF / 图片
        │
        ▼
mm_cli.py(仅 Python 标准库 —— 无需 pip install、无需 venv)
        │
        ├─ 文档 → Markdown   (PaddleOCR-VL / MinerU / DeepSeek-OCR)
        └─ 图片 → 文本       (GLM-4V-Flash / Qwen3-VL / qwen-vl-max)
        │
        ▼
Markdown / 文本 → 回到 LLM 上下文
```

模型把提取出的 Markdown 当作自己的“视网膜”:忠实 OCR 与版面解析交给专用解析 API
(一个 0.9B 的文档 VLM 在忠实解析上胜过前沿通用模型 —— OmniDocBench 基准),
语义理解由文本模型自己完成。

## 特性亮点

- **零依赖** —— 只有一个 `mm_cli.py`,纯 Python 标准库(`urllib`/`json`/`base64`)。
  无需 `pip install`、无需 `requirements.txt`、无需 venv。
- **免费优先** —— 每个默认 provider 都有慷慨的免费额度:
  PaddleOCR 官方 API(每模型 3000 页/日)、MinerU(免 key)、智谱 GLM-4V-Flash(完全免费),
  另有 SiliconFlow 与 DashScope 兜底。
- **国内网络友好** —— 默认 providers 全部国内可达,无需代理
  (Gemini / Mistral / HuggingFace 国内不可达 —— 已剔除)。
- **自愈** —— 模型下线、限流(429)、队列满、key 失效全部自动处理:
  候选模型轮换、provider 链切换、退避重试、短期失败记忆。
  见 [`references/troubleshooting.md`](references/troubleshooting.md)。
- **格式感知路由** —— txt/md/csv/tsv 与本地 HTML 零模型成本本地解析
  (stdlib 编码探测、`csv` → Markdown 表格、`html.parser` → Markdown);
  docx/xlsx/pptx 自动路由到 MinerU 打头;PDF/图片走 PaddleOCR-first 链。
  见 [`references/formats.md`](references/formats.md)。
- **内容寻址缓存** —— `sha256(文件 + prompt + 模型)` 作 key,TTL(文档 30 天 / 图片 24 小时)、
  LRU 淘汰(2000 条 / 2GB)。省配额,让重试免费。
- **干净的输出纪律** —— 结果走 stdout、诊断走 stderr,退出码 0–6 供 agent 自动化。
- **面向 LLM 的提取元数据** —— 每次 `doc parse` 结果都带事实性
  `<!-- mm-meta: {...} -->` 头(format、mode、provider、model、pages、stats、over),
  消费方 LLM 清楚知道是谁解析的,可自行决定信任与复核。绝不捏造置信度。
- **输出护栏** —— 交给 LLM 的每个字符串都经过可配置上限检查
  (字节 / 行数 / 单行 / 估算 token,默认 64K token)。超限时不输出全文:
  写入 UTF-8 文件并返回路径,由 LLM 用自己的工具读取/grep。绝不静默截断。

## 文档格式路由

`doc parse` 自动探测文件类型(magic bytes 优先、扩展名兜底)并按表格路由。
手动 `--provider` 可绕过路由(仅调试)。

| 格式 | 探测 | 路由 / 链头 | 本地零模型解析 | 备注 |
|---|---|---|---|---|
| PDF (`.pdf`) | `%PDF` magic | **paddleocr** → mineru → siliconflow | 否 | PaddleOCR 擅长扫描件/版面;≤100 页/文件(PaddleOCR),≤20 页/10MB(MinerU flash) |
| 图片 PNG/JPEG/WebP/GIF/BMP | magic bytes | **paddleocr** → mineru → siliconflow | 否 | 可作为单页文档解析;问答走 `image ask` |
| TIFF (`.tif/.tiff`) | `II*\0` / `MM\0*` magic | **paddleocr** → mineru → siliconflow | 否 | 上游上报为 `image/tiff` |
| Word/Excel/PPT (`.docx/.xlsx/.pptx`) | PK zip + `[Content_Types].xml` | **mineru**(其他 provider 无此能力,自动过滤) | 否 | MinerU 官方支持 Office;flash ≤10MB/20 页;大文件转 PDF |
| 旧版 Office (`.doc/.xls/.ppt`) | OLE2 `D0CF11E0` magic | 无 provider 支持 → **明确报错**并提示转 docx/pdf | 否 | 避免在不受支持的格式上浪费配额 |
| 纯文本 `.txt/.md/.tsv/.log/.json/.yaml` 等 | 无 magic + 非二进制启发式 | **本地读取**(零配额、零延迟、数据不出本机) | ✅ | 编码自动探测:UTF-16(BOM/启发式)→ UTF-8 → GB18030 → Latin-1 |
| CSV/TSV | 文本 + 扩展名 | **本地** `csv` → Markdown 表格 | ✅ | `csv.Sniffer` 自动识别分隔符;处理带引号的逗号/换行 |
| 本地 HTML (`.html/.htm`) | 文本 + 扩展名 | **本地** `html.parser` → Markdown | ✅ | 标题/列表/表格/链接/图片/代码块;JS 渲染页面提取过短时回退 mineru |
| 未知 | 无 magic、无扩展名匹配 | **先文本探测**(整文件可打印比例)→ 仍失败才报错 | 探测 | 报错信息列出所有支持格式 |

每次结果都带 `<!-- mm-meta: {...} -->` 头(`--json` 时为 `meta` 字段):
`format`(分组)、`mode`(`local` = 本地确定性解析 / `model` = 厂商模型)、
`provider`/`model`(实际解析器)、`pages`(厂商上报时)、`stats`(字节/行/token 测量)
与 `over`(超限标记)。超限时 `paths.result` 指向导出的 UTF-8 文件
(`paths.source` 指向原始本地文件)。skill 只报告事实——
置信度判断与是否建议人工复核,留给消费方 LLM。

## 环境要求

| 要求 | 检查 |
|---|---|
| Python 3.8+ | `python3 --version` |
| DeepSeek Harness(DSH) | `dsh --version`(dsh web / dsh CLI 均可) |
| 至少一个 API key | 见[配置](#配置) |
| 网络 | 国内直连(默认 providers);无需代理 |

## 安装(DSH 原生 skill)

DSH 的 `dsh-skill-filesystem` 会自动扫描 `~/.dsh/skills/<name>/SKILL.md`
(另有项目级 `.dsh/skills`、`.agents/skills`、`customSkillDirs` 等根目录)。
**目录名必须与 frontmatter 的 `name` 完全一致**(kebab-case)。

### 方式一:软链接(推荐,单一事实来源)

```bash
mkdir -p ~/.dsh/skills
ln -sfn "$PWD" ~/.dsh/skills/dsh-multimodal-skill
```

### 方式二:git clone(更新走 git pull)

```bash
git clone https://github.com/v587d/dsh-multimodal-skill.git ~/.dsh/skills/dsh-multimodal-skill
```

### 方式三:项目级目录(仅当前项目可用)

把 `SKILL.md`(连同 `scripts/`、`references/`)放进项目的 `.dsh/skills/dsh-multimodal-skill/`。

装好后**新开/重启一个 DSH 会话**,skill 目录里就会出现 `dsh-multimodal-skill`;
加载时 DSH 会给出资源根目录(含 `scripts/`、`references/`、`assets/`),
下文 `<skill_dir>` 即该目录。

## 配置

### 1. API key(`.env` 或环境变量)

把 skill 目录下的 `.env.example` 复制为 `.env`(或直接导出环境变量)。
**建议至少配一个文档 provider + 一个图片 provider。** CLI 读取优先级:
**系统环境变量 > `.env`**。

| 变量 | 服务 | 用途 | 获取地址 |
|---|---|---|---|
| `PADDLEOCR_ACCESS_TOKEN` | PaddleOCR 官方 API | 文档解析(默认) | https://aistudio.baidu.com/index/accessToken |
| `ZHIPUAI_API_KEY` | 智谱 GLM | 图片理解(默认) | https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys |
| `MINERU_API_TOKEN` | MinerU 精准 API | 文档解析(可选;flash 模式免 key) | https://mineru.net/apiManage |
| `SILICONFLOW_API_KEY` | SiliconFlow | 图片 + 文档兜底 | https://cloud.siliconflow.cn/account/ak |
| `DASHSCOPE_API_KEY` | 阿里云百炼 DashScope | 图片 + 文档(可选) | https://bailian.console.aliyun.com/ |

### 2. 可选:`config.json`(付费模型 / 自定义链)

```bash
python3 <skill_dir>/scripts/mm_cli.py config open   # 创建并打开 ~/.config/multimodal-skill/config.json
```

除 provider 链与模型候选外,`limits` 段为交给 LLM 的每个字符串把关(默认值均可配置):

| 检查项 | 键 | 默认 | 用途 |
|---|---|---|---|
| 总字节 | `max_bytes` | 5 MB | 整体大小上限 |
| 总行数 | `max_lines` | 20000 | 防止超大行数 |
| 单行字节 | `max_line_bytes` | 4096 | 防止压缩/Base64 大块 |
| 估算 token | `max_tokens` | **64K** | 上下文预算(CJK 加权估算器,小翻译表精确计数 —— 实测约 96ms @ 5MB) |
| 输入硬上限 | `hard_max_bytes` | 20 MB | 读文件前的 `stat` 预检 |

超限时不输出全文:CLI 写入 UTF-8 文件并返回路径(`meta.paths.result`;
本地文本输入还有 `meta.paths.source`),消费方 LLM 可用自己的工具读取/grep 片段。
`cache clear` 同时清理导出目录。

把付费/指定模型放在管线模型列表最前面即可优先使用
(如 `"image_models": ["glm-4.5v", "glm-4v-flash"]` —— 付费在前、免费兜底)。
完整模板见 [`config.example.json`](config.example.json)(JSONC 允许注释)。
快速查看:`mm_cli.py providers`。

> 注:provider 列表在代码中固定(每个 provider 协议不同);
> 配置只能覆盖模型候选与默认链 —— 不能新增 provider。

## 快速开始

```bash
CLI=python3 <skill_dir>/scripts/mm_cli.py

# 0. 健康检查 —— 连通性、key、模型
$CLI doctor

# 1. 把截图粘贴到 DSH 对话 → 出现本地路径 → 提问
$CLI image ask /path/to/screenshot.png "这个报错是什么?"

# 2. 解析文档(PDF/扫描件/图片)为 Markdown
$CLI doc parse report.pdf --pages 1-20 --out report.md

# 3. 本地文本/CSV/HTML 本地解析 —— 零配额、零延迟
$CLI doc parse notes.csv          # → Markdown 表格
$CLI doc parse page.html          # → html.parser 转 Markdown

# 4. 零配置兜底(MinerU flash,免 key)
$CLI doc parse scan.pdf --provider mineru

# 5. 超限输出落盘;CLI 返回路径,由 LLM 自行 grep
$CLI doc parse huge.log           # → 输出超限,返回落盘路径,由 LLM 自行 grep
```

完整命令参考:[`references/api.md`](references/api.md)。

## Providers 与免费额度(2026-08 实测)

| Provider | 管线 | 免费额度 | 国内可达 |
|---|---|---|---|
| PaddleOCR 官方 API | 文档 | **每模型 3000 页/日**(异步任务 API) | ✅ |
| MinerU | 文档 | flash:免费、免 key、≤20 页/10MB;精准:1000 页/日高优 | ✅ |
| SiliconFlow DeepSeek-OCR | 文档 | 免费额度实测零扣费 | ✅ |
| 智谱 GLM-4V-Flash | 图片 | 完全免费(GLM-4.6V-Flash 免费,高峰繁忙) | ✅ |
| SiliconFlow Qwen3-VL | 图片 | 按量计费(便宜) | ✅ |
| DashScope qwen-vl-max/ocr | 图片 + 文档 | 每模型约 100 万 token 免费(90 天) | ✅ |

## 安全

- key 存放在 `.env`(权限 600)或环境变量 —— 绝不硬编码;`.env` 与 `config.json` 已 gitignore。
- **免费档可能用提交的数据训练**(各 provider 通用政策)—— 不要把机密文档/截图发给免费档;
  敏感材料请用付费模型(经 `config.json`)。
- CLI 绝不自动执行 API 响应中的任何内容;输出为 stdout 上的纯文本/Markdown。
- 报错信息可能回显 provider 响应 —— 不要把你发给解析 API 的文件里塞入机密。

## 缓存

缓存**纯磁盘、文件化** —— 无内存状态、无守护进程、无加载/退出周期。
每次 CLI 调用都是全新进程,直接读写磁盘条目(写穿):

- **key** = 内容寻址 `sha256(文件字节 + 管线 + provider + 模型 + prompt + 参数)`;文件名即 key。
- **TTL** 读取时惰性检查:文档 30 天、图片 24 小时(`--ttl` / `--no-cache` 覆盖)。
- **LRU** 写入时目录扫描淘汰:2000 条 / 2GB 上限,最早访问的先丢。

简言之:一个 *缓存风味的文件算子* —— 崩溃安全、跨会话共享
(一个会话里解析过的,下一个会话直接省配额)。

## 工作原理

两条管线(行业最佳实践:忠实解析与开放视觉分开):

- **`doc parse`** —— 专用文档解析器 → Markdown(表格、LaTeX 公式、阅读顺序)。
  PaddleOCR-VL-1.6 属 OmniDocBench SOTA 级别。
- **`image ask`** —— OpenAI 兼容 VLM 对话,面向截图/照片/图表。

每次调用都走自适应链:按序尝试模型候选 → 模型下线/限流/鉴权失败时轮换或切换 provider →
聚合错误并给出有意义的退出码(2 用法 / 3 鉴权 / 4 限流 / 5 模型 / 6 网络)。

## 故障排查

常见问题(PaddleOCR 排队慢、GLM 429、MinerU 上传签名、中文输出乱码、缓存怪癖):
[`references/troubleshooting.md`](references/troubleshooting.md)。

## License

[MIT](LICENSE)

---

# English

> **[中文版(默认)](#top)** · English

> **Give text-only LLMs eyes.** A DeepSeek Harness (DSH) native skill + a
> zero-dependency Python CLI that adds **image understanding** and **document
> parsing** (OCR, tables, formulas, PDF → Markdown) to any text-only model such
> as **DeepSeek**, using free-tier-first third-party multimodal APIs. All
> default providers are reachable from mainland China without a proxy.
>
> **Key feature — 🔄 content-addressed caching**: the same file + the same
> prompt hits the API only once (sha256 fingerprint, disk-backed, shared
> across sessions, auto-evicted via TTL/LRU) — **no repeated requests, no
> wasted free quota** — on top of zero dependencies (pure Python stdlib) and a
> self-healing provider chain (auto-rotation on model deprecation / rate
> limits / bad keys).

## Why this skill?

**DeepSeek (and many coding models) are text-only — they cannot see.** When you
paste a screenshot, a PDF, or a chart into your agent, the model has no way to
understand it. This skill is that missing bridge:

```
screenshot / PDF / image
        │
        ▼
mm_cli.py (Python stdlib only — no pip install, no venv)
        │
        ├─ document → Markdown   (PaddleOCR-VL / MinerU / DeepSeek-OCR)
        └─ image    → text       (GLM-4V-Flash / Qwen3-VL / qwen-vl-max)
        │
        ▼
Markdown/text → back into the LLM's context
```

The model reads the extracted Markdown as its "retina": faithful OCR and layout
parsing are delegated to specialist APIs (a 0.9B document VLM beats frontier
models on faithful parsing — OmniDocBench), while the text model handles
semantics.

## Highlights

- **Zero dependencies** — one `mm_cli.py`, pure Python standard library
  (`urllib`/`json`/`base64`). No `pip install`, no `requirements.txt`, no venv.
- **Free-first** — every default provider has a generous free tier:
  PaddleOCR official API (3,000 pages/day/model), MinerU (free, no key),
  Zhipu GLM-4V-Flash (fully free), plus SiliconFlow & DashScope fallbacks.
- **China-network friendly** — all default providers are mainland-reachable;
  no proxy required. (Gemini/Mistral/HuggingFace are blocked from CN — dropped.)
- **Self-healing** — model deprecation, rate limits (429), queue-full, and bad
  keys are handled automatically: candidate-model rotation, provider fallback
  chains, backoff, and a short-lived failure memory. See
  [`references/troubleshooting.md`](references/troubleshooting.md).
- **Format-aware routing** — txt/md/csv/tsv and local HTML are parsed locally
  with zero model cost (stdlib encoding detection, `csv` → Markdown tables,
  `html.parser` → Markdown); docx/xlsx/pptx auto-route to MinerU as chain head;
  PDF/images keep the PaddleOCR-first chain. See
  [`references/formats.md`](references/formats.md).
- **Content-addressed caching** — `sha256(file + prompt + model)` keys, TTL
  (30d documents / 24h images), LRU eviction (2,000 entries / 2 GB). Saves
  quota, makes retries free.
- **Clean output discipline** — results to stdout, diagnostics to stderr,
  exit codes 0–6 for agent automation.
- **LLM-facing extraction metadata** — every `doc parse` result carries a
  factual `<!-- mm-meta: {...} -->` header (format, mode, provider, model,
  pages, stats, over) so the consuming LLM knows exactly who parsed the file
  and can decide trust/verification itself. Never fabricated confidence scores.
- **Output guardrails** — every string handed to the LLM is checked against
  configurable limits (bytes / lines / single-line / estimated tokens, default
  64K tokens). On over-limit the full text is **not** emitted: it is written to
  a UTF-8 file and the path is returned, so the LLM reads/greps snippets with
  its own harness tools. Never silently truncated.

## Document format routing

`doc parse` auto-detects the file type (magic bytes first, extension
fallback) and routes accordingly. Manual `--provider` bypasses routing
(debug only).

| Format | Detection | Route / chain head | Local zero-model parse | Notes |
|---|---|---|---|---|
| PDF (`.pdf`) | `%PDF` magic | **paddleocr** → mineru → siliconflow | No | PaddleOCR excels at scans/layout; ≤100 pages/file (PaddleOCR), ≤20 pages/10MB (MinerU flash) |
| Images PNG/JPEG/WebP/GIF/BMP | magic bytes | **paddleocr** → mineru → siliconflow | No | Parsable as single-page docs; Q&A via `image ask` |
| TIFF (`.tif/.tiff`) | `II*\0` / `MM\0*` magic | **paddleocr** → mineru → siliconflow | No | Reported upstream as `image/tiff` |
| Word/Excel/PPT (`.docx/.xlsx/.pptx`) | PK zip + `[Content_Types].xml` | **mineru** (other providers lack this capability and are filtered out) | No | MinerU officially supports Office; flash ≤10MB/20 pages; convert large files to PDF |
| Legacy Office (`.doc/.xls/.ppt`) | OLE2 `D0CF11E0` magic | No provider support → **clear error** with convert-to-docx/pdf hint | No | Avoid wasting quota on unsupported formats |
| Plain text `.txt/.md/.tsv/.log/.json/.yaml` etc. | no magic + non-binary heuristic | **local read** (zero quota, zero latency, data never leaves the machine) | ✅ | Encoding auto-detection: UTF-16 (BOM/heuristic) → UTF-8 → GB18030 → Latin-1 |
| CSV/TSV | text + extension | **local** `csv` → Markdown table | ✅ | `csv.Sniffer` auto-detects delimiter; handles quoted commas/newlines |
| Local HTML (`.html/.htm`) | text + extension | **local** `html.parser` → Markdown | ✅ | Headings/lists/tables/links/images/code blocks; falls back to mineru when extraction is too short (JS-rendered pages) |
| Unknown | no magic, no extension match | **text probe first** (printable ratio over whole file) → error only if that fails | probe | Error message lists all supported formats |

Every result carries a `<!-- mm-meta: {...} -->` header (`meta` field with
`--json`): `format` (group), `mode` (`local` = deterministic local parse /
`model` = vendor model), `provider`/`model` (actual parser), `pages` (when
the vendor reports it), `stats` (bytes/lines/tokens measurements) and `over`
(limit flags). When over-limit, `paths.result` points to the exported UTF-8
file (and `paths.source` to the original local file). The skill reports
facts only — confidence judgment and whether to suggest manual verification
are left to the consuming LLM.

## Requirements

| Requirement | Check |
|---|---|
| Python 3.8+ | `python3 --version` |
| DeepSeek Harness (DSH) | `dsh --version` (dsh web or dsh CLI) |
| At least one API key | see [Configuration](#configuration) |
| Network | mainland China OK (defaults); no proxy needed |

## Install (DSH native skill)

DSH's `dsh-skill-filesystem` automatically scans `~/.dsh/skills/<name>/SKILL.md`
(plus project-level `.dsh/skills`, `.agents/skills`, `customSkillDirs`, etc.).
**The directory name must exactly match the `name` in the frontmatter**
(kebab-case).

### Option 1: symlink (recommended — single source of truth)

```bash
mkdir -p ~/.dsh/skills
ln -sfn "$PWD" ~/.dsh/skills/dsh-multimodal-skill
```

### Option 2: git clone (updates via git pull)

```bash
git clone https://github.com/v587d/dsh-multimodal-skill.git ~/.dsh/skills/dsh-multimodal-skill
```

### Option 3: project-level directory (this project only)

Put `SKILL.md` (with `scripts/`, `references/`) into your project's
`.dsh/skills/dsh-multimodal-skill/`.

After installing, **start/restart a DSH session** and `dsh-multimodal-skill`
will appear in the skill catalog. When loaded, DSH provides the resource base
directory (containing `scripts/`, `references/`, `assets/`) — `<skill_dir>`
below refers to that directory.

## Configuration

### 1. API keys (`.env` or environment variables)

Copy `.env.example` to `.env` in the skill directory (or export the variables).
**At least one document provider + one image provider is recommended.** The CLI
reads keys with priority: **system environment > `.env`**.

| Variable | Service | Used for | Where to get it |
|---|---|---|---|
| `PADDLEOCR_ACCESS_TOKEN` | PaddleOCR official API | document parsing (default) | https://aistudio.baidu.com/index/accessToken |
| `ZHIPUAI_API_KEY` | Zhipu GLM | image understanding (default) | https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys |
| `MINERU_API_TOKEN` | MinerU precision API | document parsing (optional; flash mode needs no key) | https://mineru.net/apiManage |
| `SILICONFLOW_API_KEY` | SiliconFlow | image + document fallback | https://cloud.siliconflow.cn/account/ak |
| `DASHSCOPE_API_KEY` | Alibaba DashScope | image + document (optional) | https://bailian.console.aliyun.com/ |

### 2. Optional: `config.json` (paid models / custom chains)

```bash
python3 <skill_dir>/scripts/mm_cli.py config open   # creates & opens ~/.config/multimodal-skill/config.json
```

Beyond provider chains and model candidates, the `limits` section guards every
string handed to the LLM (defaults; all configurable):

| Check | Key | Default | Purpose |
|---|---|---|---|
| Total bytes | `max_bytes` | 5 MB | overall size cap |
| Total lines | `max_lines` | 20000 | guards huge line counts |
| Single-line bytes | `max_line_bytes` | 4096 | guards minified/base64 blobs |
| Estimated tokens | `max_tokens` | **64K** | context budget (CJK-weighted estimator, exact CJK count via small translate table — benchmarked ~96ms @ 5MB) |
| Input hard cap | `hard_max_bytes` | 20 MB | `stat` pre-check before reading the file |

On over-limit the full text is **not** emitted: the CLI writes it to a UTF-8
file and returns the path (`meta.paths.result`; plus `meta.paths.source` for
local text inputs), so the consuming LLM can read/grep snippets with its own
tools. `cache clear` also cleans the export directory.

Put paid/any model IDs first in a pipeline's model list to prefer them
(e.g. `"image_models": ["glm-4.5v", "glm-4v-flash"]` — paid first, free
fallback). See [`config.example.json`](config.example.json) for the full
template (JSONC comments allowed). Quick look: `mm_cli.py providers`.

> Note: the provider list is fixed in code (each provider speaks a different
> protocol); configuration can only override model candidates and default
> chains — it cannot add providers.

## Quick start

```bash
CLI=python3 <skill_dir>/scripts/mm_cli.py

# 0. Health check — connectivity, keys, models
$CLI doctor

# 1. Paste a screenshot into the DSH chat → you get a local path → ask about it
$CLI image ask /path/to/screenshot.png "What error is shown here?"

# 2. Parse a document (PDF/scanned/image) into Markdown
$CLI doc parse report.pdf --pages 1-20 --out report.md

# 3. Local text/CSV/HTML is parsed locally — zero quota, zero latency
$CLI doc parse notes.csv          # → Markdown table
$CLI doc parse page.html          # → Markdown via html.parser

# 4. Zero-config fallback (MinerU flash, no key needed)
$CLI doc parse scan.pdf --provider mineru

# 5. Over-limit output is exported to disk; the CLI returns the path
$CLI doc parse huge.log           # → 输出超限,返回落盘路径,由 LLM 自行 grep
```

Full command reference: [`references/api.md`](references/api.md).

## Providers & free tiers (verified 2026-08)

| Provider | Pipeline | Free tier | Reachable from CN |
|---|---|---|---|
| PaddleOCR official API | document | **3,000 pages/day/model** (async job API) | ✅ |
| MinerU | document | flash: free, no key, ≤20 pages/10MB; precision: 1,000 pages/day high-priority | ✅ |
| SiliconFlow DeepSeek-OCR | document | measured zero-cost on free credit | ✅ |
| Zhipu GLM-4V-Flash | image | fully free (GLM-4.6V-Flash free, busy at peak) | ✅ |
| SiliconFlow Qwen3-VL | image | pay-per-use (cheap) | ✅ |
| DashScope qwen-vl-max/ocr | image + document | ~1M tokens/model free (90 days) | ✅ |

## Security

- Keys live in `.env` (permission 600) or environment variables — never
  hardcoded; `.env` and `config.json` are gitignored.
- **Free tiers may train on submitted data** (a common policy across all
  providers) — do not send confidential documents/screenshots to free tiers.
  Use paid models (via `config.json`) for sensitive material.
- The CLI never auto-executes anything from API responses; outputs are
  plain text/Markdown on stdout.
- Error messages may echo provider responses — don't paste secrets into files
  you send to parsing APIs.

## Caching

Cache is **disk-only, file-backed** — no memory state, no daemon, no
load-on-start / flush-on-exit cycle. Every CLI invocation is a fresh process
that reads and writes entries directly on disk (write-through):

- **Key** = content-addressed `sha256(file bytes + pipeline + provider + model + prompt + params)`; file name *is* the key.
- **TTL** checked lazily on read: 30 days for documents, 24 h for images (`--ttl` / `--no-cache` to override).
- **LRU** eviction by directory scan on write: 2,000 entries / 2 GB cap, oldest-accessed dropped first.

In short: a *cache-flavored file operator* — crash-safe, survives restarts,
and shared across sessions (a parse cached in one session is a quota saved
in the next).

## How it works

Two pipelines (industry best practice: keep faithful parsing separate from
open-ended vision):

- **`doc parse`** — specialist document parsers → Markdown (tables, LaTeX
  formulas, reading order). PaddleOCR-VL-1.6 is OmniDocBench SOTA class.
- **`image ask`** — OpenAI-compatible VLM chat for screenshots/photos/charts.

Every call runs through an adaptive chain: try model candidates in order →
on model-deprecation/rate-limit/auth failure, rotate or switch provider →
aggregate error with a meaningful exit code (2 usage / 3 auth / 4 rate /
5 model / 6 network).

## Troubleshooting

Common issues (PaddleOCR slow queue, GLM 429s, MinerU upload signature,
Chinese output garbling, cache quirks): [`references/troubleshooting.md`](references/troubleshooting.md).

## License

[MIT](LICENSE)

Install

# Skills are files: copy them into $DSH_HOME/skills/dsh-multimodal-skill (defaults to ~/.dsh/skills/dsh-multimodal-skill)

Profile: web

Source