Skip to main content
Glama
README.md
# 双路抽取 MCP Server(dual-extract-mcp)

> 把「MinerU 文本/版面路 + Qwen3-VL 视觉路 + 双路融合裁决」封装成 MCP 工具集(8 个 Tool),
> 让 Claude Desktop / Cursor 等 MCP 客户端通过 **stdio** 调用,对本地 PDF 做结构化文档理解
> (字段抽取 / 表格 / 公式 / 探查 / 校验)。完全本地、离线、零外部 API。
> V2.0 起升级为形态 2「三源两段」:MinerU 结构主路 + OvisOCR2 按需校验 → 文本路内部融合 → 现有 Fusion Engine 二元契约零改动。

- 状态:**v2.0.1(2026-08-10)**:形态 2「三源两段」——MinerU 结构主路 + OvisOCR2 按需校验 → 文本路内部融合 → 现有 Fusion Engine 二元契约零改动;OvisOCR2 服务端 `POST /parse_page` 422 修复;D-2 真连实测 PASS(p213 95.71% 201/210、p8 95.45% 84/88,实质错值 0);**674 回归全绿(+9 deselected)**
- 传输:stdio(本期不监听端口)
- 语言:Python 3.11+(受管 3.13.14 实测可用)

---

## 1. 功能一览(8 个 Tool)

| Tool | 说明 | 里程碑 |
|---|---|---|
| `dual_extract` | 双路+融合主入口(字段抽取) | M3(T03) |
| `parse_text` | 文本路单跑(V2.0 起可选 OvisOCR2 校验,触发规则引擎四信号按需调用) | M2(T02) |
| `vision_extract` | 视觉路单跑(PDF 或独立图片,v1.3 起支持 `image_path`) | M2(T02) |
| `extract_table` | 表格(跨页 + 大表分层 + CSV/XLSX) | M4(T04) |
| `extract_formulas` | 公式(LaTeX + PNG 渲染) | M5(T04) |
| `inspect_document` | 文档探查(页数/表格/图片/公式统计) | M6(T05) |
| `validate_output` | 输出结构校验(schema/manifest) | M6(T05) |
| `health` | MinerU + Ollama + OvisOCR2 三服务探活(OvisOCR2 可选) | M1 |

> 完整输入/输出契约见 [`docs/api.md`](docs/api.md)。

---

## 2. 环境要求

- Python 3.11+(Windows 优先,Linux 兼容)
- 本地已部署:
  - **MinerU** 服务 `http://127.0.0.1:8000`(`/health` 返回 `{"status":"ok"}`)
  - **Ollama** `http://127.0.0.1:11434`(含 `qwen3-vl:30b` 视觉模型)
  - **OvisOCR2**(可选,V2.0 文本路校验增强):独立进程 `http://127.0.0.1:8010`
    (`scripts/ovisocr2_server.py` + 独立 venv;默认 `text_backend=mineru` 不加载/
    不调用,`DUAL_EXTRACT_TEXT_BACKEND=mineru+ovisocr2` 显式开启)

  **OvisOCR2 部署(独立 venv,不进主项目 pyproject 依赖;命令以
  `scripts/ovisocr2_server.py` 模块 docstring 为权威,V2.0 真连实测验证)**:

  ```powershell
  # 1. 建独立 venv(复用系统 CUDA 可选 --system-site-packages;用户本机标准命令)
  python -m venv .venv-ovisocr2
  .\.venv-ovisocr2\Scripts\Activate.ps1

  # 2. 安装依赖(版本对齐调研实测,与脚本 docstring 一致)
  pip install "transformers==5.14.1" "torch>=2.6.0,<3.0" `
      "fastapi>=0.110.0" "uvicorn>=0.29.0" "pillow>=10.0.0" "modelscope>=1.15.0"

  # 3. 启动(默认 127.0.0.1:8010;与 MinerU :8000 / Ollama :11434 错开)
  #    不传 --model 时默认从 ModelScope 下载 ATH-MaaS/OvisOCR2
  python scripts/ovisocr2_server.py

  #    本地模型快照加载(已下载过时用,免重复下载;快照含 model.safetensors)
  python scripts/ovisocr2_server.py --host 127.0.0.1 --port 8010 `
      --model "C:/Users/Administrator/.cache/modelscope/models/ATH-MaaS--OvisOCR2/snapshots/master"

  # 4. 健康检查(常驻显存 ~1.6GB;模型加载一次常驻,与 Qwen3-VL 串行不并发)
  curl http://127.0.0.1:8010/health
  # → {"status":"ok","model":...,"gpu_mem_mb":1626}
  ```

  > ⚠️ transformers 5.x 坑位(脚本 docstring 权威,服务端已封装,用户无需传参):
  > 显式 `eos_token_id=[248046, 248044]` 防死循环;`temperature` 参数在 Qwen3_5 架构
  > 无效必须省略;`do_sample=False`;`images_kwargs={"min_pixels":448*448,"max_pixels":2880*2880}`;
  > `enable_thinking=False`。

