Files
mnote/design/03-rust-web/process/3-25-local-folder-mineru-ocr-sidecar-v1.md
T
lix-2026 1882db7681 收口 MNote P0 P1 P2 审查尾项
- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目
- 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线
- 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径

验证:
- cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1
- cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1
- git diff --check
- git diff --cached --check
- codegraph index . --force && codegraph status .
- codegraph sync . && codegraph status .
2026-06-01 09:29:12 +08:00

18 KiB
Raw Blame History

3-25 Local Folder MinerU OCR Sidecar Checklist v1

创建时间:2026-05-29 状态:process Owner03-rust-web / local-folder resource runtime

2026-06-01 复核:后端 mock 闭环已落地 local_ocr.rs 与 OCR route;真实 MinerU HTTP client 已补本地 HTTP mock 成功路径测试;task526 已覆盖 mock OCR API、搜索命中、显式插入链接和 OCR sidecar Markdown resource tab 打开。前端 OCR task runtime、active job store、realtime event 和任务栏 UI 尚未落地。本文继续保持 process,不得被本轮 Page AI / OnlyOffice / ChatOnly 收口误归档。

背景

当前 mnote local-first 主线中,本地 Markdown 和同目录资源是默认数据真相。本地图片 / 附件上传已通过 /api/local-folder/assets/upload 写入 Markdown 文件附近,并返回 sourcePath / attachmentRef。现在需要给图片和图片型 PDF 增加 OCR 能力,识别引擎使用 MinerU API。

OCR 结果不应默认写进用户正文,也不应只存在 .mnote 私有缓存里。用户希望 OCR 附件位于当前 Markdown 文件相同目录下,并用单独文件夹承载,因为一个 Markdown 页面可能对应多个 OCR 文件。

目标

  • 对 local-folder 图片和图片型 PDF 提供手动触发 OCR。
  • 使用 MinerU API 识别本地文件,生成 Markdown OCR 结果。
  • OCR 结果作为页面旁路资源落盘到当前 Markdown 同目录的 {pageStem}.ocr/ 文件夹。
  • OCR 文本默认作为资源 sidecar 被搜索、资源面板和后续 AI 上下文消费,不自动污染正文。
  • 支持用户显式把某个 OCR 结果插入当前正文。
  • 提供全局 OCR 任务进展入口,让用户离开当前资源后仍能看到 OCR 是否完成、失败或需要重试。
  • 保持 local-first 可迁移性:OCR Markdown 文件本身可读、可备份、可编辑;.mnote 索引只作为缓存和状态加速。

非目标

  • 不做全 workspace 自动 OCR。
  • 不默认把 OCR 文本追加到页面正文。
  • 不把 MinerU token 打印到日志、前端 payload 或用户可见错误里。
  • 不在前端直接调用 MinerU API。
  • 不把 OCR 文件当成普通页面加入 Page Tree。
  • 不为 cloud / Convex / remote source 设计默认 OCR 主路径;第一阶段只覆盖 local-folder。
  • 不承诺精确百分比进度;MinerU API 第一阶段按阶段型状态展示。
  • 不把任务进展做成前端定时轮询主链;优先走现有 realtime / WS / SSE 事件。

文件布局

假设当前页面为:

docs/Page.md

附件可能为:

docs/Page.assets/photo.png
docs/Page.assets/spec.pdf

OCR 结果目录固定为:

docs/Page.ocr/

OCR 结果文件示例:

docs/Page.ocr/photo.png.ocr.md
docs/Page.ocr/spec.pdf.ocr.md

同一页面下存在同名来源文件时,用来源 root-relative path、文件大小和 mtime 派生短 hash:

docs/Page.ocr/photo.png-8f3a21.ocr.md

OCR Markdown frontmatter

OCR Markdown 是 OCR 正文真相,必须能脱离 .mnote 索引独立说明来源。

---
mnote_ocr_version: 1
provider: mineru
model_version: vlm
owner_document: ./Page.md
source_path: ./Page.assets/photo.png
source_root_relative_path: docs/Page.assets/photo.png
source_size: 123456
source_mtime_ms: 1760000000000
status: done
created_at: 2026-05-29T12:00:00+08:00
updated_at: 2026-05-29T12:00:00+08:00
---

