dsh-ocr-local
Local OCR fallback for text-only routes: when the session model declares it cannot accept images, the attached image is cached locally and its path injected so the model can call ocr_image — PP-OCRv5 + ONNX Runtime on CPU, no API key, images never leave the machine. Silent when the model can see ima
Install
dsh plugin --profile web add dsh-ocr-local
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✗ |
| 2 list | — |
| 3 mount | — |
| 4 boot | — |
dsh: initialized profile web at /home/node/.dsh/profiles/webFour stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
dsh-ocr-local
[English](README.en.md) · [中文](README.md)
 
给 DeepSeek Harness(Web 端)装一个本地 OCR 兜底:当会话路由到的模型不支持图片输入时,
把图片里的文字读出来给模型;模型能看图时,插件完全静默、不插手。
识别引擎是 PP-OCRv5 + ONNX Runtime,纯 CPU、完全离线,图片不会离开你的电脑。
插件什么时候生效,什么时候静默
这是理解本插件最重要的一张表。判定基于当前会话实际路由到的模型声明的输入能力
(inputModalities),而不是猜:
会话路由到的模型 · 插件行为 · 模型实际拿到什么
明确声明不支持图片(如纯文本模型) · ✅ 生效:图片存到本地缓存,并注入路径提示 → 模型调 ocr_image 识别 · 文字。Harness 对这类模型只给一句 [image omitted because this model accepts text only; …],没有任何路径,所以没有本插件模型就完全读不到图
明确声明支持图片(多模态模型) · 🔇 静默:不存缓存、不注入提示 · 图片本身。Harness 还会在图片前附一条只读副本路径,模型想逐字核对时可以直接对那个路径调 ocr_image
无法确定(provider 未注册 / 查询失败 / 没声明模态) · 🔇 静默 · 按 Harness 原样处理。宁可不打扰,也不在一个可能能看图的模型上多塞提示
"无法确定就静默"是刻意的。如果你确实需要无条件介入,把 autoOcr 设成 'always'(见下方配置)。
关于「未登记的 model id」:DeepSeek 适配器对没写进模型目录的 id 会显式返回
inputModalities: ["text"],所以这类纯文本路由会被上表第一行正确覆盖。
其它 provider 若对未声明模型返回「无模态信息」,则落到第三行(静默)。
⚠️ 在 Web 设置页配置的模型,能力声明可能是空的
插件的判定完全依赖配置里声明的 inputModalities,而不是模型的真实能力。
而 Web 设置页不提供这个字段——这是最容易踩空的一处,单独说清楚。
声明写在 ~/.dsh/settings.yaml 的 llm-deepseek.models[] 里:
llm-deepseek:
models:
- id: deepseek-v4-pro
name: DeepSeek-V4-Pro
contextWindow: 1000000
inputModalities: # ← 决定插件介入还是静默的就是这一项
- text
Web 模型编辑器只暴露 4 个字段:id / name / contextWindow / maxTokens。
inputModalities 不在其中,所以在页面上新增模型时,写回的条目里没有这一项,
而配置 schema 会给它兜底成 ['text']。
在 Web 页面上做的事 · 对该模型能力声明的影响
新增一个模型 · 条目里没有 inputModalities → 按纯文本处理
编辑已有模型的 name / 窗口 / tokens · 原有声明原样保留(页面的 patch 是「展开合并」语义,不会删掉它不认识的字段)
兜底成纯文本会怎样:
- 该模型确实不支持图片 → 正好是插件该介入的场景,不用管。
- 该模型其实支持图片(自建 vision 端点,或想把某个多模态模型接进来)→ 插件会多此一举地
介入,图片被 Harness 投影成 [image omitted …] 占位符,本来能看图却看不到。
这时必须手工补声明:
inputModalities:
- text
- image
改完不用重启:settings.yaml 是热加载的(文件监听 + 去抖),保存即生效。
声明要满足的约束(写错会在加载时直接报错,不会静默忽略):
- 必须是非空数组,元素只能是
text/image,且不能重复。 - 声明了
image才能写imagePixelBudget(像素预算,可填low或正整数)与imageMaxBytes
(单图字节上限)。纯文本模型写了这两个字段会直接报错:
text-only catalog model "xxx" cannot declare image request limits。
非 DeepSeek 的 provider 卡片有自己的字段集合,能力声明的字段名以该 adapter 的配置 schema 为准。
顺带说清一个相关语义:settings.yaml里的models是整体覆盖内置目录的,
而模型选择器显示的正是这个数组(适配器的 listModels 直接映射它)。
所以在 Web 页面上动过模型列表之后,选择器里有哪些模型完全以这个数组为准——
把 deepseek-v4-flash-vision-exp 删掉了,就一个多模态模型都切不过去了
(本机默认是保留的)。
为什么插件要动模型的"能力声明"
Harness 在你按下发送的那一刻(prompt 准入阶段)就会读模型声明的图片能力:只要声明里
没有 image,它会在消息进入会话之前直接拒绝,并提示
「当前模型不支持图片,请切换支持图片的模型」。图片进不了会话,插件也就永远收不到
user/message 事件——本地 OCR 兜底在这条路径上就成了一行死代码。
所以插件会在这一个方法(llm.resolveModelInfo)上给纯文本模型补上 image 声明,
把准入放行。这不会让图片真的发给纯文本模型:请求由适配器按自己的模型目录构造,
图片在送上 wire 之前仍会被 Harness 投影成
[image omitted because this model accepts text only; attachment sha256:…] 文本。
换句话说,插件扩的是「准入」,不是「能力」。
不想要这个行为,把 autoOcr 设为 false——那时插件连这层包装都不会装。
与视觉模型的关系
两条链路互不干扰,因为它们在不同的层做事:
- 插件只管本地 OCR:把图存到本地、提示模型去调
ocr_image。 - 图要不要发给模型由模型能力 / 客户端配置决定,插件不干预。
所以多模态模型下你不需要为本插件做任何事:它会自己退场。想手动让它读图里的字,
随时可以直接对 agent 说「用 ocr_image 读这张图」。
快速开始
第 1 步:安装插件
DSH 的 profile 互相隔离,插件要装到你要用的那个 profile(Web 端通常叫 web)。
先确认你能调起 dsh CLI——它有几个等价入口,按你手上有什么选一个(下面的命令都以此为准):
你的情况 · 怎么调
PATH 里有 dsh 命令 · dsh plugin …
在 harness 源码仓库里(pnpm dsh 是仓库自带脚本) · cd <仓库> && pnpm dsh plugin …
同上,想走构建产物、不起 tsx · cd <仓库> && node apps/cli/lib/bin.js plugin …
直接用 npm 上发布的 CLI 包 · npx -y @deepseek-ai/dsh@alpha plugin …
⚠️ 两点容易卡住:
1.npx -y @deepseek-ai/dsh不带版本标签可能调不起来——npm 上这个包的latest停在预发布
的0.1.5-rc.1(而源码仓库已经是0.1.6-alpha.1)。要么显式写@alpha,要么直接用源码
仓库的入口;版本最好与你的 profile 保持一致。
2.dsh plugin内部靠pnpm转发,PATH 里必须有 pnpm(缺了会报pnpm not found on PATH)。
装 npm 上的稳定版:
dsh plugin --profile web add dsh-ocr-local
也可以直接从 GitHub 安装(想跟最新提交、或 npm 不可达时):
dsh plugin --profile web add github:grelvan/dsh-ocr-local
或本地克隆后安装:
git clone https://github.com/grelvan/dsh-ocr-local.git dsh plugin --profile web add ./dsh-ocr-local
改代码做开发时,用link:装软链更快——dsh plugin --profile web add link:./dsh-ocr-local,
此后改仓库里的文件即刻生效,不用重装。
装完重启 dsh,插件才会生效。
第 2 步:准备识别引擎(只需一次)
在自己终端里跑这一条(把 web 换成你的 profile 名;直接指向 clone 下来的仓库也行):
python3 ~/.dsh/profiles/web/node_modules/dsh-ocr-local/ocr/setup.py
它会自动完成三件事。识别模型是随插件包一起发布的,不用下载 —— 所以实际只需要装
Python 依赖(onnxruntime / numpy / opencv,约 85MB,走国内镜像通常 1-3 分钟):
[setup] dsh-ocr-local —— 本地 OCR 环境准备
共 3 步,首次约 2-6 分钟(大头是下载);每步都会打进度,不用干等。
[setup] 1/3 准备 Python 虚拟环境 ...
[setup] 2/3 安装依赖(约 85MB)...
Collecting onnxruntime ...
[setup] 3/3 下载识别模型(约 21MB,已下过的会跳过)...
[1/3] PP-OCRv5_mobile_det.onnx
47% (2MB/4MB) [直连]
装完会打印自检命令。之后每次识别都在本地秒级完成,不再需要网络。
网络不好怎么办(脚本会自救,剩下的靠这几个开关):
情况 · 怎么办
GitHub 拉得动但很慢 · 会按速度换源:持续低于 200KB/s 就换下一个,日志里会写明「源 X 太慢(80KB/s),换下一个」
GitHub 完全拉不动 · 会自动回退:直连 → ghproxy → gh-proxy → ghfast。想钉死某一个就设 DSH_OCR_MODELS_MIRROR=https://ghproxy.com/
整体都很慢 · 加代理最快:https_proxy=http://127.0.0.1:7892 http_proxy=http://127.0.0.1:7892 python3 …/ocr/setup.py
PyPI 慢 · 依赖默认已走清华源;要换源设 DSH_OCR_PIP_INDEX=<index-url>(设为空 = 官方源)
想调速度阈值 · DSH_OCR_MIN_SPEED_KBPS=500(默认 200)
如果所有源都慢,最后一轮会停止换源、直接把它下完 —— 不会永远在换源里打转。
中途断掉重跑即可,已下好且 sha256 校验通过的文件会跳过。
⚠️ 为什么不建议对 agent 说「用 ocr_setup 装」?
插件执行 Bash 时可能处在受限沙箱里(网络、可写目录都收紧),而装环境恰恰要下载依赖、
写 venv 和模型目录。实测在沙箱里会卡在「uv 缓存目录只读」之类的坑上,模型反复试错、
几分钟还不一定成。让它在你自己的 shell 里跑,一次就过。
ocr_setup 工具仍然可用(它调的就是同一个脚本),只是在受限环境下成功率低。
装好后把任意一张图片发给 agent,说「识别这张图片」即可。
第 3 步:开始使用
方式 A:粘贴截图(最常用)
在 Web 输入
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::6,最后更新 2026-10-21