> ⚠️ 沙箱提示:本沙箱内 `python -m venv` 会静默失败,须用受管 Python 建 venv
> (`C:\Users\Administrator\.workbuddy\binaries\python\versions\3.13.12\python.exe -m venv .venv`)。
> 用户本机无此坑,按下方标准命令即可。

---

## 3. 安装

```powershell
# 1. 建 venv(受管 Python 3.13.14;用户本机可直接 python -m venv .venv)
C:\Users\Administrator\.workbuddy\binaries\python\versions\3.13.12\python.exe -m venv .venv

# 2. 激活 + 安装(含测试依赖)
.\.venv\Scripts\Activate.ps1
pip install -e ".[test]"

# 3. 复制配置模板并按需修改(可选;不配则用默认值)
Copy-Item .env.example .env
```

---

## 4. 运行

```powershell
# 开发期直跑(stdio 交互;起几秒确认无报错即可 Ctrl+C)
python -m dual_extract.server
# 或等价的 console script
mcp-server
```

### 接入 Claude Desktop(claude_desktop_config.json)

```json
{
  "mcpServers": {
    "dual-extract": {
      "command": "D:/Dual-Extract-MCP/.venv/Scripts/python.exe",
      "args": ["D:/Dual-Extract-MCP/src/dual_extract/server.py"]
    }
  }
}
```

### 接入 Cursor

`~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "dual-extract": {
      "command": "D:/Dual-Extract-MCP/.venv/Scripts/python.exe",
      "args": ["D:/Dual-Extract-MCP/src/dual_extract/server.py"]
    }
  }
}
```

> B1:MCP 客户端 CWD 不可控,建议在 `.env` 中把 `DUAL_EXTRACT_OUTPUT_DIR` 配成**绝对路径**。

---

## 5. 测试

```powershell
# 单元测试(默认跳过 smoke)
pytest -q

# 冒烟:真连本机 MinerU/Ollama(需服务在线)
pytest -m smoke -q
```

> 当前回归:**674 passed + 9 deselected**(2026-08-10 v2.0.1 全量回归核实,含新增 OvisOCR2 相关测试)。
> 历史演进:v0.2.1 353 passed → V1.0 406 passed → V1.3.0 615 passed → V2.0.1 **674 passed**。

---

## 5.1 V1.0 增量说明(2026-08-03)

V1.0 在 v0.2.1 基础上新增 5 项**可选/默认关**能力(全部 additive,当时 `schema_version` 为 `"1.0"`;当前统一 `"1.1"`,validate_output 兼容 1.0/1.1 白名单双值,旧客户端零改动):

| 增量 | 落点 | 说明 |
|---|---|---|
| `meta.generation` | `extraction.json` → `meta.generation` | 生成环境信息:`{generator, version, mineru_version, vlm, vlm_backend}`(PRD §5.1) |
| `artifact_id` | `extraction.json` 顶层 + `document.artifact_id` | 产物唯一身份:`{pdf_stem}_{YYYYMMDD}_{seq:03d}`(同 run 幂等复用) |
| `fields[].field_id` | `extraction.json` → `fields[]` / `aggregated_pages[]` | 字段审计 ID:`{artifact_id}.field.{key_snake}.p{page}.{seq:03d}` |
| `quality_report.json` | 抽取类 Tool job 结束自动落盘(`{产物目录}/quality_report.json`) | 质量报告:17 顶层键,`overall` 自动三态判定(达标/有条件达标/不达标) |
| `tables[].cell_provenance` | `extract_table` 输出(**默认关**,`DUAL_EXTRACT_CELL_PROVENANCE=true` 开启) | 单元格溯源五字段 `{row, col, value, page, bbox}`;chunked 大表 >500 行只记录前 500 行 + warning |

> 挂接约定(artifact_manifest,V1.0 P2):产物唯一身份由 **`artifact_id` + `manifest.json` 的 SHA256 + 相对路径** 三元组确定——`artifact_id` 标识「哪次抽取」,SHA256 保证「内容未被篡改」,相对路径定位「文件在哪」。详见 `docs/最终输出说明.md` §4 / `docs/api.md` §2.4。