识别文本...

字段口径:

  • owner_document:相对 OCR 文件所在目录到 owner Markdown 的相对路径。
  • source_path:相对 owner Markdown 所在目录的来源附件路径,和正文中的附件引用口径保持一致。
  • source_root_relative_path:相对 workspace root 的来源路径,用于稳定查找和索引。
  • source_size / source_mtime_ms:用于判断 OCR 是否 stale。
  • statusdonefailedstalequeued/running 只写入 .mnote/ocr-index.json,避免半成品 OCR 文件被误读。

.mnote 索引

.mnote/ocr-index.json 是状态缓存,不是 OCR 正文真相。

{
  "version": 1,
  "entries": {
    "docs/Page.assets/photo.png": {
      "ownerDocumentId": "local-md:docs~2FPage.md",
      "ownerDocumentPath": "docs/Page.md",
      "sourceRootRelativePath": "docs/Page.assets/photo.png",
      "ocrRootRelativePath": "docs/Page.ocr/photo.png.ocr.md",
      "provider": "mineru",
      "modelVersion": "vlm",
      "status": "done",
      "sourceSize": 123456,
      "sourceMtimeMs": 1760000000000,
      "createdAtMs": 1760000000000,
      "updatedAtMs": 1760000000000,
      "plainTextPreview": "识别文本前 240 字"
    }
  }
}

索引用途:

  • 资源面板快速展示 OCR 状态。
  • 搜索索引快速定位 OCR sidecar。
  • 避免重复提交同一个未变化资源。
  • 记录失败原因,但错误文本需要脱敏,不包含 token、预签名 URL 或完整外部响应体。

