# 7-48 [process] Paperless-ngx Reference: Resource Ingestion / Job Ledger / Evidence Index v1

> 创建时间：2026-06-05
>
> 当前状态：`PROCESS`
>
> Owner：07-ai / 03-rust-web / control-plane / 01-tree-first-graph-kernel
>
> 参考项目：`/mnt/Data1T/mnote/reference-code/paperless-ngx`
>
> 参考版本：`f56f29111`
>
> CodeGraph：已单独建立，`712 files / 16,952 nodes / 35,407 edges`
>
> 上位依据：
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/CURRENT_ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/07-ai/process/7-46-document-evidence-retrieval-kernel-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/done/3-25-local-folder-mineru-ocr-sidecar-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`

## 1. 第一结论

Paperless-ngx 对 MNote 最有价值的不是 Django / Angular / Celery 技术栈，而是三套工程结构：

1. `PaperlessTask`：所有后台任务有统一账本，能记录来源、状态、耗时、输入、结果和用户是否已确认。
2. `consume_file` plugin pipeline：资源导入、预检、解析、OCR、存储、索引、通知按阶段推进，失败可以定位到阶段。
3. `Tantivy search backend + sanity checker`：索引有 schema 版本、权限字段、锁、延迟补偿和可重建性；文件与派生物有一致性检查。

MNote 应把这些模式收口成自己的 `Resource Work Kernel`：

```text
- local file / attachment / OCR source / parsed artifact
  -> ResourceWorkJob control-plane ledger
  -> ResourceIngestionPipeline stage runner
  -> sidecar artifact + source-map
  -> evidence.sqlite / local search projection
  -> realtime job events
  -> sanity check / recovery job