---

## 6. 配置项(全部前缀 `DUAL_EXTRACT_`,仅 config.py 读取)

| 键 | 默认值 | 说明 |
|---|---|---|
| `DUAL_EXTRACT_MINERU_BASE_URL` | `http://127.0.0.1:8000` | MinerU HTTP 端点 |
| `DUAL_EXTRACT_MINERU_TIMEOUT` | `120` | MinerU 请求超时(秒) |
| `DUAL_EXTRACT_OLLAMA_BASE_URL` | `http://127.0.0.1:11434` | Ollama 端点 |
| `DUAL_EXTRACT_OLLAMA_MODEL` | `qwen3-vl:30b` | 视觉模型 |
| `DUAL_EXTRACT_OLLAMA_TIMEOUT` | `600` | Ollama 超时(秒;冷启动模型加载可达) |
| `DUAL_EXTRACT_OLLAMA_THINK` | `false` | 视觉路 thinking 开关(G2,默认关) |
| `DUAL_EXTRACT_TEXT_BACKEND` | `mineru` | 文本路后端:`mineru` \| `mineru+ovisocr2`(V2.0;默认 mineru 零加载/零调用) |
| `DUAL_EXTRACT_OVISOCR2_BASE_URL` | `http://127.0.0.1:8010` | OvisOCR2 独立进程 HTTP 端点(V2.0) |
| `DUAL_EXTRACT_OVISOCR2_MODEL` | `ATH-MaaS/OvisOCR2` | OvisOCR2 模型(ModelScope 仓库 id,V2.0) |
| `DUAL_EXTRACT_OVISOCR2_PAGE_TIMEOUT` | `180` | OvisOCR2 整页超时红线(秒;实测 96s × ~1.9 余量) |
| `DUAL_EXTRACT_OVISOCR2_TABLE_TIMEOUT` | `90` | OvisOCR2 单表超时(秒;拆表兜底 P2 预留) |
| `DUAL_EXTRACT_OVISOCR2_CONF_THRESHOLD` | `0.80` | 表格低置信触发阈值(confidence < 此值 → 触发 OvisOCR2 校验) |
| `DUAL_EXTRACT_OVISOCR2_TRUNC_RATIO` | `0.90` | 截断兜底比例(token_count ≥ 上限 × 此值 → 判截断降级用 MinerU) |
| `DUAL_EXTRACT_OVISOCR2_MAX_NEW_TOKENS` | `12288` | 服务端生成上限(< 16384 官方上限,防死循环) |
| `DUAL_EXTRACT_OUTPUT_DIR` | `./outputs` | 输出根目录(建议绝对路径) |
| `DUAL_EXTRACT_OUTPUT_VERSIONED` | `false` | true 时输出加时间戳子目录 |
| `DUAL_EXTRACT_CONFIDENCE_THRESHOLD` | `0.7` | 低置信阈值 |
| `DUAL_EXTRACT_PARALLEL` | `false` | 页并行(默认串行防 3090 OOM) |
| `DUAL_EXTRACT_MAX_FILE_MB` | `500` | 输入 PDF 大小上限 |
| `DUAL_EXTRACT_MAX_PAGES` | `1000` | 页数上限 |
| `DUAL_EXTRACT_IMAGE_DPI` | `200` | 页图/图片 DPI |
| `DUAL_EXTRACT_CROP_FIGURES` | `false` | 图表/印章裁剪开关 |
| `DUAL_EXTRACT_SINGLE_PATH_DEGRADE` | `true` | 单路降级开关 |
| `DUAL_EXTRACT_CELL_PROVENANCE` | `false` | 表格单元格溯源(V1.0;开启后 `tables[].cell_provenance` 记录 row/col/value/page/bbox,chunked 大表自动降级) |
| `DUAL_EXTRACT_MODEL_JUDGE` | `false` | 模型裁判(默认关,v0.4 前不实现) |
| `DUAL_EXTRACT_LOG_LEVEL` | `INFO` | 日志级别 |
| `DUAL_EXTRACT_TEMP_DIR` | `./tmp` | 临时目录(退出清理) |
| `DUAL_EXTRACT_CSV_SPLIT_ROWS` | `500` | CSV 分片行数 |

---

## 7. 真实样例用法

