# 03. Storage / Event / Indexing v0

更新时间：2026-04-11
适用范围：`/mnt/Data1T/mnote-rust`

---

## 1. 目标

本文件定义 `mnote-rust` 在当前路线下的持久化、事件日志、索引体系。

它要回答：

- 当前系统事实层应落在哪里
- Rust 内核怎样接入既有事实层而不重复造库
- 写操作如何进入事件链路
- 搜索、引用、RAG、回放、审计依赖什么数据面
- 如何既保证性能，又保证 AI 可观测、可维护、可回放

这部分的前提必须先讲清：

> 对当前 `mnote` / `mnote-next` 主线来说，**Convex 仍是主事实层**。

因此，这份文档不再讨论“用 SQLite 取代 Convex 做主库”，而是讨论：

- 如何在 **Convex 主事实层** 之上建立 Rust 内核
- 如何把索引、事件、导出、离线缓存、本地处理组织清楚
- 如何避免再次引入第二套事实真相

如果这层设计不好，系统最终仍会退化成：

- 页面组件自己维护真相
- 各类能力各写各的缓存
- AI 看不到完整上下文
- Rust 内核与现有系统各管一套数据
- 回滚、审计、重建索引都变得困难

---

## 2. 总原则

### 2.1 主事实层、事件日志、索引必须分层

三者职责不同：

- **主事实层**：保存系统当前真相
- **事件日志**：保存“如何变成现在”的过程
- **索引层**：为检索、聚合、推荐、RAG 提供加速读模型

禁止让任一层越权：

- 不能拿索引当真相
- 不能只靠事件流而没有可直接读取的当前状态
- 不能让页面缓存成为事实层
- 不能让本地缓存演化成第二主库

### 2.2 当前主事实层应继续落在 Convex

基于 `mnote` 与 `mnote-next` 已有主线，当前阶段应坚持：

- **Convex 是唯一主事实层**
- Rust 内核通过协议与 bridge 接入 Convex，而不是绕开它再建一套主库
- 旧仓与新仓共享的核心对象，应优先复用既有 Convex schema / deployment / caller 体系

这不是保守，而是避免推翻已经跑通的主链路。

### 2.3 本地文件系统与本地嵌入式存储仍然重要

虽然主事实层继续用 Convex，但本地层仍然需要：

- **文件系统**：资产原文件、导出文件、缓存、派生产物、临时工作目录
- **可选本地嵌入式存储**：离线缓存、索引快照、dry-run、调试态数据、临时队列

但这些都不能升级为新的主事实层。

### 2.4 写入必须原子化到“主事实层 + 事件落账”

一次命令成功后，至少要保证：

- 主事实层写入完成
- 事件写入成功
- 命令日志有记录

索引允许异步追平，但必须可检测 lag。

### 2.5 索引必须可重建

搜索索引、向量索引、聚合视图都必须满足：

- 可从主事实层 + 事件重新构建
- 可做全量重建
- 可做增量追平

否则后期会不可维护。

---

## 3. 三层数据面

```text
[主事实层]
Convex + FileSystem

[事件与日志层]
CommandLog + DomainEvent + JobLog + AuditTrail

[索引与派生层]
FTS / Reference Graph / Backlink View / Outline View / Vector Index
```

### 3.1 主事实层负责

- Workspace / Page / Block / Asset / Reference / Task / AgentSession 当前状态
- 事务性写入
- 读取当前真相
- 提供稳定 schema
- 承担权限与主对象约束

### 3.2 事件与日志层负责

- 记录命令与事件
- 记录 actor、reason、scope、结果
- 支持回放、审计、问题追踪
- 作为索引增量更新输入

### 3.3 索引与派生层负责

- 全文搜索
- 页面大纲
- 反链
- 资产片段搜索
- RAG 检索读模型
- 推荐 / 关系聚合

---

## 4. 主事实层选型

## 4.1 当前阶段的明确结论

推荐：

- **Convex**：结构化主事实层
- **File System**：存原始资产与大对象
- **Rust bridge / protocol layer**：作为统一命令、查询、工具接入面

原因：

- 与 `mnote` 中“Convex 已完全替换 Supabase”的现实一致
- 与 `mnote-next` 中“默认复用现有 Convex deployment / project”的路线一致
- 复用现有 deployment、schema、自建与 AI 开发经验，避免重建第二套数据库主线
- 更符合“复用成熟能力，重构边界，不做无意义重建”的迁移原则

## 4.2 本地嵌入式存储的正确定位

可以保留本地嵌入式存储，但只限于：

- CLI / Agent 的离线缓存
- 全文索引引擎的本地数据文件
- dry-run / scaffold / 调试态数据
- 单机导入处理中的临时工作库

不能把它写成：

- 第二套主事实层
- 与 Convex 并列的正式写入真相
- 长期双写的核心业务库

## 4.3 不建议的中心方案

- 用 SQLite 取代 Convex 成为主真相
- OnlyOffice / 第三方编辑器内部状态作为主真相
- 仅事件溯源、无当前态表
- 单纯 Markdown 文件散落 + 大量 sidecar 作为唯一结构源
- 长期维护 Convex + SQLite 双事实源