```

这不是要引入 paperless-ngx 的运行时。MNote 的数据真相仍然是 local Markdown、附件原文件、resource tree 和 control-plane；索引、OCR Markdown、parse Markdown、source-map 都是可删除重建的派生物。

## 2. Paperless-ngx 可借鉴点

### 2.1 统一后台任务账本

参考路径：

- `src/documents/models.py`：`PaperlessTask`
- `src/documents/signals/handlers.py`：Celery task publish / prerun / postrun / failure handlers
- `src-ui/src/app/components/admin/tasks/tasks.component.ts`

可借鉴点：

- 任务不是只存在于内存事件或前端状态，而是落库。
- 每个任务有 `task_type`、`trigger_source`、`status`、`date_created`、`date_started`、`date_done`、`duration_seconds`、`wait_time_seconds`、`input_data`、`result_data`、`acknowledged`。
- 系统任务和用户触发任务统一展示，但保留来源差异。

MNote 映射：

- 当前 `JobTicket`、OCR job、index refresh、agent run、OnlyOffice bridge 任务、recovery job 不应继续分散。
- 新增 control-plane 表 `resource_work_jobs`，先覆盖 local OCR / evidence parse / index refresh，后续再纳入 agent run 和 recovery job。

### 2.2 阶段化资源导入管线

参考路径：

- `src/documents/tasks.py`：`consume_file`
- `src/documents/plugins/base.py`：`ConsumeTaskPlugin`
- `src/documents/consumer.py`：解析、OCR、存储、索引、progress
- `src/documents/plugins/helpers.py`：`ProgressManager`

可借鉴点：

- 导入任务按插件链执行，每个 stage 有 `setup / run / cleanup`。
- 阶段状态通过 websocket 通知。
- 文件写入、数据库更新、索引更新分层处理，失败时能说明卡在预检、解析、写 sidecar 还是索引。

MNote 映射：

- 不引入插件框架泛化；先定义窄的 `ResourceIngestionPipeline`。
- stage 只覆盖当前真实需要：`preflight`、`parse_text`、`ocr`、`write_artifact`、`write_source_map`、`refresh_evidence_index`、`broadcast_done`。
- 当前 `local_ocr` 里的 stage 字符串和 sidecar 写入逻辑可以作为第一批迁移对象。

### 2.3 索引生命周期和自愈

参考路径：

- `src/documents/search/_backend.py`
- `src/documents/search/_schema.py`
- `src/documents/search/_query.py`
- `src/documents/tasks.py`：`index_document`、`remove_document_from_index`

可借鉴点：

- schema version sentinel 决定是否重建。
- 写索引用 file lock 和 retry，锁耗尽后排延迟任务，而不是让前台操作失败。
- 查询层有权限过滤、autocomplete、highlight、CJK bigram、simple search。

MNote 映射：

- `.mnote/index/evidence.sqlite` 当前已存在，但需要更明确的 schema sentinel 和 rebuild reason。
- `query_evidence_sqlite_results` 继续作为默认 evidence path；后续补充 autocomplete / CJK / highlight 时仍以 `EvidenceLocator` 为返回真相。
- 索引写失败不能悄悄丢失，应写入 `resource_work_jobs` 的 retry-scheduled 状态。

### 2.4 权限过滤的实时事件

参考路径：

- `src/paperless/consumers.py`
- `src/documents/plugins/helpers.py`

可借鉴点：

- websocket payload 带 owner / visible users / visible groups。
- server-side websocket consumer 根据当前用户过滤。

MNote 映射：

- local-only 阶段可以先只带 `workspaceId`、`actorId`、`rootUri`、`targetDocumentId`、`grantId`。
- 一旦进入 share / team workspace，OCR / index / agent job event 不能只按广播频道粗暴推送。
- `AiAccessScope` 和 share grants 应能映射成 job event 可见性字段。

### 2.5 Sanity checker

参考路径：

- `src/documents/sanity_checker.py`
- `src/documents/management/commands/document_sanity_checker.py`

可借鉴点：

- 独立检查原文件、派生文件、checksum、孤儿文件和空 OCR 内容。
- 输出按 error / warning / info 分级，既能 CLI 显示，也能作为后台任务结果。

MNote 映射：

- 新增 `workspace_sanity_check`，先检查 local-folder resource evidence：
  - Markdown owner 是否存在。
  - 附件路径是否存在且在 allowed root 内。
  - `{pageStem}.ocr/` sidecar 是否能回到 owner Markdown。
  - `*.source-map.json` 是否能解析为 `mnote.resource_source_map.v1`。
  - `evidence.sqlite` 是否能由 sidecar 重建。
  - evidence locator 的 `openAction` 是否能落回 document / resource tab。

## 3. 目标架构

### 3.1 ResourceWorkJob

新增 control-plane job ledger，不替代 domain event，也不替代 agent run receipt。

```rust
pub struct ResourceWorkJob {
    pub job_id: String,
    pub job_type: ResourceWorkJobType,
    pub trigger_source: ResourceWorkTriggerSource,
    pub status: ResourceWorkJobStatus,
    pub stage: Option<String>,
    pub progress_current: Option<u32>,
    pub progress_total: Option<u32>,
    pub stage_label: Option<String>,
    pub workspace_id: String,
    pub root_uri: String,
    pub actor_id: Option<String>,
    pub target_document_id: Option<String>,
    pub source_root_relative_path: Option<String>,
    pub artifact_root_relative_path: Option<String>,
    pub source_map_root_relative_path: Option<String>,
    pub input_json: serde_json::Value,
    pub result_json: Option<serde_json::Value>,
    pub error_code: Option<String>,
    pub error_message: Option<String>,
    pub created_at_ms: u128,
    pub started_at_ms: Option<u128>,
    pub finished_at_ms: Option<u128>,
    pub acknowledged_at_ms: Option<u128>,
}
```

首批枚举：

```text
job_type:
- local_ocr
- resource_parse
- evidence_index_refresh
- evidence_index_rebuild
- workspace_sanity_check

trigger_source:
- web_ui
- api
- watcher
- agent_tool
- system
- recovery

status:
- pending
- running
- succeeded
- failed
- retry_scheduled
- canceled
```

约束：

- `job_id` 由 MNote 生成，不复用外部 provider task id。
- MinerU task id、LiteParse request id、Reasonix run id 只进入 `input_json / result_json`。
- `result_json` 必须能存 evidence id、artifact path、source-map path、locator count、index row count。
- `stage / progress_current / progress_total / stage_label` 是前台任务中心的稳定合同；不能只塞在 provider 私有 payload 里。
- job ledger 是 control-plane 事实，不是用户正文真相。

### 3.2 ResourceIngestionPipeline

先实现窄接口，不做任意插件市场：

```text
preflight
  -> detect_resource_kind
  -> choose_provider
  -> parse_or_ocr
  -> write_artifact
  -> write_source_map
  -> refresh_evidence_index
  -> broadcast_job_event