```powershell
# 1. 探查:先「ls」一份 PDF 再决定抽取策略
#    inspect_document 返回 {pages, tables, images, formulas, text_chars}
#    样例:232 页财报 → 421 图 / 213,232 字符(MinerU 不可达时自动降级 PyMuPDF)

# 2. 双路字段抽取
#    dual_extract(pdf_path="D:/samples/中芯国际_年报.pdf",
#                 fields=["公司代码","公司简称","法定代表人","报告期","资产总计"],
#                 field_types={"资产总计":"money","报告期":"date"})

# 3. 表格(跨页 + 大表分层 + CSV/XLSX)
#    extract_table(pdf_path="D:/samples/中芯国际_年报.pdf", pages=[10,11],
#                  format=["json","csv"], cross_page=true)

# 4. 公式
#    extract_formulas(pdf_path="D:/samples/中芯国际_年报.pdf", pages=[20],
#                     render=true, tex=false)

# 5. 校验结果
#    validate_output(output_dir="D:/Dual-Extract-MCP/outputs/中芯_年报")
#    → {"schema_version": "1.1", "ok": true, "errors": [], "manifest_ok": true}
#    (产物目录含 extraction.json + manifest.json;manifest 记录各产物 SHA256,
#      validate_output 会与磁盘实文件比对,防产物漂移/被篡改)

# 6. 独立图片字段抽取(v1.3+,截图/扫描件/名单/表格照片,无需先包 PDF)
#    vision_extract(image_path="D:/samples/名单.png",
#                   fields=["公司代码","公司简称","注册资本"])
#    → document.source_image 填充 / source_pdf 空串 / 字段 page:0

# 7. OvisOCR2 文本路校验增强(v2.0+,形态 2「三源两段」)
#    DUAL_EXTRACT_TEXT_BACKEND=mineru+ovisocr2 python -m dual_extract.server
#    dual_extract(pdf_path="D:/samples/中芯国际_年报.pdf",
#                 fields=["公司代码","短期借款"],
#                 verify_pages=[1,2])   # 调用级指定校验页(信号②)
#    → meta.generation.text_extractors=["mineru","ovisocr2"];
#      warnings 含 ovisocr2_crosscheck:p1:rate=1.0 等校验信号;
#      默认 text_backend=mineru 不加载/不调用(零回归)
```

---

## 8. 项目结构(四层架构,依赖单向向下)

```
MCP Layer       server.py → mcp/{tools,handlers,converters}.py
    ↓
Service Layer   services/*.py(extract/parse/vision/table/formula/inspect/validate/health)
    ↓
Pipeline Layer  pipelines/*.py + fusion/*.py
    ↓
Adapter Layer   adapters/base.py(接口缝)+ mineru/ollama/storage/ovisocr2 实现
```

- 与外部服务唯一耦合点 = 四个 Adapter 的 Protocol 接口(`MinerUAdapter` / `OllamaAdapter` / `StorageAdapter` / `OvisOCR2Adapter`)。
- 铁律:不建注册表、不预写第二后端(Docling/云模型/对象存储 v0.4+ 再说)。
- 详见 [`docs/architecture.md`](docs/architecture.md)。

---

## 9. 常见问题(FAQ)

### 9.1 VPN 代理导致 localhost 请求 502 / 超时(trust_env 陷阱)

Windows 上挂极光 VPN(端口 29290)时,Python httpx 默认 `trust_env=True` 会读取系统
注册表代理,把 `http://127.0.0.1:8000` 的 localhost 请求也走代理 → 502。
本项目所有 Adapter 的 `httpx.AsyncClient` 一律 `trust_env=False`,**无需手动处理**。
若在自定义脚本里直接调 httpx,请同样加 `trust_env=False`。

### 9.2 Ollama 冷启动 600s 超时

`qwen3-vl:30b`(30B 模型)首次加载进显存可能耗时 1-5 分钟,第一请求容易超时。
本项目 `DUAL_EXTRACT_OLLAMA_TIMEOUT` 默认 `600` 秒,足够冷启动;
若你的显存/磁盘更慢,可调大该值。探活(health)用固定 8s 短超时,不受此影响。

### 9.3 3090 24G 显存:默认串行防 OOM

`qwen3-vl:30b` 在 3090 24G 下并行多页易 OOM。默认 `DUAL_EXTRACT_PARALLEL=false`(串行),
页图渲染也是串行(F1)。若显存充裕可开并行,但建议先压测单页显存占用。

### 9.4 MinerU 不可达时怎么办

- `dual_extract` / `vision_extract` / `parse_text`:自动降级(视觉路单跑或纯文本),
  输出 `warnings` 标 `degraded`,不整体失败;
