# 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 内核负责把命令、查询、工具、索引、异步任务与外部能力重新组织清楚,而不是再造一套与现有主线并列的数据库事实源。