这些都会让系统再次陷入边界混乱。

---

## 5. 主事实对象结构建议

这里讨论的是**领域对象结构**，不是要求新建第二套数据库。

## 5.1 结构原则

- 主对象必须能映射到现有 Convex schema
- 单对象一组稳定字段或有限关联表
- 避免过度 EAV
- JSON 仅用于局部扩展字段，不替代主 schema

## 5.2 建议主对象

至少包含：

- `workspaces`
- `pages`
- `page_versions`（可选）
- `blocks`
- `block_relations`（可选，若不全放在 block 表中）
- `assets`
- `asset_versions`
- `references`
- `tasks`
- `agent_sessions`
- `command_logs`
- `domain_events`
- `jobs`
- `job_logs`

这些对象应优先映射或扩展到现有 Convex 主线，而不是在新仓先落一套平行本地表。

## 5.3 blocks 对象建议

关键字段：

- `block_id`
- `workspace_id`
- `page_id`
- `parent_block_id`
- `sort_key`
- `block_type`
- `content_json`
- `props_json`
- `annotations_json`
- `revision`
- `deleted_at`

说明：

- `sort_key` 建议支持稀疏排序，避免频繁全量重排
- 软删除优于直接硬删，方便审计与恢复
- `revision` 用于乐观并发控制

## 5.4 assets 对象建议

- `asset_id`
- `workspace_id`
- `asset_kind`
- `mime_type`
- `current_version_id`
- `storage_strategy`
- `status`
- `metadata_json`

## 5.5 references 对象建议

- `reference_id`
- `source_object_type`
- `source_object_id`
- `target_object_type`
- `target_object_id`
- `anchor_json`
- `ref_kind`
- `status`

---

## 6. 文件系统布局建议

资产不要全塞数据库 blob。

建议：

```text
workspace-data/
├─ assets/
│  ├─ asset_xxx/
│  │  ├─ v1/original.docx
│  │  ├─ v2/original.docx
│  │  ├─ extracted/outline.json
│  │  ├─ extracted/text.md
│  │  └─ derived/preview.png
├─ indexes/
│  ├─ fts/
│  └─ vector/
├─ runtime/
│  ├─ jobs/
│  ├─ sessions/
│  └─ temp/
└─ export/
```

原则：

- 结构化真相仍以 Convex 为主
- 原始大文件入文件系统
- 派生产物有明确目录归属
- 临时文件与正式资产分离

---

## 7. Command Log

`CommandLog` 是所有写命令的正式记录。

### 7.1 最低字段

- `command_log_id`
- `command_name`
- `actor_type`
- `actor_id`
- `source`
- `workspace_id`
- `target_objects`
- `payload_summary`
- `refs_json`
- `idempotency_key`
- `status`
- `created_at`
- `finished_at`

### 7.2 设计要求

- 能关联到一次真实主写入
- 能关联后续 domain events
- 能关联 tool call / job / rollback
- 能为 AI 回放与人类审计提供最小闭包

### 7.3 与 Convex 的关系

命令日志可以：

- 直接进入 Convex 主线对象
- 或通过 bridge 落到与主对象同一事实层

但不要单独把命令日志只记在本地、主对象却记在远端；那会破坏统一审计链路。

---

## 8. Domain Event

`DomainEvent` 不是为了炫技，而是为了：

- 给索引层提供标准增量输入
- 给回放和审计提供结构化事件
- 给异步任务提供稳定订阅源

### 8.1 事件最低字段

- `event_id`
- `workspace_id`
- `aggregate_type`
- `aggregate_id`
- `event_type`
- `event_version`
- `payload_json`
- `command_log_id`
- `actor_type`
- `created_at`

### 8.2 事件边界

建议记录领域事件，而不是 UI 手势。

例如：

- `page_created`
- `page_renamed`
- `block_inserted`
- `block_moved`
- `block_content_replaced`
- `asset_version_added`
- `reference_created`
- `task_status_changed`

不建议记录：

- 弹窗打开
- 光标移动
- hover 展示
- 面板折叠

---

## 9. Job 与异步链路

不是所有事情都应进主事务。

应把这些放入 Job：

- OCR
- OnlyOffice 提取/转换
- 向量切片与嵌入
- 全量重建索引
- 大文件导入
- 外部同步

### 9.1 Job 最低字段

- `job_id`
- `job_type`
- `workspace_id`
- `target_objects`
- `input_json`
- `status`
- `progress`
- `error`
- `created_at`
- `started_at`
- `finished_at`

### 9.2 不能放进主事务的内容

- 大模型推理
- OCR
- 大文件转换
- 向量重建
- 远程同步

这些必须走 Job。

---

## 10. 索引体系

## 10.1 Full Text Search

最低必做：

- 页面标题全文检索
- 块内容全文检索
- 资产提取文本全文检索
- 引用 snippet 检索

建议：