- `inspect_document`:自动降级为纯 PyMuPDF 本地统计(页数/图片/文本字符),
  `degraded=true` + `warnings` 标注。

### 9.5 输出目录找不到产物

MCP 客户端 CWD 不可控,务必在 `.env` 把 `DUAL_EXTRACT_OUTPUT_DIR` 配成**绝对路径**
(如 `D:/Dual-Extract-MCP/outputs`)。产物按 `pdf_stem` 分目录:
`{输出根}/{pdf文件名}/extraction.json` + `manifest.json`。

---

## 10. Roadmap

- **v0.2-alpha gate(T01-T03 = M1-M3)**:骨架 + 单路 + 双路融合闭环 ✅
- **T04(M4-M5)**:表格 + 公式 ✅
- **T05(M6-M7)**:inspect/validate + 安全加固 + 降级完善 + 文档 ✅
- **v0.2.0(2026-08-03 转正)**:G4 gate 真连验收 PASS(8 Tool 真连全通 + 3 P1 修复闭环 + 349 回归全绿)✅
- **v0.2.1(2026-08-03 发版)**:BUG-4 MinerUAdapter 子区间页号偏移修复(`parse(pages=[7])` 时产物 page_idx 从 0 重编号,blocks 全标 page=1;应按 pages[0] 偏移)——commit `bec69aa`,353 回归全绿(4 个 BUG-4 专项回归)✅
- **V1.0(2026-08-03 增量)**:`meta.generation` / `artifact_id` / `fields[].field_id` / `quality_report.json` 自动产出 / `tables[].cell_provenance`(默认关)——5 项 additive 增量,406 回归全绿,QA 独立验证 PASS ✅
- **V1.2.0(2026-08-04)**:对齐《程序输出规范 v1.1》六项增量(source/pipeline/models 镜像、0.95 封顶、字段级 source、schema 1.1、cleaning_report、manifest 收敛)——582 回归全绿 ✅
- **V1.2.1(2026-08-04)**:MinerU 适配器降级/异常双 bug 修复(HTTP 相对路径 500 + CLI -p 误传)——586 回归全绿 ✅
- **V1.2.2(2026-08-05)**:health asyncio 嵌套修复 + BUG-5/6/7 表格取值修复——608 回归全绿 ✅
- **V1.3.0(2026-08-09)**:vision_extract 新增 image_path 独立图片输入——615 回归全绿 ✅
- **V2.0.0(2026-08-09)**:OvisOCR2 文本路校验增强接入(形态 2「三源两段」)——MinerU 结构主路 + OvisOCR2 按需校验 → 文本路内部融合 → 现有 Fusion Engine 二元契约零改动;纯 additive,`schema_version` 保持 `"1.1"`,默认 `text_backend=mineru` 零加载/零调用——**674 回归全绿 ✅**
- **V2.0.1(2026-08-10)**:OvisOCR2 服务端 `POST /parse_page` 422 修复(根因:`ParsePageRequest`/`ParsePageResponse` 原是 `create_app()` 内部局部类,FastAPI 局部 Pydantic model 请求体识别失败 → 提升为模块级)+ D-2 真连实测 PASS(p213 95.71% 201/210、p8 95.45% 84/88,均 ≥95% 达标,实质错值 0)+ D-8 文档同步——bugfix 无 API 变更,**674 回归全绿 ✅**
- 调研完成:复杂表格页降载策略(docs/复杂表格页降载策略.md)+ OvisOCR2 vs MinerU 对比(docs/OvisOCR2_vs_MinerU_对比报告.md)+ 模型分类与升级路线(docs/模型分类与升级路线.md)
- 后续登记(保持 roadmap):~~OvisOCR2 文本路 Adapter 立项(需先补 16k 截断兜底设计)~~ → ✅ 已完成(V2.0);VLM 线评估 Qwen3.6-35B-A3B(排在 OvisOCR2 之后);模型裁判(C3 默认关)→ v0.4 前不实现(保留原条目)
- 多后端注册表(Docling/云模型/对象存储)→ 保持 roadmap

详细设计见 `docs/技术方案设计_双路抽取MCP.md` / `docs/双路抽取MCP_架构设计.md` /
`docs/双路抽取MCP_PRD.md` / `docs/architecture.md` / `docs/api.md`。
输出质量评估(三层框架 + 可执行脚本)见 `docs/输出质量评估指南.md` / `tools/assess_output_quality.py`。