资源归属与树投影

  • OCR 文件夹 {pageStem}.ocr/ 应在 File Tree 中作为页面旁路资源文件夹出现。
  • {pageStem}.ocr/*.ocr.md 不应作为普通 Markdown 页面进入 Page Tree。
  • local search 不应把 OCR sidecar 当普通 Markdown 页面索引,否则会出现重复页面和错误导航。
  • 搜索 OCR 命中时,应返回 owner page 作为结果,hasOcr=trueevidence 指向来源附件和 OCR sidecar。
  • File Tree 可展示 OCR 文件,点击默认作为 Markdown resource tab 打开,不切换成页面文档。

MinerU 调用边界

后端读取 MinerU token,前端不接触 token。

配置优先级:

  1. MNOTE_MINERU_API_TOKEN
  2. MINERU_API_TOKEN
  3. 开发环境可选读取 ~/.hermes/.env 中的 MINERU_API_TOKEN

第一阶段使用 MinerU model_version=vlm

本地文件流程:

  1. POST https://mineru.net/api/v4/file-urls/batch 申请上传地址。
  2. 使用 PUT 把本地文件上传到预签名 URL。
  3. 轮询 GET /extract-results/batch/{batch_id}
  4. 下载 full_zip_url
  5. 解压并提取结果 Markdown。
  6. 写入 {pageStem}.ocr/*.ocr.md
  7. 更新 .mnote/ocr-index.json

后端 API

创建或复用 OCR job

POST /api/local-folder/ocr/jobs

请求:

{
  "rootUri": "file:///mnt/Data1T/mnote-notes",
  "documentId": "local-md:docs~2FPage.md",
  "sourcePath": "Page.assets/photo.png",
  "sourceRootRelativePath": "docs/Page.assets/photo.png",
  "provider": "mineru",
  "force": false
}

响应:

{
  "ok": true,
  "job": {
    "status": "done",
    "ownerDocumentId": "local-md:docs~2FPage.md",
    "sourceRootRelativePath": "docs/Page.assets/photo.png",
    "ocrRootRelativePath": "docs/Page.ocr/photo.png.ocr.md",
    "stale": false
  }
}

第一阶段可以同步执行并在请求内返回最终状态;如果真实耗时过长,再升级为后台 job。同步版本必须设置合理超时,并让 UI 显示运行中状态。

查询 OCR 状态

GET /api/local-folder/ocr/status?rootUri=...&sourceRootRelativePath=...

返回 .mnote/ocr-index.json 中的状态,并重新对比来源文件 size/mtime 判断 stale

查询 OCR 任务列表

GET /api/local-folder/ocr/jobs?rootUri=...

返回当前 root 下 active jobs 和最近完成 / 失败任务,用于全局任务栏首次渲染和刷新后恢复。

响应:

{
  "ok": true,
  "jobs": [
    {
      "jobId": "ocr_1760000000000_abcd",
      "fileName": "spec.pdf",
      "ownerDocumentId": "local-md:docs~2FPage.md",
      "ownerDocumentPath": "docs/Page.md",
      "sourceRootRelativePath": "docs/Page.assets/spec.pdf",
      "ocrRootRelativePath": "docs/Page.ocr/spec.pdf.ocr.md",
      "status": "mineru_processing",
      "stageLabel": "识别中",
      "startedAtMs": 1760000000000,
      "updatedAtMs": 1760000000000,
      "finishedAtMs": null,
      "stale": false
    }
  ]
}

读取 OCR 文本

GET /api/local-folder/ocr/read?rootUri=...&ocrRootRelativePath=...

返回 OCR Markdown 文本、frontmatter 摘要、来源路径和 stale 状态。

插入 OCR 到正文

POST /api/local-folder/ocr/insert

只在用户显式触发时执行。默认插入为链接块:

[OCRphoto.png](./Page.ocr/photo.png.ocr.md)

可选模式 inlineSection 插入正文段落:

### OCRphoto.png

<!-- mnote:ocr-source ./Page.assets/photo.png -->

识别文本...

UI 行为

  • 编辑器图片 toolbar / 附件右键菜单增加 OCR 识别
  • File Tree 图片/PDF 资源右键菜单增加 OCR 识别
  • PDF preview / image resource tab 显示 OCR 状态和结果入口。
  • 全局任务栏 / 任务抽屉显示运行中、完成、失败和 interrupted 的 OCR 任务。
  • 状态文案只表达:未识别、排队中、上传中、识别中、写入中、已识别、来源已变化、识别失败、已中断。
  • stale 结果展示“重新识别”操作,不自动覆盖旧 OCR 文件,除非用户确认或传 force=true
  • OCR 结果打开在 resource tab,不切到普通文档页面。

搜索与 AI 消费

  • includeOcr=false 时不搜索 OCR sidecar。
  • includeOcr=true 时,搜索 OCR 文本并返回 owner page。
  • 搜索 evidence 包含:
    • kind: "ocr"
    • sourceRootRelativePath
    • ocrRootRelativePath
    • snippet
  • 后续 image_read / Page AI attachment context 可以读取 OCR sidecar,优先返回已存在 OCR 文本,不让 agent 重复调用 OCR。

任务进展与全局入口

OCR 是慢任务,提交后必须有全局可见状态,不应只依赖 toast 或当前 resource tab。

第一阶段进度采用阶段型状态:

queued -> uploading -> mineru_processing -> downloading -> writing_sidecar -> done

失败和中断状态:

failed
interrupted
stale

状态含义:

  • queued:任务已创建,等待执行。
  • uploading:正在上传原始图片/PDF 到 MinerU 预签名地址。
  • mineru_processingMinerU 正在识别。
  • downloading:正在下载 MinerU 结果包。
  • writing_sidecar:正在写入 {pageStem}.ocr/*.ocr.md.mnote/ocr-index.json
  • doneOCR sidecar 已写入。
  • failed:任务失败,可重试。
  • interruptedmnote-web 进程或任务 worker 中断,不能确认完成,可重试。
  • stale:来源文件在 OCR 后发生变化。

后端维护一个运行时 active job store,并把每次状态变更写入 .mnote/ocr-index.json。页面刷新后,已完成、失败、stale 和 interrupted 状态可以从磁盘恢复;正在运行中的内存任务如果因进程重启丢失,应在下次读取时标记为 interrupted,由用户重试。

状态变化通过现有 realtime / WS / SSE 事件链路广播,不在前端叠加 setInterval 轮询主链。事件示例:

{
  "type": "local_ocr.job.updated",
  "jobId": "ocr_1760000000000_abcd",
  "rootUri": "file:///mnt/Data1T/mnote-notes",
  "ownerDocumentId": "local-md:docs~2FPage.md",
  "sourceRootRelativePath": "docs/Page.assets/spec.pdf",
  "ocrRootRelativePath": "docs/Page.ocr/spec.pdf.ocr.md",
  "status": "mineru_processing",
  "stageLabel": "识别中",
  "updatedAtMs": 1760000000000
}

前端需要两类展示:

  • 资源局部状态:图片/PDF resource tab、附件菜单、File Tree 行显示当前 OCR 状态。
  • 全局任务栏 / 任务抽屉:底栏或右上角显示 OCR 2 / 1 个任务进行中。点开后列出文件名、所属页面、阶段、开始时间、完成/失败状态,以及“打开 OCR”“重试”等操作。

第一阶段不强制支持取消。若 MinerU 任务 API 后续提供可靠取消,再补 cancel 动作。

隐私与安全

  • OCR 必须由用户手动触发。
  • UI 首次触发时应明确提示:文件会发送到 MinerU API。
  • 后端只允许读取 rootUri 授权根内文件,禁止绝对路径和 .. 越界。
  • 日志不记录 token、预签名 URL、完整外部错误响应和 OCR 正文全文。
  • 失败状态写入简短错误码和脱敏消息。

实施 Checklist

  • 后端:新增 MinerU client,支持本地文件上传、轮询、结果 zip 下载和 Markdown 提取。
  • 后端:新增 OCR 路由模块,提供 job/status/read/insert API。(当前 insert 只支持 link 模式)
  • 后端:新增 OCR active job store,记录 queued/uploading/mineru_processing/downloading/writing_sidecar/done/failed/interrupted/stale。
  • 后端:OCR 状态变化写入 .mnote/ocr-index.json 并广播 local_ocr.job.updated 事件。
  • 后端:提供 OCR jobs list API,用于全局任务栏刷新后恢复。
  • 后端:实现 {pageStem}.ocr/ 路径规划、文件名冲突处理和 UTF-8 OCR Markdown 写入。
  • 后端:实现 .mnote/ocr-index.json 读写、stale 检测和脱敏错误记录。
  • 后端:扩展 local-folder 文件分类,让 {pageStem}.ocr/*.ocr.md 不进入 Page Tree 普通页面。
  • 搜索:扩展 local search indexincludeOcr=true 时索引 OCR sidecar 并返回 owner page。
  • UI:在附件右键菜单、File Tree 资源菜单和资源 tab 增加 OCR 识别入口。
  • UI:展示 OCR 状态、stale、失败和重新识别动作。
  • UI:新增全局 OCR 任务栏 / 任务抽屉,显示任务阶段、所属页面、打开 OCR、重试。
  • [~] UI/API:增加“插入 OCR 链接到正文”和可选“插入 OCR 正文段落”动作。(后端 link API 已完成;前端入口和 inlineSection 待做)
  • AI:让 image/PDF 资源上下文优先读取已有 OCR sidecar。
  • 测试:Rust 单测覆盖路径规划、frontmatter、索引读写、stale、越界拒绝。
  • 测试:Rust route 测试覆盖无 token、真实 MinerU HTTP mock 成功、模拟失败和脱敏错误。
  • 测试:local search 测试覆盖 OCR 命中返回 owner pageOCR sidecar 不作为普通页面。
  • [~] Smoke:浏览器覆盖图片/PDF 手动 OCR、全局任务栏状态、打开 OCR resource tab、搜索 OCR 命中、插入 OCR 链接。(当前 task526 覆盖 mock OCR API、搜索命中和 insert link;真实 UI/任务栏/打开 OCR resource tab 待做)
  • 文档:补充 scripts/TESTING_REFERENCE.md 中 OCR smoke 基线。

验收标准

  • 本地图片或图片型 PDF 可被用户手动提交 MinerU OCR。
  • OCR 结果落在 owner Markdown 同目录的 {pageStem}.ocr/ 下,文件为 UTF-8 Markdown。
  • .mnote/ocr-index.json 删除后,仍能从 OCR Markdown frontmatter 重建核心绑定关系。
  • OCR 文件不会污染 Page Tree,也不会作为普通页面搜索结果出现。
  • 搜索 OCR 文本时返回 owner page,结果标记 hasOcr=true
  • 用户未显式选择插入时,页面正文不发生变化。
  • 用户提交 OCR 后,即使离开当前资源,也能在全局 OCR 任务栏看到阶段状态;完成后可打开 OCR,失败或中断后可重试。
  • 所有外部 API token 和预签名 URL 均不出现在日志、前端 payload 或错误响应中。

2026-06-01 后端 mock 闭环

已落地 rust/crates/mnote-web/src/routes/local_ocr.rs 与 route 注册,第一刀只承诺后端 mock 闭环:provider=mock 可同步生成 {pageStem}.ocr/*.ocr.md,写入 .mnote/ocr-index.json,提供 jobs/status/read;当时 provider=mineru 尚未执行真实外部调用,缺 token 时返回 mineru_token_missing,有 token 时返回 mineru_runtime_not_enabled

同时已把 OCR sidecar 从 Page Tree 普通 Markdown 扫描中排除,并扩展 local searchincludeOcr=false 不命中 OCR 文本,includeOcr=true 返回 owner page,结果带 hasOcr=trueocrEvidence

验证:

  • cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1
  • cargo test --manifest-path rust/Cargo.toml -p mnote-web local_search_ocr -- --test-threads=1

2026-06-01 续补:

  • local_ocr route 测试新增来源路径越界、非图片/PDF 来源和 mock 失败脱敏 response 形态覆盖。
  • local_ocr route 测试新增缺 MinerU token response 覆盖:mineru_token_missing
  • local_ocr route 测试新增 token 存在但真实 runtime 未接入 response 覆盖:mineru_runtime_not_enabled,避免把 mock OCR 闭环误报为真实 MinerU 能力。该旧口径已在后续真实后端 runtime 补齐后由 HTTP mock 成功路径测试替代。
  • 新增 POST /api/local-folder/ocr/insert,当前只支持 mode=link,显式把 OCR sidecar 链接追加到 owner Markdownlocal_ocr_insert_route_appends_explicit_ocr_link 已覆盖。

2026-06-01 浏览器 API smoke 续补:

  • 新增 scripts/task526-local-folder-ocr-api-smoke.js,在真实浏览器登录态页面内通过 fetch 串起 POST /api/local-folder/ocr/jobsstatusreadjobs/api/search/documents includeOcr=truePOST /api/local-folder/ocr/insert
  • 该 smoke 固定使用 provider=mock,不触碰真实 MinerU token、上传、轮询、zip 下载和 Markdown 提取链路。
  • 已验证 mock OCR sidecar 落盘、.mnote/ocr-index.json jobs/status/read 可读、includeOcr=false 不命中 OCR 文本、includeOcr=true 返回 owner page 且带 hasOcr/ocrEvidence、显式 insert 才向 owner Markdown 追加 OCR 链接。
  • 已通过:
    • node --check scripts/task526-local-folder-ocr-api-smoke.js
    • MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task526-local-folder-ocr-api-smoke.js
    • cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1
    • cargo test --manifest-path rust/Cargo.toml -p mnote-web local_search_ocr -- --test-threads=1

2026-06-01 后端真实 MinerU runtime 续补:

  • local_ocr.rs 已补 mineru_api_base_urlmineru_poll_intervalmineru_max_polls 配置函数。
  • provider=mineru 后端路径已接入申请上传 URL、PUT 上传、轮询 batch 结果、下载 zip、提取 Markdown。
  • 新增 local_ocr_jobs_route_runs_mineru_runtime_against_http_mock,用本地 HTTP mock 覆盖真实 client 合同,不访问外网。
  • 已通过:
    • cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1