```

Provider 策略：

- 文本型 PDF / Office parse 优先走 `LiteParseProvider` 或当前轻量 parser。
- 扫描件 / 图片走 `MinerUProvider`。
- mock provider 仅用于 smoke，不得在真实功能报告中冒充成功链路。

阶段状态：

```text
queued
preflight
parsing
ocr_uploading
ocr_processing
writing_artifact
writing_source_map
indexing
done
failed
retry_scheduled
stale
```

当前 `local_ocr.job.updated` 可兼容保留，但 payload 应逐步包含 `jobId / jobType / stage / workspaceId / rootUri / sourceRootRelativePath / targetDocumentId / artifactPath / sourceMapPath`。

### 3.3 Evidence Index Lifecycle

当前 `.mnote/index/evidence.sqlite` 继续作为默认 evidence index。升级点：

- 增加 `schema_version` 和 `build_settings` sentinel。
- 每次 write / refresh 记录 `job_id`。
- 索引 row 必须能通过 `source_map_root_relative_path` 回到 canonical artifact。
- lock 失败进入 `retry_scheduled` job，不直接吞掉。
- watcher 增量刷新失败时，不做前端轮询补偿；排 recovery job 并广播一次明确事件。

建议最小表：

```sql
CREATE TABLE IF NOT EXISTS evidence_index_meta (
  key TEXT PRIMARY KEY,
  value_json TEXT NOT NULL,
  updated_at_ms INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS evidence_index_jobs (
  job_id TEXT PRIMARY KEY,
  last_error_code TEXT,
  last_error_message TEXT,
  updated_at_ms INTEGER NOT NULL
);
```

如果 control-plane 已存 job，这里只保存 index-local rebuild metadata，不重复完整 job 账本。

### 3.4 Workspace Sanity Check

新增只读检查，不自动删除、不自动修复。

检查项：

| 级别 | 检查 | 说明 |
| --- | --- | --- |
| error | owner Markdown 不存在 | evidence locator 无法打开 |
| error | resource file 不存在 | 附件或 OCR source 丢失 |
| error | source-map JSON 无法解析 | 定位真相损坏 |
| error | evidence.sqlite schema 不匹配 | 需要 rebuild |
| warning | sidecar 孤儿文件 | 有 OCR/parse artifact 但找不到 owner link |
| warning | source hash / mtime stale | 可重建，但不阻断 |
| info | parse/OCR 内容为空 | 允许，但要可见 |

输出既可作为 API：

```text
POST /api/workspaces/sanity/check
GET /api/work/jobs/:jobId
```

也可作为 smoke / CLI helper 的 JSON 结果。

### 3.5 Unified Task Foreground UI

统一后台任务必须有统一前台可见入口。否则用户仍然只能在 OCR 设置、索引设置、toast、局部状态灯之间猜系统是否还在运行。

当前已有的 `mnote-local-ocr-task-dock` 是迁移起点，不是长期终点。它应升级为全局 `Work Task Center`：

```text
Topbar task button
  -> badge: active / failed / needs attention count
  -> drawer: current jobs + recent completed + failed
  -> row: icon + title + stage label + progress bar + actions
  -> detail: input/result/error + related document/resource + trace/run receipt
```

入口位置：

- 顶栏保留一个任务中心图标，建议使用 `progress_activity` 或 `pending_actions`。
- OCR 和索引设置按钮仍可保留，但它们不再各自承载任务列表。
- 任务中心抽屉优先靠右打开，保持轻量；不做全屏管理台作为第一阶段。

任务行最小字段：

| 字段 | 来源 | UI 用途 |
| --- | --- | --- |
| `jobId` | job ledger | 稳定 row key 和详情查询 |
| `jobType` | job ledger | 图标、分类、筛选 |
| `status` | job ledger | 颜色、分组、是否需要确认 |
| `stageLabel` | job event | 当前阶段文案 |
| `progressCurrent / progressTotal` | job event | 确定性进度条 |
| `createdAtMs / startedAtMs / finishedAtMs` | job ledger | 排队/耗时/最近完成 |
| `targetDocumentId` | job ledger | 打开 owner 文档 |
| `sourceRootRelativePath` | job ledger | 打开附件或定位资源 |
| `artifactRootRelativePath / sourceMapRootRelativePath` | result | 打开 OCR/parse/source-map |
| `errorCode / errorMessage` | job ledger | 失败摘要和复现证据 |

进度规则：

- 有 `progressCurrent / progressTotal` 时显示确定性进度条。
- 没有总量但 status 为 running 时显示细条 indeterminate，不伪造百分比。
- `queued / retry_scheduled` 显示排队态，不显示假进度。
- `failed` 和 `retry_scheduled` 必须进入“需要处理”计数。
- `succeeded` 默认保留在最近完成列表，可由用户清除/ack。

任务动作：

| 状态 | 动作 |
| --- | --- |
| running | 打开目标、查看详情 |
| succeeded | 打开结果、打开目标、清除 |
| failed | 查看错误、重试、打开目标、复制错误摘要 |
| retry_scheduled | 查看重试原因、立即重试、取消重试 |
| stale | 重新生成、打开旧结果 |

事件与数据流：

```text
GET /api/work/jobs?scope=currentWorkspace&active=true
GET /api/work/jobs?scope=currentWorkspace&recent=true
GET /api/work/jobs/:jobId
POST /api/work/jobs/:jobId/ack
POST /api/work/jobs/:jobId/retry
SSE/WS event: resource_work.job.updated
```

前端运行时：

- 新增 `browser/work-task-center-runtime.js`，由 `layout.rs` 统一注入。
- 现有 `document-resource-tab-runtime.js` 中 OCR task dock 的 state/render 逻辑迁入该 runtime。
- `mnote:local-ocr-job-updated` 作为兼容事件继续转发为 `resource_work.job.updated`，直到后端统一 payload 完成。
- 前端只在启动时拉一次 active/recent snapshot；后续靠 WS/SSE 事件更新，不新增周期轮询。

视觉约束：

- 顶栏只显示一个任务中心 badge，避免 OCR/索引/AI 各自占顶栏状态位。
- 抽屉行要密集、可扫描，不能做大卡片堆叠。
- 每行必须有可见进度或阶段文本，长路径截断但 title 保留完整路径 tooltip。
- 移动端抽屉宽度占满可用宽度，任务行按钮折到第二行，避免文字溢出。

## 4. 不做什么

- 不把 paperless-ngx 的 Django model / Angular UI / Celery worker 引入 MNote。
- 不把 OCR Markdown、parse Markdown 或 evidence.sqlite 变成正文真相。
- 不新增前端轮询来弥补 job 状态；状态更新走现有 WS / SSE / watcher event。
- 不把任务 UI 继续拆成 OCR 一套、索引一套、AI 一套；这些只能是任务中心里的分类或过滤。
- 不让 workflow UI 先行。先做 event-triggered job 和少量内置动作，再考虑可视化配置。
- 不在本轮实现向量 RAG。Paperless 的 FAISS append-only 方案只作为反例和参考，不作为 MNote 默认路径。

## 5. 实施分期

### Phase A：设计冻结和接口补齐

Owner：07-ai / 03-rust-web

任务：

- [ ] 在 `core-protocol` 增加 `ResourceWorkJob` / `ResourceWorkJobStatus` / `ResourceWorkTriggerSource` 合同。
- [ ] 在 job 合同中加入 `stage / stageLabel / progressCurrent / progressTotal`，作为前台任务中心稳定字段。
- [ ] 明确 `local_ocr.job.updated` 与新 `resource_work.job.updated` 的兼容关系。
- [ ] 在 `7-46` 里引用本设计作为 job / index lifecycle 的执行补充。

验收：

- [ ] Rust unit 覆盖 job status 序列化。
- [ ] Rust unit 覆盖 progress 字段缺省、确定性进度和 indeterminate 语义。
- [ ] 不改变现有 OCR smoke 行为。

### Phase B：OCR job ledger 收口

Owner：03-rust-web / control-plane

任务：

- [ ] 给 `local_ocr::create_job` 创建 control-plane job 记录。
- [ ] 每次 stage advance 同步 job ledger。
- [ ] `done / failed` 写入 `finished_at_ms / result_json / error_code`。
- [ ] 增加 `GET /api/work/jobs/:jobId`。
- [ ] 增加 `GET /api/work/jobs?active=true&recent=true`，供任务中心首屏 snapshot 使用。

验收：

- [ ] 现有 OCR sidecar smoke 仍通过。
- [ ] 新增 smoke 验证 OCR job 可查询、失败错误脱敏、done 后 result 包含 sidecar 和 source-map。
- [ ] 新增 smoke 验证 active snapshot 包含运行中 OCR job，完成后转入 recent。

### Phase B2：统一任务中心 UI

Owner：03-rust-web / 05-editor-mainline

任务：

- [ ] 新增 `browser/work-task-center-runtime.js`。
- [ ] 将 `mnote-local-ocr-task-dock` 的状态聚合和 drawer 渲染迁移到 work task center。
- [ ] 顶栏新增统一任务中心按钮和 badge，OCR 按钮回归 OCR 设置入口或合并进设置面板。
- [ ] 支持 active / needs attention / recent 三个分组。
- [ ] 任务行支持确定性 progress bar、indeterminate running bar、失败重试、打开目标、打开结果、ack。
- [ ] 兼容接收 `mnote:local-ocr-job-updated`，并在后端统一事件上线后接 `resource_work.job.updated`。

验收：

- [ ] 浏览器 smoke：触发 OCR 后顶栏 badge 从 0 变 1，抽屉显示阶段和进度。
- [ ] 浏览器 smoke：OCR 完成后任务移入 recent，能打开 OCR sidecar。
- [ ] 浏览器 smoke：mock 失败任务进入 needs attention，能查看错误和重试。
- [ ] 截图验证桌面和移动端抽屉不溢出、不遮挡主编辑区关键内容。

### Phase C：Evidence index lifecycle

Owner：07-ai / 03-rust-web

任务：

- [ ] 给 evidence.sqlite 增加 schema/settings sentinel。
- [ ] index refresh 写入 job id 或 index-local job metadata。
- [ ] lock / write / parse source-map 失败时进入 retry-scheduled 或 failed job。
- [ ] `mnote.index.status` 返回 schema、last build、last job、pending retry。
- [ ] index refresh / rebuild 通过任务中心显示阶段和结果摘要。

验收：

- [ ] `task528-document-evidence-liteparse-agent-smoke.js` 继续通过。
- [ ] 新增 index stale / rebuild smoke，验证删除 evidence.sqlite 后可通过 job 重建。
- [ ] 浏览器 smoke 验证 index rebuild 任务出现在任务中心。

### Phase D：Workspace sanity check

Owner：03-rust-web / 04-tree-domain

任务：

- [ ] 实现只读 `workspace_sanity_check` job。
- [ ] 检查 owner Markdown、resource file、sidecar、source-map、evidence.sqlite。
- [ ] 结果按 error / warning / info 分级。
- [ ] 从任务中心和 index/OCR 设置 surface 都能启动检查；结果统一进入任务中心详情。

验收：

- [ ] smoke 构造缺失 source-map、孤儿 sidecar、损坏 sqlite，能得到稳定 JSON。
- [ ] 不删除、不移动任何用户文件。
- [ ] 浏览器 smoke 验证 sanity check 运行时有任务行，完成后详情展示 error / warning / info 摘要。

### Phase E：内置 workflow actions

Owner：01-tree-first-graph-kernel / 07-ai

任务：

- [ ] 定义内置事件：`resource.created`、`resource.changed`、`ocr.done`、`evidence.index.failed`。
- [ ] 定义内置动作：`parse_resource`、`run_ocr`、`refresh_evidence_index`、`schedule_sanity_check`。
- [ ] 先用静态配置或 settings 控制，不做复杂 UI。

验收：

- [ ] 新增附件后能按 settings 自动排 parse/OCR/index job。
- [ ] 失败不会循环重试；必须有 retry budget 和可见错误。

## 6. 设计验收基线

本设计完成后，MNote 应具备以下能力：

- 用户能看到后台 OCR / parse / index / sanity job 的真实状态，而不是只看到散落的 toast 或局部状态灯。
- 顶栏任务中心能显示 active / failed / recent 任务，且所有任务进度来自 job ledger 或 job event，不伪造百分比。
- agent 调用 evidence 工具时，MNote 能把本次证据索引状态、evidence ids、source-map path 写入 run receipt。
- 删除或损坏 evidence.sqlite 不会让系统静默退化；会显示 rebuild required 或排 rebuild job。
- OCR sidecar、parse artifact、source-map 和 evidence locator 可以被 sanity check 证明互相可追溯。
- 多人 / share 场景下，job event 有明确可见性字段，不把本地单用户广播模型固化成长期事实。

## 7. 参考代码索引

Paperless-ngx：

- `src/documents/models.py`：`PaperlessTask`、workflow model、document version fields。
- `src/documents/tasks.py`：`consume_file`、index deferred tasks、bulk update。
- `src/documents/consumer.py`：resource consume main path。
- `src/documents/plugins/base.py`：plugin lifecycle contract。
- `src/documents/plugins/helpers.py`：progress websocket payload。
- `src/documents/search/_backend.py`：Tantivy backend、lock retry、autocomplete、highlight。
- `src/documents/search/_schema.py`：schema version sentinel。
- `src/documents/search/_query.py`：permission filter、date rewrite、CJK/simple query。
- `src/documents/sanity_checker.py`：archive consistency checker。
- `src/paperless/consumers.py`：permission-aware websocket consumer。

MNote 当前落点：

- `rust/crates/core-protocol/src/governance.rs`
- `rust/crates/core-protocol/src/evidence.rs`
- `rust/crates/core-protocol/src/tool.rs`
- `rust/crates/mnote-web/src/routes/local_ocr.rs`
- `rust/crates/mnote-web/src/routes/local_search_index.rs`
- `rust/crates/mnote-web/src/routes/evidence.rs`
- `rust/crates/mnote-web/src/routes/local_folder_events.rs`
- `rust/crates/mnote-web/src/routes/ws.rs`
- `scripts/task528-document-evidence-liteparse-agent-smoke.js`