- 初期直接用本地全文索引引擎（如 SQLite FTS5、Tantivy 或兼容方案）
- 索引文档单位统一为“可定位对象”

例如：

- page
- block
- asset_fragment
- reference_snippet

### 10.2 为什么块级索引重要

因为你的产品最终不是“整篇文档搜索”，而是：

- 找到某一段
- 跳回块位置
- 让 AI 只编辑局部
- 给引用与上下文最小闭包

## 10.3 Reference Graph

必须维护引用图读模型：

- page -> page
- block -> block
- block -> asset fragment
- task -> source object

这对：

- 反链
- 影响面分析
- AI 上下文组装
- 知识导航

都很关键。

## 10.4 Outline View

页面大纲、Office 文档提取大纲、PDF 目录、Mindmap 树都应是派生读模型。

优点：

- 不污染主事实层
- 允许多种提取策略
- 容易重建

## 10.5 Vector Index

向量索引建议延后，但结构上预留。

原则：

- 向量索引只是检索加速层
- 向量 chunk 必须能回指 `page_id / block_id / asset_id / anchor`
- 向量索引失效可重建，不影响系统主真相

---

## 11. 索引更新策略

## 11.1 起步建议：异步增量 + 可全量重建

每次事件提交后：

- 生成索引任务
- 按对象粒度增量更新
- 记录 lag 与最后处理 event id

## 11.2 索引一致性要求

- 主事实层一致性 > 索引实时性
- 查询可返回“索引处理中”状态
- 对必须强一致的局部场景，可同步刷新小范围索引

## 11.3 失败恢复

索引 worker 挂了也不能影响主写入。

必须支持：

- 从 `last_processed_event_id` 继续追平
- 指定对象重建
- 指定 workspace 全量重建

### 11.4 回放与索引重建的正式入口

从 task-034 开始，回放和重建不再只是“库里有个纯函数”，而要成为统一命令面的一部分。

最小要求：

- `event_replay`：从统一事件列表读取，输出 replay 后的 `cursor` 与受影响 event 摘要
- `index_rebuild`：基于 `last_processed_event_id` / `last_processed_at` 重建索引批次，并返回新的 cursor
- 两者都复用同一套 `request_id`、`trace_id`、`workspace_id`、`command_id` 语义，不允许临时脚本私有解释

这意味着后续无论是 CLI、AI 还是 Web 排障，都应先调用同一条 Rust 恢复命令面，而不是各自写一套索引补数脚本。

---

## 12. AI 可观测性

AI 全流程介入后，系统必须能回答：

- 某次回答引用了哪些对象
- 某次编辑基于哪些上下文
- 某个工具为什么失败
- 当前搜索索引是否滞后
- 哪些对象正在被长任务处理

因此建议额外维护：

### 12.1 AgentSession 关联表

- `agent_session_objects`
- `agent_session_tool_calls`
- `agent_session_outputs`

### 12.2 Tool Call Log

记录：

- tool name
- input
- output summary
- duration
- error
- command_log_id / query trace id

这样后期 AI 排障、回放、评估都会容易很多。

---

## 13. 性能关键点

### 13.1 不要整页全文重写

编辑块时，只更新相关块与局部派生视图。

### 13.2 不要同步重建全索引

任何全库重建都必须后台化。

### 13.3 资产处理走异步

OCR、Office 转换、预览图生成、向量切片都不能阻塞主编辑事务。

### 13.4 读写模型分离

高频 UI 读需求可通过派生视图优化，不要污染主事实 schema。

---

## 14. OnlyOffice 在存储层的正确位置

OnlyOffice 相关对象建议这样落地：

- 原始 `docx/xlsx/pptx/pdf`：作为 `Asset` + `AssetVersion`
- 文档编辑结果：生成新 `AssetVersion`
- 提取结构（目录、批注、书签、文本片段）：作为派生读模型或 `Reference`
- 当前选区、当前页码：作为短生命周期 `SelectionAnchor`

不要把：

- OnlyOffice 内部文档状态
- 插件瞬时 UI 状态
- callback 的临时 payload

直接当成主事实层。

---

## 15. 迁移建议

先做：

1. 盘点现有 Convex schema 与 `mnote-rust` 领域对象的映射
2. 落 `CommandLog / DomainEvent` 的主事实层方案
3. 落 `Page / Block / Asset / Reference` 的 bridge / repo 接口
4. 建立全文索引最小实现
5. 建立索引 worker 骨架

再做：

6. AgentSession / ToolCallLog
7. Outline / Backlink 读模型
8. 向量索引占位
9. Office / OCR / Mindmap 派生索引
10. 必要的离线缓存 / dry-run 本地层

---

## 16. 一句话结论

`mnote-rust` 当前应采用 **Convex 主事实层 + FileSystem 资产存储 + CommandLog/DomainEvent 事件链路 + 可重建全文/引用/向量索引** 的分层结构；
Rust 内核负责把命令、查询、工具、索引、异步任务与外部能力重新组织清楚，而不是再造一套与现有主线并列的数据库事实源。
