dsh-doc-router
Document intake for agents: doc_route sniffs a file's real format by magic bytes and measures a PDF's text layer and page column layout before any conversion, and pdf_markdown converts PDFs to Markdown in column reading order with PyMuPDF. The PDF path needs a Python 3 interpreter with PyMuPDF.
Install
dsh plugin --profile web add dsh-doc-router
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
dsh-doc-router




English | 中文
先路由,再读取。 一个 DeepSeek Harness
插件:先探明文件的真实格式与 PDF 的页面版面,再把它交给该用的那条管线;
对多栏论文做版面感知转换,而不是把正文切碎。
[为什么](#为什么需要它) · [提供什么](#提供什么) · [安装](#安装) · [快速开始](#快速开始) · [路由是怎么判的](#路由是怎么判的) · [配置](#配置) · [实测证据](#实测证据) · [故障排查](#故障排查) · [开发](#开发) · [路线图](#路线图) · [许可证](#许可证)
为什么需要它
默认转换器会把多栏论文做错,而且错得不轻。以一篇 Nature Letter
(nmat4749.pdf,双栏,7 页)为例,在 PyMuPDF 1.28.2 / MarkItDown 0.2.0 下实测:
管线 · 输出 · 多栏处理
pymupdf4llm(版面感知) · 第 1 页 6 533 字符 · ✅ 两栏按阅读顺序合并;标题→#、上标→<sup>、_in situ_ 斜体全部保留
pypdf · 全 7 页 30 632 字符 · ⚠️ 正文可读,但结构全丢
MarkItDown · 全 7 页 45 266 字符 · ❌ 正文碎成数百行 \ · … \ · 表格
同一失败可以在 clone 后自己复现。用本仓库的
(两栏 / 三栏内容各自带标记的合成页),MarkItDown 0.1.5 会把每一栏同时竖着读下去:
夹具 · pdf_markdown · MarkItDown 0.1.5
two-column-reading-order.pdf · L01…L15,然后 R01…R15 · L01 R01 L02 R02 … —— 29 次跨栏跳变
three-column.pdf · L01…L15、M01…M15,然后 R01…R15 · L01 M01 R01 L02 … —— 44 次跨栏跳变
整个论点就这一行:一条管线保住了栏,另一条把它们交错在一起。
npm test 断言的是上表两个 pdf_markdown 单元格里的阅读顺序。
所以让模型「随便挑个转换器」的后果,是拿到一份被悄悄弄坏的原文,而且看起来像是成功了。
解法不是找一个更好的转换器,而是路由:先看文件,再选管线。
提供什么
产物 · 作用
doc_route 工具 · 判类并给出该用的管线:格式、页数、有无文字层、单栏/多栏,以及判据背后的逐页证据
pdf_markdown 工具 · 用 PyMuPDF 做版面感知的 PDF→Markdown;支持页码区间与落盘
doc-routing 技能 · 注册进 runtime 技能层,因此对所有工作区生效,不依赖工作区里的 skills/ 目录;它告诉模型「读之前先路由」
所有判据都是确定性的:零模型调用、零网络、零 token。
安装
版本要求
DSH · >=0.2.0-rc.1 <0.3.0-0 · · >=0.2.1-rc.1 <0.3.0-0
Node · ^22.19.0 · · >=24.0.0
Python · 3.9+ —— 只有 PDF 版面探测与 pdf_markdown 需要
PyMuPDF · pip install -U pymupdf pymupdf4llm —— 可选,见[许可证](#许可证)
解释器查找顺序,命中即止:
1. config.pythonPath
2. 环境变量 DOC_ROUTER_PYTHON
3. DSH 打包的运行时
4. ~/.dsh/dsh-runtimes 下的运行时
5. PATH 上的 python
显式指定的解释器原样采用,所以裸命令名(如 python3)也能用;
而写错路径时,报错会明确报出你指定的那个路径,不会变成一个谜。
从 npm 安装(推荐)
plugin_manager action=install_bundle target="dsh-doc-router@0.6.1"
请写精确版本。 DSH 的包管理器有最小发布年龄策略,只写包名可能悄悄解析到旧版本。
装完请重启 DSH。
从 DSH 命令行安装
dsh plugin --profile <你的 profile> add dsh-doc-router@0.6.1
<你的 profile> 必须是 DSH 实际启动的那个。拿不准就用上面的 plugin_manager,
它会自动作用于当前 profile。这个子命令有两个反直觉之处:
--profile写错不会报错,而是新建一个。 该名字会被初始化成一个全新的空
profile,包装进那里,于是插件永远到不了你正在跑的那个 profile。当前 profile 就是
$DSH_HOME/profiles 下那个 package.json 里列着你的插件的目录。
--help不是空跑。dsh plugin --profile <名字> --help会先把那个 profile
初始化出来,再打印 pnpm 的帮助。
从本地源码目录安装
plugin_manager action=install_bundle target="file:<仓库绝对路径>"
file: 安装是拷贝,所以改源码不会生效:必须升版本号 → 移除 bundle →
重装 → 重启 DSH。详见
启用版面探测
<插件解析到的 python> -m pip install -U pymupdf pymupdf4llm
没有 PyMuPDF 时插件照常加载,doc_route 仍能按格式判类;
只有版面探测与 pdf_markdown 会停用,并且两者都会明确说明原因,而不是含糊地失败。
快速开始
先路由,再转换。下面的例子全部取自本仓库自带的合成夹具,因此每一行都可以在 clone 后复现。
1. 先路由
doc_route({ path: "tests/fixtures/two-column.pdf" })
→ Routed tests/fixtures/two-column.pdf — format: pdf, pages: 1, text layer: yes, columns: multi-column (est 2)
Recommended pipeline: pdf_markdown
多栏正文(估 2 栏)→ markitdown 会把正文切成表格碎片,pypdf 会丢结构
需精确核对公式/上下标/数字时,再对该页渲染成图校验
Probe detail: {"1":"0.91/2col"}
dsh-doc-router v0.6.1
上面两行提示语是中文,因为这一段是在noteLanguage: 'zh'下跑的。**0.4.0起
默认是英文**——见[配置](#配置)。判据行、recommend、probe三个字段与语言无关。
2. 再转换
pdf_markdown({ path: "tests/fixtures/two-column.pdf" })
→ Converted tests/fixtures/two-column.pdf (all 1 pages) — 3391 characters of Markdown. …
pdf_markdown({ path: "tests/fixtures/two-column.pdf", pages: "1", output: "paper.md" })
→ Converted tests/fixtures/two-column.pdf (pages 1) — wrote 3391 characters to paper.md.
两个工具都接受相对工作区的路径。当内联输出会超过 maxChars 时,
pdf_markdown 会截断并明确告知,而不是灌爆上下文——要完整文档就传 output 落盘。
真实判据(来自本仓库的夹具)
下表每一行都是在仓库自带的夹具上跑 doc_route 得到的:
夹具 · 判据 · 管线
two-column.pdf · pdf · 1 页 · 有文字层 · 多栏(估 2 栏) · pdf_markdown
three-column.pdf · pdf · 1 页 · 有文字层 · 多栏(估 3 栏) · pdf_markdown
two-column-watermark.pdf · pdf · 1 页 · 有文字层 · 多栏(估 2 栏——不是 3) · pdf_markdown
unknown-columns.pdf · pdf · 3 页 · 有文字层 · unknown · pdf_markdown(安全侧)
dense-script-column.pdf · pdf · 1 页 · 有文字层 · 多栏(估 2 栏) · pdf_markdown
single-column.pdf · pdf · 1 页 · 有文字层 · 单栏 · markitdown → pypdf
scanned.pdf · pdf · 1 页 · 无文字层 · render_to_png → read_image
sample.docx · docx · markitdown
sample.png · png · read_image
sample.txt · text · read
pdf_markdown 会拒绝非 PDF,并直接告诉你该用哪条管线:
pdf_markdown({ path: "tests/fixtures/sample.docx" })
→ pdf_markdown only converts PDFs — detected docx. Recommended pipeline: markitdown
(run doc_route for the full verdict)
[!NOTE]
关于提示语的语言。 判据行、recommend、probe三个字段与语言无关。
所有给人或给模型看的文本都由 [noteLanguage](#配置) 选择:判据之后的
自由文本提示语、pdf_markdown的拒绝信息、以及doc-routing技能正文。
0.4.0起默认en,zh保持旧行为。
路由是怎么判的
所有判据都是确定性的:零模型调用
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::9,最后更新 2026-10-26