Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
19 KiB
[recycle] 3-25 Local Folder MinerU OCR Sidecar Checklist v1
创建时间:2026-05-29 状态:
processOwner:03-rust-web / local-folder resource runtime2026-06-01 复核:后端 mock 闭环已落地
local_ocr.rs与 OCR route;真实 MinerU HTTP client 已补本地 HTTP mock 成功路径测试;resource tab 图片/PDF OCR 工具条、附件/File Tree 资源菜单入口和task526UI smoke 已补。active job store、local_ocr.job.updatedrealtime event、全局任务抽屉和 AI 资源上下文读取 OCR sidecar 尚未落地。本文继续保持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。status:done、failed或stale。queued/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=true,evidence 指向来源附件和 OCR sidecar。 - File Tree 可展示 OCR 文件,点击默认作为 Markdown resource tab 打开,不切换成页面文档。
MinerU 调用边界
后端读取 MinerU token,前端不接触 token。
配置优先级:
MNOTE_MINERU_API_TOKENMINERU_API_TOKEN- 开发环境可选读取
~/.hermes/.env中的MINERU_API_TOKEN
第一阶段使用 MinerU model_version=vlm。
本地文件流程:
POST https://mineru.net/api/v4/file-urls/batch申请上传地址。- 使用
PUT把本地文件上传到预签名 URL。 - 轮询
GET /extract-results/batch/{batch_id}。 - 下载
full_zip_url。 - 解压并提取结果 Markdown。
- 写入
{pageStem}.ocr/*.ocr.md。 - 更新
.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
只在用户显式触发时执行。默认插入为链接块:
[OCR:photo.png](./Page.ocr/photo.png.ocr.md)
可选模式 inlineSection 插入正文段落:
### OCR:photo.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"sourceRootRelativePathocrRootRelativePathsnippet
- 后续
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_processing:MinerU 正在识别。downloading:正在下载 MinerU 结果包。writing_sidecar:正在写入{pageStem}.ocr/*.ocr.md和.mnote/ocr-index.json。done:OCR sidecar 已写入。failed:任务失败,可重试。interrupted:mnote-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 index,
includeOcr=true时索引 OCR sidecar 并返回 owner page。 - UI:在附件右键菜单、File Tree 资源菜单和资源 tab 增加 OCR 识别入口。
- UI:展示 OCR 状态、stale、失败和重新识别动作。
- UI:新增全局 OCR 任务栏 / 任务抽屉,显示任务阶段、所属页面、打开 OCR、重试。
- UI/API:增加“插入 OCR 链接到正文”动作。(可选“插入 OCR 正文段落”不作为本阶段验收项,后续需要时另拆增强稿)
- AI:让 image/PDF 资源上下文优先读取已有 OCR sidecar。
- 测试:Rust 单测覆盖路径规划、frontmatter、索引读写、stale、越界拒绝。
- 测试:Rust route 测试覆盖无 token、真实 MinerU HTTP mock 成功、模拟失败和脱敏错误。
- 测试:local search 测试覆盖 OCR 命中返回 owner page,OCR sidecar 不作为普通页面。
- Smoke:浏览器覆盖图片/PDF 手动 OCR、全局任务栏状态、打开 OCR resource tab、搜索 OCR 命中、插入 OCR 链接。
- 文档:补充
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 search:includeOcr=false 不命中 OCR 文本,includeOcr=true 返回 owner page,结果带 hasOcr=true 与 ocrEvidence。
验证:
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1cargo test --manifest-path rust/Cargo.toml -p mnote-web local_search_ocr -- --test-threads=1
2026-06-01 续补:
local_ocrroute 测试新增来源路径越界、非图片/PDF 来源和 mock 失败脱敏 response 形态覆盖。local_ocrroute 测试新增缺 MinerU token response 覆盖:mineru_token_missing。local_ocrroute 测试新增 token 存在但真实 runtime 未接入 response 覆盖:mineru_runtime_not_enabled,避免把 mock OCR 闭环误报为真实 MinerU 能力。该旧口径已在后续真实后端 runtime 补齐后由 HTTP mock 成功路径测试替代。- 新增
POST /api/local-folder/ocr/insert,当前只支持mode=link,显式把 OCR sidecar 链接追加到 owner Markdown;local_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/jobs、status、read、jobs、/api/search/documents includeOcr=true和POST /api/local-folder/ocr/insert。 - 该 smoke 固定使用
provider=mock,不触碰真实 MinerU token、上传、轮询、zip 下载和 Markdown 提取链路。 - 已验证 mock OCR sidecar 落盘、
.mnote/ocr-index.jsonjobs/status/read 可读、includeOcr=false不命中 OCR 文本、includeOcr=true返回 owner page 且带hasOcr/ocrEvidence、显式 insert 才向 owner Markdown 追加 OCR 链接。 - 已通过:
node --check scripts/task526-local-folder-ocr-api-smoke.jsMNOTE_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.jscargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1cargo 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_url、mineru_poll_interval、mineru_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
2026-06-01 任务链与 AI context 收口:
- 后端在
AppState增加 OCR active job store 和local_ocr_job_txbroadcast;POST /api/local-folder/ocr/jobs会在 queued / uploading / mineru_processing / downloading / writing_sidecar / done / failed 阶段更新.mnote/ocr-index.json,并通过/api/local-folder/events发出local_ocr.job.updated。 - 资源 tab 增加全局 OCR 任务 dock / drawer;任务抽屉从 jobs API 恢复历史状态,并通过
local_ocr.job.updated事件更新,支持打开 OCR 和失败/stale 重试。 - Page AI active image/PDF resource target 会在发 run 前读取已有 OCR sidecar,把
mnote.local_ocr_context.v1放入 active_editor contextRef、targetPackage、currentFile 和 primary target。 - 已通过:
node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.jsnode --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.jsnode --check rust/crates/mnote-web/browser/sidebar-page-ai-target-runtime.jsnode --check scripts/task526-local-folder-ocr-api-smoke.jsnode --check scripts/task520-page-ai-raw-resource-target-smoke.jscargo fmt --check --allcargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1cargo test --manifest-path rust/Cargo.toml -p mnote-web local_search_ocr -- --test-threads=1MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3000 node scripts/task526-local-folder-ocr-api-smoke.jsTASK520_OCR_CONTEXT=1 MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3000 node scripts/task520-page-ai-raw-resource-target-smoke.js