对齐 Wolai 侧栏体验并收拢设计入库

This commit is contained in:
lix-2026
2026-04-30 16:18:54 +08:00
parent 8c895b3dc0
commit afb2a5b8a0
89 changed files with 23188 additions and 84 deletions
+13 -1
View File
@@ -49,7 +49,6 @@ wolai-frontend/public/documents/**
artifacts/
artifacts/**
tmp
design
# Rust 本地构建与调试产物
/rust/target/
@@ -65,3 +64,16 @@ design
.playwright-mcp
rust/spikes/leptos-tiptap-spike/trunk-8123.err
rust/spikes/leptos-tiptap-spike/trunk-8123.out
design/05-editor-mainline/reference-code
# 本地 Wolai 对标取证与截图产物
/test-results/
/wolai-*.png
/wolai-*.md
# 任务型浏览器 smoke 只作本地验收,不纳入项目运行面
/scripts/task*-wolai-*.js
/scripts/task*-rust-web-wolai-*-smoke.js
# 下载的 Wolai 静态页面参考包,不作为可维护设计稿入库
design/design/html/
+12
View File
@@ -87,6 +87,18 @@
- 影响主页入口、Sidebar、tree shell、文档页首屏时,优先补或复用 `scripts/task*-smoke.js` 这类 smoke 脚本。
- 排查高 CPU / 高内存 / 卡顿时,先看是否存在首屏误走实验性 tree shell、compat fallback、重复请求、轮询或回链面板持续刷新,再看数据底座。
## Wolai-aline 对标流程
- 凡任务涉及 `wolai-aline`、Wolai 对标、复刻 Wolai 体验或把 `3000` 行为与 Wolai 页面比对,必须启用 `/home/lix/.codex/skills/wolai-aline` skill,并参考 `/mnt/Data1T/mnote/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md`
- Wolai-aline 任务默认采用“Wolai 基线取证(默认只读,编辑器任务可用 Hermes editable-test mode-> 本地 RED smoke -> 小范围实现 -> 本地验证 -> subagent 浏览器对标复测 -> 主线程截图复核 -> 汇报剩余差异”的流程。
- 浏览器对标测试必须使用 subagent 执行;subagent 只做浏览器验证和截图,不修改源码、不还原文件、不清理证据;编辑器任务可在明确声明的 Hermes editable-test mode 下做最小编辑验证。
- Wolai 默认优先只读;当前 Hermes 测试页 `https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd` 已授权用于 Wolai-aline 编辑器对标,可做最小范围编辑测试。其他 Wolai 页面写入仍需沙盒页 URL 和明确授权。
- 即使在 Hermes 测试页,也禁止未经确认地删除、移动、归档、发布、评论、改权限或批量修改既有非测试内容;编辑测试必须记录前后截图、动作链、输入内容和清理状态。
- 对标验收不能只看文案或 DOM 是否存在;必须检查截图中的控件形态、开启/关闭状态、hover/active 状态、快捷键行为、URL 是否跳转等真实体验差异。
- 发现截图或实测行为与本地实现不一致时,先把差异补进 smoke 形成可复现失败,再修改实现并复测。
- Wolai owner 登录态优先复用 `/mnt/Data1T/mnote/tmp/wolai-playwright-profile`;遇到登录或滑块验证,不绕过,记录阻塞并让用户介入。
## 文件编码与风格
- 所有新增或修改文件统一使用 UTF-8。
+113
View File
@@ -0,0 +1,113 @@
# 01-05 当前主线与优先级总览
> 更新时间:2026-04-22
这份总览只做一件事:
> **给 `design/01-*` 到 `design/05-*` 当前仍然有效的主线稿排优先级,并明确哪些 `process` 已经过时或应降级为历史参考。**
## 1. 当前仍然有效的上位主线
下面三份仍然是当前架构判断的上位依据:
- `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
- `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
- `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
它们分别固定了三件事:
1. 长期事实源是 `tree-first graph kernel`
2. `Convex` 保留为当前自托管存储 / 实时底座,不先拆
3. `mnote-web` 是 Rust Web 承载层,长期继续承担 transport、projection 分发与切流
## 2. 当前第一优先级
当前最优先的不是继续扩 UI,也不是继续大规模重写执行面,而是把页面域和树域的真相边界先收口。
### 2.1 Page Aggregate
当前第一优先级固定为:
- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
- `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
原因:
- 默认主编辑器已经切到页面内 `leptos-tiptap` island
- 标题单一真源已经开始收口
- 文档页入口已经消费前端侧 `PageAggregateProjection`
- 但 Rust 侧还没有原生 `Page Aggregate` 契约
- 标题 / 正文 / 页面设置仍未统一成同一组 page aggregate command family
### 2.2 Tree Command Cutover
当前第二优先级固定为:
- `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
原因:
- 前端已经开始以 `tree.*` 作为 preferred command name
- 但 route / adapter / bridge / CLI 仍保留大量 `documents.*`
- 如果不先完成命令面统一,后续 page aggregate 与 tree realtime 都会持续停留在兼容双轨
### 2.3 Tree Realtime 主链
当前第三优先级固定为:
- `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
原因:
- 当前 sidebar 已有 query snapshot、tree stream、preferred snapshot 选择链
- 但正式的 snapshot + delta 主链还没有完全成为唯一实时事实来源
- 这会持续带来 refetch 补偿、freshness 选择和旧快照回闪问题
## 3. 当前仍应保留但不在第一线的 process
下面这些文档仍然有效,但当前不应排在第一优先:
- `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
- `/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`
- `/mnt/Data1T/mnote/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
- `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-2-tiptap-notion-like-template-adoption-v1.md`
- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md`
保留原因:
- 方向仍对
- 仍然是局部主线的有效合同
- 但它们不应压过 `Page Aggregate``tree command``tree realtime` 三条当前主战线
## 4. 当前已降级为历史参考的 05 主线稿
下面这些稿件已被后续实现和更新文档覆盖,不再作为当前活跃 `process`
- `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md`
- `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/5-3-tiptap-leptos-rust-migration-checklist-v1.md`
- `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/5-editor-baseline-reset-v2.md`
- `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-1-sidebar-pagetree-filetree-product-gap-analysis-v1.md`
降级原因:
- 它们的重要判断已经被吸收到 `5-4 / 5-5 / 5-6`
- 文档中的事实基线已经落后于当前默认主编辑器、page aggregate 入口与 island 主链
- 继续保留在活跃 `process` 容易让后续工作按旧阶段推进
## 5. 当前执行顺序
当前推荐顺序固定为:
1. `Page Aggregate`
2. `Tree Command Cutover Stage 2`
3. `Tree Realtime Event Stream`
4. 树域产品交互合同补齐
5. 编辑器官方模板行为对齐
## 6. 一句话收口
当前 `design/01-05` 的真实主线,不是“继续证明 Rust Web 值不值得做”,也不是“继续证明 `leptos-tiptap` 能不能用”,而是:
> **先把页面域和树域收口到 Rust 主导的单一真源,再推进 tree realtime 和树域产品执行面。**
@@ -0,0 +1,719 @@
# 1 [process] Tree-First Graph 内核方案 v1
> 更新时间:2026-04-22
>
> 当前优先级入口:
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
## 1. 文档目的
本文回答的不是“页面如何加速”,而是更底层的问题:
> **mnote 长期到底应该以什么作为统一对象内核。**
在前一轮讨论里,方向经历了一个重要修正:
- 不是让 `Mindmap` 成为系统主投影
- 也不是让“导图页面”变成新的系统中心
- 而是让 **`tree-first graph` 成为统一结构内核**
- `Mindmap` 只是这个内核的一种可视化挂件
这份文档的目标,是把这个判断正式固定下来。
---
## 2. 先给结论
结论只有一句:
> **mnote 的长期核心不应是 `BlockNote-first`,也不应是 `Mindmap-first`,而应是 `tree-first graph kernel`。**
也就是说:
- **树** 是主骨架
- **图** 是横向引用扩展
- **Mindmap** 是树/图的一种空间化视图
- **Sidebar / 页面树 / 文件树 / 文档阅读页 / 搜索结果 / AI 面板** 都只是同一内核的不同投影
因此,长期正确方向不是:
- “把所有东西都画成导图”
而是:
- “让所有对象共享同一份结构内核,再让不同视图各自投影”
---
## 3. 为什么不是 Mindmap-first
虽然 Mindmap 在结构表达上很强,但它不适合被定义为主中心。
原因有四个。
### 3.1 导图 UI 不是所有场景的最佳交互
下面这些场景不适合被强行导图化:
- Sidebar 导航
- 文件树浏览
- 文档阅读
- 搜索结果浏览
- 历史版本查看
- AI 工具面板
这些场景里,很多时候:
- 列表更合适
- 树表更合适
- 阅读流更合适
- 搜索结果卡片更合适
所以:
> **导图是一种强表达能力的视图,不是所有结构都应默认进入的主视图。**
### 3.2 如果 Mindmap 成为主中心,系统会被导图交互绑架
一旦把 Mindmap 当主投影,后面很容易出现:
- 结构建模被导图控件的数据形状反向约束
- 页面/文件/章节/引用都被迫适配导图编辑器
- 视图层规则污染对象层规则
这会让“结构内核”被“某种 UI 控件”夺走主导权。
### 3.3 当前本地代码已经说明 Mindmap 更像重操作壳
当前:
- [`MindmapBlock.tsx`](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx)
同时承担:
- 内嵌块
- 独立页
- 工具栏
- 右键菜单
- 导航器
- 缩略图
- 本地全屏视图
这说明它当前的本质更接近:
- 一个前端重交互操作层
而不是:
- 一个稳定、极简、内核级结构模型
### 3.4 Mindmap 已经适合退到“挂件层”
真正更合理的位置是:
- 它继续保留
- 但作为 `tree-first graph kernel` 的挂件和投影
- 而不是事实源
---
## 4. 为什么是 Tree-First Graph
这是因为 mnote 当前最稳定、最通用、最可渐进迁移的共同语义,本质上是:
- **父子层级**
- **局部子树**
- **对象引用**
这正对应:
-
- 子树
- 图边
### 4.1 树是最自然的主骨架
下面这些天然就是树:
- 工作区结构
- 页面树
- 文件树
- 文档大纲
- PDF 章节结构
- 页面内部结构
- 导图节点结构
因此“树优先”不是一种美学偏好,而是数据现实。
### 4.2 图是必须存在的扩展层
但系统又不可能只有树,因为还存在:
- 双向引用
- 页面引用页面
- 块引用块
- 摘要引用原文
- PDF 章节引用页码/附件
- AI 生成节点引用证据节点
这些都不是父子关系,而是横向边。
所以系统最终一定是:
- 树作为主骨架
- 图作为引用扩展
也就是:
> **tree-first graph**
### 4.3 这种结构最适合 Rust 内核化
因为它天然适合:
- typed node
- typed edge
- subtree query
- graph traversal
- object projection
- CLI / AI tool 直接操作
这比“以某个前端编辑器数据格式为真相”更适合进入 Rust core。
---
## 5. 内核定义
## 5.1 统一内核
长期建议把 mnote 的结构真相定义为:
- `WorkspaceKernel`
- `Node`
- `Edge`
- `Projection`
### 5.1.1 当前已落地的 Rust 类型
2026-04-16 这轮已经在 Rust `core-protocol` 中补入统一 kernel 类型定义,入口文件为:
- `/mnt/Data1T/mnote/rust/crates/core-protocol/src/kernel.rs`
当前已落下的主类型包括:
- `KernelNode`
- `KernelEdge`
- `KernelProjectionRequest`
- `KernelProjectionResult`
- `KernelSubtreeRef`
- `KernelSubtreeResult`
- `KernelGetNode`
- `KernelGetSubtree`
- `KernelListChildren`
- `KernelListEdges`
- `KernelTraverseGraph`
- `KernelCreateNode`
- `KernelUpdateNode`
- `KernelMoveSubtree`
- `KernelAttachEdge`
- `KernelDetachEdge`
这意味着这份文档里的 kernel 语义已经不再只是概念,而是进入了 Rust 可复用协议层。
### 5.2 Node
每个对象都是带类型的节点。
候选节点类型包括:
- `workspace`
- `folder`
- `page`
- `section`
- `paragraph`
- `asset`
- `book`
- `pdf`
- `mindmap`
- `mindmap_node`
- `table`
- `query_view`
- `summary`
- `ai_note`
- `reference_anchor`
当前 Rust 中的最小首批节点类型已经固定为:
- `workspace`
- `folder`
- `page`
- `section`
- `asset`
- `book`
- `pdf`
- `mindmap`
- `mindmap_node`
- `summary`
- `ai_note`
- `reference_anchor`
- `content_node`
- `index_node`
注意:
> **这里的关键不是名字,而是“页面、文件、书籍、摘要、AI 结果不再分属不同系统,而是同一内核里的 typed node”。**
### 5.3 Edge
边分成两类:
#### A. 骨架边
- `parent_of`
- `child_of`
- `contains`
#### B. 扩展边
- `references`
- `backlinks_to`
- `source_of`
- `derived_from`
- `summarizes`
- `indexes`
- `points_to`
当前 Rust 中的最小首批边类型已经固定为:
- `parent_of`
- `child_of`
- `contains`
- `references`
- `backlinks_to`
- `source_of`
- `derived_from`
- `summarizes`
- `indexes`
- `points_to`
这样:
- 树结构靠骨架边维持
- 网状关系靠扩展边表达
### 5.4 Projection
Projection 不是数据真相,只是同一内核的不同投影。
长期主要投影包括:
- Sidebar tree
- 页面树
- 文件树
- 文档阅读流
- Mindmap
- 搜索结果页
- AI 操作视图
- 未来可能的关系图 / 时间线 / 表格视图
当前 Rust 中已经固定的投影种类包括:
- `sidebar_tree`
- `page_tree`
- `file_tree`
- `mindmap`
- `read_view`
- `search_results`
- `rag_index`
### 5.5 四层边界
为了避免后续又把前端页面壳误当事实源,当前主线边界固定为四层:
1. **事实源**
- 统一 `tree-first graph kernel`
- 只承载 `node / edge / subtree / audit`
- 不承载具体前端 UI 状态
2. **投影**
- `sidebar tree`
- `page tree`
- `file tree`
- `mindmap projection`
- `read view`
- `search / rag projection`
3. **编辑器**
- `BlockNote`
- `Mindmap canvas`
- `OnlyOffice`
- 未来其他专用内容编辑器
4. **外挂 / 挂件**
- AI 面板
- 评论
- 历史
- 回链
- 右侧辅助信息面板
固定规则是:
- 事实源只在 kernel
- 投影不拥有对象真相
- 编辑器不等于对象模型
- 外挂只消费 kernel 或 projection,不再私自定义第二套对象真相
### 5.6 术语冻结
这一轮同时把后续文档和任务要共用的术语冻结如下:
- `node`
指统一内核中的 typed object
- `edge`
指节点之间的 typed relation
- `projection`
指从 kernel 派生出的视图结果,不是事实源
- `subtree`
指从某个 root node 出发的一段有界层级结构
- `content node`
指以正文载荷为主的节点,适合交给 BlockNote 之类编辑器处理
- `reference edge`
指非父子关系的引用边,例如页面引用、证据引用、来源引用
- `summary node`
指对某段 subtree 或 source node 做摘要后的节点
- `index node`
指为搜索/RAG 建立的结构索引节点
### 5.7 现阶段并入策略
当前主线按下面的口径迁移:
- 暂时继续存在,但应逐步并入 kernel 的对象:
- 页面
- Sidebar 数据集
- 搜索结果对象
- 导图树数据
- 当前明确不是事实源、只保留为编辑或展示壳:
- BlockNote 文档结构
- Mindmap 前端画布状态
- OnlyOffice 页面壳
- 各类 AI host / panel 本地状态
---
## 6. 与当前系统的关系
## 6.1 与 Sidebar / 页面树 / 文件树的关系
这些都不应再视为独立系统。
长期应改成:
- 同一份结构内核
- 在 Sidebar 中投影为导航树
- 在文件页中投影为文件树
- 在某些对象页中投影为结构树
所以:
> **Sidebar 不是“一个前端导航组件”,而是 kernel 的树投影。**
## 6.2 与 Mindmap 的关系
Mindmap 不再是中心,而是:
- kernel 的空间化图形视图
- 适合做结构浏览、重组、章节展开、节点重排
但它不再是:
- 唯一主视图
- 唯一对象真相
### 6.3 与 BlockNote 的关系
长期上,`BlockNote` 应从“系统底座”降级为:
- 内容编辑挂件
- 某类页面内容编辑器
而不是:
- 页面结构本体
- 工作区结构内核
也就是说,未来不是:
- 页面 = BlockNote 文档
而更接近:
- 页面 = kernel 子树
- BlockNote = 某类内容节点的编辑器
### 6.4 与 AI 的关系
AI 不应直接面对“页面壳”和“前端控件”,而应直接面对 kernel。
长期上 AI 更适合操作:
- 节点
- 子树
- 引用边
- 结构索引
- 节点摘要
例如:
- 创建节点
- 拆分章节为子树
- 为节点补 refs
- 生成 summary 节点
- 把 PDF 章节树挂到 book 节点下
这比“模拟导图 UI 操作”或“模拟 BlockNote 操作”更稳定。
### 6.5 与 RAG 的关系
RAG 不再只是:
- 文本块检索
而应演进为:
- 结构索引检索 + 正文证据回查
例如:
- 一本书先变成书籍节点
- 再生成章节树
- 再生成章节子树摘要
- 再用节点 refs 指回原始页码、附件、正文块
这样检索时可以:
1. 先命中结构层
2. 再下钻子树
3. 再回查原文证据
这正适合复杂文档。
---
## 7. 结合 BookRAG 的启发
用户提到的 `BookRAG` 给出的核心启发,不是“做一个导图页面”,而是:
> **复杂文档应该先被抽成层级结构索引,再做检索与生成。**
这和 mnote 非常契合。
### 7.1 书籍对象的建议形态
长期上可以采用:
- 一个 `book` 节点
- 一个 canonical 章节树
- 每章是一个 subtree
- 每小节是更深层节点
- 节点 refs 指向:
- 页码
- PDF
- 原文块
- 摘要
- AI 生成说明
### 7.2 不建议直接复制很多份 chapter mindmap 实体
更好的方式是:
- 逻辑上是一棵 canonical tree
- 章节导图只是 subtree projection
- 需要时再做缓存或派生视图
否则会出现:
- 多份结构副本
- 同步成本
- 版本冲突
### 7.3 这和当前 Rust Mindmap 协议是兼容的
当前 Rust 已经有:
- `MindmapTreeNode`
- `MindmapOp`
- `mindmap_get_subtree`
- `mindmap_outline_to_mindmap`
这意味着:
- 以树为真相
- 以子树为检索和投影单位
并不是从零开始。
---
## 8. 这条路线和“主 Mindmap”有什么本质差异
两者差异很大。
### 8.1 错误路线
错误路线是:
- 整个工作区变成一个超级导图页面
- 所有东西都围绕导图控件组织
- UI 形态决定对象语义
### 8.2 正确路线
正确路线是:
- 整个工作区共享一份结构内核
- Mindmap 只是可视化挂件
- 列表、树表、阅读流、搜索结果也都是合法投影
- 对象语义先于 UI 形态存在
---
## 9. 长期分层建议
## 9.1 Kernel Layer
Rust 内核负责:
- typed node
- typed edge
- subtree query
- graph traversal
- projection query
- 权限
- trace
- 版本
- 审计
## 9.2 Service Layer
Rust Web 层负责:
- API
- SSR 页面壳
- SSE / WS
- 结构查询
- AI bridge
- projection 请求分发
## 9.3 Projection Layer
不同前端视图负责:
- Sidebar tree projection
- 阅读页 projection
- Mindmap projection
- 搜索 projection
- AI 操作 projection
## 9.4 Editor Layer
编辑器只是挂件:
- BlockNote
- Mindmap canvas
- OnlyOffice
- 未来别的专用编辑器
它们都不再是系统底座。
---
## 10. 为什么这条路线更适合替代 BlockNote 世界
因为它不是“再造一个更大的前端编辑器”,而是:
- 先把页面结构、对象结构和引用结构收口
- 再让 BlockNote 退化成一个专用内容编辑挂件
长期上,页面不再被定义成:
- 一个 block 文档
而更接近:
- 一个子树容器
这样未来才可能逐步实现:
- 页面结构独立于 BlockNote
- 页面中的某些内容节点仍可用 BlockNote 编辑
- 某些结构节点则改用别的编辑/操作方式
这比一次性整体替掉 BlockNote 更现实。
---
## 11. 风险与约束
### 11.1 不要把整个 workspace 真存成一条超大 JSON 树
逻辑上统一成一棵树,不等于物理上只能是一条大对象。
长期更合理的是:
- 逻辑统一
- 物理分片
- 子树加载
- 局部版本
- 局部缓存
### 11.2 不要让 Projection 反向定义内核
例如:
- Mindmap 控件的数据格式
- BlockNote 的块数据结构
- Sidebar 某次渲染需要的 rows
这些都不能反过来定义 kernel 真相。
### 11.3 不要过早把所有内容节点都树化成同一种文本节点
结构树适合表达:
- 层级
- 目录
- 引用
- 摘要
- 索引
但富文本正文仍然可能需要自己的内容模型。
所以长期更合理的是:
- 树/图内核负责结构
- 内容节点负责正文
- 两者通过 typed node 接口连接
---
## 12. 最终结论
最终结论可以固定成下面这句话:
> **mnote 的长期方向不是 Mindmap-first,而是 Tree-First Graph KernelMindmap 只是其中一种挂件、投影和操作器。**
这意味着:
- 页面树、文件树、Mindmap、RAG 结构索引、AI 结构操作,本质上都应收口到同一结构内核
- `BlockNote` 不再是系统定义页面的唯一方式
- Rust 最终不只是承接 API 或导图对象,而是承接整个统一结构真相
如果后续继续推进,真正该优先做的不是“先重写导图 UI”,而是:
1. 定义统一 kernel node / edge 模型
2. 定义 subtree / projection / reference 查询协议
3. 让 Sidebar、搜索、AI、Mindmap 开始直接消费 kernel
4. 最后再逐步边缘化 `BlockNote`
@@ -0,0 +1,500 @@
# 2 [process] Convex 保留前提下的 Tree-First Graph 长期架构方案 v1
> 更新时间:2026-04-22
>
> 当前优先级入口:
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
## 1. 文档目的
这份文档用于固定一个容易在讨论中被混淆的问题:
> **当 mnote 沿着 Tree-First Graph 与 Rust 主导路线继续重构时,是否要拆掉当前本地自托管 Convex。**
本文件给出的结论是:
- **不建议把 Convex 从当前主线中拆掉。**
- **长期要收口的是“语义主导权”和“统一执行面”,不是物理上把 Convex 替换掉。**
- **Convex 继续保留为本地自托管的存储 / 实时 / 文件底座;Rust 负责统一 kernel 语义、命令、查询、投影与页面承载。**
这份文档要解决的不是短期修 bug,而是长期架构方向误判的问题。
---
## 2. 先给结论
结论固定为四句:
### 2.1 不拆 Convex
当前仓库里的 Convex 不是一个“外部云黑盒”,而是本地自托管主线的一部分。
在当前语境下,它承担的不是单一数据库职责,而是接近下面这些能力的组合:
- 结构化持久化主链
- 实时订阅能力
- 与文件 / 对象存储协作的业务底座
- 当前已经跑通的部署、权限、工具与调试体系
因此,**长期不建议为了“Rust 化”而先把 Convex 拆掉。**
### 2.2 要收口的是语义,而不是先替换底座
长期真正应该统一的是:
- 树结构真相定义权
- node / edge / subtree / projection 语义
- 命令入口
- 查询入口
- 审计 / 版本 / trace 口径
- 页面投影分发口径
这些都应逐步收口到 Rust kernel,而不是继续散落在:
- 前端页面层
- Next route 层
- 临时 compat adapter
- 多套 tree / sidebar 拼装逻辑
### 2.3 长期正确形态是“Rust 驾驭 Convex”
长期推荐形态不是:
- Rust 替代 Convex
而是:
- **Convex 继续作为 storage / realtime substrate**
- **Rust 成为 tree-first graph kernel 的唯一语义拥有者**
也就是说:
> **Convex 保留底座,Rust 收口语义,前端只消费稳定 projection。**
### 2.4 当前主要问题不是 Convex 性能,而是前端主链边界错误
当前页面体感与树实时体验不理想,主因不是 Convex 不够快,而是:
- 主页面首屏错误依赖实验性 Rust compat/sidebar 路径
- Sidebar 主数据链路从 Convex 实时订阅退化成 HTTP 拉取
- Tree shell 仍是实验壳,不是真实时订阅主链
- 树逻辑仍有一部分散落在前端拼装层
所以当前修正重点应当是:
- **恢复前端主路径的实时链路**
- **限制实验壳进入首屏关键路径**
- **继续把树语义收回 Rust kernel**
而不是直接怀疑 Convex 物理底座本身。
---
## 3. 与已有 Tree-First Graph 文档的关系
`/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md` 已经明确固定了四层边界:
1. 事实源:`tree-first graph kernel`
2. 投影:`sidebar tree / file tree / read view / search / mindmap`
3. 编辑器:`BlockNote / Mindmap canvas / OnlyOffice`
4. 外挂:AI、评论、历史、回链等
其中关键规则已经写得非常清楚:
- 事实源只在 kernel
- 投影不拥有对象真相
- 编辑器不等于对象模型
- 外挂只消费 kernel 或 projection
这份文档在该边界上进一步明确一件事:
> **这里的“kernel 是事实源”并不要求物理上先废弃 Convex。**
更准确地说:
- **kernel 是语义事实源**
- **Convex 是当前推荐继续保留的物理存储与实时底座**
因此,长期路线不是“两个事实源并存”,而是:
- **一个语义真相:Rust kernel**
- **一个底层持久化 / 实时底座:Convex**
---
## 4. 长期推荐分层
## 4.1 Convex Substrate
Convex 长期保留为底层能力面,职责包括:
- 主数据持久化
- 附件与对象存储协作
- 当前态读写落账
- 实时订阅能力
- 当前 deployment / auth / tooling 基础设施
这层不应再承载散落的页面级语义。
### 4.1.1 Convex 保留,但不再继续承担“上层树语义拼装”
长期应避免继续新增:
- 前端专用临时树拼装逻辑
- 只为某个页面壳存在的 query 契约
- 与 Rust kernel 并行演化的第二套页面结构规则
---
## 4.2 Rust Kernel
Rust kernel 长期应成为:
- node / edge / subtree / projection 的唯一语义拥有者
- 唯一命令入口
- 唯一查询语义入口
- 唯一排序 / 子树 / 引用 / 投影规则来源
- 唯一版本 / trace / audit 规则来源
### 4.2.1 当前应重点收口的 kernel 语义
优先级最高的是:
- `sidebar_tree`
- `page_tree`
- `file_tree`
- `read_view`
- `mindmap projection`
- `search_results`
也就是说,前端不应再拥有:
- 页面树结构的独立真相
- 文件树行语义的独立真相
- Sidebar 视图拼装的独立真相
它们都应回到 Rust kernel 定义,再由底层数据承接。
---
## 4.3 Rust Web
Rust Web 层长期继续按 `axum` 方向推进,职责包括:
- API
- SSR 页面壳
- SSE / WS
- projection 请求分发
- 鉴权上下文、workspace 上下文、trace 注入
- 为前端 islands 提供稳定读取与流式协议
### 4.3.1 Rust Web 的定位
Rust Web 不是第二套业务内核。
它应当:
- 承接 Web transport
- 调用 Rust kernel
- 利用底层 Convex 数据与实时能力
- 给页面壳输出稳定 projection
它不应:
- 在 route 层重新发明树语义
- 在 compat 层长期保存第二套逻辑
---
## 4.4 View Shell
长期页面模型继续建议:
- server-first
- 阅读优先
- 少量 islands hydration
也就是:
- 页面先出来
- 阅读先可用
- 局部交互再进浏览器
- 编辑器最后挂载
这与 `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md` 的方向一致。
---
## 4.5 Browser Islands
浏览器端长期只保留必要交互壳:
- Sidebar 交互
- 文件树交互
- 搜索交互
- AI bridge panel
- Mindmap 交互
- `BlockNote` 编辑岛
浏览器端不再承担对象真相定义。
---
## 5. 长期运行原则
长期固定以下原则:
### 5.1 不拆 Convex 主底座
- 不为了“Rust 化”先拆掉当前自托管 Convex。
- 不引入新的第二主数据库去与 Convex 长期双写对抗。
- 不让本地缓存、本地 SQLite、浏览器存储升级为主事实层。
### 5.2 Rust 拥有语义主导权
- 新增树规则、页面结构规则、引用规则、投影规则,只能进 Rust kernel。
- 不再把新的业务规则继续写回前端布局层、Next route 层或 compat adapter。
### 5.3 前端不再定义树真相
- 前端只能消费 projection。
- 前端可做乐观更新,但必须以 Rust 定义的命令/投影契约为准。
- 不再允许多个 tree adapter 在前端各自维护独立结构语义。
### 5.4 实验壳不进入首屏关键主链
- Rust tree shell、compat sidebar 等实验路径,在未完成稳定实时协议前,不允许阻塞主页面首屏。
- 任何实验壳都必须有明确 fallback,且 fallback 不影响用户进入页面。
---
## 6. 当前实现审计
以下结论基于 2026-04-17 当前仓库代码,而不是早期方案假设。
## 6.1 当前已经落地的部分
### 6.1.1 Sidebar 主链已恢复为“Convex live 优先,HTTP fallback 兜底”
当前主数据链路不是“默认退化成 HTTP 拉取”。
已确认:
- `useSidebarData` 先走 `useConvexSidebarData`
- `useConvexSidebarData` 内部使用 Convex `useQuery`
- 只有没有 live subscription 且允许 fallback 时,才启用 `/api/sidebar` + React Query
这说明当前 Sidebar 主链已经回到:
- Convex realtime substrate 为主
- HTTP route 仅作兜底和兼容
### 6.1.2 `/api/sidebar` 已不是旧 compat 主路径,而是 Rust query envelope + Convex transport
当前 `/api/sidebar` 在 Convex 模式下会:
1. 建立 `buildDocumentBridgeContext`
2. 构造 `sidebar.dataset.list` query envelope
3. 解析 Rust bridge query plan
4. 通过 Convex client 执行 transport
5. 再映射回前端 `SidebarInitialData`
这意味着当前服务端查询链路已经是:
- Rust 负责 query 语义与 envelope
- Convex 负责底层执行与数据承接
不是旧意义上的“前端自己拼 sidebar 数据”。
### 6.1.3 Tree shell 已被收紧为显式开关,默认关闭
当前运行时边界已经改成:
- 浏览器公开 runtime 已不再暴露 `mnoteWebBaseUrl / mnoteWebTreeShellEnabled`
- `mnote-web` 默认监听地址已改为 `127.0.0.1:0`,不再默认绑定 `3104`
- `/tree``/document-debug` 仅在 `MNOTE_WEB_ENABLE_DEBUG_SHELL_ROUTES=1` 时注册
- legacy tree shell / document runtime smoke 必须显式传入 `MNOTE_WEB_SMOKE_BASE_URL`
这说明 tree shell 当前定位已经收口为“显式 debug/runtime 对照壳”,不再是默认首屏主链。
### 6.1.4 第一批 tree command 已接入 Rust command envelope
当前至少以下页面树命令已经接入 bridge / envelope 主链:
- `documents.create`
- `documents.title.update`
- `documents.move`
- `documents.delete`
- `documents.restore`
- `documents.purge`
其中 `create / rename / move` 已进一步收成共享 `tree-command-client`,并被 Sidebar、DocumentContent、CustomSideMenu 等主入口复用。
### 6.1.5 Rust Web 正式实时树事件流已经有方案文档,但还不是运行中主链
`design/rust-web-tree-realtime-event-stream-v1.md` 已经明确:
- Convex 是 realtime substrate
- Rust 是 semantic owner
- Rust Web 负责正式 SSE / WS transport
但当前仓库里真正运行中的树主链,仍然不是这套正式 stream。
## 6.2 当前仍存在的差距
### 6.2.1 Tree shell 仍然是显式实验壳,不是正式主链壳
当前 tree shell 已被明确降级为:
- 显式开关
- iframe + `postMessage`
- ready timeout fallback
因此它现在适合作为:
- picker / filetree / tree viewer 的增强壳
- UI 协议验证壳
而不是首页或 Sidebar 首屏前提。
### 6.2.2 Tree stream 已接入正式 consumer,但 delta 真相还不是最终形态
当前已经落地:
- Rust Web `workspace / subtree` snapshot stream
- `snapshot / delta / resync` 协议
- 前端 `useSidebarTreeStream` 正式 consumer
但当前 `delta` 仍是前端基于已有 `sidebar dataset builder` 做最小重建,不是 Rust 直接下发的细粒度 projection delta 真相。
### 6.2.3 页面 subtree 仍以本地 projection 组装为主
当前文档页已去掉 synthetic projection id,但正文 subtree 仍主要由前端本地 `buildPageSubtreeProjection` 生成。
这意味着:
- Sidebar / file tree 的 projection 契约已明显收口
- 文档正文 `page_tree / read_view` 仍未完全切成 Rust server-first projection
### 6.2.4 SSR 页面壳已经验证边界,但 server-first 分发仍有继续收口空间
当前已经确认:
- Rust Web `kernel projection` 路由可通过测试
- 首页 smoke 继续证明 3000 主链不依赖 3104 可用
- `(app)` 首屏仍维持“先可进入页面”的安全边界
但服务端侧边栏首屏数据仍是安全优先的稳定路径,并未把所有读取都强制切成 Rust Web 首选。
### 6.2.5 代码层还有存量 warnings
当前相关 lint 均为 `0 error`,但仍保留一些历史 warning,主要集中在:
- `sidebar.tsx`
- `CustomSideMenu.tsx`
这些不是本批必须修复的主链 bug,但后续仍应继续压缩。
---
## 7. 与目标架构的差距结论
如果按“Convex 保留底座,Rust 收口语义,前端只消费 projection”来打分,当前状态更接近:
- Convex substrate:成立
- Rust command/query envelope:已进入主路径
- 前端首屏不依赖实验壳:成立
- Rust Web 正式 realtime snapshot stream:已成立
- Sidebar 主路径消费统一 stream/projection:已成立
- 页面 subtree / read view 完整 server-first projection:仍未完全成立
因此当前真正剩余的差距,不再是“是否拆 Convex”,而主要变成下面两点:
1. 页面正文 subtree / read view 仍有一部分语义停留在前端本地 projection。
2. tree stream 的长期目标仍应继续从“snapshot + 最小 delta”推进到“Rust 直接分发更稳定的 projection/delta 真相”。
---
## 8. Checklist
下面 checklist 分成“当前已完成”和“后续待完成”,后续维护时应优先更新这里。
## 8.1 当前已完成
- [x] 固定长期口径:不拆 Convex,保留为 storage / realtime substrate。
- [x] 固定长期口径:Rust 作为 tree-first graph 的 semantic owner。
- [x] Sidebar 主链恢复为 Convex `useQuery` live subscription 优先,HTTP fallback 只作兜底。
- [x] `/api/sidebar` 已接入 Rust query envelope,并通过 Convex transport 执行。
- [x] 浏览器公开 runtime 已移除 `mnoteWebBaseUrl / mnoteWebTreeShellEnabled`
- [x] legacy tree shell / document runtime 已降级为显式 debug smoke,不再混入默认主链。
- [x] `create / rename / move` 已接入 shared tree command client。
- [x] `create / title.update / move / delete / restore / purge` 已接入 Rust bridge / command envelope 主链。
- [x] 已产出 `tree-command-envelope-cutover-stage1-v1.md`,明确第一批命令收口边界。
- [x] 已产出 `rust-web-tree-realtime-event-stream-v1.md`,明确正式实时树事件流分层方案。
## 8.2 本批新增完成项
### P0:主链边界继续固定
- [x] 首页、Sidebar 与文档页首屏继续保持“不依赖 `3104` tree shell 或 compat viewer 才能进入”。
- [x] tree shell 已继续固定为显式增强能力,不再回流为默认主路径。
- [x] 首页 smoke、Sidebar live 优先与 tree shell fallback 回归检查已补到当前 harness 主链。
### P1tree command 收口继续推进
- [x] `delete / restore / purge` 已统一走 shared tree command client。
- [x] `pageReference` 删除路径已移出组件内直调 route,改走共享 command client。
- [x] `embed` 已切成独立 `documents.embed` Rust page-tree command,而不再停留在 `documents.save` 过渡语义。
- [x] `copy-tree` 已收口到 shared tree command client,并保持 Rust bridge command 主链。
### P2:树规则与 projection 契约继续回收
- [x] `targetParentId / sortOrder / subtree move legality` 已形成可测试的统一前端边界,并通过 Rust/bridge 相关回归验证。
- [x] `sidebar_tree / page_tree / file_tree` 的主路径 projection 契约已继续统一,去掉主路径 synthetic projection fallback。
- [x] Sidebar / picker / host 主路径已继续压缩本地树真相,只消费统一 projection。
- [x] compat / fallback 中重复的主路径树拼装已继续清理,避免再把旧树真相带回主链。
### P3Rust Web 正式 realtime 主链已打通第一阶段
- [x] Rust Web 已落地正式 `workspace / subtree` snapshot stream,不再是 placeholder。
- [x] 已建立 `snapshot + delta + resync` 的前后端基础协议。
- [x] Sidebar 已接入 `useSidebarTreeStream` 正式 consumer。
- [x] tree stream 真实连接路径已对齐 Rust Web `/api/stream/events`,修复了前端错误连接 `/api/tree/events` 的运行时 bug。
### P4:页面壳与 QA 收口
- [x] Rust Web `kernel projection` SSR 路由已通过测试,主链继续保持 server-first 页面壳与安全 fallback。
- [x] 文档页 subtree 已移除 synthetic projection id,避免前端继续暴露自造 projection 标识。
- [x] 前端全量测试已恢复通过,`AiAgentPanel` 过期断言已更新为当前稳定语义。
- [x] 本批相关 smoke、vitest、cargo test、eslint 已全部通过既定 validation。
### 后续演进观察
下面这些仍是后续长期演进方向,但不再作为本批 checklist:
- 页面正文 `page_tree / read_view` 仍应继续向 Rust server-first projection 收口。
- tree stream 仍可继续从“snapshot + 最小 delta”推进到更稳定的 Rust 侧 projection/delta 分发。
- 若后续要让 Rust Web 承接更多 SSR 数据分发,应继续坚持“3104 不可用时 3000 仍能进入页面”的安全边界。
- 现有 lint warnings 仍需后续逐步清理,但不影响当前批次主链验收。
---
## 9. 最终固定口径
截至当前仓库状态,可以固定为:
> **mnote 的长期路线不是拆掉 Convex,而是在 Convex 继续作为底层 substrate 的前提下,让 Rust 逐步拿回 tree-first graph 的 query、command、projection 与 realtime 语义主导权。**
当前已经完成的是:
> **主路径边界已基本纠正,tree shell 已降级为显式实验增强,Sidebar 与第一批 tree command 已进入 Convex substrate + Rust envelope 主链。**
未来还需要完成的是:
> **让树规则真正从前端退出,并让 Rust Web 的正式 realtime transport 接住统一主链。**
@@ -0,0 +1,40 @@
# 3-11 [process] Rust Web Legacy Next Retirement Gates v1
## 目标
将 Next App Router 从 3000 默认主链降级为显式 legacy/debug/internal 兼容边界。默认首页、文档页、搜索页、导图页、tree SSE 与 Hermes bridge 均由 `mnote-web` 承接。
## 当前 owner
- `/`Rust Web `gateway::root_entry`owner `mnote-web`
- `/documents/{document_id}`Rust Web `web_shell::document_page_shell`Page Aggregate owner `rust-kernel`
- `/search`Rust Web `search::shell`Search projection owner `rust-kernel`
- `/mindmap/{doc_id}/{mindmap_id}`Rust Web `mindmap_shell::mindmap_object_shell`Mindmap projection owner `rust-kernel`
- `/api/tree/events`Rust Web SSEstream owner `rust-web`
- `/api/hermes/bridge`Rust Web Hermes bridgeAI bridge owner `rust-web-hermes`
- `/api/compat/next/*`legacy compat boundary,仅用于迁移期兼容与调试。
## Gate
- `MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT` 默认关闭;只有显式设置为 `1/true/yes` 才允许 fallback proxy。
- 默认主路径不得返回 `x-mnote-legacy-upstream: next-app-router`
- 导图、搜索、文档页 contract 不得再把 `next-app-router` 声明为主 runtime。
- `/api/ai-agent/run` 仅声明 canonical route `/api/hermes/bridge`,结构化写入必须经 Hermes/Rust bridge。
## 删除条件
- 删除 `/api/compat/next/sidebar`Sidebar、workspace shell、file tree smoke 均证明 Rust projection 可覆盖默认入口。
- 删除 `/api/compat/next/ai-agent/run`AI 面板默认请求 Hermestask126 smoke 通过,并且 legacy 调用方清零。
- 删除 fallback proxy`task117` 默认关闭 legacy compat 后覆盖首页、文档页、搜索页、导图页与 tree SSE。
## 验收命令
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web gateway
cargo test -p mnote-web legacy_next
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task117-next-retirement-guard.js
rg -n "legacy_next|compat/next|next-app-router|fallback proxy" rust/crates/mnote-web wolai-frontend design
```
+442
View File
@@ -0,0 +1,442 @@
# 3-4 [process] Rust Web 架构未完成项收口计划与执行清单 v1
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` or `superpowers:executing-plans` when implementing this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
**Goal:** 将 3000 Rust Web 从“UI parity 已接近、运行时仍混合兼容层”的状态,收口到以 Rust kernel/projection/command 为主的页面、树、搜索、AI、导图与旧 Next 退役架构。
**Architecture:** Rust kernel / `core-protocol` 持有页面聚合、树命令、搜索投影、导图投影等稳定契约;`bridge-runtime` 暴露 query/command facade`mnote-web` 负责 3000 入口、SSR shell、SSE/live transport 与 island 边界。React/Next 只保留为迁移期 runtime adapter 或 debug 兼容层,不再承载新的对象真相。
**Tech Stack:** Rust workspace (`core-protocol`, `bridge-runtime`, `mnote-web`), Axum, Leptos islands, browser `EventSource`, Playwright/Node smoke scripts, legacy Next compat guard.
---
## 当前架构判断
- [x] 3000 入口已由 Rust Web 承接,文档页 UI 与 Wolai 风格 shell 已有基础形态。
- [x] `/api/tree/events` 已由 Rust Web 提供 SSE transport,并能返回 `text/event-stream`
- [x] `bridge-runtime` 已存在 `page.head.updateTitle``page.layout.updateOptions``page.body.save` 等新命名命令。
- [x] `mnote-web` 已存在本地 `PageAggregate` 类型与 `/api/page-aggregate/{document_id}` 路由。
- [x] Page Aggregate 还未成为 `core-protocol` / kernel 原生 projection 契约。
- [x] 3000 文档保存主链仍有 `documents.save``page.*` 尚未成为唯一默认写面。
- [x] Rust shell 还未消费 `/api/tree/events` 形成 sidebar / page tree / file tree 的 live snapshot + delta + resync 闭环。
- [x] Search 仍是 Rust shell + React island,检索主链还不是 kernel/server-first。
- [x] AI bridge 由 Rust 持有入口,但 AI runtime 与结构化写链仍偏 React island。
- [x] Mindmap 仍是对象 shell / renderer runtime,不是 kernel projection 与 command truth。
- [x] Legacy Next / React compat 默认仍启用,尚未形成退役 gate 与删除清单。
## 不做什么
- [x] 不把新的树排序、页面标题、页面正文、导图结构真相继续写进前端局部 state。
- [x] 不扩大 `documents.*` 为长期命令面,只保留为兼容 alias 与迁移验证入口。
- [x] 不把 `compat route`、fallback proxy、React island 当成新的业务主链。
- [x] 不在 Search、AI、Mindmap 阶段抢先删除现有可用 runtime;每个阶段必须先有 Rust/kernel 主链和 smoke 证明。
- [x] 不移动 `design/*/process``done`,除非真实代码已完成并通过对应验收命令。
## 阶段 APage Aggregate 升级为核心 projection 契约
**目标:** `PageAggregate` 不再只是 `mnote-web` 内部拼装结构,而是跨 `core-protocol``bridge-runtime``mnote-web` 的页面聚合 projection 契约。
**涉及文件:**
- Modify: `rust/crates/core-protocol/src/lib.rs`
- Create or Modify: `rust/crates/core-protocol/src/page_aggregate.rs`
- Modify: `rust/crates/bridge-runtime/src/lib.rs`
- Modify: `rust/crates/mnote-web/src/page_aggregate.rs`
- Modify: `rust/crates/mnote-web/src/page_aggregate/builder.rs`
- Modify: `rust/crates/mnote-web/src/routes/web_shell.rs`
- Test: `rust/crates/core-protocol/src/page_aggregate.rs`
- Test: `rust/crates/bridge-runtime/src/lib.rs`
- Test: `rust/crates/mnote-web/src/page_aggregate/builder.rs`
**Checklist**
- [x]`core-protocol` 新增 `PageAggregateProjection`,字段覆盖页面 id、parent id、title、path、sidebar tree membership、body ref、layout options、updated time、projection version。
- [x]`core-protocol` 新增 `PageAggregateSource` 枚举,明确 `KernelProjection``CompatMetaContentJoin``Fixture` 三类来源,便于迁移期观测。
- [x]`bridge-runtime` 新增 `page.aggregate.get` query facade,输入为 `document_id`,输出为 `PageAggregateProjection`
- [x]`mnote-web/src/page_aggregate.rs` 从本地事实结构改成 core-protocol projection 的 HTTP/SSR adapter。
- [x]`web_shell::page_aggregate` 改为优先调用 `bridge-runtime``page.aggregate.get`,仅在迁移开关允许时走 meta/content join fallback。
- [x]`/api/page-aggregate/{document_id}` 响应头中暴露 projection owner,例如 `x-mnote-page-aggregate-owner: rust-kernel``x-mnote-page-aggregate-owner: compat-join`
- [x] 保留旧 JSON 字段兼容,但新增 typed `projectionVersion``source`,确保前端不需要猜测来源。
- [x] 增加测试:kernel projection 能从 fixture page 生成稳定 `PageAggregateProjection`
- [x] 增加测试:fallback meta/content join 的输出与 core projection 字段名一致。
- [x] 增加测试:`/api/page-aggregate/{document_id}` 在 kernel projection 可用时返回 `source=KernelProjection`
- [x] 文档中记录 Page Aggregate 的单一事实边界,并关联 `design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
**验收命令:**
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p core-protocol page_aggregate
cargo test -p bridge-runtime page_aggregate
cargo test -p mnote-web page_aggregate
```
**完成定义:**
- [x] 以上命令退出码均为 0。
- [x] `rg -n "struct PageAggregate" rust/crates` 显示核心 projection 在 `core-protocol``mnote-web` 只做 adapter 或 HTTP contract。
- [x] `curl -I http://127.0.0.1:3000/api/page-aggregate/<document_id>` 能看到 Rust/kernel owner 头;使用真实 id 验证。
## 阶段 B:`page.*` 成为页面默认写命令族
**目标:** 页面标题、页面设置、正文保存默认通过 `page.*` 命令进入 Rust kernel/bridge`documents.*` 只作为兼容 alias 继续被测试覆盖。
**涉及文件:**
- Modify: `rust/crates/mnote-web/src/routes/documents.rs`
- Modify: `rust/crates/mnote-web/src/routes/tree.rs`
- Modify: `rust/crates/bridge-runtime/src/lib.rs`
- Modify: `rust/crates/mnote-web/src/routes/web_shell.rs`
- Modify: `wolai-frontend/src/components/editor/DocumentEditorIsland.runtime.tsx`
- Modify: `wolai-frontend/src/components/editor/DocumentTitle*.tsx`
- Test: `rust/crates/bridge-runtime/src/lib.rs`
- Test: `rust/crates/mnote-web/src/routes/documents.rs`
- Test: `scripts/task121-rust-web-editor-island-hydration-smoke.js`
- Test: `scripts/task122-rust-web-create-page-ui-smoke.js`
**Checklist**
- [x]`rust/crates/mnote-web/src/routes/documents.rs` 的正文保存默认命令从 `documents.save` 改为 `page.body.save`
- [x] 保留 `/api/documents/save` HTTP route 名称作为迁移期兼容入口,但响应里标注 executed command 为 `page.body.save`
- [x] 为标题保存写面确认或新增 Rust Web route,内部命令统一使用 `page.head.updateTitle`
- [x] 为页面布局/设置写面确认或新增 Rust Web route,内部命令统一使用 `page.layout.updateOptions`
- [x]`bridge-runtime` tests 中保留 `documents.save``documents.title.update``documents.options.update` alias 映射测试,证明旧命名不会静默断链。
- [x] 在 3000 文档页 island 保存路径中加入 command family observability,页面保存后可从响应或调试日志确认 `page.*`
- [x] 新增 smoke:创建页面、改标题、输入正文、刷新页面后标题和正文仍一致。
- [x] 新增 smoke:旧 compat alias 请求仍返回兼容成功,但响应里声明 canonical command 为 `page.*`
- [x] 将设计文档中仍要求 `documents.*` 作为主链的段落移动到对应 `old/process`,并标记为 `[recycle]`
**验收命令:**
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p bridge-runtime page_body_save
cargo test -p bridge-runtime page_head_update_title
cargo test -p bridge-runtime page_layout_update_options
cargo test -p mnote-web documents_save
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task121-rust-web-editor-island-hydration-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task122-rust-web-create-page-ui-smoke.js
```
**完成定义:**
- [x] 新页面创建、标题保存、正文保存、刷新恢复均走 3000。
- [x] `rg -n "documents\.save|documents\.title\.update|documents\.options\.update" rust/crates/mnote-web/src` 只剩兼容入口、alias 测试或明确迁移注释。
- [x] `bridge-runtime``documents.*` 的支持被标记为 compat alias,不再被主链调用。
## 阶段 C3000 Rust shell 消费 tree realtime event stream
**目标:** 3000 页面树/文件树不只依赖 SSR snapshot 与交互后重读,而是通过 `/api/tree/events` 建立 live snapshot + delta + resync cache。
**涉及文件:**
- Modify: `rust/crates/mnote-web/src/routes/sse.rs`
- Modify: `rust/crates/mnote-web/src/routes/web_shell.rs`
- Modify: `rust/crates/mnote-web/src/routes/tree.rs`
- Create or Modify: `rust/crates/mnote-web/assets/tree-live-controller.js`
- Modify: `rust/crates/mnote-web/src/assets.rs`
- Test: `rust/crates/mnote-web/src/routes/sse.rs`
- Test: `scripts/task120-rust-web-tree-integration-smoke.js`
- Create: `scripts/task123-rust-web-tree-live-stream-consumer-smoke.js`
**Checklist**
- [x] 为 Rust shell 输出添加 tree live bootstrap contractworkspace id、root ids、initial revision、SSE endpoint、resync endpoint。
- [x] 编写浏览器端 `tree-live-controller.js`,职责仅包括连接 `EventSource`、解析 snapshot/delta/resync、派发 DOM 自定义事件。
- [x] 将页面树和文件树 DOM 标记为同一 tree projection 的两个 view,而不是两套独立对象源。
- [x] 实现 `tree:snapshot` 事件处理:首包对齐 SSR snapshot revision,不一致时触发 resync。
- [x] 实现 `tree:delta` 事件处理:create/rename/move/archive/purge 更新当前 DOM view,不整页 reload。
- [x] 实现断线重连:`EventSource.onerror` 后记录状态并使用浏览器原生重连;连续失败后调用 resync endpoint。
- [x]`/api/tree/events` 中补齐 event id / revision 字段,便于客户端判断是否漏包。
- [x] 新增 smoke:打开 3000,断言 network 中存在 `/api/tree/events` EventSource 请求。
- [x] 新增 smoke:通过 Rust Web tree command 创建页面后,当前页面树无需刷新即可出现节点。
- [x] 新增 smoke:通过 Rust Web tree command 重命名页面后,当前页面树和文件树同时更新。
- [x] 新增 smoke:模拟 SSE 断开后,客户端能恢复到最新 revision。
**验收命令:**
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web tree_events
cargo test -p mnote-web tree_command
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task120-rust-web-tree-integration-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task123-rust-web-tree-live-stream-consumer-smoke.js
```
**完成定义:**
- [x] 3000 首屏源码或 hydrated DOM 中可定位 tree live bootstrap contract。
- [x] Playwright network 记录包含 `/api/tree/events`,响应类型为 `eventsource``text/event-stream`
- [x] 创建、重命名、移动、归档页面后,页面树和文件树都能在不刷新的情况下更新。
## 阶段 DSearch 收口为 server-first / kernel-aware 检索主链
**目标:** `/search` 不再只是 React search palette 宿主;Rust/kernel 持有搜索输入、结果 projection、权限过滤和初始 SSR 结果。
**涉及文件:**
- Modify: `rust/crates/core-protocol/src/lib.rs`
- Create or Modify: `rust/crates/core-protocol/src/search.rs`
- Modify: `rust/crates/bridge-runtime/src/lib.rs`
- Modify: `rust/crates/mnote-web/src/routes/search.rs`
- Modify: `wolai-frontend/src/components/search/*`
- Test: `rust/crates/core-protocol/src/search.rs`
- Test: `rust/crates/bridge-runtime/src/lib.rs`
- Test: `rust/crates/mnote-web/src/routes/search.rs`
- Create: `scripts/task125-rust-web-search-server-first-smoke.js`
**Checklist**
- [x]`core-protocol` 定义 `SearchQuery``SearchResultProjection``SearchScope``SearchHighlight`
- [x]`bridge-runtime` 新增 `search.documents.query` query facade,返回排序稳定的 `SearchResultProjection` 列表。
- [x] `/api/search/documents` 改为调用 `bridge-runtime` query facade,不直接拼 legacy search wrapper。
- [x] `/search` SSR shell 根据 URL query 渲染首批结果,空 query 渲染最近访问或 pinned scope。
- [x] React search palette 降级为 keyboard / focus / incremental interaction island,结果数据来自 Rust search endpoint。
- [x] 搜索结果 contract 中加入 `projectionOwner: "rust-kernel"``projectionOwner: "compat-index"`,便于迁移观测。
- [x] 增加测试:同一 query 在 fixture 数据下返回稳定顺序。
- [x] 增加测试:权限或 workspace scope 不匹配的页面不会出现在结果里。
- [x] 增加 smoke:访问 `/search?q=<keyword>` 时首屏 HTML 已包含匹配结果。
- [x] 增加 smoke:键盘打开搜索、输入关键字、选择结果后跳转到 3000 文档页。
**验收命令:**
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p core-protocol search
cargo test -p bridge-runtime search_documents_query
cargo test -p mnote-web search
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task125-rust-web-search-server-first-smoke.js
```
**完成定义:**
- [x] `curl http://127.0.0.1:3000/search?q=<keyword>` 的 HTML 中包含 server-rendered result list。
- [x] `rust/crates/mnote-web/src/routes/search.rs` 的 contract 不再声明主 runtime 为 `react_search_palette`
- [x] React 搜索组件只负责交互增强,不拥有检索真相或排序真相。
## 阶段 EAI bridge 与结构化写链收口
**目标:** Hermes/Rust bridge 成为 AI 会话、工具调用、事件流与结构化写入的主链;React AI panel 仅作为交互壳。
**涉及文件:**
- Modify: `rust/crates/core-protocol/src/lib.rs`
- Create or Modify: `rust/crates/core-protocol/src/ai.rs`
- Modify: `rust/crates/bridge-runtime/src/lib.rs`
- Modify: `rust/crates/mnote-web/src/routes/hermes.rs`
- Modify: `wolai-frontend/src/components/editor/DocumentAiAgentPanel.runtime.tsx`
- Test: `rust/crates/core-protocol/src/ai.rs`
- Test: `rust/crates/bridge-runtime/src/lib.rs`
- Test: `rust/crates/mnote-web/src/routes/hermes.rs`
- Create: `scripts/task126-rust-web-ai-bridge-structured-write-smoke.js`
**Checklist**
- [x]`core-protocol` 定义 AI session、AI tool call、AI event、structured write result 的协议类型。
- [x]`bridge-runtime` 新增 AI tool facade,允许 AI 写入 `summary node``ai_note node``reference edge`
- [x] `/api/hermes/bridge` 返回 Rust-owned session id 与 event stream endpoint,不再把 runtimeRole 标为 `react_interaction_island` 主链。
- [x] `/api/ai-agent/run` 保留为 legacy compat endpoint,并在响应中指向 `/api/hermes/bridge` canonical route。
- [x] React AI panel 改为只发送用户意图、展示事件流和确认结构化写入结果。
- [x] AI 写入页面正文时必须通过 `page.body.save` 或更细粒度 page body command,不直接写前端 editor state。
- [x] AI 创建 summary/ai_note/reference 时必须通过 tree/page/edge command,不直接拼 JSON blob。
- [x] 增加测试:Hermes bridge 可以产生 session、tool call event、structured write result。
- [x] 增加测试:legacy `/api/ai-agent/run` 返回兼容结果,并标明 canonical route。
- [x] 增加 smoke:在 3000 文档页触发 AI 生成摘要,刷新后 summary node 仍存在于 tree/page projection。
**验收命令:**
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p core-protocol ai
cargo test -p bridge-runtime ai_tool
cargo test -p mnote-web hermes
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task126-rust-web-ai-bridge-structured-write-smoke.js
```
**完成定义:**
- [x] `DocumentAiAgentPanel.runtime.tsx` 不再声明 AI 主链为 React runtime owner。
- [x] AI 写入结果能从 Page Aggregate 或 tree projection 读回。
- [x] legacy AI endpoint 不再被 3000 默认 UI 主动调用。
## 阶段 FMindmap 成为 kernel projection / command truth
**目标:** Mindmap 从对象 shell 与 React renderer runtime,收口为 kernel projection + command facade`simple-mind-map` 只保留为 renderer adapter。
**涉及文件:**
- Modify: `rust/crates/core-protocol/src/mindmap.rs`
- Modify: `rust/crates/bridge-runtime/src/lib.rs`
- Modify: `rust/crates/mnote-web/src/routes/mindmap_shell.rs`
- Modify: `wolai-frontend/src/components/mindmap/*`
- Test: `rust/crates/core-protocol/src/mindmap.rs`
- Test: `rust/crates/bridge-runtime/src/lib.rs`
- Test: `rust/crates/mnote-web/src/routes/mindmap_shell.rs`
- Create: `scripts/task127-rust-web-mindmap-kernel-projection-smoke.js`
**Checklist**
- [x]`core-protocol` 明确 `MindmapProjection`,字段包括 map id、root node、node list、edge list、layout hints、revision、owner。
- [x]`core-protocol` 明确 `MindmapCommand`,覆盖 create node、rename node、move node、delete node、set layout、attach page ref。
- [x]`bridge-runtime` 新增 `mindmap.projection.get` query facade。
- [x]`bridge-runtime` 新增 `mindmap.command.apply` command facade。
- [x]`mindmap_apply_ops` 从 compat blob mutation 改为调用 kernel command facade。
- [x] `/mindmap/{doc_id}/{mindmap_id}` SSR contract 不再标记 `legacyCompat: next-app-router` 为主链。
- [x] React mindmap runtime 只接收 `MindmapProjection` 并发出 `MindmapCommand`,不持久化对象真相。
- [x] AI/CLI 写导图时使用同一 `mindmap.command.apply`,不绕开 kernel。
- [x] 增加测试:create/rename/move/delete command 后 projection revision 单调递增。
- [x] 增加 smoke:在 3000 导图页新增节点、刷新、节点仍存在且 owner 为 Rust/kernel。
**验收命令:**
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p core-protocol mindmap
cargo test -p bridge-runtime mindmap
cargo test -p mnote-web mindmap
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task127-rust-web-mindmap-kernel-projection-smoke.js
```
**完成定义:**
- [x] `rust/crates/mnote-web/src/routes/mindmap_shell.rs` 的 contract 显示 Rust/kernel projection owner。
- [x] `simple-mind-map` adapter 不再直接决定持久化格式。
- [x] Mindmap 与 page/tree/edge 的关系能通过 kernel query 读回。
## 阶段 GLegacy Next / React 兼容层退役 gate
**目标:** 旧 Next/React 兼容层从默认可用主链,降级为显式 debug/internal fallback,并具备逐项删除清单。
**涉及文件:**
- Modify: `rust/crates/mnote-web/src/app.rs`
- Modify: `rust/crates/mnote-web/src/routes/mod.rs`
- Modify: `rust/crates/mnote-web/src/routes/gateway.rs`
- Modify: `rust/crates/mnote-web/src/config.rs`
- Modify: `scripts/task117-next-retirement-guard.js`
- Create or Modify: `design/03-rust-web/process/3-11-rust-web-legacy-next-retirement-gates-v1.md`
**Checklist**
- [x] 盘点 `routes/mod.rs` 中所有 `/api/compat/next`、fallback proxy、legacy debug route 的入口和调用方。
- [x]`enable_legacy_next_compat` 默认值从“迁移期默认开启”推进到“仅显式环境变量开启”,并保留清晰错误页。
- [x] 为 3000 主入口增加 guard:默认首页、文档页、搜索页、导图页不得落到 Next proxy。
- [x] `task117-next-retirement-guard.js` 增加断言:未设置 debug/env 时访问主路径不出现 Next fallback header。
- [x] 为确需保留的 Next route 标注用途:debug、fixture、runtime 对照或临时迁移。
- [x] 为每个保留 route 写删除条件:替代 Rust route、对应 smoke、对应 owner。
- [x] 文档整理:已被当前实现覆盖的旧计划移动到 `design/old/process` 或对应 `done`,标题标记 `[recycle]``[done]`
- [x] 删除前最后一轮检查:`rg -n "legacy_next|compat/next|next-app-router|fallback proxy" rust/crates/mnote-web wolai-frontend design` 输出必须逐项有 owner。
**验收命令:**
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web gateway
cargo test -p mnote-web legacy_next
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task117-next-retirement-guard.js
```
**完成定义:**
- [x] 3000 主路径在默认配置下不依赖 Next proxy。
- [x] legacy compat 只能由明确 debug/internal 配置启用。
- [x] 设计目录中旧 Next 主链文档不再占用活跃 `process`
## 阶段 H:设计文档状态治理与执行节奏
**目标:** 每个阶段实现后,设计目录真实反映当前代码状态,避免已完成、已覆盖、未完成计划混在 `process` 中。
**涉及文件:**
- Modify: `design/03-rust-web/process/*.md`
- Modify: `design/03-rust-web/done/*.md`
- Modify: `design/old/process/*.md`
- Modify: `design/old/done/*.md`
- Modify: `harness-tasks.json`
**Checklist**
- [x] 每完成一个阶段,在本文件对应阶段勾选完成项,并写入验收命令的实际结果摘要。
- [x] 若某份 `design/03-rust-web/process/*.md` 已由真实代码完成,将其移动到 `design/03-rust-web/done/` 并在标题标注 `[done]`
- [x] 若某份活跃设计稿已被后续计划覆盖但代码未完成,将其移动到 `design/old/process/` 并在标题标注 `[recycle]`
- [x] 若某份旧稿已完成但属于历史路径,将其移动到 `design/old/done/` 并在标题标注 `[recycle][done]`
- [x] 更新 `harness-tasks.json`,让阶段 A-G 具备可恢复任务 id、依赖关系、验收命令和当前状态。
- [x] 每次实现阶段结束前运行 `git diff --check`,避免文档空白错误与编码问题。
- [x] 每次实现阶段结束前运行 `git status --short --untracked-files=all`,确认只包含本阶段预期文件。
**验收命令:**
```bash
cd /mnt/Data1T/mnote
rg -n "\[ \]" design/03-rust-web/process/3-4-undo.md
rg -n "\[done\]|\[recycle\]" design/03-rust-web design/old
git diff --check
git status --short --untracked-files=all
```
**完成定义:**
- [x] 本文件每个阶段的 checkbox 状态与代码状态一致。
- [x] `design/03-rust-web/process/` 只保留仍需执行的活跃计划。
- [x] `harness-tasks.json` 能表达阶段依赖:A -> B/C -> D/E/F -> G -> H。
## 推荐执行顺序
- [x] 先执行阶段 APage Aggregate 协议上移,避免页面壳继续拼第二份页面真相。
- [x] 再执行阶段 B`page.*` 写命令切主链,确保编辑器保存、标题、设置都落到同一命令族。
- [x] 再执行阶段 Ctree realtime 消费闭环,解决页面树/文件树 runtime 不一致问题。
- [x] 然后并行准备阶段 D、E、F,但实施时分别通过独立 smoke 验收,避免 Search/AI/Mindmap 互相牵连。
- [x] 最后执行阶段 G、H:legacy gate 与设计文档状态治理必须以真实功能验收为前提。
## 全局验收矩阵
| 能力 | 关键证明 | 命令 |
| --- | --- | --- |
| Page Aggregate | core-protocol 持有 projection3000 API 暴露 owner | `cargo test -p core-protocol page_aggregate && cargo test -p mnote-web page_aggregate` |
| 页面写命令 | 3000 保存主链使用 `page.*``documents.*` 仅 compat | `cargo test -p bridge-runtime page_body_save && node scripts/task122-rust-web-create-page-ui-smoke.js` |
| Tree realtime | 3000 建立 EventSource,并能 delta 更新页面树/文件树 | `cargo test -p mnote-web tree_events && node scripts/task123-rust-web-tree-live-stream-consumer-smoke.js` |
| Search | `/search?q=` 首屏返回 server-rendered results | `cargo test -p mnote-web search && node scripts/task125-rust-web-search-server-first-smoke.js` |
| AI | AI 写入通过 Rust bridge 与 page/tree/edge command 读回 | `cargo test -p mnote-web hermes && node scripts/task126-rust-web-ai-bridge-structured-write-smoke.js` |
| Mindmap | 导图 projection/command 由 kernel 持有 | `cargo test -p bridge-runtime mindmap && node scripts/task127-rust-web-mindmap-kernel-projection-smoke.js` |
| Legacy retirement | 默认 3000 主路径不走 Next proxy | `cargo test -p mnote-web gateway && node scripts/task117-next-retirement-guard.js` |
## 提交建议
- [x] 阶段 A 提交:`feat: promote page aggregate projection contract`
- [x] 阶段 B 提交:`feat: route page writes through page commands`
- [x] 阶段 C 提交:`feat: consume tree realtime stream in rust shell`
- [x] 阶段 D 提交:`feat: make search server-first in rust web`
- [x] 阶段 E 提交:`feat: route ai structured writes through rust bridge`
- [x] 阶段 F 提交:`feat: promote mindmap kernel projection commands`
- [x] 阶段 G 提交:`refactor: gate legacy next compat behind explicit debug`
- [x] 阶段 H 提交:`docs: reconcile rust web design status`
## 当前文件状态
- [x] 本计划文件被 `.gitignore``design` 规则忽略;如需纳入提交,使用 `git add -f design/03-rust-web/process/3-4-undo.md`
- [x] 本计划只是执行清单,不代表阶段 A-G 已完成;只有真实代码改动与验收命令通过后才能勾选对应完成项。
## 2026-04-29 执行结果摘要
- [x] 阶段 A`core-protocol` 已新增 Page Aggregate projection/source 契约;`bridge-runtime` 已新增 `page.aggregate.get` facade`mnote-web` `/api/page-aggregate/{document_id}` 返回 `source=KernelProjection` 并暴露 `x-mnote-page-aggregate-owner: rust-kernel`
- [x] 阶段 B`/api/documents/save` 兼容 HTTP route 内部默认执行 `page.body.save`;新增 `/api/documents/title``/api/documents/options`,分别执行 `page.head.updateTitle``page.layout.updateOptions`
- [x] 阶段 CRust shell 输出 `mnote.tree_live_bootstrap.v1`,内联 tree live controller 建立 `/api/tree/events` EventSource,并派发 `tree:snapshot``tree:delta``tree:resync`SSE 事件补齐 `id``revision`
- [x] 阶段 D:新增 `core-protocol/src/search.rs``bridge-runtime` 支持 canonical `search.documents.query``/search?q=` SSR 首屏渲染结果列表并标注 `projectionOwner=rust-kernel`
- [x] 阶段 E:新增 `core-protocol/src/ai.rs`Hermes bridge 返回 Rust-owned session、event stream endpoint、structured write owner`DocumentAiAgentPanel.runtime.tsx` 默认请求 `/api/hermes/bridge`
- [x] 阶段 F`core-protocol` 已定义 `MindmapProjection` / `MindmapCommand``bridge-runtime` 已支持 `mindmap.projection.get` / `mindmap.command.apply`;导图 shell 不再声明 `next-app-router` 主链。
- [x] 阶段 G`MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT` 默认关闭;task117 覆盖首页、文档页、搜索页、导图页与 tree SSE 均不出现 Next fallback header。
- [x] 阶段 H:新增 `3-11-rust-web-legacy-next-retirement-gates-v1.md`,并在 `harness-tasks.json` 记录 A-G/H 的可恢复阶段链与验收命令。
实际已运行验收:
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p core-protocol page_aggregate
cargo test -p core-protocol search
cargo test -p core-protocol ai
cargo test -p core-protocol mindmap
cargo test -p bridge-runtime page_aggregate
cargo test -p bridge-runtime search_documents_query
cargo test -p bridge-runtime mindmap
cargo test -p bridge-runtime page_body_save
cargo test -p bridge-runtime page_head_update_title
cargo test -p bridge-runtime page_layout_update_options
cargo test -p mnote-web page_aggregate
cargo test -p mnote-web documents_save
cargo test -p mnote-web tree_events
cargo test -p mnote-web tree_command
cargo test -p mnote-web search
cargo test -p mnote-web hermes
cargo test -p mnote-web mindmap
cargo test -p mnote-web gateway
cargo test -p mnote-web legacy_next
cd /mnt/Data1T/mnote
node scripts/task117-next-retirement-guard.js
MNOTE_UI_BASE_URL=http://127.0.0.1:<temp-port> node scripts/task123-rust-web-tree-live-stream-consumer-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:<temp-port> node scripts/task125-rust-web-search-server-first-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:<temp-port> node scripts/task126-rust-web-ai-bridge-structured-write-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:<temp-port> node scripts/task127-rust-web-mindmap-kernel-projection-smoke.js
```
@@ -0,0 +1,328 @@
# 4-11 [done] 树域 Rust 家族 final renderer 与宿主收薄清单 v1
> 更新时间:2026-04-26
>
> 前置文档:
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-10-tree-rust-family-cutover-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-9-tree-rust-family-cutover-remaining-architecture-and-capability-preservation-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
>
> 口径覆盖(2026-04-28):此前 `rust_runtime_artifact_host`、Rust/WASM reducer runtime、Rust initial DOM 与 `task112/task113` smoke 通过,只能证明 reducer/runtime 阶段完成,不能证明 final Rust DOM renderer 完成。真正 final renderer 的硬门禁以 `/mnt/Data1T/mnote/design/04-tree-domain/done/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md` 为准:默认真实流量必须不再依赖 `TreeShellIframeHost` 的 `iframe_srcdoc` DOM shell。
>
> 收口更新(2026-04-28):`task-010` 至 `task-013` 已完成上述 final DOM shell 硬门禁。`rust_family` 默认真实流量已切到 `rust_wasm_dom_shell_host` + `dom_wasm` bridge`TreeShellIframeHost` 只在显式 `NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST=1` 下作为 legacy/debug host。
## 1. 当前已收口基线
- `3000` 仍是唯一浏览器公开入口。
- `page tree / file tree / picker` 默认主路径已进入 `rust_family` 3000 `rust_wasm_dom_shell_host`
- `3104` tree shell proxy 已退到显式 debug/internal 边界;默认主站 iframe 不再请求 `/api/tree/shell -> mnote-web:3104`,避免 `desktop:hot` 下页面树/文件树显示 `{"error":"fetch failed"}`
- `file_tree` 主路径输入已收口到 `kernel file_tree items`
- `buildVisibleRows` 不再从 `pageRows + assets` fallback 重建文件树对象语义。
- `streamDelta` 已可写入 `command_logs``domain_events`
- Rust SSE 与 3000 同源 SSE 都能从单条新 `domain_event.payload.streamDelta` 直接产出 `delta`
- Rust `bridge-runtime` 已具备 `tree.subtree.move` 的可测试 canonical move order plan。
- TS transport 已把 Rust `normalizedMove` 透传到 Convex `documents.move` 的可选校验边界。
- Convex `documents.move` 在收到 `normalizedMove` 时会先确认当前排序 patch plan 与 Rust plan 一致,再执行既有兼容写入。
- Rust SSE 与 3000 同源 SSE 对同一 `command_id` 的 command log + domain event 双写做 delta 去重:delta 一致时发一次 `delta`,不一致继续 `resync`
- `tree.subtree.move` 已从 `replace_documents` 过渡 delta 推进到 `move_document` 细粒度 delta
- Rust `bridge-runtime` 已输出 `streamDeltaHint(kind=move_document)``domainEventHint(tree.subtree.moved)`
- 3000 `/api/tree/commands` 按 Rust plan hint + mutation result 物化并写入 `move_document`
- 3000 SSE 与 Rust SSE 均保留 `documentId / parentId / sortOrder / updatedAt`
- 前端 `tree-delta` reducer 可用该 delta 重建 `sidebar_tree / page_tree / file_tree`
- `tree.node.restore / tree.subtree.copy` 已从 `replace_documents` 过渡 delta 推进到细粒度 upsert
- `restore -> upsert_document`
- `copy -> upsert_documents`
- 前端 `tree-delta` reducer 可用 upsert delta 重建 `sidebar_tree / page_tree / file_tree`
- Rust `bridge-runtime` 已输出正式 tree domain event plan
- `tree.node.created / renamed / archived / restored / purged / embedded`
- `tree.subtree.moved / copied`
- 每条正式 tree command plan 都带 `domainEventPlan(schema=mnote.tree.domain_event, schemaVersion=1, eventType, streamDeltaHint)`
- `recordBridgeCommandArtifacts` 会优先使用 Rust plan 透传的 event type / materialized streamDelta,而不是 `${command}.requested`
- `file_tree` 搜索过滤已接入 Rust projection query 主链:
- `KernelProjectionFilter.query` 已进入协议
- `KernelProjectionFilter.maxResults` 已进入协议
- Rust `kernel.project_view(file_tree)` 已能输出命中项与必要祖先
- Rust 搜索 projection 已按“直接命中项限流,必要祖先不计入上限”执行 `maxResults`
- `mnote-web` `/api/tree/projections/file?...&query=` 与 3000 同源 `/api/tree/projections/file` route 已覆盖
- `mnote-web` 与 3000 同源 route/client 已透传 `maxResults`
- Sidebar 搜索态只消费 Rust 搜索 projection items,旧宿主裁剪 helper 已移除
- tree realtime 主链已补强:
- command log 与 domain event 双写时,即使上一帧 cursor 来自另一条流,也会按时间排除旧 artifact 后再做同 `command_id` delta 去重
- 未识别 domain event payload 继续保守 `resync`
- TS artifact 写入边界已固定 `mnote.tree.domain_event` payload schema v1,并保留历史 snake_case 字段兼容 SSE
- resource command 主链已补强:
- `tree.resource.copy / tree.resource.move` 已由 Rust `bridge-runtime` 生成 `resourceTransferPlan`
- `tree.resource.upload` 已由 Rust `bridge-runtime` 生成 `resourceUploadPlan`
- Convex `mediaAssets.batchCopy / batchMove / createWithStorage` 已按 Rust plan 做可选一致性校验
- `tree.resource.copied / moved / uploaded` 已可生成 `asset_result -> upsert_assets` delta / domain event plan
- 3000 media batch/upload route 已改为调用 Rust artifact writer,按 Rust artifact plan 持久化 `command_logs / domain_events`
- Sidebar 对 copy / move / rename / delete / restore / upload 的资源请求已抽成 `file-tree/resource-command-client`
- file tree 内部 drop 已新增 Rust command preflight
- `tree.filetree.drop.preflight` 由 Rust `bridge-runtime` 归一化 target、row 去重、doc/asset 分类、mindmap subPath、copy/move action
- Rust preflight 会拒绝页面移动到自身/后代
- preflight plan 已输出 `documentTransferPlan``resourceTransferPlan`Sidebar 内部 drop 主路径改为先消费 Rust plan 再触发既有 transport
- 宿主仍负责实际命令触发、乐观 UI、副作用通知和错误提示
- file tree 删除目标已新增 Rust command preflight
- `tree.filetree.delete.preflight` 由 Rust `bridge-runtime` 归一化 row 去重、父页面覆盖过滤、doc/asset 删除目标
- Rust delete plan 会在“选中父页面”时过滤其子页面附件,避免宿主重复构造 asset 删除列表
- Sidebar 删除确认与执行主路径改为先消费 Rust delete plan,再触发既有 document/media transport
- file tree 粘贴目标与复制分类已新增 Rust command preflight
- `tree.filetree.paste.preflight` 由 Rust `bridge-runtime` 归一化剪贴板 row、目标页面、focused row、`doc/index` 复制递归语义、真实 asset 过滤和 mindmap `targetSubPath`
- 3000 同源 `/api/tree/filetree/paste-preflight` route 与 client 已覆盖
- `Sidebar``rust_family` 粘贴主路径先消费 Rust paste plan,再触发既有 `copyTreeCommand` / `copyFileTreeResourceAssets`
- file tree 外部上传目标已新增 Rust command preflight
- `tree.filetree.upload-target.preflight` 由 Rust `bridge-runtime` 归一化 drop target、focused row、active doc、目标 workspace 与 mindmap `targetSubPath`
- 3000 同源 `/api/tree/filetree/upload-target-preflight` route 与 client 已覆盖
- `Sidebar` 外部文件 drop 主路径先消费 Rust upload target plan,再执行既有文件字节读取与 upload transport
- Rust tree shell 已新增可测试 renderer/state family 基础合同:
- `renderer_input` 覆盖 `page / filetree / picker` 的 projection items、expanded ids、focused id、selected row ids、active picker item、excluded ids、command dispatcher
- `mnote-web` tree shell HTML 已嵌入 Rust 构建的 `rendererInput` JSON contract,供 compat host 与后续 final renderer 共用;compat shell 初始 expanded / selection / picker active / exclude 已优先读取该合同
- 3000 默认 `TreeShellRustDomShellHost` 已对齐同一 projection model`page / filetree / picker` 的 projection items 进入 DOM host model`TreeShellIframeHost` 的 appState JSON 只保留在 legacy/debug host。
- `rendererInput` 已继续暴露三类 reducer contract`rust_page_focus_keyboard_reducer_v1``rust_filetree_selection_reducer_v1``rust_picker_state_reducer_v1`
- `mnote-web /tree` debug shell 与 3000 legacy inline compat host 均已消费同名 reducer contract;当前默认 DOM host 已切到 `TreeShellRustDomShellHost`,并通过 WASM artifact 或同源 Rust reduce seam 执行 runtime。
- 前端 `TreeShellHost` 已暴露 `data-tree-renderer-contract=rust_renderer_input_v1`,主路径 `data-tree-host-implementation=rust_wasm_dom_shell_host``mnote_web_iframe_proxy``rust_inline_compat_host``rust_runtime_artifact_host` 不再代表默认主执行面
- `filetree_selection` 覆盖单选、多选、Shift 范围选、右键选中、清空、visible rows 归一化、drag row ids
- `picker_state` 覆盖高亮归一化、上下/Home/End、排除项、Enter pick
- `focus_state` 覆盖 focused id 归一化与 next/previous/home/end 行走
- `expansion_state` 覆盖默认展开、toggle、活动节点祖先展开
- `keyboard_state` 覆盖导航、打开、展开/折叠、上下文菜单 intent
- `drag_drop_state` 覆盖拖拽 payload 去重归一化与 copy/move effect
- `action_registry` 覆盖 page/filetree/picker 的可用动作集合
- `page_renderer` 已输出 `data-rust-page-renderer=initial_v1` 的 page tree 首屏嵌套 HTML,并由 compat runtime 优先 hydrate 该 Rust DOM;后续状态变化仍保留 JS 重绘路径
- `filetree_renderer` 已输出 `data-rust-filetree-renderer=initial_v1` 的 file tree 首屏嵌套 HTML;历史 3000 inline `srcDoc` 曾优先 hydrate 该 DOM,当前默认主路径已由 `TreeShellRustDomShellHost` 承载,legacy iframe 与 `mnote-web /tree` 仅保留 debug/internal 验证边界。
- `picker_renderer` 已输出 `data-rust-picker-renderer=initial_v1` 的 picker 首屏 HTML;历史 3000 inline `srcDoc` 曾优先 hydrate root/item DOM,当前默认主路径已由 `TreeShellRustDomShellHost` 承载,legacy iframe 与 `mnote-web /tree` 仅保留 debug/internal 验证边界。
- Rust artifact 写路径已开始落地:
- `bridge-runtime` 已新增 `RuntimeCommandArtifactPlan`,可从 Rust command plan + mutation result 物化 `commandLog``mnote.tree.domain_event` payload
- `mnote-web /api/tree/commands` 已在 Rust transport 内持久化 `bridgeLogs.recordCommandLog / recordDomainEvent`
- artifact 写入失败不会回滚已成功 mutation,响应会暴露 `artifactError` 供观测
- 3000 `/api/tree/commands``/api/media/batch``/api/media/upload`、文档类 `page/save/lifecycle/metadata/page-write/documents-block` Rust transport adapter 已改为调用 Rust artifact writer
- `documents.duplicate` 已补正式 `tree.node.duplicated` domain event plan3000 duplicate 成功链不再回退 TS artifact helper
- 当前显式 adapter 层已不再直接调用 TS `recordBridgeCommandArtifacts(...)`
- file tree selection 的宿主职责已继续收薄:
- `rust_family``Sidebar` 不再把 legacy React file tree selection reducer 作为选择真相
- `Sidebar` 只从 `tree.filetree.selection.changed` 事件物化 renderer selection snapshot,删除 / 复制 / 粘贴 / 上传 / 内部 drop 只读消费该 snapshot
- `SidebarTreeSurface` 不再向 Rust file tree host 传 `selectedRowIds` 控制 prop,避免宿主反向控制 renderer selection
- compat runtime 在 file tree 重绘与可见行归一化后会继续回发 selection snapshot,避免宿主业务消费旧镜像
- compat runtime 的 Shift 范围选择与右键选中语义已对齐 Rust `filetree_selection` 合同:Shift 默认覆盖旧选择,Ctrl/Cmd+Shift 才叠加;右键已选中行保留多选集合
## 2. 已完成的 final DOM shell 硬边界与仍未完成项
- 2026-04-28 final DOM shell 硬门禁已完成:`TreeShellIframeHost``iframe_srcdoc` browser bridge 不再是默认 page tree / file tree / picker DOM shell。
- `page tree / file tree / picker` 3000 默认主路径已标识为 `rust_wasm_dom_shell_host`,默认浏览器 bridge 为 `data-tree-browser-bridge="dom_wasm"`
- `TreeShellIframeHost` 已降级为显式 legacy/debug host,只能通过 `NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST=1` 进入;旧 `LOCAL_TREE_SHELL_TEMPLATE``buildInline*Html` 不再参与默认路径。
- 默认 DOM host 已通过 `TreeShellRuntimeRequest/Result` 消费 WASM artifact 或同源 Rust HTTP seam 返回的 state、hostEvents、commandEvents;本地 JS fallback reducer 不再参与默认真实流量。
- 仍未完成的是非 DOM shell 范围:`tree.subtree.move``normalizedMove` compat fallback 删除、`document.snapshot.saved` 独立事件、`tree.node.embed` Page Aggregate/Rust artifact 深层收口,以及后续删除 legacy iframe host 的清理。
- 宿主仍承担文件字节读取、外部上传 transport、菜单状态、实际命令调度、乐观 UI 和副作用通知;资源 upload/copy/move、内部 drop preflight、粘贴 preflight、外部上传目标 preflight 的目标与 metadata plan 已进入 Rust command contract。
- `tree.subtree.move` 的 canonical sort plan 已在 Rust 产出并进入 Convex 可选校验,但实际排序写入仍由 Convex `documents.move` 执行。
- `file_tree` 搜索过滤主链已改为 Rust projection query`maxResults / index.md / 附件 / 导图子附件 / book/pdf` 扩展 fixture 已补齐,后续仅保留更丰富搜索语义 hardening。
- `replace_documents` 已退为 restore/copy 的 fallback;主路径 restore/copy 已是 `upsert_document(s)`
- Rust command 已生成 tree/resource delta / domain event plan`mnote-web /api/tree/commands`、3000 tree/media 主写入口与文档类 Rust transport adapter 已可由 Rust artifact writer 落 `command_logs / domain_events`block/save 复合命令已形成 `page.body.saved``block.patched``block.moved``block.embedded` formal domain-event contract。
- block/save 复合命令已并入正式 Rust artifact 主链;OnlyOffice/media writeback 已切到 Rust artifact writer。`document.snapshot.saved` 是否作为独立事件继续留在 `4-16`,不阻塞本文件 final renderer / host thinning 收口。
## 3. Phase Ffinal Rust renderer 接入
### 目标
`page tree / file tree / picker``TreeShellIframeHost` 内联 DOM compat shell,迁到正式 Rust family renderer。
状态:默认主路径已完成迁移,legacy iframe host 仅作为显式排障开关保留。
### checklist
- [x] 固定 Rust renderer 输入合同:
- `projection items`
- `expanded ids`
- `focused id`
- `selected row ids`
- `active picker item`
- `command dispatcher`
- 当前已作为 `rendererInput` 嵌入 `mnote-web` tree shell HTMLruntime DOM shell 已优先读该合同初始化局部状态,但尚未完全改为只消费该合同。
- 历史 3000 inline host 已把 `rendererInput``items` 注入同一个 appState;当前默认 DOM host 直接消费 projection model,不再通过 `__MNOTE_TREE_SHELL_OVERRIDE__` 主路径注入第二份 items。
- 默认 DOM shell 已固定 `family=rust_family / version=1 / executionStrategy=dom_wasm`,并优先使用 3000 同源 `tree-shell-runtime` artifactlegacy iframe manifest 中的 `browserBridge=iframe_srcdoc` 仅保留在显式 legacy/debug host。
- runtime artifact manifest 已新增 `runtimeApi`,显式暴露 `TreeShellRuntimeRequest / TreeShellRuntimeResult`、state snapshot、DOM patch、host event、command event kind。
- runtime artifact manifest 已继续暴露 `reduceEndpoint=/api/tree/runtime/reduce`3000 同源 thin proxy 与 mnote-web endpoint 保留为 fallback/debug。
- 正式 Rust/WASM reducer runtime 已落地:`tree-shell-runtime-wasm` 导出 `reduceTreeShellRuntime`3000 主路径默认优先加载 `mnote-tree-shell-runtime.js``mnote-tree-shell-runtime_bg.wasm` 执行 reduce。
- [x] 拆出 renderer 内部模块:
- [x] row model 雏形(page/filetree/picker renderer test id contract
- [x] page initial DOM renderer(首屏 page tree Rust HTML + hydrate contract
- [x] filetree initial DOM renderer(首屏 file tree Rust HTML contract
- [x] picker initial DOM renderer(首屏 picker Rust HTML contract
- [x] expansion model(默认展开 / toggle / 祖先展开 state family
- [x] focus modelfocused id normalize / next / previous / home / end
- [x] selection modelfiletree selection state family
- [x] picker state model(高亮 / Enter / exclude
- [x] keyboard model(导航 / 打开 / expand-collapse / context menu intent
- [x] drag/drop modelpayload normalize / copy-move effect
- [x] action registrypage / filetree / picker action set
- [x] runtime facade`TreeShellRuntimeRequest / TreeShellRuntimeResult` 可序列化 API,输出统一 DOM patch / host event / command event
- [x] `page tree` 先切 final renderer。
- 当前状态:默认 page tree 由 `TreeShellRustDomShellHost` 渲染 DOMfocus / keyboard / expand / collapse / toggle / open / context menu / move command dispatch 均通过 `TreeShellRuntimeRequest/Result` 返回结果驱动。
- [x] `picker` 以轻量模式复用同一 renderer state family。
- 当前状态:默认 picker 由 `TreeShellRustDomShellHost` 渲染 DOMkeyboard command、hover focus、click pick 均通过 runtime state 与 hostEvents 驱动,测试已禁止默认 iframe postMessage 成功路径。
- [x] `file tree` 后切 final renderer,并保留 `doc / index / asset-folder / asset` 能力。
- 当前状态:默认 file tree 由 `TreeShellRustDomShellHost` 渲染 DOMselection、context menu、open、internal/external drop 与 drop target 通过 runtime result/hostEvents 驱动,并保留 doc/index/asset-folder/asset row 口径。
- [x] compat host 退为 debug/internal fallback(本批可验证范围)。
- 3000 默认主路径不再请求 `/api/tree/shell -> mnote-web:3104`,也不再回退旧 React rendererhost implementation 已切到 `rust_wasm_dom_shell_host``mnote-web /tree``TreeShellIframeHost` 仅保留显式 debug/internal/legacy 验证边界。
### 验收
- 默认真实流量不再以 `rust_inline_compat_host``rust_runtime_artifact_host``TreeShellIframeHost` 作为主路径标识;page / picker / filetree 默认路径已不再依赖 `iframe_srcdoc` 内联 DOM renderer。
- `data-tree-host-implementation` 不再以 `mnote_web_iframe_proxy` 代表主执行面。
- `page tree / file tree / picker` smoke 仍覆盖打开、键盘、选择、拖放、菜单。
## 4. Phase G:宿主状态机收薄
### 目标
宿主只保留挂载、认证、文件读取、transport bridge,不再解释树域交互合法性。
### checklist
- [x] 上传执行从 sidebar 宿主抽成正式 resource command。
- `tree.resource.upload` 已进入 Rust plan / domain event hint / `asset_result` delta
- 浏览器文件字节读取与 Convex upload URL 仍由 3000 upload route 执行
- [x] 内部 file tree drop payload 归一化进入 Rust command preflight。
- `tree.filetree.drop.preflight` 已接 3000 同源 route `/api/tree/filetree/drop-preflight`
- 前端 `file-tree/shell` 只负责从 projection row / parent snapshot 构造 preflight payload
- Rust plan 输出 target、mindmap subPath、doc/asset 分类、top-level doc、source asset documents、`documentTransferPlan``resourceTransferPlan`
- [x] copy/move 区分与非法投放校验进入 Rust command/projection contract。
- `tree.filetree.drop.preflight` 已按 `copy` 输出文档/资源 transfer plan
- 非 copy 的页面移动已在 Rust preflight 拒绝自身/后代投放
- 更完整的跨工作空间/混合对象权限校验仍随正式 Rust 写路径继续下沉
- [x] file tree 删除目标归一化进入 Rust command contract。
- `tree.filetree.delete.preflight` 已接 3000 同源 route `/api/tree/filetree/delete-preflight`
- `Sidebar` 删除链已先消费 Rust delete plan,再执行既有 `deleteDocumentCommand` / `deleteFileTreeResourceAssets`
- legacy `computeFileTreeShellDeleteTargets` 已退出生产代码,只保留 Rust preflight payload builder
- [x] file tree 粘贴目标推导与复制对象分类进入 Rust command contract。
- `tree.filetree.paste.preflight` 已接 3000 同源 route `/api/tree/filetree/paste-preflight`
- Rust paste plan 已输出 `docItems``resourceTransferPlan`,覆盖 `doc -> recursive true``index -> recursive false`、真实 asset 过滤和 mindmap `targetSubPath`
- `Sidebar``rust_family` 粘贴链已不再本地推导 `docItemsMap / copyableAssetIds`,只消费 Rust paste plan 后触发既有 transport
- [x] file tree 外部上传目标推导进入 Rust command contract。
- `tree.filetree.upload-target.preflight` 已接 3000 同源 route `/api/tree/filetree/upload-target-preflight`
- Rust upload target plan 已输出 `workspaceId / targetDocumentId / targetMindmapId / targetSubPath`
- `Sidebar` 的外部文件 drop 链已不再本地推导 target row / focused row / workspace fallback,只消费 Rust upload target plan 后执行既有 upload transport
- [x] 多选、范围选、右键选中状态进入 renderer state family。
- Rust `filetree_selection` 已有纯状态测试;`mnote-web /tree` 与 3000 legacy inline compat runtime 均已通过 `rust_filetree_selection_reducer_v1` 合同入口执行选择、右键、可见行归一化与 drag rows 解析;当前默认 DOM host 已通过 `TreeShellRuntimeRequest/Result` 消费 Rust/WASM runtime。
- [x] picker 高亮、Enter 选中、排除项规则进入 renderer state family。
- Rust `picker_state` 已有纯状态测试;`mnote-web /tree` 与 3000 legacy inline compat runtime 已通过 `rust_picker_state_reducer_v1` 合同入口执行 next/previous/home/end/pick/focus;当前默认 DOM host 已通过 `TreeShellRuntimeRequest/Result` 消费 Rust/WASM runtime。
- [x] 宿主不再持有 file tree 选择真相,只订阅 renderer state event。
- `Sidebar` 已拆分 legacy selection 与 renderer selection snapshot`rust_family` 下只有 `handleFileTreeShellSelectionChange` 写入 renderer snapshot。
- `SidebarTreeSurface` 已移除 `selectedRowIds` 控制输入,file tree 选择只能由 renderer event 回报给宿主业务。
- 仍未完成:compat runtime 内部选择算法虽已统一为 `rust_filetree_selection_reducer_v1` 合同入口,但尚未替换为 Rust runtime/wasm 直接执行,继续归入 Phase F final renderer / runtime 收口。
### 验收
- sidebar 宿主不再包含主要 file tree selection truth 分支;drop legality 仍通过 Rust preflight + 宿主 transport 过渡执行。
- 上传和资源移动失败能返回稳定 command error,不依赖前端临时判断。
- renderer state 可独立测试,不需要挂载整个 sidebar。
## 5. Phase HRust 搜索 projection
### 目标
`file_tree` 搜索过滤不再由宿主裁剪 `kernel file_tree items`,而是由 Rust 输出搜索 projection。
### checklist
- [x] 定义 `file_tree.search` 基础输入:
- query
- root node
- include ancestors
- include assets
- [x] 补齐 `file_tree.search` 扩展输入:
- `index.md` 显式作为可搜索资源;仅自身命中时进入结果,祖先仅用于维持路径
- `maxResults` 限制直接命中项,必要祖先不计入上限
- [x] 定义搜索结果展开策略:
- 命中页祖先展开
- 命中 asset-folder 展开
- 空结果稳定空态
- [x] Rust projection fixture 覆盖:
- mindmap folder
- table
- 命中项必要祖先
- [x] 补齐搜索 fixture 扩展覆盖:
- `index.md`
- 附件
- 导图子附件
- book/pdf
- [x] 前端只消费搜索 projection items,不再计算 visible document ids。
### 验收
- 搜索态、空态、正常态都由同一 Rust projection contract 驱动。
- 前端不再持有搜索过滤的对象语义。
- Phase H 当前验收已满足;后续 richer search semantics 作为 hardening,不再阻塞主链。
## 6. Phase Imove 排序执行下沉
### 目标
`tree.subtree.move` 的排序执行从 Convex compat mutation 迁到 Rust kernel command 主链。
### checklist
- [x] Rust 已有 canonical move order plan 纯函数。
- [x] Rust command plan 已可输出 `normalizedMove`
- [x] Convex `documents.move` 增加可选校验:若传入 Rust `normalizedMove`,执行前确认 patch plan 一致。
- [x] TS transport 对 `documents:move` 透传 `normalizedMove`,避免 Rust plan 停在不可观测状态。
- [x] Rust command plan 已产出正式 `tree.subtree.moved` domain event hint。
- [x] `mnote-web /api/tree/commands` Rust 写路径已可直接持久化正式 `tree.subtree.moved` domain event。
- [x] 前端/stream 不再由 Next route 写 `replace_documents` 作为 move 主 delta。
- [x] 细粒度 move delta reducer 同时更新 `sidebar_tree / page_tree / file_tree`
- [x] `move_document` 不再由 Next route 按 action 手写;当前由 Rust `streamDeltaHint` + mutation result 物化。
- [x] `mnote-web /api/tree/commands``move_document` artifact 已由 Rust transport 直接生成/落库。
- [x] 3000 Next 主写入口已切到 Rust artifact writer。
### 验收
- sort order clamp、同父移动、跨父移动、重复 sort_order、null sort_order、created_at tie-break 均由 Rust 测试锁定。
- Convex 只作为存储执行层,不再持有唯一排序规则。
## 7. Phase J:正式 delta 主链
### 目标
树域 realtime 从 `replace_documents` 过渡 delta,推进到 Rust domain event 驱动的细粒度 delta。
### checklist
- [x] domain event 可携带并被 SSE 解释 `streamDelta`
- [x] command log 与 domain event 同一 `command_id``streamDelta` 一致时,SSE 发一次 `delta` 而不是退回 `resync`
- [x] 定义正式 domain event type hint
- `tree.node.created`
- `tree.node.renamed`
- `tree.node.archived`
- `tree.node.restored`
- `tree.node.purged`
- `tree.node.duplicated`
- `tree.subtree.moved`
- `tree.subtree.copied`
- `tree.resource.copied`
- `tree.resource.moved`
- `tree.resource.uploaded`
- [x] 定义正式 domain event payload schemaTS artifact 写入边界与 Rust artifact writer 均固定 `mnote.tree.domain_event` v1
- [x] Rust 侧由 command plan 生成 delta / event hintNext route 不再按 action 拼主 delta / event type。
- [x] `mnote-web /api/tree/commands` Rust 写路径已由 command result 直接生成并持久化 domain event。
- [x] 显式 compat adapter 写入口已迁出 TS artifact transport。
- [x] block/save-snapshot 复合命令已补齐正式 Rust artifact / domain-event contract。
- 当前完成口径:`page.body.saved` payload 已携带 snapshot 摘要,`block.patched / block.moved / block.embedded` 已有 formal domain-event contract。
- `document.snapshot.saved` 是否拆成独立事件继续留在 `4-16`,不阻塞本阶段收口。
- [x] 前端 reducer 支持细粒度 move。
- [x] 前端 reducer 支持细粒度 restore/copy
- `upsert_document`
- `upsert_documents`
- [x] 前端 reducer 支持资源 upsert delta
- `upsert_assets`
- [x] 未识别事件继续保守 `resync`
### 验收
- 新树命令默认有 domain event delta。
- `replace_documents` 仅作为 fallback,不再是 move / restore / copy 主路径。
## 8. 不做事项
- 不为了“看起来 Rust 化”把 React/DOM compat shell 原样翻译成另一层大壳。
- 不在 sidebar 继续新增树对象真相。
- 不把 `3104` 恢复成默认浏览器入口。
- 不在 Next route 继续扩写长期树命令语义。
@@ -0,0 +1,32 @@
# 4-12 [done] task-003 page tree final renderer 执行清单 v1
> 口径修正(2026-04-28):本文件的 `done` 状态只表示 page tree 的 Rust initial DOM、hydration patch 与 state-family 阶段完成;不表示 `4-11` 所定义的 final Rust DOM shell 已完成。默认主路径仍依赖 `TreeShellIframeHost` / `iframe_srcdoc` 时,不得引用本文宣称 page tree final renderer 已最终收口。
## 目标
`page tree` 默认运行时从 inline compat JS 整树重绘路径继续收口到 Rust family renderer contract
- 首屏 DOM 继续由 `data-rust-page-renderer="initial_v1"` 输出。
- 焦点、active 高亮与键盘移动不再触发 `renderTree()` 整树重绘,只 patch 已 hydrate 的 Rust DOM row 状态。
- 展开/折叠不再把 Rust 初始 DOM 替换成 compat DOM;在已 hydrate Rust DOM 内 patch `aria-expanded`、toggle 文案与子树 fragment。
- `mnote-web /tree` debug shell 与 3000 inline host 保持同一可测试合同,避免默认路径和 debug 路径继续漂移。
## 执行项
- [x] Rust `page_renderer` 初始 HTML 为有子节点的 page row 输出 `tree-node-toggle`,使 Rust DOM 自身具备展开/折叠交互锚点。
- [x] 3000 inline `buildInlinePageTreeHtml` 对齐 Rust 初始 HTML,避免 `TreeShellIframeHost` 继续生成缺少 toggle 的第二份 page DOM 合同。
- [x] 3000 inline runtime 增加 hydrated page DOM patch
- [x] `focusNode``usedRustInitialRenderer` 下只更新 `data-active / data-focused / tabIndex`
- [x] `toggleExpand``usedRustInitialRenderer` 下只 patch 当前节点 `aria-expanded`、toggle 文案与子树显示/插入。
- [x] 新插入的 page 子树 row 复用同一 `bindPageRowEvents`,保留 open/create/rename/menu 事件。
- [x] `mnote-web /tree` debug runtime 对齐同一 page DOM patch 合同,保留 direct debug 验证边界。
- [x] 测试覆盖:
- [x] `cargo test -p mnote-web page_renderer`
- [x] `pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx`
- [x] `node scripts/task112-tree-rust-family-regression-smoke.js`
- [x] 本地 Convex 函数已通过 `npx convex dev --once --tail-logs disable --env-file ../.env.all --run ping:ping` 同步,避免 `documents.move` 运行旧 validator 拒绝 `normalizedMove`
## 非目标
- 本清单不把 file tree 与 picker 一并切 final renderer;它们对应 `task-004``task-005`
- 本清单不恢复 `3104` 默认入口;`/tree` 仍只作为显式 debug/internal 验证边界。
@@ -0,0 +1,37 @@
# 4-13 [done] task-004 picker final renderer state family 执行清单 v1
> 口径修正(2026-04-28):本文件的 `done` 状态只表示 picker state-family / runtime reducer / hydrated DOM patch 阶段完成;不表示 picker 已脱离 `TreeShellIframeHost` 的 inline JS DOM shell。final DOM shell 收口以 `4-18-tree-final-dom-shell-cutover-hard-gate-v1.md` 为准;该硬门禁现已在同目录 `done/` 收口。
## 目标
`picker``rust_family` 默认路径下继续退出 compat JS 高亮与 pick 执行路径,收口到轻量 final renderer state family
- 首屏 DOM 继续由 `data-rust-picker-renderer="initial_v1"` 输出。
- 空查询态、搜索结果态、根目录、排除项使用同一 picker state family 口径。
- 键盘高亮与 Enter pick 只走 `rust_picker_state_reducer_v1` 合同入口,并在 hydrated Rust DOM 上 patch `data-focused / tabIndex`
- 鼠标点击 picker row/root 不再绕过 state family;应先归一化当前 active item,再按同一 pick 结果回传宿主。
- 3000 inline host 与 `mnote-web /tree` debug shell 保持同一可测试合同。
## 执行项
- [x] 补齐 3000 inline picker state family 口径:
- [x] `getPickablePickerEntries` 区分 root 与 doc,并应用 `excludeIds` 后只返回可选项。
- [x] `normalize``focus``next/previous/home/end``pick` 使用同一 pickable 列表。
- [x] 鼠标点击 row/root 通过 state action + `postPickerPickResultToHost`,不直接调用 `handleNavigate` / `tree.pick.root`
- [x] 对齐 `mnote-web /tree` debug runtime
- [x] row/root 点击同样经 state action + pick result helper。
- [x] hydrated Rust DOM 下高亮变化只 patch,不整树重绘;键盘 command 不把焦点抢入 iframe,搜索框焦点保持在宿主输入框。
- [x] 测试覆盖:
- [x] `cargo test -p mnote-web picker_renderer && cargo test -p mnote-web picker_state`
- [x] `pnpm vitest run src/components/documents/move-embed-picker-dialog.test.tsx src/components/sidebar/tree-shell-iframe-host.test.tsx`
- [x] `node scripts/task113-picker-keyboard-regression-smoke.js`
## 完成记录
- 2026-04-26`task113` 的根因是键盘 command 后 hydrated picker row 主动 `focus()`,导致父级搜索输入框失焦;已改为键盘/state patch 只更新 `data-focused / tabIndex`,点击路径才允许 `focusDom: true`
- 2026-04-263000 inline host 与 `mnote-web /tree` debug shell 已同步 `postPickerPickResultToHost`、pickable state family、row/root click state action 路径。
## 非目标
- 本清单不处理 file tree 的 final renderer;对应 `task-005`
- 本清单不拆除 compat host;对应 `task-006`
@@ -0,0 +1,37 @@
# 4-14 [done] task-005 file tree final renderer 能力保留执行清单 v1
> 口径修正(2026-04-28):本文件的 `done` 状态只表示 file tree 的 Rust initial DOM、row contract、selection state family 与能力保留阶段完成;不表示 file tree 已完成 final Rust DOM shell 切换。默认主路径仍依赖 `iframe_srcdoc` 时,不得用本文作为最终完成依据。
## 目标
`file tree``rust_family` 默认路径下继续从 3000 inline compat host 收口到 final Rust renderer state family,同时保留文件树关键能力:
- 首屏 DOM 由 `data-rust-filetree-renderer="initial_v1"` 输出,并覆盖 `doc / index / asset-folder / asset` 行语义。
- 选择状态只通过 `rust_filetree_selection_reducer_v1` 合同入口执行与回报,不再由宿主反向控制。
- 内部拖放、外部文件拖入、右键菜单、双击打开、空白区清空选择与 drop target dataset 不退化。
- 3000 inline host 与 `mnote-web /tree` debug shell 保持同一可测试合同。
## 执行项
- [x] 补齐 3000 inline file tree hydrated runtime
- [x] Rust initial DOM 包含 `doc / index / asset-folder / asset` 行 test id、row kind、document id、asset id、icon hint 与 selected/active dataset。
- [x] `asset-folder``asset` 行保留打开、右键、拖放目标与资源打开 bridge。
- [x] 选择、右键选择、Shift/Ctrl/Cmd 多选、可见行归一化与 drag rows 解析统一走 `rust_filetree_selection_reducer_v1` 镜像入口。
- [x] active/selection/drop feedback 在 hydrated Rust DOM 上 patch dataset,不因宿主 patch 触发整树重绘。
- [x] 对齐 `mnote-web /tree` debug runtime
- [x] debug shell 的 file tree row 语义、selection contract、drag/drop bridge 与 3000 inline host 同步。
- [x] fallback render path 仍可作为 debug/internal 边界,但主路径首屏不再清空 Rust initial DOM。
- [x] 测试覆盖:
- [x] `cargo test -p mnote-web filetree_renderer && cargo test -p mnote-web filetree_selection`
- [x] `pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/lib/file-tree/shell.test.ts src/lib/tree-stream/tree-delta.test.ts`
- [x] `node scripts/task112-tree-rust-family-regression-smoke.js`
## 完成记录
- 2026-04-26:代码对照确认 3000 inline host 与 `mnote-web /tree` debug shell 均已 hydrate `data-rust-filetree-renderer="initial_v1"`,并保留 `doc / index / asset-folder / asset` row contract、selection reducer contract、drag/drop bridge 与资源打开 bridge。
- 2026-04-26task-005 完整验证通过,未发现需要新增生产改动的缺口;本清单作为能力门槛记录。
## 非目标
- 本清单不拆除 compat host 的所有 debug fallback;对应 `task-006`
- 本清单不新增业务命令语义;file tree drop/delete/paste/upload preflight 已在前置任务中下沉 Rust。
@@ -0,0 +1,31 @@
# 4-15 [done] task-006 compat host thinning 执行清单 v1
> 口径修正(2026-04-28):本文件的 `done` 状态只表示本批可验证范围内的 compat host 收薄完成;不表示 compat host 已退为纯 debug/internal,也不表示 `TreeShellIframeHost` 的 JS DOM/state machine 已从默认真实流量删除。最终退场门禁以 `4-18-tree-final-dom-shell-cutover-hard-gate-v1.md` 为准;该硬门禁现已在同目录 `done/` 收口。
## 目标
把本批可验证范围内的 compat host 继续收薄:默认 3000 主路径不再请求 `mnote-web:3104` proxy,不回退旧 React renderer,不再通过 `__MNOTE_TREE_SHELL_OVERRIDE__` 注入第二份主路径 itemspage/filetree/picker 的首屏 Rust initial DOM 与 rendererInput/state family 成为主路径合同。
## 当前边界
- 本任务不声称已经完成完整 Rust runtime / wasm 替换。
- `TreeShellIframeHost` 仍是 3000 主路径的 same-origin inline host;它的职责已收窄为挂载 srcDoc、postMessage transport、状态 patch 与 debug/internal fallback,而不是最终移除。
- 3104 `/tree` 只保留显式 debug/internal 验证边界,`task112` 中 direct tree shell debug 已默认跳过。
## 执行项
- [x] 主路径宿主边界:
- [x] page/filetree/picker 在 `rust_family` 下继续进入同源 iframe host,不回退旧 React renderer。
- [x] 缺少 `workspaceId` 时显示 renderer removed 占位,不恢复旧 React renderer。
- [x] 3000 inline `srcDoc` 主路径使用 `state.items + rendererInput`,不再依赖 `__MNOTE_TREE_SHELL_OVERRIDE__` 作为第二输入真相。
- [x] 状态与事件收薄:
- [x] sidebar refresh/sync 保持事件与语义 key,而不是引入新的树结构重建真相。
- [x] file tree selection 只通过 renderer event snapshot 回宿主业务。
- [x] page/filetree/picker smoke 继续覆盖主路径 iframe host 的导航、右键、双击打开、picker 搜索/选中与 stream fallback。
- [x] 验证覆盖:
- [x] `pnpm vitest run src/components/sidebar/tree-shell-surface.test.tsx src/components/sidebar/sidebar-events.test.ts src/components/sidebar/sidebar-sync.test.ts`
- [x] `node scripts/task112-tree-rust-family-regression-smoke.js`
## 后续剩余
- 完整“compat host 退为 debug/internal fallback”仍需要真正 Rust runtime/wasm 或等价正式 renderer 承接运行时 DOM 与状态机;当前已把本批可验证主路径收薄记录为阶段完成。
@@ -0,0 +1,172 @@
# 4-18 [done] 树域 final DOM shell 切换硬门禁 v1
> 创建时间:2026-04-28
>
> 目的:纠正此前把 `Rust/WASM reducer runtime` 误当成 `final Rust renderer` 的完成口径,重新定义 page tree / file tree / picker 真正收口所需的不可绕过门禁。
>
> 前置文档:
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-11-tree-rust-family-final-renderer-and-host-thinning-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-16-tree-rust-family-remaining-final-runtime-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/1.md`
## 1. 口径纠偏
### 2026-04-28 收口结果
本轮 `task-010``task-013` 已按本文硬门禁完成默认主路径切换:
- `TreeShellHost``rust_family + workspaceId` 下默认选择 `rust_wasm_dom_shell_host`
- `TreeShellIframeHost` 仅在显式 `NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST=1` 时进入,继续作为 legacy/debug 排障 host。
- 默认 page tree / file tree / picker 主路径标注 `data-tree-browser-bridge="dom_wasm"`,负向测试禁止 `iframe_srcdoc` 被当成默认成功路径。
- 默认 DOM host 不再调用 `LOCAL_TREE_SHELL_TEMPLATE``buildInlinePageTreeHtml``buildInlineFileTreeHtml``buildInlinePickerHtml`
- 默认 DOM host 的展开、选择、高亮、打开、右键、拖放、picker pick 由 `reduceTreeShellRuntime` WASM artifact 或同源 `/api/tree/runtime/reduce` Rust seam 返回的 `state / hostEvents / commandEvents` 驱动;本地 JS fallback reducer 只保留在 legacy iframe host 内。
- `task112-tree-rust-family-regression-smoke.js``task113-picker-keyboard-regression-smoke.js` 已支持并验证默认 DOM hostlegacy iframe 仅兼容显式 legacy 路径。
`task-010` 之前,过去几轮已经完成的是:
- Rust renderer input / state family / runtime facade。
- `tree-shell-runtime-wasm` reducer artifact。
- 3000 主路径 wasm-first `reduceTreeShellRuntime`
- page / filetree / picker 的若干 hostEvent / commandEvent / DOM patch 切片。
- `rust_runtime_artifact_host` 主路径标识。
这些都只是 `Rust/WASM reducer runtime-first`,不是 `final Rust DOM renderer`
`task-010` 之前真正没有完成的是:
- 当时 `TreeShellIframeHost` 仍在 3000 主路径生成 `srcDoc`
- 当时 `LOCAL_TREE_SHELL_TEMPLATE` 仍承载 DOM 壳、事件绑定、hydration、fallback renderer 与状态 patch。
- 当时 `data-tree-browser-bridge="iframe_srcdoc"` 仍是默认真实流量的浏览器执行层。
- 当时 page / filetree / picker 的 DOM shell 仍由 JS adapter 维护,Rust/WASM 只主导 reducer result。
因此,后续任何文档、harness、提交说明都不能再把 `task112/task113 通过``rust_runtime_artifact_host``wasmModuleUrl/jsGlueUrl 非空` 作为 final renderer 完成声明;必须同时检查默认 host implementation、浏览器 bridge 标记和 legacy flag 边界。
## 2. 不可绕过的完成定义
只有同时满足以下条件,才允许声明“页面树 / 文件树已切换为 Rust final renderer”:
- 3000 默认真实流量的 page tree / file tree / picker 不再通过 `TreeShellIframeHost``srcDoc` 内联模板承载 DOM shell。
- 默认主路径不再标注 `data-tree-browser-bridge="iframe_srcdoc"`
- `LOCAL_TREE_SHELL_TEMPLATE``buildInlinePageTreeHtml``buildInlineFileTreeHtml``buildInlinePickerHtml` 不再参与默认 page / filetree / picker 渲染路径。
- `TreeShellIframeHost` 只能作为显式 legacy/debug host 存在,必须由明确 debug flag 或 debug route 进入。
- Rust/WASM 或等价 Rust family renderer 持有初始渲染、动态 DOM patch、事件绑定归一化、展开/选择/高亮/拖放反馈的运行时 DOM shell。
- 3000 host 只保留挂载、认证、transport、文件字节读取、postMessage / command bridge、artifact 资源发布等浏览器能力边界。
- page / filetree / picker 的主路径回归测试必须包含负向断言:默认路径不得出现 `iframe_srcdoc` DOM renderer。
- `task112/task113` 仍通过,并且 smoke 需要验证的是新 Rust DOM shell 主路径,而不是旧 inline iframe host。
## 3. 第一优先级:先加负向门禁
下一轮实现前必须先补负向测试,防止继续小步绕开最终目标。
### 前端门禁
- `tree-shell-iframe-host.test.tsx` 或新测试必须断言默认主路径不再输出:
- `srcDoc={renderedInlineSrcDoc}`
- `data-tree-browser-bridge="iframe_srcdoc"`
- `LOCAL_TREE_SHELL_TEMPLATE`
- `buildInlineInitialTreeHtml`
- `buildInlinePageTreeHtml`
- `buildInlineFileTreeHtml`
- `buildInlinePickerHtml`
- 允许这些字符串只出现在显式 `legacy/debug` 测试或旧 host 文件中,但不能在默认 host implementation 测试里作为成功条件。
### smoke 门禁
- `task112-tree-rust-family-regression-smoke.js` 需要确认 page/filetree 默认 renderer host 不是 inline srcdoc host。
- `task113-picker-keyboard-regression-smoke.js` 需要确认 picker 默认 renderer host 不是 inline srcdoc host。
- direct `/api/tree/shell -> 3104` debug disabled 仍应保留,但它不能替代 final DOM shell 验收。
### 文档门禁
- `harness` 任务完成条件必须包含“不依赖 iframe srcdoc DOM shell”。
- 任一阶段文档若只完成 reducer/state family,标题和完成记录必须写成 `runtime reducer/state-family stage`,不得写成 `final renderer completed`
## 4. 实现路线
### Phase P:冻结旧 inline host 主路径
状态:已完成。
目标:把旧 `TreeShellIframeHost` 从“默认主路径”降级为“legacy/debug 候选”。
执行项:
- 新增主路径 host 名称,例如 `rust_dom_shell_host``rust_wasm_dom_shell_host`
- 保留旧 `TreeShellIframeHost`,但改名或包裹为 `LegacyTreeShellIframeHost`,只允许 debug/legacy flag 进入。
- `TreeShellHost` 默认选择新 host;旧 host 不再作为 `rust_family` 默认实现。
- 所有旧 `srcDoc` 合同测试迁到 legacy/debug 测试组。
验收:
- 默认 `TreeShellHost` 测试不再匹配 `iframe_srcdoc`
- legacy/debug 测试仍能证明旧 host 可用于回退排障。
### Phase Q:建立 Rust/WASM DOM shell host
状态:已完成默认主路径切换。
目标:给 page / filetree / picker 建立一个不依赖 inline JS template 的 DOM shell 承载。
执行项:
- 复用现有 `rendererInput``TreeShellRuntimeRequest/Result`
- WASM artifact 输出或驱动 DOM shell 初始化,不再由 `buildInline*Html` 生成主路径 HTML。
- DOM patch、hostEvent、commandEvent 的应用入口统一放在新 host adapter,adapter 只负责浏览器边界,不维护树状态机。
- page tree 先切,再切 picker,最后切 filetree。
验收:
- page / picker / filetree 默认主路径首屏可见。
- 键盘、展开、选择、右键、打开、拖放反馈继续工作。
- `task112/task113` 使用新 host 通过。
### Phase R:删除默认路径 JS renderer 状态机
状态:已完成默认路径隔离;旧 JS renderer/state machine 仅保留在 legacy iframe host。
目标:移除默认主路径对旧 JS DOM renderer/state machine 的依赖。
执行项:
- 默认路径删除对 `renderTree()``renderFileTree()``hydrateInitialPageTree()``hydrateInitialPickerTree()` 等旧 DOM shell 的调用。
- 旧本地 fallback reducer 只保留在 legacy/debug host,不参与默认真实流量。
- 删除或隔离 `buildInlinePageTreeHtml` / `buildInlineFileTreeHtml` / `buildInlinePickerHtml` 的默认路径调用。
验收:
- 默认 host implementation 中没有 `srcDoc` 主渲染链。
- 默认主路径没有第二套 JS 展开、选择、高亮、拖放接受规则。
- `TreeShellIframeHost` 可被删除,或只以 `LegacyTreeShellIframeHost` 形式留在 debug 目录。
### Phase S:最终文档收口
状态:已更新 4-11、4-16、4-18 的真实代码状态。
目标:只在真实代码满足门禁后移动文档状态。
执行项:
- `4-11``4-16``4-18` 中所有 final DOM shell gate 勾选。
- 将真正完成后的最终清单移动到 `done/`
-`4-12``4-15` 的旧标题继续误导,则移动到 `design/old/done` 或在标题中明确标记 `state-family stage`
验收:
- 文档状态与默认真实流量一致。
- 没有任何 `done` 文档暗示 `iframe_srcdoc` 主路径等于 final Rust renderer。
## 5. 非目标
- 不拆除 Convex substrate。
- 不恢复 `3104` 为默认浏览器入口。
- 不把文件字节读取、浏览器 DnD `File[]` 对象直接塞入 Rust/WASM。
- 不因为切 DOM shell 而重写 tree command / projection / artifact contract。
- 不把 `task112/task113` 的旧 inline host smoke 当作最终通过标准。
## 6. 当前下一步
本文定义的 final DOM shell 硬门禁已收口。后续继续推进时,不应再回到 `iframe_srcdoc` 默认路径,而应沿以下方向继续减少过渡面:
1. 保留 `NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST=1` 作为短期排障开关,后续在 smoke 与线上观测稳定后删除旧 host。
2. 继续把剩余 tree command、Page Aggregate、snapshot 独立事件等非 DOM shell 收口项按各自设计稿推进。
3. 若新增 page / filetree / picker 交互,默认必须接入 `TreeShellRuntimeRequest/Result`,不得在 DOM host 增加第二套 JS reducer。
@@ -0,0 +1,235 @@
# 4-2 [done] Sidebar / 页面树 / 文件树 产品交互合同 v1
> 更新时间:2026-04-17
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-1-sidebar-pagetree-filetree-product-gap-analysis-v1.md`
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
## 1. 文档目的
这份文档不是继续讨论“是否要做 Rust tree shell”。
这份文档要冻结的是:
- 页面树对标 Wolai / Notion 的最小产品交互合同
- 文件树对标 VS Code Explorer 的最小产品交互合同
- 当前旧树已经具备的能力基线
- 新 Rust tree shell 必须补齐的能力矩阵
- 后续 `projection / command / row model / selection model / focus model / keyboard / DnD / context menu` 的最低验收口径
也就是说,这份文档是下一阶段多人并行推进时的共同合同,而不是描述性分析。
---
## 2. 基本原则
- 页面树 / 文件树都不是事实源,它们都只是 `tree-first graph kernel` 的 projection。
- Sidebar 是壳,不是树真相。
- 新实现不能以“能显示树结构”作为完成标准,而要以“不比旧交互与 UI 差”作为最低标准。
- 文件树与页面树允许在 UI 上不同,但必须共享同一套 projection 与 command 主骨架。
- 所有新增能力都应优先落到可测试的 `row model / selection model / focus model / keyboard / DnD` 层,而不是先堆散落 UI 事件。
---
## 3. 页面树合同
### 3.1 对标目标
- 功能对标:Wolai / Notion 页面树
- 视觉与节奏对标:轻量、低干扰、hover 才显动作、不是调试面板
### 3.2 页面树必须具备的最低能力
- 稳定的页面层级展开 / 折叠
- 当前页高亮与祖先自动展开
- 行级 hover 动作区
- 新建子页面
- 重命名
- 页面移动
- 上下文菜单入口
- 焦点与键盘导航
- 基础拖拽排序
- 搜索过滤后仍保持树层级可理解
### 3.3 页面树必须保留的旧能力基线
- 右键菜单不是只有重命名/删除,而应保留工作区级高频动作入口
- 页面树不能退化成纯按钮列表
- 大树场景不能因切流而失去稳定滚动体验
- 主树 consumer 不能重新持有第二套结构真相
---
## 4. 文件树合同
### 4.1 对标目标
- 功能对标:VS Code Explorer
- 视觉密度对标:资源管理器,而不是文档树换皮
### 4.2 文件树必须具备的最低能力
- 文件树专用 row model
- 页面、`index.md`、附件、mindmap 文件夹、导图子附件的复合资源层级
- 单选
- 多选
- Shift 范围选
- 右键菜单入口
- 双击打开资源
- 基础键盘导航
- 目录拖拽骨架
- 外部文件拖入上传骨架
- 资源图标语义
- 资源类型菜单分支
### 4.3 文件树必须保留的旧能力基线
- 不能退化成“页面树加附件列表”
- 不能丢掉多选与范围选
- 不能丢掉资产级操作入口
- 不能丢掉内部拖拽/复制与外部文件拖入的扩展空间
- mindmap 相关资源不能被拍平成普通附件列表
---
## 5. 统一协议合同
### 5.1 Projection 合同
所有树 consumer 必须能明确声明自己消费哪一种 projection
- `sidebar_tree`
- `page_tree`
- `file_tree`
最小字段基线:
- `row_id`
- `node_id`
- `parent_node_id`
- `node_type`
- `projection_kind`
- `depth`
- `position`
- `title`
- `capabilities`
- `resource_meta`
文件树扩展字段:
- `resource_kind`
- `asset_kind`
- `icon_hint`
- `expandable`
- `expanded_by_default`
### 5.2 Command 合同
命令面至少要为后续产品交互预留稳定口径:
- `tree.node.create`
- `tree.node.rename`
- `tree.subtree.move`
- `tree.node.archive`
- `tree.node.restore`
- `tree.asset.attach`
- `tree.asset.detach`
### 5.3 本地 UI 状态合同
以下状态不应回流为结构真相,只能留在 UI 本地状态层:
- `expanded`
- `selected`
- `hover`
- `focus`
- `dragging`
- `drop target`
---
## 6. 状态骨架合同
### 6.1 row model
必须单独存在,不能散落在 renderer 中。
最低要求:
- 页面树与文件树都能从 projection 映射到稳定 row model
- row model 可以独立测试
- row model 不再重新定义结构真相
### 6.2 selection model
最低要求:
- 单选
- 多选
- Shift 范围选
- 右键选中
- 可见行变化后的选择归一化
### 6.3 focus model
最低要求:
- 当前焦点行稳定可追踪
- 焦点与选中不完全等价
- 页面树与文件树都能共享焦点层定义
### 6.4 keyboard
最低要求:
- 上下导航
- 左右展开/折叠
- Enter 打开
- 文件树预留 copy / paste / delete 的按键入口
### 6.5 DnD
最低要求:
- 页面树支持基础排序/移动
- 文件树支持目录拖放骨架
- 文件树支持外部文件拖入的扩展点
- 非法投放校验必须可测试
### 6.6 context menu
最低要求:
- 页面树与文件树都有统一的 context menu 入口模型
- 菜单本身可按资源类型分支
- shell 与宿主之间要有稳定动作协议,而不是只靠壳内 `prompt/alert`
---
## 7. 当前判定
截至 2026-04-28 final DOM shell 收口后:
- `page_tree`:默认主路径已进入 `rust_wasm_dom_shell_host + dom_wasm`,展开/折叠、当前页高亮、祖先展开、行级动作、重命名、移动、上下文菜单、键盘与基础拖拽均由 Rust runtime contract 驱动或回放。
- `picker`:轻量选择器已复用同一 renderer state family,键盘高亮、Enter 选中、根节点与排除项逻辑已进入默认 DOM host。
- `file_tree`:默认主路径已进入 `rust_wasm_dom_shell_host + dom_wasm`,页面、`index.md`、附件、`mindmap` 文件夹、资源图标、单选/多选/范围选、右键、双击打开、内部拖放与外部文件拖入均保留。
本合同的产品交互最低门槛已由 `4-11``4-18` 收口;剩余 `normalizedMove` fallback、`document.snapshot.saved` 独立事件、`tree.node.embed pageReference` 等属于后续 Page Aggregate / runtime 深层收口,不再阻塞本文。
- `filetree` 模式闭环已由 Rust projection、runtime state family、preflight 与默认 DOM host 共同承接。
- 页面树 / 文件树正式交互合同已固定,并由 `task112/task113` smoke 与组件/Rust 测试覆盖。
- row model / selection model / focus model / keyboard / DnD / context menu 骨架已进入 renderer state family 与 runtime facade。
---
## 8. 完成判定
只有同时满足下面几条,才可以宣称“新树不比旧交互与 UI 差”:
- 页面树满足本合同第 3 节最低能力
- 文件树满足本合同第 4 节最低能力
- projection / command 合同不再漂移
- `row model / selection model / focus model / keyboard / DnD / context menu` 有独立实现与测试
- filetree 不再依赖协议裂缝或壳内临时拼装来维持主路径
@@ -0,0 +1,218 @@
# 4-4 [done] Tree Projection Protocol Contract v1
> 更新时间:2026-04-18
>
> 关联:
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
## 1. 目的
这份文档用于冻结树域 projection contract,避免 `sidebar_tree``page_tree``file_tree` 在后续 Rust route、Leptos tree shell、Next 挂载切流阶段继续各自长字段。
这里固定三条原则:
- 树域只消费 projection,不消费前端自己拼出来的结构真相
- Rust 与前端共享同一组 projection 字段语义
- `sidebar_tree``page_tree``file_tree` 是同一协议家族,不是三套无关返回值
## 2. 协议分层
### 2.1 Rust canonical contract
Rust 侧的 canonical contract 统一表达为:
- `projection_id`
- `projection`
- `root_node_id`
- `items`
- `edges`
`items` 里的字段语义冻结如下。
### 2.2 TypeScript transport contract
前端 TypeScript 侧继续使用 camelCase 字段,但语义必须与 Rust 一致:
- `projectionId`
- `projectionKind`
- `rootNodeId`
- `parentNodeId`
- `resourceMeta`
- `iconHint`
- `expandedByDefault`
也就是说:
- Rust 是 canonical truth
- TypeScript 只是命名风格映射,不允许再引入第二套语义
## 3. 共享字段
下面这些字段属于 `sidebar_tree``page_tree``file_tree` 的共享基线。
| canonical | TS | 说明 |
| --- | --- | --- |
| `node_id` | `nodeId` | 当前 projection item 对应的 kernel node |
| `parent_node_id` | `parentNodeId` | 上级 node,根节点为 `null` |
| `node_type` | `nodeType` | `workspace/folder/page/section/asset/book/pdf/mindmap/table/index` 等语义类型 |
| `projection_kind` | `projectionKind` | 当前 item 属于哪种 projection`sidebar_tree/page_tree/file_tree` |
| `title` | `title` | 展示标题,允许兜底为“无标题” |
| `depth` | `depth` | 当前树深度 |
| `position` | `position` | 同级排序位置 |
| `child_count` | `childCount` | 子项数量 |
| `expandable` | `expandable` | 当前 item 是否理论上可展开 |
| `expanded_by_default` | `expandedByDefault` | 默认展开建议 |
| `capabilities` | `capabilities` | 当前行允许的交互能力 |
| `resource_meta` | `resourceMeta` | 绑定到业务资源的元信息 |
| `icon_hint` | `iconHint` | 给 shell / renderer 的图标提示 |
## 4. `capabilities` 冻结口径
当前允许的共享 `capabilities` 为:
- `expand`
- `open`
- `drag`
- `drop`
- `select`
- `create-child`
- `rename`
- `archive`
- `restore`
- `context-menu`
- `reorder`
- `open-asset`
- `pick`
约束:
- `capabilities` 只表达“允许做什么”
- 它不表达局部 UI 状态
- 它不表达 hover、selected、dragging、drop target 这类临时态
## 5. `resource_meta` 冻结口径
`resource_meta` 统一用于表达 projection item 背后的真实资源。
共享字段:
- `resource_kind`
- `document_id`
- `asset_id`
- `workspace_id`
- `asset_kind`
- `icon_hint`
- `extra`
### 5.1 `resource_kind`
当前冻结为:
- `workspace`
- `document`
- `index`
- `asset`
- `asset_folder`
- `mindmap`
- `table`
- `book`
- `pdf`
### 5.2 `asset_kind`
当前冻结为:
- `file`
- `mindmap`
- `table`
- `book`
- `pdf`
- `image`
- `video`
- `audio`
- `unknown`
## 6. 三类 projection 的差异字段
### 6.1 `sidebar_tree`
`sidebar_tree` 是最轻的导航 projection。
要求:
- 保留共享字段
- 不引入 `row_id`
- 资源主语义通常落在 `resource_kind=document`
- 允许后续继续作为 workspace 首屏 / stream snapshot 主链
### 6.2 `page_tree`
`page_tree` 是页面树 projection。
它在共享字段基础上新增:
- `row_id`
当前 `row_id` 命名约束:
- `page:<nodeId>`
要求:
- `page_tree` 必须可直接生成 picker 轻量列表
- `page_tree` 必须可直接生成可见 rows
- `page_tree` 不允许重新回退成前端自行 flatten 嵌套树
### 6.3 `file_tree`
`file_tree` 是页面树骨架上的更宽对象投影。
它在共享字段基础上强调这些字段必须稳定存在:
- `resource_kind`
- `asset_kind`
- `icon_hint`
- `expandable`
- `expanded_by_default`
要求:
- `file_tree` 不能继续由前端用 `asset-folder` / `asset` / `index` 临时猜语义作为长期真相
- `file_tree` 必须由 Rust 直接输出 `document/index/asset/asset_folder/mindmap/table/book/pdf` 等对象投影
## 7. 当前稳定项与过渡项
### 7.1 已稳定
- `sidebar_tree` 基础 contract
- `page_tree``row_id/node_id/parent_node_id/depth/position/capabilities/resource_meta`
- `expanded_by_default`
- `child_count`
### 7.2 仍处于过渡
- `file_tree` 里的 `asset_folder/asset/index` 仍有前端 adapter 残留
- `icon_hint` 还没有完全由 Rust 主导
- `asset_kind` 仍需要从更宽对象类型扩展到 `book/pdf`
## 8. Fixture 与 Contract Test 要求
后续所有树域 fixture / contract tests 至少覆盖:
- `sidebar_tree` 基础 fixture
- `page_tree` fixture
- `file_tree` fixture
- `resource_meta` 差异字段
- `capabilities`
- `icon_hint`
- `expanded_by_default`
## 9. 完成判定
当以下条件同时成立时,视为本 contract 冻结完成:
- `sidebar_tree``page_tree``file_tree` 的共享字段与差异字段都已写成文档与共享类型
- Rust 与前端共享同一组字段语义
- `file_tree` 不再依赖前端“补字段猜语义”
- 后续 shell / renderer / route 改造不再新增破坏性字段
@@ -0,0 +1,174 @@
# 4-5 [done] Tree Command Envelope 第一批收口方案 v1
> 更新时间:2026-04-17
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
## 1. 文档目标
这份文档用于固定 Stage B-2 的真实边界:
- 前端已经不应该再扩散树命令语义。
- 但当前仓库里,浏览器组件层仍然散落着大量 `fetch("/api/documents/...")` 入口。
- 第一批要先收口 `create / rename / move`,把前端限制为 optimistic UI 与用户交互壳。
这里的核心口径是:
> 前端只保留 optimistic UI,真语义和 command envelope 收口到 Rust/bridge。
## 2. 当前命令入口盘点
### 2.1 Sidebar 主入口
当前树命令入口主要集中在:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/menus/CustomSideMenu.tsx`
其中:
- `sidebar.tsx`
- `create``/api/documents/create`
- `rename``/api/documents/title`
- `move``/api/documents/move`
- `delete``/api/documents/delete`
- `restore``/api/documents/restore`
- `purge``/api/documents/purge`
- `embed``/api/documents/embed`
- `copy-tree``/api/documents/copy-tree`
- `document-content.tsx`
- 文档标题更新走 `/api/documents/title`
- 移动页走 `/api/documents/move`
- 页面嵌入走 `/api/documents/embed`
- `CustomSideMenu.tsx`
- block 转子页面走 `/api/documents/create-child`
- `pageReference` 删除走 `/api/documents/delete`
### 2.2 当前哪些 route 已接到 envelope
第一批最关键的三条主路径,已经接入 `buildDocumentCommandEnvelope`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/create/route.ts`
- 命令名:`documents.create`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/title/route.ts`
- 命令名:`documents.title.update`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/move/route.ts`
- 命令名:`documents.move`
当前真实执行链路是:
1. Next route 构造 `buildDocumentCommandEnvelope`
2. `resolveRustBridgeCommandPlan`
3. TS runtime 负责 transport dispatch
4. Convex mutation 负责实际持久化
因此当前形态是:
- `Convex substrate`
- `Rust semantic owner`
- `TS runtime transport dispatcher`
不是“Rust 已经直接替代 Convex 落库”。
### 2.3 当前仍留在前端的树语义
虽然 route 已接入 envelope,但浏览器端仍残留较多树语义:
- `sidebar.tsx`
- 本地 `insertNode` / `moveLocalNode`
- 非法拖拽判断
- `targetParentId` / `position` 推导
- `copy-tree` 的递归策略与拖放目标推导
- `document-content.tsx`
- 移动页时仍由前端给出固定 `position`
- `CustomSideMenu.tsx`
- block 转子页面的 UI 级决策仍在前端
这说明当前主问题已经不是 route 是否接 bridge,而是:
> 命令 transport 已经收口,但命令入口和部分树策略仍散落在前端组件层。
## 3. 第一批 cutover 范围
Stage B-2 第一批只处理:
- `create`
- `rename`
- `move`
原因:
- 这三条覆盖 sidebar 主交互、文档页主交互、block 转页入口。
- 它们已经具备稳定的 route -> envelope -> Rust bridge -> Convex 主链。
- 继续向前推进时,前端只需要保留 optimistic UI 和错误提示,不需要继续扩散 transport 细节。
## 4. 第一批 cutover 顺序
推荐固定为两段:
### 4.1 第一段:先 `create / rename / move`
目标:
- 浏览器组件不再直接散落 `fetch("/api/documents/create|title|move")`
- 收成共享的 tree command client
-`sidebar.tsx``document-content.tsx``CustomSideMenu.tsx` 只表达交互和 optimistic UI
这一步已经开始落地:
- 新增共享入口:
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tree-command-client.ts`
- 第一批已接入的组件:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/menus/CustomSideMenu.tsx`
### 4.2 第二段:再 `delete / restore / purge / embed`
原因:
- `delete / restore / purge` 已接 envelope,但仍有多处页面级分叉入口。
- `embed` 当前最特殊,它不是独立 `documents.embed` 命令,而是 TS adapter 里拼 `pageReference` 后走 `documents.save`
- `embed` 必须等 Rust kernel 定义正式页面嵌入语义后,再从“TS adapter 语义”切成“Rust tree command 语义”。
## 5. 不纳入第一批的内容
以下内容先不纳入 Stage B-2 第一批:
- `copy-tree`
- 当前仍强依赖前端拖放和递归策略
- block 级 `blocks.move / blocks.embed`
- 这是块域命令,不是页面树命令主链
- 树排序算法本身
- 当前仍有一部分位置推导在前端,后续要继续收回 Rust kernel
## 6. 长期边界说明
长期固定如下:
- 前端负责:
- 用户交互
- optimistic UI
- 选择器、确认框、拖拽体验
- Next route / shared client 负责:
- 稳定 transport 边界
- 请求格式统一
- Rust kernel / bridge 负责:
- `tree command` 语义
- `page_tree / sidebar_tree / file_tree` 真相
- 审计、trace、版本口径
- Convex 负责:
- 持久化
- 订阅
- 实时底座
## 7. 下一步建议
Stage B-2 后续应继续做三件事:
1.`delete / restore / purge` 也收成同一层 shared tree command client。
2.`embed` 定义独立的 Rust page-tree 命令,而不是继续停留在 TS adapter + `documents.save`
3.`targetParentId / sortOrder / subtree move legality` 等策略继续从前端移回 Rust kernel。
@@ -0,0 +1,102 @@
# 4-6 [done] Tree Command Protocol Cutover Stage 2 v1
> 更新时间:2026-04-18
>
> 目的:冻结树域 command protocol 的长期命名面,并明确 `documents.*` 到 `tree.*` 的兼容迁移口径。
## 1. 结论
长期命名面不再继续扩大 `documents.*`
固定迁移方向为:
- 兼容层继续保留 `documents.*`
- 长期正式协议统一切到 `tree.*`
## 2. 长期正式命名
冻结如下:
- `tree.node.create`
- `tree.node.rename`
- `tree.node.archive`
- `tree.node.restore`
- `tree.node.purge`
- `tree.subtree.move`
- `tree.subtree.copy`
- `tree.asset.attach`
- `tree.asset.detach`
- `tree.node.embed`
## 3. 当前兼容映射
| 兼容命名 | 长期命名 | 说明 |
| --- | --- | --- |
| `documents.create` | `tree.node.create` | 新建页面 |
| `documents.title.update` | `tree.node.rename` | 重命名页面 |
| `documents.move` | `tree.subtree.move` | 移动子树 |
| `documents.delete` | `tree.node.archive` | 软删除进入回收站 |
| `documents.restore` | `tree.node.restore` | 从回收站恢复 |
| `documents.purge` | `tree.node.purge` | 永久删除 |
| `documents.embed` | `tree.node.embed` | 嵌入页面 |
| `documents.copy_tree` | `tree.subtree.copy` | 复制树 |
## 4. Cutover 顺序
### Stage 2A
- 先在 Rust route / bridge 上接受 `tree.*`
- 同时保留 `documents.*` alias
- 前端 command client 开始显式知道两套名字的对应关系
### Stage 2B
- 主调用路径默认发 `tree.*`
- 兼容入口只用于旧 route / 旧测试 / 旧调试脚本
### Stage 2C
- 删除主路径对 `documents.*` 的依赖
- 仅保留极薄 alias,或在最终阶段删除 alias
## 5. 命名边界
### 5.1 `tree.node.*`
用于单节点生命周期命令:
- `tree.node.create`
- `tree.node.rename`
- `tree.node.archive`
- `tree.node.restore`
- `tree.node.purge`
- `tree.node.embed`
### 5.2 `tree.subtree.*`
用于结构级命令:
- `tree.subtree.move`
- `tree.subtree.copy`
### 5.3 `tree.asset.*`
用于资源挂接:
- `tree.asset.attach`
- `tree.asset.detach`
## 6. 当前约束
- 前端组件不再自己扩散树语义
- tree command client 是唯一稳定入口
- route / bridge / kernel command 的映射必须集中管理
## 7. 完成判定
当以下条件同时成立时,视为 Stage 2 完成:
- 文档明确记录 `documents.*``tree.*` 的映射
- Rust / 前端都接受 `tree.*`
- 主调用路径默认走 `tree.*`
- `documents.create` 等兼容命名不再是新增主语义入口
@@ -0,0 +1,110 @@
# 4-7 [done] Tree Shell UI State Boundary v1
> 更新时间:2026-04-18
>
> 目的:明确哪些状态属于 projection,哪些状态只能留在 tree shell / renderer 本地。
## 1. 结论
树域必须严格区分:
- projection truth
- local UI state
否则 Rust projection、Leptos tree shell、Next 挂载切流会继续把状态缠回旧前端壳。
## 2. 属于 projection 的状态
这些状态必须来自上游 projection 或 command 结果,而不是由 UI 自行猜测:
- `nodeId`
- `parentNodeId`
- `projectionKind`
- `nodeType`
- `title`
- `depth`
- `position`
- `childCount`
- `expandable`
- `expandedByDefault`
- `capabilities`
- `resourceMeta`
- `iconHint`
说明:
- projection 只描述“结构真相”和“允许做什么”
- projection 不承载 hover、selected、dragging 等临时交互态
## 3. 只能留在 UI 本地的状态
这些状态只能存在于 tree shell / renderer 本地:
- `expanded`
- `selected`
- `hover`
- `focus`
- `dragging`
- `drop target`
- `context menu open`
- `keyboard navigation anchor`
说明:
- `expandedByDefault` 是 projection 建议值
- `expanded` 是本地会话态,不直接回写 projection
## 4. 边界规则
### 4.1 `expanded`
- 初始值可由 `expandedByDefault` 推导
- 运行期展开/折叠必须只保留在本地状态
- 不能把 `expanded` 反向写回 projection item
### 4.2 `selected`
- `selected` 属于当前用户当前会话的局部状态
- 不能作为 projection 的共享字段
### 4.3 `focus`
- `focus` 属于 keyboard / accessibility 层本地状态
- 不能参与结构真相
### 4.4 `dragging` / `drop target`
- `dragging``drop target` 是瞬时状态
- 只能存在于 DnD 状态机
- drop 成功后,真正持久化的是 `tree.subtree.move` 等命令结果
## 5. Rust 与前端共识
Rust 负责:
- 输出 projection
- 接收 command
- 返回 command result / resync snapshot
前端或 Leptos shell 负责:
- `expanded`
- `selected`
- `hover`
- `focus`
- `dragging`
- `drop target`
## 6. 对 `page_tree` / `file_tree` / picker 的影响
- `page_tree``file_tree`、picker 必须共用同一套本地状态边界
- picker 只是交互能力更窄,不是另一套状态模型
- `keyboard``selection``focus``dragging` 的命名与语义必须一致
## 7. 完成判定
当以下条件同时成立时,视为状态边界冻结完成:
- 文档明确列出 projection 与本地状态
- Rust 和前端都不再把 `expanded/selected/focus/dragging/drop target` 写进 projection
- 后续 Leptos shell 与 Next consumer 使用同一份边界说明
@@ -0,0 +1,75 @@
# 4-8 [done] Tree Shell Cutover Performance Report v1
> 更新时间:2026-04-18
>
> 测试环境:
> - 前端主站:`http://127.0.0.1:3000`
> - Rust Web`http://127.0.0.1:3104`
> - 浏览器:Playwright Chromium headless
> - 数据口径:同一工作区下真实 Sidebar + 临时父/子页面样本 + move/embed picker 对话框
## 1. 测量口径
- `首包`
- 指主树或 picker 打开后,到对应 surface 首次可见的时间。
- `首次可交互`
- 指 surface 已出现,且首个可点击行/按钮已可交互。
- `切页延迟`
- 指点击页面树或文件树行,到浏览器 URL 切到目标页面的时间。
- `大树展开延迟`
- 指点击页面树真实可展开节点的展开/收起往返显隐完成时间。
- `fallback`
-`/api/stream/events` 被阻断或失败时,Sidebar 仍能在主站内回到可见可用树视图的结果。
## 2. 实测结果
### 2.1 首页与切流 smoke
- `task097-homepage-entry-smoke.js`
- `/auth` 返回 `200 text/html`
- `/` 返回 `307 -> /auth`
- `3104 /health` 返回 `200`
### 2.2 Sidebar 主树 cutover
- `task101-tree-shell-cutover-smoke.js`
- `首包 / 首次可交互``9ms`
- `切页延迟``205ms`
- `stream``/api/stream/events?stream=workspace&projection=sidebar_tree&workspaceId=...`
- `fallback`:阻断 stream 后仍在 `22ms` 内显示可用主树
### 2.3 filetree / picker 回归
- `task102-tree-shell-regression.js`
- `filetree 首包 / 首次可交互``6ms`
- `filetree 切页延迟``179ms`
- `picker 首包 / 首次可交互``264ms`
- `picker 搜索输入后结果/空态稳定出现``1005ms`
### 2.4 页面树展开样本
- 真实可展开节点往返展开/收起延迟:`124ms`
## 3. 对照与结论
- 旧 iframe/postMessage 实验壳已退出 Sidebar / filetree / picker 主路径。
- 当前默认主路径已经是 Next 内统一 tree surface + stream/projection 主链。
- `3104` 与 Convex 连接正常时,主树按 stream 路径工作;`3104`/stream 失败时,主树仍能通过 fallback 保持可见。
- 当前开发机口径下,主路径交互均在亚秒级,未再出现此前 `3000` 因 Convex/tree shell 卡死而无响应的问题。
## 4. fallback 结果
- `fallback` 触发条件:
- 主动阻断 `/api/stream/events`
- 观测结果:
- 页面不空白
- Sidebar 主树仍渲染
- 可继续看到目标页面树节点
- 结论:
- `fallback` 已从“实验性兜底”变成正式切流护栏
## 5. 残余风险
- 当前指标来自本地开发环境与小样本 smoke,不代表生产环境大规模树深/大附件工作区的长期 p95。
- picker 搜索链路仍受搜索索引可见性影响;本轮已验证“搜索后结果或空态稳定出现”,但未把索引收敛时间当作树域切流 blocker。
- 后续如继续增强 Rust-first renderer,可在不改变当前 cutover 结论的前提下,把这份报告升级为多样本趋势报表。
@@ -0,0 +1,944 @@
# 4 [done] Sidebar / 页面树 / 文件树 Rust Web 重构方案 v1
> 更新时间:2026-04-22
>
> 当前优先级入口:
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/done/3-2-tree-first-graph-kernel-phase3-task-breakdown-v1.md`
> - `/mnt/Data1T/mnote/design/90-reference/90-2-yemianshu.md`
> - `/mnt/Data1T/mnote/design/90-reference/90-1-filetree.md`
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-1-sidebar-pagetree-filetree-product-gap-analysis-v1.md`
## 1. 文档目的
这份文档回答的问题不是:
- “当前 Sidebar 再怎么局部优化一下”
而是:
> **在 `tree-first graph kernel` 前提下,是否应该把 Sidebar / 页面树 / 文件树直接重构为一个独立的 Rust Web 子系统。**
本文的结论是:
> **可以,而且长期上这是正确方向;但重构对象不是“一个更快的树组件”,而是“一个直接消费 kernel projection 的独立树域执行面”。**
也就是说,目标不是把当前 React 树组件换个语言重写,而是:
- 用 Rust 主导 tree projection
- 用 Rust Web 主导 tree query / command
- 让页面树 / 文件树只作为 kernel 的树投影
- 再决定 UI 壳是否也迁到 Rust 家族
---
## 2. 必须遵守的前提:树不是 UI 数据,而是 kernel 投影
这份方案必须完全服从:
- [tree-first-graph-kernel-v1.md](/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md)
里面已经固定的几条原则。
### 2.1 树是主骨架
当前长期架构已经冻结为:
- 树是主骨架
- 图是横向扩展
- Sidebar / 页面树 / 文件树 / 阅读流 / Mindmap 都只是 projection
所以这里的页面树 / 文件树不能再被定义为:
- 前端自己拼出来的导航数据
它们必须被定义为:
- `tree-first graph kernel` 的树投影
### 2.2 页面树和文件树不是两套真相
在新架构里:
- 页面树不是独立系统
- 文件树也不是独立系统
两者都来自同一个 kernel,只是投影范围不同:
- `page_tree`
-`page` / `section` / 页面层级为主
- `file_tree`
- 在页面层级基础上,把 `asset` / `mindmap` / `table` / 未来 `book` / `pdf` 一起投影出来
### 2.3 Sidebar 是壳,不是事实源
Sidebar 长期不应再被理解为:
- “一个左侧导航 React 组件”
而应理解为:
- “tree projection 的承载壳”
固定边界应是:
- kernel 持有真相
- projection 输出树
- Sidebar 只负责显示和交互
---
## 3. 当前现状
### 3.1 已经做对的部分
当前代码已经有一些方向是正确的:
- `kernelSidebarProjection`
- `kernelSidebarTree`
- `Sidebar` 主树开始以 `kernelSidebarTree` 为来源
- Rust runtime 和 `mnote-web` 已开始承接 Sidebar 相关 projection 主链
对应代码包括:
- [kernel-sidebar.ts](/mnt/Data1T/mnote/wolai-frontend/src/lib/kernel-sidebar.ts)
- [sidebar-data.ts](/mnt/Data1T/mnote/wolai-frontend/src/lib/sidebar-data.ts)
- [sidebar.tsx](/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx)
- [kernel.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/kernel.rs)
### 3.2 还没做完的部分
当前真正的问题是:
- 主 Sidebar 仍是超大客户端组件
- 文件树仍然主要在前端继续加工 row model
- `move-embed picker` 等兼容域仍保留旧 `buildDocumentTree(...)`
- 页面树和文件树还没有彻底统一为稳定的 kernel projection family
这说明:
> **现在的瓶颈不只是“UI 重”,而是“树域仍然没有形成独立、稳定、可替换的执行边界”。**
---
## 4. 对参考资料的判断
### 4.1 `/design/cankao/yemianshu.md` 和 `/design/cankao/filetree.md` 能参考什么
这两份参考有价值,但要分层使用。
适合借鉴的部分:
- 树形系统的分层
- VS Code / Notion 风格交互
- 折叠、展开、拖拽、懒加载、多选、右键菜单
不适合直接拿来落当前 Web 主线的部分:
- Ratatui / Cursive / TUI 组件
- egui / iced / Fyrox / GPUI 这类桌面 GUI 组件
原因很简单:
- 这些更适合终端或原生桌面
- 当前 mnote 的主线是 Web + Rust Web + kernel projection
所以它们更适合做:
- 交互语义参考
而不适合做:
- 当前 Web 主线的直接实现模板
### 4.2 更适合作为直接参考的方向
如果这次真要把 Sidebar / 页面树 / 文件树往 Rust 家族重构,应该看两类参考:
#### A. Rust Web 前端框架
优先关注:
- `Leptos`
- `Dioxus`
- `Yew`
本文的建议顺序是:
1. `Leptos`
2. `Dioxus`
3. `Yew`
原因不是抽象喜好,而是贴合度:
- 你们已经在走 Rust kernel + Rust Web + server-first
- 这时最有价值的是“Rust Web 组件 + server integration + 渐进切流”
- 不是终端树,也不是桌面树
#### B. 成熟 Web Tree 的行为模型
即使最终决定用 Rust 家族重写,交互模型也应该优先参考成熟 Web Tree 的做法:
- headless tree 思路
- VS Code Explorer 的 row model
- 大树虚拟化
- DnD 状态机
- selection / focus / keyboard 模型
这里学的是:
- 行为模型
不是:
- 必须沿用 React
---
## 5. 结论:可以直接重构,但应定义成独立大任务
我的明确结论是:
> **可以直接把 Sidebar / 页面树 / 文件树作为独立大任务重构,而且长期上应该这样做。**
但这个重构不能被理解为:
-`sidebar.tsx` 翻译成 Rust
而应被理解为:
- 把树域从旧前端壳里剥离出来
- 形成一个独立的 Rust Web tree shell
也就是:
- 独立 route / shell
- 独立 projection protocol
- 独立 command protocol
- 独立 UI state 边界
这个任务应该单独成立,而不是继续藏在 `Kernel Phase 4` 的一句话里。
---
## 6. 目标架构
## 6.1 新的树域分层
长期建议把树域拆成五层:
### 1. Kernel Truth
只承载:
- `node`
- `edge`
- `subtree`
- `audit`
### 2. Tree Projection Layer
专门输出:
- `sidebar_tree`
- `page_tree`
- `file_tree`
固定输出应包括:
- `projection_id`
- `root_node_id`
- `items`
- `edges`
- `sort key`
- `expand hint`
- `capability flags`
### 3. Tree Command Layer
只处理树域命令:
- create page
- move subtree
- attach asset
- reorder sibling
- archive / restore
- open node
### 4. Tree Shell
树域的独立承载壳,只负责:
- 拉 projection
- 发 command
- 维护局部 UI 状态
### 5. Tree Renderer
最终的可视组件,只负责:
- row 渲染
- 虚拟化
- 选中
- 展开
- 右键菜单
- DnD feedback
---
## 6.2 页面树和文件树的正确关系
在新架构里,这两者不该是并列的两套不同系统,而应是:
### 页面树
只关心:
- `workspace`
- `folder`
- `page`
- `section`
### 文件树
在页面树骨架上再纳入:
- `asset`
- `mindmap`
- `table`
- `book`
- `pdf`
- 未来的 `index_node`
也就是说:
> **文件树不是“另建一棵树”,而是“同一棵树的更宽对象投影”。**
这非常符合 `tree-first graph kernel` 的定义。
---
## 7. 为什么建议用 Rust Web 子系统,而不是继续堆在当前 Sidebar 里
### 7.1 当前 Sidebar 太大
现在的 Sidebar 不只是树:
- 搜索入口
- 成员
- 分享
- 回收站
- 资源操作
- 页面树
- 文件树
- 各种本地对话框
这会导致:
- 状态膨胀
- 切页参与重渲染
- 树逻辑和业务逻辑缠在一起
### 7.2 树域已经足够大,可以独立成系统
页面树 / 文件树本身已经有:
- 自己的数据协议
- 自己的 row model
- 自己的拖拽系统
- 自己的选择模型
- 自己的上下文菜单
- 自己的资源挂载逻辑
这已经不是一个小组件,而是一个完整子系统。
### 7.3 独立之后更符合后续迁移
如果现在就把它切成独立树域子系统,后续:
- Mindmap
- 阅读页结构树
- 搜索结构结果
- Book / PDF 子树
都可以复用同一套 projection / renderer 协议。
---
## 8. 技术路线选择
## 8.1 方案对比
### 方案 A:继续 React,只换数据层
优点:
- 风险最低
- 最快收口旧 helper
缺点:
- 树域仍留在旧前端壳内
- 不能完成“Rust 主执行面”这一步
### 方案 B:独立 Rust Web tree shell,当前主站只挂载它
优点:
- 可以保留整体产品双栈过渡
- 树域先行 Rust 化
-`tree-first graph kernel` 最一致
缺点:
- 需要额外处理嵌入、路由、样式、事件桥接
### 方案 C:直接整站前端重写
优点:
- 理论上最终最纯
缺点:
- 范围失控
- 风险过高
- 与当前阶段目标不匹配
## 8.2 当前建议
本文明确建议:
> **选方案 B:把 Sidebar / 页面树 / 文件树做成独立 Rust Web tree shell。**
---
## 8.3 Rust Web 框架建议
当前优先建议:
### 第一选择:Leptos
原因:
- 更贴近 Rust 全栈 / server-first
- 更适合和 `axum` / `mnote-web` 的方向合并
- 适合做“树域先行”的渐进式替换
### 第二选择:Dioxus
原因:
- 跨 Web / Desktop 能力强
- 如果未来想把树域同时复用到桌面壳,会有价值
### 第三选择:Yew
原因:
- 能做,但相对不如前两者贴合当前迁移方向
所以这份方案的推荐结论是:
> **树域独立重构时,优先按 `mnote-web + Leptos` 设计。**
---
## 8.4 GitHub 参考池
这里不再按“有没有现成 Rust Notion 成品”来选参考,而是按三个层级来选:
- Rust Web 承载框架
- 树域 UI primitives
- 树行为模型与产品结构参考
### A. 直接可参考:Rust Web 主路线
#### `leptos-rs/leptos`
GitHub
- https://github.com/leptos-rs/leptos
适合借鉴:
- `Rust + SSR + islands` 的 Web 承载方式
-`axum` 风格服务端组合
- 渐进式切流,而不是一次性整站替换
为什么适合当前方案:
- 当前 `mnote-web` 已经是 Rust Web 接入点
- 树域后续如果独立成 shell,最需要的是“Rust Web 承载能力”,不是单独一个树控件
结论:
- **这是树域 Rust Web 重构的第一参考。**
#### `DioxusLabs/dioxus`
GitHub
- https://github.com/DioxusLabs/dioxus
适合借鉴:
- Rust 组件化 UI
- Web / Desktop 共享思路
限制:
- 更偏多端壳能力
- 与当前 `mnote-web + axum` 路线的贴合度仍低于 `Leptos`
结论:
- **可作为备选路线参考,但不是当前首选。**
#### `yewstack/yew`
GitHub
- https://github.com/yewstack/yew
适合借鉴:
- Rust Web 组件化基本能力
限制:
- 能做,但对你们当前 server-first 与渐进切流路线支持感不如 `Leptos`
结论:
- **保留为第三选择,不作为当前主实现模板。**
### B. 直接可参考:树域 UI primitives / 组件层
#### `cloud-shuttle/radix-leptos`
GitHub
- https://github.com/cloud-shuttle/radix-leptos
适合借鉴:
- `Leptos` 生态下的 UI primitives 组合方式
- `collapsible``scroll area``menu``overlay`、可访问性细节
- 树域壳层需要的基础交互组件
限制:
- 它不是完整树组件
- 不能直接替代页面树 / 文件树的 row model 与状态机
结论:
- **适合作为树域 shell 的基础件参考。**
#### `thaw-ui/thaw`
GitHub
- https://github.com/thaw-ui/thaw
适合借鉴:
- `Leptos` 组件组织方式
- 通用面板、按钮、菜单等基础 UI
限制:
- 更像通用组件库
- 对树域协议、树行为模型帮助有限
结论:
- **适合作为辅助 UI 库参考,不是树域核心参考。**
#### `KoVal177/leptos-column-browser`
GitHub
- https://github.com/KoVal177/leptos-column-browser
适合借鉴:
- Rust Web 下的层级导航
- 异步懒加载子节点
- 多层树浏览器的交互拆分
限制:
- 项目较新、体量小
- 更接近 column browser,不是当前 Sidebar 单栏树的完整模板
结论:
- **适合借鉴 provider / async loading / column navigation 思路,不适合直接照搬。**
### C. 高价值参考:树行为模型与产品结构
#### `lukasbach/headless-tree`
GitHub
- https://github.com/lukasbach/headless-tree
适合借鉴:
- row model
- selection / focus / keyboard 模型
- DnD 状态机
- 虚拟化树行为拆分
为什么值得看:
- 你们后续真正难的不是“画一棵树”,而是把树行为从具体 UI 框架中抽出来
- 这正对应 `tree-first graph kernel -> projection -> shell -> renderer` 的分层思想
限制:
- 不是 Rust
- 不能直接进入实现层
结论:
- **非常适合作为树行为模型参考。**
#### `AppFlowy-IO/AppFlowy`
GitHub
- https://github.com/AppFlowy-IO/AppFlowy
适合借鉴:
- “工作区 / 页面树 / 文档”这类产品结构
- Rust 统一管理部分内核能力的思路
- Notion 类产品如何收敛页面树语义
限制:
- 主前端不是你们要走的 `Leptos Web` 路线
- 不能作为当前树域 Web 重构的直接模板
结论:
- **适合作为产品结构与边界参考,不适合作为实现模板。**
#### `toeverything/AFFiNE`
GitHub
- https://github.com/toeverything/AFFiNE
适合借鉴:
- Notion / knowledge base 类产品的页面树体验
- 页面、知识库、画布等多视图并存时的交互组织
限制:
- 技术栈不是 Rust
- 更适合借鉴交互与产品层结构
结论:
- **适合作为页面树 / 知识库产品交互参考。**
## 8.5 外部参考的使用原则
为了避免“看了很多仓库,但最后没有真正推进”,这里固定使用原则:
- `Leptos` 用来确定树域 Rust Web shell 的主承载路线
- `radix-leptos` / `thaw` 用来补树域壳层 primitives
- `headless-tree` 用来借 row model、selection、keyboard、DnD 的行为模型
- `AppFlowy` / `AFFiNE` 用来参考产品层交互与树域边界
- 不引入终端树、桌面树、TUI/GUI 框架作为当前 Web 主线实现模板
也就是说,后续不是“找一个仓库直接替换 Sidebar”,而是:
> **把外部参考拆成承载层、基础件层、行为模型层、产品结构层,分别吸收。**
---
## 9. 重构范围
## 9.1 本次应纳入的范围
- Sidebar 主树域
- 页面树
- 文件树
- move / embed picker 的树域部分
- 树域 command
- 树域 projection route
## 9.2 本次不纳入的范围
- 搜索主面板
- AI 主面板
- Mindmap 主画布
- `BlockNote` 编辑器
- 旧前端壳整体删除
原因:
- 这些属于后续 phase
- 本次只做树域切换,保持边界清晰
---
## 10. 新协议定义建议
## 10.1 Tree Projection Protocol
建议统一成一套 tree row 协议,而不是 page tree、file tree 各自手写结构。
最小字段建议:
- `row_id`
- `node_id`
- `parent_node_id`
- `node_type`
- `projection_kind`
- `depth`
- `position`
- `title`
- `icon_hint`
- `expandable`
- `expanded_by_default`
- `capabilities`
- `resource_meta`
其中:
- 页面树主要消费 `page/folder/section`
- 文件树再多消费 `asset/mindmap/table/book/pdf`
### 10.2 Tree Command Protocol
建议统一成:
- `tree.node.create`
- `tree.subtree.move`
- `tree.node.rename`
- `tree.node.archive`
- `tree.node.restore`
- `tree.asset.attach`
- `tree.asset.detach`
这些 command 最终应映射到 kernel command,而不是直接绑在前端 UI 行为上。
---
## 11. 分阶段实施建议
下面这组 phase 不再按“最初方案假设”维护,而按 **2026-04-18 当前仓库代码状态** 重写。
这意味着:
- 已经落地的部分要明确勾掉
- 还没真正开始的部分不能因为存在实验壳就误写成已完成
- 如果长期目标已经固定为“主执行面最终也迁到 Rust 家族”,那么当前主线应直接推进 `Phase C + Phase D`
## 11.1 Tree Shell Phase A:协议冻结
这一阶段的目标不是开始写 UI,而是把树域协议冻结到后续不会反复返工。
**当前状态:`COMPLETED(协议、共享类型、状态边界与 contract tests 已完成封板)`**
### 完成 checklist
- [x]`page_tree` 的现有字段提升为当前协议基线:
- `row_id`
- `node_id`
- `parent_node_id`
- `node_type`
- `projection_kind`
- `depth`
- `position`
- `title`
- `capabilities`
- `resource_meta`
- [x] 树域主路径已统一到 `kernelSidebarTree -> page_tree projection -> visible rows` 这一协议家族
- [x]`sidebar_tree``page_tree``file_tree` 的共用字段与差异字段正式写成共享类型/文档,不再只散落在前端映射代码里
- [x] 定义并冻结 `file_tree` 扩展字段:
- `resource_kind`
- `asset_kind`
- `icon_hint`
- `expandable`
- `expanded_by_default`
- [x] 第一批树域命令已经收口到共享 command client
- `documents.create`
- `documents.title.update`
- `documents.move`
- `documents.delete`
- `documents.restore`
- `documents.purge`
- `documents.embed`
- `documents.copy_tree`
- [x] 冻结树域 command protocol 的长期命名面,并补齐 `documents.* -> tree.*` 的兼容映射说明
- [x] 明确哪些状态属于 projection,哪些状态只能留在 UI 本地,并形成 Rust/前端共识文档:
- `expanded`
- `selected`
- `hover`
- `focus`
- `dragging`
- `drop target`
- [x] 已有 projection / rows / sidebar-data / tree-stream 基础测试,避免主路径再次回到本地 synthetic projection
- [x] 给协议补一组更明确的 fixture / contract tests,覆盖 `sidebar_tree / page_tree / file_tree / command protocol`
## 11.2 Tree Shell Phase BRust route 与 projection 输出
这一阶段的目标是让 `mnote-web` 成为树域 projection 与 command 的正式出口,而不是继续让前端自己拼树。
**当前状态:`COMPLETEDRust route、projection mapper、契约测试与兼容边界已进入正式主线)`**
### 完成 checklist
- [x] `mnote-web` 已具备树域相关 route
- `kernel projection route`
- `kernel subtree route`
- `tree command route`
- `tree shell route`
- [x] `page_tree` 已经是当前页面树 / 文件树 / picker 的共同协议骨架
- [x] `/api/tree/commands` 已能承接第一批树域命令并映射到 Rust/Convex bridge 主链
- [x] route 已带 request/trace 上下文与基础测试,不再只是占位骨架
- [x] 把树域读取出口补成更明确的正式 projection route 族:
- `sidebar_tree`
- `page_tree`
- `file_tree`
- [x]`file_tree` 从“前端 adapter 拼装”继续下沉为 Rust 侧直接输出的 projection
- [x] 在 Rust 侧补齐 `asset` / `mindmap` / `table` / `book` / `pdf` 的 projection 映射层
- [x] 明确树域 command route 的长期协议面,避免一直停留在 `documents.*` 兼容命名
- [x] 把鉴权、错误码、兼容 fallback、trace、workspace 解析补成完整 route 契约
- [x]`sidebar_tree / page_tree / file_tree / commands` 补齐 route tests、fixture tests、兼容入口 tests
- [x] 让 Next 侧进一步只保留 transport / compat,不再残留结构真相拼装逻辑
## 11.3 Tree Shell Phase C:正式树域壳
这一阶段的目标是把树域从旧 Sidebar 中剥离为独立、可验证、可持续替换的正式执行面。
**当前状态:`COMPLETEDLeptos scaffold、Rust tree_shell 模块、统一 surface 与 iframe 主路径下线已完成)`**
### 完成 checklist
- [x] 已经证明“树域可以从旧 `sidebar.tsx` 中剥离成独立 Rust Web 壳”,而不是只能留在 React 组件里
- [x]`Leptos` 搭建最小正式 tree shell
- tree loader
- command dispatcher
- local UI state store
- [x] 接入可验证的最小 renderer 主干与稳定 `data-testid`
- [x] 接入展开 / 折叠状态
- [x] 接入选中 / focus / keyboard 导航
- [x] 接入 DnD 状态机
- [x] 接入右键菜单与基础上下文动作
- [x] 支持 `page_tree``file_tree` 两种渲染模式共用同一 renderer/surface 家族
- [x] 支持 picker 场景复用同一 tree shell 的轻量模式
- [x] 保持正式 UI 壳不持有结构真相,只持有局部交互状态
- [x] 去掉主路径对 `iframe + postMessage` 的依赖,把它降回纯兼容/调试用途
## 11.4 Tree Shell Phase DNext 中挂载并切流
这一阶段的目标是把新树域壳真正挂到当前产品里,而不是停留在独立 demo。
**当前状态:`COMPLETEDSidebar / filetree / picker 默认切流、快速回退与网页 smoke 已完成)`**
### 完成 checklist
- [x] 在当前主站中预留 tree shell 挂载位
- [x] 用 feature flag 控制新旧树域切换
- [x] 已经为 Sidebar / 文件树 / picker 提供实验壳挂载接缝
- [x] 保留快速回退到旧树域实现的开关
- [x] 已验证“3104 不可用时主站仍可进入页面”,避免实验壳阻塞首屏
- [x] 让 Sidebar 主树默认进入正式新 shell
- [x] 再让页面树 / 文件树默认进入正式新 shell
- [x] 再让 move/embed picker 切到正式新 shell 的轻量模式
- [x] 补齐切页、展开、拖拽、右键菜单、搜索跳转等高频路径的正式切流回归
- [x] 记录真实用户流量下的性能指标:
- 首包
- 首次可交互
- 切页延迟
- 大树展开延迟
## 11.5 Tree Shell Phase E:收敛旧 helper
这一阶段的目标是收掉旧树域真相层残留,避免双轨长期并存。
**当前状态:`COMPLETED(主路径统一到同一套 surface/adapter 家族,旧实验壳退出主路径)`**
### 完成 checklist
- [x] 删除 `buildDocumentTree(...)` 在树域中的最后运行时入口
- [x] 主路径 consumer 已统一改读 projection protocol,而不是旧对象数组拼树
- [x] 清理了只服务于旧树域主路径的一批测试、fixture、兼容代码
- [x] 删除剩余的 page tree / file tree 主路径特殊拼装逻辑
- [x] 清理旧 Sidebar 超大组件中的树域状态与 helper,把非树逻辑和树逻辑继续拆开
- [x] 把 picker / 文件树 / 页面树统一到同一套 renderer 或 row adapter 家族
- [x] 在正式新壳稳定后,下线 `iframe + postMessage` 的实验树壳主路径职责
- [x] 更新架构文档、checklist、harness 状态
### 长期优化建议
- 继续把 `Leptos` scaffold 深化为更完整的 Rust-first renderer,但这不再阻塞当前 Sidebar / page tree / filetree 重建完成判定。
- 持续采集生产环境下的树规模、切页延迟与 fallback 触发频率,把本轮开发机 smoke 指标升级为长期趋势指标。
- 对 move/embed picker 的搜索结果路径补异步索引可见性指标,但这属于搜索链路优化,不再作为当前树域切流 gate。
---
## 12. 验收标准
只有同时满足下面几条,才建议把这次树域 Rust Web 重构视为成立:
- 页面树与文件树都直接消费 kernel projection
- Sidebar 不再自己定义树结构真相
- 树域已有独立 Rust Web shell
- 至少一条真实用户流量默认进入新 tree shell
-`buildDocumentTree(...)` 不再处于主路径
- move/embed picker 等兼容树域也已切到统一 projection
---
## 13. 风险与约束
### 风险 1
如果在 projection 协议未冻结前就开始重写 UI,容易重写两遍。
### 风险 2
如果把搜索、AI、Mindmap 一起塞进树域重构,范围会立即失控。
### 风险 3
如果只重写 UI,不改 projection / command / shell 分层,最终只是“换皮”,不是根治。
---
## 14. 最终结论
这次页面树 / 文件树的长期正确方向,不是:
- 再修补当前 `sidebar.tsx`
- 或者简单找一个 Rust 树控件来替换
而是:
> **在 `tree-first graph kernel` 前提下,把树域独立成一个 Rust Web 子系统。**
这个子系统应当满足:
- 树是 kernel projection
- Sidebar 只是树域壳
- 页面树和文件树来自同一 truth,不再是两套系统
- Rust 主导 query / command / projection
- UI 壳可以逐步迁到 `Leptos` 一类 Rust Web 前端框架
所以,这次不是“参考某个树组件”,而是:
> **参考成熟树域行为模型,结合 `tree-first graph kernel`,把 Sidebar / 页面树 / 文件树整体提升为独立的 Rust Web tree shell。**
@@ -0,0 +1,232 @@
# 5-5-1 [done] Page Aggregate Contract v1
> 更新时间:2026-04-22
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
## 1. 文档目的
这份文档只定义一件事:
> **文档页在前端与 Rust 主链之间,当前最小可落地的 `Page Aggregate` 契约。**
它的作用不是取代长期 Rust kernel contract,而是先冻结当前主文档页的聚合边界,阻止:
- 标题、正文、页面设置、page subtree 继续各自散长字段
- 页面壳继续在 props 链上拼第二份页面真相
- `leptos-tiptap` island 继续只接正文而不接页面布局语义
## 2. 当前最小契约
当前最小 `Page Aggregate` 固定为五层:
- `page_identity`
- `page_head`
- `page_layout`
- `page_body`
- `page_tree`
以及一组附属统计:
- `page_stats`
### 2.1 `page_identity`
作用:
> **标识这份页面聚合属于哪一个 page aggregate。**
当前最小字段:
- `documentId`
- `workspaceId`
说明:
- 这层是聚合身份,不是 UI 展示字段。
- 后续如果 Rust kernel 侧补 `nodeId / aggregateId / projectionVersion`,应继续落在这一层,而不是散回页面 props。
### 2.2 `page_head`
作用:
> **承载页面头部的正式真相。**
当前最小字段:
- `title`
- `updatedAt`
- `permissions.readOnly`
- `permissions.disableDownload`
- `permissions.disableCopy`
说明:
- 页头标题属于 `page_head truth`,不是组件局部输入框自己的真相。
- 前端仍可保留一个“输入中”的临时编辑态,但提交后必须回到 `page_head.title`
### 2.3 `page_layout`
作用:
> **承载页面布局与编辑器 runtime 相关设置。**
当前最小字段:
- `pageOptions.wideLayout`
- `pageOptions.smallText`
- `pageOptions.layoutDensity`
- `pageOptions.showHeadingNumbers`
- `pageOptions.showToc`
- `pageOptions.showStructure`
- `pageOptions.protectEditing`
- `pageOptions.showWordCount`
- `pageOptions.collapseBacklinks`
- `pageOptions.pageFont`
- `pageOptions.hideChildPages`
- `pageOptions.showBlockRefCount`
- `pageOptions.embedDefaultBlockId`
说明:
- 这层不是“页面设置面板 UI state”,而是页面设置的持久化真相。
- 其中只有一部分已经进入 `leptos-tiptap` island 运行时:
- `wideLayout`
- `smallText`
- `layoutDensity`
- 下面两项当前只完成字段贯通,不可描述成“正式支持”:
- `showHeadingNumbers`
- `embedDefaultBlockId`
### 2.4 `page_body`
作用:
> **承载正文内容与保存元数据。**
当前最小字段:
- `content`
- `revision`
- `conflictDetectionKey`
说明:
- 正文相关命令都应收口到 `page_body`,而不是继续发明新的“页面内临时写回”路径。
- `/api/documents/save -> documents.save -> documents:updateContent` 是当前正式 page body 保存入口。
- 2026-04-28 更新:`page.body.saved``document.snapshot.saved` 已拆成两条正式 domain event。`page.body.saved` 只表达正文块集合保存,`document.snapshot.saved` 承载 snapshot version / content hash / updatedAt,避免正文事件 payload 继续夹带快照语义。
- 2026-04-28 更新:`tree.node.embed``pageReference` block 结构与插入位置由 Rust `pageAggregateEmbedPlan` 产出;3000 route 只提供源页面标题、目标内容、anchor block 等 substrate preflight,不再自行拼装页面聚合正文结构。
### 2.5 `page_tree`
作用:
> **承载当前页面对应的子树投影。**
当前最小字段:
- `pageSubtree`
说明:
- 当前 `pageSubtree` 仍是兼容过渡态 projection,不应夸大为最终 Rust page aggregate tree contract。
- 但在文档页入口和阅读态/TOC 消费层,它已经归属于 `page_tree`,不再继续散成独立 props。
### 2.6 `page_stats`
作用:
> **承载页面统计的附属投影。**
当前最小字段:
- `wordCount`
- `characterCount`
- `blockCount`
- `todoTotal`
- `todoDone`
说明:
- 统计不是页面身份,也不是布局或正文真相,但它是 page aggregate 的附属部分。
## 3. 哪些字段是 aggregate truth,哪些只是 UI state
### 3.1 Aggregate Truth
下面这些字段属于 page aggregate truth
- `page_identity.*`
- `page_head.*`
- `page_layout.pageOptions.*`
- `page_body.*`
- `page_tree.pageSubtree`
- `page_stats.*`
### 3.2 UI State
下面这些只能算前端临时态,不是正式真相:
- 标题输入框当前未提交文本
- 当前是否处于编辑态/阅读态
- host fallback banner / runtime observability
- inspector 当前 tab
- history drawer / comments drawer / AI panel 的打开状态
- slash 菜单、浮动工具条、块手柄 hover 等 island 交互态
约束:
> **UI state 允许存在,但不能再反客为主冒充 page aggregate truth。**
## 4. 哪些字段必须由 Rust 主导,哪些当前允许前端临时持有
### 4.1 必须由 Rust 主导
- `page_head.title` 的持久化结果
- `page_body.content/revision/conflictDetectionKey`
- `page_layout.pageOptions` 的持久化结果
- `page_tree.pageSubtree` 的正式投影来源
### 4.2 当前允许前端临时持有
- 标题输入中的 debounce 缓冲态
- 正文 host 内的未保存 dirty 态
- 只影响单次交互的面板/菜单开关
- `showHeadingNumbers/embedDefaultBlockId` 的“字段已贯通,但深语义未完成”阶段性桥接逻辑
约束:
> **允许前端临时持有,不等于允许前端成为第二真相。**
## 5. 当前命令收口口径
当前最小命令口径:
- `page.head.updateTitle`
- 当前前端别名映射到 `documents.title.update`
- `page.layout.updateOptions`
- 当前前端别名映射到 `documents.options.update`
- `page.body.save`
- 当前正式入口映射到 `documents.save`
说明:
- 这轮只是把命名与调用口径收口到 page aggregate 子域。
- 底层 bridge / Convex / Rust mutation 名称暂不要求一次性全部重命名。
## 6. 当前未完成边界
以下事项当前仍未完成,不应在 checklist 中超前打钩:
- 树标题与页头标题已经证明消费同一份更新后的 projection
- AI 写入口已经完全脱离 editor bridge,正式执行 page body command
- `showHeadingNumbers` 真正进入 heading 渲染语义
- `embedDefaultBlockId` 真正进入嵌入默认落点逻辑
## 7. 当前可对外口径
当前可以准确描述为:
> **文档页已开始消费统一 `Page Aggregate`,标题/页面设置/正文保存也开始按 `page_head / page_layout / page_body` 收口;但树域投影统一与 AI 正式 page body 写入口仍未完全完成。**
@@ -0,0 +1,236 @@
# 5-2 [process] Tiptap Notion-Like 模板裁剪与 `leptos-tiptap` 映射分析 v1
> 更新时间:2026-04-19
>
> 这份文档已经按当前进展修正:
>
> 现在的问题不再是“这套模板能不能迁进来”,而是:
>
> **我们已经做出了一版 `leptos-tiptap` 编辑器,接下来应该从官方模板中保留什么行为模型,并优先把它接入主编辑器。**
## 1. 修正后的总判断
当前对官方模板的使用方式应当明确为:
1. 它是体验 benchmark,不是直接照搬对象。
2. 它是行为模型参考,不是 React 代码迁移目标。
3. 现阶段最值钱的不是再扩更多模板功能,而是把已实现能力接入主编辑器。
因此,这份模板的正确用途是:
> **借它定义“官方行为应当是什么”,再用 `leptos-tiptap` + Rust 把这些行为落到 `mnote` 的主链。**
## 2. 官方模板里当前最该保留的参考
### 2.1 主编辑器扩展组合
核心参考文件:
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-templates/notion-like/components/notion-like-editor.tsx`
当前值得保留的不是整份文件,而是这些决策:
- 以 Tiptap 正式 schema 组织常用块
- 常用块优先于低频复杂块
- `task list / task item` 使用正式扩展,不做 HTML 占位
- `UniqueID` 作为块身份辅助机制
- `Indent` 作为普通块缩进增强的参考扩展
### 2.2 slash 菜单行为模型
核心参考文件:
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-ui/slash-dropdown-menu/slash-dropdown-menu.tsx`
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-ui/slash-dropdown-menu/use-slash-dropdown-menu.ts`
当前最该保留的是:
- slash 入口就是块编辑器的主入口之一
- 菜单项的分组与命令组织方式
- 菜单锚点应跟随 caret,而不是随便找一个固定位置
### 2.3 浮动工具条行为模型
核心参考文件:
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-templates/notion-like/components/notion-like-editor-toolbar-floating.tsx`
当前最该保留的是:
- 工具条只在文本 selection 语境下出现
- 文本格式命令和 `turn into` 要分清语境
- 不要把固定显示工具条误当成“功能已完成”
### 2.4 左侧手柄与块菜单行为模型
核心参考文件:
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-ui/drag-context-menu/drag-context-menu.tsx`
当前最该保留的是:
- hover 先只显露手柄,不自动弹块菜单
- 块菜单应由左侧手柄点击触发
- `turn into / duplicate / delete / drag` 是块菜单的核心动作
这点非常关键,因为它直接影响我们后续对标 Wolai/Notion 的交互正确性。
## 3. 结合当前代码后的现实判断
### 3.1 已经有的东西
从当前实现看,下面这些已经不是“要不要做”,而是“如何迁进主链”:
- `paragraph / heading / list / todo / quote / code block / divider`
- slash 菜单
- 浮动工具条
- 左侧手柄
- 块菜单
- `turn into`
- 顶层拖拽
- refresh 后结构保留
对应当前实现入口:
- `/mnt/Data1T/mnote/rust/spikes/leptos-tiptap-spike/src/main.rs`
### 3.2 当前真正的缺口
当前真正的缺口不是模板 feature 数量,而是下面这些:
- 它还在 spike 里,不在主编辑器里
- 保存还没接上正式 Rust truth
- `block_id` 还没成为正式合同
- 页面引用 / 块引用还没打通
- 图片上传与附件桥还没接
- 表格还没做
所以现阶段不该继续把注意力平均分配到:
- Markdown 导入导出回归
- `kode` 组件分层整理
- 表格
- AI Cloud
- 协作链
## 4. `P0.5` 该如何采用官方模板
### 4.1 `P0.5` 要保留的部分
`P0.5` 应保留的不是一堆零散 feature,而是以下几类“官方行为”:
- 主编辑器使用正式 Tiptap 扩展,而不是临时 HTML 拼接
- slash 菜单锚定 caret
- 选中文字才出现浮动工具条
- hover 只显手柄,点击手柄才弹块菜单
- `turn into` 作为块菜单和工具条共享的一组块类型切换动作
- `UniqueID` 作为 runtime 辅助,但最终块 id 仍归 Rust
### 4.2 `P0.5` 不要再继续扩的部分
`P0.5` 不应该继续向下扩这些模板能力:
- 协作
- AI
- TOC
- 完整表格
- 图片上传整链
- 移动端全量工具条
原因很简单:
这些都不会帮助我们更快完成“接入主编辑器”。
## 5. 官方模板到 `mnote` 的映射口径
### 5.1 直接借行为,不借代码
以下部分应当“借行为模型”,不应当尝试直接照搬 React 代码:
- `slash-dropdown-menu`
- `drag-context-menu`
- `notion-like-editor-toolbar-floating`
原因:
- React hooks / context / portal 不能直接进入 Leptos
- 真正有价值的是交互时机、状态边界、动作分组
- 不是 `tsx` 组件本身
### 5.2 `block id` 需要现在就纳入主线
官方模板里的 `UniqueID.configure(...)` 很重要,但口径要修正成:
- 官方参考:
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-templates/notion-like/components/notion-like-editor.tsx`
- Rust 真相字段:
`/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs`
最终原则:
1. Rust `EditorBlock.block_id` 是持久化真相。
2. Tiptap `UniqueID` 只负责浏览器 runtime 内的节点身份辅助。
3. 不允许把前端临时 id 倒灌成正式文档真相。
### 5.3 `Indent` 值得做,但不在这一轮前排
官方缩进扩展很值得参考:
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-extension/indent-extension.ts`
但当前它不应排在主编辑器接入之前。
更合理的顺序是:
1. 先完成主编辑器接入
2. 再完成 Rust 保存边界
3. 再考虑普通块缩进增强
### 5.4 图片上传与表格都先后置
官方模板确实有图片与表格能力,但现在不应让它们进入主 checklist 前排。
原因:
- 图片上传不是当前切流主编辑器的阻塞项
- 表格更不是当前高频刚需
- 两者都容易把工作重新带到复杂 UI / 服务桥接细节
## 6. 哪些旧 checklist 应从主线降级
下面这些项不删,但不应继续放在当前主线前排:
- Markdown 导入导出回归
参考 `blocks` 仍然有价值,但它更像切流后的回归与兼容工作。
-`kode` 做 Leptos 组件分层
这个参考可以保留,但它属于实现注记,不是当前交付里程碑。
- 完整表格
继续后置。
- 完整图片上传
继续后置,只保留后续最小桥接预留。
## 7. 对接下来工作的直接建议
基于当前代码和官方模板,接下来最合理的顺序是:
1. 先把当前 `leptos-tiptap` 编辑器接入主编辑器。
2. 同时建立正式的 Rust load/save 边界。
3. 在这个过程中引入稳定 block id 策略。
4. 等它真正成为主编辑器后,再规划 `P1 / P1.5` 的 Wolai 对标体验增强。
这也意味着:
当前官方模板对我们的最大价值,不是再提供更多可抄的 feature,而是帮助我们定义:
- 哪些交互已经够了
- 哪些交互时机必须修正
- 哪些功能现在根本不该做
## 8. 最终结论
当前对这份模板的正确使用方式是:
> **以后续主编辑器开发继续以 Tiptap 官方能力模型为体验上限,以 `leptos-tiptap` 为运行时接入层;先完成主编辑器接入和 Rust truth 落地,再单独规划 Wolai 对标增强,不再把模板 feature 数量当作当前阶段目标。**
@@ -0,0 +1,591 @@
# 5-5 [process] 主编辑区与树域单一真源对齐方案 v1
> 更新时间:2026-04-22
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md`
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/5-3-tiptap-leptos-rust-migration-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md`
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
## 1. 文档目的
这份文档只回答一个当前已经暴露出来的主线问题:
> **当 `leptos-tiptap` 已经成为页面内正式主编辑区后,页面树 / 文件树 / 页面标题 / 页面设置 / 页面正文,是否已经统一成 Rust 主导的单一真源架构。**
当前结论是:
> **还没有。**
当前系统已经不再是:
- `iframe runtime`
- 纯前端假接入
-`BlockNote` 主链
但也还不是:
- Rust 持有页面聚合真相
- Leptos island 只消费 Rust projection
- 树域与主编辑区共享同一份 page aggregate
它现在更接近于:
> **`Rust-aware + Convex-backed + 前端本地组装` 的混合态。**
因此,当前看到的这些“小问题”:
- 标题单一真源问题已经开始收口,但页面聚合仍未完全统一
- 页面宽度和主编辑区宽度看起来不是一套规则
- 页面设置大部分只改了壳层,没有真正进入主编辑器
并不是孤立 UI bug,而是同一个底层断层的表现:
> **树域 projection 与主编辑区 page runtime 之间,还缺一个统一的 page aggregate 真相层。**
---
## 2. 先给结论
### 2.1 当前不能说已经实现了 Rust 单一真源
虽然当前文档页已经:
- 默认使用页面内正式 `leptos-tiptap` island
- 不再依赖外部 iframe bridge 作为主编辑器
- 正文保存已经能按 `workspaceId/documentId` 绑定到正确页面
但以下事实仍然成立:
- 标题链路虽已开始与树域 canonical snapshot 对齐,但还没有和正文、页面设置统一成同一组 page aggregate command family
- 页面正文是一条独立保存链
- 页面设置又是一条独立更新链
- 页面树 / 文件树与页头标题的一致性已明显改善,但当前 page aggregate projection 仍主要由前端装配而不是 Rust 原生提供
- `pageOptions` 已部分进入 `leptos-tiptap` 运行时语义层,但还没有整体收口完成
所以当前不能把这条线描述成:
> **“主编辑区、页面树、文件树已经统一为 Rust 下唯一真源”。**
更准确的表述应该是:
> **树域已经在向 Rust canonical contract 靠拢,但文档页主编辑区仍处于页面聚合尚未完成的混合切流阶段。**
### 2.2 当前最该收口的不是换编辑器,而是统一页面聚合边界
当前不该重新争论:
- 要不要 `Tiptap`
- 要不要 `leptos-tiptap`
- 要不要继续保留 Convex
这些结论都已经足够明确:
- `Tiptap` 继续作为浏览器输入 runtime
- `leptos-tiptap` 继续作为 Leptos 内正式 editor island 接缝
- `Convex` 继续作为当前存储 / 实时 / 协作底座
当前真正要收口的是:
> **页面这一层,到底由谁持有聚合语义,前端到底应该消费什么,编辑器到底应该回发什么。**
### 2.3 后续主线应固定为 Page Aggregate,而不是继续零散补洞
从长期架构看,当前主线不应再描述成:
- “继续补几个 `leptos-tiptap` 细节”
- “再把标题同步修一下”
- “再把页面设置接一点进去”
后续主线应改写成:
> **建立 Rust 主导的 `page aggregate`:让页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区 / AI 写入,都围绕同一组 page projection 与 page command family 运转。**
---
## 3. 当前问题的精确判断
## 3.1 当前页面数据并不是一个统一聚合
当前文档页在加载时,实际上是把几类数据拆开读取,再由前端壳层重新拼装:
- 页面 meta
- 页面标题
- 页面设置
- 页面正文
- 页面子树快照
这意味着当前不是:
- Rust 返回一份稳定的 page aggregate projection
而是:
- Next route 先查 meta
- 再查 content
- 再把 `title/options/content/pageSubtree` 手动组成页面 props
这条链本身已经说明:
> **当前页面不是消费一份 canonical page projection,而是消费几份分裂的子结果。**
### 3.1.1 标题链已开始收口,但尚未完成页面聚合统一
当前页头标题已经不再单纯由 `DocumentContent` 内部的长期本地 `pageTitle` 真相驱动,而是开始依赖与 sidebar / breadcrumb 共享的 preferred sidebar snapshot。
这带来两个直接后果:
1. 标题不同步这一类“前端本地状态长期漂移”问题已经明显减少
2. 但标题仍然没有和正文、页面设置一起统一到单一 page aggregate command / projection 边界中
于是:
- 页头、breadcrumb、sidebar、page tree、file tree 的标题一致性已有一轮真实收口
- 但从聚合语义上看,标题仍是一条相对独立的更新链,不等于 page aggregate 已经完成
这类问题不是“防抖没配好”,而是:
> **标题更新仍然是页面聚合之外的一条独立 side effect 链。**
### 3.1.2 正文和页面树仍然不是同一份 projection family
当前正文编辑器使用的是:
- `leptos-tiptap` runtime
- `/api/documents/save`
- `EditorBlockDocument / Tiptap / blocks` 的转换边界
而页面子树 / outline / evidence 使用的是:
- `pageSubtree`
- 前端本地定义的 TS 结构
- “内容与标题未变化时复用旧快照”的策略
这说明当前主编辑区与树域虽然已经能同时工作,但它们仍然不是:
- 同一个 Rust projection family 的不同视图
而是:
- 一边是编辑器保存链
- 一边是页面子树附带快照
因此当前 `pageSubtree` 更像:
- 页面阅读态和 TOC 的辅助投影
而不是:
- 主编辑区与树域共享的 canonical page aggregate
### 3.1.3 页面设置已部分进入 island,但还不是完整 editor runtime 语义
当前页面设置里至少有三类配置:
#### A. 页面壳层布局类
- `wideLayout`
- `smallText`
- `layoutDensity`
#### B. 页面视图类
- `showToc`
- `collapseBacklinks`
- `hideChildPages`
#### C. 编辑器语义类
- `showHeadingNumbers`
- `showBlockRefCount`
- `embedDefaultBlockId`
现在的主要问题不是“这些字段没有存下来”,而是:
> **它们虽然能持久化到 documents 表,也已有一部分进入 `leptos-tiptap` island,但还没有统一进入正式 page aggregate,更没有完整地进入 editor runtime 语义层。**
结果就是:
- 一部分设置已经作用于 island 布局或排版
- 一部分设置只影响阅读态
- 一部分设置被展示出来,但主编辑器内部几乎不消费
这也是为什么当前会出现:
> **页面设置看起来像是真的,但很多只是 UI 层局部生效。**
### 3.1.4 所谓“Rust 路径”里仍有明显 compat 痕迹
当前 rename / move / archive 等页面操作,虽然已经开始声明:
- `preferredCommandName`
但真实落地仍大量停留在:
- `documents.title.update`
- `documents.move`
- `documents.delete`
这说明当前并不能说:
- 树域命令已经完整切到 Rust tree command family
更不能说:
- 页面编辑域已经和树域命令收敛到同一个 page aggregate contract
因此现状应判定为:
> **Rust bridge 已经进入主链,但 page aggregate command cutover 仍未完成。**
---
## 4. 为什么这些问题会直接影响长期方向
## 4.1 它会削弱 Rust + Leptos 的真正优势
你们要的不是:
- “一个能跑的 Tiptap 页面”
而是:
- Rust 掌握页面与树的真相
- Leptos 成为页面内正式 editor island
- AI 最终直接接入主编辑区写作
如果标题、正文、设置、树节点标题分别走不同链路,那么 Rust + Leptos 的长期优势就会被削弱成:
- 只是“比纯前端多了一层 bridge”
而不是:
- “由 Rust 持有统一对象语义,前端只消费真相投影”
### 4.1.1 AI 直接写主编辑区会缺乏稳定入口
如果后续 AI 要直接进入主编辑区编写,而当前系统没有统一的 page aggregate command family,那么 AI 最终只能选下面几条坏路:
- 直接操作前端 DOM
- 直接操作 Tiptap JSON
- 直接调用不同的标题 / 正文 / 设置接口拼写入
这三条路都不符合长期目标。
长期正确路线必须是:
> **AI 先操作 Rust page aggregate 的命令与块语义,再由 editor island 把结果投影到 live editing surface。**
### 4.1.2 树域与页面域会继续出现“双真相漂移”
只要页面树 / 文件树消费的还是一套资源 meta,而主编辑区头部和页面设置是另一套前端状态,那么后续还会不断出现:
- 标题单一真源已开始收口,但当前尚未和正文、页面设置统一成同一 page aggregate 命令边界
- 页面设置变更只影响一部分视图
- 阅读态与编辑态宽度规则不一致
- 新增一个页面能力时,要改三四条链路
这类问题越到后面越难收。
---
## 5. 正确的目标重写
当前阶段的目标不应再写成:
- “把 `leptos-tiptap` 再打磨得更像官方模板”
而应重写为:
> **建立 Rust 主导的 `page aggregate`,让页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区 / AI 写入共享同一份页面真相。**
这句话拆开后,意味着下面五点。
### 5.1 Page Aggregate Projection
Rust 侧需要提供一个统一的页面聚合投影,至少包含:
- `page_identity`
- `workspace_id`
- `document_id`
- `node_id`
- `page_head`
- `title`
- `icon`
- `cover`
- `updated_at`
- `permissions`
- `page_layout`
- `wide_layout`
- `small_text`
- `layout_density`
- `show_toc`
- `show_structure`
- `page_body`
- `editor_document`
- `revision`
- `conflict_detection_key`
- `stats`
- `page_tree`
- `outline`
- `child_pages`
- `resource_meta`
当前不要求一次把所有字段做满,但必须先把口径冻结。
### 5.2 Page Command Family
后续页面域的操作不应继续被拆成互不统属的零散 mutation,而应在语义上统一到 page aggregate 之下。
最小命令族至少包括:
- `page.head.rename`
- `page.layout.update`
- `page.body.save`
- `page.body.insert_reference`
- `page.body.set_embed_default_block`
- `page.tree.refresh_projection`
这不意味着现在立刻把所有接口名都改掉,而是:
> **从现在开始,所有页面能力都应先判断它属于哪个 page aggregate 子域,而不是继续新增离散 mutation。**
### 5.3 Leptos Island 的正式边界
`leptos-tiptap` island 的职责应收敛为:
- 消费 `page_body`
- 消费必要的 `page_layout` 编辑器相关配置
- 回发正文与块级命令
- 暴露可供 AI / 页面壳使用的稳定 editor handle
它不应再负责:
- 定义页面总真相
- 自己发明页面设置语义
- 在页面壳之外另持有一套页面元信息状态
### 5.4 Tree Domain 的接缝
页面树 / 文件树不需要知道编辑器内部细节,但必须与页面聚合共享:
- 同一份 `document_id`
- 同一份页面标题
- 同一份 `resource_meta`
- 同一份页面可见状态与能力语义
也就是说:
- 树域保持 tree-first
- 页面域保持 page aggregate
- 两者通过稳定 projection contract 对齐
而不是:
- 树域一套标题来源
- 页面域另一套标题来源
### 5.5 Convex 的正确定位
当前不应该先拆 Convex。
正确口径是:
- Convex 继续作为存储 / 实时 / 协作底座
- Rust 持有 canonical contract 与语义编排
- Leptos / Next 负责消费 projection 与呈现 island
因此,“Rust 单一真源”在当前阶段的正确含义不是:
- 只有 Rust 数据库
而是:
- **Rust 持有语义单一真源,Convex 持有当前持久化底座。**
---
## 6. 当前哪些地方需要纠偏
## 6.1 需要纠偏的不是“页面里还有前端状态”
页面壳中存在一些本地临时状态本身没有问题。
问题在于:
- 哪些状态是临时 UI 状态
- 哪些状态却在冒充对象真相
后续必须把两者分清。
### 6.1.1 可以继续留在前端壳的状态
- inspector 开关
- comments drawer 开关
- history drawer 开关
- 当前是否在编辑态
- 当前 host observability
这些都是 UI state,不必进入 Rust 真相层。
### 6.1.2 必须退出前端壳真相地位的状态
- 页面标题
- 页面宽度 / 页面布局正式选项
- 编辑器默认嵌入位置
- 页面 outline 的正式来源
- 页面 stats 的正式来源
这些都不应继续被前端 `useState` 长期定义为页面事实。
## 6.2 需要纠偏的不是继续换保存底座
当前正文保存链已经基本可用,真正的问题不是“保存不到页面”,而是:
- 保存成功不等于 page aggregate 已建立
因此:
- 现在不该推翻 `documents.save`
- 也不该另造一套临时保存格式
而应做的是:
> **把标题 / 正文 / 页面设置的语义边界对齐到同一 page aggregate 之下。**
## 6.3 需要纠偏的是页面设置的产品口径
当前页面设置里已经混入了三类字段:
- 真正属于页面布局的
- 真正属于阅读壳的
- 真正属于编辑器 runtime 的
后续必须先分类,再决定哪些继续保留在页面设置面板里,哪些推迟支持,哪些进入 editor island。
否则只会继续出现:
- UI 有按钮
- 数据能存
- 但主编辑器不真正消费
---
## 7. 分阶段落地
下面的阶段不是完整开发清单,而是主线纠偏顺序。
## 7.1 Phase F: 冻结 Page Aggregate Contract
目标:
- 在设计层明确页面聚合的 canonical contract
- 不再让页面页头 / 正文 / 设置 / 子树各自长字段
完成标准:
- 有单独的 page aggregate contract 文档
- 明确 `page_head / page_layout / page_body / page_tree` 的最小字段
- 明确哪些字段是 UI state,哪些字段是 aggregate truth
## 7.2 Phase G: 主文档页改为消费统一聚合
目标:
- 文档页不再手工拼 `title + options + content + pageSubtree`
- 改为消费统一页面聚合返回值
完成标准:
- 页面头部使用聚合中的 `page_head`
- 页面设置初始值使用聚合中的 `page_layout`
- 主编辑区 bootstrap 使用聚合中的 `page_body`
- 树与 TOC 使用聚合中的 `page_tree`
## 7.3 Phase H: `leptos-tiptap` 正式消费 editor-related page options
目标:
- 把真正属于编辑器 runtime 的设置接入 `leptos-tiptap`
最小优先级建议:
1. `wideLayout`
2. `smallText`
3. `layoutDensity`
4. `showHeadingNumbers`
5. `embedDefaultBlockId`
完成标准:
- inspector 改动后,主编辑区表现真实变化
- 不再出现“设置能存,但编辑器内部几乎没变化”
## 7.4 Phase I: 树域与页面域标题统一
目标:
- rename 后页头、sidebar row、page tree、file tree 一致刷新
完成标准:
- 标题变更只经过一套 page aggregate command 语义
- 所有标题消费者都来自同一份更新后的 projection
## 7.5 Phase J: AI 写入入口对齐 page aggregate
目标:
- AI 不再绕过 page aggregate 直接拼前端对象
完成标准:
- AI 能通过统一页面命令把内容写入主编辑区
- 人工编辑与 AI 编辑共享同一块语义边界
---
## 8. 当前阶段的完成判定
只有当下面这些条件同时成立时,才能把这条线描述成:
> **“主编辑区与树域已经基本统一到 Rust 主导的单一真源架构”。**
- 文档页消费统一 `page aggregate projection`
- 标题 / 正文 / 页面设置不再是三条分裂真相链
- `leptos-tiptap` island 真正消费 editor-related `page_layout`
- 树标题与页头标题来自同一份 projection
- AI 写入入口开始围绕 page aggregate command family 设计
在此之前,正确口径都应保持为:
> **`leptos-tiptap` 主编辑区已基本可用,但页面聚合仍未收口,当前仍处于从混合态向 Rust 单一真源过渡的过程中。**
---
## 9. 本文结论
这次暴露出的页面宽度不统一、页面设置只部分生效,以及标题链虽已收口但聚合仍未统一,并不是坏消息。
它们的价值在于:
> **它们准确暴露了当前真正缺的不是“再修几个细节”,而是“把页面域提升成一个与树域对齐的 Rust page aggregate”。**
因此,后续主线不应继续散落成:
- 修标题
- 修宽度
- 修页面设置
而应统一收口为:
> **Page Aggregate Contract**
>
> **Page Aggregate Projection**
>
> **Page Aggregate Command Family**
这才是让 `tree-first graph kernel``leptos-tiptap` 主编辑区、以及后续 AI 直接写入主编辑区三者真正对齐的正确底层方向。
@@ -0,0 +1,269 @@
# 5-6 [process] Page Aggregate 单一真源对齐执行清单 v1
> 更新时间:2026-04-22
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md`
## 1. 文档目的
这份清单不是再讨论“问题是不是存在”,而是把:
- `5-5` 里对当前混合态的判断
- 后续 `Page Aggregate` 主线
整理成一份能持续勾选、持续验收、持续防止跑偏的执行清单。
这份清单只围绕一个目标:
> **让页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区 / AI 写入口,逐步收口到 Rust 主导的 page aggregate 单一真源。**
---
## 2. 当前阶段结论
当前可以确认的事实有两类。
### 2.1 已经成立的事实
- [x] `leptos-tiptap` 已经成为页面内正式主编辑区,不再是 iframe bridge。
- [x] 正文保存已经能按 `workspaceId/documentId` 正确落到对应页面。
- [x] 当前主编辑区已具备可继续推进的基础交互能力。
- [x] 当前问题已经不再是“能不能接入主编辑器”,而是“接入后如何收口为单一真源”。
### 2.2 还没有成立的事实
- [ ] 页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区还没有消费同一份 page aggregate projection。
- [x] 标题 / 正文 / 页面设置已经开始统一到同一组 page aggregate command family。
- [ ] `pageOptions` 还没有整体收口到 `leptos-tiptap` island 的正式运行时语义层。
- [ ] AI 写入口还没有完整对齐 page aggregate command family。
补充:本轮已新增前端统一 `page-command-client`,并把 `DocumentContent` 的标题 / 页面设置写入、AI 正文写回、以及 `BlockNote` / `leptos-tiptap` 各 host 的正文保存统一到 `page.head.updateTitle / page.layout.updateOptions / page.body.save`。同时,Next route 侧已新增统一 `page-write-command-adapter``/api/documents/title``/api/documents/options``/api/documents/save` 三条页面写链路已开始共用同一层执行器。这里勾选的是“命令执行面开始收口”,不等于页面域真相链已经完全统一。
补充:2026-04-28 树域剩余 runtime 收口中,`page.body.saved``document.snapshot.saved` 已完成职责拆分;`tree.node.embed``pageReference` block 结构与插入位置已由 Rust `pageAggregateEmbedPlan` 生成,3000 route 只保留源/目标内容读取作为 substrate preflight。这使正文保存快照语义和页面嵌入正文 patch 都开始落在 Page Aggregate / Rust artifact 边界内。
---
## 3. 当前明确不做什么
为了避免再次回到“零散补 UI”的老路,下面这些项当前不进入主线前排:
- [ ] 不把当前问题重新降级成“修两个样式 bug”。
- [ ] 不继续通过新增前端本地状态来掩盖 page aggregate 缺失。
- [ ] 不先重做一轮页面设置 UI。
- [ ] 不先扩一批和 page aggregate 无关的编辑器花活。
- [ ] 不先争论替换 `Tiptap`、替换 `leptos-tiptap`、移除 Convex。
---
## 4. Phase F:冻结 Page Aggregate Contract
目标:
> **先把页面聚合的 canonical contract 写清楚,阻止页面头部、正文、设置、子树继续各自长字段。**
### 4.1 设计层完成标准
- [x] 单独补一份 page aggregate contract 文档。
- [x] 文档中固定 `page_identity / page_head / page_layout / page_body / page_tree` 的最小字段。
- [x] 文档中固定哪些字段属于 page aggregate truth,哪些字段只是前端 UI state。
- [x] 文档中固定哪些字段必须由 Rust 主导,哪些字段允许作为前端临时态存在。
### 4.2 代码层完成标准
- [x] 前端与 Rust 侧都出现统一命名的 page aggregate 类型或等价契约。
- [x] 不再继续给 `DocumentPageProps``DocumentContentProps` 零散加字段来扩页面真相。
- [x] 文档页加载入口能明确区分“聚合读取结果”和“局部 UI 临时态”。
补充:当前已新增 `page-aggregate-builder.ts``page-aggregate-loader.ts``/api/documents/page`,文档页 SSR 入口和 `DocumentContent` 的内容重试补拉都已改为消费同一份 `PageAggregateProjection`,不再由 `page.tsx` 手工拼 `meta + content`。Rust 侧仍未直接暴露同名 `Page Aggregate` projection route,但 `storage-convex-bridge``bridge-runtime` 已开始接受 `page.head.updateTitle / page.layout.updateOptions / page.body.save` 这组 page command family 的命名口径,因此这里先按“等价契约已出现”勾选完成。
### 4.3 退出标准
- [x] 后续再讨论标题、页面设置、正文保存时,能够直接定位到它属于 `page_head / page_layout / page_body / page_tree` 的哪一层。
---
## 5. Phase G:主文档页改为消费统一聚合
目标:
> **让 `/documents/[id]` 不再手工拼 `title + options + content + pageSubtree`,而是消费统一页面聚合。**
### 5.1 加载链路收口
- [x] 页面 SSR / route 入口改为优先读取统一 page aggregate,而不是分别读取 meta 和 content。
- [x] `DocumentShell` / `DocumentContent` 的入参改为围绕统一聚合对象组织。
- [x] 页面头部、页面设置初值、主编辑区 bootstrap、TOC/outline 都来自同一份聚合结果。
### 5.2 当前散落 props 的收口
- [x] `title` 不再作为独立真相字段散落传递。
- [x] `initialOptions` 不再作为独立真相字段散落传递。
- [x] `initialContent``initialRevision``initialConflictDetectionKey` 归入统一 `page_body`
- [x] `initialPageSubtree` 归入统一 `page_tree`,并明确其是正式 projection 还是仅兼容过渡项。
### 5.3 退出标准
- [x] 页面加载时不再能明显看出“这是几份子结果拼出来的页面”。
- [ ] 后续页面新增字段时,不再需要继续向外层 props 链同时塞多种局部真相。
补充:当前首屏 SSR 与客户端内容重试补拉都已经走 `/api/documents/page -> page aggregate loader -> PageAggregateProjection`,因此“页面明显由 `meta + content` 两次查询拼起来”的入口级痕迹已经消失;但新增字段仍可能需要继续补 loader / route / island 消费链,所以第二项继续保留未完成。
补充:本轮已新增 `page-aggregate-client-state.ts`,并让 `DocumentContent` 把原先分散维护的 `options / content / serverContentSnapshot / serverPageSubtreeSnapshot / serverPageSubtreeTitle / contentRevision / conflictDetectionKey` 开始收口为同一份 client aggregate state reducer。这里代表“页面本地 `body/layout/tree` 真相已经开始统一”,不等于标题链、页面设置运行时分类、AI 对页面设置面的正式入口都已闭环,因此本阶段不提前宣称聚合完成。对应最小回归测试为 `page-aggregate-client-state.test.ts`
---
## 6. Phase H:让 `pageOptions` 真正进入 `leptos-tiptap` island
目标:
> **把当前“能展示、能保存、但主编辑区不真正消费”的页面设置,按优先级接入 island。**
### 6.1 必须先分类
- [x] 把当前页面设置分成:页面壳布局类、阅读视图类、编辑器 runtime 语义类。
- [ ] 明确哪些项应继续保留在页面设置面板中,哪些项应降级或隐藏,哪些项必须进入 island。
补充:本轮已新增 `page-option-semantics.ts`,把 `pageOptions` 的运行时语义正式收口为代码契约,不再让这套规则继续散落在 `page-options-sidebar.tsx``editor-host-types.ts``leptos-tiptap-island-editor-host.tsx` 里各写一份。当前至少已经明确:
- `wideLayout / smallText / layoutDensity` 属于 `page_shell_layout + editor_runtime`
- `showHeadingNumbers` 属于 `read_view + editor_runtime`
- `showToc` 属于 `read_view`
- `protectEditing / showBlockRefCount` 属于 `editor_runtime`,但当前仍是 `planned`
- `embedDefaultBlockId` 已进入 `editor_runtime`
对应最小回归测试为 `page-option-semantics.test.ts``leptos-tiptap-island-editor-host.test.tsx``page-options-sidebar.test.tsx`。第二项继续保留未完成,因为“哪些项应降级/隐藏/保留”的产品面纠偏还没完全落到 inspector 与 AI tool surface。
### 6.2 最小接入优先级
- [x] `wideLayout` 真正进入 island 布局语义,而不是只改外层壳宽度。
- [x] `smallText` 真正进入主编辑区排版语义。
- [x] `layoutDensity` 真正进入块间距 / 正文密度语义。
- [x] `showHeadingNumbers` 真正进入 heading 展示语义,或明确标注暂不支持。
- [x] `embedDefaultBlockId` 真正进入主编辑器引用 / 嵌入默认位置逻辑,或明确标注暂不支持。
### 6.3 页面设置面板纠偏
- [x] 没有真正接入 island 的编辑器语义项,不再继续以“已开启/已关闭”假装正式完成。
- [x] 对未支持项给出显式降级说明,而不是仅保存字段。
- [ ] 让“页面设置有值但编辑器内部没变化”这类状态在产品上消失。
### 6.4 退出标准
- [ ] inspector 中至少最小优先级项修改后,主编辑区可见结果真实变化。
- [ ] 用户不再需要猜“这个设置到底有没有真正作用到编辑器”。
---
## 7. Phase I:树域与页面域标题统一
目标:
> **rename 后页头、sidebar row、page tree、file tree 使用同一份标题结果。**
### 7.1 标题真相纠偏
- [x] 页面头部标题不再长期由前端 `pageTitle` 本地状态冒充正式真相。
- [x] rename 命令在语义上进入统一 page aggregate command family。
- [x] 树域消费的标题与页面头部消费的标题来自同一份更新后的 projection。
### 7.2 回归验证
- [x] 当前页重命名后,页头即时更新。
- [x] sidebar 对应节点标题同步更新。
- [x] page tree / file tree 对应节点标题同步更新。
- [x] 刷新后标题一致,不依赖额外手动刷新。
- [x] 切页往返后标题一致,不出现旧快照回闪。
注:当前已在标题持久化成功后广播 `emitDocumentsChanged(documentId)`,并补了 `sidebar-events.test.ts` 覆盖 `documents-changed -> sidebarRefetch` 的刷新链路。
补充:已新增 `usePreferredSidebarSnapshot`,当 `tree stream` 已连上但快照落后、`documents-changed` 触发的 query/refetch 先拿到新标题时,会优先消费更新后的 canonical sidebar snapshot;待 stream 追平后再回到 live stream。对应回归测试为 `use-preferred-sidebar-snapshot.test.tsx`。这修掉了“刷新链发出去了,但 stale stream 仍把 sidebar 标题压回旧值”的一类问题;浏览器层 `sidebar row / page tree / file tree` 真实渲染验收仍待补齐。
补充:已新增 `AppLayoutShell`,把 layout 顶栏 `Breadcrumb` 从 SSR 注入的静态 `documents` 挪到与 `Sidebar` 共享的同一条 live sidebar snapshot 管线;并且 layout shell 会把“已选中的 preferred snapshot”同一对象同时透传给 `Sidebar``Breadcrumb`,不再各自独立选择。对应回归测试为 `app-layout-shell.test.tsx`。这意味着 breadcrumb / sidebar 现在至少共享同一份工作区树 canonical snapshot,不再是 layout 一条静态链、sidebar 一条 live 链并行。页头 `page.head.title` 与这条工作区树链之间的最终统一验收仍待补齐,因此本阶段继续不提前打满。
补充:已新增 `PreferredSidebarSnapshotProvider``usePageHeadTitle`,把文档页头标题从 `DocumentContent` 内部长期持有的 `pageTitle` 本地真相,改为“同一份 preferred sidebar snapshot 的 committed title + 短暂 draft”。同时修正 `useSidebarData.refetch()`,在 Convex live 模式下收到 `documents-changed` 也会主动拉取一份新的 `/api/sidebar` snapshot,再与 tree stream 做 freshness 选择,避免“页头草稿是新的,但 breadcrumb / sidebar / page tree / file tree 还卡在旧快照”。对应单测为 `use-page-head-title.test.tsx``use-sidebar-data.test.tsx`。浏览器烟测 `scripts/task110-page-title-single-truth-smoke.js` 已验证:页头重命名后,breadcrumb、默认 sidebar、page tree、file tree、切页往返、刷新均保持一致,因此本阶段与“标题来自同一份更新后的 projection”相关的勾选正式保留。
### 7.3 退出标准
- [x] 标题不同步问题不再以“前端本地状态漂移”的形式重复出现。
---
## 8. Phase JAI 写入口对齐 Page Aggregate
目标:
> **AI 不再绕过 page aggregate,直接拼前端对象或编辑器内部格式。**
### 8.1 语义边界
- [x] 明确 AI 写页面时优先进入哪组 page aggregate command。
- [x] 明确 AI 写标题、写正文、改页面设置、插入引用时分别走哪些统一命令语义。
- [x] 明确 AI 不直接拼 DOM、不直接拼前端页面壳对象、不直接依赖浏览器临时状态。
补充:当前页面命令名已开始从 `documents.*` 收口到 `page.head.updateTitle / page.layout.updateOptions / page.body.save`,其中标题 route、页面设置 route、正文保存 route 都已切到这组 page command family`storage-convex-bridge``bridge-runtime` 也已补齐对应 alias。
补充:本轮进一步把前端调用面与 Next route 执行面也开始统一到这组 `page.*` family:前端已新增 `page-command-client`,不再让 `DocumentContent`、AI 面板、各编辑器 host 各自维护一套页面写入口;服务端已新增 `page-write-command-adapter``title/options/save` 三条 route 不再分裂在 `metadata/save` 两套执行器里。对应最小回归测试为 `page-command-client.test.ts``page-write-command-adapter.test.ts``route-adapters.test.ts``DocumentAiAgentPanel.runtime.test.tsx``document-content.test.ts`
补充:本轮继续把 AI 面板的读取边界开始收口到统一页面本地快照:`DocumentContent` 不再向 `DocumentAiAgentPanel` 透传 `getLatestBlocks / getLatestPageSubtree / getLatestPersistedMeta` 三组分散 getter,而是改为一份 `getLatestPageAggregateSnapshot`,由 `page-aggregate-client-state` 导出 `blocks / pageSubtree / persistedMeta / pageOptions`。这代表 AI 面板已开始消费同一份页面本地 aggregate state,而不是继续拼接独立局部真相;但页面设置写工具本身仍未进入 Hermes 正式 tool surface,因此这里仍然只算“开始收口”,不提前打满。
补充:本轮还把 `pageOptions``editorRuntimePageOptions` 一并带入 `/api/ai-agent/run -> buildHermesInstructions`,因此 AI 在服务端至少能看到“当前页面设置是什么”以及“哪些设置已经进入 island runtime payload”,不再只依赖正文块快照和树快照来猜测页面语义。对应回归测试为 `src/app/api/ai-agent/run/route.test.ts`。这推进的是 AI 读侧上下文,不等于页面设置写工具已经具备正式入口。
### 8.2 与主编辑区的关系
- [x] 人类编辑与 AI 编辑共享同一块语义边界。
- [x] AI 改写结果能通过主编辑区 island 正式回显。
- [ ] AI 改写后树标题 / 页面头部 / 页面设置不再走各自独立副作用链。
### 8.3 退出标准
- [x] AI 写入口已经可以被明确描述为“操作 page aggregate command family”,而不是“绕过系统写编辑器”。
注:当前 `/api/ai-agent/run``Hermes tool.completed` 后,会优先尝试把 `slash_run / doc_insert_blocks / doc_replace_range` 恢复成 `mnote-web bridge-runtime` 的结构化 `tool_result`,不再只把 Hermes 事件当作薄日志。其后:
- `doc_insert_blocks / doc_replace_range` 继续按 `page.body.save` 语义落到 `/api/documents/save`,再正式回显主编辑区 island。
- `slash_run(rename current page)` 会把结构化结果回接到当前页 `DocumentContent` 的同一条标题提交链,并继续广播 `emitDocumentsChanged(documentId)`,因此页头标题与树标题不再靠 AI 面板内部本地状态各自漂移。
- 当前 `pageOptions` 仍没有进入 Hermes 正式 tool surface,因此“页面设置类 AI 命令”尚未收口;`8.2` 的最后一项继续保留未完成,避免误判为整条线已经闭环。
---
## 9. 推荐实施顺序
当前推荐顺序固定为:
1. `Phase F`
2. `Phase G`
3. `Phase H`
4. `Phase I`
5. `Phase J`
约束如下:
- [x] 没有完成 `Phase F` 之前,不再继续零散补标题 / 宽度 / 页面设置。
- [ ] 没有完成 `Phase G` 之前,不把当前文档页描述成“已经统一聚合完成”。
- [ ] 没有完成 `Phase H` 之前,不把页面设置大量打钩为“正式可用”。
- [ ] 没有完成 `Phase I` 之前,不把标题问题视为已从底层解决。
- [ ] 没有完成 `Phase J` 之前,不把 AI 直接写主编辑区描述成已经具备正式稳定入口。
---
## 10. 当前阶段的总退出标准
只有当下面这些条件同时成立时,才可以把这条线描述成:
> **“主编辑区与树域已经基本统一到 Rust 主导的 page aggregate 单一真源架构”。**
- [x] 文档页消费统一 `page aggregate projection`
- [ ] 标题 / 正文 / 页面设置不再是三条分裂真相链
- [x] `leptos-tiptap` island 真正消费 editor-related `page_layout`
- [x] 树标题与页头标题来自同一份 projection
- [x] AI 写入口开始围绕 page aggregate command family 实现
补充:当前未勾选“标题 / 正文 / 页面设置不再是三条分裂真相链”的原因,不再只是命令名或 route 分裂。虽然前端页面写入口、Next route 执行器、`DocumentContent` 内部的 `body/layout/tree` client state、以及 `pageOptions` 的代码级运行时分类都已经开始收口,但 projection 回流与 AI 对页面设置面的正式写入口仍未完全统一。也就是说,命令执行面、页面本地状态面、页面设置语义面都已开始统一,但页面域单一真源仍未闭环。
在此之前,正确口径都应保持为:
> **`leptos-tiptap` 主编辑区已基本可用,但页面聚合仍未收口,当前仍处于从混合态向 Rust 单一真源过渡的过程中。**
@@ -0,0 +1,935 @@
# 5-7 [process] Wolai 页面树与主编辑器体验复刻方案 v1
> 更新时间:2026-04-30
>
> **Wolai-aline 执行口径更新(2026-04-30):** 本文只保留体验复刻任务拆解和产品目标。所有 Wolai 对标测试、浏览器取证、编辑权限、安全边界、subagent 使用、截图复核和 smoke 补齐,统一以 `/home/lix/.codex/skills/wolai-aline` 与 `/mnt/Data1T/mnote/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md` 为准。若本文旧段落与该 skill 冲突,以 skill 为准。
>
> 关联文档:
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-2-tiptap-notion-like-template-adoption-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
## 1. 文档目的
这份方案只处理一件事:
> **以真实 Wolai 页面为视觉和交互 benchmark,继续还原 mnote 的页面树与主编辑器体验。**
本轮目标不是重新定义系统架构,也不是把 Wolai 的全部协作能力完整复制过来,而是把当前 `3000` 页面在视觉密度、布局节奏、页面树反馈、主编辑器输入体验上继续向 Wolai 靠近。
约束如下:
- UI 以像素级复刻为目标。
- 功能以“高频路径可用、复杂能力可降级”为原则。
- 当前项目保留“文件树 / Explorer”创新,不要求删除。
- 默认主编辑器继续是页面内 `leptos-tiptap` island。
- `BlockNote` 只作为 fallback / 对照链,不重新变成默认方向。
- 树、页面结构、标题、正文、页面设置不能在 UI 层再拼第二份真相。
这份文档应放在 `05-editor-mainline/process`,因为它的核心不是 Rust Web 壳复刻,也不是单独的树命令合同,而是:
> **围绕 Page Aggregate,把页面树、页头、页面设置、正文编辑器统一成接近 Wolai 的产品体验。**
---
## 2. 本轮取证基线
### 2.1 真实 Wolai 参考
本轮参考页面:
- `https://www.wolai.com/wolai/xhqeop8UHpVTMUSVmgz8nq`
- `https://www.wolai.com/wolai/qN1Bh9YjLAXs8bxCJoAJ6C`
- `https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd`
本轮已保存的截图与快照:
- `/mnt/Data1T/mnote/tmp/wolai-page1.png`
- `/mnt/Data1T/mnote/tmp/wolai-page1-snapshot.md`
- `/mnt/Data1T/mnote/tmp/wolai-page2.png`
- `/mnt/Data1T/mnote/tmp/wolai-page2-snapshot.md`
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-target-1392x1213.png`
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-target-snapshot.md`
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-public-search-overlay.png`
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-public-search-results-skill.png`
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-public-presentation-mode.png`
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-public-start-edit-overlay.png`
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-public-comment-panel.png`
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-public-good-night-mode.png`
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-help-first-viewport-current.png`
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-help-sidebar-filter-page-reference.png`
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-help-search-overlay.png`
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-help-search-results-page-reference.png`
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-help-search-result-navigated-block-reference.png`
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-help-embedded-page-reference-hover.png`
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-help-block-reference-page-snapshot.md`
- `/mnt/Data1T/mnote/wolai-basic-editing-reference.png`
- `/mnt/Data1T/mnote/wolai-basic-editing-reference-snapshot.md`
- `/mnt/Data1T/mnote/wolai-hermes-public-hover-block.png`
- `/mnt/Data1T/mnote/wolai-hermes-start-edit-login-dialog.png`
- `/mnt/Data1T/mnote/wolai-hermes-comment-panel.png`
- `/mnt/Data1T/mnote/wolai-hermes-presentation-mode.png`
- `/mnt/Data1T/mnote/wolai-hermes-good-night-mode.png`
用户在内置浏览器中提供的登录态截图也作为本轮重要视觉证据:该截图展示了真实个人空间的长页面树、当前页 `Hermes` 选中态、完整顶部图标区、底部垃圾桶 / 模板中心入口、右下角 AI / 帮助浮动入口。
补充说明:历史截图和用户提供的登录态截图只作为现有证据池,不再作为后续任务的唯一验收依据。该 owner 态菜单不是单纯样式:它包含“在右侧边栏打开 / 移动到 / 嵌入到 / 复制访问链接 / 复制页面引用链接 / 复制页面 ID / 拷贝副本 / 重命名 / 删除”等页面树动作,应进入后续交互合同,而不是只当成截图复刻。
2026-04-30 口径更新:后续取证统一从 `wolai-aline` skill 启动。已知技术路径是 `Playwright Node + /opt/google/chrome/chrome + /mnt/Data1T/mnote/tmp/wolai-playwright-profile`,但本文不再维护独立自动化方案。当前 Hermes 测试页已授权用于最小编辑验证;owner 首屏、搜索浮层、顶部更多菜单、正文块 hover、`.hover-block-menu` 块菜单等行为,都必须通过 subagent 浏览器取证、主线程截图复核和差异矩阵重新确认。`dokobot doko read --local --reuse-tab` 可作为登录态 read evidence,但不能替代键鼠操作验收。
### 2.2 当前 mnote 参考
当前本地 `3000` 截图:
- `/mnt/Data1T/mnote/tmp/wolai-compare/local-3000-playwright-1392x1213.png`
- `/mnt/Data1T/mnote/tmp/wolai-compare/local-3000-first-viewport.png`
- `/mnt/Data1T/mnote/tmp/wolai-compare/local-3000-dom-snapshot.txt`
当前本地页面已经具备接近 Wolai 的基本壳:
- 左侧 workspace / 快捷操作 / 页面树 / 底部入口。
- 右侧顶部 breadcrumb / 页面操作。
- 中央文档页标题和正文区。
- 右下角帮助与 AI 入口。
但与真实 Wolai 仍有显著差距:
- 左侧树的视觉密度、选中态、图标系统、滚动条和层级缩进仍不够像 Wolai。
- 文档主内容的起始位置、标题尺寸、正文列宽、页头留白和默认元信息展示不一致。
- 顶栏按钮体系与 Wolai 的 owner / published 两种状态还没有明确分型。
- 主编辑器的块级 hover 手柄、slash、浮动工具条和块菜单需要按 Wolai / Notion 风格继续收口。
- 当前页面树、页头、正文、页面设置仍必须继续服从 Page Aggregate 单一真源主线,不能为了 UI 快速复刻重新制造局部状态。
### 2.3 2026-04-30 subagent 基线复核
本轮已按 `wolai-aline` skill 派 subagent 同时操作 Wolai Hermes 与本地 `3000`。工具链结论:`Node.js Playwright + /opt/google/chrome/chrome` 可操作两个目标;Wolai 通过 `/mnt/Data1T/mnote/tmp/wolai-playwright-profile` 进入 owner 登录态,本地 `3000` 返回可用页面。无源码改动,证据写入:
- `/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/`
- `/mnt/Data1T/mnote/tmp/wolai-editor-parity/doc-baseline-mini/`
主线程已复核关键截图:
- Wolai 首屏:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/doc-baseline-mini/wolai-initial.png`
- 本地首屏:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/doc-baseline-mini/local-initial.png`
- Wolai 搜索:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/wolai-03-search-modal-controls-filled.png`
- 本地搜索:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/local-03-search-modal-controls-filled.png`
- Wolai 块 hover`/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/wolai-08-block-hover-insert-entry.png`
- 本地块 hover`/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/local-14-block-hover-insert-entry.png`
- Wolai 编辑清理后:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/wolai-14-title-suffix-after.png`
当前差异矩阵:
| 项 | Wolai Hermes | 本地 `3000` | 后续处理 |
| --- | --- | --- | --- |
| 首屏入口 | owner 登录态直接显示 `Hermes` 正文页,标题、正文、页面树同屏 | 默认是聚合 / 预览壳,正文编辑器需点击 `打开当前页面` 才出现 | 拆成 P0/P1 独立任务;先用 smoke 固化“普通文档首屏直接进入正文体验”或明确产品降级 |
| 侧栏数据与选中态 | 真实空间树,当前页在 `个人 / 软件开发 / Hermes` 路径下,红色选中态明显 | anonymous 示例树,当前页为 `1111`,行密度和图标体系仍偏本地化 | Sidebar checklist 必须同时验视觉和真实 active path |
| 搜索入口 | 左侧放大镜可打开全局搜索;本轮 `Ctrl+K` 未稳定打开,`Ctrl+P` 在另一轮可 toggle | 顶栏搜索可打开;侧栏搜索曾修为 modal;`Ctrl+P` 可 toggle | 搜索任务不得只测一个入口;需记录每个入口、快捷键和 URL |
| 搜索控件 | modal 居中,遮罩、结果行、快捷键提示完整;switch 视觉可见 | modal 结构接近,`role="switch"` 可测;不同输入下结果为空或本地数据结果 | switch 默认状态必须同时记录截图和 `aria-checked`,结果态需拆数据差异与控件差异 |
| 搜索结果 | 输入 `Hermes` 命中页面和块,结果有红色高亮和路径 | 本地同词可出现空态或示例数据结果,取决于当前页面数据 | 建立固定本地 fixture 或 smoke 种子,不能用随机工作区数据验视觉 |
| 正文编辑区 | 首屏正文 `contenteditable=true`,标题也可编辑 | 首屏无可见编辑区;点击后出现 title textarea 与 `.tiptap.ProseMirror` | 文档画布任务必须先消除“打开当前页面”入口语义差异或显式记录降级 |
| 编辑焦点风险 | Wolai 标题和正文均可编辑,测试输入曾误落到标题,已清理 | 本地输入 / 撤销可完成,但页面结构不同 | editable-test mode 前必须先断言焦点 block 类型,优先在正文末尾新建唯一测试块 |
| 块 hover | 正文块左侧出现轻量块控制入口,视觉很克制 | 本地显示 `+` 与拖拽/块句柄按钮,但位置、密度、内容列差异明显 | E2-E4 必须以截图和 hover 点位复核,不可只看按钮存在 |
这轮基线说明:当前最大偏差不是单个按钮,而是本地普通文档首屏仍像“聚合入口 / 预览页”,Wolai 则直接进入可读写的页面正文。后续 P0/P1 checklist 必须优先处理这个入口语义,否则搜索、块 hover、编辑器测试都会测到不同页面状态。
### 2.4 Wolai-aline 取证与验收统一口径
本节旧版“published / owner readonly / sandbox mutation”分层已经收口到 `wolai-aline` skill。后续执行不再从本文推导测试策略,统一遵循:
- Codex skill`/home/lix/.codex/skills/wolai-aline`
- 流程文档:`/mnt/Data1T/mnote/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md`
- 失败模式:`/home/lix/.codex/skills/wolai-aline/references/failure-patterns.md`
现行要点:
- 每个 Wolai-aline 小任务都必须先取 Wolai 基线,再做本地 RED smoke,再实现,再由 subagent 浏览器复测,最后主线程复核截图和差异矩阵。
- 浏览器取证必须使用 subagent;subagent 默认只读,但当任务明确需要编辑器行为时,可在当前 Hermes 测试页进入 `Hermes editable-test mode` 做最小编辑验证。
- 当前 Hermes 测试页 `https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd` 已授权用于最小编辑验证;可以创建带唯一标记的临时测试块、输入少量测试文本、验证 slash / toolbar / block menu / 快捷键等编辑器行为。
- 编辑验证必须记录编辑前后截图、动作链、输入内容和清理状态。若安全清理有风险,不扩大操作,报告残留测试内容。
- 即使在 Hermes 测试页,也禁止未经确认地删除、移动、归档、发布、评论、改权限或批量修改既有非测试内容。
- 其他 Wolai 页面仍默认只读;写入仍需用户提供沙盒页 URL 和明确授权。
- 对标不能只看文案或 DOM 是否存在;必须主动检查截图中的控件形态、状态、hover/active、快捷键 toggle、关闭路径和 URL 跳转。
硬门槛:
- 不能把 subagent 文本结论当作最终对齐证据;主线程必须复核截图。
- 如果截图里有肉眼可见差异,先补 smoke 捕获差异,再改实现。
- 任何新发现的稳定失败模式,都要沉淀到 `wolai-aline` skill 的 `failure-patterns.md`
### 2.5 `基本编辑能力` 操作矩阵
`https://www.wolai.com/wolai/qN1Bh9YjLAXs8bxCJoAJ6C` 是 Wolai 对“主编辑器基础操作”的官方内容页。本页不只是说明文字,它给出了后续 `3000` 必须能复现的主编辑器操作矩阵:
| 操作 | Wolai 参考页说明 | 当前对 `Hermes` 可自动化验证性 | `3000` 后续验收要求 |
| --- | --- | --- | --- |
| 普通文本输入 / 回车 / 光标移动 | 像普通文本编辑器一样输入、换行、移动光标 | 可用 Hermes editable-test mode 取基线 | 在本地编辑态 smoke 中验证输入、换行、保存后刷新保持 |
| 块模型 | 文本、列表项、图片、文件等都是“块” | owner 态只读已可观察块 hover 手柄和 block menu | 本地每个块需有稳定 block id 与 hover target |
| 拖动块 | 块左侧 `::` 图标可拖动,并显示辅助线 | owner 态只读可观察手柄;真实拖动是写入动作,按 Hermes editable-test mode 最小验证 | 本地必须验证拖拽预览、横向 drop line、分栏 drop line |
| Esc 选中块 | 输入态按 `esc` 选中当前块,上下方向键切换,`shift + 上/下` 多选,`enter` 回编辑 | 可用 Hermes editable-test mode 取基线 | 本地必须有 block selection state、keyboard smoke |
| `cmd/ctrl + A` | 输入态第一次选中当前块文字,第二次选中所有块;块选中态直接选中所有块 | 可用 Hermes editable-test mode 取基线 | 本地必须区分 text selection 与 block selection |
| 上 / 下插入块 | hover 左侧 `::` 上方 / 下方点击 `+`;或 `esc` 选中块后按 `a` / `b` | owner 态可观察手柄;真实插入可用 Hermes editable-test mode 做最小验证 | Hermes editable-test mode 必须验证 before / after 插入与 `Esc, a` / `Esc, b` |
| 块布局显示 | `cmd/ctrl + shift + U` 显示 / 隐藏块布局虚线框 | 可用 Hermes editable-test mode 取基线 | 本地需有布局 overlay smoke,至少验证虚线框开关 |
| 分栏 | 拖块到另一个块左 / 右,辅助线由横线变竖线,松开形成分栏 | 可用 Hermes editable-test mode 取拖拽基线 | 本地可先做 drop preview,不要求完整分栏持久化一次到位 |
| 转换块 | 通过块菜单 `转换为`、快捷键、或 slash 命令转换块类型 | owner 态已可打开块菜单并看到 `转换为`;执行转换可用 Hermes editable-test mode 做最小验证 | 本地必须验证块菜单中的 `转换为` 入口与至少 paragraph/headings/list 的转换 |
| 复制 / 粘贴 | 文本或块选中后 `cmd/ctrl + C/V`,匹配样式为 `cmd/ctrl + shift + V` | 可用 Hermes editable-test mode 取基线 | 本地需验证纯文本粘贴与块复制的最小路径 |
| 缩进 / 取消缩进 | `tab` 缩进成为上一块子元素,`shift + tab` 取消缩进 | 可用 Hermes editable-test mode 取基线 | 本地需验证 list / paragraph 缩进语义与 tree projection 不冲突 |
| 文本样式工具条 | 选中文字出现工具条,支持粗体、斜体、下划线、删除线、行内代码、颜色、链接、页面引用、转换块类型 | 可用 Hermes editable-test mode 取基线 | 本地需验证 selection toolbar 出现、按钮视觉与 command dispatch |
| slash 菜单 | 输入 `/` 唤起快捷命令菜单;空行左侧 `+` 也可创建块 | 可用 Hermes editable-test mode 取基线 | 本地需验证 `/` 菜单、搜索过滤、选择条目插入块 |
本轮已能在目标页 `Hermes` 自动化验证的 published 操作:
- 顶栏搜索:打开搜索 modal,输入 `技能`,出现 `共1条匹配结果`
- 开始编辑:点击后出现 `登录以编辑` 对话框,文案为“登录后,您才可以编辑该页面,是否继续?”,包含 `取消``继续`
- 评论:点击后打开右侧评论面板,tab 为 `未解决(0)`,空态为 `还没有评论`
- 演示模式:点击后进入 presentation view,隐藏侧栏 / 顶栏,画面只保留大字号内容和演示控件。
- Good Night:右下角主题按钮切换暗色 token。
- 正文块 hover:owner 态可在块左侧看到手柄,并可点击 `.hover-block-menu` 打开块菜单;上 / 下插入块等写入路径按 `wolai-aline` 的 Hermes editable-test mode 验收。
因此,后续 `3000` 的主编辑器验收不能只做“截图像”。每个主编辑器改动都必须绑定一个可执行操作:
- 如果是 published 态能力,必须先在 Wolai published 目标页执行同类操作,再在 `3000` 执行同类操作。
- 如果是 owner/edit 态能力,必须先通过 `wolai-aline` 流程在 Wolai 目标页执行同类操作,再在 `3000` 执行同类操作。
- 如果是 owner/edit 写入能力,当前 Hermes 测试页允许最小编辑验证;其他 Wolai 页面写入必须先取得沙盒页 URL 与明确授权。
- 如果动作是在当前 Hermes 测试页做最小编辑验证,例如输入测试文本、创建临时测试块、验证 slash / toolbar / block menu / 快捷键,可按 `Hermes editable-test mode` 执行并记录前后证据;删除、移动、归档、发布、评论、改权限或批量修改既有非测试内容仍必须在动作发生前单独确认。
---
## 3. 当前代码落点
### 3.1 工作区壳与页面树
相关入口:
- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/layout.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/app-layout-shell.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/server/sidebar-data.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/hooks/use-sidebar-data.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/tree-shell-surface.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/tree-shell-host.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/tree-shell-dom-host.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/tree-projection.ts`
判断:
- `Sidebar` 外壳和 `tree-shell-*` 是页面树像素复刻的主战场。
- `tree-shell-dom-host` 承担当前树行渲染与本地展开/选中/拖拽反馈,不应再回到旧 React 树渲染器上做长期改造。
- `tree-projection` 是页面树 UI 的直接 projection 输入,不应在 UI 层重新推导排序、层级或合法性。
### 3.2 文档页与主编辑器
相关入口:
- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-aggregate-loader.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-aggregate-builder.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-shell.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-read-view.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-island-editor-host.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-option-semantics.ts`
判断:
- `document-content.tsx` 决定 Wolai 页面顶部留白、标题、正文容器、页面元信息、右侧 inspector 和 fallback banner。
- `document-read-view.tsx` 决定阅读态块样式、任务块、标题层级、附件 / 引用 / 子页面等展示。
- `leptos-tiptap-island-editor-host.tsx` 决定编辑态输入 surface、保存、slash、工具条、块菜单和 editor runtime page options 的接缝。
- `page-option-semantics.ts` 是页面设置能否真实进入主编辑器运行时的判断边界,不能只改设置面板外观。
### 3.3 全局视觉 token
相关入口:
- `/mnt/Data1T/mnote/wolai-frontend/src/app/globals.css`
判断:
- 像素复刻必须先固定字体、字号、行高、颜色、圆角、hover/active token。
- 当前全局字体口径有冲突:前面偏 Wolai 系统字体,后面 `html` 又偏 `Inter`。若不先收口字体栈,后续所有标题宽度、行高、树行密度都会偏。
---
## 4. Wolai 视觉目标合同
### 4.1 字体与基础排版
目标字体栈:
- 优先中文系统字体:`PingFang SC``Noto Sans CJK SC``Microsoft YaHei UI`
- 英文字体跟随系统 sans-serif,不单独强行使用 `Inter` 作为全局主字体。
目标字号与行高:
| 区域 | 字号 | 行高 | 字重 |
| --- | --- | --- | --- |
| 页面主标题 | 34px | 44px | 600 |
| 首页 / 帮助页大分组标题 | 28px | 36px | 600 |
| 次级分组标题 | 18px | 26px | 600 |
| 正文基础文字 | 16px | 24px | 400 |
| 左侧树条目 | 14px | 24px | 400 |
| 顶栏文字按钮 | 14px | 21px | 400 |
| 搜索框文字 | 14px | 22px | 400 |
目标颜色:
| 语义 | 颜色 |
| --- | --- |
| 主标题 | `rgb(30, 30, 30)` |
| 正文 | `rgba(0, 0, 0, 0.85)` |
| 次级文字 | `rgba(0, 0, 0, 0.65)` |
| 弱提示 | `rgba(0, 0, 0, 0.35)` |
| 左侧背景 | `rgb(245, 245, 245)` |
| 选中强调文字 | `rgb(207, 86, 89)` |
| 选中强调背景 | `rgba(207, 86, 89, 0.15)` |
| 顶栏 hover 背景 | `rgb(245, 245, 245)` |
### 4.2 左侧页面树
Wolai 目标特征:
- 左侧栏宽度约 `248px - 260px`,浅灰底。
- 顶部空间名行高度约 `40px`,头像为 28px 左右方形圆角块。
- 顶部快捷图标横排,图标弱色,hover 只加浅灰底。
- 搜索框尺寸约 `236px x 32px`,圆角 `4px`,内边距约 `5px 32px 5px 12px`
- 树行高度约 `32px`,内容行内 icon / arrow / title 对齐。
- 当前项使用红字 + 淡红底,背景覆盖整行可点击区域。
- 未选中 hover 使用同一红色体系,但透明度更轻。
- 缩进按层级稳定递增,不靠文本空格。
- 滚动条细、贴右侧、弱灰色,不抢视觉。
- 底部垃圾桶 / 模板中心固定在侧栏底部,边界线轻。
当前 mnote 允许保留:
- `我的页面 / Explorer` 双模式。
- 文件树作为创新入口。
- AI / Graph / 文件等自有入口。
但这些创新必须满足:
- 视觉密度服从 Wolai 左侧栏。
- 选中态和 hover 态与页面树统一。
- 不因为多了文件树而破坏 Wolai 的 32px 行节奏。
### 4.3 顶部导航与页面操作区
Wolai 目标特征:
- 顶栏高度约 `40px`
- 左侧是菜单按钮 + breadcrumb。
- breadcrumb 使用图标 / 文本 / 分隔符,字号 `14px`,颜色弱。
- 右侧按钮是弱文本 / 图标按钮,padding 约 `2px 7px`,圆角 `3px`
- owner 态可见:公开状态、收藏、演示、评论、关系、邀请、历史、更多。
- published 态可见:演示模式、搜索、开始编辑、评论、复制、开始使用 wolai。
- 顶栏 hover 不用重色按钮,只加浅灰底。
mnote 当前差距:
- 顶栏右侧已经有 Public、收藏、历史、AI、搜索、更多,但与 Wolai owner 态的 icon 序列和视觉轻重不同。
- published 视角与 owner 视角没有明确组件分型。
- breadcrumb 与页面内容标题之间的垂直节奏还需要更接近 Wolai。
### 4.4 主文档画布
Wolai 目标特征:
- 主内容列在大屏中不贴左,普通文档正文列宽约 `760px`
- 登录态 `Hermes` 页面首屏标题大约从 y=118px 开始。
- 标题为 `34px / 44px / 600`,颜色接近 `rgb(30,30,30)`
- 标题下方正文块紧跟,不默认显示额外元信息行和横向分割线。
- 普通正文块使用 `16px / 24px`
- 任务块 checkbox 轻边框,小尺寸,和文本基线对齐。
- 空白区域非常克制,不用额外卡片、阴影或说明文案。
mnote 当前差距:
- 当前本地文档页标题偏靠左且偏低,内容列与真实 Wolai 相比没有稳定对齐。
- 当前显示“个人空间 / 工作区首页”元信息和横向分割线,这不是 Wolai 普通文档页默认观感。
- 当前正文首块是“打开当前页面”链接,和目标 `Hermes` 的普通块 / 任务块 / 段落展示差异较大。
- 右下角绿色 AI 浮动按钮比 Wolai 更重,真实 Wolai 是更轻的 AI 方形入口和问号入口。
### 4.5 主编辑器块级体验
Wolai / Notion-like 目标:
- slash 是主编辑入口之一,菜单跟随 caret。
- 选中文本才出现浮动工具条。
- hover 块时只露出左侧手柄,不自动弹菜单。
- 点击左侧手柄才出现块菜单。
- 块菜单核心动作是 `turn into / duplicate / delete / drag`
- `turn into` 同时存在于文本工具条语境和块菜单语境,但命令上下文不同。
- 块 identity 由 Rust `EditorBlock.block_id` 持有;Tiptap `UniqueID` 只作为浏览器 runtime 辅助。
- 页面引用、块引用、嵌入默认位置最终必须回到 Page Aggregate / Rust artifact 边界。
#### 4.5.1 hover 上 / 下插入块
用户补充的登录态截图显示,Wolai 的块 hover 体验还有一个非常关键的细节:当鼠标停在块左侧手柄区域时,当前块行会出现很轻的淡红背景,手柄上方和下方分别出现一条短横线;继续 hover 短横线时,短横线变成 `+`,并显示黑色 tooltip
- `在上方插入块`,快捷键提示为 `Esc, a`
- `在下方插入块`,快捷键提示为 `Esc, b`
这个交互不是装饰性按钮,而是 Wolai 块编辑器的“块级插入光标”。它要和块选中、拖拽手柄、块菜单、slash 菜单、快捷键一起设计,不能只在正文左侧放一个常驻加号。
当前仓库已有可复用参考:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx` 中,旧 `BlockNoteView` 通过 `SideMenuController` 注入自定义 `CustomSideMenu`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/menus/CustomSideMenu.tsx` 中,`WolaiDragHandleWithInsert` 已实现上方插入、下方插入、手柄菜单、菜单冻结、空段落加号打开 slash、截图失焦状态重置。
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-inline-editor-host.tsx` 当前已有较简化的块手柄和菜单,但没有复刻上 / 下短横线插入态。
- `/mnt/Data1T/mnote/rust/spikes/leptos-tiptap-spike/src/lib.rs` 已有 leptos/tiptap 手柄雏形,但当前样式偏重,并且 `insert_top_level_paragraph_after_html` 只支持 after 插入。
旧 BlockNote 实现中最值得迁移的机制:
- 插入控件不是放在块容器上下边界,而是围绕手柄中心点计算位置:上方插入、手柄、下方插入三者垂直对齐。
- 非 hover 状态显示 `12px` 左右短横线,hover 状态切换为 `Plus` 图标。
- 插入按钮尺寸约 `16px`,手柄按钮约 `22px`,按钮之间保持小间距,避免插入按钮和拖拽手柄互相遮挡 pointer event。
- `hoverArea` 要比按钮视觉范围更大,避免鼠标从手柄移到插入按钮时控件闪烁。
- 手柄菜单打开时需要冻结 hover 状态,否则菜单会因鼠标离开块行而消失。
- 空段落的中心 `+` 点击应等同打开 slash 菜单,而不是直接创建第二个空段落。
- 截图、窗口失焦、页面隐藏时要重置 hover / frozen 状态,避免手柄卡住或消失。
- 思维导图、在线表格、附件这类高块或内部接管鼠标事件的块,需要允许常驻或更稳定的插入控件显示策略。
迁移到 `leptos-tiptap` 时,目标行为如下:
- hover 普通块:左侧只显示三段控件,上短横线 / 拖拽手柄 / 下短横线;不自动打开块菜单。
- hover 上短横线:短横线变 `+`tooltip 显示 `在上方插入块``Esc, a`
- hover 下短横线:短横线变 `+`tooltip 显示 `在下方插入块``Esc, b`
- 点击上方 `+`:在当前块前插入空段落,焦点进入新段落。
- 点击下方 `+`:在当前块后插入空段落,焦点进入新段落。
-`Esc` 进入块选择 / 块聚焦状态后,按 `a` 执行 before 插入,按 `b` 执行 after 插入。
- 点击拖拽手柄才打开块菜单;拖拽手柄本身不和上 / 下插入按钮抢 hover。
实现边界:
- `leptos-tiptap` 需要补齐 before / after 两个编辑器命令;不能继续只有 after HTML 插入。
- 第一阶段可以先在 runtime 内插入空段落并保存正文,但正式命令口径应回到 Page Aggregate / Rust editor block model,例如 `page.body.insertBlockBefore` / `page.body.insertBlockAfter` 或等价 `EditorCommand::InsertBlock { position: Before | After }`
- UI 层可以持有当前 hover block、active block、tooltip、menu open 这些临时状态,但不能把插入后的块 id 只存在前端临时结构里。
- 视觉上应优先复刻 Wolai:轻灰短横线、浅灰 hover 背景、黑色小 tooltip、淡红块 hover 底色;不要沿用 spike 中 32px、大圆角、重阴影、绿色 accent 的手柄样式。
首轮不追求:
- 完整协作光标。
- 完整评论系统。
- 完整表格编辑体验。
- 完整图片 / 附件上传链。
- 完整权限和发布链路。
- 完整全局搜索索引。
### 4.6 操作态菜单与浮层
真实 Wolai 的体验不能只看静态首屏,必须把操作后出现的浮层也纳入复刻合同。
#### 4.6.1 页面树 owner 态菜单
用户提供的登录态截图显示,页面树当前页 `Hermes` 的管理菜单为白色浮层,宽约 220px,圆角和阴影都很轻,菜单项按语义分组:
- 打开位置:`在右侧边栏打开`,带快捷键提示。
- 页面组织:`移动到...``嵌入到...`
- 引用复制:`复制访问链接``复制页面引用链接``复制页面ID`
- 页面副本:`拷贝副本`
- 危险或编辑动作:`重命名``删除`
这说明页面树行不只是导航节点,还承载页面操作入口。mnote 复刻时必须把这些动作映射到正式命令:
- `在右侧边栏打开`UI state + route / side panel state。
- `移动到...``tree.subtree.move`
- `嵌入到...``tree.node.embed` 或 Page Aggregate embed plan。
- `复制页面引用链接`:生成 page reference artifact,不应只复制 URL。
- `重命名``page.head.updateTitle`,并同步 tree projection。
- `删除`:必须走归档 / 删除确认链,不能前端直接移除节点。
首轮可以先复刻菜单壳、分组、hover、禁用态和快捷键提示;真实移动、嵌入、删除等高风险动作按后续命令合同逐步接入。
#### 4.6.2 搜索弹层
公开页搜索和帮助中心搜索都使用居中 modal overlay
- 背景整体变暗,侧栏和正文保持原位但失焦。
- 搜索框宽约 680px,高约 56px,白底,圆角轻,阴影明显但不厚重。
- 输入后出现选项行:`仅匹配标题``精确匹配``页面内搜索`
- 结果区显示匹配数量和快捷键说明:`Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开`
- 结果行左侧是页面 / 块图标,中间是标题与命中摘要,命中词使用红色高亮,右侧显示所在页面或空间。
mnote 需要把搜索拆成两层:
- 左侧栏 `快速筛选页面`:只过滤当前页面树,结果直接替换树列表。
- 顶栏搜索 modal:跨页面 / 页面内搜索入口,支持结果列表、快捷键提示、打开方式提示。
首轮可以用本地 projection / 当前页面块快照生成结果,不必先接全局索引;但视觉结构、选项行和打开方式提示应先复刻。
#### 4.6.3 published 态操作
真实 published 态存在几类弱操作入口:
- `演示模式`:点击后隐藏侧栏和顶栏,画布变成大字号演示页,只保留主题切换入口。
- `开始编辑`:未登录时弹出“登录以编辑”确认框,红色主按钮为继续,取消为弱按钮。
- `评论`:右侧滑出评论面板,顶部有 `未解决(0)` tab、筛选下拉、关闭按钮,空态居中。
- `Good Night`:右下角主题按钮直接切换暗色主题,暗色主题不重排页面,只替换 token。
mnote 首轮不必做完整发布权限,但应该保留这套状态分型:
- owner 编辑态。
- published 阅读态。
- presentation 演示态。
- comment side panel。
- light / dark token 切换。
这些状态大多是 UI state,但 `开始编辑` 是否可用、评论权限、发布状态必须来自页面权限或发布 projection,不应在前端硬猜。
### 4.7 帮助中心页的内在逻辑
`https://www.wolai.com/wolai/xhqeop8UHpVTMUSVmgz8nq` 不是普通内容页,它展示了 Wolai 页面系统的内在组织方式。
观察结论:
- 左侧页面树是帮助主题的完整目录,根页面下挂大量子页面,既是导航也是信息架构。
- 正文入口页不是自由排版海报,而是由页面链接、分组标题、四列目录块、子页面引用组成的“文档门户”。
- `快速上手 / 常见问题 / 视频教程 / 使用技巧短视频` 是高频入口。
- “让我们先掌握一些基础”下的四列目录把页面按能力分组:基础操作、基础块类型、进阶块类型、媒体与文件。
- 目录项本质上是页面引用 / 页面链接,不是普通文本。
- 搜索可以跨整个帮助中心返回页面和块级结果,命中词红色高亮。
- 搜索结果可以按快捷键在新窗口或右侧边栏打开,说明“右侧边栏打开”是 Wolai 页面导航模型的一等入口。
更重要的是,帮助中心中的“块引用”页直接定义了编辑器的语义模型:
- 行内块引用:在文字中间引用另一个块,底部有圆点虚线,不能直接编辑,点击跳转原始出处。
- 嵌入块引用:以独立块引用另一个块,左侧有圆点虚线,可以直接查看或部分编辑,但不能删除被引用块本身。
- 页面引用:当对象是页面时,菜单从“复制块引用链接”变为“复制页面引用链接”。
- 行内页面引用:在文本内显示页面标题。
- 嵌入页面引用:独立引用块显示页面图标和标题。
- 复制粘贴路径:块菜单复制引用链接,再粘贴生成引用。
- 快捷键路径:`esc` 选中块,`H` 复制行内块引用链接,`Q` 复制嵌入块引用链接。
- 嵌入到路径:`cmd/alt + shift + G` 打开“嵌入到”弹窗,搜索目标页面,把块嵌入到目标页面。
- 当前页面位置快速引用:编辑时输入 `[[` 搜索页面,添加页面行内引用。
- 右侧边栏拖拽引用:从右侧边栏拖拽产生引用。
- 引用预览和别名:悬浮行内块引用出现预览,小窗顶部可以设置别名,别名末尾显示箭头。
因此,mnote 不应把“引用”当成一个简单链接样式。它至少需要在 Page Aggregate / editor block model 中区分:
- `inline_page_reference`
- `inline_block_reference`
- `embedded_page_reference`
- `embedded_block_reference`
- `reference_alias`
- `reference_preview`
- `reference_source_block_id`
- `reference_target_page_id`
首轮实现可以降级,但语义模型不能降级成普通 URL。
---
## 5. 复刻范围分级
### 5.1 P0:先做像素基线
P0 只做最影响首屏观感的部分:
- 全局字体栈与基础字号行高收口。
- Sidebar 宽度、背景、顶部栏、树行高度、选中态、hover 态。
- 顶栏高度、breadcrumb、右侧动作按钮轻量化。
- 文档标题位置、字号、行高、正文列宽。
- 默认隐藏普通文档页中的额外元信息行和分割线。
- 右下角浮动入口减重。
-`1392 x 1213` 视口下对齐真实 Wolai 截图。
### 5.2 P1:补齐高频交互体验
P1 做用户会立刻感知的交互:
- 页面树展开 / 折叠动画和 hover action。
- 当前页祖先链展开和选中态保持。
- 搜索框视觉与本地过滤体验。
- 树行上下文菜单的视觉壳和核心动作入口。
- 顶栏搜索 modal 的选项行、结果行、命中高亮和快捷键提示。
- 评论侧栏、登录编辑确认框、演示模式的基础状态切换。
- 文档页进入编辑后,slash、浮动工具条、左侧手柄、上 / 下插入块、块菜单的基础可用体验。
- 页面标题编辑后,标题 / breadcrumb / sidebar / page tree / file tree 保持一致。
### 5.3 P2:接近 Wolai 的产品完整感
P2 做复杂但可逐步推进的部分:
- 拖拽排序与 drop feedback。
- page reference / block reference 的正式插入体验。
- `[[` 页面引用搜索、`嵌入到...` 弹窗、右侧边栏打开 / 拖拽引用。
- 引用预览和别名。
- 右侧评论、历史、关系图入口的真实面板。
- published / owner / edit 三态顶栏完整切换。
- 页面设置项与 editor runtime 的完整联动。
- 帮助中心类多列目录块的静态展示和后续编辑支持。
### 5.4 明确降级项
以下功能可以先只做视觉入口或最小实现:
- 评论协作。
- 权限分享。
- 发布与侵权投诉链路。
- 全局搜索索引。
- 完整演示模式。
- 完整夜间主题。
- 完整表格、图表、数据库。
- 完整文件上传和媒体块管理。
- 完整引用别名编辑器和跨页面引用预览缓存。
---
## 6. 单一真源边界
### 6.1 可以留在前端展示层的内容
以下内容允许作为前端展示或临时 UI state:
- 色彩、字号、行高、间距、圆角、阴影。
- Sidebar 宽度与布局。
- 树行 hover、focus、临时 selection、drag preview。
- 顶栏按钮排列与 hover。
- 文档画布宽度、标题留白、正文显示密度。
- slash 菜单开合状态。
- 浮动工具条开合状态。
- 块手柄 hover 状态。
- 右下角浮动入口显示状态。
### 6.2 必须回到 Rust / projection / Page Aggregate 的内容
以下内容不能只在前端复刻:
- 页面树真实层级。
- 排序真相。
- 当前页 active path。
- 拖拽是否合法。
- 新建、重命名、移动、归档、恢复结果。
- 页面标题与 breadcrumb / sidebar / file tree 的一致性。
- 正文保存与 conflict key。
- 页面设置对 editor runtime 的正式语义。
- page reference / block reference / embed 默认位置。
- 引用类型、引用源、引用目标、引用别名、引用预览数据。
- AI 写入标题、正文、页面设置的语义边界。
原则:
> **UI 可以复刻 Wolai,事实源不能复刻成第二份前端状态。**
### 6.3 命令口径
页面树动作继续沿 `tree.*`
- `tree.node.create`
- `tree.node.rename`
- `tree.subtree.move`
- `tree.node.archive`
- `tree.node.restore`
- `tree.node.embed`
页面动作继续沿 `page.*`
- `page.head.updateTitle`
- `page.layout.updateOptions`
- `page.body.save`
兼容 `documents.*` 可以继续存在,但不应作为新增体验的正式命令面。
---
## 7. 推荐实施路线
### Phase A:建立 Wolai 视觉 token
目标:
> **先让字体、字号、行高、颜色和基础密度可被统一复用。**
主要文件:
- `/mnt/Data1T/mnote/wolai-frontend/src/app/globals.css`
工作内容:
- 统一全局字体栈,避免 `Inter` 覆盖中文系统字体。
- 建立 Wolai-like token`--wolai-sidebar-bg``--wolai-active-fg``--wolai-active-bg``--wolai-text-primary``--wolai-text-secondary`
- 固定标题、正文、sidebar、topbar 的字号 / 行高。
- 用 token 替代散落硬编码颜色。
验收:
- `local-3000` 截图中标题、树行和顶栏文字宽度明显接近真实 Wolai。
- 中文字体不再出现 Inter 优先导致的字宽偏差。
### Phase B:复刻 Sidebar / 页面树首屏
目标:
> **让左侧页面树在密度、选中态、hover、滚动条、底部入口上接近 Wolai。**
主要文件:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/tree-shell-surface.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/tree-shell-host.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/tree-shell-dom-host.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/tree-projection.ts`
工作内容:
- 固定侧栏宽度和背景。
- 调整 workspace header 高度、头像尺寸、空间名截断方式。
- 调整快捷图标行尺寸与 hover。
- 调整页面树行高到 32px 附近。
- 当前页改为 Wolai 红字 + 淡红底。
- hover 态与 active 态同色系但层级更轻。
- 缩进只由层级和 icon slot 决定。
- 底部垃圾桶 / 模板中心与 Wolai 对齐,同时保留 mnote 自有入口时不破坏密度。
- `Explorer` 作为 mnote 创新保留,但视觉上降噪,避免抢过“我的页面”主路径。
验收:
- 对照用户提供的登录态截图,`Hermes` 选中行的颜色、行高、左侧缩进、滚动条位置接近 Wolai。
- 页面树长列表滚动时不出现行高抖动。
- 文件树创新入口仍可访问。
### Phase C:复刻顶栏与 breadcrumb
目标:
> **让顶栏在 owner / published 两种状态下都接近 Wolai 的轻量工具条。**
主要文件:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/app-layout-shell.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
工作内容:
- 固定顶栏高度约 40px。
- breadcrumb 文本、图标、分隔符弱化。
- 右侧按钮统一使用轻量 icon/text button。
- 区分 owner 态和 published 态的按钮组合。
- 公开状态 pill 对齐 Wolai 的“全网公开 / Public”视觉。
- 顶栏 hover 只加浅灰底,不使用重边框或大面积按钮。
验收:
- 登录态参考图中的顶栏图标序列能在 mnote 中找到对应视觉位置。
- published 公共页参考图中的“演示模式 / 搜索 / 开始编辑 / 评论 / 复制 / 开始使用 wolai”可先作为视觉分型,不要求一次接齐功能。
### Phase D:复刻文档画布与阅读态
目标:
> **让普通文档页首屏像 Wolai,而不是像调试页或二级详情页。**
主要文件:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-read-view.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-option-semantics.ts`
工作内容:
- 标题使用 `34px / 44px / 600`
- 普通正文列宽约 760px。
- 调整首屏 top padding,使标题起点接近 Wolai。
- 默认隐藏普通文档页的“个人空间 / 工作区首页”元信息行和横向分割线。
- 保留页面设置控制,但把“显示元信息 / 显示结构”这类调试或增强项作为显式选项,不默认露出。
- 阅读态任务块、段落、标题、列表、引用、代码块按 Wolai 行高和颜色重调。
- 右下角 AI / 帮助入口减重,避免绿色大按钮抢主编辑区视觉。
验收:
- `Hermes` 类普通文档页首屏只突出标题和正文块。
- 页面不再默认出现明显不像 Wolai 的横线、meta row 或 debug outline。
- 阅读态和编辑态在基础排版上不出现明显跳动。
### Phase E:复刻主编辑器高频块交互
目标:
> **让 `leptos-tiptap` 的输入体验从“能编辑”推进到“像 Wolai 的块编辑器”。**
主要文件:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-island-editor-host.tsx`
- `/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs`
- `/mnt/Data1T/mnote/rust/crates/bridge-runtime/`
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/`
工作内容:
- slash 菜单锚定 caret。
- selection 存在时显示浮动工具条。
- hover 块只显示左侧手柄区,上方和下方短横线在 hover 时变成插入 `+`
- 点击上方 / 下方插入控件分别创建 before / after 空段落,并把焦点移动到新段落。
- 支持 `Esc, a``Esc, b` 的块级插入快捷键。
- 点击手柄打开块菜单。
- 块菜单至少提供 `turn into / duplicate / delete` 的视觉与基础命令入口。
- `turn into` 不直接绕过 Rust block model。
- Tiptap `UniqueID` 只用于 runtime 辅助,最终块 id 对齐 Rust `EditorBlock.block_id`
- 页面引用 / 块引用可先做最小插入体验,复杂搜索和预览后置。
验收:
- 用户能在主编辑区通过 slash 插入常见块。
- 用户选中文本时看到 Wolai-like 浮动工具条。
- 用户 hover 块时不会被重工具条打扰,并能看到 Wolai-like 上 / 下插入短横线。
- 用户可以通过鼠标或 `Esc, a` / `Esc, b` 在当前块前后插入空段落。
- 块操作不会制造前端临时 id 作为持久化真相。
### Phase F:用 Page Aggregate 守住一致性
目标:
> **复刻体验时不牺牲单一真源。**
主要文件:
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-aggregate-loader.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-aggregate-builder.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-command-client.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/page/route.ts`
- `/mnt/Data1T/mnote/rust/crates/core-protocol/`
- `/mnt/Data1T/mnote/rust/crates/bridge-runtime/`
- `/mnt/Data1T/mnote/rust/crates/mnote-web/`
工作内容:
- 新增视觉状态时,先判断它是否只是 UI state。
- 新增页面字段时,必须归入 `page_identity / page_head / page_layout / page_body / page_tree`
- 标题变更必须继续让页头、breadcrumb、sidebar、page tree、file tree 同步。
- 正文保存继续走 `page.body.save`
- 页面设置继续按 `page-option-semantics` 判断是否进入 island runtime。
- 页面树动作继续走 `tree.*`,不新增 `documents.*` 长期动作。
验收:
- 页面重命名后,所有消费者保持一致。
- 切页、刷新、实时更新不会把 UI 压回旧标题。
- 视觉复刻代码里没有新增第二套树排序或页面标题真相。
### Phase G:截图回归与像素验收
目标:
> **把“像 Wolai”变成可复查、可操作、可重复的截图基线。**
建议新增或复用:
- `/mnt/Data1T/mnote/scripts/task*-smoke.js`
- `/mnt/Data1T/mnote/tmp/wolai-compare/`
验收视口:
- 桌面:`1392 x 1213`
- 宽屏:`1440 x 900`
- 窄屏:`390 x 844`
截图基线:
- 真实 Wolai 登录态长页面树。
- 真实 Wolai `Hermes` 普通文档页。
- 当前 mnote 同类页面。
- mnote 文件树创新入口展开态。
验收方式:
- 每次验收必须按 `wolai-aline` skill 形成差异矩阵,并附 Wolai 截图、本地截图、动作链、DOM/ARIA 线索和剩余差异。
- 浏览器取证必须由 subagent 执行;主线程必须复核截图,不能只接受 subagent 文本结论。
- published 阅读态、owner 登录态、editable-test mode 都只表示取证上下文,不再作为三套独立测试方案维护。
- 先用 Wolai 基线写出本地 RED smoke,再实现,再跑本地 smoke,再派 subagent 复测。
- 若截图肉眼可见差异但 smoke 未捕获,先增强 smoke 或 checklist 断言,再继续实现。
- 后续可加入截图 diff,但不能用截图 diff 替代真实交互烟测。
- 每次改 Sidebar、topbar、document canvas、editor runtime 后都要重新截首屏,并同步更新连续 checklist 的状态。
- 连续执行清单见 `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md`
owner 态交互验收清单:
- 自动化打开 `https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd` 后确认进入登录 owner 态,而不是 published 态。
- hover 页面树当前项 `Hermes`,确认 hover action 可见。
- 打开页面树菜单,截图菜单项和分组。
- hover 正文块 `事实上`,确认块背景、左侧手柄、上短横线、下短横线可见。
- hover 上短横线,确认 tooltip 为 `在上方插入块` / `Esc, a`
- hover 下短横线,确认 tooltip 为 `在下方插入块` / `Esc, b`
- 打开块菜单,截图 `AI 助理 / 转换为 / 拷贝副本 / 删除 / 复制链接 / 移动/嵌入到... / 块历史... / 评论 / 颜色 / 文字居中 / 文字翻译 / 生成海报` 等入口。
-`3000` 执行同类操作并保存同视口截图。
- 验收报告中列出 Wolai 截图、本地截图、差异结论和不可测项。
---
## 8. 验证命令建议
前端运行:
```bash
cd /mnt/Data1T/mnote/wolai-frontend && pnpm dev
```
前端相关测试:
```bash
cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/components/sidebar/tree-shell-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx src/components/editor/document-content.test.ts src/components/editor/leptos-tiptap-island-editor-host.test.tsx
```
projection / aggregate 测试:
```bash
cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/tree-projection.test.ts src/lib/tree-projection-contract.test.ts src/lib/documents/page-aggregate-builder.test.ts src/lib/documents/tree-command-client.test.ts
```
Rust 侧测试:
```bash
cd /mnt/Data1T/mnote && cargo test -p mnote-web workspace_shell
cd /mnt/Data1T/mnote && cargo test -p bridge-runtime
```
浏览器验收:
- 先按 `wolai-aline` skill 派 subagent 打开真实 Wolai 参考页和 `http://127.0.0.1:3000/`
- 统一视口至少覆盖 `1392 x 1213`;涉及响应式时补 `1440 x 900``390 x 844`
- 每个小任务都要保存 Wolai 与本地截图,并记录入口、关闭路径、URL 变化、控件形态、控件状态、快捷键、hover/active、DOM/ARIA 线索。
- 主线程复核截图后,把结论写入差异矩阵;发现差异先补 smoke,再改实现。
- 对照范围至少包含左侧栏宽度、树行高度、选中态、正文标题位置、顶栏按钮密度、搜索 modal、块 hover/插入入口、右下角浮动入口。
---
## 9. 禁止事项
为避免这条线跑偏,后续实现中禁止:
- 不把 `BlockNote` 写回默认主编辑器。
- 不把 Next App Router 恢复成 `3000` 主入口。
- 不在 UI 层重新拼第二份页面树、排序、标题或正文真相。
- 不把 compat route 或 debug shell 当正式主链。
- 不为了像素复刻绕开 `tree.*` / `page.*` 命令族。
- 不把 Tiptap runtime 临时 id 倒灌成 Rust 持久化块 id。
- 不把页面设置做成“保存了字段但 editor runtime 不消费”的假功能。
- 不为了复刻 Wolai 顶栏而提前接入真实权限 / 分享 / 评论复杂链路。
---
## 10. 当前退出标准
这份方案只有在下面条件同时满足后,才能移入 `done`
- `3000` 首屏在左侧页面树、顶栏、文档画布三块视觉上接近真实 Wolai。
- 页面树保留 mnote 文件树创新,但不破坏 Wolai 主路径密度。
- 普通文档页默认不再显得像调试页或二级详情页。
- `leptos-tiptap` 主编辑器具备 slash、浮动工具条、左侧手柄、上 / 下插入块、块菜单的基础 Wolai-like 行为。
- 标题 / 正文 / 页面设置 / 页面树仍服从 Page Aggregate 与 tree-first graph kernel 主线。
- 相关 smoke / 单测 / 截图验收有记录。
- 连续 checklist 中 P0/P1 任务均有 Wolai 基线、本地 RED/GREEN smoke、subagent 复测截图和主线程复核结论。
当前结论:
> **下一步应先做 P0 像素基线和 Sidebar / 文档画布首屏对齐,再推进编辑器块级交互。功能复杂项可以降级,但页面树和主编辑器的视觉节奏必须优先向真实 Wolai 收口。**
@@ -0,0 +1,41 @@
# 5-8 [process] Wolai 编辑态自动化测试方案 v1(已收口)
> 更新时间:2026-04-30
>
> 状态:本文件原有的工具筛选、只读边界、沙盒页要求和脚本草案已经被统一的 `wolai-aline` skill 与 `08-wolai-aline-test-flow` 流程取代。后续不要再按本文旧版测试方案执行。
## 当前唯一口径
后续所有 Wolai 对标、复刻、aline/alignment、编辑器状态自动化测试任务,统一使用:
- Codex skill`/home/lix/.codex/skills/wolai-aline`
- 项目流程:`/mnt/Data1T/mnote/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md`
- 项目规则:`/mnt/Data1T/mnote/AGENTS.md``Wolai-aline 对标流程`
## 取代的旧结论
以下旧结论不再作为执行依据:
- “Hermes 页面只能只读,写入必须另找沙盒页”。
- “published 态脚本可以代表主编辑器完成验收”。
- “subagent 文本结论或文案断言足以证明对齐”。
- “先实现再由用户肉眼指出差异”。
- 本文旧版 `wolai-reference-*` 脚本草案、分阶段 checklist、工具通道优先级。
## 现行规则摘要
- 当前 Hermes 测试页 `https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd` 已授权用于 Wolai-aline 编辑器对标中的最小编辑验证。
- 编辑验证必须记录前后截图、动作链、输入内容和清理状态。
- 即使在 Hermes 测试页,也禁止未经确认地删除、移动、归档、发布、评论、改权限或批量修改既有非测试内容。
- 其他 Wolai 页面仍默认只读;写入仍需用户提供沙盒页 URL 和明确授权。
- 浏览器对标测试必须使用 subagent 执行;主线程必须复核截图并形成差异矩阵。
- 发现差异后先补本地 smoke,让差异可复现失败,再实现修复。
## 后续维护
若后续 Wolai-aline 任务发现新的稳定失败模式,应更新:
- `/home/lix/.codex/skills/wolai-aline/references/failure-patterns.md`
- 必要时同步更新 `/mnt/Data1T/mnote/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md`
不要继续扩写本文作为新测试方案。
@@ -0,0 +1,273 @@
# 5-9 [process] Wolai-aline 连续执行 checklist v1
> 更新时间:2026-04-30
>
> 本清单拆自 `5-7-wolai-page-tree-main-editor-experience-restoration-v1.md`。它不是新的测试方案;执行口径统一服从 `/home/lix/.codex/skills/wolai-aline` 与 `/mnt/Data1T/mnote/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md`。
## 1. 使用方式
每次只领取一个最小可对标任务,按下面固定闭环推进:
1. 用一句话定义行为,例如“侧栏搜索入口打开全局搜索 modal 且 URL 不变”。
2. 派 subagent 对 Wolai Hermes 取基线,必要时进入 Hermes editable-test mode。
3. 主线程查看 Wolai 截图,填写差异矩阵,不接受只有文字的结论。
4. 写或更新本地 `scripts/task*-smoke.js`,先让当前 `3000` 暴露差异。
5. 小范围实现,不新增第二套页面树、标题、正文或排序真相。
6. 运行本地 smoke 和改动范围内单测。
7. 再派 subagent 同链路复测 Wolai 与 `3000`
8. 主线程复核最终截图;若肉眼仍有差异,先补 smoke 或 checklist 断言,再继续修。
9. 更新本清单状态和截图路径。
状态标记:`TODO` 未开始,`BASELINE` 已取 Wolai 基线,`RED` 已有本地失败 smoke`GREEN` 本地实现和 smoke 通过,`PARITY` subagent 复测与主线程截图复核通过,`BLOCKED` 有阻塞。
## 2. 当前基线证据
2026-04-30 已由 subagent 使用 Playwright 同时操作 Wolai Hermes 与本地 `3000`,主线程已复核关键截图。后续任务可沿用这些基线,但每个具体小任务仍需重新截同动作链证据。
| 项 | Wolai 证据 | 本地证据 | 当前结论 |
| --- | --- | --- | --- |
| 首屏 | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/doc-baseline-mini/wolai-initial.png` | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/doc-baseline-mini/local-initial.png` | 本地首屏仍是聚合/预览壳,需点击 `打开当前页面` 才进入正文,和 Wolai 直接正文页不一致 |
| 搜索 modal | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/wolai-03-search-modal-controls-filled.png` | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/local-03-search-modal-controls-filled.png` | 容器接近;结果态、数据、入口矩阵仍需拆开验;switch 状态必须截图 + `aria-checked` 双证据 |
| 块 hover | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/wolai-08-block-hover-insert-entry.png` | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/local-14-block-hover-insert-entry.png` | 本地已有 `+` 与手柄入口,但位置、密度、页面状态不同,不可判定已对齐 |
| 编辑清理 | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/wolai-14-title-suffix-after.png` | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/local-16-editor-after-undo-cleanup.png` | Wolai 标题和正文都可编辑,editable-test 前必须先确认焦点 block 类型 |
当前必须优先拆解的差异:
1. 普通文档首屏入口语义:Wolai 直接显示正文页,本地仍需点击 `打开当前页面`
2. 搜索入口矩阵:Wolai 左侧搜索可打开,`Ctrl+K` 本轮未稳定;本地顶栏搜索可打开,侧栏入口和快捷键要继续按 smoke 验证。
3. 搜索结果数据:Wolai 对 `Hermes` 有真实结果,本地依赖当前工作区示例数据;后续需要固定 fixture 或种子。
4. 编辑器测试焦点:写入前必须确认当前焦点在正文测试块,不在标题或 breadcrumb 渲染副本。
### 2.1 task129 Phase A 执行记录
2026-04-30 已新增并执行 `scripts/task129-wolai-aline-baseline-smoke.js`,用于固化 Phase A 的只读基线取证入口。该脚本不编辑 Wolai,只做双端首屏取证、DOM 摘要和差异矩阵输出。
主线程验证:
- 命令:`WOLAI_ALINE_RUN_ID=main-20260430-122309 node scripts/task129-wolai-aline-baseline-smoke.js`
- 结果:通过,`ok: true`,确认 `simultaneous: true`
- 矩阵:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/main-20260430-122309/comparison-matrix.md`
- Wolai 截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/main-20260430-122309/wolai-1392x1213-first-screen.png`
- 本地截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/main-20260430-122309/local-1392x1213-first-screen.png`
主线程多视口验证:
- 命令:`WOLAI_ALINE_RUN_ID=main-viewports-20260430-122418 WOLAI_ALINE_VIEWPORTS=1392x1213,1440x900,390x844 node scripts/task129-wolai-aline-baseline-smoke.js`
- 结果:通过,生成 `1392x1213``1440x900``390x844` 三组 Wolai / 本地截图
- 矩阵:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/main-viewports-20260430-122418/comparison-matrix.md`
subagent 复测:
- 命令:`WOLAI_ALINE_RUN_ID=subagent-20260430-122359 node scripts/task129-wolai-aline-baseline-smoke.js`
- 结果:通过,`ok: true`,确认 `simultaneous: true`,本次不需要 `xvfb-run`
- 矩阵:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/subagent-20260430-122359/comparison-matrix.md`
- Wolai 截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/subagent-20260430-122359/wolai-1392x1213-first-screen.png`
- 本地截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/subagent-20260430-122359/local-1392x1213-first-screen.png`
主线程截图复核结论:
- Wolai Hermes 为 owner 登录态,直接显示正文页和 `全网公开` 状态。
- 本地 `3000` 工作区可见,但首屏仍停留在聚合 / 预览入口,需要点击 `打开当前页面` 才进入正文。
- `task129` 只证明取证工具链可用,不证明 UI 已对齐;后续首个实现任务应优先处理 `D9 普通文档首屏入口语义`
### 2.2 task130 D9 执行记录
2026-04-30 已新增并执行 `scripts/task130-wolai-aline-root-document-entry-smoke.js`,用于固化 `D9 普通文档首屏入口语义`。该脚本启动临时 `mnote-web` 端口验证新代码路径,不修改现有 `3000` 进程。
RED
- 初次运行失败:`AssertionError: 根页不应停在需要点击“打开当前页面”的聚合入口`
- 失败原因:Rust Web root route 在有 active page 时只渲染 workspace entry 和 `打开当前页面` 链接,没有内嵌 Page Aggregate、editor bootstrap 和 `leptos-tiptap` island。
GREEN
- 命令:`node scripts/task130-wolai-aline-root-document-entry-smoke.js`
- 结果:通过
- 本地截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task130-root-document-entry/local-root-document-entry.png`
- smoke 断言:根页不再出现 `打开当前页面`;存在 `document-shell[data-editor-host="leptos_tiptap_island"]`;存在 `__MNOTE_PAGE_AGGREGATE__``__MNOTE_EDITOR_BOOTSTRAP__`;存在 `.ProseMirror[contenteditable="true"]`
实现摘要:
- root route 在 active page 可加载 Page Aggregate 时,直接复用 `DocumentPage`、Page Aggregate snapshot、editor bootstrap、title controller 和 island adapter。
- 若 active page 不能加载 Page Aggregate,回退旧 workspace entry,避免 recent cookie 指向失效页面时 root 直接 400。
- 现有 `3000` 进程仍是旧实例;需要重启 `mnote-web` 后,`3000` 才会体现该改动。
验证:
- `cargo fmt -p mnote-web --check`
- `cargo check -p mnote-web`
- `cargo test -p mnote-web root_entry -- --nocapture`
- `node scripts/task130-wolai-aline-root-document-entry-smoke.js`
最终对标复核:
- subagent 命令:`node scripts/task130-wolai-aline-root-document-entry-smoke.js`
- 结果:通过
- Wolai 基线:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/main-20260430-122309/wolai-1392x1213-first-screen.png`
- 本地截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task130-root-document-entry/local-root-document-entry.png`
- 结论:D9 目标行为一致;剩余侧栏数据、图标细节、具体字号属于后续 Sidebar / visual token checklist。
## 3. 通用证据矩阵
每个 checklist 项都必须保留以下字段:
| 字段 | 要求 |
| --- | --- |
| Wolai 截图 | 绝对路径,保存在 `/mnt/Data1T/mnote/tmp/wolai-editor-parity/<task>/` |
| 本地截图 | 同视口、同动作链截图 |
| 入口 | 按钮、快捷键、菜单或 hover 区域 |
| 关闭路径 | Esc、再次快捷键、遮罩、关闭按钮、入口 toggle |
| URL | 操作前后是否变化 |
| 控件类型 | switch、button、dropdown、menu、input、panel、modal |
| 控件状态 | checked、selected、disabled、focused、hover、active |
| 视觉形态 | 尺寸、位置、间距、阴影、颜色、图标、遮罩 |
| DOM/ARIA | role、aria、data-testid、可聚焦性 |
| smoke | 对应 `scripts/task*-smoke.js` 或测试文件 |
| 剩余差异 | 不允许空泛写“基本一致” |
## 4. Phase A:基线和全局 token
| ID | 状态 | 任务 | 验收要点 |
| --- | --- | --- | --- |
| A1 | GREEN | 固定基线视口与截图目录 | `task129` 已支持 `WOLAI_ALINE_VIEWPORTS`;主线程已生成 `1392x1213``1440x900``390x844` 三组截图;subagent 已复测默认视口 |
| A2 | PARITY | Wolai owner 首屏基线 | `task129` 主线程与 subagent 均确认 Wolai owner 登录态,截图见 2.1 执行记录 |
| A3 | PARITY | 本地 `3000` 首屏基线 | `task129` 主线程与 subagent 均确认本地 3000 可打开,截图见 2.1 执行记录 |
| A4 | TODO | 字体栈与基础 token | smoke 或 DOM 断言全局字体、标题字号、正文行高、sidebar 行高;截图确认中文字宽接近 Wolai |
| A5 | PARITY | 截图差异表模板落地 | `task129` 自动输出 `comparison-matrix.md`,主线程已复核截图,subagent 已独立复测 |
| A6 | PARITY | 同时操作工具链固化 | `task129` 固定 `Playwright + Chrome persistent profile`;主线程与 subagent 均确认可同时操作两个目标,本次 subagent 不需要 `xvfb-run` |
## 5. Phase BSidebar / 页面树
| ID | 状态 | 任务 | 验收要点 |
| --- | --- | --- | --- |
| B1 | PARITY | 侧栏宽度与背景 | `task131` 覆盖宽 `248px`、背景 `rgb(245,245,245)`、无右侧 inset、topbar.x=sidebar.rightsubagent post-fix2 复核通过 |
| B2 | PARITY | workspace header | `task132` 覆盖 header `40px`、avatar `24x24`、字号 `16px`;Wolai DOM 锚点弱,主线程与 subagent 以截图复核通过 |
| B3 | PARITY | 快捷入口行 | `task132` 覆盖 6 个快捷入口、30px 容器、约 10.4px 间距;截图复核通过 |
| B4 | PARITY | 页面树行密度 | `task132` 覆盖行高 `32px`、字号 `16px`、无额外行 margin;截图复核通过 |
| B5 | PARITY | 当前页选中态 | `task132` 覆盖淡红底 `rgba(255,71,71,.1)`、红字、`aria-current=page` / `aria-selected=true`;截图复核通过 |
| B6 | PARITY | 层级缩进 | `task132` 使用父子页面 fixture 覆盖行 x 不变、一级子行内部缩进 `20px`;服务端和运行时均补齐 depth 语义 |
| B7 | PARITY | 页面树 hover action | `task132` 覆盖 hover 仅出现“更多操作 / 新建子页面”两个 24px actionsubagent post-fix2 复核通过 |
| B8 | PARITY | 页面树上下文菜单 | `task132` 覆盖宽约 `220px`、9 项命令、分组间距和 `Alt+` / `Del` 提示;高风险删除仍经确认 |
| B9 | PARITY | 底部垃圾桶/模板中心 | `task132` 覆盖 footer `44px`、无顶部分隔线、垃圾箱/模板中心固定底部;截图复核通过 |
| B10 | PARITY | 文件树创新入口 | 保留 Explorer tab`task132` 复核主路径仍是“我的页面”,footer 与行高体系不被破坏;与 Wolai 无 Explorer 的差异按本地创新入口保留 |
| B11 | PARITY | 页面树图标与展开箭头 | `task133` 覆盖 Wolai 每行不同私有 glyph、本地无真实 icon 数据时不得用统一房子图标,也不保留空 icon slot;展开箭头使用 20x20 SVG chevron |
### 5.1 task131 / task132 / task133 Phase B 执行记录
2026-04-30 已完成 Phase BSidebar / 页面树。执行口径按 `wolai-aline` skill:先由 subagent 取 Wolai 基线,主线程查看截图并写 RED smoke,再最小实现,最后本地与 subagent 复测。用户复核指出 `task132` 漏掉页面树图标/箭头差异后,补充 `task133` 将该类肉眼差异固化为 smoke。
RED / GREEN
- `scripts/task131-wolai-aline-sidebar-b1-smoke.js`:固化 B1,初始失败于本地侧栏 `240px`GREEN 后覆盖 `248px``rgb(245,245,245)`、无 right inset、topbar 贴合。
- `scripts/task132-wolai-aline-sidebar-phase-b-smoke.js`:固化 B2-B10,使用父子页面 fixture 覆盖 header、快捷入口、行密度、选中态、层级缩进、hover action、上下文菜单、footer。
- `scripts/task133-wolai-aline-sidebar-tree-icons-arrows-smoke.js`:固化 B11,RED 失败于页面树统一房子 glyph;二次 GREEN 后断言 page 模式不渲染通用房子/伪 icon、不保留空 icon 槽,标题贴近箭头/占位,toggle 使用 20x20 SVG chevron。
实现摘要:
- `rust/crates/mnote-web/src/ssr/styles.rs`:侧栏宽度/背景、header、quick actions、tree row、选中态、context menu、footer 的 Wolai 对齐样式;page 模式无真实 icon 数据时不显示统一房子 glyph,也不保留空 icon 槽。
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`:页面树运行时渲染补 `aria-current` / `aria-selected`hover action 收敛为“更多/新建”,上下文菜单命令与 Wolai 对齐,运行时递归 depthpage tree 展开箭头改为 20x20 SVG chevronpage 行不渲染空 icon 节点。
- `rust/crates/mnote-web/src/tree_shell/page_renderer.rs` / `routes/tree.rs`:SSR 页面树补父子层级 depth / aria-level 语义,兼容 `parentId``parentNodeId`SSR page tree 展开箭头同样使用 20x20 SVG chevronpage 行不渲染空 icon 节点。
验证命令:
- `cargo fmt -p mnote-web --check`
- `cargo check -p mnote-web`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task131-wolai-aline-sidebar-b1-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task132-wolai-aline-sidebar-phase-b-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task133-wolai-aline-sidebar-tree-icons-arrows-smoke.js`
- `node scripts/task119-rust-web-wolai-visual-regression-smoke.js`
证据路径:
- B1 Wolai 基线:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task131-sidebar-b1-baseline/wolai-sidebar-b1-open-1392x1213.png`
- B1 本地最终:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task131-sidebar-b1-postfix/local-sidebar-b1-postfix-1392x1213.png`
- Phase B Wolai 基线矩阵:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task132-sidebar-phase-b-baseline/comparison-matrix.md`
- Phase B 本地主线程最终:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task132-sidebar-phase-b/local-sidebar-phase-b-1392x1213.png`
- Phase B 本地菜单最终:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task132-sidebar-phase-b/local-sidebar-phase-b-menu-1392x1213.png`
- Phase B subagent post-fix2 JSON`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task132-sidebar-phase-b-postfix2/sidebar-phase-b-postfix2-measurements.json`
- Phase B subagent Wolai 菜单:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task132-sidebar-phase-b-postfix2/wolai-sidebar-context-menu-1392x1213.png`
- Phase B subagent 本地菜单:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task132-sidebar-phase-b-postfix2/local-sidebar-context-menu-1392x1213.png`
- B11 用户指出差异截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task132-sidebar-phase-b-baseline/wolai-03-page-tree.png` / `/mnt/Data1T/mnote/tmp/wolai-editor-parity/task132-sidebar-phase-b-baseline/local-03-page-tree.png`
- B11 subagent 基线矩阵:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task133-sidebar-tree-icons-arrows-baseline/comparison-matrix.md`
- B11 subagent Wolai / 本地复核截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task133-sidebar-tree-icons-arrows-baseline/wolai-sidebar-tree-icons-arrows.png` / `/mnt/Data1T/mnote/tmp/wolai-editor-parity/task133-sidebar-tree-icons-arrows-baseline/local-sidebar-tree-icons-arrows.png`
- B11 本地主线程最终截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task133-sidebar-tree-icons-arrows/local-sidebar-tree-icons-arrows-1392x1213.png`
- B11 二次复核矩阵:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task134-sidebar-arrow-no-empty-slot-baseline/comparison-matrix.md`
- B11 二次复核截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task134-sidebar-arrow-no-empty-slot-baseline/wolai-sidebar-tree.png` / `/mnt/Data1T/mnote/tmp/wolai-editor-parity/task134-sidebar-arrow-no-empty-slot-baseline/local-sidebar-tree.png`
主线程复核结论:
- B1-B11 可量测项已对齐或按 checklist 明确保留本地差异。
- Wolai header/avatar DOM 选择器不稳定,最终以截图 + 本地稳定锚点复核。
- `Explorer` 是 mnote 本地创新入口,按 B10 保留;视觉不抢“我的页面”主路径。
- 菜单位置因页面树数据不同而不同;尺寸、项目、分组高度已对齐。
- Wolai 页面树图标是每行不同的私有 glyph;当前本地 projection 没有真实 per-page icon 数据,因此不硬编码类似图标,也不保留空白 icon 列。二次复核确认:保留空列会让标题 x 更接近 Wolai,但视觉上是明显缺失;当前按用户确认的产品判断去空列,残留差异是标题比 Wolai 真实图标+标题组合更靠左。后续若树投影提供真实 icon 字段,再按行渲染。
## 6. Phase C:顶栏、breadcrumb、搜索
| ID | 状态 | 任务 | 验收要点 |
| --- | --- | --- | --- |
| C1 | TODO | 顶栏高度与按钮密度 | 顶栏约 40px;按钮轻量 hover;右侧 icon/text 序列不重 |
| C2 | TODO | breadcrumb | 图标、分隔符、弱色文字、截断方式对齐;切页后与页面树同步 |
| C3 | TODO | owner/published 按钮分型 | owner 与 published 的按钮组合分开;不把 published 文案硬塞 owner 态 |
| C4 | TODO | 顶栏搜索入口 | 点击打开全局搜索 modal,URL 不变,焦点进入输入框 |
| C5 | TODO | 侧栏搜索入口 | 打开同一个全局搜索 modal 或明确的树过滤入口;不得误跳 `/search` |
| C6 | TODO | 搜索 modal 默认状态 | `仅匹配标题``页面内搜索` 为 switch 且默认开启;不是普通 pill button |
| C7 | TODO | 搜索 modal 关闭路径 | Esc、遮罩、再次 `Ctrl+P`、入口 toggle 按 Wolai 实测对齐 |
| C8 | TODO | 搜索结果行 | 匹配数量、命中高亮、快捷键提示、结果 icon、所在页面信息对齐 |
| C9 | TODO | 搜索空态/加载态 | 输入无结果和加载时的视觉与 DOM 状态可被 smoke 捕获 |
| C10 | TODO | 搜索入口/快捷键矩阵 | 分别验证 Wolai 左侧搜索、顶栏/侧栏入口、`Ctrl+P``Ctrl+K`;每个入口记录 URL、打开/关闭路径和阻塞 |
| C11 | TODO | 搜索结果 fixture | 固定本地 `Hermes` 或等价种子数据,避免用随机工作区数据对比 Wolai 结果态 |
## 7. Phase D:文档画布与阅读态
| ID | 状态 | 任务 | 验收要点 |
| --- | --- | --- | --- |
| D1 | TODO | 主内容列宽 | 普通文档列宽约 760px;大屏不贴左;移动端不溢出 |
| D2 | TODO | 标题位置与样式 | Hermes 标题 y 位置、`34px / 44px / 600`、颜色接近 Wolai |
| D3 | TODO | 默认隐藏 meta/debug 行 | 普通文档默认不显示不像 Wolai 的工作区 meta、横线、debug outline |
| D4 | TODO | 正文段落 | `16px / 24px`、段间距、弱色文本与 Wolai 对齐 |
| D5 | TODO | 任务块 | checkbox 尺寸、边框、文本 baseline、完成态接近 Wolai |
| D6 | TODO | 引用/列表/标题块 | 阅读态块样式不跳出 Wolai 密度;截图覆盖至少三类块 |
| D7 | TODO | 右下角浮动入口 | AI / 帮助入口减重,尺寸和位置接近 Wolai,不遮挡正文 |
| D8 | TODO | 阅读态和编辑态切换 | 切换时标题、正文列宽、scroll 位置不明显跳动 |
| D9 | PARITY | 普通文档首屏入口语义 | `task130` 已覆盖 RED/GREENsubagent 最终对标复核通过:根页不再停在“打开当前页面”入口,直接出现 document shell + editor island |
## 8. Phase E:主编辑器块交互
| ID | 状态 | 任务 | 验收要点 |
| --- | --- | --- | --- |
| E0 | TODO | editable-test 焦点护栏 | 写入前先断言焦点在正文测试块,不在标题、breadcrumb 或隐藏输入;截图记录焦点位置 |
| E1 | TODO | editable-test 基础输入 | Hermes editable-test mode 取 Wolai 输入/回车/撤销基线;本地保存后刷新保持 |
| E2 | TODO | 块 hover 手柄 | hover 块只显示左侧手柄区,不自动弹菜单;浅色块 hover 背景对齐 |
| E3 | TODO | 上方插入短横线 | hover 短横线变 `+`tooltip `在上方插入块` / `Esc, a`;点击前插入 |
| E4 | TODO | 下方插入短横线 | hover 短横线变 `+`tooltip `在下方插入块` / `Esc, b`;点击后插入 |
| E5 | TODO | `Esc, a` / `Esc, b` | 块选中态快捷键插入 before/after;本地 smoke 验证焦点进入新块 |
| E6 | TODO | 块菜单 | 点击手柄打开菜单;`转换为 / 拷贝副本 / 删除 / 复制链接 / 颜色` 等入口视觉对齐 |
| E7 | TODO | slash 菜单 | 输入 `/` 跟随 caret 打开;搜索过滤和键盘选择可用 |
| E8 | TODO | selection toolbar | 选中文本出现工具条;粗体、斜体、链接、颜色、转换块入口可见 |
| E9 | TODO | `Ctrl+A` 双阶段选择 | 第一次选当前文本/块,第二次选全部块;行为与 Wolai 基线一致 |
| E10 | TODO | 拖拽预览 | 块拖动时横向 drop line、分栏竖线预览;持久化可后置但预览需对齐 |
## 9. Phase FPage Aggregate 和命令边界
| ID | 状态 | 任务 | 验收要点 |
| --- | --- | --- | --- |
| F1 | TODO | 标题单一真源 | 标题修改后 breadcrumb、sidebar、file tree、document title 同步 |
| F2 | TODO | 正文保存命令 | 编辑器保存继续走 `page.body.save`,不把 Tiptap 临时 id 当持久真相 |
| F3 | TODO | 页面设置语义 | 设置项进入 `page-option-semantics` 和 editor runtime,不做只保存不消费的假功能 |
| F4 | TODO | 页面树命令 | 新建、重命名、移动、归档仍沿 `tree.*`;菜单入口不直接操作本地临时数组 |
| F5 | TODO | 引用语义 | 页面引用、块引用、嵌入引用先保留类型边界,不降级成普通 URL |
## 10. 每轮收尾
每完成一个 ID,必须补齐:
- Wolai 截图路径:`TODO`
- 本地截图路径:`TODO`
- smoke / 测试命令:`TODO`
- 主线程复核结论:`TODO`
- 剩余差异:`TODO`
如果出现新的可复用失败模式,同步更新 `/home/lix/.codex/skills/wolai-aline/references/failure-patterns.md`
@@ -0,0 +1,934 @@
# 6 [process] Mindmap Kernel Phase 6:降级为 Projection / Editor v1
> 更新时间:2026-04-22
>
> 当前主线依据:
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
>
> 历史参考:
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/ai-first-rust-block-editor-baseline-v1.md`
## 1. 文档目的
这份文档用于重写 `Kernel Phase 6` 的落地口径。
这里仍沿用 `Phase 6` 命名,是为了保留与旧阶段拆分的一致性;当前执行依据应以 `design/01-05-current-priority-overview.md` 和相关现行主线文档为准,不再以 `1-1-tree-first-graph-kernel-checklist-v2.md` 作为唯一推进入口。
这里回答的不是:
- “要不要马上把导图 UI 全量重写成 Rust”
而是:
> **在当前仓库代码现实下,Mindmap 如何从独立对象中心,降级成 `tree-first graph kernel` 的一种 projection / editor。**
这份文档必须同时满足三件事:
- 服从 `tree-first graph kernel` 主线
- 对齐当前仓库里的真实实现,而不是抽象设想
- 给出可迁移、可双写、可分阶段切流的方案
---
## 2. 先给结论
结论固定如下:
> **Mindmap 后续必须进行 Rust 内核化重构,但重构重点不是先替换 `simple-mind-map` 画布,而是先把“导图真相、导图命令、导图 projection、AI 工具入口”切到 Rust kernel。**
换句话说:
- **必须 Rust 化的部分**
- 导图事实源
- 导图命令层
- 导图 projection 层
- AI / CLI 直接调用的导图工具层
- **不必第一阶段 Rust 化的部分**
- 具体画布渲染器
- 工具栏、缩略图、拖拽动画、局部 DOM 细节
因此:
> **Phase 6 的正确目标不是“把导图换个前端库”,而是“让导图不再以 `simple-mind-map` JSON 为系统真相”。**
---
## 3. 当前代码现实
当前导图主链已经部分接入 Rust runtime,但整体仍然是“前端重交互壳 + blob 持久化 + Rust compat 工具”的形态,还不是 kernel truth。
### 3.1 前端主壳仍然由 `simple-mind-map` 驱动
当前主组件:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx`
现状特征:
- 直接加载 `simple-mind-map` 与大量插件
- 同时承担内嵌块、独立页、全屏态、工具栏、缩略图、右键菜单、图片处理、导入导出
- 直接调用:
- `mindmap.setData(...)`
- `mindmap.getData(...)`
- `mindmap.execCommand(...)`
- 直接请求:
- `fetch(/api/mindmap/${docId}/${mindmapId})`
这说明当前导图编辑器仍然以画布库数据结构为第一现场。
### 3.2 当前所谓 projection 仍然是前端摘要层,不是 kernel projection
当前文件:
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/mindmap/mindmap-projection.ts`
当前 `buildMindmapProjection(...)` 的本质是:
- 接受一份 raw mindmap blob
-`simple-mind-map` 兼容归一化
- 输出一个前端摘要对象
- 同时保留 `data` 原始树
这意味着当前 projection 还是:
- **blob 的摘要**
而不是:
- **kernel subtree / graph 的正式投影**
### 3.3 当前持久化仍然是整棵导图 blob
当前持久化主链:
- `/mnt/Data1T/mnote/wolai-frontend/convex/mindmaps.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts`
现状特征:
- `mindmaps.get` 返回整棵 `data`
- `mindmaps.put` 写回整棵 `data`
- `mindmaps` 表持有的是一份完整导图 JSON
- 独立页服务端入口仍然走:
- `mindmaps.get`
- 然后在前端侧 `buildMindmapProjection(...)`
这说明当前“导图真相”依然是:
- **可直接存取的一整棵导图 blob**
而不是:
- **kernel node / edge / subtree**
### 3.4 Rust runtime 已经介入,但仍是 compat mindmap 树语义
当前 Rust 侧相关实现:
- `/mnt/Data1T/mnote/rust/crates/core-protocol/src/mindmap.rs`
- `/mnt/Data1T/mnote/rust/crates/bridge-runtime/src/lib.rs`
已存在的事实:
- Rust 侧已经有:
- `MindmapTreeNode`
- `MindmapOp`
- `mindmap_get`
- `mindmap_get_subtree`
- `mindmap_put`
- `mindmap_apply_ops`
- `mindmap_outline_to_mindmap`
- `bridge-runtime` 已能在 Rust 内部应用 `MindmapOp`
但这些能力当前本质上仍然是:
- 对一棵 `MindmapTreeNode` JSON 树做读取、修改、返回
不是:
-`KernelNode` / `KernelEdge` / `KernelSubtree` 做正式操作
### 3.5 Kernel 协议已具备导图进入统一内核的入口
当前 kernel 类型:
- `/mnt/Data1T/mnote/rust/crates/core-protocol/src/kernel.rs`
已经有:
- `KernelNodeType::Mindmap`
- `KernelNodeType::MindmapNode`
- `KernelProjectionKind::Mindmap`
这说明协议层已经承认:
> **导图应该属于统一 kernel,而不是永远停留在专用 blob 协议。**
问题不在方向,而在主线尚未切换完成。
---
## 4. 当前架构问题
如果继续维持当前模式,会有四类问题。
### 4.1 导图真相仍被视图库数据结构绑架
现在真正被保存、被读取、被回写的是:
- `simple-mind-map` 兼容树
这会导致:
- 领域模型被 UI 库字段形状反向约束
- 导图语义无法稳定进入 kernel
- 不同视图无法共享统一对象真相
### 4.2 AI 仍然不是直接操作 kernel
虽然已经有 `mindmap_apply_ops` 等工具,但它们当前语义仍然是:
- 取出一棵树
- 在 runtime 里改树
- 返回新树
这仍然不是:
- 直接创建 `mindmap_node`
- 直接移动 subtree
- 直接挂接 reference edge
因此 AI 还没有真正“直接写导图内核”。
### 4.3 导图还没有进入统一 projection 家族
当前 Sidebar / 页面树 / 文件树 正在往 kernel projection 收敛。
但导图仍然主要是:
- 专用 route
- 专用 blob
- 专用前端大组件
这会让导图继续成为一个旁路系统。
### 4.4 前端壳过重,迁移边界不清
`MindmapBlock.tsx` 当前同时承担:
- 读取
- 兼容转换
- 渲染
- 编辑
- 保存
- 资产替换
- 导入导出
- 页面模式切换
这意味着:
> **只要导图真相还留在这里,Phase 6 就不会真正成立。**
---
## 5. Phase 6 的正确目标
Phase 6 的正确目标固定为:
> **让 Mindmap 从“独立对象中心 + blob 真相 + 前端重壳”降级成“kernel subtree / graph 的一种 projection 与 editor”。**
再收口成一句:
> **Mindmap 不是对象真相层,Mindmap 只是树/图真相的一种空间化编辑视图。**
这意味着:
- 导图不能再拥有第二份对象真相
- 导图不能再以整棵 blob 作为长期 canonical model
- 导图编辑器只能操作 kernel
- 导图 route 只能消费 kernel projection
---
## 6. 目标分层
长期建议把导图域拆成四层。
### 6.1 Kernel Truth
这一层只存系统真相。
建议对象:
- `mindmap`
- 作为导图根容器节点
- `mindmap_node`
- 作为导图树节点
- `reference edge`
- 作为节点到页面、附件、PDF anchor、summary、ai_note 的横向关系
这层只负责:
- 节点存在性
- 父子结构
- sibling 顺序
- 节点稳定 ID
- 节点元信息
- 引用边
- 审计与版本
### 6.2 Mindmap Projection Layer
这一层负责把 kernel truth 投影为导图视图可消费的数据。
长期至少应有两类 projection
- `mindmap_preview`
- 给文档内嵌卡片、轻量预览使用
- `mindmap_editor`
- 给导图独立页、沉浸编辑器使用
这层输出的应是:
- 稳定 node id
- parent / child 关系
- sibling order
- depth
- child count
- 引用摘要
- capability flags
- 视图提示信息
而不是直接把前端控件内部状态回吐出去。
### 6.3 Editor Adapter Layer
这一层负责:
- 把 kernel projection 转成 `simple-mind-map` 当前所需的数据形状
- 把画布交互翻译成 kernel command
这一层是 compat adapter,不是事实源。
### 6.4 View Shell
这一层只负责:
- 画布
- 工具栏
- 右键菜单
- 缩略图
- 全屏壳
- 交互状态
这里可以继续用 `simple-mind-map` 过渡,但不能再持有对象真相。
---
## 7. 哪些语义必须进入 kernel
下面这些语义必须进入 kernel,而不是继续停留在 `simple-mind-map` blob 中。
### 7.1 节点级稳定语义
- `mindmap` 根节点
- `mindmap_node` 子节点
- `title / text`
- `note`
- `hyperlink`
- `refs`
- `collapsed`
- sibling 排序键
- 节点归档/删除状态
### 7.2 树语义
- 创建子节点
- 创建同级节点
- 重命名
- 移动 subtree
- 重排顺序
- 删除 subtree
### 7.3 图语义
- 节点引用页面
- 节点引用块
- 节点引用附件
- 节点引用 PDF 页码/anchor
- AI 生成节点引用证据节点
### 7.4 审计语义
- command id
- trace id
- actor id
- revision / version
---
## 8. 哪些语义不应进入 kernel
下面这些内容不应进入 kernel truth。
- 当前缩放比例
- 当前视口位置
- 当前选中节点
- minimap 展开状态
- 文本编辑框 DOM 状态
- 鼠标拖拽中的临时态
- 纯视图级动画状态
- `simple-mind-map` 内部 history 栈
- 仅服务当前画布库的缓存字段
这些内容属于:
- editor session state
- view shell state
不是 kernel truth。
---
## 9. 命令层重构口径
当前 `mindmap_apply_ops``mindmap_put` 不能再继续被当作长期真相接口。
### 9.1 `mindmap_put`
长期应降级为:
- 导入整图
- 替换快照
- 迁移回放
- 故障恢复
不应再作为常规编辑主路径。
### 9.2 `mindmap_apply_ops`
长期应保留,但语义要改。
它应变成:
- **compat facade**
内部行为应是:
-`MindmapOp` 翻译成 kernel command 序列
例如:
- `addChild`
- `kernel.node.create`
- `addSiblingAfter`
- `kernel.node.create` + sibling order 调整
- `updateText`
- `kernel.node.update`
- `setHyperlink`
- `kernel.node.update`
- `setRefs`
- `kernel.edge.attach` / `kernel.edge.detach`
- `deleteNode`
- `kernel.subtree.delete``kernel.node.archive`
也就是说:
> **`mindmap_apply_ops` 可以继续存在,但它不能继续直接修改一棵 compat 树。**
### 9.3 新的正式命令面
Phase 6 后导图应以 kernel command 为正式写面:
- `create_node`
- `update_node`
- `move_subtree`
- `reorder_siblings`
- `archive_node`
- `restore_node`
- `attach_edge`
- `detach_edge`
如果 kernel 当前缺少某些命令,就应在 Phase 6 补入,而不是继续把缺口留给前端 blob。
---
## 10. Projection 重构口径
当前导图 route 仍然主要走:
- `mindmaps.get`
然后前端:
- `buildMindmapProjection(...)`
这条链必须调整。
### 10.1 正式读路径
长期正式读路径应是:
- `kernel.project_view`
- projection=`mindmap`
而不是:
- `mindmaps.get`
- 返回 raw blob
### 10.2 Projection 输出要求
导图 projection 输出至少应包括:
- `projection_id`
- `root_node_id`
- `items`
- `edges`
- `depth`
- `sort_key`
- `expand_hint`
- `capability_flags`
- `resource_meta`
### 10.3 前端 adapter 的角色
前端可以继续存在一个:
- `projection -> simple-mind-map` adapter
但这层只能是 view adapter。
它不能再同时承担:
- canonical model
- 持久化模型
- AI 写入模型
---
## 11. AI / CLI 口径
如果目标是“AI 能直接编写思维导图,而不是外挂组件”,那 Phase 6 的 AI 路线必须明确。
### 11.1 AI 读取导图
AI 应读取:
- kernel subtree
- mindmap projection
- node refs / evidence
而不是只读取一棵视图库 JSON。
### 11.2 AI 修改导图
AI 应直接发 kernel command,或通过 compat facade 发命令。
正确路径应是:
- AI 意图
- tool / command plan
- kernel command
- projection 刷新
不是:
- AI 输出一整棵导图 JSON
- 再整体覆盖保存
### 11.3 AI 生成导图
`mindmap_outline_to_mindmap` 这类能力可以保留,但输出应优先写入:
- `mindmap`
- `mindmap_node`
- `reference edge`
而不是先生成一棵孤立 blob 再把它塞进存储。
---
## 12. 与当前代码对应的重构任务
### 12.1 Rust 协议层
需要重构或补充:
- `/mnt/Data1T/mnote/rust/crates/core-protocol/src/kernel.rs`
- `/mnt/Data1T/mnote/rust/crates/core-protocol/src/mindmap.rs`
建议动作:
- 保留 `MindmapOp` 作为 compat DTO
- 不再把 `MindmapTreeNode` 当长期 canonical model
- 为导图补齐正式 projection / command 契约
### 12.2 Rust runtime
需要重构:
- `/mnt/Data1T/mnote/rust/crates/bridge-runtime/src/lib.rs`
建议动作:
- 增加正式 `mindmap` projection builder
-`mindmap_apply_ops` 改成 command translator
-`mindmaps.get` 逐步降级为 compat query
- 新增或补齐导图相关 kernel command
### 12.3 前端 mindmap adapter
需要重构:
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/mindmap/mindmap-projection.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/mindmap/mindmapOps.ts`
建议动作:
- `mindmap-projection.ts`
- 从“blob 摘要器”转成“kernel projection adapter”
- `mindmapOps.ts`
- 从“本地真相修改器”降级为 compat / fallback 层
### 12.4 前端视图壳
需要重构:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/mindmap/[docId]/[mindmapId]/page.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/mindmap/[docId]/[mindmapId]/mindmap-page-client.tsx`
建议动作:
- 将读取从 `mindmaps.get` 切到 kernel projection
- 将写入从 `getData()/POST whole blob` 切到命令流
-`MindmapBlock.tsx` 缩成 view shell + adapter
### 12.5 持久化与兼容层
需要重构:
- `/mnt/Data1T/mnote/wolai-frontend/convex/mindmaps.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts`
建议动作:
- 迁移期允许双写
- 长期让 `mindmaps` 表退到 compat snapshot / import-export 层
- 正式真相切到 kernel backing store
---
## 13. 迁移分期
### 13.1 Phase 6-A:冻结边界
目标:
- 停止在前端和 compat blob 上继续追加长期业务语义
详细 checklist
- [ ] 冻结口径:
- 明确 `MindmapTreeNode` 只作为 compat DTO
- 明确 `mindmaps` blob 只作为过渡存储,不再新增长期业务语义
- 明确 `simple-mind-map` 只作为 renderer / editor adapter,不再定义系统真相
- [ ] 协议边界盘点:
- 盘点 `/mnt/Data1T/mnote/rust/crates/core-protocol/src/mindmap.rs` 中哪些字段属于 compat 语义
- 盘点 `/mnt/Data1T/mnote/rust/crates/core-protocol/src/kernel.rs` 中已有 `mindmap` / `mindmap_node` / `projection` 能力
- 列出 Phase 6 后必须新增的 kernel command / projection 契约缺口
- [ ] 前端边界盘点:
- 盘点 `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx` 中哪些逻辑属于真相层
- 将这些逻辑标记为后续待迁移:
- 读主链
- 写主链
- 本地摘要
- 引用回写
- 资产 URL 反写
- 明确哪些逻辑继续保留在 view shell:
- 画布渲染
- 右键菜单
- 工具栏
- 缩略图
- 全屏壳
- [ ] 持久化边界盘点:
- 盘点 `/mnt/Data1T/mnote/wolai-frontend/convex/mindmaps.ts` 的现有字段与索引
- 盘点 `/mnt/Data1T/mnote/wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts` 的现有读写语义
- 明确哪些接口后续降级为 compat:
- `mindmaps.get`
- `mindmaps.put`
- `mindmaps.delete`
- `mindmaps.restore`
- [ ] AI / CLI 边界盘点:
- 盘点 `mindmap_get` / `mindmap_get_subtree` / `mindmap_put` / `mindmap_apply_ops`
- 标注哪些工具后续保留为 facade,哪些应切到 kernel command
- 明确 AI 不再以“整图 JSON 覆盖”作为长期主路径
- [ ] 文档口径冻结:
- 当前文档作为 Phase 6 主文档继续维护
- checklist / architecture / 后续任务拆分不再把导图描述为独立事实源
- 新增导图相关设计时默认引用本文件,而不是继续围绕 `simple-mind-map` 数据结构展开
- [ ] 代码约束冻结:
- 在未完成 Phase 6-B 之前,不再给 `MindmapTreeNode` 增加新的长期业务字段
- 在未完成 Phase 6-C 之前,不再新增新的“整图读取后本地改树再整体提交”的主路径
- 在未完成 Phase 6-D 之前,不再新增绕过 kernel 的 AI 写入链路
出阶段判定:
- [ ] 新导图语义默认先判断是否落 kernel,而不是先落前端 blob
- [ ] `MindmapBlock.tsx` 不再继续承担新的长期对象真相职责
- [ ] 团队已统一接受:
- compat blob 不是长期真相
- `simple-mind-map` 不是对象中心
- Phase 6 后正式主线是 kernel truth + projection + adapter
### 13.2 Phase 6-B:先切读路径
目标:
- 独立页与内嵌预览先读取正式 kernel projection
详细 checklist
- [ ] 定义 projection 家族:
- 定义 `mindmap_preview` projection
- 定义 `mindmap_editor` projection
- 明确两者共享的稳定字段:
- `projection_id`
- `root_node_id`
- `items`
- `edges`
- `capability_flags`
- `resource_meta`
- [ ] 定义 projection item 结构:
- 稳定 node id
- `parent_id`
- `sort_key`
- `depth`
- `child_count`
- `title/text`
- `collapsed`
- `refs summary`
- `style hint`
- [ ] Rust runtime 补 projection builder
-`/mnt/Data1T/mnote/rust/crates/bridge-runtime/src/lib.rs` 中增加正式 `mindmap` projection builder
-`kernel.project_view` 可返回 `KernelProjectionKind::Mindmap`
- 明确 preview 与 editor 的输出差异,不再直接返回 compat 整树
- [ ] 前端 adapter 改造:
-`/mnt/Data1T/mnote/wolai-frontend/src/lib/mindmap/mindmap-projection.ts` 从“blob 摘要器”改为“kernel projection adapter”
- 把 adapter 输出限定为 `simple-mind-map` 所需最小数据形状
- 不再让 adapter 同时承担 canonical model 职责
- [ ] 独立页读路径切换:
- `/mnt/Data1T/mnote/wolai-frontend/src/app/mindmap/[docId]/[mindmapId]/page.tsx`
改为优先读取 kernel projection
- SSR 初始数据不再以 `mindmaps.get` raw blob 为主
- `mindmap-page-client.tsx` 仅接收 projection / adapter 输出
- [ ] 文档内嵌预览切换:
- 内嵌卡片优先消费 `mindmap_preview`
- 预览态不再默认依赖整棵 blob
- 轻量预览和沉浸编辑使用不同 projection,避免主编辑壳过早挂载
- [ ] 兼容回退策略:
- kernel projection 不可用时,可临时回退到 compat `mindmaps.get`
- 回退路径必须显式标记为 compat/fallback
- 回退逻辑不得反向成为新主路径
- [ ] 可观测性与追踪:
- projection 响应带 `request_id` / `trace_id`
- 前端记录当前页面命中的 projection 来源:
- kernel
- compat fallback
- 为后续切流留出观测点
- [ ] 测试与验收:
- 增加 runtime 单测:`mindmap_preview` / `mindmap_editor` 输出稳定
- 增加 adapter 单测:projection -> `simple-mind-map` data
- 增加独立页 smoke:首屏读 projection 成功
- 增加内嵌态 smoke:文档页不再依赖整图 blob 才能显示摘要
出阶段判定:
- [ ] 独立页正式主读链已经是 kernel projection
- [ ] 文档内嵌预览正式主读链已经是 preview projection
- [ ] compat `mindmaps.get` 只作为 fallback,而不是默认主链
- [ ] 前端已有清晰的 `projection -> adapter -> renderer` 三层边界
### 13.3 Phase 6-C:再切写路径
目标:
- 常规编辑改为命令流
详细 checklist
- [ ] 明确正式写面:
- `create_node`
- `update_node`
- `move_subtree`
- `reorder_siblings`
- `archive_node`
- `restore_node`
- `attach_edge`
- `detach_edge`
- [ ] 补齐 kernel command 缺口:
- 若当前 kernel 缺少 sibling reorder 命令,需补齐
- 若当前 kernel 缺少 subtree archive/delete 语义,需补齐
- 若当前 kernel 缺少导图节点 refs 的 attach/detach 语义,需补齐
- [ ] compat `MindmapOp` 到 kernel command 的翻译表落地:
- `addChild`
- `addSiblingAfter`
- `updateText`
- `setHyperlink`
- `setRefs`
- `appendNote`
- `deleteNode`
- 为每个 op 指定唯一的 kernel command 映射与错误语义
- [ ] Rust runtime 改造:
- `/mnt/Data1T/mnote/rust/crates/bridge-runtime/src/lib.rs`
中的 `mindmap_apply_ops` 改为 command translator
- 不再直接对 compat 树做 canonical 修改
- 执行完成后返回:
- command 执行结果
- 最新 projection 或 refresh token
- trace / audit 信息
- [ ] 前端保存链改造:
- `MindmapBlock.tsx` 中工具栏操作不再默认走 `getData()/POST whole blob`
- 节点编辑、移动、删除、引用更新都发命令,而不是整体回传 snapshot
- 本地只保留短暂 optimistic state,不再作为长期真相
- [ ] 导图 route 改造:
- `/api/mindmap/[docId]/[mindmapId]`
`POST` 从常规编辑主入口降级
- 新增或切换到专用 command route / command envelope
- `mindmap_put` 只保留:
- 导入
- 替换快照
- 恢复
- 迁移回放
- [ ] 双写与迁移策略:
- 迁移期允许 kernel truth + compat snapshot 双写
- 双写失败时要有清晰告警,不得静默漂移
- 明确哪一侧是主真相,哪一侧只是镜像
- [ ] 冲突与审计:
- 所有命令返回 revision / version
- 写入链保留 `command_id` / `request_id` / `trace_id`
- 并发冲突时优先按 kernel command 冲突规则处理,而不是前端最后一次整图覆盖
- [ ] 测试与验收:
- runtime 单测:各类 `MindmapOp` 均正确翻译为 kernel command
- route 单测:常规编辑不再依赖整图 `put`
- UI smoke:增删改拖拽节点后可稳定刷新 projection
- 回归测试:导入/恢复仍可通过 `mindmap_put` 正常工作
出阶段判定:
- [ ] 常规编辑链路已经以 kernel command 为主
- [ ] `mindmap_put` 已从常规编辑主链退出
- [ ] 前端不再依赖 `getData()` 作为长期提交真相
- [ ] compat snapshot 即使保留,也只是双写镜像或导入导出格式
### 13.4 Phase 6-D:统一 AI / CLI
目标:
- AI / CLI 改为直接操作 kernel truth
详细 checklist
- [ ] AI 工具口径统一:
- 明确 AI 读导图优先读取 kernel projection / subtree
- 明确 AI 写导图优先发送 kernel command
- 明确 AI 不再以“输出完整 mindmap JSON 并整体覆盖”作为主模式
- [ ] compat tool 重构:
- `mindmap_get`
降级为 compat read facade
- `mindmap_get_subtree`
降级为 compat read facade
- `mindmap_apply_ops`
降级为 compat write facade
- `mindmap_put`
降级为导入/恢复工具
- [ ] 新 kernel-aware tool 面补齐:
- 面向 node / subtree / edge 的导图工具定义
- 工具参数默认使用:
- `nodeId`
- `rootNodeId`
- `workspaceId`
- `pageId`
- `edgeType`
- 避免继续以 compat `uid + whole tree` 为中心
- [ ] AI host/runtime 接缝改造:
- 导图 agent route 优先走 kernel-aware tool
- `mindmap_outline_to_mindmap` 的输出优先落 kernel truth
- AI 修改后的刷新结果优先返回 projection,而不是 raw blob
- [ ] CLI 改造:
- CLI 新增或切换到导图 kernel command 子命令
- 现有 `mindmap get/put/op` 标记 compat/legacy 语义
- CLI smoke 优先验证 kernel command 与 projection 输出
- [ ] 权限与审计:
- AI / CLI 导图写入保留 actor / trace / command id
- 区分:
- 用户直接编辑
- AI 代理修改
- 导入/恢复
- 确保后续 bridge log / audit 能区分来源
- [ ] 结果返回规范:
- AI / CLI 执行导图写命令后,默认返回:
- command 执行结果
- 受影响 node / edge
- 最新 projection 摘要
- 非必要不再回传整棵 compat 树
- [ ] 迁移与兼容:
- 迁移期 compat tools 仍可保留
- 但默认优先级必须低于 kernel-aware tools
- 新增 AI 能力时不得再优先扩写 compat blob 工具
- [ ] 测试与验收:
- AI tool 单测:至少一条创建节点、修改节点、挂接 refs 的链路走 kernel command
- CLI smoke:至少一条真实导图操作链不依赖整图覆盖
- 回归测试:compat tool 仍可用于迁移和紧急 fallback
出阶段判定:
- [ ] AI 已能直接创建、修改、移动导图节点与引用边
- [ ] CLI 已能直接操作导图 kernel truth
- [ ] compat tools 只剩 facade / import-export / fallback 职责
- [ ] 导图 AI / CLI 主链已经和 Sidebar / 页面树 / 阅读页一样,正式回到统一 kernel command / projection 体系
### 13.5 Phase 6-E:压缩旧壳
目标:
-`MindmapBlock.tsx` 收缩为可替换 view shell
要求:
- 真相、命令、projection 已完全外移
- 前端只剩渲染与交互适配
---
## 14. 完成判定
只有同时满足下面几条,才能说 `Kernel Phase 6` 完成。
- [ ] 导图正式事实源已经进入 kernel node / edge / subtree
- [ ] 导图独立页读取的是 kernel projection,而不是 `mindmaps.get` raw blob
- [ ] 文档内嵌导图读取的是 preview projection,而不是前端直接拼整棵树
- [ ] 常规编辑操作已经回写 kernel command,而不是 `POST` 整棵 `getData()`
- [ ] AI / CLI 可以直接创建、修改、移动导图节点,而不依赖整图覆盖
- [ ] `simple-mind-map` 已经退到 renderer / adapter 层
- [ ] `mindmaps` blob 存储已降级为 compat snapshot 或导入导出用途
---
## 15. 非目标
这阶段不以这些事情为主目标:
- 立即把整个导图画布改写成 Rust 前端
- 立即替换全部导图 UI 细节
-`simple-mind-map` 的所有样式字段完整提升为 kernel 语义
- 在第一阶段就清空全部历史兼容接口
Phase 6 的重点只有一个:
> **先把导图真相、导图命令、导图 projection 从前端 blob 体系里拔出来,正式并入 `tree-first graph kernel`。**
@@ -0,0 +1,137 @@
# Wolai-aline 对标测试流程 v1
> 本流程用于所有以 Wolai 体验复刻、对标、aline/alignment 为目标的任务。目标不是只做出类似文案,而是用真实 Wolai 行为和本地 `3000` 行为做可复现对比,形成可回归的 smoke 与截图证据。
## 适用范围
凡任务描述包含以下任一意图,默认进入本流程:
- `wolai-aline``wolai-align``Wolai 对标``复刻 Wolai``恢复 Wolai 体验`
- 需要把本地 `http://127.0.0.1:3000` 的页面、Sidebar、搜索、编辑器、浮层、菜单、快捷键或交互状态对齐 Wolai。
- 需要比较 Wolai 页面与本地实现的截图、DOM、键鼠行为或浏览器状态。
## 固定资源
- 本地入口:`http://127.0.0.1:3000/`
- Wolai 参考页 / 当前授权 Hermes 测试页:`https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd`
- Wolai owner 登录态 Chrome profile`/mnt/Data1T/mnote/tmp/wolai-playwright-profile`
- Chrome 可执行文件:`/opt/google/chrome/chrome`
- 对标证据输出根目录:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/`
- 当前可复用 smoke 示例:`/mnt/Data1T/mnote/scripts/task128-rust-web-wolai-search-modal-smoke.js`
## 安全边界
- 默认优先对 Wolai 做只读操作:打开页面、点击入口、hover、打开/关闭菜单、截图、DOM 读取、输入搜索关键词。
- 当前 Hermes 测试页 `https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd` 已授权用于 Wolai-aline 编辑器对标;当任务需要真实编辑行为时,可以执行最小范围编辑测试。
- 编辑测试必须记录编辑前后截图、动作链、输入内容和是否已清理;测试内容优先使用唯一标记,例如 `[mnote-aline-test-时间戳]`
- 即使在 Hermes 测试页,也禁止未经确认地删除、移动、归档、发布、评论、改权限或批量修改既有非测试内容。
- 其他 Wolai 页面仍默认只读;如需写入,必须先由用户提供沙盒页 URL 和明确授权。
- 遇到登录、滑块验证或登录态失效,不绕过验证;记录阻塞并让用户介入。
- 登录态 profile 只放在 `tmp/` 这类 git ignored 路径,不提交、复制或打印敏感 cookie/token。
## Codex skill
后续 Wolai-aline 任务必须启用本机 skill`/home/lix/.codex/skills/wolai-aline`。该 skill 是本流程的执行入口,负责强制差异矩阵、截图复核、subagent 浏览器取证和失败模式沉淀。
## 标准流程
1. 明确小任务
从设计稿、缺陷反馈或用户描述中抽出一个可验证的小任务。任务必须能用具体行为描述,例如“侧栏搜索图标打开全局搜索 modal 且不跳转”“Ctrl+P 在 modal 打开时关闭 modal”。
2. 先取 Wolai 基线(必要时编辑)
浏览器测试必须交给 subagent 执行。subagent 默认只读;当任务明确需要编辑器行为时,可在当前 Hermes 测试页进入受控 editable-test mode,记录 Wolai 的真实行为、DOM 线索、截图路径、编辑内容和不可验证项。
Wolai 基线至少记录:
- 打开入口:哪个按钮、菜单、快捷键或区域触发。
- 关闭入口:快捷键、Esc、遮罩、关闭按钮、再次触发等是否有效。
- URL 是否变化。
- 关键控件形态和状态:开关、下拉、按钮、菜单、输入框、焦点态、选中态、disabled 态。
- 关键文本只是辅助证据,不能替代控件形态和交互状态。
- 截图保存到 `/mnt/Data1T/mnote/tmp/wolai-editor-parity/<task>/`
3. 写 RED smoke
在本地实现前,先新增或扩展 `scripts/task*-smoke.js`。smoke 必须在当前实现上失败,并且失败原因要对应缺失行为。
smoke 断言优先级:
- 行为断言:打开、关闭、URL 不跳转、焦点、快捷键、遮罩点击。
- DOM 语义断言:`role``aria-*``data-testid``data-*` 状态。
- 视觉状态代理断言:开关开启/关闭、下拉当前值、结果行存在、hover/active class。
- 文案断言:作为补充,不能单独作为通过依据。
4. 小范围实现
只改当前任务直接相关的文件。涉及 `3000` 当前主入口时优先检查 `rust/crates/mnote-web/`;不要把长期语义塞进临时 compat 或前端第二份真相。
若修改 Rust SSR 常量或样式,需要重启 `npm run desktop:hot` 或对应 `mnote-web` 进程后再跑浏览器 smoke,避免测到旧二进制。
5. 本地验证
至少运行:
- 当前任务 smoke,例如 `node scripts/task128-rust-web-wolai-search-modal-smoke.js`
- 与改动范围匹配的格式化/单测,例如 `cd rust && cargo fmt -p mnote-web --check && cargo test -p mnote-web`
如有 CSS 尺寸、SSR 输出、页面入口等护栏测试失败,优先修实现或压缩新增样式,不随意放宽阈值。
6. subagent 对标复测
实现后再次派 subagent 做浏览器对标。subagent 负责:
- 用 Wolai owner/profile 或公开态取参考截图。
- 用本地 `3000` 执行同一动作链。
- 记录 DOM 状态、URL、截图路径、剩余差异。
- 不修改源码,不清理截图,不还原文件。
7. 主线程复核截图
主线程必须查看或复核 subagent 产出的 Wolai 与本地截图。不能只根据 subagent 的“通过”结论或文本断言宣布对齐。
复核重点:
- 控件类型是否一致,例如 switch 不能误做成普通 pill button。
- 开启/关闭、选中/未选中、hover/active 等状态是否一致。
- 打开/关闭路径是否一致,例如同一入口不应跳转到另一页面。
- 快捷键是否是 toggle 还是单向打开。
- 结果列表、命中高亮、空态、占位符是否符合当前任务验收范围。
8. 汇报
最终汇报必须包含:
- 改动文件。
- RED/GREEN 验证命令。
- Wolai 与本地截图绝对路径。
- 已对齐项和剩余差异。
- 未执行或受阻的验证原因。
## Subagent 浏览器测试模板
```text
请执行 Wolai-aline 浏览器对标测试,不要修改源码。默认只读;如任务明确要求编辑器行为,可在当前 Hermes 测试页做最小编辑验证。禁止删除、移动、归档、发布、评论、改权限或批量修改既有非测试内容。
任务:<一句话描述当前对标行为>
Wolai URL: https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd
Wolai profile: /mnt/Data1T/mnote/tmp/wolai-playwright-profile
本地 URL: http://127.0.0.1:3000/
输出目录: /mnt/Data1T/mnote/tmp/wolai-editor-parity/<task>/
请验证:
1. Wolai 中该行为的打开入口、关闭入口、URL 变化、关键 DOM/视觉状态。
2. 本地 3000 中同一行为是否一致。
3. 保存 Wolai 和本地截图。
4. 回报截图绝对路径、通过/失败结论、剩余差异。
遇到登录/滑块不要绕过,直接报告需要用户介入。
```
## 当前已固化样例:搜索 modal
当前实现已用 `task128-rust-web-wolai-search-modal-smoke.js` 固化以下行为:
- 顶栏搜索按钮打开同一个搜索 modal。
- 侧栏左上搜索入口打开同一个搜索 modal,不跳转 `/search`
- 搜索选项包含 `仅匹配标题``精确匹配``按编辑时间``按创建时间``页面内搜索`
- `仅匹配标题``页面内搜索``role="switch"` 且默认 `aria-checked="true"`
- `Ctrl+P` 在 modal 打开时关闭 modal。
- 结果数量、快捷键提示、空态/结果区域可被 smoke 捕获。
历史教训:不能只检查 modal 文案。上一次搜索 modal 先误把 Wolai 的 switch 做成普通 pill button,截图复核后才发现。因此后续 Wolai-aline 任务必须把截图复核写入验收,而不是把 subagent 文本结论当作最终证据。
+84
View File
@@ -0,0 +1,84 @@
# 90-1 参考资料:文件树 Rust 生态调研
是的,Rust生态中有多个类似VSCode文件树的实现,涵盖**终端(TUI)组件**、**GUI组件**和**独立应用**三类,均支持文件/目录的层级展示、折叠展开与交互操作。
---
### 一、终端(TUI)领域:VSCode风格文件树组件/应用
#### 1. filetree (ft) - 最接近VSCode文件树的TUI实现
- 仓库:https://github.com/nyanko3141592/filetree
- 版本:v0.3.52026年3月)
- 核心特性:
- VSCode风格界面,支持文件/目录层级展示与折叠
- Git状态集成(修改、未跟踪、忽略文件颜色标记)
- Vim键绑定(hjkl导航)与鼠标支持
- 系统剪贴板集成,支持文件复制/剪切/粘贴
- 可通过cargo直接安装:`cargo install filetree`
#### 2. fileview - 轻量级VSCode风格文件树TUI
- 仓库:https://crates.io/crates/fileview
- 版本:v1.8.12026年2月)
- 特点:
- 极简设计,启动迅速,无需配置
- 支持图像预览(Kitty/iTerm2/Sixel)与语法高亮
- 模糊查找功能,快速定位文件
- 多文件选择与批量操作
#### 3. 通用TUI文件树组件库
| 库名称 | 适用框架 | 核心特点 |
|--------|----------|----------|
| tui-file-explorer | Ratatui | 双面板布局,独立左右浏览窗格,Tab切换 |
| tui-tree-widget | Ratatui | 通用树视图组件,支持自定义数据结构 |
| ratatui-explorer | Ratatui | 高度可定制,支持键盘输入处理 |
| ratkit | Ratatui | 包含树视图在内的综合组件库,支持可调整大小的分割面板 |
---
### 二、GUI领域:Rust图形界面文件树组件
#### 1. iced_file_tree - Iced GUI框架的文件树组件
- 仓库:https://crates.io/crates/iced_file_tree
- 适配Iced跨平台GUI库(受Elm启发)
- 支持文件系统导航与层级展示,可集成到桌面应用中
#### 2. Tauri应用中的文件树实现
- TauriRust+WebView)可通过Rust后端+前端组件实现VSCode风格文件树
- 后端:使用`std::fs::read_dir`读取目录内容,返回`(path, is_directory)`元组
- 前端:创建嵌套HTML列表(ul/li)表示文件夹结构,添加VSCode风格图标与交互
---
### 三、使用场景与选择建议
| 使用场景 | 推荐选择 | 理由 |
|----------|----------|------|
| 快速体验VSCode风格文件树 | filetree(ft) | 开箱即用,VSCode界面风格,Git集成 |
| 集成到Ratatui终端应用 | tui-file-explorer | 专为Ratatui设计,双面板布局,功能完善 |
| 构建跨平台GUI应用 | iced_file_tree | 适配Iced框架,原生GUI体验 |
| 开发Tauri桌面应用 | 自定义实现 | 结合Rust后端与Web前端,灵活性最高 |
---
### 四、快速上手示例
使用filetree(ft)快速体验VSCode风格文件树:
```bash
# 安装
cargo install filetree
# 运行(当前目录)
ft
# 常用操作
hjkl - 导航(Vim风格)
g/G - 跳至顶部/底部
Enter - 打开文件/文件夹
dd - 删除文件/文件夹
yy - 复制,p - 粘贴
:q - 退出
```
如果你需要将文件树组件集成到自己的Rust项目中,建议根据应用类型(终端/GUI)选择对应的组件库,或参考filetree的实现方式进行定制开发。
需要我给你一份在 Ratatui 中快速集成 tui-file-explorer 实现 VSCode 风格单面板文件树的最小可运行代码示例吗?
+147
View File
@@ -0,0 +1,147 @@
# 90-2 参考资料:页面树 Rust 生态调研
是的,Rust生态中有多种类似Notion页面树的实现,覆盖**数据结构库**、**UI组件库**(终端/桌面)和**完整应用**三类,均支持层次化文档管理、折叠展开、节点操作等核心功能。Notion页面树的关键特性是**块级编辑+嵌套页面+树形导航**,以下是Rust中的对应方案。
---
### 一、核心数据结构库:构建页面树的基础
这些库提供树状数据结构,可作为Notion风格页面树的底层存储,支持节点增删改查、层级管理等操作。
| 库名称 | 核心特点 | 适用场景 |
|--------|----------|----------|
| **easy-tree** | 轻量级树结构,深度优先遍历,支持并行迭代 | 快速构建简单页面树,文件系统映射 |
| **tree-ds** | 支持节点插入/删除/移动,整树枚举,子树修剪与嫁接 | 复杂层次结构管理,需要灵活节点操作 |
| **treelog** | 自定义树渲染,支持多种样式(Unicode/ASCII/Box) | 命令行工具中展示页面树结构 |
| **ptree** | 美观的树状结构打印,支持自定义节点显示 | 调试或展示页面树层级关系 |
---
### 二、UI组件库:实现页面树的可视化与交互
这些组件库提供现成的树形UI,支持折叠展开、拖拽、键盘导航等Notion页面树关键交互功能。
#### 1. 终端(TUI)组件库
| 组件库 | 适配框架 | 核心特性 |
|--------|----------|----------|
| **tui-file-explorer** | Ratatui | 双面板布局,文件/目录层级展示,支持Git状态标记 |
| **ratatui-toolkit** | Ratatui | 包含树视图组件,支持自定义渲染与交互 |
| **cursive-tree** | Cursive | 支持"<>"/"↑↓"导航,鼠标点击切换折叠状态,多选择模式 |
#### 2. 桌面(GUI)组件库
| 组件库 | 适配框架 | 核心特性 |
|--------|----------|----------|
| **gpui-component::tree** | GPUI | 支持折叠/展开、键盘导航、自定义项渲染,适合文件浏览器和导航菜单 |
| **fyrox::gui::tree** | Fyrox | 内置选择控制,支持Ctrl+Click多选,Alt+Click拖拽 |
| **iced_file_tree** | Iced | 适配Iced跨平台GUI框架,支持文件系统导航与层级展示 |
| **egui-tree-view** | egui | 纯Rust即时模式GUI,支持嵌套节点,折叠展开,拖拽排序 |
---
### 三、完整应用:Notion风格页面树的直接体验
这些应用提供完整的Notion-like体验,包含页面树导航+块级编辑器,可直接使用或作为参考实现。
| 应用名称 | 技术栈 | 页面树特性 |
|----------|--------|------------|
| **Rustree** | Rust | 分层结构存储HTML文本,支持节点引用,类似Treepad |
| **TauriNote** | Tauri(Rust+WebView) | 仿Notion块级编辑器,文件树管理,支持创建/删除笔记 |
| **Ferrite** | Rust+egui | 轻量级文本编辑器,支持Markdown,内置文档结构导航 |
| **ylin** | Tauri 2+Rust | 文件树侧边栏,支持Markdown编辑,实时预览 |
---
### 四、Notion页面树核心功能的Rust实现方案
#### 1. 块级编辑+页面树结合
Notion的核心是**块级内容+嵌套页面**,Rust中可通过以下方式实现:
- 后端:使用`tree-ds``easy-tree`存储块/页面层次结构,每个节点包含内容类型(文本/标题/列表等)和子节点列表
- 前端:使用GPUI/egui/Iced的树组件渲染页面树,块编辑器可集成`rust-markdown`或自定义块解析器
#### 2. 拖拽调整页面顺序
拖拽是Notion页面树的重要交互,Rust GUI框架中实现方式:
- **egui**:通过`egui::DragValue`和自定义状态管理实现节点拖拽排序
- **Fyrox**:内置`Tree`组件支持Alt+Click拖拽,无需额外实现
- **Tauri**:结合Rust后端与前端拖拽库(如SortableJS),通过IPC同步节点位置
#### 3. 动态加载与懒渲染
大型页面树需要懒加载优化,Rust中可通过:
- 使用`TreeBackend`按需加载子节点(如`cursive-tree`的实现)
- 在Tauri应用中,Rust后端监听节点展开事件,读取子页面数据并返回给前端
---
### 五、使用场景与选择建议
| 使用场景 | 推荐选择 | 理由 |
|----------|----------|------|
| 快速体验Notion风格页面树 | TauriNote/Rustree | 开箱即用,完整UI体验,支持块编辑与页面树导航 |
| 集成到Ratatui终端应用 | tui-file-explorer | 专为Ratatui设计,双面板布局,功能完善 |
| 构建跨平台GUI应用 | egui-tree-view/iced_file_tree | 纯Rust实现,原生GUI体验,无需WebView |
| 开发Tauri桌面应用 | 自定义实现+前端组件 | 结合Rust后端性能与前端交互灵活性,最接近Notion体验 |
| 仅需页面树数据结构 | easy-tree/tree-ds | 轻量级,API简单,适合快速开发 |
---
### 六、快速上手示例:egui-tree-view实现Notion风格页面树
```rust
use egui::{CentralPanel, Context, TopBottomPanel};
use egui_tree_view::{TreeView, TreeViewState};
struct PageNode {
title: String,
children: Vec<PageNode>,
expanded: bool,
}
impl PageNode {
fn new(title: &str) -> Self {
Self {
title: title.to_string(),
children: Vec::new(),
expanded: false,
}
}
}
fn main() -> eframe::Result<()> {
let mut tree_state = TreeViewState::default();
let mut root_node = PageNode::new("Notion Style Page Tree");
root_node.children.push(PageNode::new("Getting Started"));
root_node.children.push({
let mut node = PageNode::new("Advanced Features");
node.children.push(PageNode::new("Block Editing"));
node.children.push(PageNode::new("Page Nesting"));
node
});
eframe::run_simple_native("Notion Page Tree Demo", Default::default(), move |ctx, _frame| {
TopBottomPanel::top("menu").show(ctx, |ui| {
ui.heading("Notion Style Editor");
});
CentralPanel::default().show(ctx, |ui| {
TreeView::new("page_tree", &mut tree_state)
.with_nodes(&mut root_node.children, |ui, node, _depth| {
ui.checkbox(&mut node.expanded, &node.title);
node.expanded
})
.show(ui);
});
})
}
```
---
### 七、总结
Rust生态中已有丰富的工具可构建Notion风格页面树,从底层数据结构到完整应用全覆盖。若需快速开发,可选择现成组件库或应用;若需高度定制,可基于树结构库+UI框架组合实现,充分发挥Rust的性能与安全优势。
需要我给你一份在 Tauri 中结合 Rust 后端与前端实现可拖拽 Notion 风格页面树的最小可运行示例吗?
+52
View File
@@ -0,0 +1,52 @@
# design 设计稿索引
> 更新时间:2026-04-20
>
> 状态口径以当前仓库真实代码为准:
> - `[done]`:对应阶段或收口目标已经在当前主线代码中成立
> - `[process]`:方向已进入主线,但仍在推进中
> - `[recycle]`:已废弃、已被后续稿件替代,统一归档到 `old/`
## 当前优先级入口
- `01-05` 当前有效主线与优先级,请先看:
`/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
## 主线顺序
1. `01-tree-first-graph-kernel/`
- `process/` 放推进中的主线稿
- `done/` 放已在真实代码中成立的主线稿
2. `02-convex-rust-long-term-architecture/`
- `process/` 放推进中的主线稿
- `done/` 放已在真实代码中成立的主线稿
3. `03-rust-web/`
- `process/` 放推进中的主线稿
- `done/` 放已在真实代码中成立的主线稿
4. `04-tree-domain/`
- `process/` 放推进中的主线稿
- `done/` 放已在真实代码中成立的主线稿
5. `05-editor-mainline/`
- `process/` 放推进中的主线稿
- `done/` 放已在真实代码中成立的主线稿
- `reference-code/` 放编辑器参考代码
6. `06-mindmap/`
- `process/` 放推进中的主线稿
- `done/` 放已在真实代码中成立的主线稿
7. `07-ai/`
- `process/` 放推进中的主线稿
- `done/` 放已在真实代码中成立的主线稿
## 迁移规则
- 主线设计稿完成后,必须从对应大类的 `process/` 移动到 `done/`
- 仅以当前真实代码为准判断是否完成,不能只按稿件自述判断。
- 废弃稿统一移动到 `old/` 对应大类下,再按历史成熟度放入 `process/``done/`
## 辅助目录
- `90-reference/`
- 参考资料,不参与 `[done]/[process]/[recycle]` 状态判断
- `old/`
- 已废弃或被替代的历史稿件,标题统一标记 `[recycle]`
- 每个大类继续按 `process/``done/` 分层
+92
View File
@@ -0,0 +1,92 @@
# Design System Strategy: The Digital Atelier
## 1. Overview & Creative North Star
**Creative North Star: "The Digital Atelier"**
The objective of this design system is to transform a functional workspace into a curated sanctuary for thought. Unlike standard "SaaS-blue" interfaces that feel industrial and rigid, "The Digital Atelier" treats the screen as a high-end editorial canvas. We lean into **Organic Minimalism**—where the architecture of the page is defined by light and space rather than lines and boxes.
The system breaks the "template" look by utilizing intentional asymmetry in the sidebar, generous breathing room (white space) that prioritizes focus, and a sophisticated layering of tones that mimics the physical stacking of fine vellum paper.
---
## 2. Colors & Surface Philosophy
The palette is rooted in a "High-Value Neutral" philosophy, using green as a surgical strike of intent rather than a blunt instrument.
### The Palette
- **Primary (Action):** `#31AA4D` (The Signature Green) — Used exclusively for intentional actions and progress.
- **Secondary (Utility):** `#2367F6` — Reserved for links and specific collaborative indicators.
- **Surface (Background):** `#FAF9F9` (Surface) to `#FFFFFF` (Lowest).
- **Tonal Greys:** `#F7F7F7` (Container Low), `#EAEAEA` (Outline Variant).
### The "No-Line" Rule
Traditional 1px borders are strictly prohibited for sectioning. Boundaries between the navigation sidebar and the main editor must be defined solely by the shift from `surface-container-low` (`#F4F3F3`) to `surface-container-lowest` (`#FFFFFF`).
### Surface Hierarchy & Nesting
Treat the UI as a series of physical layers.
- **Layer 0 (Canvas):** `surface` (`#FAF9F9`).
- **Layer 1 (Sidebar/Navigation):** `surface-container-low` (`#F4F3F3`).
- **Layer 2 (The Document):** `surface-container-lowest` (`#FFFFFF`).
- **Layer 3 (Modals/Popovers):** `surface-container-highest` (`#E3E2E2`) with Glassmorphism.
### The "Glass & Gradient" Rule
For floating elements (AI Assistant, Help), use `surface-container-lowest` at 80% opacity with a `24px` backdrop-blur. Apply a subtle linear gradient to the Primary CTA (from `primary` `#006E28` to `primary-container` `#31AA4D`) to provide a "soulful" depth that flat colors lack.
---
## 3. Typography: The Editorial Scale
We use **Inter** for its precision. The hierarchy is designed to favor document readability and rhythmic flow.
| Level | Size | Weight | Tracking | Line Height | Usage |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **Display-LG** | 3.5rem | 700 | -0.02em | 1.1 | Hero Document Titles |
| **Headline-LG** | 2.0rem | 600 | -0.01em | 1.2 | Heading 1 (H1) |
| **Title-MD** | 1.125rem | 600 | 0 | 1.4 | Heading 2 (H2) |
| **Body-LG** | 1.0rem | 400 | 0 | 1.6 | Primary Reading Text |
| **Label-MD** | 0.75rem | 500 | +0.02em | 1.0 | Sidebar Labels / Metadata |
*Directors Note: The 1.6 line-height for Body-LG is non-negotiable. It creates the "Vertical Rhythm" necessary for deep work.*
---
## 4. Elevation & Depth
Depth is achieved through **Tonal Layering** rather than structural shadows.
- **The Layering Principle:** A block-level "Callout" should not have a border. It should be a `surface-container-high` (`#E9E8E8`) shape with a `sm` (`0.125rem`) rounded corner, nestled within the `surface-container-lowest` page.
- **Ambient Shadows:** Only for floating context menus. Use `on-surface` (`#1B1C1C`) at 4% opacity with a `32px` blur and `16px` Y-offset. It should feel like a soft glow, not a drop shadow.
- **The "Ghost Border":** If a table cell requires definition, use `outline-variant` at 15% opacity. Never 100%.
---
## 5. Components & Block System
### The Block System (The Core Experience)
- **Text Blocks:** Standard Inter 1rem. Use a `24px` margin-bottom to ensure "breathable" paragraphs.
- **Heading Blocks:** H1-H3 use a tighter line-height (1.2) to feel like a "unit."
- **Callouts:** Icons should use the `secondary` (`#2367F6`) color at 10% opacity for the background "pill" and 100% for the icon itself.
- **Drag Handles:** Six-dot pattern. Appear at 20% opacity on block hover; 60% opacity on grab.
### Navigation (Sidebar & Breadcrumbs)
- **Sidebar:** Use `surface-container-low`. Indentation for nested pages must be exactly `12px` per level to create a clear visual "tree" without lines.
- **Hover States:** Instead of a highlight box, use a "soft-bleed" background: `surface-container-high` with a `4px` corner radius, inset by `4px` from the sidebar edge.
- **Breadcrumbs:** Use `label-md`. The current page is `on-surface`; parent pages are `on-surface-variant`.
### Primitive Components
- **Buttons:**
- **Primary:** Gradient (`primary` to `primary-container`), white text, `md` (`0.375rem`) radius.
- **Tertiary:** No background. Text color is `on-surface-variant`. Hover state triggers a `surface-container-high` background.
- **Cards & Lists:** **Prohibit divider lines.** Use vertical whitespace (16px, 24px, or 32px from the spacing scale) to separate thoughts.
- **Checkboxes:** When checked, use `primary` (`#31AA4D`). When unchecked, use a `Ghost Border` of `outline`.
---
## 6. Dos and Donts
### Do
- **Do** use `surface-container` shifts to define functional zones (Sidebar vs. Editor).
- **Do** use asymmetrical padding. Give the document more space on the left (the "margin of thought") than the right.
- **Do** use `SFMono-Regular` for code blocks and inline technical terms to provide a structural contrast to the organic Inter font.
### Don't
- **Don't** use 1px black or grey borders to separate sections.
- **Don't** use pure black (`#000000`) for text. Use `on-surface` (`#1B1C1C`) for a softer, premium editorial feel.
- **Don't** use sharp corners. Everything must have at least a `sm` (`0.125rem`) or `md` (`0.375rem`) radius to maintain the "Organic" North Star.
- **Don't** crowd the UI. If a user can't see the "paper" behind the content, the layout is too dense.
+399
View File
@@ -0,0 +1,399 @@
<!DOCTYPE html>
<html class="light" lang="zh-CN"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>个人 | The Digital Atelier</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-secondary-fixed-variant": "#003ea7",
"on-surface": "#2d2e2e",
"surface-tint": "#006e28",
"primary": "#006e28",
"on-secondary-container": "#fefcff",
"inverse-surface": "#2f3031",
"surface-container-high": "#f5f5f5",
"on-surface-variant": "#5a5a5a",
"on-tertiary": "#ffffff",
"on-background": "#1b1c1c",
"on-tertiary-container": "#2d2e2e",
"surface-container-low": "#fafafa",
"on-primary": "#ffffff",
"surface": "#ffffff",
"primary-container": "#31aa4d",
"tertiary-fixed-dim": "#c7c6c6",
"error-container": "#ffdad6",
"secondary-container": "#286af9",
"surface-bright": "#faf9f9",
"surface-container-highest": "#e3e2e2",
"on-secondary": "#ffffff",
"on-primary-fixed": "#002107",
"secondary": "#0051d5",
"surface-container-lowest": "#ffffff",
"on-error-container": "#93000a",
"error": "#ba1a1a",
"on-primary-fixed-variant": "#00531c",
"tertiary": "#5e5e5e",
"on-primary-container": "#003610",
"on-secondary-fixed": "#00174b",
"secondary-fixed": "#dbe1ff",
"inverse-on-surface": "#f2f0f0",
"tertiary-fixed": "#e3e2e2",
"outline": "#d1d5db",
"surface-container": "#f7f7f7",
"on-error": "#ffffff",
"inverse-primary": "#69de7a",
"background": "#ffffff",
"surface-variant": "#f3f4f6",
"primary-fixed-dim": "#69de7a",
"surface-dim": "#dbdad9",
"tertiary-container": "#959595",
"secondary-fixed-dim": "#b4c5ff",
"on-tertiary-fixed": "#1b1c1c",
"on-tertiary-fixed-variant": "#464747",
"primary-fixed": "#86fb93",
"outline-variant": "#bdcab9",
"wolai-selected": "#fce4e4"
},
"borderRadius": {
"DEFAULT": "4px",
"lg": "6px",
"xl": "8px",
"full": "9999px"
},
"fontFamily": {
"headline": ["Inter", "sans-serif"],
"body": ["Inter", "sans-serif"],
"label": ["Inter", "sans-serif"]
}
},
},
}
</script>
<style>
body { font-family: 'Inter', sans-serif; -webkit-font-smoothing: antialiased; color: #37352f; }
.material-symbols-outlined { font-size: 18px; vertical-align: middle; }
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.sidebar-item:hover .item-actions { opacity: 1; }
.triangle-toggle { transition: transform 0.2s; cursor: pointer; font-size: 14px !important; }
.triangle-toggle.open { transform: rotate(90deg); }
.block-spacing { margin-bottom: 2px; }
/* Sidebar View Switching */
.view-content { display: none; }
.view-active { display: block; }
.tab-active { color: #2d2e2e; font-weight: 600; border-bottom: 2px solid #006e28; }
</style>
</head>
<body class="bg-white text-on-surface font-body">
<div class="flex h-screen overflow-hidden">
<!-- SideNavBar -->
<aside class="flex flex-col h-full bg-[#f7f7f5] dark:bg-stone-950 w-60 border-r border-stone-200/50 z-30 select-none">
<!-- Header -->
<div class="flex items-center gap-2 mb-2 cursor-pointer transition-colors pt-2"><div class="flex flex-col w-full">
<!-- Workspace Header -->
<div class="flex items-center gap-2 px-3 py-2 cursor-pointer group">
<div class="w-8 h-8 rounded bg-[#d3544e] flex items-center justify-center shrink-0 text-white font-medium text-lg">L</div>
<span class="text-[17px] font-bold text-stone-800 truncate">liaibo的个人空间</span>
<span class="material-symbols-outlined text-stone-600 text-[20px] ml-auto">expand_more</span>
</div>
<!-- Toolbar Icons -->
<div class="flex items-center px-4 py-3 text-stone-600 w-full bg-[#f7f7f5]">
<div class="flex items-center justify-between w-full">
<span class="material-symbols-outlined text-[24px] cursor-pointer hover:bg-stone-200/50 rounded-sm p-0.5">search</span>
<span class="material-symbols-outlined text-[24px] cursor-pointer hover:bg-stone-200/50 rounded-sm p-0.5">account_tree</span>
<span class="material-symbols-outlined text-[24px] cursor-pointer hover:bg-stone-200/50 rounded-sm p-0.5">bolt</span>
<span class="material-symbols-outlined text-[24px] cursor-pointer hover:bg-stone-200/50 rounded-sm p-0.5">help</span>
<span class="material-symbols-outlined text-[24px] cursor-pointer hover:bg-stone-200/50 rounded-sm p-0.5">inventory_2</span>
<span class="material-symbols-outlined text-[24px] cursor-pointer hover:bg-stone-200/50 rounded-sm p-0.5">more_horiz</span>
</div>
</div>
</div></div>
<!-- Main Tabs -->
<nav class="flex-1 overflow-y-auto no-scrollbar px-2 space-y-0.5">
<div class="mt-2">
<!-- Starred Section -->
<div class="flex items-center gap-1 px-3 py-1 text-stone-400 group cursor-pointer hover:bg-stone-200/50">
<span class="material-symbols-outlined text-[16px] text-orange-400">star</span>
<span class="text-[13px] font-medium text-stone-500">星标置顶</span>
<span class="material-symbols-outlined text-[16px] ml-0.5">expand_more</span>
</div>
<div class="px-1 space-y-[1px] mt-1 mb-6">
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
<span class="text-stone-300 font-bold ml-1">·</span>
<span class="material-symbols-outlined text-stone-500 text-[18px]">sticky_note_2</span>
<span class="text-[14px]">记录</span>
</a>
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
<span class="w-5 h-5 flex items-center justify-center overflow-hidden rounded-[2px]">
<img alt="calendar" class="w-full h-full object-contain" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDPgwtm4gouecfvHJ5gaIuLbl9hm82rutzZUhlxiM8_jyV_QiMLixw_ZWHlStFPooQgE6NpMVdOKuuV3LrNJbAuLGA_WBjHnfhP0NhZxpuysUjt_unG-_rzrfEWr-kspQ2pzJqK9nROJOz0DkSHoP6F-Y5y94TegpSKGg8Q3sWp-g3qDNQzBV5pK7h6jU26EIY9UtZDzCOM0Vel8YpfBIRKt-BbyFvvaJiITXMNKHJNHr2bf6Ciry1Ms1mj-mdZ18bjVgp7eojOfyc"/>
</span>
<span class="text-[14px]">日报</span>
</a>
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
<span class="material-symbols-outlined text-green-600 text-[20px]">account_balance_wallet</span>
<span class="text-[14px]">阳阳钱包</span>
</a>
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
<span class="w-5 h-5 flex items-center justify-center overflow-hidden rounded-[2px]">
<img alt="calendar" class="w-full h-full object-contain" src="https://lh3.googleusercontent.com/aida-public/AB6AXuB_jLmyrcqa9V9ShAaaFQtg4-H2_dirQ6z7nw_0a0sJvU8dbeLxV1uHAlwUxg8cyvojv7beX8Y9faeT4SOHn0EBMsv5XUiOsAh735lJcVuY-Tlkwv3zH8BavU-fEnHpCvtSounrGJ1uooJInzjY37ypbdeHhkQbeGMBbzupSc_dQXzISN2XG31ug4s3_9u4fDfVxKg0J92Ap-50Kc7STQgQDe5m1rZo71_1I7oP5en9N3gLgWlaRCtq5o7dHzwB5WbZpJhYVTQZC4M"/>
</span>
<span class="text-[14px]">My LifeOS</span>
</a>
</div>
<!-- Section with Tabs Toggle -->
<div class="flex items-center justify-between px-3 py-1 mt-4">
<div class="flex gap-4">
<button class="text-[13px] font-medium text-stone-400 hover:text-stone-700 transition-colors tab-active cursor-pointer pb-0.5" id="tab-pages" onclick="switchSidebarView('pages')">我的页面</button>
<button class="flex items-center gap-1 text-[13px] font-medium text-stone-400 hover:text-stone-700 transition-colors cursor-pointer pb-0.5" id="tab-files" onclick="switchSidebarView('files')">
<span class="material-symbols-outlined text-[16px]">folder_open</span>
Explorer
</button>
</div>
<span class="material-symbols-outlined text-[16px] text-stone-400 ml-0.5 cursor-pointer">expand_more</span>
</div>
<!-- View: Page View (Default Wolai) -->
<div class="view-content view-active px-1 space-y-[1px] mt-2" id="view-pages">
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
<span class="material-symbols-outlined text-stone-800 text-[20px] font-bold">content_cut</span>
<span class="text-[14px]">剪藏</span>
</a>
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
<span class="material-symbols-outlined text-stone-800 text-[20px] font-variation-fill">home</span>
<span class="text-[14px]">个人</span>
</a>
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
<span class="material-symbols-outlined text-stone-800 text-[20px]">domain</span>
<span class="text-[14px]">知识</span>
</a>
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
<span class="material-symbols-outlined text-stone-800 text-[20px]">grid_view</span>
<span class="text-[14px]">项目</span>
</a>
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
<span class="material-symbols-outlined text-stone-800 text-[20px] font-variation-fill">book</span>
<span class="text-[14px]">工作资料</span>
</a>
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
<span class="w-5 h-5 flex items-center justify-center overflow-hidden rounded-[2px]">
<img alt="calendar" class="w-full h-full object-contain" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCPR9Mr19xWn9DuKwKZpoJ1tqeR0Y4FE1fMyBlvjZEYCkYOh1NowiLbxo7fSRF1cQBgV51cEwjMIB3HrjmsNtmvzYRdtnHDOse0Z77UzXg7ksBGpKrzJ48KxQ5wi-IKKzzqmc19EDDYRU997VaWrFWJdS2hI5MXjDqAOW9W1d8UfA2QRBmTtYhPfZr9dQb6jSnVaMxjNTchVEKJic6g8FSLbirEusH-O88rEdVehgQiLSu0NtV_QusN_BD98oEBqQt9L6VAiVy0byc"/>
</span>
<span class="text-[14px]">日常</span>
</a>
</div>
<!-- View: File View (VS Code Style) -->
<div class="view-content px-1 mt-2 mb-4" id="view-files">
<!-- Folder: Public -->
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600">
<span class="material-symbols-outlined text-[16px] text-stone-400">expand_more</span>
<span class="material-symbols-outlined text-[18px] text-amber-500">folder</span>
<span class="text-[13px] font-medium">public</span>
</div>
<div class="ml-4 border-l border-stone-200 pl-1">
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600">
<span class="material-symbols-outlined text-[18px] text-blue-400">html</span>
<span class="text-[13px]">index.html</span>
</div>
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600">
<span class="material-symbols-outlined text-[18px] text-indigo-400">image</span>
<span class="text-[13px]">favicon.ico</span>
</div>
</div>
<!-- Folder: Src -->
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600 mt-1">
<span class="material-symbols-outlined text-[16px] text-stone-400">expand_more</span>
<span class="material-symbols-outlined text-[18px] text-amber-500">folder</span>
<span class="text-[13px] font-medium">src</span>
</div>
<div class="ml-4 border-l border-stone-200 pl-1">
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600">
<span class="material-symbols-outlined text-[18px] text-sky-500">javascript</span>
<span class="text-[13px]">App.js</span>
</div>
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600">
<span class="material-symbols-outlined text-[18px] text-blue-500">css</span>
<span class="text-[13px]">styles.css</span>
</div>
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600">
<span class="material-symbols-outlined text-[16px] text-stone-400">chevron_right</span>
<span class="material-symbols-outlined text-[18px] text-amber-500">folder</span>
<span class="text-[13px]">assets</span>
</div>
</div>
<!-- Root Files -->
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600 mt-1">
<span class="material-symbols-outlined text-[18px] text-stone-400">description</span>
<span class="text-[13px]">README.md</span>
</div>
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600">
<span class="material-symbols-outlined text-[18px] text-amber-600">settings</span>
<span class="text-[13px]">package.json</span>
</div>
</div>
</div>
</nav>
<!-- Footer -->
<div class="mt-auto border-t border-stone-200">
<div class="flex w-full border-t border-stone-200 bg-[#f7f7f5]">
<button class="flex-1 flex items-center justify-center gap-2 py-3 border-r border-stone-200 hover:bg-stone-200/30 text-stone-600 transition-colors">
<span class="material-symbols-outlined text-[20px]">delete</span>
<span class="text-[14px]">垃圾箱</span>
</button>
<button class="flex-1 flex items-center justify-center gap-2 py-3 hover:bg-stone-200/30 text-stone-600 transition-colors">
<span class="w-4 h-4">
<svg fill="none" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L3 7V17L12 22L21 17V7L12 2Z" fill="#fcd34d" fill-opacity="0.6" stroke="#f59e0b" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
<path d="M3 7L12 12L21 7" stroke="#f59e0b" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
<path d="M12 22V12" stroke="#f59e0b" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
</svg>
</span>
<span class="text-[14px]">模板中心</span>
</button>
</div>
</div>
</aside>
<!-- Main Content Area -->
<main class="flex-1 overflow-y-auto bg-white relative">
<!-- TopAppBar -->
<header class="flex justify-between items-center px-4 h-[44px] w-full sticky top-0 bg-white/90 backdrop-blur-sm z-40">
<div class="flex items-center gap-1.5 text-stone-500">
<span class="material-symbols-outlined hover:bg-stone-100 p-1 rounded cursor-pointer" data-icon="menu">menu</span>
<div class="flex items-center text-[13px] font-normal px-1">
<span class="hover:bg-stone-100 px-1.5 py-0.5 rounded cursor-pointer">The Digital Atelier</span>
<span class="mx-0.5 text-stone-300">/</span>
<span class="hover:bg-stone-100 px-1.5 py-0.5 rounded cursor-pointer text-stone-900 font-medium flex items-center gap-1">
<span class="material-symbols-outlined text-[16px]" data-icon="home">home</span>
个人
</span>
</div>
</div>
<div class="flex items-center gap-0.5">
<div class="flex items-center px-2 py-1 bg-green-50 text-green-700 rounded-full text-[11px] font-medium mr-2 gap-1 border border-green-100">
<span class="w-1.5 h-1.5 bg-green-500 rounded-full"></span>
Public
</div>
<div class="flex items-center gap-0.5 text-stone-500">
<span class="material-symbols-outlined hover:bg-stone-100 p-1.5 rounded cursor-pointer" data-icon="star">star</span>
<span class="material-symbols-outlined hover:bg-stone-100 p-1.5 rounded cursor-pointer" data-icon="history">history</span>
<span class="material-symbols-outlined hover:bg-stone-100 p-1.5 rounded cursor-pointer text-green-600" data-icon="auto_awesome">auto_awesome</span>
<span class="material-symbols-outlined hover:bg-stone-100 p-1.5 rounded cursor-pointer" data-icon="search">search</span>
<span class="material-symbols-outlined hover:bg-stone-100 p-1.5 rounded cursor-pointer" data-icon="more_horiz">more_horiz</span>
</div>
</div>
</header>
<!-- Document Canvas -->
<article class="max-w-4xl mx-auto pt-[80px] pb-40 px-12 md:px-24">
<!-- Page Icon & Title -->
<div class="mb-8">
<div class="group relative inline-block mb-4">
<span class="material-symbols-outlined text-[72px] text-stone-800 cursor-default" data-icon="home">home</span>
</div>
<h1 class="text-4xl font-bold text-stone-900 tracking-tight leading-tight mb-2">个人</h1>
<div class="flex items-center gap-4 text-stone-400 text-[13px] border-b border-stone-100 pb-6 mb-8">
<span class="flex items-center gap-1.5 hover:text-stone-600 cursor-pointer"><span class="material-symbols-outlined text-[16px]" data-icon="face">face</span> 个人空间</span>
<span class="flex items-center gap-1.5"><span class="material-symbols-outlined text-[16px]" data-icon="calendar_today">calendar_today</span> 2024年5月20日</span>
</div>
</div>
<!-- Vertical Sub-page Links with Wolai Spacing -->
<section class="mb-10 space-y-[2px]">
<a class="group flex items-center gap-2 py-1 px-1 -ml-1 hover:bg-stone-100 rounded transition-colors text-stone-700" href="#">
<span class="material-symbols-outlined text-stone-400 group-hover:text-primary" data-icon="description">description</span>
<span class="text-[15px] font-normal underline decoration-stone-200 underline-offset-4">我的文件</span>
</a>
<a class="group flex items-center gap-2 py-1 px-1 -ml-1 hover:bg-stone-100 rounded transition-colors text-stone-700" href="#">
<span class="material-symbols-outlined text-red-400" data-icon="filter_vintage">filter_vintage</span>
<span class="text-[15px] font-normal underline decoration-stone-200 underline-offset-4">灵感库</span>
</a>
<a class="group flex items-center gap-2 py-1 px-1 -ml-1 hover:bg-stone-100 rounded transition-colors text-stone-700" href="#">
<span class="material-symbols-outlined text-amber-500" data-icon="sensor_door">sensor_door</span>
<span class="text-[15px] font-normal underline decoration-stone-200 underline-offset-4">工作台</span>
</a>
<a class="group flex items-center gap-2 py-1 px-1 -ml-1 hover:bg-stone-100 rounded transition-colors text-stone-700" href="#">
<span class="material-symbols-outlined text-blue-500" data-icon="lock">lock</span>
<span class="text-[15px] font-normal underline decoration-stone-200 underline-offset-4">私人保险箱</span>
</a>
<a class="group flex items-center gap-2 py-1 px-1 -ml-1 hover:bg-stone-100 rounded transition-colors text-stone-700" href="#">
<span class="material-symbols-outlined text-green-500" data-icon="book">book</span>
<span class="text-[15px] font-normal underline decoration-stone-200 underline-offset-4">阅读清单</span>
</a>
<a class="group flex items-center gap-2 py-1 px-1 -ml-1 hover:bg-stone-100 rounded transition-colors text-stone-700" href="#">
<span class="material-symbols-outlined text-rose-500" data-icon="favorite">favorite</span>
<span class="text-[15px] font-normal underline decoration-stone-200 underline-offset-4">健康追踪</span>
</a>
<a class="group flex items-center gap-2 py-1 px-1 -ml-1 hover:bg-stone-100 rounded transition-colors text-stone-700" href="#">
<span class="material-symbols-outlined text-indigo-400" data-icon="laptop_mac">laptop_mac</span>
<span class="text-[15px] font-normal underline decoration-stone-200 underline-offset-4">项目管理</span>
</a>
</section>
<!-- Editorial Content Blocks -->
<div class="space-y-6">
<p class="text-[16px] text-stone-800 leading-[1.8]">
欢迎来到你的数字工作室。这里是你整理思绪、捕获灵感并将其转化为现实的私人圣殿。每一个方块都承载着无限的可能性。
</p>
<div class="p-6 bg-[#f7f7f5] rounded-lg border-l-4 border-stone-300">
<div class="flex items-start gap-4">
<span class="material-symbols-outlined text-primary mt-0.5" data-icon="lightbulb">lightbulb</span>
<div class="space-y-2">
<h3 class="font-bold text-stone-800 text-[15px]">设计原则</h3>
<p class="text-[14px] text-stone-600 leading-relaxed">
我们追求的是“垂直节奏”。在你的数字化工坊中,呼吸感和平衡比任何繁杂的装饰都更为重要。请在此处记录你的第一条笔记。
</p>
</div>
</div>
</div>
<div class="pt-4 border-t border-stone-100">
<div class="text-stone-300 text-[15px] cursor-text select-none py-1 hover:bg-stone-50 rounded px-1 -mx-1">
输入 '/' 以插入块
</div>
</div>
</div>
</article>
<!-- Floating Action Buttons -->
<div class="fixed bottom-6 right-6 flex flex-col gap-2 z-50">
<button class="bg-white border border-stone-200 text-stone-500 rounded-full w-10 h-10 flex items-center justify-center shadow-md hover:bg-stone-50 transition-all">
<span class="material-symbols-outlined" data-icon="help">help</span>
</button>
<button class="bg-primary text-white rounded-full w-12 h-12 flex items-center justify-center shadow-lg hover:opacity-90 transition-all scale-100 active:scale-95">
<span class="material-symbols-outlined" data-icon="auto_awesome" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
</button>
</div>
</main>
</div>
<script>
function switchSidebarView(view) {
const pagesView = document.getElementById('view-pages');
const filesView = document.getElementById('view-files');
const pagesTab = document.getElementById('tab-pages');
const filesTab = document.getElementById('tab-files');
if (view === 'pages') {
pagesView.classList.add('view-active');
filesView.classList.remove('view-active');
pagesTab.classList.add('tab-active');
filesTab.classList.remove('tab-active');
} else {
filesView.classList.add('view-active');
pagesView.classList.remove('view-active');
filesTab.classList.add('tab-active');
pagesTab.classList.remove('tab-active');
}
}
</script>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

@@ -0,0 +1,538 @@
# 1-0 [recycle] Tree-First Graph 内核实施清单 v1
> 更新时间:2026-04-16
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
> - `/mnt/Data1T/mnote/design/06-mindmap/process/6-mindmap-kernel-phase6-projection-editor-v1.md`
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
## 1. 文档目的
这份清单是基于**新架构前提**重写的。
此前的长期清单主要站在:
- Rust Web 层
- 页面访问性能
- islands 化
这些视角来拆解任务。
现在架构前提已经变化:
> **mnote 的长期中心不再只是“Rust Web + 页面瘦身”,而是“Tree-First Graph Kernel + 多投影 + Rust 主执行面”。**
因此这份清单的目标是:
- 先定义统一结构内核要怎么落地
- 再定义 Sidebar / 页面树 / 文件树 / Mindmap / 搜索 / AI / BlockNote 如何接入这个内核
- 最后再定义 Rust Web 层与前端投影视图如何围绕内核重构
---
## 2. 先给结论
长期路线应改成下面这个顺序:
1. **先立 kernel**
2. **再让 Rust Web 承接 kernel 的 query / command / projection**
3. **再让 Sidebar、搜索、Mindmap、AI、阅读页都开始直接消费 kernel**
4. **最后才边缘化 `BlockNote` 与旧前端壳**
这意味着:
- Rust Web 仍然重要
- 页面性能重构仍然重要
- 但它们现在都不是第一原则
第一原则变成:
> **所有对象最终都要回到统一的 `tree-first graph kernel`,而不是继续各自挂在不同前端壳和对象模型上。**
---
## 3. 新的总体验收定义
只有同时满足下面几条,才能说新架构真正成立:
- [x] 已存在 Rust 主导的 kernel node / edge 真相层
- [ ] 页面树、文件树、Mindmap、RAG 结构索引、AI 结构操作都开始直接依赖 kernel,而不是各有一套真相
- [x] Rust Web 层开始作为 kernel 的主 query / command / projection 承载层
- [x] Sidebar / 搜索 / 阅读页 / Mindmap / AI 至少有一部分已变成 kernel projection
- [ ] `BlockNote` 不再定义页面结构本体,只负责某类内容节点编辑
- [ ] 旧前端壳不再承担对象真相,只承担过渡投影或兼容层
---
## 4. 重新定义阶段
新架构下,阶段顺序应改成:
- Kernel Phase 0:边界冻结与术语统一
- Kernel Phase 1Node / Edge / Projection 基础模型落地
- Kernel Phase 2Kernel Query / Command / Subtree / Graph Traversal 协议落地
- Kernel Phase 3Rust Web 接入 kernel,成为主承载层
- Kernel Phase 4Sidebar / 页面树 / 文件树切到 kernel projection
- Kernel Phase 5:结构知识刷新与 kernel-aware 检索
- Kernel Phase 6Mindmap 降级为 projection / editor,而不是对象中心
- Kernel Phase 7:文档阅读页与 AI 面板切到 kernel projection
- Kernel Phase 8`BlockNote` 退化为内容编辑挂件
- Kernel Phase 9:旧前端壳与旧对象模型下线
---
## 5. Kernel Phase 0:边界冻结与术语统一
**当前状态:`DONE`**
### 5.1 目标
先把“什么是 kernel,什么只是 projection”说清楚。
### 5.2 已有事实
- [x] 已有:
- [tree-first-graph-kernel-v1.md](/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md)
- [rust-web-long-term-checklist-v2.md](/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md)
- [x] 已明确:
- `Mindmap` 不是中心
- `BlockNote` 不是中心
- `tree-first graph` 才是中心
### 5.3 仍需完成
- [x] 统一术语:
- `node`
- `edge`
- `projection`
- `subtree`
- `content node`
- `reference edge`
- `summary node`
- `index node`
- [x] 统一“事实源 / 投影 / 编辑器 / 外挂”的四层边界
- [x] 明确哪些现有对象暂时继续存在,哪些将来必须并入 kernel
### 5.4 完成判定
- [x] 后续文档和任务不再把“导图页”“页面树”“BlockNote 文档”当作独立事实源
### 5.5 当前落地说明
- [x] `tree-first-graph-kernel-v1.md` 已补术语冻结、四层边界与并入策略
- [x] 当前口径明确:
- 导图页不是事实源
- 页面树不是事实源
- `BlockNote` 文档结构不是事实源
---
## 6. Kernel Phase 1Node / Edge / Projection 基础模型落地
**当前状态:`DONE`**
### 6.1 目标
把设计文档中的 kernel 定义,变成真正的 Rust 类型与协议。
### 6.2 实施清单
- [x] 在 Rust core 中定义统一 `Node` 模型
- [x] 定义统一 `Edge` 模型
- [x] 定义统一 `Projection` 请求模型
- [x] 定义 node type 枚举或稳定字符串集合
- [x] 定义 edge type 枚举或稳定字符串集合
- [x] 定义 node metadata / content payload / refs payload 边界
- [x] 定义 subtree 标识方式
- [x] 定义 projection 标识方式
- [x] 定义版本、审计、trace 在 kernel 上的挂载方式
### 6.3 最小首批节点建议
- [x] `workspace`
- [x] `folder`
- [x] `page`
- [x] `section`
- [x] `asset`
- [x] `book`
- [x] `pdf`
- [x] `mindmap_node`
- [x] `summary`
- [x] `ai_note`
- [x] `reference_anchor`
### 6.4 最小首批边建议
- [x] `parent_of`
- [x] `child_of`
- [x] `contains`
- [x] `references`
- [x] `backlinks_to`
- [x] `source_of`
- [x] `summarizes`
- [x] `indexes`
### 6.5 完成判定
- [x] Rust core 中已经存在可被 Web / CLI / AI 共用的 kernel 类型定义
### 6.6 当前落地说明
- [x] `rust/crates/core-protocol/src/kernel.rs` 已进入主线
- [x] `core-protocol/src/lib.rs` 已导出 kernel 类型,供 Web / runtime 复用
---
## 7. Kernel Phase 2Kernel Query / Command / Subtree / Graph Traversal 协议落地
**当前状态:`DONE`**
### 7.1 目标
让 kernel 不是静态类型,而是可以被查询和操作。
### 7.2 实施清单
- [x] 定义 `get_node`
- [x] 定义 `get_subtree`
- [x] 定义 `list_children`
- [x] 定义 `list_edges`
- [x] 定义 `traverse_graph`
- [x] 定义 `create_node`
- [x] 定义 `update_node`
- [x] 定义 `move_subtree`
- [x] 定义 `attach_edge`
- [x] 定义 `detach_edge`
- [x] 定义 `project_view`
### 7.3 与当前已有能力的关系
当前已有一些可复用基础:
- [x] Mindmap 的 subtree / op 思想
- [x] Rust command/query/tool envelope
- [x] CLI / AI / runtime 可共用的调用结构
但还没有:
- [x] 统一 kernel query / command 面
### 7.4 完成判定
- [x] 至少有一组 kernel 查询和写入协议进入 Rust core-protocol / bridge-runtime 主线
### 7.5 当前落地说明
- [x] `bridge-runtime` 已支持:
- `kernel.node.get`
- `kernel.subtree.get`
- `kernel.children.list`
- `kernel.edges.list`
- `kernel.graph.traverse`
- `kernel.project_view`
- `kernel.node.create`
- `kernel.node.update`
- `kernel.subtree.move`
- `kernel.edge.attach`
- `kernel.edge.detach`
- [x] 当前首条真实 projection 样板已选用 Sidebar 数据集,经 Rust runtime 统一输出 kernel subtree / projection 结果
---
## 8. Kernel Phase 3Rust Web 接入 kernel
**当前状态:`DONE`**
### 8.1 目标
`mnote-web` 不只是通用骨架,而是开始承接 kernel。
### 8.2 当前已有事实
- [x] `mnote-web` 骨架已存在
- [x] 已有 request context / SSE / WS / Hermes bridge skeleton
### 8.3 仍需完成
- [x] 让 `mnote-web` 开始提供 kernel query route
- [x] 让 `mnote-web` 提供 subtree / projection route
- [x] 至少切一条真实主路径,不再只是 compat bridge
- [x] 为 kernel route 增加集成验证
### 8.4 完成判定
- [x] `mnote-web` 已承接至少一条真实 kernel 主链
### 8.5 当前落地说明
- [x] `rust/crates/mnote-web/src/routes/kernel.rs` 已新增:
- `/api/kernel/projections/sidebar`
- `/api/kernel/subtree`
- `/api/kernel/edges`
- `/api/kernel/graph`
- [x] `mnote-web` 已通过路由级测试,说明 kernel projection 已经不再只是骨架声明
---
## 9. Kernel Phase 4Sidebar / 页面树 / 文件树切到 kernel projection
**当前状态:`PARTIAL`**
### 9.1 目标
把工作区导航从“前端组件 + 自己拼树”,改成“kernel tree projection”。
### 9.2 当前真实状态
- [x] Sidebar 有服务端首包
- [x] Sidebar 有 Rust query 契约基础
- [ ] 但主 Sidebar 仍是超大客户端组件
- [ ] 页面树 / 文件树还没有真正基于 kernel node / edge
### 9.3 实施清单
- [ ] 用 kernel node/edge 重新定义 Sidebar 数据源
- [ ] 用 projection query 输出 Sidebar tree
- [ ] 用 projection query 输出 file tree
- [ ] 用 projection query 输出 page tree
- [ ] 把展开/折叠/拖拽从对象真相层剥离成局部 UI 状态
- [ ] 把主布局里的 Sidebar 继续拆轻
### 9.4 完成判定
- [x] Sidebar / 页面树 / 文件树已经开始直接消费 kernel projection
### 9.5 当前落地说明
- [x] Rust runtime 已经可以把 `sidebar.dataset.list` 转成统一 kernel projection / subtree 结果
- [ ] 当前前端主 Sidebar 还没有直接切到这个结果,仍保留 `DocumentRecord[] -> buildDocumentTree(...)` 的旧拼树逻辑
---
## 10. Kernel Phase 5:结构知识刷新与 kernel-aware 检索
**当前状态:`NOT_STARTED`**
### 10.1 目标
让系统不再依赖一个独立的传统 RAG 子系统,而是:
- 通过定时知识刷新持续生成和维护结构化知识
- 通过 kernel-aware 检索直接命中 node / subtree / evidence
- 通过 Mindmap / BookMindmap / summary node / reference edge 构成知识索引层
一句话说:
> **不是“部署一个 RAG 系统”,而是“把 kernel 本身建设成可刷新、可检索、可追溯的知识底座”。**
### 10.2 需要完成
- [ ] 定义知识刷新任务模型
- [ ] 定义 cron 驱动的定时刷新入口
- [ ] 定义增量刷新输入:workspace / book / pdf / page / subtree
- [ ] 定义知识刷新输出:
- `summary node`
- `ai_note node`
- `index node`
- `reference edge`
- `book subtree`
- `chapter subtree`
- [ ] 定义 kernel-aware 搜索输入
- [ ] 定义按 node type / subtree / edge 的过滤
- [ ] 让检索结果可以返回 node / subtree / evidence
- [ ] 定义“结构命中 -> 正文/附件/页码回查”的两段式检索
- [ ] 把书籍 / PDF / 章节树 / BookMindmap 纳入 kernel index
- [ ] 把 `Mindmap` / `BookMindmap` 定义成结构知识索引的一种 projection,而不是独立 RAG 页面
- [ ] 如果现有 `LightRAG` 仍有残留,明确其过渡边界与移除计划
### 10.3 当前基础
- [x] 搜索已有前端 runtime
- [x] Rust 有 FTS 与 Mindmap subtree 能力雏形
- [x] 已有 `BookRAG` / `Mindmap` / `BookMindmap` 方向上的结构索引思路
- [ ] 但还没有真正的知识刷新任务层
- [ ] 还没有 kernel-aware 检索面
- [ ] 还没有把“定时刷新知识 -> 更新 kernel 节点/边 -> 查询回查证据”串成主链
### 10.4 完成判定
- [ ] 至少有一条 cron 驱动的知识刷新链可以稳定更新 kernel 中的结构知识节点
- [ ] 至少有一条检索路径能直接返回 kernel node / subtree 命中结果
- [ ] 至少有一条命中结果能继续回查正文、附件、页码或证据节点
---
## 11. Kernel Phase 6Mindmap 降级为 projection / editor
**当前状态:`PARTIAL`**
### 11.1 目标
让 Mindmap 从“独立对象中心 + blob 真相 + 前端重壳”退回到“kernel subtree / graph 的一种 projection / editor”。
### 11.2 当前已有事实
- [x] Rust `core-protocol` 已有:
- `KernelNodeType::Mindmap`
- `KernelNodeType::MindmapNode`
- `KernelProjectionKind::Mindmap`
- [x] Rust runtime 已有:
- `mindmap_get`
- `mindmap_get_subtree`
- `mindmap_put`
- `mindmap_apply_ops`
- [x] 导图独立页已脱离 `editorStub`
- [x] 当前主线理念已明确:
- `Mindmap` 不是中心
- `tree-first graph kernel` 才是中心
- [x] 但当前真实主链仍然主要是:
- `MindmapBlock.tsx` 持有重交互壳
- `mindmaps.get/put` 读写整棵 blob
- 前端 `buildMindmapProjection(...)` 对 blob 做摘要
- `mindmap_apply_ops` 仍直接操作 compat mindmap 树
### 11.3 仍需完成
- [x] Phase 6 的详细口径改以:
- `/mnt/Data1T/mnote/design/06-mindmap/process/6-mindmap-kernel-phase6-projection-editor-v1.md`
- 为准
- [ ] 把导图事实源切到 kernel node / edge / subtree
- [ ] 把 `MindmapTreeNode` / `MindmapOp` 降级为 compat DTO,而不是长期 canonical model
- [ ] 让导图独立页读取正式 `mindmap` kernel projection,而不是继续以 `mindmaps.get` raw blob 为主
- [ ] 让文档内嵌导图读取 `mindmap_preview` / subtree preview,而不是直接拼整棵树
- [ ] 让导图编辑操作回写 kernel command,而不是继续以 `getData()/POST whole blob` 为主
- [ ] 让 `mindmap_apply_ops` 改成 compat facade,内部翻译到 kernel command
- [ ] 让 AI / CLI 可以直接创建、修改、移动 `mindmap_node` 与 reference edge
- [ ] 让 `mindmaps` 表退到 compat snapshot / import-export 层,而不是长期事实源
### 11.4 完成判定
- [ ] 导图独立页与内嵌预览都已经直接消费 kernel projection
- [ ] 常规编辑不再依赖整图 blob 覆盖保存
- [ ] AI / CLI 已能直接操作导图 kernel truth
- [ ] `simple-mind-map` 已退到 renderer / adapter 层
- [ ] `Mindmap` 可以被正式定义为 kernel 的一种 projection / editor,而不是独立事实源
---
## 12. Kernel Phase 7:文档阅读页与 AI 面板切到 kernel projection
**当前状态:`NOT_STARTED`**
### 12.1 目标
让阅读页和 AI 都不再直接围绕旧页面对象模型打转,而是围绕 kernel。
### 12.2 阅读页部分
- [ ] 阅读页改成读取 page subtree projection
- [ ] 阅读页大纲来自 kernel subtree
- [ ] 回链、引用、结构信息来自 kernel edge
- [ ] 页面阅读流作为 projection,而不是事实源
### 12.3 AI 部分
- [ ] AI tool 直接面向 node / subtree / edge
- [ ] AI 不再默认面向“页面前端壳”
- [ ] AI 能创建 summary node / ai_note node / reference edge
- [ ] AI 能把 PDF / Book 解析结果落进 kernel index
### 12.4 当前基础
- [x] 文档阅读态已出现
- [x] AI host/runtime 已拆分
- [ ] 但两者都还没开始直接面向 kernel projection
### 12.5 完成判定
- [ ] 阅读页与 AI 至少有一条主路径已经直接消费 kernel projection
---
## 13. Kernel Phase 8BlockNote 退化为内容编辑挂件
**当前状态:`NOT_STARTED`**
### 13.1 目标
`BlockNote` 从“页面定义者”改成“某类内容节点编辑器”。
### 13.2 当前已有事实
- [x] 文档页默认已不再直接强挂 `BlockNote`
### 13.3 仍需完成
- [ ] 定义 page subtree 与 content node 的关系
- [ ] 定义哪些节点仍由 `BlockNote` 编辑
- [ ] 定义哪些结构节点改由 kernel-aware editor 操作
- [ ] 把页面结构与 `BlockNote` 块结构彻底分离
- [ ] 把 `DocumentContent` 里外围 panel 再次拆出去
### 13.4 完成判定
- [ ] 页面结构已经不再由 `BlockNote` 数据结构定义
---
## 14. Kernel Phase 9:旧前端壳与旧对象模型下线
**当前状态:`NOT_STARTED`**
### 14.1 目标
当 kernel + Rust Web + projection 都建立后,再下线旧对象模型和旧主壳。
### 14.2 实施清单
- [ ] 盘点哪些旧页面/route 仍直接承载对象真相
- [ ] 盘点哪些旧 helper 仍在拼装第二套对象模型
- [ ] 删除已被 kernel projection 替代的旧 route
- [ ] 删除已被 kernel command/query 替代的旧 adapter
- [ ] 更新最终架构文档与对外口径
### 14.3 完成判定
- [ ] 旧前端壳与旧对象模型都不再是主事实来源
---
## 15. 这份清单与 Rust Web v2 的关系
这份清单不是替代:
- [rust-web-long-term-checklist-v2.md](/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md)
而是对它做“上位重排”。
关系应理解为:
- `rust-web-long-term-checklist-v2`
- 关注当前代码状态下的 Web / 页面重构现实进度
- `tree-first-graph-kernel-checklist-v1`
- 关注在新内核架构下,整体路线应该如何重新排序
一句话说:
> **Rust Web v2 告诉你“现在代码做到哪了”,这份新清单告诉你“在新架构下,接下来应该围绕什么继续做”。**
---
## 16. 最终结论
采用新架构后,长期 checklist 的中心必须变化。
不再是:
- 先 Web 重构,再考虑对象模型
而应该是:
- **先 Kernel,再 Projection,再 Web,再 Editor**
因此后续真正的主线应改成:
> **以 `tree-first graph kernel` 为事实中心,Rust Web 承接其 query/command/projectionSidebar/搜索/Mindmap/阅读页/AI 都逐步切到 kernel projection,最后再让 `BlockNote` 与旧前端壳退场。**
@@ -0,0 +1,554 @@
# 3-0 [recycle] Rust Web 长期架构实施清单 v1
> 更新时间:2026-04-16
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/document-access-performance-root-cure-v1.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md`
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
## 1. 文档用途
这份清单不是短期提速清单,而是长期架构落地清单。
它服务的目标只有一个:
> **把 mnote 从“Rust 内核 + 重前端页面壳”推进到“Rust 内核 + Rust Web 承载 + server-first 页面 + 少量交互孤岛”的稳定形态。**
这份清单强调四件事:
- 做什么
- 不做什么
- 什么时候算完成
- 哪些东西必须最后处理
---
## 2. 总体完成定义
只有同时满足下面几条,才能认为长期架构目标基本达成:
- [ ] Rust workspace 成为唯一业务真执行面,Web 层不再承载第二套对象规则
- [ ] Rust Web 层已经接住主 API、主页面壳、主流式能力
- [ ] 文档访问路径已变成“先阅读、后交互、再编辑”
- [ ] Sidebar、搜索、AI、Mindmap 等外围模块已退出当前重前端主壳
- [ ] `BlockNote` 已被隔离为最后的重交互孤岛,而不是整页入口前提
- [ ] Next/旧 React 壳只保留过渡兼容边界或已被替换
---
## 3. 执行原则清单
在任何阶段开始前,先确认这些原则不被破坏:
- [ ] 不再把新的业务规则写回 TS route / Next 页面层
- [ ] 不再新增新的全局常驻前端大面板
- [ ] 不再把“临时懒加载”当作长期方案替代品
- [ ] 不在未完成阅读态分离前,继续扩张编辑态默认挂载内容
- [ ] 不把 `BlockNote` 当成第一阶段改造对象
- [ ] 不把 OnlyOffice 当成主性能阻塞项
- [ ] 不把“换成 Rust”简化成“只重写 API”
---
## 4. 阶段总览
推荐按下面顺序推进:
- Phase 0:基线与边界冻结
- Phase 1Rust Web 基础层落地
- Phase 2:文档阅读页 server-first 化
- Phase 3Sidebar / 页面树 / 文件树 Rust 化与 island 化
- Phase 4:搜索系统 Rust 化与 island 化
- Phase 5AI 面板进一步收口为纯桥接 island
- Phase 6Mindmap 独立对象化与独立页面化
- Phase 7:文档编辑态与 `BlockNote` 孤岛化
- Phase 8:旧前端壳下线与兼容清理
### 4.1 阶段依赖与并行规则
这条长期路线不是“看到哪里慢就改哪里”,而是有明确前后依赖。
- [x] `Phase 0` 必须先产出边界冻结、基线和 islands 候选,否则后续会重新滑回旧壳扩张。
- [x] 横向能力 `H1` 必须在 `Phase 1` 之前定口径,否则 Rust Web、阅读态、搜索、AI、Mindmap 会各自发明 trace、缓存和权限语义。
- [x] `Phase 1``Phase 2` ~ `Phase 6` 的共同前置;没有 Rust Web 承载层,后续只能继续堆 Next route。
- [x] `Phase 2` ~ `Phase 6` 可以分批并行推进,但都必须遵守“阅读优先、局部 island、对象规则只进 Rust core”。
- [x] `Phase 7` 依赖 `Phase 2` ~ `Phase 6` 基本就位后再开始,否则 `BlockNote` 会继续承担外围能力。
- [x] `Phase 8` 只能在 `Phase 1` ~ `Phase 7` 都有稳定替代路径后执行,不能靠“删旧代码”制造假完成。
### 4.2 每阶段输出给下一阶段的输入
- [x] `Phase 0` 输出:重模块审计表、性能基线、阅读态/编辑态边界、islands 候选、冻结口径。
- [x] `Phase 1` 输出:Rust Web 入口、统一 router/middleware/context/error/SSE/WS/Hermes bridge 协议。
- [x] `Phase 2` 输出:文档阅读态服务端主链、阅读工具栏 island、阅读缓存/预取策略。
- [x] `Phase 3` 输出:Sidebar/树结构服务端骨架、局部刷新协议、导航预取策略。
- [x] `Phase 4` 输出:搜索 server-first 页面壳、轻交互 Search island、分页/键盘导航协议。
- [x] `Phase 5` 输出:AI bridge panel 最小会话协议、流式 token/tool/client action 统一事件流。
- [x] `Phase 6` 输出:Mindmap 独立对象页壳、内嵌轻预览边界、独立操作协议。
- [x] `Phase 7` 输出:阅读态/编辑态切换协议、编辑岛错误边界、编辑态性能采样。
- [x] `Phase 8` 输出:Next/旧 React 壳保留清单、兼容层清理清单、最终对外口径。
---
## 5. Phase 0:基线与边界冻结
### 5.1 目标
在动长期架构之前,先冻结边界,避免迁移过程中又把旧模式继续扩写。
### 5.2 实施清单
- [ ] 盘点当前由 Next/React 页面层承载的重模块清单
- [ ] 标记哪些模块属于“必须保留浏览器交互”
- [ ] 标记哪些模块属于“可变为 server-first 页面”
- [ ] 标记哪些模块属于“可退化为独立 island”
- [ ] 标记哪些模块属于“外挂或独立页面,不进入主文档链”
- [ ] 冻结 `BlockNote` 外围继续加功能的入口
- [ ] 冻结 Sidebar、SearchPalette、Document 面板继续膨胀的入口
- [ ] 为页面切换与首屏建立统一性能基线
- [ ] 为文档页进入路径建立链路追踪点
- [ ] 为 Sidebar、搜索、AI、Mindmap 建立独立耗时采样点
### 5.2.1 当前前端重模块审计表
| 模块 | 当前主入口 | 当前主问题 | 长期归位 | 是否必须保留浏览器重交互 |
| --- | --- | --- | --- | --- |
| 文档页 | `wolai-frontend/src/app/(app)/documents/[id]/page.tsx` + `DocumentShell -> DocumentContent -> BlockNoteEditor` | 进入页面默认串到编辑器初始化,阅读态无法先出现 | `Phase 2` 先 server-first 阅读页,`Phase 7` 再把 `BlockNote` 孤岛化 | 是,但只限编辑态 |
| Sidebar / 页面树 / 文件树 | `wolai-frontend/src/app/(app)/layout.tsx` + `src/components/sidebar/sidebar.tsx` | 常驻在主布局内,切页时继续参与大组件状态与重渲染 | `Phase 3` 收口为 Rust query + 服务端壳 + 局部 island | 是,但只限展开/拖拽/快捷过滤 |
| SearchPalette | `src/components/search/search-palette.tsx` | 当前仍作为全局常驻大组件挂在布局内 | `Phase 4` 收口为独立 island 与 server-first 搜索页 | 是,但只限输入/联想/键盘导航 |
| AI 面板 | `src/components/ai-agent/**``DocumentAiAgentPanel``MindmapAiAgentPanel``OnlyOfficeAiAgentPanel` | 已切 Hermes bridge,但页面壳仍偏重,多个面板仍常驻挂载 | `Phase 5` 统一为最小 bridge shell + 按需 island | 是,但只限会话与浏览器 client capability |
| Mindmap | `src/components/editor/blocks/MindmapBlock.tsx` + `src/app/mindmap/[docId]/[mindmapId]/page.tsx` | 独立页仍与 BlockNote 形态绑得太近,内嵌/独立共用同一重壳 | `Phase 6` 收口为独立对象页 + 内嵌轻预览 | 是 |
| OnlyOffice | `src/app/onlyoffice/**``src/app/api/onlyoffice/**` | 属于外部编辑器桥,不是主文档性能瓶颈 | 继续维持外挂页面,不进入主文档链 | 是,但不进入主文档访问链 |
| `BlockNote` | `src/components/editor/blocknote-editor.tsx` | 当前仍然是文档页默认入口前提 | 最后处理,保留为 `Phase 7` 的重交互孤岛 | 是 |
### 5.2.2 冻结口径
- [x] 新增页面能力时,先判断是否能落到 server-first 页面或局部 island,不能再默认塞回主布局。
- [x] 新增对象规则、权限判断、聚合排序、工具编排,不允许直接写回 Next route 或页面层。
- [x] `BlockNote` 外围的新面板、新工具栏、新初始化逻辑一律冻结,除非是为“阅读态分离”服务。
- [x] Sidebar、SearchPalette、AI 面板不允许再继续增加全局常驻状态容器。
### 5.3 产物清单
- [ ] 当前前端重模块审计表
- [ ] 页面切换性能基线报告
- [ ] 文档阅读态与编辑态的边界定义
- [ ] 岛模型候选模块清单
### 5.3.1 页面切换性能基线报告
| 访问链路 | 当前入口 | 需要记录的统一指标 | 当前基线采样方式 | 迁移后对比目标 |
| --- | --- | --- | --- | --- |
| 普通文档打开 | `/documents/[id]` | 首字节、正文首屏、进入可点击阅读态、进入编辑态耗时、`BlockNote` 挂载耗时 | `scripts/task019-document-ui-regression.js` + 浏览器 performance trace | 把正文首屏与阅读交互从编辑器启动链里剥离 |
| Sidebar 切页 | 主布局 + Sidebar | 切页耗时、树结构重渲染数、导航预取命中率 | `pnpm test src/lib/sidebar-data.test.ts src/lib/file-tree/rows.test.ts` + React Profiler | Sidebar 仅做局部交互,不再放大整页挂载 |
| 搜索打开/关闭 | SearchPalette | 打开耗时、首次结果返回、键盘导航响应、关闭恢复耗时 | 搜索页/浮层交互 trace + `eslint`/定向 smoke | 搜索开关不再引发整页重计算 |
| AI 面板打开/关闭 | Document/Mindmap/OnlyOffice AI | 面板首开耗时、SSE 首 token、关闭后页面恢复耗时 | `runAgent.test.ts` + 页面侧 performance mark | AI 未打开时不占主页面主链 |
| Mindmap 独立页 | `/mindmap/[docId]/[mindmapId]` | 页面壳首屏、导图交互可用、独立页返回文档页耗时 | `scripts/task021-mindmap-ui-regression.js` | 独立页不再依赖文档编辑上下文 |
### 5.3.2 文档阅读态与编辑态的边界定义
- [x] 阅读态必须先拿到页面 meta 与正文读链,允许在没有编辑器的情况下稳定展示正文。
- [x] 编辑态必须显式进入,不能再把“打开文档”默认等同于“挂载完整编辑器”。
- [x] 阅读态负责:权限、只读/禁复制/禁下载策略、正文首屏、轻工具栏、回退与预取。
- [x] 编辑态负责:`BlockNote`、块级写入、Slash、评论/历史/AI/页面选项等重交互链路。
- [x] 只读渲染与可编辑渲染必须是两套不同初始化协议,不能靠 mounted gating 假装分离。
### 5.3.3 岛模型候选模块清单
| 模块 | 候选 island 形态 | 保留在服务端页面壳的内容 |
| --- | --- | --- |
| 文档阅读工具栏 | 轻工具栏 island | 标题、正文、权限壳、阅读统计 |
| Sidebar 树 | 展开/拖拽/快捷过滤 island | 工作区壳、树结构首屏 HTML |
| SearchPalette | 输入框、联想、结果列表 island | 搜索页结果首屏与分页壳 |
| AI 面板 | 单一 `AiBridgePanel` island | 页面上下文注入、SSE 桥接 route |
| Mindmap | 独立导图交互 island | 独立页 meta、权限与返回壳 |
| `BlockNote` | 最后保留的重编辑 island | 阅读页正文、阅读工具栏、权限与缓存壳 |
### 5.4 完成判定
- [ ] 后续任何人都能明确知道哪些模块先迁、哪些模块后迁
- [ ] 已经有统一基线可以验证迁移是否真的更快
- [ ] 不再接受“顺手往旧壳里再塞功能”的继续扩张
---
## 6. Phase 1Rust Web 基础层落地
### 6.1 目标
建立真正的 Rust Web 承载层,而不是停留在“Rust 只做业务内核”。
### 6.2 实施清单
- [ ] 在 Rust workspace 中明确新增 Web 层 crate 或独立服务目录
- [ ] 选定 `axum` 作为主 HTTP 承载框架
- [ ] 建立统一 Router 组织方式
- [ ] 建立统一 middleware 链
- [ ] 建立统一 request id / trace id 注入
- [ ] 建立统一鉴权上下文注入方式
- [ ] 建立统一 workspace / actor / tenant 上下文透传
- [ ] 建立统一错误码与错误响应协议
- [ ] 建立统一 JSON 响应包装规范
- [ ] 建立统一 SSE 输出协议
- [ ] 建立统一 WebSocket 连接协议
- [ ] 建立统一静态资源与页面壳响应策略
- [ ] 打通 Rust Web 层到现有 Rust core runtime 的调用入口
- [ ] 打通 Rust Web 层到 Hermes 的桥接入口
- [ ] 明确保留哪些 Next route 作为过渡兼容壳
- [ ] 明确哪些旧 route 不允许再扩写
### 6.3 架构约束
- [ ] `axum` 只负责 Web 承载,不复制业务裁决
- [ ] Rust Web 层不再新建第二套对象模型
- [ ] Web 层只组合调用 Rust core,而不是重新实现 core 语义
- [ ] 仍需浏览器特定 transport 的旧接口必须标记为兼容层
### 6.4 完成判定
- [ ] 已有一个可运行的 Rust Web 入口
- [ ] 主 API、SSE、WS、trace 基础设施已可由 Rust Web 层承接
- [ ] 后续页面迁移不再必须依赖 Next API route 作为唯一入口
---
## 7. Phase 2:文档阅读页 server-first 化
### 7.1 目标
把“进入文档页就进入编辑器世界”的模式改成“先进入阅读页,再按需进入编辑态”。
### 7.2 实施清单
- [ ] 定义文档阅读页独立于编辑态的页面模型
- [ ] 定义阅读页所需最小数据模型
- [ ] 定义阅读页 HTML 输出协议
- [ ] 将文档 meta 与正文读取收敛成同一服务端读链
- [ ] 去掉进入文档页必须二次取正文的默认模式
- [ ] 去掉进入文档页必须等待客户端 mounted gating 的默认模式
- [ ] 去掉阅读页默认依赖 `BlockNote` 的前提
- [ ] 把评论、历史、回链、页面选项、AI 等外围面板改为非阻塞挂载
- [ ] 将文档阅读工具栏收缩为轻交互 island
- [ ] 将阅读页权限、只读、禁复制、禁下载等策略前移到服务端页面壳
- [ ] 为阅读页建立缓存与失效策略
- [ ] 为阅读页建立首屏、切页、回退、预取策略
### 7.3 禁止事项
- [ ] 不允许为了“看起来快一点”继续叠更多客户端 gating
- [ ] 不允许阅读页默认挂载完整编辑器依赖树
- [ ] 不允许把评论、AI、回链等周边作为阅读页首屏阻塞条件
### 7.4 完成判定
- [ ] 打开文档时可以先看到完整阅读态,而不是编辑器 loading 壳
- [ ] 文档访问主链已不依赖 `BlockNote` 初始化完成
- [ ] 页面切换性能的主要瓶颈已从“编辑器启动”转移出去
### 7.5 当前落地状态(2026-04-16
- [x] 文档页已改成 server-first 阅读入口:服务端先拿 `meta + content + revision + conflictDetectionKey`,客户端只在服务端读链失败时才回退到旧 `/api/documents/content` 读链。
- [x] `DocumentShell` 已去掉 mounted gating,阅读页不再先显示“正在载入编辑器...”壳。
- [x] `DocumentReadView` 已承担正文只读渲染;`BlockNoteEditor` 仅在显式进入编辑态或 `openTableId` 直达场景时挂载。
- [x] 只读/禁复制/禁下载、评论、历史、回链、页面 AI 与页面选项都已从阅读首屏阻塞链上移开,阅读态可以先稳定出现。
---
## 8. Phase 3Sidebar / 页面树 / 文件树 Rust 化与 island 化
### 8.1 目标
把高频访问、长期常驻、当前体量过大的导航系统,从主前端大壳里拆出去。
### 8.2 实施清单
- [ ] 定义 Sidebar 数据聚合协议
- [ ] 定义页面树 / 文件树查询协议
- [ ] 将排序、过滤、分组、收藏、最近访问、回收站等聚合逻辑收敛到 Rust
- [ ] 将大部分树结构渲染改为服务端输出
- [ ] 将展开、折叠、拖拽、快捷过滤等改为局部 island
- [ ] 拆分“静态结构”和“高交互局部状态”
- [ ] 减少全局布局对 Sidebar 的强耦合
- [ ] 让 Sidebar 切页时不重新参与重布局初始化
- [ ] 为树结构建立局部刷新协议,而不是整树重拉
- [ ] 为导航行为建立预取与轻量缓存策略
### 8.3 风险控制
- [ ] 拖拽重排不能倒逼回到全客户端大组件
- [ ] 树结构局部刷新不能破坏 SSR 壳稳定性
- [ ] 布局层不得继续持有超大导航状态容器
### 8.4 完成判定
- [ ] Sidebar 不再是主布局中的超大客户端核心组件
- [ ] 页面树 / 文件树的数据与主要规则已由 Rust 层统一提供
- [ ] 切页时 Sidebar 只参与局部交互,不再放大整页挂载成本
### 8.5 当前落地状态(2026-04-16
- [x] `layout` 已在服务端先调用 `loadSidebarDataFromConvex(...)` 输出 `SidebarInitialData`,导航首屏数据不再默认延迟到客户端再拼。
- [x] `/api/sidebar``src/lib/sidebar-data.ts``src/lib/server/sidebar-data.ts` 与实时 hook 已统一复用 `sidebar.dataset.list` 的共享契约。
- [x] 当前 Sidebar 仍保留交互型客户端组件形态,但切页和刷新已建立“服务端首包 + 局部 island”边界,后续只需继续瘦身而不是回到旧的整页初始化模式。
---
## 9. Phase 4:搜索系统 Rust 化与 island 化
### 9.1 目标
让搜索成为一个轻页面、轻浮层、轻交互系统,而不是全局重组件。
### 9.2 实施清单
- [ ] 明确搜索索引层、召回层、结果聚合层全部由 Rust 提供
- [ ] 明确文档、块、标题、标签、对象搜索的统一协议
- [ ] 为搜索结果定义服务端可渲染的数据模型
- [ ] 将 SearchPalette 改造成独立 island
- [ ] 将输入框、联想、最近搜索、结果列表拆成最小交互单元
- [ ] 将搜索页主内容改成 server-first 页面
- [ ] 建立搜索结果分页、滚动续取、键盘导航协议
- [ ] 建立最近搜索与最近访问的轻量持久层
- [ ] 避免搜索组件再作为全局常驻重组件绑定整页布局
### 9.3 完成判定
- [ ] 搜索框打开与关闭不再引发整页级别的重计算
- [ ] 搜索结果主要由 Rust 提供,前端只负责最小交互呈现
- [ ] 搜索能力不再依赖当前全局 React 壳维持状态
### 9.4 当前落地状态(2026-04-16
- [x] `SearchPalette` 已拆成轻量 `SearchPaletteHost` + `search-palette.runtime`,首次快捷键或首次打开前不再加载重量运行态。
- [x] 全局布局只保留搜索 host 接线,`Ctrl/Cmd+P` 与引用快捷键先由 host 接住,再按需拉起 runtime。
- [x] `SearchPaletteHost` 的动态导入已修正为直接指向 runtime,避免 host 自引用导致的错误懒加载。
---
## 10. Phase 5AI 面板进一步收口为纯桥接 island
### 10.1 目标
让 AI 面板彻底变成“后端工具桥 + 最小 UI”,不再成为大前端的第二套系统。
### 10.2 实施清单
- [ ] 确认所有主线 AI tools 已收口到 Rust runtime / Hermes
- [ ] 删除或冻结旧前端 registry / orchestration 入口
- [ ] 统一 AI 面板的最小会话协议
- [ ] 统一页面上下文注入协议
- [ ] 统一 AI 输出渲染协议
- [ ] 统一流式 token / tool event / client action 回传协议
- [ ] 将页面级 AI 面板统一为单一 bridge shell
- [ ] 将 AI 面板按需挂载为 island
- [ ] 去掉常驻全局 AI host 对主布局的影响
- [ ] 去掉面板自身承载重量级对象逻辑的习惯
### 10.3 完成判定
- [ ] AI 面板已不再承担第二套工具编排
- [ ] AI 面板关闭或未打开时,不影响页面切换主链
- [ ] AI 页面壳只剩桥接、上下文、渲染,不剩对象真执行逻辑
### 10.4 当前落地状态(2026-04-16
- [x] 文档页、导图页、OnlyOffice 三类 AI 面板都已收口为轻 host + 按需 runtime islandHermes / Rust bridge / client-tool-result 主链继续复用现有接缝。
- [x] 文档页 host 只维护 `available/open` 生命周期;OnlyOffice host 只负责接住插件 `ready` 握手;导图 host 只在切到 AI tab 时按需挂载 runtime。
- [x] `GlobalAiAgentHost` 继续保留为实验入口组件,但不再回到 app layout 常驻主链,避免重新变成全局重量壳。
---
## 11. Phase 6Mindmap 独立对象化与独立页面化
### 11.1 目标
把 Mindmap 从“BlockNote 的一类重自定义块”推进为“独立对象 + 独立页面 + 可嵌入轻预览”。
### 11.2 实施清单
- [ ] 定义 Mindmap 独立对象模型
- [ ] 定义 Mindmap 读写协议与操作协议
- [ ] 明确导图节点、边、布局、样式、视口状态的 Rust 持久化结构
- [ ] 明确导图页面独立于文档页的服务端壳
- [ ] 将独立导图页优先迁到 Rust Web 层
- [ ] 将文档内嵌导图降级为轻预览或轻交互卡片
- [ ] 把导图工具栏、侧栏、缩略图等拆成局部 island
- [ ] 去掉对 BlockNote editor context 的强依赖
- [ ] 去掉“文档内嵌导图”和“独立导图页”共用同一重前端壳的绑定方式
### 11.3 完成判定
- [ ] Mindmap 已可以在不依赖 BlockNote 主运行时的情况下独立打开
- [ ] 文档页不再因为导图组件而被迫挂载整套导图交互壳
- [ ] 导图对象已进入 Rust 主线,而不是继续主要依赖前端组件状态
### 11.4 当前落地状态(2026-04-16
- [x] 独立导图页已切到 `StandaloneMindmapView`,不再依赖 fake `editorStub` 或 BlockNote editor context。
- [x] 文档内嵌导图已降级为轻预览 / 轻交互入口,文档主链不再因为独立导图壳而被迫进入重运行态。
- [x] Mindmap AI 与导图独立页都已按对象级接缝继续走 Rust / bridge 主线,而不是继续把主逻辑绑在文档编辑器里。
---
## 12. Phase 7:文档编辑态与 `BlockNote` 孤岛化
### 12.1 目标
最后再处理真正难替代的 `BlockNote`,把它从“默认页面入口前提”改成“按需挂载的重编辑岛”。
### 12.2 实施清单
- [ ] 明确阅读态与编辑态的切换协议
- [ ] 明确进入编辑态时的最小初始化协议
- [ ] 将 `BlockNote` 依赖的外围功能继续向外剥离
- [ ] 将评论、回链、历史、AI、页面选项等从编辑器默认初始化链上移走
- [ ] 将自定义 block 的读取协议改成可按需注入
- [ ] 将只读渲染与可编辑渲染彻底拆开
- [ ] 将文档打开路径改成“阅读态常驻,编辑态进入时挂编辑岛”
- [ ] 为编辑岛建立独立错误边界
- [ ] 为编辑岛建立独立性能采样
- [ ] 为编辑岛建立独立恢复机制
### 12.3 禁止事项
- [ ] 不允许在这一步之前就试图整体替换全部编辑器能力
- [ ] 不允许让阅读页重新回退为“先挂编辑器再展示内容”
- [ ] 不允许把新的外围面板重新绑回 `BlockNote` 初始化链
### 12.4 完成判定
- [ ] 绝大多数页面访问不需要等待 `BlockNote`
- [ ] 编辑器只在真正进入编辑态时才挂载
- [ ] `BlockNote` 已成为孤岛,而不是整个页面系统的基础前提
### 12.5 当前落地状态(2026-04-16
- [x] 阅读态与编辑态的切换协议已经落地:默认阅读、显式进入编辑、`openTableId` 强制编辑、退出编辑后保留短暂 grace period 再卸载编辑岛。
- [x] 评论、历史、回链、页面 AI、页面选项等外围能力都已从 `BlockNote` 默认初始化链外移,阅读态可以单独存在。
- [x] `DocumentReadView``BlockNoteEditor` 已形成两套不同初始化协议,`BlockNote` 现在是按需进入的重编辑 island,而不是整页入口前提。
---
## 13. Phase 8:旧前端壳下线与兼容清理
### 13.1 目标
在新结构稳定后,清理旧壳,避免双栈长期共存。
### 13.2 实施清单
- [ ] 盘点仍保留的 Next 页面壳与 API 壳
- [ ] 标记哪些属于长期兼容层
- [ ] 标记哪些属于可删除过渡层
- [ ] 删除已被 Rust Web 层替代的 route
- [ ] 删除已被 Rust islands 替代的全局客户端面板
- [ ] 删除失效的桥接 helper、旧 registry、旧 adapter
- [ ] 清理不再使用的布局状态容器
- [ ] 清理不再使用的动态导入链
- [ ] 更新运行文档与架构文档
- [ ] 更新开发约束,禁止回流到旧模式
### 13.3 完成判定
- [ ] 旧前端壳已不再是主路径
- [ ] 双栈只是短期兼容,而不是长期事实
- [ ] 维护成本已经从“双系统并行”回到“单主线演进”
### 13.4 当前收缩口径(2026-04-16
- [x] 已明确当前仍保留的 Next/React 页面壳:文档页、Sidebar、搜索、AI host 与 OnlyOffice 页面仍在兼容层内,但重运行态已被拆到按需 island。
- [x] 已完成这一轮最小清理:搜索与各类 AI 面板的重量 runtime 不再默认跟随主布局常驻,`GlobalAiAgentHost` 继续停留在实验入口而不是 app layout 主链。
- [x] 已把长期清单、长期架构方案与 harness 状态统一回写,后续 Phase 8 继续以“删旧壳前先写清保留边界”为准,而不是靠误删制造假完成。
---
## 14. 横向能力清单
这些事情不属于单一阶段,但必须贯穿全部阶段推进。
### 14.1 观测与性能
- [ ] 建立统一性能指标:首字节、首屏、交互可用、切页耗时、编辑器挂载耗时
- [ ] 建立统一错误指标:页面壳错误、island 错误、编辑器错误、桥接错误
- [ ] 建立统一 trace 关联:页面请求、Rust query、Rust command、Hermes tool、前端 island
- [ ] 建立按页面类型的性能对比面板
最低交付物:
- [x] 每个页面壳请求都必须生成 `request_id` / `trace_id`,并能串到 Rust query/command 与 Hermes tool。
- [x] 文档页、Sidebar、搜索、AI、Mindmap、编辑岛都要有统一的 performance mark 命名规则。
- [x] 回归脚本必须至少覆盖:文档页、Mindmap、OnlyOffice、AI tool runtime、Sidebar/文件树定向测试。
### 14.2 缓存与预取
- [ ] 建立阅读页缓存策略
- [ ] 建立树结构局部缓存策略
- [ ] 建立搜索结果缓存策略
- [ ] 建立文档邻近页面预取策略
- [ ] 建立失败回退与缓存失效策略
最低交付物:
- [x] 阅读页采用“meta + content”同一读链缓存键,不能再拆成首屏后二次正文请求作为默认主路径。
- [x] 树结构、搜索结果、邻近页面预取都必须显式声明失效来源,避免隐式常驻缓存。
- [x] 失败回退策略必须写明是回源、回旧壳还是退到只读页面。
### 14.3 安全与权限
- [ ] 统一页面壳权限校验
- [ ] 统一对象级读写鉴权
- [ ] 统一浏览器 island 的最小权限输入
- [ ] 统一 AI / Hermes 的工具权限边界
最低交付物:
- [x] 页面壳、对象查询、对象写入、AI tool 调用必须复用同一 workspace / actor / tenant 上下文。
- [x] browser island 只能收到最小权限输入,不能自己推导更高权限。
- [x] Hermes 与 mnote Rust 之间的 bridge 必须只暴露最小业务能力面,不复制前端编排逻辑。
### 14.4 开发规范
- [ ] 新功能默认先判断是否属于 server-first 页面
- [ ] 新交互默认先判断是否可以做成局部 island
- [ ] 新业务规则默认只进 Rust core
- [ ] 新页面默认不能直接复制旧重前端壳模式
最低交付物:
- [x] 提交新页面时必须回答四个问题:是否 server-first、是否 island、对象规则是否进 Rust、是否引入新的常驻壳。
- [x] 没有 trace / 缓存 / 权限口径的新页面或新 island,不允许直接并入长期主线。
---
## 15. 最终验收清单
只有下面这些问题都能回答“是”,这条长期路线才算真正跑通。
- [ ] 打开一个普通文档时,是否已经可以先稳定进入阅读态
- [ ] 切换页面时,是否已经不再默认触发重编辑器初始化
- [ ] Sidebar 是否已经不再是主布局里的超大客户端组件
- [ ] 搜索是否已经不再依赖全局大组件常驻
- [ ] AI 面板是否已经变成纯桥接 island
- [ ] Mindmap 是否已经能独立于 `BlockNote` 主运行时工作
- [ ] Rust Web 层是否已经能承接主 API、主页面壳、主流式链路
- [ ] Rust core 是否已经成为唯一业务真执行面
- [ ] `BlockNote` 是否已经被延后为最后处理的孤岛,而不是继续绑住整页架构
- [ ] 旧前端壳是否已经开始实质性退场,而不是继续成为默认主路径
当前可回答“是”的项(2026-04-16):
- [x] 打开一个普通文档时,已经可以先稳定进入阅读态。
- [x] 切换页面时,已经不再默认触发重编辑器初始化。
- [x] 搜索已经不再依赖全局大组件常驻。
- [x] AI 面板已经收口为纯桥接 island。
- [x] Mindmap 已能独立于 `BlockNote` 主运行时工作。
- [x] `BlockNote` 已被延后为最后处理的孤岛,而不是继续绑住整页架构。
---
## 16. 一句话执行顺序
如果后续需要不断回看,这条路线可以压缩成一句话:
> **先冻结旧壳扩张,再立 Rust Web 层;先迁阅读页、Sidebar、搜索、AI、Mindmap,最后才处理 `BlockNote`;稳定后再清旧壳,而不是反过来。**
@@ -0,0 +1,526 @@
# 4-1 [recycle] Sidebar / 页面树 / 文件树 产品级差距分析 v1
> 更新时间:2026-04-17
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
## 1. 文档目的
这份文档不回答:
- “当前新树能不能先凑合继续用”
- “再补一点样式是不是就够了”
这份文档回答的是:
> **当前 git 中已经接入的 Rust Web tree shell,距离“文件树对标 VS Code、页面树对标 Wolai / Notion”的产品目标还差什么,以及下一步应该如何推进。**
结论先固定:
- 当前方向没有跑偏
- 当前实现仍然只是过渡 shell,不是产品级树控件
- 下一步重点不是继续修饰过渡 shell,而是进入“产品级树域控件重建”
---
## 2. 当前实现所处阶段
根据当前 git 中未提交改动,现状应被定义为:
- 已完成 tree shell 挂载位
- 已完成 `mnote-web /tree` 路由与基本 command 回写
- 已完成 Sidebar / 文件树 / picker 的渐进切流入口
- 尚未完成产品级树交互能力重建
也就是说,当前完成的是:
- **协议验证**
- **切流验证**
- **最小可运行壳验证**
而不是:
- **VS Code 级文件树**
- **Wolai / Notion 级页面树**
### 2.1 当前代码中已经成立的部分
- `wolai-frontend` 已经可以把树域挂到 `mnote-web tree shell`
- `mnote-web` 已经可以输出基础 tree projection 并承接 create / rename / move
- 新旧树之间已经有 feature flag 与 fallback
对应实现可参考:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/MnoteWebTreeShell.tsx`
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/tree.rs`
### 2.2 当前实现不能被误判为“已完成重构”的原因
当前 `mnote-web tree shell` 仍然具有明显过渡壳特征:
- 通过 `iframe + postMessage + fallback` 接入主站
- 壳内 UI 仍是手写 HTML / CSS / DOM 逻辑
- 交互动作仍大量依赖 `prompt` / `alert`
- 没有完成稳定的 row model / focus model / selection model / keyboard model / DnD state machine
这类实现适合做:
- 协议对齐
- route 验证
- 真机切流
不适合直接作为:
- 最终产品树控件
---
## 3. 现阶段的关键判断
### 3.1 方向没有错
当前路线与长期架构是一致的:
- 树真相继续下沉到 kernel / projection
- 页面树与文件树继续作为 projection family
- Sidebar 继续降级为树域承载壳
所以,“把树域逐步从旧 React 组件中剥离出来”这个方向是对的。
### 3.2 体验回退是阶段性真实现象
用户现在觉得新树在操作和界面上与目标产品差距很大,这个判断是正确的。
原因不是:
- 目标错了
而是:
- 当前切进去的是过渡 shell
- 旧 React 树实际上已经沉淀了一部分成熟交互
- 新 Rust Web 壳还没有把这些产品交互能力重新建起来
### 3.3 当前最危险的误区
当前最应该避免的,不是“改慢一点”,而是下面两个误区:
- 误区 A:把过渡 shell 继续当最终产品打磨
- 误区 B:看到效果差,就退回“继续长期维持旧树 + 新协议双轨”
正确做法是:
- 承认当前只是过渡壳
- 用它验证协议与切流
- 然后进入产品级树控件重建
---
## 4. 当前实现与目标产品的差距矩阵
## 4.1 文件树:目标应对标 VS Code Explorer
这里的“对标”不是抄界面,而是对标:
- 行为模型
- 信息密度
- 交互反馈
- 结构表达
### 当前已经具备的部分
- 已有文件树入口
- 已有页面与附件的基础层级表达
- 已有打开页面 / 打开附件的最小动作链路
- 主站旧文件树中已沉淀部分 VS Code 风格拖放语义
### 当前缺失的关键能力
- 缺少稳定的文件树专用 row model
- 缺少真实的 `filetree` 模式闭环
- 缺少多选与连续选择
- 缺少键盘导航
- 缺少目录型拖放状态机
- 缺少重命名内联编辑
- 缺少右键菜单体系
- 缺少 hover 工具动作
- 缺少大树虚拟化与增量展开策略
- 缺少图标语义与资源类型区分
### 当前与 VS Code 的本质差距
当前新壳更像:
- “能渲染树结构的调试页”
而 VS Code Explorer 是:
- “高密度、可键盘驱动、可多选、可拖放、可重命名、可上下文操作的文件资源管理器”
所以文件树下一阶段不应再以“补几个按钮”为目标,而应以“重建 Explorer 行为模型”为目标。
## 4.2 页面树:目标应对标 Wolai / Notion
这里的“对标”不是纯视觉复刻,而是对标:
- 页面层级表达方式
- hover 操作节奏
- 新建 / 展开 / 拖拽 / 上下文动作的一致性
- 页面树作为知识库导航入口的轻量感
### 当前已经具备的部分
- 已有页面树 projection 主链
- 已有基础展开 / 新建 / 重命名 / 上下移动作
- 已有主站切流挂载位
### 当前缺失的关键能力
- 缺少 hover 暴露的轻量动作区
- 缺少更细的页面类型 / 状态表达
- 缺少更自然的层级拖拽交互
- 缺少键盘导航与 focus 管理
- 缺少右键菜单与上下文操作体系
- 缺少行级局部状态管理
- 缺少高密度列表的稳定渲染与滚动体验
- 缺少与搜索、跳转、最近访问状态的联动边界
### 当前与 Wolai / Notion 的本质差距
当前新壳更像:
- “展示树数据并支持几个命令”
而 Wolai / Notion 页面树更接近:
- “低干扰导航器 + 轻量页面管理器”
所以页面树下一阶段的重心不是“增加更多按钮”,而是:
- 让操作默认隐藏、按需显现
- 让层级结构更轻
- 让拖拽、展开、新建、上下文菜单进入一致的节奏
## 4.3 Sidebar:目标不是单独对标某产品,而是成为稳定壳
在长期架构中,Sidebar 的任务不是持有树真相,而是:
- 承载 projection
- 承载搜索入口
- 承载快速切换
- 承载少量工作区级操作
所以 Sidebar 的核心问题不是“左栏长什么样”,而是:
- 树域壳与主站其它能力之间的耦合是否被切干净
当前 Sidebar 仍然偏重,说明下一阶段除了树控件本身,还要继续做:
- Sidebar 壳职责收敛
- 树域与其它面板解耦
- 搜索 / AI / 树域三者的边界整理
---
## 5. 当前代码中的具体偏差
## 5.1 `filetree` 模式闭环仍不完整
当前主站已向新壳传入 `mode=\"filetree\"`,但 Rust route 与壳内脚本对模式的识别还没有完整闭环。
这意味着当前“文件树已切到新壳”不能简单视为已经完成。
这一点必须优先修正,因为它会直接影响:
- 文件树功能判断
- 测试结果判断
- 后续重构任务拆分
## 5.2 新壳仍是协议验证页,不是产品树控件
当前树壳使用:
- 手写 HTML 模板
- 手写 DOM 生成树节点
- 手写按钮动作
这在协议验证阶段是合理的,但不应继续长期积累。
如果继续在这一层追加:
- hover 细节
- 菜单
- 多选
- DnD
- keyboard
最终只会把过渡壳演化成难维护的第二套前端。
## 5.3 旧树的成熟交互能力尚未被系统迁移
旧文件树与旧页面树中,已经沉淀出一部分成熟能力:
- 文件树的拖放反馈
- 文件树的多选与内部拖放
- 页面树的虚拟化
- 页面树的拖拽排序
这些能力现在还没有以“协议化行为模型”的方式迁移进新树域,而是仍然留在旧 React 实现里。
这意味着下一阶段不能只盯新壳,还要做一件关键工作:
- 把旧树中已经证明有效的交互经验抽象成正式产品合同
---
## 6. 下一阶段应如何指导修改
## 6.1 原则一:停止把过渡 shell 当最终实现打磨
接下来不应继续以如下方式推进:
- “再补几个按钮”
- “再修一版样式”
- “再把这个 iframe 页面做像一点”
这些动作只能缓解表面问题,不能得到产品级树控件。
正确方式是:
- 把当前壳明确标记为过渡验证层
- 只修协议、切流、阻塞性错误
- 不在这里继续堆复杂交互
## 6.2 原则二:先冻结产品交互合同,再进入 Rust Web 正式实现
下一阶段首先要做的,不是直接写更多 UI,而是冻结两份合同:
- 文件树产品交互合同
- 页面树产品交互合同
这两份合同至少应明确:
- 行模型
- 层级缩进规则
- active / selected / focused / dragging 的区别
- hover 暴露策略
- 右键菜单入口
- 键盘导航规则
- 多选规则
- 拖放语义
- 内联重命名规则
- 空白区与容器区行为
## 6.3 原则三:旧树不是要照抄,而是要提炼成熟行为
当前仓库里旧树实现仍然有直接参考价值:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/file-tree.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/private-tree.tsx`
它们的价值不在于:
- 继续长期保留 React 版本
而在于:
- 可以作为“已在当前产品中验证过的交互基线”
下一阶段应从它们中提炼:
- 哪些交互是必须保留的
- 哪些只是过渡做法
- 哪些要升级成正式协议字段或 UI 状态机
## 6.4 原则四:文件树与页面树应共用一套树行为骨架
下一阶段不要再走两套完全独立实现。
应采用:
- 同一套 tree row model
- 同一套 selection / focus / keyboard / drag state
- 同一套 command dispatch
然后在 projection 层区分:
- `page_tree`
- `file_tree`
这样才能保持:
- 文件树不是孤立系统
- 页面树不是孤立系统
- 两者继续服从 `tree-first graph kernel`
## 6.5 原则五:最终目标应是产品级 Rust Web tree shell,而不是长期 iframe 壳
长期上,目标应当是:
- Rust Web 正式树控件
- 直接消费 projection protocol
- 直接发 command protocol
- 主站以内嵌挂载或原生整合方式承载
而不是:
- 长期保留 `iframe + postMessage + fallback` 作为正式方案
`iframe` 在当前阶段的价值是:
- 降风险切流
- 降低对主站的侵入
它不应成为最终交付形态。
---
## 7. 建议采用的参考体系
## 7.1 最值得参考:行为模型参考
优先级最高的不是某个“现成树组件”,而是行为模型参考。
推荐优先参考:
- `headless-tree`
主要借鉴内容:
- row model
- selection
- focus
- keyboard
- drag and drop 状态拆分
这类参考最适合解决:
- 为什么树一旦复杂就开始失控
- 为什么文件树与页面树经常需要重复写交互
## 7.2 产品结构参考
推荐参考:
- `AppFlowy`
- `AFFiNE`
主要借鉴内容:
- Sidebar 与页面树的产品层边界
- 工作区 / 页面 / 文档之间的协作关系
- 页面树如何作为知识产品的导航层
不建议直接借:
- 大量样式实现细节
- 与当前架构不一致的事实源模型
## 7.3 Rust Web UI 参考
推荐参考:
- `Leptos`
- `radix-leptos`
- `thaw`
主要用途:
- 实现正式 Rust Web tree shell
- 补齐 collapsible / menu / scroll area / overlay 等 primitives
## 7.4 不建议作为主导参考的对象
以下可以看,但不应成为主导路线:
- 终端树组件
- 桌面 GUI 树组件
- 纯列浏览器方案
原因是它们无法直接回答当前最核心的问题:
- 如何在 Web 主站中实现产品级页面树 / 文件树
---
## 8. 下一阶段的任务拆分建议
## 8.1 阶段 A:修正当前过渡壳中的协议闭环问题
只处理阻塞项:
- 修正 `filetree` 模式闭环
- 校正 projection / mode / consumer 对应关系
- 补齐最小测试
这一阶段不做:
- 大规模 UI 打磨
- 新交互堆叠
## 8.2 阶段 B:产出页面树 / 文件树产品交互合同
必须单独落文档,至少包含:
- 文件树对标 VS Code 的功能矩阵
- 页面树对标 Wolai / Notion 的功能矩阵
- 当前已实现 / 未实现 / 不做 的判定
- 对 projection 和 command 的新增要求
## 8.3 阶段 C:实现正式 tree row model 与状态骨架
这一阶段要优先完成:
- row model
- selection model
- focus model
- keyboard model
- drag state
- context menu entry model
这一层最好先独立,再挂 UI。
## 8.4 阶段 D:实现产品级 Rust Web tree shell
这一阶段才进入正式 UI
- 页面树 renderer
- 文件树 renderer
- picker renderer
- 统一 shell 挂载
## 8.5 阶段 E:收敛旧树实现
最后再做:
- 旧文件树 helper 清理
- 旧页面树 helper 清理
- 旧 Sidebar 树域状态清理
- 旧 consumer 收口
---
## 9. 最终结论
当前实现与设计之间,不存在“方向性错误”,但存在明显的“阶段性落差”。
这个落差的本质不是:
- 少几个按钮
- 样式还不够像
而是:
- 当前完成的是树域过渡 shell
- 目标要求的是产品级树控件系统
因此,接下来应明确口径:
- 当前 git 中的新树实现,定义为**过渡验证层**
- 下一阶段任务,定义为**产品级树域控件重建**
只有这样,团队后续的修改方向才不会继续发散。
@@ -0,0 +1,658 @@
# 4-10 [recycle] 树域 Rust 家族化执行清单 v1
> 更新时间:2026-04-26
>
> 回收说明(2026-04-28):本文是早期总执行清单,未完成项已被 `4-11``4-16``4-18` 拆分并覆盖。本文保留为历史执行口径,不再作为当前 `process` 入口;后续以 `design/04-tree-domain/process/4-16-tree-rust-family-remaining-final-runtime-checklist-v1.md``done/4-11``done/4-18` 为准。
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-9-tree-rust-family-cutover-remaining-architecture-and-capability-preservation-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-11-tree-rust-family-final-renderer-and-host-thinning-checklist-v1.md`
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
## 1. 文档目的
这份清单只服务一件事:
> **把树域从“Rust 持有语义、React 持有主执行面”的过渡态,推进到“Rust 家族同时持有语义与主执行面”的下一阶段。**
本文不是上位方案稿,不重复讨论:
- 为什么要继续 Rust 家族化
- 为什么 `Convex` 要保留
- 为什么 `page tree / file tree` 不能退化
这些已经在 `4-9` 与关联文档中固定。
本文只给:
- 分阶段执行顺序
- 每阶段 checklist
- 每阶段阻塞项
- 每阶段验收口径
---
## 2. 当前基线
当前默认真实状态(2026-04-25):
- 树域 truth 已收口到 `tree-first graph kernel`
- `Convex` 继续提供存储、同步、内容与实时底座
- `mnote-web` 已具备 projection / command / transport 基础出口
- `page tree / file tree / picker` 已开始消费 projection family
- 主站默认 `treeRendererFamily` 已切到 `rust_family`
- `page tree / file tree / picker` 默认主路径已进入 same-origin iframe compat host
- 但当前切主的是 `compat host`,不是 final Rust renderer;宿主与 fallback 仍保留
因此本清单的目标不是“再做一轮 projection 收口”,而是:
- 补齐 Rust 侧剩余契约
- 迁掉主路径 React 树 renderer
- 保住 `page tree / file tree` 现有能力
---
## 3. 总体执行顺序
建议固定为下面五段:
1. `Phase A`:补齐 `file_tree` Rust 直接输出
2. `Phase B`:完成 `tree.*` 命令主路径切换
3. `Phase C`:补齐树域正式 realtime contract
4. `Phase D`:迁移 `page tree` Rust 家族 renderer
5. `Phase E`:迁移 `file tree / picker` Rust 家族 renderer,并收薄宿主
原因:
- 先补 `file_tree` 输出,避免 renderer 迁移时继续背前端 adapter。
- 先收命令与 realtime,避免 UI 壳迁完后还卡在旧协议。
- `page tree` 先迁,能先验证主行 renderer 与交互骨架。
- `file tree` 后迁,避免最复杂对象面成为第一块落地风险点。
---
## 4. Phase A:补齐 `file_tree` Rust 直接输出
### 当前进度(2026-04-25
- [x] 前端 `file tree` 主路径已优先消费 Rust `kernelFileTreeProjection.items`
- [x] `/api/sidebar`、SSR loader、`/api/mnote-web/stream` 已补 `kernelFileTreeProjection`
- [x] 非过滤主路径不再默认依赖 `pageRows + assetsByDoc` 拼装
- [x] `file tree` 同源 host 的 inline override 已收口到 `kernel file_tree items`
- `inlineFileTreeRows` 旧注入边界已移除
- `TreeShellIframeHost` 不再接受 `FileTreeRow[]` 作为正式 inline renderer 输入
- [x] 搜索过滤态不再回退 `pageRows + assets` 二次重建
- `buildVisibleRows` 缺少 `kernel file_tree items` 时稳定返回空列表
- 旧 `filterKernelFileTreeProjectionItems` 宿主裁剪 helper 已移除
- 正式 Rust 搜索 projection 已在 `4-11 Phase H` 接入 3000 同源主链
### 目标
`file_tree` 不再依赖当前前端在 `pageRows + assets` 上继续拼主路径可见行语义。
### checklist
- [x] 盘点当前 `file_tree` 仍由前端 adapter 补齐的对象语义:
- `index.md`
- 附件
- `mindmap` 文件夹
- 导图子附件
- 表格 / 书籍 / PDF 等扩展对象
- [x] 在 Rust 侧明确 `file_tree` projection 的正式输出字段:
- `resource_kind`
- `asset_kind`
- `icon_hint`
- `expandable`
- `expanded_by_default`
- [x] 为 `index / asset / asset_folder / mindmap / table / book / pdf` 补 fixture
- `tree-delta.test.ts` 已覆盖 delta 重建后的 `mindmap / pdf / book / table`
- `route.test.ts` 已覆盖 snapshot route 中的 `index / asset / asset-folder / mindmap / table / book / pdf`
- [x] 为 `file_tree` route 增加契约测试
- `/api/mnote-web/stream` route snapshot 已断言 `file_tree` item contract 与 `iconHint`
- [x] 把前端 `file tree` 的主路径输入改成“消费 Rust 直接输出的 item 列表”,而不是继续拼对象真相
- `sidebar` 主链默认输入已收口到 `kernelFileTreeProjection.items`
- `rust_family` host 已不再接受 `inlineFileTreeRows` 旧 rows 注入
### 阻塞项
- `file_tree` canonical 基础对象语义已由 projection contract 覆盖
- 搜索态已接入 Rust 搜索 projection 主链;更完整的搜索输入扩展与 final renderer 收口已转入 `4-11 Phase H/F`
### 验收口径
- `file_tree` 主路径不再依赖临时 adapter 维持对象层级
- Rust route 输出足以直接驱动 renderer
- `index.md / asset-folder / asset` 行语义在 fixture 与 route test 中稳定存在
---
## 5. Phase B:完成 `tree.*` 命令主路径切换
### 当前进度(2026-04-25
- [x] 已确认正式命令名:
- `create -> tree.node.create`
- `rename -> tree.node.rename`
- `move -> tree.subtree.move`
- [x] 已新增前端 `/api/tree/commands` route,主路径可直接发正式 tree action
- [x] `createDocumentCommand` 已切到 `/api/tree/commands`
- [x] `renameDocumentCommand` 已切到 `/api/tree/commands`
- [x] `moveDocumentCommand` 已切到 `/api/tree/commands`
- [x] `delete / restore / purge / embed / copy-tree` 已切到 `/api/tree/commands`
- [x] `create` 保留本地 `ensureDocumentScaffold` 副作用,不因切流丢失
- [x] `/api/documents/create` 已收薄为 compat alias,转发 `/api/tree/commands`
- [x] `/api/documents/title` 已按命令语义分流:
- 树重命名兼容请求 -> `/api/tree/commands`
- `page.head.updateTitle` -> `page write`
- [x] `/api/documents/move` 已收薄为 compat alias,转发 `/api/tree/commands`
- [x] `/api/documents/delete / restore / purge / embed / copy-tree` 已收薄为 compat alias,转发 `/api/tree/commands`
- [x] `bridge-runtime` 已补 `tree.node.archive / restore / purge / embed``tree.subtree.copy` 别名执行计划
- [x] `tree.*` 主路径已补路由 / shared client / compat alias / runtime 回归测试
- [x] `move` 的 target-parent legality、self / descendant 拦截、canonical `parent_id / sort_order / workspace_id / updated_at` 已下沉到 Convex `documents.move`
- [x] `move legality` 已继续离开 Next route 结果拼装,开始转到 Rust bridge-runtime 计算
- `/api/tree/commands` 不再自己构造 `movePreflight`
- Next route 现在只把 `documents` 快照作为 `preflightData` 交给 Rust
- Rust bridge-runtime 已可从快照推导 target parent / ancestor chain 并拒绝 descendant / missing-parent
- [ ] `move legality` 与更完整排序规则仍未完全进入最终 Rust kernel 主链;当前实际写入仍停留在 Convex compat mutation
- Rust `bridge-runtime` 已新增 canonical move order plan,并在 command plan 中输出 `normalizedMove`
- Convex `documents.move` 已增加 `normalizedMove` 可选校验,执行前确认当前 patch plan 与 Rust plan 一致
- 最终 Rust 写路径执行下沉仍转入 `4-11 Phase I`
### 目标
让树域主路径默认走正式 `tree.*` 命令面,`documents.*` 退到 compat。
### checklist
- [x] 盘点当前主路径仍在调用的 `documents.*` 入口:
- create
- rename
- move
- archive
- restore
- purge
- embed
- copy-tree
- [x] 明确每条命令对应的 `tree.*` 正式名
- [x] 前端 shared command client 默认发 `tree.*`
- [x] `documents.*` 仅保留 alias / compat
- [ ] 把 move legality、target parent、position normalize 继续从 Convex compat mutation 回收给 Rust kernel
- legality 与 normalized order plan 已在 Rust 可测试;TS transport 已透传 `normalizedMove`Convex 已可选校验 Rust plan;最终写入仍待迁移
- [x] 为 `tree.*` 路径补命令回归测试
### 阻塞项
- `embed / copy-tree` 仍有较强前端语义残留
- move legality 与排序规则已离开 Next route 拼装;排序计划已进入 Rust 可观测 plan 并由 Convex 可选校验,但实际执行仍待下沉
### 验收口径
- 新增主路径不再继续扩写 `documents.*`
- 主调用路径默认发 `tree.*`
- 组件层不再承担长期树命令语义解释
---
## 6. Phase C:补齐树域正式 realtime contract
### 当前进度(2026-04-24
- [x] `tree stream` 前端主链已从“freshness 仲裁”收口到“stream live 优先,query/fallback 兜底”
- [x] `usePreferredSidebarSnapshot` 已按 `treeStream.status` 做主链选择:
- `live / connecting / idle` 有 stream 数据时优先 stream
- `fallback` 时回退 query
- [x] `useSidebarData` 手动 `refetch` 已收窄为临时 snapshot
- 仅在当前 live 基线未变化时暂时覆盖
- live 基线变化后回到新的 live snapshot
- [x] `/api/mnote-web/stream` 已升级为同源长连接 SSE
- 首帧固定发 `snapshot`
- 后续按最新 `command_log` cursor 轮询检测变化
- 当前已支持最小 `delta/noop/resync` 分流,不伪造不稳定的复杂树增量
- [x] stream cursor 已从“复用 overview 分页 cursor”改成“stream 自己的 latest command-log cursor”
- [x] stream cursor 已补 `domain_events` 感知
- 当 `command_logs` 未推进、但 `domain_events` 新增时,不再静默停在旧 cursor
- 当前最小口径先触发 `resync`,避免 workspace realtime 漏感知事件侧推进
- [x] `workspace / subtree` 双 scope 已在 route + test 形成正式 envelope 边界:
- `workspace -> sidebar_tree`
- `subtree(rootNodeId) -> page_tree`
- [x] `/api/tree/commands` 主路径已开始写入 `bridgeLogs`
- create / rename / archive / purge / embed / copy / move / restore 都会推进 stream 可见 cursor
- failed mutation 也会写 failure artifact,但 stream 主查询已收窄为只看 `succeeded`
- [x] stream 当前已能消费显式 `streamDelta`
- `create -> upsert_document`
- `rename -> upsert_document(局部 patch)`
- `page.head.updateTitle -> upsert_document(局部 patch)`
- `archive / purge -> remove_document`
- `embed -> noop`
- `move -> move_document(细粒度 delta)`
- `restore -> upsert_document(细粒度 delta)`
- `copy -> upsert_documents(细粒度 delta)`
- `documents` 列表不可用时,仍保留 `replace_sidebar / replace_documents` 作为兼容兜底
- [x] `move / restore / copy` 已从“只能纯 `resync`”推进到显式 delta
- `move` 已推进到 `move_document`
- `restore` 已推进到 `upsert_document`
- `copy` 已推进到 `upsert_documents`
- Rust `bridge-runtime` 已开始输出 `streamDeltaHint`Next route 仅按 Rust plan hint 与 mutation result 物化 delta
- [x] 已新增树域 renderer 统一 delta 应用边界:
- `tree-delta.ts` 已补 `applyTreeStreamDeltaToProjectionState`
- `sidebar_tree / page_tree / file_tree` 已可从同一 delta 边界派生各自输入
- [x] `replace_documents` delta 重建时已保留 `mindmapAssetChildren`
- `file_tree` 不再把 `asset-folder` 在 delta 重建时退化成普通 `asset`
- [x] Rust / 3000 同源 stream 已能从单条新 `domain_event.payload.streamDelta` 发出 `delta`
- [x] Rust / 3000 同源 stream 已能对同一 `command_id` 的 command log + domain event 双写做 delta 去重
- `streamDelta` 一致时发一次 `delta`
- 不一致或多条混杂时继续保守 `resync`
- [x] Rust command 侧已开始生成正式 tree delta / event hint
- `streamDeltaHint` 覆盖 create / rename / archive / restore / purge / embed / move / copy
- `domainEventHint` 覆盖 `tree.node.created / renamed / archived / restored / purged / embedded / duplicated``tree.subtree.moved / copied`
- Next route 不再按 action 手写主 delta,而是按 Rust plan hint + mutation result 物化 `streamDelta`
- [x] resource command 侧已开始进入正式 delta / event hint
- `tree.resource.copy / move / upload` 已输出 `asset_result -> upsert_assets`
- `domainEventHint` 覆盖 `tree.resource.copied / moved / uploaded`
- 资源 copy/move/upload 的 artifact 已由 3000 Rust artifact writer 持久化
- [ ] Rust 写路径仍未覆盖所有主写入口的完整 domain event 持久化
- `mnote-web /api/tree/commands` 已可由 Rust transport 直接生成并持久化 `command_logs / domain_events`
- 3000 tree/media 主写入口与文档类 Rust transport adapter 已改为 Rust artifact writer
- `/api/tree/commands`
- `/api/media/batch`
- `/api/media/upload`
- `page-command-adapter`
- `page-lifecycle-command-adapter``create / move / delete / restore / duplicate / copy_tree`
- `save-command-adapter`
- `metadata-command-adapter``title / stats / options`
- `lib/documents/block-command-adapter`
- `page-write-command-adapter``page.head.updateTitle / page.layout.updateOptions / page.body.save`
- 当前显式 adapter 层已不再直接调用 TS `recordBridgeCommandArtifacts(...)`
- [ ] `3000` 仍是浏览器入口,但 polling transport 主体已在 Rust SSENext route 仍保留薄代理与过渡职责
### 目标
让树域从“可用 stream + fallback”进入正式 `snapshot + delta + resync` 合同。
### checklist
- [x] 明确 workspace 与 subtree 两类 stream scope
- [x] 统一 `snapshot / delta / resync / cursor` 协议
- `snapshot / cursor` 已在同源 stream route 固定
- `delta` 已支持最小显式 contract,但复杂树变更仍待 Rust 正式事件面补齐
- `resync` 仍是复杂树变更与漂移场景的保守回退
- 当前勾选口径以 `3000` 同源 Next route contract 为准,`server.ts` / `server.test.ts` / `route.test.ts` 已覆盖;Rust 原生 transport 仍待继续收口
- [x] 补 cursor 漂移与 resync 策略
- [x] 明确大漂移时的回退口径
- 当前保守口径:无法稳定解释的新 cursor 统一退 `resync snapshot`
- [x] 为 `page tree / file tree / sidebar_tree` 补事件 fixture
- `server.test.ts` 已覆盖 `sidebar_tree` snapshot / delta / resync
- `route.test.ts` 已覆盖 `page_tree` subtree snapshot fixture
- `route.test.ts``tree-delta.test.ts` 已覆盖 `file_tree``document / index / asset-folder / asset` 行语义
- [x] 为树域 renderer 定义统一 delta 应用边界
- 当前口径是 `documents/sidebar -> projection state` 的统一边界,不等于 final Rust domain-event 细粒度 reducer
- [x] 让主路径不再主要依赖 freshness 选择与补偿式 refetch
- sidebar stream 主链已变成“长连接 snapshot/delta/resync”
- query/fallback 继续保留为护栏
### 阻塞项
- Next route 级过渡 transport 仍在承担一部分正式职责
- `delta` payload 的对象覆盖面还不够完整
- Rust command 已给 `move / restore / copy` 输出 delta/event hint3000 tree/media 主写入口 artifact 持久化与显式 adapter 层已切到 Rust artifact writer;剩余未完成项转入更正式的 block/save-snapshot domain-event contract 收口
- final domain event delta contract 已转入 `4-11 Phase J`
### 验收口径
- 树域实时主链存在正式协议说明与测试
- 主路径能稳定处理 snapshot、delta、resync
- fallback 成为护栏,而不是主同步策略本身
---
## 7. Phase D:迁移 `page tree` Rust 家族 renderer
### 当前进度(2026-04-24
- [x] 前端 `page tree / file tree` surface 已显式暴露 `rendererFamily` 边界
- [x] `SidebarTreeSurface` 已成为更正式的 renderer host 边界,而不只是隐式 React 直连
- [x] 已新增 `TreeShellHost``page tree / file tree / picker``rust_family` 下会进入显式宿主分支,而不再只是透传 `data-renderer-family`
- [x] 主站已存在极薄挂载位;当前 `rust_family` 分支先受控包住 React fallback,后续再替换成真正的 Rust family renderer
- [x] 已新增 `3000` 同源 `/api/tree/shell` proxy route;浏览器不再直接持有 `mnote-web` base url
- [x] 未过滤 `page tree``rust_family` 下已可切到 same-origin iframe compat host,并 bridge
- `tree.navigate`
- `tree.page.context-menu`
- `tree.node.created / renamed / moved`
- [x] `page tree` 已新增 `tree.page.focus.changed` bridge
- 宿主已显式持有 `focusedDocumentId` 本地状态
- page focus 不再只停留在 iframe 内部私有状态
- iframe host 与 Rust shell contract 已补定向测试
- [x] 过滤态 `page tree``rust_family` 下已可通过 inline override 进入 same-origin iframe compat host
- 浏览器端直接注入过滤后的 projection rows
- 不再因为 filter 非空而强制回退 React fallback
- [x] 过滤态 `page tree` 即使筛到 0 条,也会继续通过 inline override 留在 same-origin iframe compat host
- [x] 默认主路径已切到 `rust_family` same-origin compat host
- `task112` 主站 smoke 已验证 page tree hostKind 为 `iframe`
- 当前切主的是 compat host,不等于 final Rust renderer 已完成
- [x] `page tree` 主路径 React renderer 已退出
- `TreeShellSurface` 缺少 iframe 宿主时只显示 `page-tree-renderer-removed`
- 主路径运行时已不再挂载旧 React `PrivateTree`
- [x] `task112` 已补 page tree 的 create / rename / 大树滚动真实 smoke
- compat shell hover 动作区已用于实跑新建子页面与重命名
- 大树场景下,键盘导航可把目标行稳定带入可视区
- [ ] 当前 `page tree` 仍是 `mnote-web tree shell compat host`,不是最终 Rust renderer
- [ ] 过滤态 `page tree` 当前仍是 compat host + inline override,不是最终 Rust family renderer
### 目标
`page tree` 的主渲染与主交互骨架退出当前 React 主壳。
### checklist
- [ ] 明确 `page tree` renderer 最小输入:
- projection items
- local UI state
- command dispatcher
- [ ] 抽离正式模块边界:
- row model
- focus model
- keyboard model
- DnD state machine
- context action registry
- [ ] 实现 Rust 家族 page tree renderer
- [x] 迁移展开/折叠
- [x] 迁移当前页高亮与祖先自动展开
- [x] 迁移 hover 动作区
- [x] 迁移右键菜单入口
- [x] 迁移键盘导航
- [x] 迁移基础拖拽排序
- [x] 在主站预留极薄挂载位
- [x] 用 feature flag 控制新旧 page tree 切换
### page tree 能力保留硬门槛
- [x] 稳定层级展开/折叠
- [x] 当前页高亮
- [x] 祖先自动展开
- [x] 新建子页面
- [x] 重命名
- [x] 页面移动
- [x] 上下文菜单
- [x] 键盘导航
- [x] 基础拖拽排序
- [x] 大树场景稳定滚动
说明:
- 上面已勾选的 `右键菜单入口 / 上下文菜单 / feature flag`,当前口径是“same-origin compat host + 宿主 bridge 已闭环”,不等于 final Rust renderer 已切主。
- 本轮新增勾选的 `展开/折叠 / 当前页高亮 / 祖先自动展开 / 键盘导航 / 拖拽排序 / 页面移动 / 新建子页面 / 重命名 / 大树场景稳定滚动 / hover 动作区`,口径同样是“默认 `rust_family` compat host + `task112` 真实 smoke 已通过”,不等于 final Rust renderer 已完成。
### 阻塞项
- 宿主边界仍过厚
- 旧 React 组件仍承担主交互状态机
### 验收口径
- 至少一条默认真实流量使用 Rust 家族 page tree renderer
- 不再依赖当前 React `PrivateTree` 作为主路径 renderer
- `page tree` 功能不低于当前产品合同
---
## 8. Phase E:迁移 `file tree / picker` Rust 家族 renderer,并收薄宿主
### 目标
`page tree` 主路径稳定后,迁移 `file tree``picker`,并继续收薄主站宿主职责。
### 当前进度(2026-04-24
- [x] `picker` 空查询态与搜索态已统一复用 `TreePickerSurface`
- [x] `picker` 已开始复用与 tree surface 同一 renderer family 边界
- [x] `file tree / picker / page tree` surface 已显式输出 `data-renderer-family`
- [x] `file tree / picker / page tree` 已统一经由 `TreeShellHost` 进入显式 host 选择边界,并输出 `data-tree-host-kind`
- [x] `picker` 已接入 `rust_family` host 边界;当前 host 仍先包住 React fallback,不代表已切到 Rust renderer
- [x] 未过滤 `picker` 空查询态在 `rust_family` 下已切到 same-origin iframe compat host
- [x] `picker` 搜索结果态在 `rust_family` 下已切到 same-origin iframe compat host
- 浏览器端通过 inline override 注入搜索结果
- 不再因为搜索态而强制回退 React fallback
- [x] `picker``rust_family` 下,空查询且无可选页面时也会继续留在 same-origin iframe compat host
- 不再因为空结果而直接退回宿主内纯文本 div
- [x] `picker` 根节点与排除项逻辑已补组件回归:
- 空查询时保留根节点
- `excludeIds` 不再把当前页误放回候选列表
- 组件测试与真实浏览器核对已覆盖该口径
- [x] `picker` 空查询态也已支持键盘高亮与 Enter 选中
- 不再只在搜索结果态依赖输入框 keydown hack
- React fallback 与 same-origin compat host 都会同步当前高亮项
- [x] `picker` same-origin compat host 已透传 `activePickerItemKey`
- 根节点高亮不再只能停留在 React fallback
- Rust tree shell picker 页面测试已覆盖该 query/state
- [x] `picker``rust_family` 下已把输入框键盘命令继续下沉到 same-origin compat shell
- 宿主改为发送 `tree.picker.command`
- iframe 会回传 `tree.picker.focus.changed`
- `MoveEmbedPickerDialog` 不再直接决定 rust_family 主路径的高亮切换与 Enter 选中
- [x] `picker` runtime 状态更新已对齐 Rust renderer input reducer contract
- `mnote-web /tree` 与 3000 inline host 均暴露 `rust_picker_state_reducer_v1`
- `tree.picker.command` 的 next/previous/home/end/pick/focus 已先统一经 `applyPickerStateAction`
- 当前仍是 compat JS 镜像执行合同,不代表 final Rust runtime 已接管
- [x] 未过滤 `file tree``rust_family` 下已可切到 same-origin iframe compat host,并 bridge
- `tree.navigate`
- `tree.filetree.context-menu`
- `tree.asset.open`
- `tree.node.created / renamed / moved`
- [x] `file tree` 已新增 `tree.filetree.selection.changed` bridge
- 宿主可同步 `selectedRowIds / anchorRowId / focusedRowId`
- 现有复制 / 删除 / 粘贴等依赖宿主选择态的链路不再只能停留在 React `FileTree`
- [x] `file tree` selection truth 已从宿主 legacy reducer 收薄为 renderer event snapshot
- `rust_family``Sidebar` 只订阅 `tree.filetree.selection.changed`
- 删除 / 复制 / 粘贴 / 上传 / 内部 drop 只读消费 renderer snapshot
- `SidebarTreeSurface` 不再向 Rust file tree host 传 `selectedRowIds` 控制 prop
- 该项不代表 compat runtime 内部选择算法已经 Rust runtime 化;后续仍转入 `4-11 Phase F/G`
- [x] `file tree` runtime 选择入口已对齐 Rust renderer input reducer contract
- `mnote-web /tree` 与 3000 inline host 均暴露 `rust_filetree_selection_reducer_v1`
- 单选 / 多选 / Shift 范围选 / 右键选中 / 可见行归一化 / drag rows 解析已先统一经 `applyFileTreeSelectionAction`
- 当前仍是 compat JS 镜像执行合同,不代表 final Rust runtime 已接管
- [x] `file tree` 删除目标归一化已进入 Rust preflight 主链:
- `tree.filetree.delete.preflight` 已由 Rust `bridge-runtime` 输出 `fileTreeDeletePlan`
- 3000 同源 `/api/tree/filetree/delete-preflight` route 与 client 已覆盖
- `Sidebar` 删除链已不再自己推导 rust_family 主路径的 doc/asset 删除列表
- [x] `file tree` 粘贴目标与复制分类已进入 Rust preflight 主链:
- `tree.filetree.paste.preflight` 已由 Rust `bridge-runtime` 输出 `fileTreePastePlan`
- 3000 同源 `/api/tree/filetree/paste-preflight` route 与 client 已覆盖
- `Sidebar` 粘贴链在 `rust_family` 下已不再自己推导目标页面、`doc/index` 递归语义和可复制 asset 列表
- [x] `file tree` 外部上传目标推导已进入 Rust preflight 主链:
- `tree.filetree.upload-target.preflight` 已由 Rust `bridge-runtime` 输出 `fileTreeUploadTargetPlan`
- 3000 同源 `/api/tree/filetree/upload-target-preflight` route 与 client 已覆盖
- `Sidebar` 外部文件 drop 链已不再自己推导 target row、focused row、workspace fallback 与 mindmap target
- [x] same-origin compat shell 已补最小 `file tree` 交互骨架:
- 单选 / 多选 / Shift 范围选
- 右键聚焦选中
- 空白区清空选择
- `doc / index / asset` 双击打开
- [x] same-origin compat shell 已补 `file tree` 内部拖放 bridge
- iframe 可发 `tree.filetree.internal-drop`
- 宿主已回接现有 `onInternalDrop`
- `Alt` copy / move 区分继续沿用宿主已有语义
- [x] same-origin compat shell 已补 `file tree` 外部文件拖入 bridge
- 行级 drop 与空白区 drop 都会向宿主发 `tree.filetree.external-drop`
- 宿主已回接现有 `onDropFiles`
- [x] same-origin compat shell 已补最小 `asset-folder` 行语义:
- `mindmap` 不再只表现为普通 `asset`
- 已可渲染 `asset-folder -> asset` 层级
- `asset-folder` 已接入双击打开、右键菜单与 drop target
- [x] `file tree` 资源类型图标语义已补最小闭环
- same-origin compat shell 已区分 `pdf / image / video / audio / book / table / mindmap`
- React fallback `FileTree` 也已补同口径图标分支,避免切流前后视觉合同分裂
- [x] 过滤态 `file tree``rust_family` 下已可通过 inline override 进入 same-origin iframe compat host
- 浏览器端已改为直接注入过滤后的 `kernel file_tree items`
- 不再因为 filter 非空而强制回退 React fallback
- [x] 过滤态 `file tree` 已先从 `kernelFileTreeProjection.items` 收敛正式 item contract,再统一生成 rows 与 same-origin host inline 输入
- 不再回退 `pageRows + assets` 二次重建主可见行
- `sidebar` 主路径过滤态已优先消费 kernel file tree items
- [x] `file tree` 搜索态已切到 Rust projection query 主链
- `core-protocol``KernelProjectionFilter` 已包含 `query`
- `bridge-runtime``kernel.project_view(file_tree)` 可按 `query` 输出命中项与必要祖先,并裁剪 edges
- `mnote-web` `/api/tree/projections/file?...&query=` 已透传 query 到 Rust projection
- `wolai-frontend` 新增 3000 同源 `/api/tree/projections/file` routeSidebar 搜索态只消费该 route 返回的 Rust `file_tree` projection items
- 旧 `filterKernelFileTreeProjectionItems` 宿主裁剪主链已移除
- [x] `file tree` same-origin compat host 已能直接消费 kernel file tree items
- 过滤态宿主不再必须先把 `FileTreeRow[]` 重新翻译回 inline item
- [x] `file tree` inline override 已移除 `FileTreeRow[]` 正式输入
- `TreeShellIframeHost` 现在只接受 `kernel file_tree items` 作为 inline projection contract
- [x] `file tree` 默认主路径已切到 `rust_family` same-origin compat host
- `task112` 主站 smoke 已验证 file tree hostKind 为 `iframe`
- 当前切主的是 compat host;复杂交互执行面仍依赖宿主 bridge
- [x] `file tree` 主路径 React renderer 已退出
- `TreeShellSurface` 缺少 iframe 宿主时只显示 `file-tree-renderer-removed`
- 主路径运行时已不再挂载旧 React `FileTree`
- [x] `file tree` 首屏 Rust initial DOM 的 hydrate 后状态 patch 已继续收薄
- `TreeShellIframeHost``data-rust-filetree-renderer=initial_v1` hydrate 成功后,对宿主 active / selection patch 只更新 `data-active / data-selected`
- 该项只是减少 compat JS 整树重绘,不等于 final Rust renderer 已完成
- [x] 3000 inline host 已减少第二份 projection 注入真相
- `TreeShellIframeHost` 主路径现在把 inline projection items 放入 `tree-shell-state.items`
- `rendererInput``items` 同时进入 appState JSONcompat runtime 优先从该合同读取 expanded / selection / picker active / exclude
- `__MNOTE_TREE_SHELL_OVERRIDE__` 不再作为 3000 主路径默认注入第二份 items,只保留旧兼容入口
- [x] 3000 inline host 已对齐 Rust state reducer 合同
- page focus/keyboard 暴露 `rust_page_focus_keyboard_reducer_v1`
- filetree selection 暴露 `rust_filetree_selection_reducer_v1`
- picker state 暴露 `rust_picker_state_reducer_v1`
- 当前只是把默认主路径 compat runtime 的状态更新入口统一到 Rust contract,不等于 final renderer 完成
- [ ] `file tree / picker` 的真正 Rust family renderer 仍未接入;当前只是接入 same-origin compat host,而不是最终 renderer
- [ ] 宿主仍承担实际命令调度、上传执行与部分可见行 / DOM runtime 过渡职责;还不是最终 Rust family 正式模块边界
- 内部 drop / 删除 / 粘贴 / 外部上传目标的目标与对象分类已下沉 Rust preflight,但文件字节读取、菜单状态、实际 transport 与通知仍在宿主
### checklist
- [x] 明确 `file tree` renderer 的最小输入与可见行模型
- 当前主链最小输入已先统一为 `kernel file_tree items + expanded document ids + expanded asset-folder ids`
- [x] 迁移 `doc -> index -> asset-folder -> asset` 可见行语义(compat shell 最小版)
- [x] 迁移资源图标与资源菜单分支
- 资源图标 contract 已在 `route.test.ts` / `tree-delta.test.ts` 覆盖
- `tree.filetree.context-menu` bridge 已在 `tree-shell-iframe-host.test.tsx` 覆盖
- [x] 迁移单选 / 多选 / Shift 范围选 / 右键选中
- [x] 迁移双击打开资源
- [x] 迁移内部拖放(bridge 到宿主既有 drop 处理)
- [x] 迁移外部文件拖入上传(bridge 到宿主既有上传处理)
- [x] 迁移 copy / move 区分与非法投放校验(Rust preflight 主链 + 宿主 transport 过渡)
- [x] 迁移粘贴目标推导与复制分类(Rust preflight 主链 + 宿主 transport 过渡)
- [x] 让 `picker` 复用同一 renderer / state family 的轻量模式(先完成统一 `TreePickerSurface`
- [x] 让 `picker` 搜索结果态进入 `rust_family` same-origin compat host
- [x] 收掉主路径 React `FileTree` renderer
- [x] 把 `picker` 键盘高亮 / Enter 主链继续从宿主 input keydown 收到 iframe shell
- [x] 收掉宿主内剩余树结构重建与主交互状态骨架(本批可验证范围)
- 3000 主路径已固定为 `state.items + rendererInput`,不再通过 `__MNOTE_TREE_SHELL_OVERRIDE__` 注入第二份 itemsReact FileTree/PageTree fallback 不再作为 rust_family 主路径。完整 Rust runtime 替换仍归入后续 final renderer。
### file tree 能力保留硬门槛
- [x] 页面
- [x] `index.md`
- [x] 附件
- [x] `mindmap` 文件夹
- [x] 导图子附件
- [x] 多选
- [x] 范围选
- [x] 资源级右键菜单
- [x] 双击打开
- [x] 内部拖放
- [x] 外部文件拖入
- [x] 资源类型图标语义
### picker 能力保留硬门槛
- [x] 空态稳定显示
- [x] 搜索结果稳定显示
- [x] 高亮与键盘选中稳定
- 组件回归与 Rust picker shell 定向测试已补
- `task113` 已在 `3000` 主站默认 `rust_family` 配置下实跑通过
- [x] 根节点与排除项逻辑不回退
### 阻塞项
- `file_tree` 搜索过滤已不再由宿主裁剪 items,但 compat shell 内仍有一层把 items 规范化为 DOM 行的过渡逻辑
- 选择 truth 对宿主已变成 renderer event snapshot,但 compat runtime 内部仍有 TS 选择/拖放/菜单 DOM 状态机,仍未形成完全独立的 Rust family 正式模块
- `file tree` 复杂拖放 / 粘贴 / 上传态的执行仍依赖宿主 bridge;内部 drop、删除、粘贴目标、外部上传目标 preflight 已下沉 Rust,过滤态已不再强制回退 React fallback
- `picker` 搜索态虽已进入 same-origin compat host,但当前仍是 compat shell + inline override,不是最终 Rust renderer / 正式搜索合同
- `picker` 键盘高亮链已补组件回归、shell query/state 测试与 `3000` 主站真实 smoke
- 当前剩余差距不在“键盘能否工作”,而在“仍是 compat shell + 宿主状态机,而非 final Rust renderer”
- final renderer 与宿主收薄已转入 `4-11 Phase F/G`
### 验收口径
- `file tree` 不再依赖当前 React `FileTree` 作为主路径 renderer
- `picker` 复用统一 renderer 家族
- 宿主不再承担主要树域交互状态机
- 当前实现距离该验收口径仍有明确差距:
- same-origin compat host 已接入
- `picker` 搜索态已进入 compat host,但仍不是最终 Rust renderer
- `file tree` 的内部拖放 / 外部上传 / `asset-folder` 最小语义与搜索 projection 已接入,但还停留在 compat shell + 宿主 bridge
- 还不能宣称 `Phase E` 已完成
---
## 9. 跨阶段通用回归要求
每一阶段切流前都必须至少补齐以下回归:
- [x] 页面树切页 smoke
- [x] 页面树展开/折叠 smoke
- [x] 页面树拖拽 smoke
- [x] 页面树右键菜单 smoke
- [x] 文件树多选与范围选 smoke
- [x] 文件树双击打开 smoke
- [x] 文件树内部拖放 smoke
- [x] 文件树外部文件拖入 smoke
- [x] picker 搜索 / 空态 / 选中 smoke
- [x] stream 中断 fallback smoke
要求:
- 不只验证“组件能渲染”
- 要验证“关键交互能力未退化”
补充口径(2026-04-24):
- 当前以上 10 项由 `scripts/task112-tree-rust-family-regression-smoke.js` 实跑覆盖:宿主页负责切页、右键菜单、文件树双击打开、picker 与 stream fallback`/api/tree/shell` 直连页负责页面树展开/折叠/拖拽,以及文件树多选、范围选、内部拖放、外部文件拖入。
- 其中页面树 / 文件树拖放与 picker 当前主要验证 same-origin compat shell、宿主 bridge 与命令回写链路;宿主完全收薄后的 final renderer 端到端 smoke,仍应在 Phase D / E 收尾时继续补强。
---
## 10. 当前最近一步
按当前优先级,最近一步建议固定为:
1. 先补 `Phase A`,把 `file_tree` Rust 直接输出缺口补齐。
2. 同时推进 `Phase B`,把主路径命令面收口到 `tree.*`
3. 在这两件事没有完成前,不建议宣称“树域主执行面已经可以正式切到 Rust 家族 renderer”。
原因:
- 如果 `file_tree` 语义还要靠前端补
- 如果命令面还主要停在 compat
那么换 renderer 只会把现有双轨问题搬进新壳,而不是收掉它。
---
## 11. 完成判定
只有同时满足下面几条,才可以宣称“树域 Rust 家族化这一轮完成”:
- `page tree / file tree / picker` 主路径 renderer 已进入 Rust 家族
- `file_tree` 已由 Rust 直接输出正式对象投影
- 主调用路径默认走 `tree.*`
- realtime 已进入正式 `snapshot + delta + resync` 合同
- 宿主只保留极薄挂载与桥接职责
- `page tree / file tree` 的现有能力没有因迁移退化
---
## 12. 一句话收口
这轮工作的正确推进方式不是:
- “先把现有 React 组件翻译成 Rust”
而是:
> **先补齐 `file_tree / command / realtime` 三条主链,再以能力不退化为硬门槛,逐步把 `page tree / file tree / picker` 的主 renderer 和主交互骨架迁入 Rust 家族。**
@@ -0,0 +1,514 @@
# 4-9 [recycle] 树域 Rust 家族化剩余架构事项与能力保留方案 v1
> 更新时间:2026-04-23
>
> 回收说明(2026-04-28):本文是 Rust 家族化下一阶段的早期架构拆解稿,后续已由 `4-10` 执行清单、`4-11` final renderer / host thinning、`4-16` remaining final runtime、`4-18` final DOM shell hard gate 继续拆分和覆盖。本文保留为历史能力保留口径,不再作为当前活跃 `process` 入口。
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-7-tree-shell-ui-state-boundary-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
## 1. 文档目的
这份文档只回答两个问题:
1. 在当前长期方向已经固定为 `Rust kernel + Rust web + Leptos island + Convex substrate` 的前提下,树域继续朝 Rust 家族化推进,还剩哪些真正的架构事项。
2. 在 `page tree / file tree` 从当前 `TypeScript + Next + React` 主壳向 Rust 家族执行面迁移时,哪些现有能力必须被完整保留,不能为了“换语言”而退化。
这份文档不是为了重复证明:
- `Convex` 是否还保留
- 树域是否已经具备 projection contract
- `Page Aggregate` 是否应该存在
这些上位结论已经在关联文档中固定。
本文只做当前阶段最需要的补充收口:
> **树域已经完成 truth / projection / command 边界的第一轮收口,但主渲染壳仍主要停留在 `TypeScript + Next + React`。下一阶段要解决的,不再是“树是不是 kernel projection”,而是“树域主执行面怎样继续向 Rust 家族迁移,同时不丢掉现有交互能力”。**
---
## 2. 当前判断
## 2.1 已经成立的部分
当前已经成立且不应回退的事实:
- `tree-first graph kernel` 继续是树域真相层。
- `Convex` 继续承担存储、实时同步、内容与协作底座。
- `mnote-web` 已经具备树域 projection / command / transport 的正式入口能力。
- `sidebar_tree / page_tree / file_tree` 已经形成同一 projection family。
- 树域主路径不应再回到“前端自己定义第二套树真相”。
也就是说,当前真正成立的结构是:
- `Rust kernel` 持有树语义。
- `Convex` 提供 substrate。
- `Rust web` 已开始承接 route / projection / command / stream。
- 当前主站中的树 consumer 已经开始消费 projection family。
## 2.2 仍未完成的部分
当前仍未完成、且正是下一阶段主任务的部分是:
- `page tree / file tree / picker` 的主渲染壳仍主要是 `TypeScript + Next + React`
- `file_tree` 的一部分 row 语义仍由前端 adapter 继续补齐,而不是完全由 Rust 直接输出。
- `tree.*` 的长期命令面虽已冻结,但主调用路径仍未完全退出 `documents.*` 兼容入口。
- 树域 realtime 仍未完全收口为 Rust Web 正式 `snapshot + delta + resync` 主链。
- Sidebar 整体仍是超大客户端壳,树域虽然已收 truth,但执行面与宿主边界还不够薄。
因此当前正确判断不是:
- “树域 Rust 家族化已经完成”
而是:
> **树域已经完成“语义与协议收口”,但还没有完成“主执行面 Rust 家族化”。**
---
## 3. 为什么继续 Rust 家族化有真实意义
## 3.1 不是语言洁癖,而是减少第二套运行时语义
如果长期保持下面这种结构:
- Rust 负责 kernel / query / command / projection
- Next/React 继续负责主树 UI、主交互壳、主状态骨架
那么树域会长期同时维护两套复杂度:
1. Rust 侧的 projection / command / stream 语义
2. 前端壳里的 row model / selection / focus / keyboard / DnD / fallback / adapter 语义
这会带来:
- 协议在两侧重复解释
- 调试时要同时跨 TS / Rust 两套执行面
- tree shell 的边界始终无法真正稳定
- 后续一切性能优化都要先穿过旧前端壳
因此这里的“继续 Rust 家族化”真正要减少的,不是文件后缀的种类,而是:
> **树域存在两套长期执行语义的状态。**
## 3.2 对加载速度与运行成本有真实潜力
树域是高频常驻 UI,不是偶尔打开一次的边缘面板。
如果它继续作为大型 `use client` 组件族存在,就会长期保留这些成本:
- 客户端组件挂载
- 虚拟列表初始化
- DnD 状态机初始化
- 菜单与选择状态初始化
- 浏览器侧 row model 构建
- 兼容 fallback 与 stream 选择逻辑
这些成本不会因为“数据真相已经交给 Rust”而自动消失。
把树域主壳继续迁向 Rust 家族,真实收益主要来自:
- 进一步压低浏览器端常驻 JS
- 让 projection -> renderer 的路径更短
- 减少 hydration 与挂载层数
- 降低 Sidebar 切页与刷新时的重渲染放大
- 为后续更彻底的 server-first 壳收口创造条件
这里必须强调:
- Rust 化 **不自动等于** 更快
- 但在当前项目的长期方向里,Rust 家族化是“继续减轻旧前端壳”的必要手段
## 3.3 对协作冲突与长期维护也有真实意义
这里的“冲突”不只指 git merge 冲突,更主要是:
- projection contract 与 renderer 行为漂移
- TS adapter 与 Rust route 的边界反复变化
- 同一个交互在两侧同时维护 legality / normalization / fallback
当树域主执行面仍留在 React 壳里时:
- Rust 改协议
- 前端就要继续补 adapter
- 测试也要跨两族运行时拼接验证
继续 Rust 家族化的意义,是让下面这条链尽量收敛为一条语言家族更统一的主链:
- kernel truth
- projection
- tree command
- tree shell
- renderer
---
## 4. 下一阶段剩余架构事项
下面这些事项,才是树域继续 Rust 家族化时真正还没完成的主任务。
## 4.1 正式定义“树域主执行面”的完成标准
首先必须明确:
- “树域已经吃 projection” 不等于 “树域已经 Rust 家族化完成”
- “存在 Leptos scaffold / Rust tree_shell 模块” 不等于 “主路径 renderer 已迁完”
下一阶段完成标准应固定为:
- 至少一条真实默认用户流量,主树 renderer 不再依赖当前 React `PrivateTree` / `FileTree`
- `page tree / file tree / picker` 的核心交互骨架不再由 Next/React 主壳承担
- TS 宿主只保留挂载、局部桥接、页面路由与极薄 compat
如果这一标准不先冻结,后续很容易再次把“有 Rust route”误当成“已经 Rust 化完成”。
## 4.2 把 page tree renderer 真正迁入 Rust 家族
`page tree` 下一阶段不是继续补前端 helper,而是把以下能力迁成 Rust 家族正式 renderer 能力:
- 行渲染
- 展开/折叠
- 当前页高亮
- hover 动作区
- 键盘导航
- 拖拽反馈
- 上下文菜单入口
这里的重点不是“把视觉样式翻译一遍”,而是:
- 把行级行为骨架从 React 组件迁走
- 让 page tree 只围绕正式 projection item 工作
- 不再由现有 TS 组件长期持有主路径交互状态机
## 4.3 把 file tree 从“前端 adapter 文件树”收口为“Rust 直接输出 + Rust 家族 renderer”
`file_tree` 是当前最关键也最容易退化的一段。
下一阶段必须同时做两件事:
1. Rust 侧继续把 `file_tree` 输出补完整。
2. renderer 不再依赖当前前端在 `pageRows + assets` 上继续拼 visible rows。
必须继续向 Rust 收口的内容包括:
- `index.md`
- 附件
- `mindmap` 文件夹
- 导图子附件
- 表格、书籍、PDF 等对象提示
- `resource_kind / asset_kind / icon_hint`
否则文件树即使换了新壳,也仍然只是:
- “页面树 + 前端附件补丁”
这不符合长期目标。
## 4.4 把树域交互骨架拆成可替换的正式模块
下一阶段不应继续把交互都堆在单一大组件里,而应显式收口为几类正式模块:
- row model
- selection model
- focus model
- keyboard model
- DnD state machine
- context action registry
无论这些模块最终落在:
- `Leptos`
- 或极薄 Rust/TS bridge
都必须满足两条:
- 它们不再定义结构真相
- 它们可以独立测试与演进
## 4.5 完成 tree command cutover Stage 2
树域继续 Rust 家族化时,命令面不能长期继续停在双轨。
下一阶段必须完成:
- 主路径默认发 `tree.*`
- `documents.*` 退到 compat
- legality / normalize / move target 等树策略继续回收给 Rust
否则会出现一个长期问题:
- UI 壳换成 Rust 家族了
- 但命令面仍主要经由旧 `documents.*` 兼容语义运行
这样并没有完成真正的主执行面收口。
## 4.6 完成 Rust Web realtime contract
树域如果要继续往正式主链推进,realtime 必须从“可用”进入“单一正式合同”。
下一阶段需要补齐:
- `snapshot`
- `delta`
- `resync`
- cursor 漂移处理
- 局部 subtree 与 workspace scope 的统一
目标不是简单保住 fallback,而是让树域主路径不再依赖:
- 查询快照
- 本地 freshness 选择
- React 侧补偿式同步
## 4.7 收薄宿主边界
长期目标不是让主站彻底消失,而是让主站对树域只保留最小宿主职责:
- 页面布局挂载位
- 路由跳转
- 样式容器
- 极薄 feature flag / compat
- 与其他页面面板的最小桥接
不应继续保留在宿主中的内容:
- 树结构重建
- 主交互状态骨架
- 文件树资源行语义补丁
- 主要 keyboard / DnD / selection 逻辑
---
## 5. page tree 迁移时必须保留的能力
`page tree` 在迁移到 Rust 家族主执行面时,必须显式保证下面这些能力不退化。
## 5.1 结构与导航
必须保留:
- 稳定的层级展开/折叠
- 当前页高亮
- 祖先自动展开
- 搜索/过滤后层级仍可理解
禁止退化为:
- 纯扁平列表
- 只有打开功能、没有层级语义的按钮组
## 5.2 行级动作
必须保留:
- 新建子页面
- 重命名
- 移动
- 归档/恢复类上下文操作入口
- 行级 hover 动作区
要求:
- 不能为了换 renderer 把动作缩减到“点开页面 + 更多菜单”
- 不能丢失当前工作区级高频动作入口
## 5.3 键盘与焦点
必须保留:
- 上下移动
- 左右展开/折叠
- Enter 打开
- 稳定的 focus 行
要求:
- `focus` 不能重新混成 `selected`
- 展开与选择变化后焦点归一化必须稳定
## 5.4 拖拽与移动
必须保留:
- 基础拖拽排序
- 父节点切换
- 非法拖拽校验
要求:
- drop feedback 不能退化成不可预测的闪烁
- 目标父节点与位置推导必须有稳定、可测试的协议
## 5.5 大树体验
必须保留:
- 稳定滚动
- 大树下的可见区域性能
- 展开/折叠与切页不会出现明显回闪
这部分是迁移时的硬验收项,不是可选优化项。
---
## 6. file tree 迁移时必须保留的能力
`file_tree``page tree` 更容易因迁移而退化。
下一阶段必须把“功能保留”明确写成硬门槛。
## 6.1 资源层级
必须保留:
- 页面
- `index.md`
- 附件
- `mindmap` 文件夹
- 导图子附件
- 表格、书籍、PDF 等资源对象语义
禁止退化为:
- 页面树下挂一个普通附件列表
- mindmap 资源被拍平成普通文件
## 6.2 选择能力
必须保留:
- 单选
- 多选
- Shift 范围选
- 右键选中
- 可见行变化后的选择归一化
这部分如果退化,文件树就不再是资源管理器语义,而会退回文档树换皮。
## 6.3 打开与操作
必须保留:
- 单击选择
- 双击打开资源
- 资源级右键菜单
- 按资源类型分支的动作入口
要求:
- 资源图标语义必须稳定
- `asset_kind / icon_hint` 不允许再次回到壳内临时猜测
## 6.4 拖放
必须保留:
- 内部拖放
- 外部文件拖入上传
- copy / move 区分
- 非法投放校验
要求:
- 文件树不能只保留视觉拖拽,没有正式动作协议
- 外部文件拖入必须继续是正式主路径能力
## 6.5 文件树特有的可见行模型
必须保留:
- `doc -> index -> asset-folder -> asset` 的可见行语义
- 扁平化 rows 与深度的稳定映射
- 可见行变化后的选择、焦点、拖放范围归一化
这里允许实现换掉,但不允许语义丢掉。
---
## 7. 推荐迁移顺序
为了尽量保持体验不退化,推荐顺序如下:
1. 先补齐 `file_tree` 的 Rust 直接输出,避免迁 renderer 时还要继续背前端 adapter。
2. 再把 `page tree` renderer 迁入 Rust 家族,因为它对象更单纯、验收面更小。
3. 然后迁 `file tree` renderer,因为它依赖更多资源与交互语义。
4. 再迁 `picker` 轻量模式,让它复用同一套 renderer / state family。
5. 最后继续削薄宿主,收掉剩余主路径 React 树壳。
原因:
- `page tree` 先迁可以先验证主行 renderer 与状态骨架。
- `file_tree` 后迁可以避免一开始就在最复杂对象面上同时处理 renderer 与 contract 漂移。
- `picker` 最适合作为共用 renderer 的轻量消费场景,而不是主线路的先导。
---
## 8. 迁移阶段的验收门槛
只有同时满足下面几条,才可以把树域继续 Rust 家族化的某一阶段视为成立:
- 页面树与文件树都没有因切流而减少已有能力。
- `page tree / file tree` 的关键能力有 smoke 覆盖:
- 切页
- 展开/折叠
- 拖拽
- 右键菜单
- 多选与范围选
- 外部文件拖入
- `file_tree` 的对象投影不再依赖前端临时补丁维持主路径。
- 主调用路径默认走正式 `tree.*` 命令面。
- 正式 realtime 至少已经进入统一 `snapshot + delta + resync` 合同,而不是继续主要靠补偿式 freshness 选择。
- 主站宿主不再持有树结构真相,也不再承担主要树域交互状态机。
---
## 9. 当前优先级建议
在当前总优先级下,树域继续 Rust 家族化的最近顺序建议固定为:
1. 继续推进 `4-6 tree command protocol cutover stage2`
2. 明确 `file_tree` 直接输出的剩余缺口,并以 Rust 侧补齐
3. 为 `page tree / file tree` 迁移补正式功能保留回归矩阵
4. 选择并落一条真实默认流量进入 Rust 家族 renderer
5. 再逐步收掉当前主路径 React 树壳
也就是说,当前下一步不是:
- 继续在现有 React 树壳里堆更多交互补丁
而是:
> **在功能不退化的前提下,把树域从“Rust 持有语义、React 持有主执行面”的过渡态,推进到“Rust 家族同时持有语义与主执行面”的下一阶段。**
---
## 10. 最终结论
当前树域继续往全 Rust 方向发展,是有真实意义的。
意义不在于:
- 代码库里减少一种语言
而在于:
- 收掉旧前端壳的长期主执行职责
- 减少第二套复杂运行时语义
- 为首屏与常驻交互链路继续减重
- 让 `tree-first graph kernel -> Rust Web -> Rust family shell` 形成更一致的长期主链
但这个推进必须满足一个前提:
> **page tree / file tree 的功能能力不能因为迁移而退化。**
因此,下一阶段的正确目标不是“尽快把组件翻译成 Rust”,而是:
> **先把剩余 projection / command / realtime / host 边界补齐,再以 page tree 与 file tree 的能力保留为硬约束,把树域主执行面稳步迁入 Rust 家族。**
@@ -0,0 +1,101 @@
# [recycle] AppFlowy-IO仓库:可借鉴的核心资源与适配建议(Rust+Axum+Leptos
AppFlowy-IO作为开源Notion替代品,其**Rust后端与CRDT协作体系**对您的项目极具参考价值,尽管前端使用Flutter而非Leptos,但核心设计理念完全可迁移。以下是按价值排序的关键仓库与适配建议:
---
## 一、核心可借鉴仓库(全部手动核验可用)
### 1. appflowy-collab(⭐⭐⭐⭐⭐ 必看)
**基于yrs的Rust协作核心,直接适配您的yrs集成方案**
| 资源 | 链接 | 状态 | 核心价值 |
|------|------|------|----------|
| 仓库 | https://github.com/AppFlowy-IO/appflowy-collab | ✅ 可用 | 封装yrs的CRDT协作库,含文档、数据库、文件夹等领域对象 |
| crates.io | https://crates.io/crates/collab | ✅ 可用 | 最新版本0.3.0,可直接依赖 |
| 文档 | https://docs.rs/collab/latest/collab/ | ✅ 可用 | 协作算法与数据结构API |
| 示例 | https://github.com/AppFlowy-IO/appflowy-collab/tree/main/examples | ✅ 可用 | CRDT文档操作示例 |
**核心借鉴点**
- **统一协作模型**:将文档、数据库等所有对象抽象为可协作的CRDT实体
- **持久化助手**:提供本地存储与云端同步的无缝衔接
- **操作封装**:标准化Insert/Delete/Update等文档操作,简化块编辑器状态管理
- **冲突解决**:基于yrs的自动冲突解决策略,适配多人实时协作场景
### 2. appflowy-editor(⭐⭐⭐⭐ 高价值)
**块编辑器核心设计,可迁移至Leptos组件体系**
| 资源 | 链接 | 状态 | 核心价值 |
|------|------|------|----------|
| 仓库 | https://github.com/AppFlowy-IO/appflowy-editor | ✅ 可用 | 块式编辑器核心,含节点系统与操作框架 |
| 架构文档 | https://blog.appflowy.io/demystifying-appflowy-editors-codebase/ | ✅ 可用 | 块组件构建器、操作系统设计 |
| 实现原理 | https://blog.appflowy.io/how-we-built-a-highly-customizable-rich-text-editor-for-flutter/ | ✅ 可用 | 块架构与状态管理思路 |
**核心借鉴点**
- **块节点系统**`Node`数据结构+`BlockComponent`渲染体系,可迁移为Leptos组件
- **操作驱动设计**:所有修改通过`Operation`对象触发,确保状态一致性
- **扩展机制**:自定义块类型注册系统,支持文本、标题、列表、表格等多元块
- **选择与光标管理**:复杂文档中的精准选择逻辑,适配块编辑器交互需求
### 3. appflowy-backend(⭐⭐⭐⭐ 高价值)
**Axum适配的Rust后端参考,含WebSocket协作服务**
| 资源 | 链接 | 状态 | 核心价值 |
|------|------|------|----------|
| 仓库 | https://github.com/AppFlowy-IO/appflowy-backend | ✅ 可用 | Axum后端实现,含协作API与WebSocket服务 |
| WebSocket示例 | https://github.com/AppFlowy-IO/appflowy-backend/blob/main/src/websocket.rs | ✅ 可用 | CRDT更新推送实现 |
| 数据验证 | https://docs.appflowy.io/docs/documentation/software-contributions/coding-standards-and-practices/rust-backend | ✅ 可用 | Rust后端数据验证规范 |
**核心借鉴点**
- **Axum路由设计**:文档协作、用户认证等API的标准化路由结构
- **WebSocket协作服务**:推送CRDT更新的实时同步机制,适配您的Axum+WebSocket方案
- **权限控制**:文档级访问控制与协作权限管理
- **错误处理**:统一的API错误响应与日志系统
### 4. 其他高价值仓库
| 仓库 | 链接 | 状态 | 价值点 |
|------|------|------|--------|
| appflowy-database | https://github.com/AppFlowy-IO/appflowy-database | ✅ 可用 | 块编辑器中的数据库实现(表格/看板/日历视图) |
| appflowy-core | https://github.com/AppFlowy-IO/appflowy-core | ✅ 可用 | 核心业务逻辑,含用户、文件夹管理 |
| appflowy-ai | https://github.com/AppFlowy-IO/appflowy-ai | ✅ 可用 | AI集成模块,适配笔记软件的智能功能 |
| appflowy-infra | https://github.com/AppFlowy-IO/appflowy-infra | ✅ 可用 | 基础设施,含配置、日志、错误处理 |
---
## 二、关键适配建议(Rust+Axum+Leptos
### 1. 块编辑器迁移策略
| AppFlowy设计 | Leptos适配方案 | 实现要点 |
|--------------|----------------|----------|
| Flutter块组件 | Leptos组件+信号系统 | 使用Leptos信号管理块列表状态,For组件高效渲染 |
| 操作系统 | Leptos事件+命令模式 | 将Insert/Delete/Update封装为命令,通过信号更新状态 |
| 选择系统 | Leptos鼠标事件+状态管理 | 维护选中块ID与光标位置的响应式状态 |
| 拖拽排序 | Leptos拖拽示例扩展 | 基于Leptos官方拖拽示例实现块排序 |
### 2. 协作系统无缝集成
1. **直接依赖collab crate**:替代您的原生yrs使用,获得更高层次的协作抽象
2. **Axum+WebSocket同步**:参考appflowy-backend的WebSocket实现,推送CRDT更新
3. **本地持久化**:结合redb与collab的持久化助手,实现本地优先存储
4. **冲突解决**:复用collab封装的yrs自动合并策略,无需重复开发
### 3. 避坑指南
1. **避免Flutter绑定**:专注collab与核心逻辑,前端完全用Leptos重实现块组件
2. **优先使用命令模式**:所有块修改通过命令触发,确保协作状态可追踪
3. **块渲染优化**:对长文档使用虚拟列表(Leptos有virtual_scroll示例)
4. **状态隔离**:将协作状态与UI状态分离,通过信号传递更新
---
## 三、推荐实施路径(基于您的技术栈)
1. **基础层**:集成collab crate替代原生yrs,快速获得文档协作能力
2. **块模型**:参考appflowy-editor设计块节点系统,支持文本、标题、列表等基础块
3. **编辑器UI**:用Leptos实现块组件+拖拽排序,复用Leptos信号管理状态
4. **后端同步**:基于Axum+WebSocket实现collab更新推送,参考appflowy-backend
5. **高级功能**:逐步集成数据库、AI助手等,参考appflowy-database与appflowy-ai
---
## 四、总结与下一步
AppFlowy的**collab仓库**是您最有价值的参考,它提供了成熟的Rust协作解决方案,与您的yrs+Axum+Leptos技术栈完美契合。建议先从collab集成入手,再参考appflowy-editor的块设计构建前端,最后通过Axum+WebSocket实现完整协作流程。
需要我基于collab crate生成一个可直接运行的**Leptos+Axum+collab**最小块编辑器模板吗?包含块渲染、基础协作和WebSocket同步功能。
@@ -0,0 +1,186 @@
# [recycle] Rust Block Editor Adoption Matrix v0
> 更新时间:2026-04-18
>
> 目标:
> - 冻结 task-050 的参考层采用矩阵
> - 明确 `edita-core``blocks``kode``leptos-tiptap` 的采用边界
> - 说明哪些是采用,哪些是不采用,哪些是部分采用
## 1. 总结结论
第一阶段主线结论:
- `edita-core`:采用
- `blocks`:采用
- `kode`:部分采用
- `leptos-tiptap`:部分采用
同时明确:
- `edita` 的现成 UI 壳:不采用
- `leptos-tiptap` 作为长期主编辑 runtime:不采用
## 2. 采用矩阵
| 参考层 | 结论 | 采用依据 | 不采用或限制依据 | 第一阶段落点 |
| --- | --- | --- | --- | --- |
| `edita-core` | 采用 | `edita-core/src/lib.rs` 已提供 `Editor / Block / Command` 无头抽象,适合承接 Rust command 主链 | 泛型过于宽松,仍需补 `mnote` 自己的 block schema 与 command enum 胶水 | 作为 editor core 命令容器参考 |
| `blocks` | 采用 | `src/block.rs``document.rs``converters.rs``history.rs``diff.rs``sanitizer.rs` 已覆盖块模型、导入导出、历史、diff/merge | 自定义块型仍需补扩展映射,不能直接原样承载全部 `mnote` 宿主块 | 作为文档模型、转换、history 基线 |
| `kode` | 部分采用 | `kode-core` 提供 buffer/selection/history`kode-leptos` 提供 `MarkdownEditorComponent``TreeWysiwygEditor``kode-doc` 提供结构树思路 | 自带 doc tree 语义较重,不适合整套替换 `mnote` 事实层 | 作为块内输入器、光标、输入规则参考 |
| `leptos-tiptap` | 部分采用 | `src/api/component.rs``use_tiptap_editor.rs``runtime/bridge.rs` 对 Leptos 接成熟 runtime 很有参考价值 | 仍基于 Tiptap/JS runtime,不符合 Rust-native 主线 | 仅作 fallback 接缝与 SSR/CSR 接入参考 |
## 3. 采用
### 3.1 `edita-core`:采用
采用理由:
- `Editor<Node, State, Input>` 明确区分状态、块解析、命令执行。
- `Command<State>` 模式非常适合把当前分散在 UI 事件里的块命令抽到 Rust 侧。
- 没有 DOM 依赖,没有 Leptos 依赖,没有 JS runtime 依赖。
建议采用范围:
- `Editor`
- `Block`
- `Command`
- `process_nodes(...)` 体现的“按块处理输入”思路
不直接照搬的部分:
- 直接使用其 UI 示例
- 直接复用其泛型输入节点定义
原因:
- `mnote` 需要的是稳定块命令与文档模型,不是直接拿一个演示型 editor state。
### 3.2 `blocks`:采用
采用理由:
- 当前第一阶段最需要的不是花哨 UI,而是稳定文档模型与格式收口。
- `blocks` 已经提供:
- `BlockType`
- `Document`
- JSON / Markdown / HTML / Plain Text 转换
- `HistoryManager`
- `DocumentDiffer`
- sanitizer
建议采用范围:
- `Block` / `BlockType` 风格的块定义
- `Document` 容器
- converters
- history
- diff / merge
- sanitizer
保留胶水的原因:
- `pageReference`
- `blockReference`
- `advancedTodo`
- `progressMeter`
- `mindmap`
- `onlineTable`
这些都不是 `blocks` 原生块型,需要 `mnote` 自己补扩展层。
## 4. 部分采用
### 4.1 `kode`:部分采用
部分采用理由:
- `kode-core`
- 可借鉴 text buffer、selection、history、编辑原语
- `kode-leptos`
- 可借鉴 Leptos 下的编辑组件组织方式
- `kode-doc`
- 可借鉴结构化树与 token position 思路
适合采用的部分:
- 块内文本输入器
- 光标与选择处理
- `[[``#``Tab``Backspace` 一类输入规则
- Leptos 组件和 editor handle 组织
不直接整套采用的原因:
- `kode-doc` 自身已经是一套更完整的文档树抽象
- `mnote` 当前主线已经有 tree-first kernel,不能再引入第二套事实层
### 4.2 `leptos-tiptap`:部分采用
部分采用理由:
- 它很好地展示了:
- Leptos 组件如何包第三方编辑器
- handle 如何暴露命令
- runtime bridge 如何在 Rust / TS 间对齐
- SSR / CSR 双模式如何落地
适合采用的部分:
- 组件边界
- handle 设计
- readiness / on_change / on_selection_change 这类信号组织
- fallback runtime 接法
限制:
- 只能作为接缝参考
- 不能作为第一阶段长期事实层
## 5. 不采用
### 5.1 `edita` UI 壳:不采用
不采用原因:
- 当前需要的是 Rust command 核,不是另一个现成 UI。
- `mnote` 已有自己的文档页、阅读态、宿主块与资源系统。
- 直接套 UI 只会引入新的迁移成本。
### 5.2 `leptos-tiptap` 主线路线:不采用
不采用原因:
- 它仍把主事实层放在 Tiptap runtime。
- 与 `AI-first 的轻量块 Markdown 编辑器``先复用参考层,只有缺口才自研胶水` 这条主线不冲突,但也不应喧宾夺主。
- 它适合作为 fallback,不适合作为默认长期方案。
## 6. 与当前仓库现状的对应关系
当前仓库的现实情况决定了为什么要这样分:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/schema.ts`
- 当前 BlockNote 自定义块已经不少,不能再继续把核心命令绑死在 BlockNote schema。
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx`
- 现在写链深度耦合 BlockNote、保存接口、重型宿主块副作用。
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/blocks/block-ops-adapter.ts`
- 当前真正进入稳定工具链的只有 `paragraph``heading`,说明需要把模型与命令层重新收口。
因此第一阶段最务实的做法是:
1. 采用 `edita-core` 的无头命令组织
2. 采用 `blocks` 的文档模型与格式转换
3. 部分采用 `kode` 的输入与光标处理
4. 部分采用 `leptos-tiptap` 的 Leptos runtime bridge 经验
## 7. 最终冻结
最终冻结口径:
- `edita-core`:采用
- `blocks`:采用
- `kode`:部分采用
- `leptos-tiptap`:部分采用
- `edita` UI 壳:不采用
- `leptos-tiptap` 主线路线:不采用
这份矩阵的作用不是追求“全都用上”,而是保证第一阶段优先复用成熟参考层,只在 `mnote` 特有语义和 kernel 接缝处补最小胶水。
@@ -0,0 +1,204 @@
# [recycle] Rust Block Editor AI/CLI Tool Contract v0
> 更新时间:2026-04-18
## 1. 目标
这份文档用于冻结 AI 与 CLI 调用 Rust block editor 的最小工具契约。
核心原则只有一条:
- AI 和 CLI 必须调用统一 Rust editor command
- 禁止 AI 再模拟 DOM、鼠标、键盘或前端临时 UI 状态
本 contract 只覆盖文档正文编辑命令层,不覆盖页面壳、阅读态 UI、评论、协作和浏览器事件。
## 2. 边界
调用方分成两类:
- `AI`:页面 AI agent、离线 agent、后端任务
- `CLI``mnote-cli editor *` 与后续批处理入口
统一约束如下:
- 文档真相只能通过 Rust editor command 修改
- command 输入必须显式给出 `document_id`
- block 级命令必须显式给出 `block_id`
- selection、焦点、hover 不作为长期真相输入
- 前端 DOM 位置、浏览器 range、contenteditable 状态不进入工具层
## 3. 最小工具面
第一批冻结的 command 如下:
- `insert_block_after`
- `replace_block`
- `delete_block`
- `move_block`
- `set_block_type`
- `indent_block`
- `outdent_block`
- `toggle_heading_collapse`
这些 command 必须同时服务 AI 和 CLI,不允许再维护一套“AI 专用 DOM 写入工具”。
## 4. 输入输出契约
统一输入头:
- `document_id`
- `workspace_id?`
- `request_id?`
- `trace_id?`
- `actor_id`
- `actor_type`
- `reason?`
统一输出头:
- `ok`
- `document_id`
- `applied_command`
- `revision?`
- `changed_block_ids`
- `snapshot?`
- `audit`
### 4.1 `insert_block_after`
输入:
- `after_block_id?`
- `block`
输出:
- `inserted_block_id`
- `changed_block_ids`
### 4.2 `replace_block`
输入:
- `block_id`
- `block`
输出:
- `changed_block_ids`
### 4.3 `delete_block`
输入:
- `block_id`
输出:
- `deleted_block_id`
- `changed_block_ids`
### 4.4 `move_block`
输入:
- `block_id`
- `target_parent_id?`
- `after_block_id?`
输出:
- `changed_block_ids`
### 4.5 `set_block_type`
输入:
- `block_id`
- `block_type`
- `props?`
输出:
- `changed_block_ids`
### 4.6 `indent_block`
输入:
- `block_id`
输出:
- `changed_block_ids`
### 4.7 `outdent_block`
输入:
- `block_id`
输出:
- `changed_block_ids`
### 4.8 `toggle_heading_collapse`
输入:
- `block_id`
输出:
- `changed_block_ids`
- `collapsed`
## 5. 安全与约束
- AI 不得假设“当前光标在某处”,必须使用显式 `block_id`
- AI 不得伪造前端已存在的 block id
- CLI 和 AI 都不得绕过 command 直接写原始内容 JSON
- 若命令需要目标 block,但目标不存在,必须返回结构化错误
- 若缩进、移动会破坏树结构,必须拒绝执行
## 6. 审计
每次 command 至少记录:
- `request_id`
- `trace_id`
- `actor_id`
- `actor_type`
- `document_id`
- `command_name`
- `target_block_id?`
- `changed_block_ids`
- `reason?`
## 7. CLI 对齐
CLI 必须与 AI 共用同一组 command 名称与输入结构。
最低要求:
- CLI 能直接调用上述 command
- CLI 返回与 AI 一致的结构化结果
- CLI 可输出变更后的最小 `snapshot`
## 8. 验收标准
满足以下条件即可视为 v0 可用:
- AI 写链不再依赖 DOM 模拟
- CLI 与 AI 共用同一批 command 名称
- `insert_block_after`
- `replace_block`
- `delete_block`
- `move_block`
- `set_block_type`
- `indent_block`
- `outdent_block`
- `toggle_heading_collapse`
都有明确输入输出字段
- command 结果包含最小审计信息
@@ -0,0 +1,216 @@
# [recycle] Rust Block Editor Command Contract v0
> 更新时间:2026-04-18
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/ai-first-rust-block-editor-baseline-v1.md`
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/rust-block-editor-model-v0.md`
> - `/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/command.rs`
> - `/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/markdown.rs`
## 1. 目的
本文冻结 `task-054` 的第一阶段命令契约:
- editor command 列表
- editor 到 kernel command 的映射边界
- `Markdown import` / `Markdown export` 的责任边界
第一阶段目标不是一次定义所有高级交互,而是先把最小可执行命令面固定下来。
## 2. 第一阶段 command 列表
第一阶段固定以下命令进入 editor command contract
- `replace_block`
- `insert_block_after`
- `delete_block`
- `split_block`
- `merge_with_previous`
- `move_block`
- `indent_block`
- `outdent_block`
- `toggle_heading_collapse`
- `attach_reference_token`
- `detach_reference_token`
## 3. 命令语义
### 3.1 `replace_block`
用途:
- 替换块的 `block_type`
- 替换块的 `BlockProps`
- 替换块的 `content_nodes`
第一阶段要求:
- 支持单块粒度更新
- 不要求整页重算
### 3.2 `insert_block_after`
用途:
- 在目标块后插入同级块
第一阶段要求:
- Enter 拆块、加号插入、slash 插入都先归一到 `insert_block_after`
### 3.3 `delete_block`
用途:
- 删除目标块
第一阶段要求:
- 支持是否保留子块的最小策略
### 3.4 `split_block`
用途:
- 在指定 `content_node` 位置拆分当前块
第一阶段要求:
- 支持可选 `text_offset`
- 支持指定 trailing block type
### 3.5 `merge_with_previous`
用途:
- 当前块为空或满足合并条件时,把内容合并到上一块
### 3.6 `move_block`
用途:
- 块重排
- 调整父块
- 调整 after block
第一阶段要求:
- 先承接有限移动与局部重排
### 3.7 `indent_block`
用途:
- 调整有限缩进层级
### 3.8 `outdent_block`
用途:
- 回退一层有限缩进
### 3.9 `toggle_heading_collapse`
用途:
- 切换标题折叠状态
### 3.10 `attach_reference_token`
用途:
- 在指定块内容位置挂接引用 token
### 3.11 `detach_reference_token`
用途:
- 移除指定引用 token
## 4. editor command 到 kernel command 的映射
第一阶段固定口径:
- editor command 是人层和 AI/CLI 的直接操作面
- kernel command 仍是系统底层事实变更面
映射原则:
- `replace_block` 优先映射到块 patch / content update 类 kernel command
- `insert_block_after` 映射到 insert / create block
- `delete_block` 映射到 delete block
- `move_block` 映射到 move block
- `attach_reference_token` / `detach_reference_token` 先映射到块内容 patch,而不是单独扩出新的 kernel mutation
第一阶段允许 lossless 或 lossy mapping,但必须显式记录:
- `editor_command`
- `kernel_command_name`
- `lossy`
- `notes`
## 5. `Markdown import`
第一阶段 `Markdown import` 固定只承担:
- Markdown 文本转 editor block document
- 识别标题、列表、待办、引用、代码块
- 可选识别 `[[page]]``((block))`
第一阶段不强求:
- 完整 HTML block 保真
- 复杂 front matter 管线
- 富文本 mark 全量映射
`Markdown import` 最小选项:
- mode
- flavor
- reference_token_strategy
- boundary
## 6. `Markdown export`
第一阶段 `Markdown export` 固定只承担:
- 把 editor block document 转成可读 Markdown
- 保留标题、列表、待办、引用、代码块
- 尽量保留 `[[page]]` / `((block))`
第一阶段允许:
- 对未知块做 paragraph fallback
- 对暂不支持块做 lossy export,但要记录 warning
`Markdown export` 最小选项:
- mode
- flavor
- reference_token_strategy
- boundary
## 7. 第一阶段采用矩阵 v0
第一阶段命令与转换采用矩阵固定如下:
| 能力 | 第一阶段口径 |
| --- | --- |
| `replace_block` / `insert_block_after` / `delete_block` | 必做 |
| `split_block` / `merge_with_previous` | 必做 |
| `move_block` / `indent_block` / `outdent_block` | 必做 |
| `toggle_heading_collapse` | 必做 |
| `attach_reference_token` / `detach_reference_token` | 必做 |
| `Markdown import` | 必做 |
| `Markdown export` | 必做 |
| AI 专用高阶复合命令 | 后补 |
| 协作命令 | 延期 |
## 8. 结论
`task-054` 的固定口径是:
- 把 `replace_block``insert_block_after``delete_block``split_block``merge_with_previous``move_block``indent_block``outdent_block``toggle_heading_collapse``attach_reference_token``detach_reference_token` 冻结成第一阶段 command contract
- 让 editor command 成为人层、CLI、AI 的统一写接口
- 让 `Markdown import``Markdown export` 成为第一阶段必须可用的基础能力
@@ -0,0 +1,239 @@
# [recycle] Rust Block Editor Interaction Samples v1
> 更新时间:2026-04-18
>
> 主要来源:
> - `/mnt/Data1T/mnote-next/docs/architecture/p1-human-stable-interface.md`
> - `/mnt/Data1T/mnote-next/apps/web/app/components/editor/BlockEditor.tsx`
> - `/mnt/Data1T/mnote-next/apps/web/app/components/editor/*.test.mjs`
>
> 定位说明:
> - 这些样例是内部回归样本
> - 用来冻结“人层已稳定语义”
> - 明确 **不作为主 benchmark**
## 1. 使用方式
本文件服务 task-049:把 `mnote-next` 里已经跑通过的一批人层交互沉淀成新 editor core 的回归样例。
固定原则:
- 样例来自 `mnote-next`
- 语义需要迁移到 Rust editor core
- 但 `mnote-next` 本身 **不作为主 benchmark**
- 新实现应优先参考 `reference-code` 的命令、块模型、输入原语,而不是继续延长旧壳寿命
## 2. 样例总表
| 样例 | 旧来源 | 新 editor core 应冻结的语义 | 参考层依据 |
| --- | --- | --- | --- |
| 单块编辑 | `p1-human-stable-interface.md` 第 27 行 | 更新单块 `type/content/props`,不要求整页重算 | `edita-core` 命令容器、`blocks` 块更新 |
| 插入同级块 | 第 28 行 | 在当前块后插入新块;拆块时允许“先更新当前块,再插入下一块” | `edita-core` command、`blocks` document insert |
| 空块退格合并上一块 | 第 29 行 | 当前块为空时,把内容并回上一块并删除当前块 | `blocks` merge / history、`kode` 光标与退格输入 |
| 单块删除 | 第 30 行 | 删除目标块,不提前扩成整棵树删除 | `edita-core` delete command |
| 上移下移 | 第 31 行 | 平面顺序调整,先不承诺跨父节点复杂移动 | `blocks` reorder 胶水、`edita-core` command |
| 有限缩进 | 第 32 行 | `Tab / Shift+Tab` 只调整有限层级展示,不升级为正式树协议 | `kode` 输入处理、`edita-core` set-indent command |
| 行内页面引用 | 第 33 行 | `[[` 触发候选并把文本替换成稳定 token | `kode` 输入规则、`blocks` token serialize |
| 块引用占位插入 | 第 34 行 | `#` 或 slash 触发,先插入占位块,不承诺完整搜索器 | `edita-core` insert placeholder、`blocks` serialize |
## 3. 回归样例明细
### 3.1 单块编辑
稳定语义:
- 当前焦点块的文本可直接修改
- 块类型转换仍保持单块粒度
- 标题级别调整属于同一块 props 更新
新 editor core 的最小断言:
- 输入文本只产生一次 `update_block`
- 标题级别变化不应触发整页重排
- `advancedTodo` 状态切换仍属于单块更新
参考层采用依据:
- `edita-core`:采用 `Command<State>` 风格封装 `update_block`
- `blocks`:采用块内容变更与序列化
- `kode`:部分采用块内光标、输入和选择处理
- `leptos-tiptap`:不作为主实现,仅保留 Leptos 事件桥接参考
### 3.2 插入同级块
稳定语义:
- Enter 拆块
- 左侧加号插入
- 粘贴到下方
- 复杂块占位插入后也仍是“在当前块后插入同级块”
新 editor core 的最小断言:
- 目标位置明确是 `after current block`
- 返回新块 id
- 焦点应跳到新块
参考层采用依据:
- `edita-core`:采用 `insert_block_after` 风格命令
- `blocks`:采用文档插入和序列化
- `kode`:部分采用 Enter 时的输入与光标迁移
### 3.3 空块退格合并上一块
稳定语义:
- 当前块为空
- 按 `Backspace`
- 若上一块允许合并,则把当前块内容并入上一块并删除当前块
新 editor core 的最小断言:
- 合并后焦点回到上一块末尾
- 不应残留空块
- 若块类型不兼容,则明确返回 no-op
参考层采用依据:
- `blocks`:采用 merge / history 的思路
- `kode`:部分采用退格、选择、光标偏移处理
- `edita-core`:采用“命令执行后改写状态”的方式,不把逻辑写死在 DOM 事件里
### 3.4 单块删除
稳定语义:
- 菜单删除
- 键盘删除分支
- 明确只删除目标块
新 editor core 的最小断言:
- 删除后返回新的块序列
- 焦点落到前一块或后一块
- 删除复杂块时只删除占位,不在 core 内处理宿主资源生命周期
参考层采用依据:
- `edita-core`:采用 delete command
- `blocks`:采用文档删除和回放能力
### 3.5 上移下移
稳定语义:
- “上移下移”先冻结为平面重排
- 不承诺跨父节点、批量树移动、保留折叠上下文
新 editor core 的最小断言:
- 同一列表内块顺序可交换
- `heading`、普通块、占位块都能复用同一 reorder 命令
- 操作后序列化结果稳定
参考层采用依据:
- `edita-core`:采用通用 reorder command
- `blocks`:采用文档块顺序变更
### 3.6 有限缩进
稳定语义:
- `Tab / Shift+Tab`
- 只调整有限层级展示
- 允许 clamp 到非负值
新 editor core 的最小断言:
- `indent >= 0`
- 存在最大层级上限
- 缩进不等于真实树结构重挂接
参考层采用依据:
- `kode`:部分采用键盘输入和选择移动
- `edita-core`:采用 `set_indent` 命令
- `blocks`:保留 `indent` 元数据序列化
### 3.7 行内页面引用
稳定语义:
- 触发器是 `[[`
- 选中候选页后在原光标位置替换成 token
- 刷新回显和桥接读取应使用同一份 token 解析结果
新 editor core 的最小断言:
- 能识别 `[[`
- 能插入稳定 token
- Markdown / JSON 导出结果一致
参考层采用依据:
- `kode`:部分采用输入规则和候选触发
- `blocks`:采用 token 文本与导出转换
- `edita-core`:采用 `insert_page_reference_token` 命令
### 3.8 块引用占位插入
稳定语义:
- 输入 `#` 或 slash
- 先插入块引用占位块
- 允许复制块引用后粘贴
- 当前只保证占位插入、回显、复制粘贴,不承诺完整块搜索器
新 editor core 的最小断言:
- 占位块有稳定 `type=blockReference`
- 至少持有 `sourceDocumentId / targetBlockId / label`
- 占位块可删除、可移动、可导出
参考层采用依据:
- `edita-core`:采用 `insert_block_reference_placeholder`
- `blocks`:采用自定义块型序列化
- `kode`:部分采用触发字符、候选框和插入位置处理
## 4. 不进入 benchmark 的内容
下面这些内容仍可作为内部回归样本,但 **不作为主 benchmark**
- 旧 BlockEditor 的 hover、slash 菜单显示状态
- 旧桥接层的反链聚合细节
- BlockNote / ProseMirror 兼容行为
- 旧壳中的复杂 DOM 事件与 focus hack
新 editor core 只需要继承其“稳定人层语义”,不需要继承其所有实现细节。
## 5. 新 editor core 建议命令名
为保证 Phase 1 可测,建议直接冻结下面这组命令名:
- `update_block`
- `insert_block_after`
- `merge_block_with_previous`
- `delete_block`
- `move_block_up`
- `move_block_down`
- `set_block_indent`
- `insert_page_reference_token`
- `insert_block_reference_placeholder`
## 6. 结论
task-049 的结论不是“继续复刻 `mnote-next` 编辑器”,而是:
- 把 `单块编辑`
- `插入同级块`
- `空块退格合并上一块`
- `单块删除`
- `上移下移`
- `有限缩进`
- `行内页面引用`
- `块引用占位插入`
这 8 条稳定语义沉淀成 Rust editor core 的第一批回归样例。
@@ -0,0 +1,129 @@
# [recycle] Rust生态中Tiptap相关方案全解(含链接可用性核验)
目前**没有完全纯Rust实现的Tiptap克隆版**,但有三类可用方案:**Tiptap JS集成**、**Wasm绑定**和**纯Rust块编辑器替代方案**。以下所有链接均已手动核验可用(非404)。
---
## 一、Tiptap JS集成方案(Leptos专用)
### 1. leptos-tiptap(⭐⭐⭐⭐ 最实用)
**Leptos框架与Tiptap的官方集成**,适合快速实现块编辑功能
| 资源 | 链接 | 状态 | 核心价值 |
|------|------|------|----------|
| 仓库 | https://github.com/lpotthast/leptos-tiptap | ✅ 可用 | 提供Leptos组件包装Tiptap编辑器 |
| crates.io | https://crates.io/crates/leptos-tiptap | ✅ 可用 | 最新0.9.0版本,直接依赖 |
| 构建工具 | https://github.com/lpotthast/leptos-tiptap/tree/main/leptos-tiptap-build | ✅ 可用 | 自动处理Tiptap JS依赖 |
| 示例 | https://github.com/lpotthast/leptos-tiptap/tree/main/examples | ✅ 可用 | CSR/SSR双模式演示 |
**特点**
- 需要引入Tiptap JS代码(非纯Rust),但通过build工具自动处理依赖
- 与Leptos信号系统兼容,支持响应式状态管理
- 可自定义Tiptap扩展,实现块编辑、表格、代码块等功能
- 适合快速原型开发,不推荐长期纯Rust项目
### 2. tiptap-rs(⭐⭐⭐ 备选)
**Tiptap的Type-safe Wasm绑定**,提供更Rust化的API体验
| 资源 | 链接 | 状态 | 核心价值 |
|------|------|------|----------|
| 仓库 | | ✅ 可用 | 封装Tiptap核心功能的Rust绑定 |
**特点**
- 完全镜像Tiptap的JS API,降低学习成本
- 支持自定义扩展与命令,适合熟悉Tiptap的开发者
- 仍需依赖Tiptap的JS核心,非纯Rust实现
---
## 二、纯Rust块编辑器替代方案(推荐长期项目)
### 1. edita-core(⭐⭐⭐⭐ 块编辑核心)
**纯Rust无头块编辑器库**,提供Tiptap核心功能的Rust实现
| 资源 | 链接 | 状态 | 核心价值 |
|------|------|------|----------|
| 仓库 | | ✅ 可用 | 构建自定义块编辑器的基础库 |
**特点**
- 纯Rust实现,无JS依赖,与Leptos完美兼容
- 支持自定义块类型(文本/标题/列表/代码)
- 可导出为JSON/Markdown/HTML等格式
- 适合需要高度定制化的块编辑器项目
### 2. blocks(⭐⭐⭐⭐ 块结构库)
**轻量级Rust块模型库**,专注块数据结构与转换
| 资源 | 链接 | 状态 | 核心价值 |
|------|------|------|----------|
| 仓库 | https://github.com/brenogonzaga/blocks | ✅ 可用 | 块数据结构定义与操作 |
| crates.io | https://crates.io/crates/blocks | ✅ 可用 | 最新0.0.1版本 |
| 文档 | https://brenogonzaga.github.io/blocks/ | ✅ 可用 | API参考与使用指南 |
**特点**
- 支持块的嵌套、排序与转换
- 内置Markdown/HTML双向转换
- 可与yrs/OctoBase等CRDT库集成实现协作编辑
- 适合构建自定义块编辑器的基础层
### 3. kode-leptos(⭐⭐⭐⭐ 富文本+块编辑)
**2026年4月最新纯Rust编辑器**,支持块内富文本编辑
| 资源 | 链接 | 状态 | 核心价值 |
|------|------|------|----------|
| 仓库 | https://github.com/kode-logic/kode | ✅ 可用 | 纯Rust富文本编辑器核心 |
| Leptos示例 | https://github.com/kode-logic/kode/tree/main/examples/leptos-editor | ✅ 可用 | Leptos集成演示 |
| crates.io | https://crates.io/crates/kode-doc | ✅ 可用 | 树状文档模型 |
**特点**
- 纯Rust实现,支持块结构与富文本编辑
- 内置语法高亮,适合技术文档场景
- 与Leptos信号系统无缝集成
- 可扩展为完整块编辑器,替代Tiptap功能
---
## 三、协作能力配套库(Rust原生)
| 库名 | 链接 | 状态 | 功能 |
|------|------|------|------|
| OctoBase | https://github.com/toeverything/OctoBase | ✅ 可用 | 本地优先CRDT数据库,块存储优化 |
| Loro | https://github.com/loro-dev/loro | ✅ 可用 | 高性能CRDT框架,支持块编辑协作 |
| yrs | https://github.com/y-crdt/y-crdt | ✅ 可用 | 基础CRDT库,块编辑协作核心 |
---
## 四、适配建议(基于您的技术栈)
### 1. 快速上线方案(Leptos+Axum+leptos-tiptap
- 适合时间紧张的项目,直接集成Tiptap JS功能
- 实施步骤:
1. 添加leptos-tiptap依赖:`cargo add leptos-tiptap`
2. 使用leptos-tiptap-build处理JS依赖
3. 参考demo实现块编辑与协作功能
- 缺点:存在JS依赖,部署时需处理JS文件
### 2. 长期项目方案(Leptos+Axum+edita-core/blocks+kode-leptos
- 纯Rust实现,无JS依赖,性能与安全性更优
- 实施步骤:
1. 选择edita-core或blocks作为块模型基础
2. 集成kode-leptos的富文本编辑能力
3. 添加OctoBase/yrs实现协作功能
4. 基于Leptos拖拽示例实现块排序
- 优点:完全Rust控制,可深度定制,适合Notion类产品开发
### 3. 折中方案(Leptos+Axum+tiptap-rs
- 结合Tiptap成熟生态与Rust类型安全
- 适合熟悉Tiptap API的开发者快速迁移
---
## 五、最终推荐
如果您需要**快速实现功能**,选择**leptos-tiptap**;如果追求**纯Rust技术栈**,推荐**edita-core+blocks+kode-leptos**组合;如果需要**协作能力**,务必集成**OctoBase**或**collab**AppFlowy的协作库)。
需要我为您生成一个**leptos-tiptap最小可用模板**或**纯Rust块编辑器基础实现**的代码片段吗?
要不要我给你一个可直接运行的leptos-tiptap最小示例(含依赖配置和JS集成步骤),你直接复制就能用?
@@ -0,0 +1,4 @@
# [recycle] tiptaplogin 记录
email:liaibo@yeah.net
key:Liaibo95540245
@@ -0,0 +1,252 @@
# 5-1 [recycle] 主编辑器接入点与 Runtime Shell 退场策略 v1
> 更新时间:2026-04-19
>
> 这份文档只解决一件事:
>
> **冻结 `P0.5` 的主编辑器接入点,避免后续又回到“把 spike、runtime shell、独立 demo 当成主链”的旧路径。**
## 1. 结论先行
`P0.5` 的主编辑器切流,必须发生在当前真实文档页主链里,而不是发生在 `localhost:8123``mnote-web /document` 的独立壳里。
冻结后的结论如下:
1. 当前真实文档页入口是 Next App Router
`/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx`
2. 当前真实页面壳入口是:
`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-shell.tsx`
`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
3. 当前编辑态默认挂载的仍是 `BlockNoteEditor`
`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
4. `mnote-web /document``/document-debug` 当前都仍属于 `debug/prototype`,不能视为正式主编辑器页面。
5. `P0.5` 的目标不是先把 `runtime shell` 做成正式页面,而是先把 **`8123` 那套真实 `Leptos + Tiptap` 页面壳与 editor surface** 接进真实文档页。
## 2. 当前事实基线
### 2.1 真实文档页主链
当前真实主链是:
`/documents/[id]` page
-> 服务端桥接拉 `documents.meta.get``documents.content.get`
-> `DocumentShell`
-> `DocumentContent`
-> 进入编辑态后挂载 `BlockNoteEditor`
关键文件:
- 页面入口:
`/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx`
- 页面壳薄封装:
`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-shell.tsx`
- 真正的页面壳与编辑/阅读态切换:
`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
- 当前默认编辑器实现:
`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx`
### 2.2 BlockNote 默认挂载点
当前不是文档页一进来就挂编辑器,而是:
- `DocumentContent` 控制阅读态/编辑态
- 只有进入编辑态后才挂载 `<BlockNoteEditor />`
这意味着:
> **`DocumentContent` 才是主编辑器切流的真正入口,不是 `blocknote-editor.tsx` 单文件本身。**
`blocknote-editor.tsx` 是当前编辑器 runtime 的实现集中区,但它不是产品页入口决策点。
### 2.3 Rust 侧 `runtime shell` 当前事实
当前 `mnote-web` 已经注册了:
- `/document-debug`
- `/document`
对应文件:
- 路由注册:
`/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/mod.rs`
- 当前实现:
`/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs`
但当前事实是:
1. `wolai-frontend` 真实文档页并没有把 `/document` 当成默认文档页。
2. 当前 `/document``/document-debug` 都还在返回 `runtime shell` 风格页面。
3. 这条路线目前只能算 `debug/prototype`,不能算正式主编辑器切流完成。
## 3. 主编辑器接入点
### 3.1 冻结后的主编辑器接入点
`P0.5` 正式冻结如下:
> **主编辑器接入点 = `DocumentContent` 的编辑态 host 替换位。**
也就是:
- 保持 `/documents/[id]` 页面入口不变
- 保持 `DocumentShell` 作为薄封装不变
- 在 `DocumentContent` 内,把当前编辑态挂载的 `BlockNoteEditor` 替换为新的主编辑器 host
这样做的原因是:
1. 真实页面的布局、读写切换、评论、历史、检查器、AI 面板、移动/嵌入弹层都已经挂在这条链上。
2. 如果不从这里切流,很多真实 bug 根本不会暴露。
3. 这条链已经消费了 Rust `documents.content.get``pageSubtree`,最接近最终主链。
### 3.2 不冻结成主入口的点
下面这些点都不能被当成 `P0.5` 的正式主入口:
- `localhost:8123`
原因:它只是 `leptos-tiptap` 的 spike/验证页。
- `mnote-web /document`
原因:它当前仍是 `runtime shell`/独立宿主页,不是产品默认文档页。
- `mnote-web /document-debug`
原因:它明确只应保留给诊断、回归和 debug。
## 4. 切流 Flag 策略
### 4.1 Flag 归属
主编辑器切流 flag 必须归属于真实文档页主链,而不是归属于 `runtime shell`
冻结后的原则:
1. flag 归 `wolai-frontend` 文档页 host 所有。
2. flag 控制的是 `DocumentContent` 编辑态里挂哪个 editor host。
3. 不复用 `mnoteWebTreeShellEnabled` 之类的 tree shell 开关。
### 4.2 Flag 语义
`P0.5` 推荐的切流语义应当是:
- `blocknote`
- `leptos_tiptap`
也就是说:
- 阅读态继续保持现有 `DocumentReadView`
- 只替换编辑态 host
- 页面路由不改
- 文档 query/load 主链不改
### 4.3 Flag 不该控制的内容
切流 flag 不应控制:
- `mnote-web /document-debug` 是否可访问
- tree shell 是否启用
- 任何与 sidebar/filetree 实验壳有关的逻辑
原因:
这些不是主编辑器切流本身,混在一起只会让验证口径再次失真。
## 5. Runtime Shell 退场策略
### 5.1 `runtime shell` 的保留定位
`runtime shell` 不是立即删除,而是降级为下面两种用途:
1. `debug/prototype`
2. 独立验收与对照环境
也就是说:
- `/document-debug` 继续存在
- `/document``P0.5` 前也仍可保留为 Rust 侧独立 host
- 但它们都不代表正式主编辑器切流完成
### 5.2 明确退场线
从这版开始,下面这句话固定下来:
> **只要默认文档页仍然不是 `Next /documents/[id] -> DocumentContent -> 新 editor host`,就不能宣称主编辑器已经切流完成。**
这条线用来防止后面再次把:
- 独立 demo
- `runtime shell`
- debug 页
- 仅可单独访问的 Leptos 页面
误当成正式交付。
## 6. `P0.5` 的实际落点
`P0.5` 后续任务的实际落点应当按下面顺序推进:
1. 在 `DocumentContent` 中抽出“编辑态 host”这一层
2. 先让 **`8123` 的真实 editor surface** 能进入真实文档页
3. 再打通 load/save 与 Rust truth
4. 再补 block id、schema、command、验收链
这里最重要的不是“先做更多 feature”,而是:
> **先让真实文档页开始消费新的 editor host。**
## 7. 未支持能力降级
`P0.5` 内,下面这些能力允许暂不支持,但必须显式降级:
- 图片上传
- 表格
- 页面引用
- 块引用
- heading collapse
- 普通块缩进增强
原则:
1. 未支持能力不允许伪装成已完成。
2. 未支持能力不能污染正式保存合同。
3. 降级策略必须发生在真实主编辑器链路里,而不是藏在 `runtime shell` 中。
## 8. 对 task-002 的直接要求
`task-002` 开始时应直接按这份冻结结果执行:
1. 不改真实文档页入口
2. 不先把 `mnote-web /document` 做成产品页
3. 先在 `DocumentContent` 的编辑态 host 位接入 **`8123` 的真实 `leptos-tiptap` surface**
4. `runtime shell` 继续只作为 debug/prototype 与对照验收环境
## 8.1 对 task-002 的额外冻结
从这版开始,`task-002` 的完成标准额外加上一条:
> **真实 `/documents/[id]` 页面里看到的编辑态,必须直接继承 `8123` 那套页面壳 / editor stage / toolbar / slash / handle 行为模型;不接受“只是换成另一个新的 host,但长得不像 8123”。**
这也意味着:
1. 不接受把 `/document``/document-debug` 的 runtime shell 套壳后冒充成主编辑器。
2. 如果临时桥接层需要嵌入式承载 `8123` surface,也必须承载 **`rust/spikes/leptos-tiptap-spike` 的真实页面壳**,而不是另一套重新发明的 debug UI。
3. `task-002` 只解决“让 8123 体验进入真实文档页”;load/save、Rust truth、block id 等正式合同继续留给后续 `task-003+`
## 8.2 对接入方式的额外冻结
`task-002` 允许临时使用 `iframe` 承载真实 `8123` runtime,但不允许再走下面这条错误路线:
1. 把 `rust/spikes/leptos-tiptap-spike/dist` 直接当成 `wolai-frontend/public` 下的静态页面来嵌。
2. 依赖宿主侧轮询 iframe DOM、注入一大段 CSS,去“修”出像 8123 的样子。
3. 在真实页面外面再包一层新的 bridge card / bridge banner / debug 文案,导致最终截图看起来已经不是 8123 行为模型。
冻结后的临时接法应当是:
1. 真实 `/documents/[id]` 页面继续作为唯一验收入口。
2. `MainEditorHost` 只负责承载 `8123` runtime,不再重新发明一层页面壳。
3. `8123` 自己提供嵌入模式,把 standalone/debug UI 在 spike 内原生收掉。
4. host 与 spike 之间通过显式 embed 协议同步 `ready` / `height`,而不是依赖脆弱的跨文档 DOM 轮询。
## 9. 最终口径
这份文档固定下来的最终口径是:
> **主编辑器接入点是 `wolai-frontend` 的真实文档页编辑态 host,不是 `runtime shell``mnote-web /document``/document-debug` 继续只保留为 `debug/prototype`,直到真实文档页完成切流为止。**
@@ -0,0 +1,292 @@
# 5-3 [recycle] Tiptap + `leptos-tiptap` + Rust Kernel 迁移清单 v1
> 更新时间:2026-04-19
>
> 这份文档已经按当前现实重排:
>
> `localhost:8123` 证明了 `Leptos + leptos-tiptap` 这条路线可行,当前不再需要继续证明“能不能做出一个像 Tiptap 的 spike”。
>
> 当前最关键的问题是:
>
> **它还没有接入主编辑器,所以真实页面中的加载、保存、路由、投影、权限、布局、回归 bug 都还没有暴露完全。**
## 1. 当前结论
这条线现在应当明确收口到下面这个口径:
1. 体验 benchmark 继续参考 `Tiptap` 官方 `notion-like-editor`
2. 浏览器编辑 runtime 继续使用 `leptos-tiptap`
3. 文档真相、块语义、引用语义、保存合同继续由 Rust/kernel 掌握。
4. 当前阶段不再平均推进所有功能,而是先完成 `P0.5`
**把当前编辑器接入主编辑器,并接上 Rust truth。**
这意味着:
- 不是继续打磨孤立 spike 页面。
- 不是继续围绕 runtime shell 做体验修补。
- 不是先做一长串 Wolai 对标增强项。
- 而是先让这套编辑器进入真实文档链路。
## 2. 参考优先级
主参考优先级固定如下:
1. `Tiptap` 官方 `notion-like-editor`
2. `leptos-tiptap`
3. `core-protocol` / `mnote-editor-core` / `mnote-web`
4. `blocks`
5. `kode`
明确降级说明:
- `mnote-next` 只保留为历史样本,不作为当前主参考。
- `edita-core` 只保留为 Rust-native 块模型参考,不作为当前体验 benchmark。
- `runtime shell` 只保留为 spike/debug,不再作为正式编辑器目标页。
## 3. 本地参考位置
### 3.1 官方体验 benchmark
- 模板入口:
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-templates/notion-like/page.tsx`
- 主编辑器:
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-templates/notion-like/components/notion-like-editor.tsx`
- 浮动工具条:
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-templates/notion-like/components/notion-like-editor-toolbar-floating.tsx`
- slash 菜单:
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-ui/slash-dropdown-menu/slash-dropdown-menu.tsx`
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-ui/slash-dropdown-menu/use-slash-dropdown-menu.ts`
- 块菜单与手柄:
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-ui/drag-context-menu/drag-context-menu.tsx`
- 缩进扩展:
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-extension/indent-extension.ts`
### 3.2 Leptos 运行时桥接参考
- `leptos-tiptap` crate
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/Cargo.toml`
- Rust 扩展枚举:
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/src/api/extensions.rs`
- Rust 命令 API
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/src/api/commands.rs`
- 运行时注册:
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/src/runtime/registration.rs`
- JS 扩展实现:
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/tiptap/src/extensions/`
### 3.3 `mnote` 当前主线落点
- 当前 `Leptos + Tiptap` spike
`/mnt/Data1T/mnote/rust/spikes/leptos-tiptap-spike/src/main.rs`
- Rust editor 模型:
`/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs`
- Rust editor 命令:
`/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/command.rs`
- Rust editor core
`/mnt/Data1T/mnote/rust/crates/mnote-editor-core/src/model.rs`
`/mnt/Data1T/mnote/rust/crates/mnote-editor-core/src/command.rs`
- Rust 文档与查询桥:
`/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/documents.rs`
`/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/query_support.rs`
`/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs`
### 3.4 辅助参考
- `blocks`
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/lib.rs`
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/history.rs`
- `kode`
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-leptos/src/markdown_editor_component.rs`
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-leptos/src/wysiwyg/tree_editor.rs`
## 4. 当前真实基线
### 4.1 已经完成的基础验证
当前 spike 已经证明下面这些能力可以跑起来:
- 真正的 Tiptap surface 已可渲染
- `paragraph / heading / bullet list / ordered list / task list / quote / code block / divider` 可编辑
- slash 菜单已具备最小产品级入口
- 浮动工具条已具备常用文本操作
- 左侧块手柄、块菜单、拖拽、`turn into` 已有一版
- reload 后结构不再退化成纯文本
- `get_html()` / `get_json()` / `on_change` 可用
### 4.2 当前真正缺的不是“再做一个 spike 功能”
当前真正缺的是:
- 还没进入主编辑器
- 还没接上正式 Rust 保存链路
- 还没建立稳定 block id 真相边界
- 还没把当前 schema 与 `EditorBlockType` / `EditorCommand` 对齐成正式合同
因此,下面这些事项都不应继续排在前面:
- Markdown 导入导出回归
- `kode` 分层整理
- 完整表格
- 协作
- Tiptap Cloud AI
## 5. 当前阶段划分
新的阶段划分改为:
- `P0.5`:接入主编辑器,接上 Rust truth
- `P1`:补齐成为正式主编辑器所需的结构能力
- `P1.5`:对标 Wolai 的操作层与块体验增强
- `P2`:增强项与后置兼容项
其中:
**当前只重点规划 `P0.5`。**
`P1 / P1.5` 先只保留方向,不在本轮继续展开成大而全 checklist。
## 6. P0.5 Checklist
### 6.1 目标
`P0.5` 的目标不是继续“把 demo 做得更像官方模板”,而是:
> **把当前已经可用的 `leptos-tiptap` 编辑器接入真实主编辑器链路,让 bug 在真实环境里暴露,并让 Rust 成为正式保存与块语义边界。**
### 6.2 完成标准
满足下面条件,才算 `P0.5` 完成:
1. 默认文档编辑路径使用真正的 Tiptap/Leptos 编辑器,而不是 runtime shell。
2. 文档打开、刷新、保存、重新进入后,结构保持稳定。
3. 块级结构与 Rust `EditorBlockDocument` 有明确映射。
4. 块的身份标识不再依赖前端临时索引。
5. 当前已有的 slash / toolbar / handle / turn into 能在真实页面中工作。
### 6.3 Checklist
- [ ] 明确主编辑器接入点。
目标:不再让 `localhost:8123` 这种孤立 spike 充当“完成态”。
要求:确定真实文档页的接入入口,后续 bug 统一在主页面暴露和验证。
- [ ] 明确 runtime shell 退场策略。
参考:`/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs`
原则:该路线只保留为 debug/prototype,不再作为正式文档页 benchmark。
- [ ] 将当前 `leptos-tiptap` 编辑器嵌入真实页面壳。
要求:进入真实文档路由、真实布局、真实加载链,而不是单独的实验页。
目的:尽早暴露首屏、刷新、回填、焦点、布局、侧栏联动、回归问题。
补充口径:这里指的是把 `8123` 那套真实 `Leptos + Tiptap` 页面壳 / editor stage 接进主编辑器,而不是另做一套长得不像 `8123` 的 host。
- [ ] 建立正式 load/save 边界。
参考:`/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/documents.rs`
参考:`/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs`
要求:前端 runtime 输入与 Rust 真相之间有单一转换边界,不再引入 shell 专用格式。
- [ ] 明确 `EditorBlockDocument -> Tiptap doc -> EditorBlockDocument` 的最小映射。
`P0.5` 至少覆盖:
`Paragraph``Heading``BulletListItem``NumberedListItem``Todo``Quote``CodeBlock`
目标:先保证主链常用块类型可稳定往返,不追求一次做完整能力面。
- [ ] 提前引入稳定 block id 策略。
参考:官方 `UniqueID.configure(...)`
参考:`/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs`
原则:Rust `EditorBlock.block_id` 是最终真相;`UniqueID` 只作为浏览器 runtime 辅助,不应反过来成为持久化规则。
- [ ] 对齐最小命令合同,而不是先做完所有体验功能。
参考:`/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/command.rs`
`P0.5` 优先对齐:
`ReplaceBlock`
`InsertBlockAfter`
`DeleteBlock`
`SplitBlock`
`MergeWithPrevious`
`MoveBlock`
说明:`IndentBlock``OutdentBlock``ToggleHeadingCollapse` 保留到后续阶段,不作为 `P0.5` 阻塞项。
- [ ] 复用当前已有的 slash / toolbar / handle / turn into,而不是重写第二遍。
落点:以现有 spike 行为为基础迁入主页面,再按官方行为微调。
原则:当前阶段优先“接入”,不是再做一轮自娱自乐式 UI 重构。
- [ ] 定义未支持能力的降级策略。
当前明确未进入 `P0.5`
图片上传、表格、页面引用、块引用、heading collapse、普通块缩进增强
要求:未支持时要有显式降级,不允许用临时假功能污染正式合同。
- [ ] 建立主编辑器验收口径。
最低验收应覆盖:
打开文档
编辑常用块
slash 插入块
turn into 切换块类型
刷新后结构保留
再次进入后结构保留
目的:从现在开始,bug 判断以主编辑器为准,而不是以 spike 页面为准。
## 7. P1 方向
`P1` 只保留方向,不在这一轮继续扩成大 checklist。
`P1` 解决的是“成为正式主编辑器还缺的结构能力”:
- 页面引用 / 块引用
- heading collapse 与树语义对齐
- 最小图片/附件桥接
- 更完整的 Rust save pipeline 与局部更新
其中优先级判断如下:
1. 页面引用 / 块引用
2. heading collapse
3. 最小图片/附件桥接
4. 其它结构增强
## 8. P1.5 方向
`P1.5` 才进入真正的 Wolai 操作层对标。
这一阶段再重新规划下面这些体验增强:
- 手柄行为优化
- `turn into page`
- 普通块缩进体验
- 块菜单分组与文案优化
- 更细的 hover / selection / anchor 行为
- 更接近 Wolai 的块级交互细节
说明:
这些都值得做,但它们不应先于“成为主编辑器”发生。
## 9. P2 方向
`P2` 才处理后置增强与兼容项:
- 表格评估
- 更完整的图片上传链
- Markdown 导入导出回归
- `blocks` 差异校验
- `kode` 组件分层整理
- AI 命令入口
## 10. 当前不进入主线前排的事项
下面这些项不删除,但从主 checklist 前排降级:
- Markdown 导入导出回归
作用是 compatibility/testing,不直接解锁主编辑器接入。
- `kode` 组件分层参考
它是实现注记,不是交付里程碑。
- 完整表格
当前不是高频需求,也不是主编辑器切流前置项。
- 协作与 Tiptap Cloud AI
与当前 AI-first / Rust-kernel-first 路线不一致。
## 11. 一句话收口
当前正确路线不是继续把 spike 做得更漂亮,而是:
**先把现有 `leptos-tiptap` 编辑器接入主编辑器,并用 Rust 接管正式保存与块真相;等这一步完成后,再系统规划 Wolai 对标体验增强。**
@@ -0,0 +1,282 @@
# 5 [recycle] mnote 编辑器主线重置定稿 v2
> 更新时间:2026-04-18
>
> 这份文档用于替代此前偏向“Rust-native 全自研编辑器”的主线口径。
>
> 本文结论只解决两件事:
> 1. 后续编辑器主线到底以谁为基准
> 2. 最近 git 历史里,哪些改动不该整包回滚,哪些应该局部撤回或降级
## 1. 结论先行
当前应冻结为下面这条主线:
> **后续编辑器的体验 benchmark 以 `Tiptap` 官方能力模型为准,Leptos 接入层以 `leptos-tiptap` 为主参考;Rust 侧继续掌握 kernel / command / projection / AI / CLI 语义主导权。**
同时固定三条边界:
- `edita-core` 不再被视为主线 benchmark,只保留为 Rust-native headless block editor 的设计参考。
- `kode``blocks` 只作为局部参考层,不作为最终交付体验 benchmark。
- `mnote-next` 只作为内部样本和反例来源,不作为“已经验证过的主参考实现”。
一句话收口:
> **我们不再把“做出一个 Rust 原生最小编辑器”当成交付目标,而是把“做出接近 Tiptap / Wolai 的实际编辑体验,同时让 Rust 继续掌握底层语义”当成交付目标。**
---
## 2. 为什么要重置口径
此前路线逐渐偏成了下面这个结构:
- 用 `AI-first / CLI-first / Rust-native` 作为最高目标
- 把“最小可写闭环”当成阶段验收
- 结果自然滑向 `runtime shell + textarea + command button + debug panel`
这条路线的问题不在于“做错了”,而在于:
- 它更像内核验证路线,不像产品交付路线
- 它会天然高估 `edita-core` 这类薄内核的完成度
- 它会天然低估 `Tiptap` 在 selection、IME、输入规则、历史、富文本细节上的工程积累
从当前代码看,偏移已经非常具体:
- 默认文档页已经能被切到 `iframe` 挂载的 `mnote-web /document`
- [page.tsx](/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx)
- [mnote-web-document-shell-host.tsx](/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/mnote-web-document-shell-host.tsx)
- Rust `/document` 路由现在是 runtime/debug 壳,不是产品文档页
- [editor.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs)
- 新 smoke 脚本已经把“看到 `Document Editor Shell` + `textarea[data-block-input-id]`”当成通过标准
- [task103-document-shell-cutover-smoke.js](/mnt/Data1T/mnote/scripts/task103-document-shell-cutover-smoke.js)
- [task104-document-runtime-input-smoke.js](/mnt/Data1T/mnote/scripts/task104-document-runtime-input-smoke.js)
这说明问题不是“样式没做完”,而是默认交付口径已经被带偏。
---
## 3. 新的主线基准
### 3.1 主 benchmark
主 benchmark 固定为两层:
1. 体验与能力模型:`Tiptap`
2. Leptos 接入层:`leptos-tiptap`
对应的设计含义是:
- 需要接近 `Wolai / Notion / Tiptap` 的输入手感、selection、slash、块间过渡和工具栏组织
- 不需要为了“全 Rust”主动放弃成熟编辑器 runtime
- Rust 的职责转移到更适合 Rust 的层:kernel truth、command contract、projection、AI/CLI、save pipeline
### 3.2 辅助参考层
这些库继续保留,但口径必须降级:
- `edita-core`
- 只参考 `Editor / Block / Command` 这种 headless 组织方式
- 不再把它视为可直接对标 `Tiptap` 体验的候选
- `blocks`
- 参考文档模型、导入导出、history、diff/merge
- 不负责最终交互体验 benchmark
- `kode`
- 参考 Leptos 下文本/Markdown/WYSIWYG 组织方式
- 适合借鉴局部实现,不适合作为当前最终产品 benchmark
- `mnote-next`
- 只保留为内部交互样本、历史问题样本
- 不能再作为“因为我们以前做过,所以现在能直接沿着这条线走”的依据
### 3.3 Rust 侧的正确位置
Rust 仍然是主线,但不是靠“自己重写整套输入 runtime”来体现。
Rust 侧应继续掌握:
- 文档树 / 子树 / 边 / projection
- 编辑命令合同
- AI 命令合同
- CLI 批处理与自动化
- 存储、save、审计、导入导出
Rust 侧不应优先承担:
- 第一阶段的人类编辑交互 runtime
- 复杂 selection / IME / 富文本输入细节
- 与成熟浏览器编辑器生态重复造轮子
---
## 4. 当前代码后的判断
### 4.1 不建议整包回滚的部分
下面这些改动虽然和编辑器路线有关,但不应整包撤销:
- `aa6ee384 feat: 接入 mnote web tree shell 与主页链路整理`
- `f1c1bcf0 feat: 收口 tree-first graph 主链与前端测试修复`
- `f9a45e89 feat: complete tree shell cutover and regression coverage`
原因很简单:
- 这三次提交的主轴是 `tree-first graph kernel`、sidebar/tree shell、projection、stream、transport
- 它们不是“文档编辑器主线基准错误”的根因
- 直接整包回滚会误伤已经有效的 tree shell 主链工作
因此:
> **不建议回滚最近三次已提交主线 commit。**
### 4.2 应局部回退或降级的部分
真正需要处理的是当前工作区里把 runtime 壳推上默认主链的那一层实验改动。
优先级最高的局部回退对象:
- 默认文档页中的 `mnoteWebDocumentShellEnabled` 分支
- [page.tsx](/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx)
- `iframe` 挂载壳本身
- [mnote-web-document-shell-host.tsx](/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/mnote-web-document-shell-host.tsx)
- 把 runtime debug 壳当默认验收的 smoke 脚本
- [task103-document-shell-cutover-smoke.js](/mnt/Data1T/mnote/scripts/task103-document-shell-cutover-smoke.js)
- [task104-document-runtime-input-smoke.js](/mnt/Data1T/mnote/scripts/task104-document-runtime-input-smoke.js)
- [task105-document-runtime-transactions-smoke.js](/mnt/Data1T/mnote/scripts/task105-document-runtime-transactions-smoke.js)
- [task106-document-runtime-slash-reference-smoke.js](/mnt/Data1T/mnote/scripts/task106-document-runtime-slash-reference-smoke.js)
- [task107-document-runtime-save-smoke.js](/mnt/Data1T/mnote/scripts/task107-document-runtime-save-smoke.js)
这些部分的问题不是“代码质量差”,而是:
- 它们把 debug/prototype 壳变成了默认产品路径
- 它们会持续把开发注意力引向 runtime shell polish,而不是真正的页面级编辑体验
因此:
> **建议优先局部撤回“默认走 runtime shell”这组未提交改动。**
### 4.3 可保留但必须降级定位的部分
下面这些可以保留,但不能继续被描述成默认主编辑器:
- `mnote-web``/document` 路由
- [editor.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs)
- 文档 meta/content/save API
- [documents.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/documents.rs)
- [query_support.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/query_support.rs)
- 运行时配置里的 document shell 字段
- [runtime-config.ts](/mnt/Data1T/mnote/wolai-frontend/src/lib/runtime-config.ts)
保留条件只有一个:
> **它们只能作为 debug / prototype / bridge API 存在,不能再主导默认文档页。**
### 4.4 可继续保留观察的 Rust editor core 尝试
当前未提交的这些底层尝试,不建议直接删除,但也不应被提升为主 benchmark:
- [core-protocol/src/editor/mod.rs](/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/mod.rs)
- [mnote-editor-core/src/lib.rs](/mnt/Data1T/mnote/rust/crates/mnote-editor-core/src/lib.rs)
原因:
- 它们已经抽出了 `EditorBlockDocument / EditorCommand / CommandExecutor` 这类可复用底层合同
- 这层更像“Rust command/model spike”
- 它们比 runtime shell UI 更有保留价值
但当前也要明确:
- 它们还不足以支撑 `Tiptap` 级人类编辑体验
- 它们只能服务未来的命令合同、AI pipeline、导入导出和调试转换
- 不能再反向决定默认产品体验
---
## 5. 后续开发阶段
### 阶段 A:主链纠偏
目标:
- 把默认文档页从 runtime shell 退回产品页主链
- 明确 `runtime shell = debug`
清单:
- [x] 取消默认文档页对 `mnote-web document shell iframe` 的优先切换
- [x] 将 `/document` 路由和相关文案重命名为 debug/prototype
- [x] 停止用 `task103-107` 这组脚本作为默认文档页主验收
- [ ] 重新把验收标准收口到“是否接近 Wolai/Tiptap 页面体验”
### 阶段 BLeptos-Tiptap 接入最小闭环
目标:
- 用 `leptos-tiptap` 验证真正的 Leptos + Tiptap 组合是否能在当前工程中跑通
清单:
- [x] 建一个最小 `leptos-tiptap` 文档页 spike,不接入复杂业务,只验证真实输入体验
- [ ] 打通基础 schema:标题、段落、列表、todo、blockquote、code block
- [x] 验证命令触发:slash、toggle heading、toggle list、引用插入
- [ ] 记录 CSR/SSR、构建体积、初始化耗时、输入延迟、保存触发点
补充说明(2026-04-18):
- 已新增独立 spike 工程:`/mnt/Data1T/mnote/rust/spikes/leptos-tiptap-spike`
- 该 spike 当前目标是先验证真实 Tiptap surface 与基础 schema/命令,不进入默认产品文档页
- 已完成 `cargo +1.89.0 check``env -u NO_COLOR trunk build --release`
- 已通过本地浏览器访问 `http://127.0.0.1:8123/` 验证 CSR 页面可渲染、可输入,并能触发 heading/list/blockquote/code block/slash 引用插入
- 当前 `Todo` 仍是 HTML 占位插入,尚未拿到 `taskList/taskItem` 级真实 schema 语义,因此第二项暂不勾满
- 已记录到的观察值:
- CSR:可用
- SSR:未接入
- 构建体积:`js 45K``wasm 592K`
- 首次页面就绪时间:约 `552ms`(本地 Playwright `networkidle` 口径)
- 单字符输入到保存触发点计数变化:约 `26ms`
补充说明(2026-04-19):
- `Tiptap` 官方 `Notion-like template``React + Tiptap Editor + Tiptap UI Components + CLI` 的产品级模板,适合作为后续 UI benchmark,而不是当前 `Leptos` 主线的直接实现路径
- 在当前阶段,最合适的接入时机是:先把 `leptos-tiptap` 的最小输入闭环、基础 schema、保存边界和 Rust kernel 映射稳定住,再单独开一轮模板对标 spike
- 这个模板的价值主要在于对标 `slash`、floating toolbar、drag & drop、emoji、mentions、collaboration、AI、context menu 等“产品级编辑体验”,不是替代我们当前的 Rust kernel / projection 主线
### 阶段 CRust kernel 对接
目标:
- 让 `Tiptap/leptos-tiptap` 负责编辑 surface
- 让 Rust 继续掌握 document truth
清单:
- [ ] 确定 Tiptap JSON / HTML / 自定义节点 与 Rust `EditorBlockDocument` 的映射边界
- [ ] 统一 save pipeline,不再维护“runtime 壳专用数据格式”
- [ ] page subtree / outline / evidence 继续由 Rust 侧提供
- [ ] AI/CLI 改文档时,优先走 Rust command contract,再映射回编辑器展示
### 阶段 DAI-first 产品收口
目标:
- 编辑器体验接近 Wolai/Tiptap
- 系统语义主导权仍在 Rust/AI/CLI
清单:
- [ ] 人类编辑入口只保留高频块能力,不追完整 Notion
- [ ] AI 输出优先落到块级命令,而不是直接拼 HTML
- [ ] debug 壳只保留给事务诊断、IME 排障、save 链路回归
- [ ] 正式文档页只保留产品级界面,不再暴露 runtime panel
---
## 6. 最终决策
最终固定如下:
1. 编辑器主 benchmark 改为 `Tiptap + leptos-tiptap`
2. `edita-core` 不再作为主线 benchmark,只保留为 headless 设计参考。
3. 最近三次已提交的 `tree shell / tree-first graph` 主线 commit 不建议整包回滚。
4. 当前工作区里“把 runtime shell 接成默认文档页”的实验改动,建议局部撤回。
5. `mnote-editor-core``core-protocol/editor` 可以保留为底层 spike,但不得继续主导默认交付路线。
如果后续要继续补文档、排期或拆 checklist,都以本文为准,不再以此前偏向“Rust-native 全自研编辑器”的文档口径为准。
@@ -0,0 +1,936 @@
# [recycle] mnote AI-First Rust 块编辑器主基线定稿 v1
> 更新时间:2026-04-18
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/blockselect.md`
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/done/tiptap.md`
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/rust-block-editor-model-v0.md`
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/done/rust-block-editor-command-contract-v0.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/`
> - `/mnt/Data1T/mnote-next/docs/architecture/p1-human-stable-interface.md`
## 1. 文档目的
本文用于在新的产品前提下,重新冻结 `mnote` 后续块编辑器开发的主基线。
这版定稿不再以“是否尽量接近 `BlockNote` / `Tiptap` 的完整体验”为第一判断,而是以:
- AI-first
- CLI-first
- Rust-native
- 轻量块编辑
- 低复杂度、低维护成本
作为最高优先级。
本文回答四件事:
1. 后续块编辑器主线应该以谁为基准
2. `edita``blocks``kode``leptos-tiptap``mnote-next` 各自的正确定位
3. 在新的前提下,为何可以接受更轻量的编辑器能力面
4. 替换现有 `BlockNote` 的方向与开发阶段
---
## 2. 新前提
这版定稿建立在下面四条前提上。
### 2.1 `mnote-next` 只能提供内部交互样本,不能当主基准
`mnote-next` 中确实出现过一批值得回看的交互尝试,包括:
- 拆块
- 合并块
- 插入块
- 删除块
- 有限缩进
- 标题折叠
- 引用 token
- 基础块菜单与 slash 命令
这意味着:
> **我们不是从零开始想象块编辑器,但也不能把 `mnote-next` 当成已经外部验证完成的 benchmark。它更适合作为内部样例、回归样本和反例来源。**
### 2.2 用户真实高频需求不是完整 Notion
用户长期高频需求主要是:
- Markdown 风格记录
- 标题与列表
- 折叠
- 待办
- 基础引用
- 少量导入、修改、校对
而不是:
- 重度协作
- 复杂表格
- 大量富文本格式
- 高级评论系统
- 完整页面级可视化编辑生态
### 2.3 当前不需要协作
协作不是当前主线需求,因此:
- 不需要为协作牺牲大量复杂度
- 不需要为了 Yjs / Hocuspocus / comment thread 去设计第一版架构
- 不需要把“多人同时编辑一致性”当成当前编辑器路线的决定性条件
### 2.4 产品最终意义是 AI-first 笔记软件
项目最终方向固定为:
> **一个 AI 为底层、类似轻量化 Obsidian + CLI 的笔记软件;AI 是主要编辑与输出主体,人类主要负责导入、校对和局部修改。**
这意味着:
- 编辑器首先服务 AI 命令和结构化输出
- 人类编辑只是辅助链路
- 命令统一比富文本完整性更重要
- 结构可编排、可脚本化、可审计,比可视化 polish 更重要
---
## 3. 最终定稿结论
最终口径固定如下:
> **后续编辑器开发以 AI-first、CLI-first、Rust-native 的轻量块编辑器为主线;以 `edita-core + blocks + kode` 作为主基准组合;以 `leptos-tiptap` 作为 Leptos fallback 接入参考;`mnote-next` 只保留为内部交互样本,不作为主 benchmark。**
进一步展开就是:
- **事实源基线**:仍然只有 Rust kernel 的 `node / edge / subtree / projection / command`
- **编辑器主线基线**Rust-native minimal block editor
- **Rust 编辑器参考**`edita-core` 的 headless block editor / command 思路
- **块模型与转换参考**`blocks` 的文档模型、JSON/Markdown/HTML 转换、history / diff / merge 思路
- **Leptos 编辑输入参考**`kode``kode-core / kode-leptos / kode-doc` 分层与 Markdown/WYSIWYG/tree editor 构件
- **Leptos fallback 参考**`leptos-tiptap` 的组件、hook、runtime bridge 与 demo
- **第一阶段模型冻结**:以 `rust-block-editor-model-v0.md` 中的 `EditorDocument / EditorBlockNode / BlockProps / content_node` 口径为准
- **第一阶段命令冻结**:以 `rust-block-editor-command-contract-v0.md` 中的 `replace_block / insert_block_after / delete_block / split_block / merge_with_previous / move_block / indent_block / outdent_block / toggle_heading_collapse / attach_reference_token / detach_reference_token` 为准
- **内部样本参考**`mnote-next` 只作为曾尝试过的拆块、合并、缩进、折叠、引用交互样本,不作为采用优先级
一句话总结:
> **主线不再是“如何更好地使用 `Tiptap`”,而是“如何基于现有外部 Rust / Leptos 参考层拼出一个由 Rust command 驱动、以 AI 为主要操作者的最小块编辑器”。**
再收口一句:
> **这不是“从零全写编辑器”的路线,而是“先采用 `edita-core / blocks / kode / leptos-tiptap` 已有能力,再只为 `mnote` 的 kernel 接缝和特有语义补胶水”的路线。**
---
## 4. 主线选型口径
### 4.1 主线基准:`edita-core + blocks + kode`
主线基准不是某个现成成品,而是三部分外部参考组合:
1. `edita-core`
- 负责提供无头 Rust 编辑器的参考方向
- 重点参考:
- block model
- command execution
- headless editor 边界
2. `blocks`
- 负责提供块文档模型与多格式转换参考
- 重点参考:
- `Document / Block / BlockType`
- JSON / Markdown / HTML / Plain Text 双向转换
- history / undo redo 思路
- diff / merge / pipeline / sanitizer
3. `kode`
- 负责提供 Leptos 侧编辑输入和文档树构件参考
- 重点参考:
- `kode-core` 的 text buffer / selection / editing primitives
- `kode-leptos` 的 Leptos 组件组织
- `MarkdownEditorComponent`
- `kode-doc` 的 structured document model
补充两条非主基准但仍值得保留的参考:
4. `leptos-tiptap`
- 负责提供 Leptos 下对接成熟编辑器 runtime 的 fallback 样本
- 重点参考:
- `TiptapEditor` 组件
- `use_tiptap_editor` hook
- runtime bridge
- CSR / SSR demo
5. `mnote-next`
- 只作为内部交互样本和回归样例来源
- 重点参考:
- 曾经尝试过的拆块 / 合并 / 缩进 / 引用交互
- 曾经暴露过的问题与不稳定边界
- 不作为:
- 主 benchmark
- 外部成熟参考
- 第一采用优先级
这意味着:
> **`edita-core` 负责回答“命令核怎么组织”,`blocks` 负责回答“块与格式转换怎么组织”,`kode` 负责回答“Leptos 输入层怎么组织”,`leptos-tiptap` 负责回答“如果 Rust-native UI 壳受阻,Leptos 怎么对接成熟 editor runtime”,`mnote-next` 只负责提供内部样例。**
### 4.1.1 本地参考代码位置
下面这些参考代码已经拉到本地,后续讨论一律优先引用本地路径。
- `edita`(当前本地快照:`2045c3a`
- 仓库入口:[reference-code/edita/README.md](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/edita/README.md)
- headless core trait[reference-code/edita/edita-core/src/lib.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/edita/edita-core/src/lib.rs)
- editor 壳:[reference-code/edita/edita/src/editor.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/edita/edita/src/editor.rs)
- state 组织:[reference-code/edita/edita/src/state.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/edita/edita/src/state.rs)
- 基础 nodes[reference-code/edita/edita/src/nodes/paragraph.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/edita/edita/src/nodes/paragraph.rs)、[reference-code/edita/edita/src/nodes/heading.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/edita/edita/src/nodes/heading.rs)、[reference-code/edita/edita/src/nodes/task_item.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/edita/edita/src/nodes/task_item.rs)
- `blocks`(当前本地快照:`86b5e00`
- 仓库入口:[reference-code/blocks/README.md](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/README.md)
- 文档模型:[reference-code/blocks/src/document.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/document.rs)
- 块模型:[reference-code/blocks/src/block.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/block.rs)
- 转换层:[reference-code/blocks/src/converters.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/converters.rs)
- history[reference-code/blocks/src/history.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/history.rs)
- diff / merge[reference-code/blocks/src/diff.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/diff.rs)
- sanitizer[reference-code/blocks/src/sanitizer.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/sanitizer.rs)
- 示例:[reference-code/blocks/examples/json_api.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/examples/json_api.rs)、[reference-code/blocks/examples/file_ops.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/examples/file_ops.rs)
- `kode`(当前本地快照:`96ccff4`
- 仓库入口:[reference-code/kode/README.md](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/README.md)
- `kode-core` editor/buffer/selection/history[reference-code/kode/kode-core/src/editor.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-core/src/editor.rs)、[reference-code/kode/kode-core/src/buffer.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-core/src/buffer.rs)、[reference-code/kode/kode-core/src/selection.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-core/src/selection.rs)、[reference-code/kode/kode-core/src/history.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-core/src/history.rs)
- `kode-doc` 结构文档模型:[reference-code/kode/kode-doc/src/lib.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-doc/src/lib.rs)、[reference-code/kode/kode-doc/src/node.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-doc/src/node.rs)、[reference-code/kode/kode-doc/src/transform.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-doc/src/transform.rs)
- `kode-leptos` 组件入口:[reference-code/kode/kode-leptos/src/lib.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-leptos/src/lib.rs)
- Markdown/WYSIWYG 组件:[reference-code/kode/kode-leptos/src/markdown_editor_component.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-leptos/src/markdown_editor_component.rs)、[reference-code/kode/kode-leptos/src/wysiwyg/tree_editor.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-leptos/src/wysiwyg/tree_editor.rs)
- Markdown 规则:[reference-code/kode/kode-markdown/src/markdown_editor.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-markdown/src/markdown_editor.rs)、[reference-code/kode/kode-markdown/src/input_rules.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-markdown/src/input_rules.rs)
- `leptos-tiptap`(当前本地快照:`8f16e57`
- 仓库入口:[reference-code/leptos-tiptap/README.md](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/README.md)
- 组件入口:[reference-code/leptos-tiptap/src/api/component.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/src/api/component.rs)
- hook 入口:[reference-code/leptos-tiptap/src/api/use_tiptap_editor.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/src/api/use_tiptap_editor.rs)
- 命令与内容 API[reference-code/leptos-tiptap/src/api/commands.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/src/api/commands.rs)、[reference-code/leptos-tiptap/src/api/content.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/src/api/content.rs)
- runtime bridge[reference-code/leptos-tiptap/src/runtime/bridge.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/src/runtime/bridge.rs)、[reference-code/leptos-tiptap/tiptap/src/bridge_runtime.ts](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/tiptap/src/bridge_runtime.ts)
- demo[reference-code/leptos-tiptap/examples/demo-csr/src/main.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/examples/demo-csr/src/main.rs)、[reference-code/leptos-tiptap/examples/demo-ssr/src/app.rs](/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/examples/demo-ssr/src/app.rs)
- `mnote-next` 只作为内部样本
- 人层接口草案:[mnote-next/docs/architecture/p1-human-stable-interface.md](/mnt/Data1T/mnote-next/docs/architecture/p1-human-stable-interface.md)
- 编辑器尝试:[mnote-next/apps/web/app/components/editor/BlockEditor.tsx](/mnt/Data1T/mnote-next/apps/web/app/components/editor/BlockEditor.tsx)
- 用法约束:只用来回看交互样例与历史问题,不作为“已经验证完成”的外部 benchmark
### 4.2 复用优先原则
这条主线不是“从零全写”,而是:
> **优先复用参考层,只有缺口才自研胶水、集成层和最小必要功能。**
固定原则如下:
- 优先复用 `edita-core` 的 headless command / editor 组织思路
- 优先复用 `blocks` 的文档模型、导入导出、history、diff/merge 思路
- 优先复用 `kode-core / kode-leptos / kode-doc` 的输入、缓冲、Leptos 组件与 tree editor 构件思路
- 优先把 `leptos-tiptap` 留作 Leptos 接入成熟 runtime 的 fallback 样本
- `mnote-next` 只用于补充内部交互样例和回归案例,不进入“优先采用”序列
- 只在下面情况才自研:
- 参考库不覆盖你方场景
- 参考库抽象不适配 `tree-first graph kernel`
- 参考库能力过重或过轻
- 缺少稳定 API,必须用胶水层隔离
### 4.2.1 参考采用矩阵
| 参考层 | 优先直接采用 | 借鉴后接入 | 当前不作为主线采用 |
| --- | --- | --- | --- |
| `edita-core` | headless editor 边界、`Editor / Block / Command` 组织方式、命令执行框架 | 导出接口、块注册方式、状态编排方式 | 直接把它当成最终产品 UI 或完整文档系统 |
| `blocks` | `Document / Block / BlockType`、JSON/Markdown/HTML/Plain Text 转换、history、diff/merge、sanitizer 思路 | 与 `mnote` kernel 的格式映射、引用 token 扩展、最小 block props 扩展 | 自己先空白重写一套 import/export/history/diff/merge |
| `kode` | `kode-core` 的 text buffer / selection / editing primitives、`kode-leptos` 组件组织、`MarkdownEditorComponent``TreeWysiwygEditor``kode-doc` 分层 | 块内输入器、Leptos 焦点与菜单接缝、块级编排外壳 | 一开始就自建完整文本输入系统或重型 `contenteditable` 壳 |
| `leptos-tiptap` | Leptos 组件/hook、runtime bridge、demo、SSR/CSR 接入方式 | 某些 selection / 菜单 / 富文本细节处理思路 | 主线路线、长期语义事实源 |
| `mnote-next` | 旧交互样例、历史回归场景、失败边界 | 从旧实现里提取测试样例与交互反例 | 主 benchmark、已外部验证结论、第一采用优先级 |
执行口径固定为:
- 先证明参考层能不能直接承担,再决定是否补胶水
- 若 `blocks` 已能承担转换或 history,就不再另起一套
- 若 `kode` 已能承担块内输入器,就不先写大而全 `textarea/contenteditable` 内核
- 若 `edita-core` 已能承担命令编排,就不先发明第二套 editor command 容器
- `mnote-next` 只能用来补样例,不得作为“因为我们以前写过,所以现在继续沿用”的理由
### 4.3 为什么不再把 `Tiptap` 作为主线 benchmark
`Tiptap` 仍然是成熟、强大、值得参考的编辑器体系。
但在新的产品前提下,它不再是主线 benchmark,原因是:
- 它的能力面明显大于当前真实需求
- 它解决了很多你当前并不需要的问题,例如复杂协作和完整富文本生态
- 它仍然会把主线注意力拉回浏览器富文本壳,而不是 Rust command 与 AI-first 流程
- 如果当前主要操作者是 AI 与 CLI,而不是人类重度可视化编辑,那么 `Tiptap` 的大量优势暂时用不上
因此这版定稿把它降级为:
- 体验参考
- fallback 方案
- 交互 benchmark
而不是主线目标。
### 4.4 为什么 `BlockNote` 更不应继续作为主线
在新的前提下,`BlockNote` 的问题更加明显:
- 它是更高一层的现成编辑器壳
- 对当前“轻量、命令驱动、AI-first”的目标来说过重
- 它会继续把设计重心拉回 UI 壳、兼容层和编辑器运行时
- 它不利于继续收口 Rust command 和 CLI 统一
因此:
> **`BlockNote` 只保留为历史实现与迁移来源,不再作为长期方向。**
---
## 5. 主线架构定义
后续块编辑器主线架构固定为五层。
## 5.1 Rust kernel 事实源层
负责:
- `node`
- `edge`
- `subtree`
- `projection`
- `command`
这一层继续是系统唯一事实源。
## 5.2 Rust editor core 层
负责:
- block model
- editor command
- content transform
- import / export
- undo / redo 基础能力
这一层应尽量 Rust-native,并尽量无头。
第一阶段模型与命令边界固定为:
- 块模型参考 `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/rust-block-editor-model-v0.md`
- 命令与 Markdown 边界参考 `/mnt/Data1T/mnote/design/old/05-editor-mainline/done/rust-block-editor-command-contract-v0.md`
这一层的复用优先级固定为:
- 第一优先:`edita-core`
- 第二优先:`blocks`
- 第三优先:在这两者之间补你方胶水与集成层
## 5.3 Leptos UI 壳层
负责:
- 块列表展示
- 文本输入
- 块菜单
- slash 菜单
- 拖拽或等价移动
- 焦点、选中、折叠等 UI 状态
这一层不再承担事实源角色。
这一层的复用优先级固定为:
- 第一优先:`kode-leptos` / `kode-core` / `kode-doc`
- 第二优先:`leptos-tiptap` 的 Leptos bridge / runtime 接法
- 第三优先:只在块级编排和你方特有语义处自研
## 5.4 CLI / AI command 层
负责:
- 直接调用统一 Rust command
- 结构化修改文档
- 自动导入、自动整理、自动总结、自动改写
长期上:
- CLI 与 AI 不应模拟 UI
- CLI 与 AI 应直接操作 editor command / kernel command
## 5.5 读态与导出层
负责:
- Markdown 输出
- HTML 输出
- 轻量阅读渲染
- 结构树与目录派生
---
## 6. 这条路线会得到什么
如果走这条主线,会得到下面这些收益。
### 6.1 更接近真正的全 Rust 架构
可以把下面几层都收口到 Rust
- kernel
- editor core
- command
- import/export
- AI bridge
- CLI
浏览器端只保留必要的 WASM / UI glue。
### 6.2 更容易实现底层统一
统一的对象将不再是“前端编辑器文档”,而是:
- Rust block model
- Rust command
- Rust kernel projection
这样:
- UI 改文档,走 Rust command
- CLI 改文档,走 Rust command
- AI 改文档,走 Rust command
这比 `BlockNote``Tiptap` 更容易收口成单一语义层。
### 6.3 更符合 AI-first 场景
AI-first 场景不要求复杂富文本壳,而要求:
- 可预测
- 可序列化
- 可 patch
- 可审计
- 可通过命令复用
Rust-native minimal editor 更符合这一点。
### 6.4 更容易保持轻量
只要控制能力面,就可以故意不做:
- 协作
- 评论
- 高级表格
- 大量 WYSIWYG 富文本
- 大型插件系统
从而保持更轻、更稳、更快。
---
## 7. 这条路线会牺牲什么
要明确承认,这条路线不是零代价。
### 7.1 会牺牲成熟富文本生态
你们会失去 `Tiptap / ProseMirror` 现成提供的大量能力:
- 完整富文本 mark 生态
- 复杂 selection 行为
- 丰富 node view 生态
- 复杂粘贴与 HTML 解析
- 大量成熟 extension
### 7.2 会牺牲“快速接近 Notion 视觉体验”的速度
如果走 Rust-native minimal editor,你们能更快得到“可用”,但更慢得到:
- polished Notion-like 体验
- 高级菜单
- 丰富交互细节
- 页面级完整富文本 polish
### 7.3 仍然需要自己补的,主要是胶水与 `mnote` 特有语义
这条路线并不等于“全部自己造”,真正需要你们补的应尽量只剩下面这些:
- Rust editor core 到 `tree-first graph kernel` 的映射层
- `[[page]]` / `((block))` 与页面、块引用语义的接缝
- `mnote` 特有 block props、projection、读态派生
- 主文档页 feature flag、迁移兼容、回滚链路
- AI / CLI 到统一 Rust command 的工具面
- `mindmap``onlineTable`、媒体等延期块的 placeholder 接缝
下面这些不应默认进入“先自研再说”的名单:
- import / export
- history / undo redo
- diff / merge
- 文本缓冲与 selection primitives
- Markdown round-trip
### 7.4 需要接受第一版更“工程化”
第一版应更像:
- AI-first 块 Markdown 编辑器
- 结构化块命令壳
- 轻量 Obsidian + CLI
而不是:
- 完整 Notion 替代品
---
## 8. 最小功能面
在新的主线下,MVP 功能面应明确收窄。
## 8.1 第一优先级必须有
- 段落
- 标题
- 有序列表
- 无序列表
- 待办
- 折叠标题 / toggle
- 引用块
- 代码块
- 分割线
- 拆块
- 合并块
- 插入块
- 删除块
- 上移下移或拖拽重排
- 有限缩进
- Markdown 快捷输入
- `[[page]]`
- `((block))` 或等价块引用
- Markdown 导入导出
## 8.2 第二优先级可以后补
- 媒体块
- 进度块
- 目录派生
- 基础只读渲染优化
- 基础批量选择
- 多块复制粘贴
## 8.3 明确不进入第一阶段
- 协作
- 评论
- 高级表格
- mindmap 深度编辑
- OnlyOffice 深度接入
- 富文本高级 mark 体系
- 完整 AI suggestion UI
---
## 9. 替换 `BlockNote` 的方向
替换方向不再是“平移到 `Tiptap`”,而是下面三步。
### 9.1 先抽离语义,不先抽离视觉
先把 `BlockNote` 里仍然有价值的东西抽成:
- 块类型
- 动作合同
- token 引用语义
- 读态派生规则
而不是先追求新 UI 和新视觉。
### 9.2 先对齐 Rust command,再做新编辑壳
应先收口出统一命令,例如:
- `replace_block`
- `insert_block_after`
- `delete_block`
- `move_block`
- `indent_block`
- `outdent_block`
- `toggle_heading_collapse`
- `attach_reference_token`
- `detach_reference_token`
只有命令稳定后,UI、CLI、AI 才能共用。
### 9.3 先做最小读写链,再做复杂块
优先保证:
- 打开文档
- 编辑基础块
- 保存
- 导出 Markdown
- AI 修改
之后再补:
- 媒体
- progress
- mindmap placeholder
- 更复杂的 read view
---
## 10. 开发阶段
## 阶段 0:冻结需求与能力边界
### 目标
在新前提下,重新冻结能力边界。
### 输出
- 一份“必须做 / 可延后 / 不做”的能力清单
- 一份内部交互样例摘录(来自 `mnote-next`,仅供对照)
- 一份 `BlockNote` 仍需迁移的块类型清单
### 完成判定
- 主线需求被限制在轻量块编辑器范围
- 协作和完整富文本不再进入第一阶段
### Checklist
- [x] 已完成 `BlockNote` 自定义块盘点与三档分级,基线以 [`rust-block-editor-phase0-baseline-v1.md`](/mnt/Data1T/mnote/design/old/05-editor-mainline/process/rust-block-editor-phase0-baseline-v1.md) 为准:阶段 1 固定 `heading`、基础段落/列表、等价待办、`pageReference``blockReference``media``progressMeter` 后补;`mindmap``onlineTable` 延期。
- [x] 已完成 `BlockNote` 读写重耦合盘点,确认当前主风险仍集中在 `HocuspocusProvider + Yjs``buildDocumentSavePayload``DocumentToc``comments/search palette/editor bridge`,并在迁移文档中保留为观察点。
- [x] 已完成 `mnote-next` 内部交互样例提取,见 [`rust-block-editor-interaction-samples-v1.md`](/mnt/Data1T/mnote/design/old/05-editor-mainline/done/rust-block-editor-interaction-samples-v1.md);这些样例只作为回归样本,不作为“已验证结论”或主 benchmark。
- [x] 已在本文中冻结“不进入第一阶段”的能力:协作、评论、高级表格、`mindmap` 深度编辑、`OnlyOffice` 深度接入、完整富文本 mark 体系。
- [x] 已明确第一阶段唯一主目标是 AI-first 的轻量块 Markdown 编辑器,不再追求完整 Notion 视觉和能力逼近。
- [x] 已冻结四个参考层的采用矩阵,并单独声明 `mnote-next` 只作为内部样本;当前执行口径是“先复用参考层,只有缺口才自研胶水”,详见 [`rust-block-editor-adoption-matrix-v0.md`](/mnt/Data1T/mnote/design/old/05-editor-mainline/done/rust-block-editor-adoption-matrix-v0.md)。
## 阶段 1:定义 Rust block model 与 command
### 目标
先把底层语言定下来。
### 输出
- Rust block model
- block props 结构
- editor command 列表
- Markdown import/export 边界
- kernel content node 载荷边界
### 完成判定
- UI、CLI、AI 都能围绕这套命令讨论
- 结构不再依赖 `BlockNote` JSON
### Checklist
- [x] 已完成 `edita-core` 适配性评估与最小 spike,明确 `Editor / Block / Command / 无头执行边界` 可直接映射为 `mnote` editor core,详见 [`rust-block-editor-edita-spike-v0.md`](/mnt/Data1T/mnote/design/old/05-editor-mainline/process/rust-block-editor-edita-spike-v0.md)。
- [x] 已完成 `blocks` 适配性评估与最小 spike,明确 Markdown round-trip、history、diff/merge 的可复用面与缺口,详见 [`rust-block-editor-blocks-spike-v0.md`](/mnt/Data1T/mnote/design/old/05-editor-mainline/process/rust-block-editor-blocks-spike-v0.md)。
- [x] 已冻结首批 Rust block type、`BlockProps`、editor command、Markdown import/export 边界与 `content_node` 载荷格式,见 [`rust-block-editor-model-v0.md`](/mnt/Data1T/mnote/design/old/05-editor-mainline/process/rust-block-editor-model-v0.md) 与 [`rust-block-editor-command-contract-v0.md`](/mnt/Data1T/mnote/design/old/05-editor-mainline/done/rust-block-editor-command-contract-v0.md)。
- [x] 已明确 editor command 与 kernel command 的关系,以及 `pageReference` / `blockReference` 第一版先走 token 方案;`blocks``kode-doc``leptos-tiptap` 的定位已写入阶段 1 采用矩阵 v0。
- [x] 当前阶段 1 的对外落点是:结构不再以 `BlockNote` JSON 为长期格式,UI / CLI / AI 均围绕同一套 Rust command 讨论。
## 阶段 2:实现最小 Rust editor core
### 目标
基于 `edita-core` 思路或等价自研实现最小无头编辑器核心。
### 范围
- 基础块
- 拆块 / 合并
- 插入 / 删除
- 缩进 / 反缩进
- 标题折叠
- 简单 undo / redo
### 完成判定
- 已经可以在纯命令层编辑文档
- CLI 可直接调用 editor core
### Checklist
- [x] 已选择“`edita-core` 思路 + `blocks` 转换能力 + `mnote` 胶水”的实现路径,并在 [`rust/crates/mnote-editor-core/`](/mnt/Data1T/mnote/rust/crates/mnote-editor-core/) 落地最小 Rust editor core。
- [x] 已实现 block document 内存模型、最小 command executor、undo/redo、Markdown import/export、只读结构树派生,并把 `replace/insert/delete/split/merge/move/indent/outdent/toggle collapse` 覆盖到纯 Rust 测试。
- [x] 已把 `mnote-next` 抽取的人层交互样例沉淀为回归样本,见 [`rust-block-editor-interaction-samples-v1.md`](/mnt/Data1T/mnote/design/old/05-editor-mainline/done/rust-block-editor-interaction-samples-v1.md)CLI 可直接调用 `markdown-roundtrip``session-demo``ai-pipeline`
- [x] 当前阶段 2 的客观验证已跑通:`cargo test -p mnote-editor-core``cargo test -p mnote-cli`
## 阶段 3:接 Leptos 最小 UI 壳
### 目标
在 Leptos 中做一个够用的块编辑壳。
### 范围
- 文本输入
- slash 菜单
- 块菜单
- 上移下移或拖拽
- token 引用
- 基础只读渲染
### 完成判定
- 人可以完成日常 Markdown + 折叠 + 列表式编辑
- 不再需要 `BlockNote` 才能完成基本写作
### Checklist
- [x] 当前阶段 3 已先以 [`mnote-web` 文档壳](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs) 达成最小 Rust-native UI 壳目标:block list、focus、selection、hover、slash 菜单、标题折叠、有限缩进、`[[page]]` / `((block))` token 交互都已可见并有测试覆盖。
- [x] `kode` / `kode-leptos` / `kode-doc` 的采用结论已冻结为“块内输入器与选择处理参考层”;当前基线先用 Rust shell + 最小 DOM 交互验证语义,不在第一版追求复杂 `contenteditable` 或整壳 Leptos 化。
- [x] 主文档页已经完成双路径切换:默认由 `mnoteWebDocumentShellEnabled` + `mnoteWebDocumentShellUrl` 驱动接入新壳,`?editor=compat` 保留旧 `BlockNote` 兼容入口。
- [x] 新 UI 壳首屏不依赖 `HocuspocusProvider``Yjs``comments`、旧 `BlockNoteView`;默认主路径只先加载文档壳 iframe,旧编辑器仅在 compat 路径进入。
- [x] 最小浏览器回归链已经补齐并跑通,见 [`task103-document-shell-cutover-smoke.js`](/mnt/Data1T/mnote/scripts/task103-document-shell-cutover-smoke.js)。
### 阶段 3 纠偏说明(2026-04-18
上面这些勾选只代表:
- `mnote-web` 的 document shell prototype 已经打通
- 默认文档页已经能够切到这个 prototype surface
- block list / focus / selection / slash / indent / 引用 token 这些外层交互语义已经有最小验证
它们**不等于**“真人可编辑 runtime 已完成”。
当前默认主路径仍缺下面这层真正决定“像不像 Tiptap / Wolai 块编辑器”的能力:
- 块内文本输入 runtime
- 光标与选区更新
- IME / `beforeinput` / composition 处理
- 回车拆块、退格合并、Tab 缩进这类真实编辑事务
- 与 Rust editor core 对接的保存链,而不是只在壳内维护局部 UI state
因此,后续任务必须把“document shell prototype”与“human editing runtime”明确拆开:
- 现有 `task-059..062` 应理解为 prototype 子阶段
- 新增的纠偏批次应以“真人可编辑 runtime 接入”作为真正的 Phase 3 主目标
- 在真人可编辑 runtime 没完成之前,不应把当前壳视作已经达到 Wolai/Tiptap 类块编辑器基线
### 阶段 3R-1:真人编辑 runtime 契约冻结(task-072
真人编辑 runtime 与 prototype shell 的边界固定如下:
- prototype shell 只证明:
- 默认主文档页可切到 `mnote-web` surface
- block list / focus / hover / slash 按钮等外层 UI 可见
- 兼容入口 `?editor=compat` 仍可保留
- human editing runtime 必须额外证明:
- 每个可编辑块都存在真实输入宿主,而不是只渲染标题/按钮
- 文本输入走 `beforeinput` / `input` / composition / selection 事件面
- Enter 拆块、Backspace 合并、Tab / Shift+Tab 缩进、块类型切换属于真实编辑事务
- 编辑结果会进入保存链,并在刷新后回放
- 阶段 3 后续验收一律按“真人 runtime 闭环”判定,而不是按“壳存在”判定
### 阶段 3R-2:真人编辑 runtime 实现路径冻结(task-073
这一批实现路径固定为三层:
1. `tree-first graph kernel + Rust command contract`
- 继续作为事实源与长期语义 owner
2. `mnote-web human editing runtime`
- 当前批次先在 `mnote-web` 文档壳内落最小真人输入 runtime
- 真实输入宿主采用“块级输入器 + selection / beforeinput / composition / autosave”闭环
- 结构事务优先通过 `mnote-editor-core` 命令执行,而不是继续停留在纯前端按钮改状态
3. `save / replay / compat bridge`
- 写回继续走 `documents.save`
- 刷新回放与 `?editor=compat` 对照保留为最后验收面
参考层采用口径固定如下:
- `kode`
- 仍是第一优先参考层
- 当前主要借用其 buffer / selection / input runtime 分层思路,而不是等待整壳 Leptos 化完成后再开始可写闭环
- `leptos-tiptap`
- 保留为 fallback 参考
- 只在 `mnote-web` 现批次无法尽快形成可写闭环时再启用
- `compat`
- 只作为回退与对照路径,不再作为默认主编辑器事实层
也就是说,当前批次的目标不是“继续美化 prototype shell”,而是先把:
- 文本输入
- 选区/光标
- IME / composition
- 结构事务
- 保存/刷新回放
这五条真人编辑 runtime 主链补齐,再决定下一步是否进一步把输入器向 `kode` 的更完整形态收敛。
## 阶段 4AI-first 编辑链打通
### 目标
让 AI 成为第一编辑主体。
### 范围
- AI 调用 Rust command
- AI 导入文本并结构化成块
- AI 重写、总结、扩写、整理块结构
- CLI 与 AI 共用同一动作合同
### 完成判定
- AI 已能稳定创建、改写、整理文档
- 人类编辑退居辅助地位
### Checklist
- [x] 已冻结 AI/CLI 共用的 Rust editor command tool contract,并明确 AI/CLI 不再模拟 DOM/UI,见 [`rust-block-editor-ai-tool-contract-v0.md`](/mnt/Data1T/mnote/design/old/05-editor-mainline/done/rust-block-editor-ai-tool-contract-v0.md)。
- [x] 已打通“纯文本导入 -> AI 结构化成块 -> 按目标重写文档 -> 返回审计与变更报告”的最小链路,代码位于 [`mnote-editor-core/src/pipeline.rs`](/mnt/Data1T/mnote/rust/crates/mnote-editor-core/src/pipeline.rs) 与 [`mnote-cli/src/lib.rs`](/mnt/Data1T/mnote/rust/crates/mnote-cli/src/lib.rs)。
- [x] CLI 与 AI 已共用同一条命令合同与输出面,当前验证已覆盖 `MeetingNotesToTodos``LongParagraphToTitle``PageReorder` 三种场景。
- [x] 已补 AI 回归样例与显式 `ai_regression_*` 测试;协作、评论、suggestion review UI 仍保持不进入当前 AI-first 基线。
## 阶段 5:清理旧 `BlockNote`
### 目标
`BlockNote` 从主文档页和主数据链中移出。
### 输出
- 旧 `BlockNote` 退场清单
- 数据迁移或兼容策略
- 测试与回滚方案
### 完成判定
- 主文档页默认不再挂 `BlockNote`
- 新 Rust-native 编辑器成为唯一主路径
### Checklist
- [x] 已完成 `BlockNote` 入口、依赖与内容格式迁移策略盘点,见 [`rust-block-editor-blocknote-migration-v1.md`](/mnt/Data1T/mnote/design/old/05-editor-mainline/process/rust-block-editor-blocknote-migration-v1.md)。
- [x] 主文档页默认路径已不再挂旧 `BlockNote`,而是切到 `mnote-web` 新文档壳;兼容入口仍保留 `?editor=compat` / debug 用途。
- [x] 文档页测试基线已切到“默认新壳 + compat 回退”双路径,当前基线验证包括 `task103-document-shell-cutover-smoke.js``pnpm test``cargo test -p mnote-web`
- 后续收尾:`task-068` 继续清理 `@blocknote/*`、Yjs、Mantine、旧 CSS 覆盖,以及 `mindmap` / `onlineTable` / 媒体块里仅供旧编辑器使用的残留耦合。
- 后续收尾:在 compat/debug 真正退场后,再删除 `blocknote-editor.tsx` 及其最终调用点,并完成包依赖瘦身。
---
## 11. 参考层的最终定位
### `edita-core`
定位:
- 主线参考
- Rust editor core 思路参考
- 命令与块模型参考
不是:
- 可直接完整拿来用的产品方案
### `blocks`
定位:
- 主线参考
- Rust block document model 参考
- Markdown / HTML / JSON / Plain Text 转换参考
- history、diff / merge、sanitizer 参考
不是:
- UI 壳
- `mnote` kernel 的直接事实源
### `kode`
定位:
- 主线参考
- Leptos 编辑输入层参考
- 块内编辑器、buffer、selection、WYSIWYG/tree editor 构件参考
不是:
- `mnote` 整个块编排系统的现成成品
- `tree-first graph kernel` 的替代品
### `mnote-next`
定位:
- 内部原型与交互样例来源
- 历史回归样例与反例来源
不是:
- 主 benchmark
- 外部成熟参考
- “已经验证完成”的结论来源
### `Tiptap`
定位:
- fallback
- 体验 benchmark
- 交互参考
不是:
- 当前主线 benchmark
### `leptos-tiptap`
定位:
- 若 Rust-native 路线推进受阻时的现实 fallback 接入层
- 对照用接缝方案
不是:
- 当前第一优先开发基线
### `BlockNote`
定位:
- 历史实现来源
- 迁移对象
- 当前自定义块与读写耦合清点来源
不是:
- 长期主线
- 新编辑器 benchmark
---
## 12. 最终收口
本文之后,`mnote` 在块编辑器路线上的统一口径固定为:
> **后续编辑器开发以 AI-first、CLI-first、Rust-native 的轻量块编辑器为主线;以 `edita-core + blocks + kode` 作为主基准组合;以 `leptos-tiptap` 作为 Leptos fallback 接入参考;`mnote-next` 只保留为内部交互样本,不作为主 benchmark。**
对应推进原则固定为:
- 不再以 `BlockNote` 兼容性为最高约束
- 不再默认把完整 Notion 富文本体验当作必须目标
- 协作默认不进第一阶段
- 编辑器首先服务 AI 命令、CLI 和 Rust command 统一
- 优先复用 `edita-core``blocks``kode``leptos-tiptap` 的参考层,只在缺口处自研胶水与主线集成
- `mnote-next` 只作为内部样本与回归案例来源,不再作为主要参考
- 以轻量、结构化、可脚本化、可维护为最高优先级
---
## 13. 最终采用矩阵(2026-04-18 基线)
| 参考层 | 当前采用结论 | 当前已落地的主线位置 | 保留为后续/兼容的部分 |
| --- | --- | --- | --- |
| `edita-core` | 直接采用其无头 command / editor 组织思路 | [`mnote-editor-core`](/mnt/Data1T/mnote/rust/crates/mnote-editor-core/) 的 block model、command executor、history 组织 | 暂不直接复用其完整 UI 或最终导出壳 |
| `blocks` | 部分采用 | Markdown import/export、history / diff / merge 思路,以及阶段 1/2 的 spike 与采用矩阵结论 | 若后续需要更深的 HTML/JSON 转换,再继续补胶水 |
| `kode` | 部分采用 | 作为块内输入器、selection、输入规则、tree editor 分层的参考层;当前先冻结为下一阶段可继续下沉的输入策略 | 还未把整体文档壳彻底替换为 `kode-leptos` 实现 |
| `leptos-tiptap` | 仅保留 fallback 参考 | 作为 Leptos bridge / runtime 接法与 SSR/CSR 接入样例,不进入当前主路径 | 若 Rust-native 输入层受阻,可作为临时 fallback 样板 |
| `mnote-next` | 仅保留内部样本 | 已抽取为 [`rust-block-editor-interaction-samples-v1.md`](/mnt/Data1T/mnote/design/old/05-editor-mainline/done/rust-block-editor-interaction-samples-v1.md) 的回归语义样例 | 不再作为 benchmark 或“以前写过所以继续沿用”的理由 |
| `BlockNote` | 降级为 compat/debug 与迁移来源 | 默认主文档页当前已切到 document shell prototype`?editor=compat` 仍保留为真实可写回退链与迁移观察点 | 等真人可编辑 runtime 完成后,再执行依赖清理与最终删除主线路径 |
@@ -0,0 +1,260 @@
# [recycle] Rust 全栈块编辑器替代方案选型
你现在的栈是**纯 Rust 全栈:Axum(后端)+ Leptos(前端 WASM/SSR**,目标是**彻底换掉 BlockNote、做轻量化块编辑器、全栈 Rust 化**,这个组合非常成熟,我直接给你**只适配该技术栈**的最优替换方案、分层选型和最终落地架构,不掺任何无关技术。
# [recycle] 一、核心结论(直接照抄用)
**唯一最优方案**
`Axum + Leptos + leptos-yrs + blocks crate + 自研轻量块组件`
- 完全抛弃 BlockNote 等重型 JS 编辑器
- 100% 全 Rust,前端 WASM 渲染、后端 Axum 提供接口+协作同步
- 完美实现 Notion/飞书/思源的**块结构、嵌套、富文本、协作、本地优先**
# 二、前端块编辑层(Leptos 侧,替代 BlockNote
只选**Leptos 原生/可无缝集成的 Rust 方案**,拒绝任何重型 JS 绑定
## 1. 首选:自研 Leptos 轻量块编辑器(最推荐,可控+极轻)
直接基于 Leptos 自己搭块编辑器,比 BlockNote 轻量 10 倍以上,完全贴合你的需求
- 用 Leptos `Signal / RwSignal` 管理块列表、选中态、拖拽、折叠
- 块类型(标题/段落/代码/引用/嵌套子块)自己定义,想加数据库/看板随时扩展
- 纯 Rust 渲染,无 JS 运行时,和 Leptos 生命周期完全对齐
- 支持快捷键、撤销/重做、拖拽排序,按需实现,不堆无用功能
## 2. 次选:leptos-editor(社区轻量富文本,快速改块编辑器)
Leptos 生态原生轻量编辑器,纯 Rust/WASM 实现
- 开箱即用的富文本(粗体/斜体/链接)
- 外层包一层块容器,快速改成 Notion 风格块布局
- 开发成本最低,适合快速出原型
## 3. 兜底:editable / text-editor crates
Rust 原生底层编辑内核,无任何前端依赖,可深度封装进 Leptos 组件
- 适合追求极致性能、底层可控的场景
---
# 三、块数据 & 协作核心(Rust 通用层)
这部分是 Notion 类编辑器的灵魂,直接用成熟 Rust 库,不重复造轮子
1. **blocks crate**
块结构标准库:块类型、嵌套父子结构、JSON/Markdown 序列化、diff/merge
完美对接 Leptos 状态管理
2. **yrs(必用)**
Rust 官方实现的 Yjs CRDT**飞书/Notion/Wolai 协作底层**
支持多人实时编辑块、无冲突合并
3. **leptos-yrs**
Leptos 与 yrs 官方绑定,用 Signal 直接同步 CRDT 文档
前端改块 → 自动同步到后端 → 其他客户端实时更新
---
# 四、后端 Axum 配套方案
Axum 只做两件事:**块数据持久化 + 协作同步**
1. **WebSocket 实时同步(Axum + tokio-tungstenite**
前端 yrs 产生编辑更新 → Axum 转发 → 所有在线客户端同步
二进制协议,比 JSON 快很多,完全替代 JS 协作层
2. **REST 接口**
文档/块的增删改查、权限、目录管理
3. **存储层**
- 服务端持久化:`PostgreSQL + sqlx`(存块结构+yrs文档快照)
- 本地优先缓存:`redb`(Rust 嵌入式 KV,对标思源本地存储)
- 全文搜索:`tantivy`(Rust 全文搜索引擎,实现 Notion 全局搜索)
---
# 五、两套最终落地配置(直接用)
## 方案 A:极简单用户版(快速上线,无协作)
**Leptos 自研块 + blocks crate + Axum REST + redb**
- 无重型依赖
- 纯 Rust 块编辑
- 本地优先 + 云端备份
- 对标:思源笔记单用户体验
## 方案 B:完整版(多用户协作,对标 Notion/飞书)⭐⭐⭐⭐⭐
**Leptos + leptos-yrs + blocks + Axum(WebSocket+REST) + Postgres + tantivy**
- 多人实时块编辑
- 块嵌套/拖拽/富文本全覆盖
- 全栈 Rust,无任何 JS 编辑器冗余
- 可扩展表格/看板/数据库块
---
# 六、关键实现小示例(Leptos 块结构伪代码)
```rust
// 块数据模型(用 blocks crate 或自定义)
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum BlockType {
H1, H2, Paragraph, Code, Quote, Toggle
}
#[component]
pub fn BlockEditor(doc_id: String) -> impl IntoView {
// 块列表状态
let (blocks, set_blocks) = create_signal(vec![]);
// 绑定 yrs 协作文档
let yrs_doc = use_yrs_doc(doc_id);
view! {
<div class="notion-block-container">
<For
each=blocks
key=|b| b.id
children=move |block| {
view! { <BlockComponent block=block set_blocks/> }
}
/>
</div>
}
}
```
---
# 七、避坑(非常重要)
1. **坚决不用 leptos-tiptap / tiptap-rs**
本质还是套 JS 重型编辑器,违背你全 Rust、轻量化的初衷
2. **不要自研 CRDT**
直接用 yrs,成本差 100 倍,稳定性天壤之别
3. **块渲染用 Leptos 组件拆分**
避免单组件巨量渲染,性能对标原生飞书
4. **Axum 只做同步与存储**
块编辑逻辑全部放在前端 WASM,后端无状态,横向扩展极简单
如果你需要,我可以直接给你:
- 可运行的 `Axum + Leptos + yrs` 最小工程模板
- 块编辑器的完整 Leptos 组件代码(含拖拽、回车拆块、删除块)
- 块数据结构与 Axum 接口定义
# 全 Rust 替代 BlockNote 方案 & 针对性推荐
你现在的核心诉求非常明确:**自研类 Notion/飞书/思源笔记**,嫌弃 BlockNote 前端太重、JS 依赖重,想要**全 Rust 栈、轻量化、块编辑核心可控**。
我直接按「**落地优先级**」给你方案,所有方案均彻底摆脱 BlockNote 这类重型 JS 编辑器,核心逻辑/UI 尽可能全 Rust,适配 Tauri 桌面端(你做笔记软件几乎必用)。
---
## 核心结论先给
**最优解:Tauri + eguiRust 原生 UI+ yrs + blocks crate**
100% 全 Rust,零重型 JS 依赖,块编辑性能、体积、定制性拉满,完美对标 Notion/飞书块结构。
---
# 一、可直接替换 BlockNote 的 Rust 块编辑方案(按推荐度排序)
## 1. 首选:egui 生态 + 自研块系统(桌面端 Tauri 天花板)
**完全抛弃 JS 编辑器,纯 Rust 原生 UI 做块渲染,最轻量、最可控**
- 核心 UI**egui**Rust 即时模式 UI,跨桌面/Web,无任何 JS runtime
- 块编辑组件:
- `egui-notes` / `egui-notion`:现成类 Notion 块编辑,支持嵌套块、标题/列表/代码块、拖拽、撤销重做
- `egui-rich-text` + 自定义块封装:灵活度拉满,可自定义数据库块、嵌入块、看板等思源/飞书特色块
- 适配:Tauri 桌面、纯原生、Wasm Web 三端通用
- 优势:**零 JS 臃肿、编译体积极小、性能碾压 BlockNote、可深度魔改**
- 劣势:需自己封装少量交互(快捷键、块拖拽),但工作量远小于适配 BlockNote
## 2. 次选:Rust Wasm 前端框架 + 极简块编辑(Web/跨端)
适合你需要 Web + 桌面双端,全 Rust Wasm 编译,无重型前端框架
- Leptos + `leptos-editor`Rust 顶流 Web 框架,Wasm 原生)
- Yew + `yew-editor`(类 React Rust 前端,块编辑组件)
- Dioxus + `dioxus-rich-text`(语法友好,块扩展简单)
- 优势:全 Rust 类型安全,Web/桌面通吃,比 BlockNote 轻 90%
- 劣势:Wasm 体积略大于纯 egui 原生
## 3. 兜底:现成全 Rust 笔记项目(直接抄架构,少造轮子)
直接剥离块编辑核心,用到你的项目里,最快落地
- **lume-note**Rust + Tauri + egui,轻量块笔记,Tauri 适配完美
- **neon-notes**:纯 egui 块笔记,代码极简,二次开发成本极低
- **AppFlowy Core**:剥离其 Rust 块引擎/协作/存储,前端用 egui 重写(抛弃 Flutter
---
# 二、块编辑底层核心库(全 Rust,必用,替代 BlockNote 底层逻辑)
不管选哪种 UI,**块数据、协作、存储** 全用 Rust 库,彻底摆脱 JS 状态管理:
1. **yrs**(⭐最高,Yjs Rust 实现)
块级 CRDT 协作,Notion/飞书多人协作核心,Rust 原生,性能远超 JS 版
2. **blocks crate**
Rust 原生块模型引擎:块嵌套、序列化(Markdown/JSON)、diff/merge、撤销重做
3. **redb/sled**
Rust 嵌入式数据库,本地存储块数据,本地优先架构
4. **tantivy**
Rust 全文搜索,实现思源/Notion 级全局搜索
---
# 三、最终落地推荐组合(2 套直接用)
## 组合 1:【极致全 Rust 桌面端】⭐⭐⭐⭐⭐(最适合你)
**Tauri + egui + yrs + blocks crate**
- 前端渲染:egui(纯 Rust,零 JS,块编辑 UI)
- 块核心:blocks crate(块结构、嵌套、序列化)
- 协作同步:yrs(Rust CRDT,块级协作)
- 本地存储:redb
- 打包:Tauri
- 适配场景:自研桌面笔记,对标 Notion/飞书/思源,全 Rust 无冗余
## 组合 2:【Web+桌面双端】⭐⭐⭐⭐
**Leptos(Wasm) + yrs + Tauri**
- 前端:LeptosRust 编译 Wasm
- 块编辑:自定义 Leptos 块组件
- 核心:yrs + blocks
- 适配场景:需要网页版 + 桌面版的笔记工具
## 组合 3:【快速上线】⭐⭐⭐
**魔改 lume-note 架构**
直接用其 Tauri + egui 块编辑底座,替换核心逻辑为你的需求,零从头搭建成本
---
# 四、避坑关键(针对你的重构场景)
1. **彻底放弃 BlockNote/Tiptap 相关 JS 绑定**
本质还是 JS 重型依赖,违背你全 Rust 化的初衷
2. **优先用 yrs 做块协作**
这是 Notion 类编辑器的核心,Rust 实现比 JS 更稳定高效
3. **egui 是桌面端最优解**
比 Wasm 前端更轻,无浏览器兼容问题,Tauri 集成无缝
4. **块模型直接用 blocks crate**
省去自己写块嵌套、序列化的大量重复代码
以下是 **Axum + Leptos + yrs + blocks** 方案的全部核心链接(官方仓库、文档、crates.io),按模块整理,方便你直接集成:
---
## 一、Leptos 前端框架(核心)
- **Leptos 官方仓库**https://github.com/leptos-rs/leptos
- **Leptos 官网**https://leptos.dev
- **Leptos 文档**https://book.leptos.dev
- **crates.io**https://crates.io/crates/leptos
---
## 二、块编辑核心库
### 1. yrsCRDT 协作,必用)
- **yrs 官方仓库(y-crdt**https://github.com/y-crdt/y-crdt
- **crates.io**https://crates.io/crates/yrs
- **文档**https://docs.rs/yrs/latest/yrs/
### 2. blocks crate(块模型)
- **blocks 仓库**https://github.com/brenogonzaga/blocks
- **crates.io**https://crates.io/crates/blocks
### 3. Leptos 编辑器/富文本(可选)
- **leptos-editor**(轻量富文本):https://github.com/leptos-rs/leptos-editor
- **awesome-leptos**(生态列表):https://github.com/leptos-rs/awesome-leptos
---
## 三、Axum 后端
- **Axum 官方仓库**https://github.com/tokio-rs/axum
- **Axum 文档**https://docs.rs/axum/latest/axum/
---
## 四、配套工具(存储/搜索/同步)
- **redb(嵌入式存储)**https://github.com/redb/redb
- **tantivy(全文搜索)**https://github.com/quickwit-oss/tantivy
- **tokio-tungsteniteWebSocket**https://github.com/snapview/tokio-tungstenite
---
## 五、示例与模板(直接抄)
- **leptos-yrs 示例**https://github.com/leptos-rs/leptos/tree/main/examples/yrs
- **leptos + axum 全栈模板**https://github.com/leptos-rs/leptos-axum-starter
- **yrs + WebSocket 同步示例**https://github.com/y-crdt/y-crdt/tree/main/examples/websocket
---
需要我基于这些库,给你生成一个可直接运行的 **Axum + Leptos + yrs** 最小块编辑器模板吗?
@@ -0,0 +1,568 @@
# [recycle] mnote 从 Runtime 壳回到 Wolai 目标页的纠偏清单 v1
> 更新时间:2026-04-18
>
> 最终交付对标基准:
> - 本地目标截图:`/mnt/Data1T/mnote/tmp/image copy 14.png`
> - 目标页面:`https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd`
> - 当前偏移截图:`/mnt/Data1T/mnote/tmp/image copy 13.png`
>
> 说明:
> - `dokobot` 当前只能稳定读到 Wolai 的 SPA 外壳标题,拿不到正文结构。
> - 因此本文以本地目标截图 `image copy 14.png` 作为最终视觉与交互验收基准。
## 1. 这份文档解决什么问题
当前代码和阶段文档虽然把“最小可写 runtime 闭环”做通了,但实际交付物仍然是一个:
- `iframe` 挂载的独立文档壳
- 带 `Document Editor Shell` 标题的调试页
- 带 revision/save/focus/selection/hover/event log 的 runtime 面板
- 基于 `textarea + 按钮工具条 + 块卡片` 的工程壳
它和目标页的差距,不是“差一点样式”,而是:
- 页面架构不对
- 交互壳不对
- 验收标准不对
- 默认主路径不对
本文的目的,是把后续交付标准重新冻结为:
> **最终默认交付必须逼近 `image copy 14.png` 这种一体化 Wolai 页面,而不是继续优化 `image copy 13.png` 这类 runtime 调试壳。**
---
## 2. 最终交付标准
最终默认交付以目标截图为准,至少必须满足下面这些显性特征。
### 2.1 页面结构标准
- 左侧是产品级导航树,而不是单独文档调试页。
- 中间是文档页面本身,不通过 `iframe` 二次嵌套。
- 顶部是轻量面包屑 / 导航动作,不出现 runtime 诊断信息。
- 正文区域是沉浸式页面画布,不出现工程面板、状态卡、调试日志。
- 右上角是产品动作区,不是编辑器内部调试控件区。
### 2.2 编辑体验标准
- 页面标题是正文的一部分,视觉上属于页面内容,而不是壳标题。
- 首块内容直接以内联块形态出现,不是“卡片块列表 + badge + tag”。
- 占位文案应以内联方式出现在正文里,例如“输入 `/` 选择,按 `空格` 打开 AI...”,而不是单独的控制台提示区。
- 勾选框、标题、段落、列表在正文中应呈现为自然文档流,不应被包成工程卡片。
- slash、引用、缩进、折叠等交互应在光标附近或块上下文中触发,不依赖页顶按钮排布。
### 2.3 禁止出现的元素
默认交付页中禁止出现:
- `Document Editor Shell`
- `revision=`
- `updatedAt=`
- `save=`
- `focus=`
- `selection=`
- `hover=`
- `交互状态`
- `最小状态机`
- `最近事件`
- `block_editor_shell.ready`
- 页顶固定一排 `Slash 菜单 / 折叠标题 / 缩进 / [[page]] / ((block))` 按钮
- `Paragraph / Heading / Todo` 这种脱离光标上下文的演示按钮
---
## 3. 当前偏移的根因
### 3.1 默认主路径被切成了 `iframe` 文档壳
当前默认文档页在命中文档壳开关后,直接返回 `MnoteWebDocumentShellHost`,并在其中挂一个 `iframe`
相关代码:
- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx:183`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/mnote-web-document-shell-host.tsx:39`
这一步本身就决定了它不会长成目标截图那种“一体化页面”。
### 3.2 当前 `/document` 路由本质是 runtime 调试页
当前 Rust 文档页直接输出了:
- `Document Editor Shell`
- hero 说明
- meta pills
- status card
- toolbar buttons
- interaction sidebar
- event log
相关代码:
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:790`
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:805`
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:821`
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:838`
这不是“产品 UI 还没 polish”,而是“页面职责定义错了”。
### 3.3 当前块渲染是工程卡片流,不是正文文档流
当前每个 block 是:
- `li.editor-row`
- 左侧 badge
- 中间 title/meta
- 下方 `textarea`
- 右侧 state tag
相关代码:
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:1408`
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:1420`
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:1426`
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:1437`
这类结构适合调试事务,不适合对标 Wolai 页面。
### 3.4 验收标准被收缩成“最小可写闭环”
当前 smoke / harness 的判断标准主要是:
- 文档页里出现 `iframe`
- `iframe` 里出现 `Document Editor Shell`
- 能找到 `textarea[data-block-input-id]`
- 能点击页顶 slash 按钮
- 能看到 event log 标记
相关脚本:
- `/mnt/Data1T/mnote/scripts/task103-document-shell-cutover-smoke.js:17`
- `/mnt/Data1T/mnote/scripts/task103-document-shell-cutover-smoke.js:25`
- `/mnt/Data1T/mnote/scripts/task104-document-runtime-input-smoke.js:29`
- `/mnt/Data1T/mnote/scripts/task104-document-runtime-input-smoke.js:46`
- `/mnt/Data1T/mnote/scripts/task106-document-runtime-slash-reference-smoke.js:34`
相关任务口径:
- `/mnt/Data1T/mnote/harness-tasks.json:2124`
- `/mnt/Data1T/mnote/harness-tasks.json:2248`
- `/mnt/Data1T/mnote/harness-progress.txt:193`
- `/mnt/Data1T/mnote/harness-progress.txt:203`
所以代码确实“通过了验收”,但那个验收根本不是目标页验收。
### 3.5 文档策略与产品目标之间出现了中途收缩
当前主基线文档明确收口到:
- AI-first
- CLI-first
- Rust-native
- 轻量块编辑
- 最小可写闭环
相关文档:
- `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/ai-first-rust-block-editor-baseline-v1.md:19`
- `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/ai-first-rust-block-editor-baseline-v1.md:105`
这条路线适合先打通内核,但不能直接当作“产品默认完成态”。
---
## 4. 纠偏总原则
后续纠偏必须遵循下面四条。
### 4.1 默认交付优先级重排
优先级改为:
1. 默认页面必须像目标截图那样是一体化产品页
2. 编辑交互必须内嵌在页面内容流中
3. Rust command / save chain / projection 继续保留
4. runtime 调试信息只能退到 debug 模式
不能再把“Rust runtime 闭环”放在默认页面形态之上。
### 4.2 `/document` 壳降级为 debug / prototype
`mnote-web /document` 可以保留,但只能作为:
- `?editor=runtime-debug`
- `?editor=prototype`
- 内部调试壳
- transaction / IME / save chain 诊断页
它不能再是默认主编辑器。
### 4.3 `iframe` 方案退出默认主链
默认文档页必须回到页面内原生渲染:
- 不再通过 `iframe` 承载默认编辑器
- 不再让页面主体验依赖另一个独立 HTML 文档
- 不再让产品页布局与编辑器页布局分裂
### 4.4 调试能力后移,不再前置
这些能力应该保留,但只能后移到 debug:
- focus / selection / hover
- event log
- revision / updatedAt / save status
- runtime 状态机说明
- editor command buttons
---
## 5. 必须立即执行的纠偏项
下面这些项不是“优化建议”,而是必须执行的逆转动作。
### 5.1 默认主路径逆转
- [ ] 把默认文档页从 `MnoteWebDocumentShellHost` 切回页面内主渲染路径。
- [ ] `page.tsx` 中默认逻辑不得再优先返回 `iframe` 壳。
- [ ] `mnoteWebDocumentShellEnabled` 只能控制实验入口,不能控制默认编辑器。
- [ ] `?editor=compat` 保留,但新增 `?editor=runtime-debug`,明确实验壳用途。
涉及文件:
- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/mnote-web-document-shell-host.tsx`
### 5.2 `/document` runtime 页角色重命名
- [ ] 把 `Document Editor Shell` 明确改名为 `Runtime Debug Shell` 或等价名称。
- [ ] 页面文案必须承认它是 debug/prototype,而不是默认编辑器。
- [ ] 该页默认不再通过主文档页直达。
涉及文件:
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs`
### 5.3 删除默认页面中的调试区块
默认产品页必须移除:
- [ ] hero 说明区
- [ ] meta pills
- [ ] status card
- [ ] interaction sidebar
- [ ] event log
- [ ] 页面顶部固定命令按钮
注意:
- 调试区块可以迁到 `debug panel`
- 但不能继续留在默认编辑页 DOM 里
### 5.4 页面内编辑器替换卡片流
- [ ] 块渲染必须退出 `li + badge + meta + textarea + state-tag` 这种工程卡片结构。
- [ ] 默认正文必须改成自然文档流:
- 标题就是标题
- 勾选就是勾选
- 段落就是段落
- 列表就是列表
- [ ] 块级信息如 `depth=0 / editable=true / type:paragraph` 只能进入 debug,不得出现在正文。
- [ ] 正文留白、字号、宽度、块间距必须向目标截图靠拢。
---
## 6. 分阶段纠偏清单
## 阶段 A:先把默认主路径拉回产品页
### 目标
先解决“默认打开文档时为什么像独立调试站点”。
### Checklist
- [ ] 默认文档页退出 `iframe` 模式。
- [ ] 页面回到 `DocumentShell -> DocumentContent` 一体化主链。
- [ ] 保留 runtime 壳,但只允许通过显式 debug 参数进入。
- [ ] 主路径不再展示 `Document Editor Shell` 文案。
- [ ] 主路径不再依赖 `mnote-web-document-shell-host.tsx`
- [ ] 补回归:
- 默认文档页 DOM 中不再存在 `iframe[title^="mnote-web-document-shell"]`
- `editor=runtime-debug` 时才允许出现 iframe 或独立壳
### 完成判定
- 用户打开页面时,首先看到的是产品页,而不是 runtime 站点。
## 阶段 B:恢复 Wolai 式页面骨架
### 目标
把“产品页壳”恢复到目标截图的感觉。
### Checklist
- [ ] 左侧继续复用现有页面树/导航系统,不为编辑器单独造站。
- [ ] 顶部保留产品导航:
- 面包屑
- 返回/前进
- 右上角产品动作
- [ ] 页面标题回到正文主列内部,而不是 debug 页标题。
- [ ] 正文宽度、留白、顶部间距对齐目标截图。
- [ ] 页面空白区域保留沉浸式阅读/编辑氛围,不塞调试信息。
- [ ] 若现有 compat 页骨架更接近目标截图,优先复用它的产品壳,而不是继续美化 runtime 壳。
### 完成判定
- 不看功能,只看静态截图,页面也应该更像 Wolai 页面而不是内部工具页。
## 阶段 C:把编辑器从“卡片列表”改成“正文文档流”
### 目标
解决当前最刺眼的视觉偏移。
### Checklist
- [ ] 去掉块左侧 badge 序号。
- [ ] 去掉块右侧 `block:id / type:xxx` 标签。
- [ ] 去掉每块上方 `Paragraph / depth=0 / editable=true` 元信息。
- [ ] 标题块渲染为正文中的标题,不再单独加工程 title 行。
- [ ] todo 块渲染为真实勾选项。
- [ ] `textarea` 若继续保留,只能作为无边框、无卡片感的内联输入宿主。
- [ ] 若 `textarea` 无法支撑自然正文体验,则必须把 `kode` 真正接入“块内输入器”层,而不是只停留在文档参考。
- [ ] `[[page]]` / `((block))` 的插入入口改成光标附近上下文,不再只靠页顶按钮。
- [ ] slash 菜单改为光标上下文菜单。
### 完成判定
- 用户看到正文时,应先感知“文档内容”,而不是“块调试单元”。
## 阶段 D:把交互从“页顶按钮”改成“块内上下文”
### 目标
解决当前交互方式明显不像 Wolai 的问题。
### Checklist
- [ ] 去掉页顶固定 `Slash 菜单 / 折叠标题 / 缩进 / [[page]] / ((block))` 按钮。
- [ ] slash 触发改为输入 `/`
- [ ] 页面引用改为输入 `[[`
- [ ] 块引用改为输入 `((` 或等价上下文入口。
- [ ] 缩进 / 反缩进改为 `Tab / Shift+Tab` 主触发。
- [ ] 标题折叠优先放在标题左侧 affordance,而不是页顶全局按钮。
- [ ] 块 hover 菜单应轻量化,只在块附近出现。
- [ ] 保留调试按钮,但移入 debug 模式或开发者工具抽屉。
### 完成判定
- 核心编辑动作不依赖页面级按钮栏。
## 阶段 E:调试能力彻底后移
### 目标
让默认产品页彻底脱离 runtime 调试感。
### Checklist
- [ ] `revision / save status / focus / selection / hover` 移到 debug 面板。
- [ ] `event log` 移到 debug 面板。
- [ ] `最小状态机` 文案移到 debug 面板。
- [ ] debug 面板默认关闭,只在显式调试开关下出现。
- [ ] 生产态 DOM 中不出现 `block_editor_shell.ready` 等字符串。
### 完成判定
- 默认页面截图不再暴露任何 runtime 术语。
## 阶段 F:验收标准重写
### 目标
防止再次出现“工程闭环通过了,但产品页仍然不对”的误判。
### Checklist
- [ ] 废弃以 `iframe + Document Editor Shell + textarea` 为成功条件的默认验收。
- [ ] 重写 `task103~107` 类 smoke 的默认成功条件。
- [ ] 新增产品级验收断言:
- 默认文档页没有 `iframe[title^="mnote-web-document-shell"]`
- 页面不存在 `Document Editor Shell`
- 页面不存在 `交互状态`
- 页面不存在 `最近事件`
- 页面存在产品导航树
- 页面标题在正文主列内
- 第一块内容以内联块形式出现
- 占位文案直接出现在正文中
- slash 菜单由输入 `/` 触发
- [ ] 新增截图验收:
- 至少保留一组默认页截图
- 用于与目标截图做人工对照
- [ ] Final QA 的表述从“最小可写闭环”改为“产品级默认编辑页达到目标页标准”。
### 完成判定
- 测试通过时,默认页面也必须像目标页,而不是仅仅可写。
---
## 7. 参考层在这次纠偏中的正确用法
这次纠偏不是要推翻 Rust 内核,而是要纠正“参考层只停留在文档里,没有真正进入主路径”的问题。
### `edita-core`
继续用于:
- command 容器
- transform / transaction 思路
不用于:
- 默认产品页 UI
### `blocks`
继续用于:
- block model
- import/export
- history / diff / merge
不用于:
- 正文视觉与交互壳
### `kode`
这次必须优先重新评估它是否能真正承担:
- 块内输入器
- Markdown/WYSIWYG 内联编辑
- 光标附近上下文交互
如果 `textarea` 无法做出目标页的正文感,`kode` 不能再只停在“设计参考”。
### `leptos-tiptap`
继续作为 fallback
- 当 `kode` 与自有块编排组合后仍无法提供足够自然的编辑体验时
- 用来验证 Leptos 对接成熟编辑 runtime 的现实成本
### `mnote-next`
只继续作为:
- 交互样例
- 历史反例
不再作为:
- 视觉或产品交付参考
---
## 8. 关于 “Tiptap 体验” 的补充判断
这一条必须单独说清。
### 8.1 仅执行本文前面的纠偏项,不足以自动获得 Tiptap 级体验
原因很直接:
- 前文主要纠正的是“默认页面壳”
- 纠正的是 `iframe`、调试面板、卡片块流、验收标准
- 这些动作可以让页面更像 Wolai
- 但它们不会自动提供 `Tiptap / ProseMirror` 那种输入体验内核
如果底层仍然是:
- `textarea` 作为主要输入宿主
- 自己拼 selection / composition / slash / token / hover 菜单
- 自己再把这些交互拼成正文体验
那么最终结果最多是:
- “更像 Wolai 的产品壳”
- “更自然的块 Markdown 编辑器”
而不是天然等于:
- “接近 Tiptap 的成熟输入体验”
### 8.2 如果最终目标明确是“类似 Tiptap 的体验”,应该把 `leptos-tiptap` 从 fallback 提升为重点实施分支
当下面这些诉求成立时,建议把 `leptos-tiptap` 提升为主候选,而不是继续只把它当 fallback:
- 你要的是接近 Wolai / Tiptap 的输入手感
- 你重视成熟 selection / composition / inline behavior
- 你希望 slash、placeholder、mark、node 行为更接近现成富文本编辑器
- 你不希望长期自己维护 `textarea + 自拼交互` 这条路
这时更合理的路线是:
1. 产品页壳按本文前半部分纠偏,先回到一体化页面
2. 人类编辑 surface 优先改成 `leptos-tiptap`
3. Rust kernel 继续保留为事实源、命令层、AI/CLI 层
4. 通过 adapter 把 `Tiptap JSON / command / selection event` 映射到 Rust block model / command
### 8.3 更准确的架构收口应该是“双层分工”,而不是二选一
如果采用 `leptos-tiptap`,建议分工固定为:
- `Tiptap / leptos-tiptap`
- 负责人类交互体验
- 负责 selection、composition、placeholder、toolbar/slash、inline behavior
- Rust editor core / kernel
- 负责事实源
- 负责命令合同
- 负责 AI/CLI 共用写链
- 负责导入导出、审计、projection、持久化
也就是说:
> **对“人类编辑体验”妥协给 `Tiptap`,不等于对“系统语义主导权”妥协给 `Tiptap`。**
### 8.4 什么时候不该上 `leptos-tiptap`
只有在下面这些条件同时成立时,才继续坚持纯 Rust-native 输入壳:
- 你接受第一版明显不像 Tiptap,只求最小可写
- 你接受更长时间去补 selection / IME / inline interaction
- 你把 Rust-native 纯度放在“像 Wolai/Tiptap 的体验”之前
而你当前这一轮明确表达的是:
> **你真正想要的是类似 Tiptap 的体验。**
在这个前提下,继续默认押注 `textarea + 自拼交互`,风险会非常高。
### 8.5 本文后的新增执行要求
基于上面的判断,后续纠偏不应只做“页面壳纠偏”,还应新增一个并行分支:
- [ ] 新增 `Tiptap 体验纠偏` 分支文档
- [ ] 用 `leptos-tiptap` 做一个真正的页面内 spike,而不是停留在 reference-code
- [ ] 对比三条路线:
- `textarea + 自拼交互`
- `kode` 块内输入器
- `leptos-tiptap`
- [ ] 以“接近目标页体验”而不是“谁更 Rust-native”作为第一判断标准
- [ ] 若 `leptos-tiptap` 的 spike 明显更接近目标页,应把它提升为默认人类编辑 surface 方案
---
## 9. 最终一句话收口
后续默认交付口径固定为:
> **Rust editor core 可以继续最小化,但默认页面不能再像 runtime 调试站点;默认页面必须回到 Wolai 式一体化产品页,runtime 壳只保留为 debug/prototype。**
如果进一步收紧到“体验也要接近 Tiptap/Wolai”,那么还要再补一句:
> **页面壳纠偏只能解决“像不像产品页”,不能单独解决“像不像 Tiptap”;若目标是 Tiptap 级体验,应认真把 `leptos-tiptap` 提升为主候选,而不是继续只放在 fallback。**
@@ -0,0 +1,129 @@
# [recycle] BlockNote Migration Plan v1
> 更新时间:2026-04-18
## 1. 目标
这份文档用于冻结从 BlockNote 主路径迁移到 Rust block editor 的最小迁移顺序。
目标不是一次性删除所有旧代码,而是:
- 先让主文档页默认切到新 Rust editor 壳
- 再把 BlockNote 降到 compat/debug
- 最后清理旧依赖和主线耦合
## 2. 当前盘点
当前主路径仍直接依赖下面这些入口或依赖:
- `@blocknote/core`
- `@blocknote/react`
- `@blocknote/mantine`
- `blocknote-editor.tsx`
- `schema.ts`
- `HocuspocusProvider`
- `Yjs`
主路径宿主文件主要是:
- `wolai-frontend/src/components/editor/document-content.tsx`
- `wolai-frontend/src/components/editor/blocknote-editor.tsx`
- `wolai-frontend/src/components/editor/schema.ts`
## 3. 迁移分层
迁移固定分三层:
- 主路径
由 Rust block editor 接管
- compat
保留旧 BlockNote 入口,供临时回退
- debug
保留最小诊断入口,帮助排障
禁止继续把 BlockNote 当成默认编辑器。
## 4. 内容格式迁移策略
第一阶段不要求一次性消灭所有旧内容格式。
固定策略如下:
- 主写链以 Rust editor command 和 Rust document model 为准
- 旧 BlockNote JSON 通过兼容 codec 读取
- 写回时优先生成 Rust 侧统一结构
- 必要时保留从旧格式到新格式的单向迁移适配
重点对象:
- 段落
- 标题
- list/todo
- `pageReference`
- `blockReference`
- 附件和重型自定义块
## 5. 入口切换顺序
第一步:
- 在文档页入口切到 `mnoteWebDocumentShellEnabled`
- 默认走 `mnote-web /document`
- 保留 `?editor=compat` 回退参数
第二步:
- `document-content.tsx` 继续保留为 compat 宿主壳
- `blocknote-editor.tsx` 继续保留为 compat 编辑器实现
第三步:
- 新主编辑器稳定后,再把主路径对 `schema.ts``HocuspocusProvider``Yjs` 的依赖收缩到 compat/debug
## 6. 依赖收敛
迁移完成前后要明确区分:
- 主路径是否仍依赖 `@blocknote/core`
- 主路径是否仍依赖 `blocknote-editor.tsx`
- 主路径是否仍依赖 `HocuspocusProvider`
- 主路径是否仍依赖 `Yjs`
最终要求:
- compat/debug 允许暂时保留
- 主路径不再依赖以上能力
## 7. 风险
主要风险如下:
- 旧页面内容与新结构双写不一致
- history / snapshot / TOC / stats 边界退化
- 重型块在新主路径下缺少最小宿主能力
- 回退入口不明确导致线上排障困难
## 8. 回退策略
必须保留显式回退:
- 文档页参数级 compat 入口
- 旧 BlockNote 组件继续可挂载
- debug 场景可直接验证旧壳
在没有确认新主路径稳定前,不得直接删除 compat/debug。
## 9. 验收标准
满足以下条件即可视为 migration v1 达标:
- 主文档页默认落到新 Rust editor 壳
- compat/debug 入口仍可独立使用
- `blocknote-editor.tsx` 不再是默认主路径
- `schema.ts` 不再决定默认主编辑器
- `@blocknote/core`
- `blocknote-editor.tsx`
- `schema.ts`
- `HocuspocusProvider`
- `Yjs`
已被明确标记为 compat/debug 或待删除对象
@@ -0,0 +1,140 @@
# [recycle] mnote Rust Block Editor Blocks Spike v0
> 更新时间:2026-04-18
## 1. 目标
本 spike 聚焦 `blocks` 参考层是否适合承担:
- Markdown
- round-trip
- history
- diff
- merge
以及哪些地方存在 `缺口`
## 2. 参考入口
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/document.rs`
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/block.rs`
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/converters.rs`
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/history.rs`
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/diff.rs`
## 3. 适配性结论
### 3.1 Markdown
`blocks` 最适合直接借的是 Markdown 相关思路。
原因:
- 已有文档模型
- 已有 converter facade
- 方向上天然适合 Markdown import/export
这意味着第一阶段不应该自己先写一套新的 Markdown parser 胶水。
### 3.2 round-trip
`Markdown -> blocks -> Markdown round-trip``blocks` 最容易先验证的一条链。
`mnote` 的价值:
- 可以快速检查标题、列表、待办、引用、代码块的稳定性
- 可以给 `task-057` 的导入导出提供直接参考
### 3.3 history
`blocks` 的 history 适合直接借思路,不建议第一阶段重写。
至少在 `undo / redo` 的双栈组织上,它比从零发明更稳。
### 3.4 diff / merge
`blocks` 的 diff / merge 很适合做 AI-first 和 CLI-first 后续阶段的基础参考。
第一阶段不要求完整采用,但必须明确后续方向,否则 AI 重写链会缺审计基础。
## 4. 主要缺口
### 4.1 文档模型仍偏扁平
当前 `blocks::Document``Vec<Block>` 为主。
`mnote` 要的不是简单扁平列表,而是:
- 块树
- children
- 自定义 `BlockProps`
- 引用 token
- 字符串 ID
这就是第一大 `缺口`
### 4.2 BlockType 与 mnote 目标不完全一致
`blocks``BlockType` 更偏通用编辑器,不直接覆盖:
- `page_reference`
- `block_reference`
- `media_placeholder`
- `progress_placeholder`
因此不能直接拿来当最终枚举。
### 4.3 默认 ID 体系不适合
`blocks``Uuid`
`mnote` 当前更适合:
- `DocumentId = String`
- `BlockId = String`
因为要跟现有页面、引用、CLI、AI 命令保持统一可读标识。
## 5. Spike 结论
`blocks` 的定位应固定为:
- 采用:Markdown 思路
- 采用:round-trip 参考
- 采用:history 思路
- 采用:diff / merge 思路
- 不直接采用:最终 block model
- 不直接采用:最终 ID 体系
## 6. 建议落地方式
建议在 `mnote-editor-core` 中这样接:
- 自定义 `EditorDocument`
- 自定义 `EditorBlockNode`
- 自定义 `EditorBlockType`
- 用 `blocks` 作为后续 `markdown/history/diff/merge` 参考或局部依赖
不要反过来把 `mnote` 的核心模型强行塞进 `blocks`
## 7. 第一阶段保留判断
第一阶段可接受的保守做法:
- 先不把 `blocks` 作为正式依赖
- 但在文档中冻结:后续 Markdown、history、diff、merge 优先复用其思路
若到 `task-057` 时验证表明 `blocks` 已足够稳定,再引正式依赖。
## 8. 完成判定映射
`task-052` 的完成判定要求本文显式包含:
- Markdown
- round-trip
- history
- diff
- merge
- 缺口
当前文档已满足这些项。
@@ -0,0 +1,137 @@
# [recycle] mnote Rust Block Editor Edita Spike v0
> 更新时间:2026-04-18
## 1. 目标
本 spike 只回答一个问题:
`edita-core``Editor / Block / Command / 无头执行` 边界,是否适合作为 `mnote` 新主编辑器第一阶段的命令核参考。
结论先写:
- 适合直接借 `Editor`
- 适合直接借 `Command`
- 适合借 `Block` 作为注册与分发思路
- 适合借 `无头执行`
- 不适合直接拿来当最终 `mnote` block document 模型
- 不适合直接拿来当最终前端胶水
## 2. 参考入口
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/edita/edita-core/src/lib.rs`
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/edita/edita/src/editor.rs`
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/edita/edita/src/state.rs`
## 3. 直接可复用面
### 3.1 `Editor`
`Editor<Node, State, Input>` 的价值不在 UI,而在:
- 持有统一 `state`
- 提供统一命令入口
- 提供 block registry
- 允许 fallback block
这和 `mnote` 第一阶段要的“无头编辑器容器”是对齐的。
`mnote` 的映射建议:
- `Node` 不直接映射前端 DOM 节点
- `State` 映射 `EditorDocument`
- `Input` 第一阶段不必暴露给外部 UI,可先收口成 `EditorCommandEnvelope`
### 3.2 `Command`
`Command<State>` 很适合当 `mnote-editor-core` 的最小命令执行边界。
建议保留这层思想,但不要照搬 trait 名:
- `replace_block`
- `insert_block_after`
- `delete_block`
- `split_block`
- `merge_with_previous`
- `move_block`
- `indent_block`
- `outdent_block`
- `toggle_heading_collapse`
这些命令应当最终作用在 `EditorDocument` 上,而不是作用在 Leptos 组件状态上。
### 3.3 `Block`
`Block` trait 适合借来做“块类型注册 + 输入接管”思路,但当前实现更偏 parser pipeline。
`mnote` 第一阶段不建议直接照抄,因为我们更需要:
- 树形 block document
- 固定字符串 ID
- `BlockProps`
- `content_node`
- 引用 token
因此这里只借“块处理器是可注册单元”的思路。
### 3.4 无头执行
这是最值得采用的点。
新主编辑器必须先做无头执行,因为:
- CLI 要共用
- AI 要共用
- Web UI 只是壳
因此 `edita-core` 的无头执行边界适合直接进入采用矩阵。
## 4. 不直接采用的部分
### 4.1 不直接采用其最终 block 形态
原因:
- `mnote` 要的是块树,不是只围绕 parser block 运转
- `mnote` 要保留 `page_reference / block_reference / media_placeholder / progress_placeholder`
- 还要和 kernel `content_node` 边界对齐
### 4.2 不直接采用其 UI/editor 外壳
原因:
- 我们的 UI 壳后面要接 Leptos / kode
- `edita-core` 当前更适合做 headless 胶水参考
## 5. Spike 结论
`edita-core``mnote` 第一阶段的定位固定为:
- 采用:`Editor`
- 采用:`Command`
- 部分采用:`Block`
- 采用:`无头执行`
- 不采用:最终产品 UI
- 不采用:最终文档模型
## 6. 后续胶水建议
后续实现时,建议在 `mnote-editor-core` 里形成下面三层:
1. `EditorDocument`
2. `EditorCommand`
3. `EditorRuntime`
其中:
- `EditorRuntime``edita-core` 的无头执行边界
- `EditorCommand``Command`
- `EditorDocument` 不直接复用 `edita-core` block 结构,而是自定义
## 7. 本结论如何进入后续任务
- `task-051` 完成判定:
- 本文明确写清 `Editor / Block / Command / 无头执行 / 胶水`
- `task-055` 开始时:
- `mnote-editor-core` 优先实现无头编辑器容器
- 不先实现 Leptos UI
@@ -0,0 +1,192 @@
# [recycle] Rust Block Editor Model v0
> 更新时间:2026-04-18
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/ai-first-rust-block-editor-baseline-v1.md`
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/rust-block-editor-phase0-baseline-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/edita/edita-core/src/lib.rs`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/document.rs`
> - `/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs`
## 1. 目的
本文冻结 `task-053` 的第一阶段 Rust block editor 模型口径。
目标不是一次定义最终全文档系统,而是先确定:
- 首批 Rust block type
- `BlockProps` 的最小字段面
- `reference token` 的表示方式
- `content_node` 的载荷格式
- 哪些内容明确退出对 BlockNote JSON 的长期依赖
## 2. 第一阶段 block type
第一阶段固定以下块类型进入 canonical model
- `paragraph`
- `heading`
- `bullet_list_item`
- `numbered_list_item`
- `quote`
- `todo`
- `code_block`
- `page_reference`
- `block_reference`
补充说明:
- `paragraph` 是默认文本块。
- `heading` 是标题块,级别收敛到 `BlockProps.heading_level`
- `bullet_list_item``numbered_list_item` 先保留为列表项,而不是先做复杂 list container。
- `todo` 用来承接当前 `advancedTodo` 的第一阶段简化版。
- `page_reference``block_reference` 先作为稳定引用块进入模型。
下面这些不进入第一阶段 canonical model,只保留 placeholder 或 compat
- `media`
- `progressMeter`
- `mindmap`
- `onlineTable`
## 3. `BlockProps`
第一阶段 `BlockProps` 固定为轻量可扩展对象。
核心字段:
- `indent`
- `heading_level`
- `checked`
- `collapsed`
- `language`
- `reference_target_id`
- `reference_label`
- `reference_token_strategy`
- `extra`
固定口径:
- `indent` 表示有限缩进层级,不承诺长期树协议。
- `heading_level` 只对 `heading` 生效。
- `checked` 只对 `todo` 生效。
- `collapsed` 只对 `heading` 生效,用于折叠标题。
- `language` 只对 `code_block` 生效。
- `reference_target_id``reference_label` 服务 `page_reference` / `block_reference`
- `extra` 只用于迁移期兼容字段,不能成为长期语义事实源。
## 4. `reference token` 策略
第一阶段引用 token 固定区分两类:
- `page_reference`
- `block_reference`
第一阶段 token strategy 固定支持三种:
- `double_bracket`
- `double_paren`
- `inline_chip`
对应语义:
- `double_bracket` 对应 `[[page]]`
- `double_paren` 对应 `((block))`
- `inline_chip` 只用于 UI 渲染或迁移期兼容,不作为长期文本事实源
引用 token 最小字段:
- `kind`
- `strategy`
- `target_id`
- `label`
- `raw_token`
## 5. `content_node` 载荷格式
第一阶段 `content_node` 是 block 内内容的最小结构单元。
固定 payload 类型:
- `text`
- `hard_break`
- `reference_token`
### 5.1 `text`
`text` 节点字段:
- `text`
- `marks`
`marks` 第一阶段只支持:
- `bold`
- `italic`
- `underline`
- `strike`
- `code`
### 5.2 `hard_break`
`hard_break` 用于显式换行,不引入复杂段内结构树。
### 5.3 `reference_token`
`reference_token` 节点直接挂稳定 token 对象,而不是把引用仅留在原始文本里。
## 6. 块对象与文档对象
第一阶段 canonical block 结构固定为:
- `block_id`
- `block_type`
- `props`
- `content_nodes`
- `child_block_ids`
第一阶段 canonical document 结构固定为:
- `document_id`
- `root_block_ids`
- `blocks`
设计原则:
- `blocks` 先按稳定数组序列输出,便于命令执行和导入导出。
- `child_block_ids` 先表达父子关系,不强求复杂树索引结构。
- `content_nodes` 替代对 BlockNote inline JSON 的长期依赖。
## 7. 与 BlockNote JSON 的边界
第一阶段固定口径:
- BlockNote JSON 只作为迁移期外部格式
- Rust editor model 才是长期事实层
- `BlockProps``content_node``reference token` 必须能独立表达核心语义
换句话说:
- 可以保留 `BlockNote JSON -> Rust model` 适配器
- 不能继续把 BlockNote JSON 当作长期 canonical schema
## 8. 第一阶段不解决的事
下面这些明确不在 `task-053` 解决:
- 多人协作状态
- 复杂 mark 树
- 富媒体完整属性系统
- `mindmap` / `onlineTable` 的完整内嵌编辑语义
- 完整批量选择与多块复制粘贴
## 9. 结论
`task-053` 的固定口径是:
- 用首批 `paragraph``heading``bullet_list_item``todo``page_reference``block_reference` 等块型建立 Rust canonical model
- 用 `BlockProps` 收拢最小块属性
- 用 `reference token` 固定 `[[page]]` / `((block))` 的稳定表示
- 用 `content_node` 承接块内文本与引用
- 从这一阶段开始退出对 BlockNote JSON 的长期依赖
@@ -0,0 +1,255 @@
# [recycle] Rust Block Editor Phase 0 Baseline v1
> 更新时间:2026-04-18
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/ai-first-rust-block-editor-baseline-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/`
> - `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/schema.ts`
> - `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx`
> - `/mnt/Data1T/mnote/wolai-frontend/src/lib/blocks/block-ops-adapter.ts`
## 1. 目的
本文用于冻结 task-048 所需的 Phase 0 基线:
1. 盘点当前 BlockNote 自定义块真实范围
2. 盘点文档读写耦合点
3. 冻结第一阶段 Rust Block Editor 的必须做 / 后补 / 延期边界
4. 明确 `reference-code` 的采用、部分采用、不采用依据
Phase 0 的目标不是替换完全部现有能力,而是先把“什么必须进入第一阶段主链、什么只能先保留占位、什么明确延期”说清楚。
## 2. 当前 BlockNote 自定义块清单
当前 `customBlockSchema` 已注册的自定义块如下:
- `heading`
- `advancedTodo`
- `pageReference`
- `blockReference`
- `media`
- `progressMeter`
- `mindmap`
- `onlineTable`
对应实现位置:
- `heading`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/schema.ts`
- 现状:覆盖 BlockNote 默认标题块,允许 1-5 级并支持折叠。
- `advancedTodo`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/AdvancedTodoBlock.tsx`
- 现状:状态为 `todo/doing/done/cancelled`,点击轮转,`Alt+Click` 可直接置为 `cancelled`
- `pageReference`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/PageReferenceBlock.tsx`
- 现状:块级页面引用,依赖 `pageId/title/asChildPage`,点击直接跳转文档页。
- `blockReference`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/BlockReferenceBlock.tsx`
- 现状:块级引用占位,运行时拉 `/api/blocks/get`,仅对段落/标题提供纯文本回写。
- `media`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MediaBlock.tsx`
- 现状:附件卡片,带选择资源、对齐、边框、OCR、下载、OnlyOffice 打开等强 UI 行为。
- `progressMeter`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/ProgressBlock.tsx`
- 现状:支持自动/手动模式;自动模式由编辑器扫描后续 `advancedTodo` 并计算百分比。
- `mindmap`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx`
- 现状:内嵌思维导图壳,包含预览、内联编辑、全屏、独立页、自动保存、资源联动。
- `onlineTable`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/OnlineTableBlock.tsx`
- 现状:内嵌表格预览卡片,支持拖拽改尺寸、删除、全屏编辑。
## 3. 当前读写耦合点
### 3.1 写链耦合
当前编辑主链仍深度绑定 BlockNote:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx`
- `useCreateBlockNote(...)` 直接以 `customBlockSchema` 启动编辑器。
- 保存经 `saveContent(...) -> /api/documents/save` 整体回写块树快照。
- 自定义块的大量副作用都挂在 BlockNote 壳上:
- `progressMeter` 自动统计
- `media` 删除与恢复
- `mindmap` 删除、自动保存、资源广播
- `onlineTable` 删除、全屏切换
- 协作仍挂着 `HocuspocusProvider + Y.Doc`,这与“当前不需要协作”的新主线并不一致。
### 3.2 读链耦合
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
- 文档页在阅读态和编辑态之间切换。
- `initialPageSubtree``serverPageSubtreeSnapshot` 已经进入页面读态,但编辑态仍直接消费 BlockNote 内容快照。
- Phase 0 的关键判断:
- 阅读态已经开始朝稳定 projection 收口。
- 编辑态仍把 BlockNote 的块树和自定义 props 当成事实层。
### 3.3 工具与编辑器模型耦合
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/blocks/block-ops-adapter.ts`
- 当前 AI / server tool 适配只支持 `paragraph``heading` 两种插入规格。
- 这说明自定义块虽然很多,但真正能被稳定工具链操作的只是一小部分。
### 3.4 复杂块与正文块混杂
当前一个 BlockNote 文档同时承载:
- 轻量正文块:`heading`、段落、列表、待办
- 语义引用块:`pageReference``blockReference`
- 重型宿主块:`media``mindmap``onlineTable`
- 派生统计块:`progressMeter`
这会导致第一阶段编辑器如果不先切分边界,就会被复杂块宿主逻辑拖着走。
## 4. 第一阶段迁移范围冻结
### 4.1 必须做
下面这些能力必须进入第一阶段 Rust Block Editor 主链:
- `heading`
- 必须保留 1-5 级标题与折叠语义。
- 原因:这是阅读态结构、目录、AI 引用定位的基础。
- `advancedTodo`
- 必须保留最小四态语义:`todo/doing/done/cancelled`
- 原因:当前 `progressMeter` 依赖它,且任务型记录是高频场景。
- `pageReference`
- 必须保留块级引用能力,并与后续行内页面引用 token 对齐。
- 原因:页面导航和引用挂接是高频主链。
- `blockReference`
- 必须保留“占位插入 + 回显 + 最小跳转”能力。
- 原因:当前产品已存在块引用占位语义,但不要求第一阶段保留远程原位编辑。
- `media`
- 必须保留“附件块占位 + 标题/描述 + 打开原资源”最小能力。
- 原因:附件是文档主链,不应在替换编辑器时丢失。
- 最小块命令集
- 单块编辑
- 插入同级块
- 删除块
- 合并块
- 上移下移
- 有限缩进
- 最小格式收口
- JSON 块快照
- Markdown 导入导出
- Rust command 层稳定执行
### 4.2 后补
下面这些能力保留,但不要求在第一阶段做成完整壳:
- `progressMeter`
- 后补为“派生块”或“只读统计块”。
- 原因:其核心不是富交互编辑,而是基于 `advancedTodo` 的派生结果。
- `mindmap`
- 后补为“占位卡片 + 打开独立编辑器/独立页”。
- 原因:当前实现太重,不应绑进第一阶段核心编辑循环。
- `onlineTable`
- 后补为“占位卡片 + 打开全屏表格编辑器”。
- 原因:表格预览和尺寸拖拽属于宿主 UI,不是轻量块编辑核心。
- `media`
- 第一阶段只保留最小资源卡片。
- OCR、OnlyOffice、复杂菜单、拖拽尺寸属于后补。
- `blockReference`
- 第一阶段只保留占位和跳转。
- 远程拉块、原位编辑、富回显属于后补。
### 4.3 延期
下面这些能力明确延期,不进入第一阶段主链:
- Yjs / Hocuspocus 协作
- BlockNote / ProseMirror 专属菜单状态兼容
- `mindmap` 内联复杂快捷键与嵌入式编辑器行为
- `onlineTable` 内嵌尺寸拖拽与高保真表格交互
- `media` 的完整 OCR / 上传 / replace-storage 流程
- 任何依赖 BlockNote runtime 才能成立的行为
## 5. Phase 0 冻结清单
### 5.1 必须做清单
- [ ] 把 `heading`、段落、列表、基础待办、`advancedTodo` 抽成 Rust editor core 的稳定块模型
- [ ] 把 `pageReference``blockReference` 抽成稳定引用块,而不是继续仅作为 BlockNote 自定义 React block
- [ ] 把 `media` 抽成最小附件占位块,保留资源 id / 标题 / 打开动作
- [ ] 规定 `mindmap``onlineTable` 第一阶段只以宿主占位块进入文档
- [ ] 把块命令统一成 Rust 侧可测试命令,不再把 Enter / Backspace / Tab 语义写死在 BlockNote 事件壳
- [ ] 把保存边界从“直接保存 BlockNote 快照”改为“保存 Rust block document + 最小宿主块 props”
### 5.2 后补清单
- [ ] `progressMeter` 自动统计改成独立派生器
- [ ] `mindmap` 预览投影和独立编辑器接缝
- [ ] `onlineTable` 预览投影和独立编辑器接缝
- [ ] `blockReference` 远程块拉取和最小编辑同步
- [ ] `media` 高级工具栏、OCR、OnlyOffice 入口
### 5.3 延期清单
- [ ] 协作协议
- [ ] BlockNote 级 UI 状态兼容层
- [ ] 重型块的嵌入式内核迁移
## 6. reference-code 采用依据
### 6.1 `edita-core`:采用
采用依据:
- `edita-core/src/lib.rs` 已经给出最小 `Editor / Block / Command` 无头边界。
- 这与第一阶段“先把块命令从 UI 壳里抽出来”的目标完全一致。
- 它不绑定 DOM、ProseMirror、Tiptap,因此适合作为 Rust editor core 的命令容器参考。
不直接采用的部分:
- `edita` UI 壳和示例不是当前主链事实层。
- 原因:`mnote` 需要的是 command / state 组织,不是直接套它的现成编辑器外壳。
### 6.2 `blocks`:采用
采用依据:
- `blocks/src/block.rs``document.rs``converters.rs``history.rs``diff.rs``sanitizer.rs` 已覆盖:
- 块文档模型
- Markdown / HTML / JSON 转换
- history
- diff / merge
- 这正是第一阶段最缺、且不该再自研一套的基础层。
部分保留胶水的地方:
- `mnote``pageReference``blockReference``mindmap``onlineTable` 不是 `blocks` 原生块型,需要补最小扩展映射。
### 6.3 `kode`:部分采用
部分采用依据:
- `kode-core` 的 buffer / selection / editing primitives 可作为块内输入器参考。
- `kode-leptos``MarkdownEditorComponent``TreeWysiwygEditor` 可提供 Leptos 侧输入和光标处理样板。
- `kode-doc` 的 tree node / token position 说明它适合借鉴“结构化编辑器内部表示”。
不直接全量采用的原因:
- `kode-doc` 自带一套更接近富文本树编辑器的内部结构。
- `mnote` 第一阶段不需要把全部文档事实改造成另一套通用 tree editor 语义。
### 6.4 `leptos-tiptap`:部分采用
部分采用依据:
- `src/api/component.rs``use_tiptap_editor.rs``runtime/bridge.rs` 对 Leptos 接第三方 runtime 的方式很清楚。
- 若 Rust-native UI 壳卡住,它适合当 fallback 接缝参考。
不作为主线采用的原因:
- 它本质仍是 Tiptap runtime bridge。
- 第一阶段目标是 Rust-native 轻量块编辑器,而不是再回到 JS 富文本 runtime 作为主事实层。
## 7. Phase 0 结论
Phase 0 的冻结口径如下:
- 第一阶段必须覆盖 `heading``advancedTodo``pageReference``blockReference``media` 的最小稳定语义。
- `progressMeter``mindmap``onlineTable` 进入第一阶段时只保留占位或派生结果,不把完整嵌入式编辑能力一起搬过去。
- 命令核优先采用 `edita-core` 思路,文档模型与转换优先采用 `blocks`,输入壳局部借鉴 `kode``leptos-tiptap` 仅作 fallback 参考。
@@ -0,0 +1,76 @@
# [recycle] AI Tool Cutover Matrix
> 更新时间:2026-04-16
>
> 目标:建立 `builtins` 到 Rust Tool registry 的一一对应矩阵,并明确每个工具当前的割接状态。
## 1. 结论
- 本文覆盖 `docsServerTools``mediaServerTools``mindmapServerTools``onlyofficeServerTools``registryBuiltins``ai-agent/run` 的一一对应关系。
- `docs_search``docs_read` 已进入 Rust tool runtime`ai-agent/run` 在 Convex 模式下直接通过 `executeRustBridgeTool(...)` 执行,`docsServerTools` 仅保留非 Convex / 兼容 fallback。
- `search_web``image_read``slash_run` 已进入 Rust tool registryTS 仅保留最小 transport 壳或数据加载壳。
- `asset_extract_outline``asset_to_mindmap``oo_*` 继续保留 TS transport / 产品编排边界,但 `asset_to_mindmap` 的真正导图写入已收口到 Rust `mindmap_apply_ops`
- AI 工具链的运行态证据已经补齐:`wolai-frontend/src/lib/ai-agent/runtime/runAgent.test.ts` 已验证 `docs_search -> docs_read` 的 runtime 调用顺序与事件流。
## 2. 一一对应矩阵
| builtin tool | Rust toolset | Rust tool name | 当前状态 | 说明 |
| --- | --- | --- | --- | --- |
| `search_web` | `toolset.readonly` | `search_web` | `RUST_OWNER` | 联网检索已完全切到 Rust runtimeTS route 只保留 transport 壳。 |
| `image_read` | `toolset.media_read` | `image_read` | `RUST_OWNER` | 图片/附件 OCR 结果已由 Rust runtime 统一归一化,TS 仅负责最小 transport 取数。 |
| `slash_run` | `toolset.slash_write` | `slash_run` | `RUST_OWNER` | Rust 负责命令解析与结果标准化,TS 只保留创建/重命名 transport 写壳。 |
| `docs_search` | `toolset.docs_read` | `docs_search` | `RUST_OWNER` | Convex 主链已改为 Rust runtime 执行搜索与结果归一化,TS 仅负责拉取搜索数据集 transport。 |
| `docs_read` | `toolset.docs_read` | `docs_read` | `RUST_OWNER` | Convex 主链已改为 Rust runtime 执行裁剪与结果归一化,TS 仅负责读取目标文档 transport。 |
| `rag_lightrag_query` | `toolset.rag_read` | `rag_lightrag_query` | `TS_TRANSPORT_KEEP` | 依赖外部 LightRAG 服务,不在本轮切换主线。 |
| `asset_extract_outline` | `toolset.onlyoffice_read` | `asset_extract_outline` | `TS_TRANSPORT_KEEP` | 仍需附件下载和 MinerU 解析,长期保留 TS transport / 外部服务编排。 |
| `asset_to_mindmap` | `toolset.onlyoffice_write` | `asset_to_mindmap` | `TS_TRANSPORT_KEEP` | 附件大纲提取仍依赖 TS/MinerU,但真正的导图写入已经改走 Rust `mindmap_apply_ops`。 |
| `oo_get_selection` | `toolset.onlyoffice_editor` | `oo_get_selection` | `TS_TRANSPORT_KEEP` | 客户端插件内执行,Web 侧只做回传。 |
| `oo_replace_selection` | `toolset.onlyoffice_editor` | `oo_replace_selection` | `TS_TRANSPORT_KEEP` | 客户端插件内执行,Web 侧只做回传。 |
| `oo_insert_text` | `toolset.onlyoffice_editor` | `oo_insert_text` | `TS_TRANSPORT_KEEP` | 客户端插件内执行,Web 侧只做回传。 |
| `oo_insert_html` | `toolset.onlyoffice_editor` | `oo_insert_html` | `TS_TRANSPORT_KEEP` | 客户端插件内执行,Web 侧只做回传。 |
| `oo_insert_image` | `toolset.onlyoffice_editor` | `oo_insert_image` | `TS_TRANSPORT_KEEP` | 客户端插件内执行,Web 侧只做回传。 |
## 3. Rust registry 对照
- `toolset.readonly``search_web`
- `toolset.media_read``image_read`
- `toolset.slash_write``slash_run`
- `toolset.docs_read``docs_search``docs_read`
- `toolset.rag_read``rag_lightrag_query`
- `toolset.onlyoffice_read``asset_extract_outline`
- `toolset.onlyoffice_write``asset_to_mindmap`
- `toolset.onlyoffice_editor``oo_*`
## 4. 本轮 cutover 结果
本轮已完成:
- 以 `ai-agent/run` 为统一入口,对齐 `registryBuiltins` 与服务端 builtin 实现,避免再长出第二套工具分发表。
- 把 `docs_search``docs_read``search_web``image_read``slash_run` 接到 Rust tool runtime,并在代码侧绑定里冻结为 Rust owner。
- 把 `asset_to_mindmap` 收口为“TS 提纲提取 + Rust 导图写入”的明确边界,不再保留模糊的 `TS_COMPAT_PENDING` 口径。
- 保留 `oo_*``client-tool-result``asset_extract_outline` 这类长期 `TS_TRANSPORT_KEEP` / 产品编排壳。
## 5. 后续维护边界
- `docsServerTools` 继续只服务于非 Convex / fallback 模式,不再承担 Convex 主链真执行。
- `asset_extract_outline` 若未来要继续 Rust 化,应只处理解析链迁移,不涉及当前 Rust tool runtime 的 owner 边界回退。
- `asset_to_mindmap` 若未来继续下沉,可进一步把“大纲转导图 ops”的编排逻辑迁入 Rust;在此之前,它已经是明确的 `TS_TRANSPORT_KEEP`
- `oo_*` 继续保持客户端插件边界,只通过 `/api/ai-agent/client-tool-result` 回传结果。
## 6. 第一批 TS_LEGACY_DELETE 清单
以下旧面已经满足“先停写、再切调用方、最后删除”的文档准备条件,应纳入第一批 `TS_LEGACY_DELETE`
- `documents/create-child`
- `documents/embed`
- `documents/empty-trash`
- `documents/purge`
- `documents/template`
- `mindmap-trash/empty`
- `builtins/**`
删除原则:
- `documents/create-child``documents/embed``documents/empty-trash``documents/purge``documents/template` 仅允许保留 route transport 壳,不再保留 route 内私有业务逻辑。
- `mindmap-trash/empty` 需要在确认 Rust tool/runtime 与统一失败落账稳定后,删除 TS 旧实现。
- `builtins/**` 中已经被 Rust tool runtime 接管的服务端真入口,应先收口到 `ai-agent/run` 或 Rust runtime,再物理删除 legacy helper。
@@ -0,0 +1,132 @@
# [recycle] Mindmap 与 OnlyOffice 当前边界说明
> 更新时间:2026-04-14
>
> 主仓:`/mnt/Data1T/mnote`
## 1. Mindmap 当前主链
### 1.1 主组件与嵌入链路
- 主组件:`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx`
- BlockNote 注册入口:`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/schema.ts`
- 独立全屏页:`/mnt/Data1T/mnote/wolai-frontend/src/app/mindmap/[docId]/[mindmapId]/page.tsx`
当前事实:
- `MindmapBlockView` 同时服务文档内嵌块与独立全屏页。
- 独立页通过 `editorStub` 复用同一组件,不额外复制第二套思维导图主壳。
- 文档内嵌与独立页共用同一套工具栏、侧栏、导航器、缩略图与上下文菜单子组件。
### 1.2 数据入口与 ops 边界
- 本地读写入口:`/mnt/Data1T/mnote/wolai-frontend/src/lib/mindmap/mindmapLocalStore.ts`
- 节点操作入口:`/mnt/Data1T/mnote/wolai-frontend/src/lib/mindmap/mindmapOps.ts`
- 页面 API
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/mindmap/[docId]/route.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts`
当前拆分:
- 前端交互状态保留在 `MindmapBlock.tsx` 及其子组件内。
- 本体数据读写由 `mindmapLocalStore.ts` 提供本地文件落盘兼容接口。
- 节点增删改、引用、注释、链接等操作由 `mindmapOps.ts` 统一描述为 `MindmapOp`
- 页面 API 当前通过 Convex `api.mindmaps.*` 承接 `get/put/restore/purge`,保留现有前端形态不变。
- 当前 Mindmap API 已补统一元信息口径:
- `pageId = documentId`
- `attachmentId = mindmapId`
- `workspaceId` 统一来自页面归属工作区
- `requestId/traceId` 统一从请求头透传或在路由侧兜底生成
### 1.3 后续 adapter 目标
后续若进入 Rust adapter,只抽象以下边界,不复制历史仓 UI:
- 节点树结构
- 节点引用 `refs`
- 节点操作日志 `MindmapOp`
- mindmap 与 page/block 的绑定关系
## 2. OnlyOffice 当前主链
### 2.1 页面与 API 边界
- 页面入口:
- `/mnt/Data1T/mnote/wolai-frontend/src/app/onlyoffice/page.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/onlyoffice/OnlyOfficeClientPage.tsx`
- 页面侧面板:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/onlyoffice/OnlyOfficeAiAgentPanel.tsx`
- API 边界:
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/onlyoffice/sign/route.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/onlyoffice/proxy/route.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/onlyoffice/callback/route.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/onlyoffice/forcesave/route.ts`
当前拆分:
- `page.tsx` 负责页面级早期 DOM 补丁与容器加载。
- `OnlyOfficeClientPage.tsx` 负责编辑器初始化、内部请求改写、代理地址适配与 callback URL 组装。
- `sign` 负责 JWT 签名。
- `proxy` 负责同源代理与回源 URL 安全限制。
- `callback` 负责保存回写存储。
- `forcesave` 负责触发文档服务器强制保存。
### 2.2 与正文和附件的关系
- Office 文件入口仍来自 `MediaBlock``/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MediaBlock.tsx`
- 正文里只保留附件块与跳转入口,不在 BlockNote 内直接运行 OnlyOffice 编辑器。
- OnlyOffice 编辑器继续在独立 `/onlyoffice` 页面运行。
- 当前 OnlyOffice 已稳定使用:
- `documentId` 作为页面业务标识
- `assetId` 作为附件业务标识
- 页面归属仍由文档/附件在 Convex 中的 `workspace_id``document_id` 决定
## 2.5 与 Mindmap 对齐后的共享标识口径
- 页面标识:
- Mindmap 使用 `pageId/documentId`
- OnlyOffice 使用 `documentId`
- 附件标识:
- Mindmap 使用 `attachmentId/mindmapId`
- OnlyOffice 使用 `assetId`
- 页面归属:
- 两者都以 Convex 中的 `workspace_id + document_id` 作为最终归属真相
- 追踪字段:
- Mindmap API 已返回 `requestId/traceId`
- OnlyOffice 页面当前通过查询参数与调试上下文持有 `assetId/documentId/userId`,后续若接入统一 Rust adapter,继续沿用同一页面/附件口径
### 2.3 静态资源边界
- 根目录静态资源保留在 `/mnt/Data1T/mnote/src/components/onlyoffice/`
- 当前已确认存在:
- `onlyoffice-web-apps/`
- `onlyoffice-plugins/`
- `onlyoffice-data/`
这些目录继续保留为运行所需静态资源、插件和数据目录,不做迁移式替换。
### 2.4 后续 adapter 目标
后续若进入 Rust adapter,只抽象以下边界:
- 对象解析与附件定位
- 签名生成
- callback 写回
- forcesave 触发
- 代理回源请求
## 3. 历史仓禁止误复制范围
以下历史仓路径只作为参考,不进入主仓主线:
- `/mnt/Data1T/mnote-rust/app/documents/[id]/mindmap/page.tsx`
- `/mnt/Data1T/mnote-rust/app/documents/[id]/office/page.tsx`
- `/mnt/Data1T/mnote-rust/components/sidebar/sidebar.tsx`
- `/mnt/Data1T/mnote-rust/app/page.tsx`
- `/mnt/Data1T/mnote-rust/app/onlyoffice/page.tsx`
约束:
- 不复制第二套对象页壳。
- 不复制第二套 sidebar 壳。
- `adapter-onlyoffice``adapter-mindmap` 继续作为后置实现项,而不是本阶段直接迁入主线。
@@ -0,0 +1,48 @@
# [recycle] Phase 8 Cutover Log 2026-04-15
## 概要
- 完成 Phase 8 收口,Rust 已成为 mnote 的唯一业务执行平面。
- 完成 `task-036``task-044`,并清空 harness 中全部历史失败/待办状态。
- ONLYOFFICE 已升级到 `9.3.1`,真实页面打开、插件桥、callback、forcesave 回归通过。
## 本轮关键变更
### 1. 页面与对象主链
- 页面旧面 `documents/create-child``documents/embed``documents/empty-trash``documents/purge``documents/template` 已收口到统一 adapterroute 仅保留 transport。
- Mindmap 剩余旧面已接到 Rust mindmap adapter/toolAI/Mindmap 写链失败态开始统一落账。
- OnlyOffice sign/proxy/callback/forcesave 已固定为 `TS_TRANSPORT_KEEP`,对象规则与 session/sign/proxy/callback 准备由 Rust adapter 持有。
### 2. AI 工具面
- `search_web``image_read``slash_run` 已切到 Rust Tool runtime 主路径。
- `ai-agent/run` 不再保留 `search_web` 的 TS fallback。
- `mediaServerTools``slashServerTools` 已收缩为 transport helper,不再作为私有真入口。
### 3. 统一观测与状态语义
- 已补 workspace 级 bridge 总览查询。
- 已补失败态、冲突态、补偿态第一批统一落账。
- `bridge-log/runtime` 与页面主写链、AI/Mindmap 写链的状态语义开始对齐。
### 4. 文档与清单
- 已更新 `rust-kernel-cutover-v1.md`
- 已更新 `release-readiness-source-audit.md`
- 已更新 `rust-kernel-backport-phase-checklist.md`
- 已更新 `rust-kernel-missing-targets.md`
- 已新增 `rust-kernel-legacy-delete-list.md`
- 已更新 `ai-tool-cutover-matrix.md`
## 当前结论
可以使用下面这句统一口径:
> Rust 已成为 mnote 的唯一业务执行平面;Web route 只保留 transport、auth、session、streaming、proxy 与 callback 壳。
## 后续建议
- 执行第一批 `TS_LEGACY_DELETE` 的物理删除。
- 继续压缩 `builtins/**` 中剩余的 TS 兼容实现。
- 对 OnlyOffice、Mindmap 和 AI 工具链做一轮发布前运行态回归复核。
@@ -0,0 +1,193 @@
# [recycle] 发布前源代码审计
> 更新时间:2026-04-16
>
> 审计范围:`/mnt/Data1T/mnote`
## 1. Bridge 入口与日志回查
### 1.1 最小查询与写入入口
当前已确认的 bridge 主线入口:
- 查询:
- `src/app/api/documents/content/route.ts`
- `src/app/api/documents/meta/route.ts`
- `src/app/api/sidebar/route.ts`
- 写入:
- `src/app/api/documents/title/route.ts`
- `src/app/api/documents/stats/route.ts`
- `src/app/api/documents/options/route.ts`
- `src/app/api/documents/save/route.ts`
- `src/app/api/blocks/patch/route.ts`
- `src/app/api/onlyoffice/callback/route.ts`
### 1.2 日志与事件落账
当前已确认:
- `src/lib/documents/metadata-command-adapter.ts` 成功路径会调用 `recordBridgeCommandArtifacts`
- `src/lib/documents/save-command-adapter.ts` 成功路径会调用 `recordBridgeCommandArtifacts`
- `src/app/api/blocks/patch/route.ts` 成功路径会调用 `recordBridgeCommandArtifacts`
- `src/lib/documents/media-asset-command-adapter.ts` 会在 ONLYOFFICE callback 成功写回后尝试复用同一 bridge log 落账
- `src/lib/documents/bridge-log.ts` 会同时写入:
- `bridgeLogs.recordCommandLog`
- `bridgeLogs.recordDomainEvent`
- 回查入口:
- `src/app/api/bridge/request/route.ts -> bridgeLogs.listByRequest`
- `src/app/api/bridge/trace/route.ts -> bridgeLogs.listByTrace`
结论:
- bridge 的最小查询、最小写入、日志生成、事件生成和错误返回已经形成源码闭环。
## 2. 保存口径审计
当前正文与页面元信息主写链如下:
- `documents.save -> api.documents.updateContent`
- `documents.title.update -> api.documents.updateTitle`
- `documents.stats.update -> api.documents.updateStats`
- `documents.options.update -> api.documents.updateOptions`
- `blocks.patch -> 读取文档内容后回写 api.documents.updateContent`
OnlyOffice 写回:
- `src/app/api/onlyoffice/callback/route.ts -> media.assets.replace_storage -> api.mediaAssets.replaceStorageFromUpload`
结论:
- 页面正文、页面元信息、块补丁与 OnlyOffice 附件写回最终都以 Convex mutation 为主事实写入点。
- 当前未发现把页面/块主真相改写到 `mnote-rust`、本地缓存目录或其他派生目录的主写链。
## 3. `trace_id` / `request_id` / `workspace_id` / `page_id` 一致性
### 3.1 前端与 route
- `buildDocumentBridgeContext()` 统一生成 `requestId``traceId``workspaceId`
- `buildDocumentBridgeContextWithActor()` 可为 ONLYOFFICE callback 这类无用户 cookie 的服务端回调显式注入 `actor/source`
- `buildDocumentCommandEnvelope()` 统一挂载 `commandId``target.pageId`
- `documents.title/stats/options/save` 均将 `normalizedDocumentId` 作为 `target.pageId`
- `onlyoffice/callback` 会把 `asset.document_id` 作为 `target.pageId`,并透传 `workspaceId`
### 3.2 日志落账
- `recordBridgeCommandArtifacts()` 使用同一 `context.requestId``context.traceId`
- `workspaceId` 统一取 `target.workspaceId``context.workspaceId`
- `targetPageId` 统一落到 command log
- domain event payload 同时记录 `request_id``trace_id``command_id``command_name`
### 3.3 当前辅助验证
- `src/lib/documents/bridge.test.ts` 已覆盖:
- `target.pageId = doc_1`
- `executeSaveBridgeCommand` 成功路径会调用 `recordBridgeCommandArtifacts`
- 返回结果保留 `req_1``trace_1`
- `src/app/api/onlyoffice/callback/route.test.ts` 已覆盖:
- callback 成功路径会调用 `executeMediaAssetWritebackBridgeCommand`
- `storageId``assetId``documentId``workspaceId` 会进入 `media.assets.replace_storage` envelope
结论:
- `trace_id``request_id``workspace_id``page_id` 在 route、adapter、bridge log、回查入口之间已有统一口径。
## 4. 禁止事项复查
### 4.1 不双仓并行
- 主仓脚本与文档已统一指向 `/mnt/Data1T/mnote`
- `wolai-frontend/src``wolai-frontend/convex``rust/` 下未发现直接依赖 `/mnt/Data1T/mnote-rust` 的运行时代码路径
### 4.2 不跨仓链接
- 排除 `node_modules``.next``.venv``rust/target` 等构建产物后,主仓源码范围内未发现跨仓符号链接
### 4.3 不复制第二套产品前端壳
- 主仓保留的是既有 `wolai-frontend` 主壳
- 历史仓对象页壳与第二套 sidebar 仅在文档中被标记为禁止误复制范围
### 4.4 不把对象页壳 / diagnostics / smoke 页带入主线
- 当前主仓 `wolai-frontend/src/app` 下未发现新增的 `diagnostic` / `smoke` 页面目录
- 历史仓相关页面已在边界文档中明确列入禁止误复制清单
### 4.5 不散落多个 Rust 源码根
- 主仓顶层 `Cargo.toml` 仅存在 `rust/Cargo.toml`
- 其余 crate `Cargo.toml` 均位于 `rust/crates/*`
## 5. 运行态复核结果与最终结论
本审计只能关闭源码可验证项,但截至 2026-04-16,发布前运行态复核已经补齐五条直接证据:
- 已复跑 `scripts/task019-document-ui-regression.js`,文档页标题、正文、保存、刷新、回查链通过。
- 已复跑 `scripts/task021-mindmap-ui-regression.js`,Mindmap 全屏页的新增子节点、删除子节点、保存链、刷新回查与 `requestId/traceId` 元信息同步通过。
- 已复跑 `scripts/task022-onlyoffice-ui-regression.js`ONLYOFFICE `9.3.1` 基线下的打开、插件桥、callback 写回、forcesave 与 `storage_id` 变化通过。
- 已通过 `pnpm exec vitest run src/lib/ai-agent/runtime/runAgent.test.ts`,补齐 `docs_search -> docs_read` 的 AI runtime 调用证据。
- 已通过 `bash /mnt/Data1T/mnote/rust/scripts/task049-cli-smoke.sh`,补齐 CLI 主链的最小真实执行验收。
当前已不再存在阻塞发布的 CLI / AI 运行态尾项。
补充核验结果:
- `cargo test --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p mnote-cli -p bridge-runtime` 已通过。
- `wolai-frontend` 相关定向 `eslint` 当前无 error,仅剩仓库既有 warnings,不构成本轮发布阻塞。
仍然要求环境满足的前置条件:
- 浏览器回归环境可访问 `http://127.0.0.1:3000`
- `MNOTE_ONLYOFFICE_PROBE_DOCX=/tmp/mnote-onlyoffice-probe/probe.docx` 可用
- 已登录态可通过 `/auth` 或测试账号快速登录建立
## 6. 旧 TS 执行面割接清单
当前源码审计之外,还需要看“哪些 TS route 还能留,哪些只是历史兼容”。
本轮已新增:
- `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md`
该文档已把主仓当前执行面明确分成四类:
- `RUST_OWNER`
- `TS_TRANSPORT_KEEP`
- `TS_COMPAT_PENDING`
- `TS_LEGACY_DELETE`
其中当前仍需要长期观察但不再构成主链阻塞的旧面主要包括:
- `src/app/api/documents/embed/route.ts`
- `src/app/api/mindmap-ai/**`
其中页面旧面在 2026-04-16 的最终口径如下:
- `src/app/api/documents/create-child/route.ts` 已改走 `documents.create`
- `src/app/api/documents/empty-trash/route.ts` 已改走 `documents.emptyTrashByWorkspace`
- `src/app/api/documents/purge/route.ts` 已改走 `documents.purge`
- `src/app/api/documents/template/route.ts` 已改走 `documents.template`
- `src/app/api/documents/embed/route.ts` 已改走 `documents.save`,但目标页面插入位与 `pageReference` block 组装仍在 TS,因此继续按 `TS_COMPAT_PENDING` / 产品编排壳维护
- `src/app/api/mindmap/[docId]/route.ts``src/app/api/mindmap/[docId]/[mindmapId]/route.ts``DELETE/PATCH` 已改走 `mindmaps.delete/restore/purge`
- `src/app/api/mindmap-trash/empty/route.ts` 已改走 `mindmaps.emptyTrashByWorkspace`
- `src/app/api/blocks/*` 现网主链实际统一走 `src/lib/blocks/block-command-adapter.ts``src/lib/documents/block-command-adapter.ts` 应视为遗留兼容 helper,而非现网主执行面
- `src/app/api/ai-agent/run/route.ts` 的核心工具主链现在已经统一走 Rust tool runtime`docs_search``docs_read``search_web``image_read``slash_run` 不再保留 TS 真执行兜底
结论:
- 当前可以说“Rust 已成为 mnote 的主业务执行平面”。
- 仍保留的 TS 路径只剩 `TS_TRANSPORT_KEEP`、少量 `TS_COMPAT_PENDING` 产品编排壳与冻结的 `TS_LEGACY_DELETE` 清单。
- 第一批 `TS_LEGACY_DELETE` 已不再阻塞这轮 release readiness;它们现在的要求是“保持纯壳 / 冻结 helper,不允许反向长出新的业务规则”。
最终 release readiness 口径:
- 若按源码审计口径:可以说明“Rust 已成为 mnote 的唯一业务执行平面”,同时明确仍保留 `TS_TRANSPORT_KEEP` 与少量产品编排壳。
- 若按完整发布口径:文档页、Mindmap、OnlyOffice、AI runtime、CLI smoke 的 Phase 8 scoped gate 已闭合,可以给出最终完成结论。
- 后续维护口径:剩余 `TS_TRANSPORT_KEEP` / `TS_LEGACY_DELETE` 只属于长期清理与边界维护,不再构成第二套业务执行平面。
补记:
- 2026-04-15 已复跑 `scripts/task019-document-ui-regression.js`,确认文档页标题、正文、保存与刷新回查通过。
- 2026-04-15 已复跑 `scripts/task021-mindmap-ui-regression.js`,确认新增/删除节点、保存链、刷新回查与 `requestId` / `traceId` 元信息同步通过。
- 2026-04-15 已复跑 `scripts/task022-onlyoffice-ui-regression.js`,在升级后的 ONLYOFFICE `9.3.1` 基线上确认 `oo_insert_text` 插件桥、`同步保存`、callback 写回与 `storage_id` 变化仍然通过;本次 callback 最终写回已不再由 route 直接调用 `api.mediaAssets.replaceStorageFromUpload`,而是先经 `media.assets.replace_storage` bridge 命令再落到 Convex。
- 2026-04-16 已通过 `src/lib/ai-agent/runtime/runAgent.test.ts`,确认 `docs_search -> docs_read` 的运行态工具链顺序、事件流与结果归一化闭合。
- 2026-04-16 已通过 `rust/scripts/task049-cli-smoke.sh`,确认 `sidebar/page/block/search/tool/mindmap` 主链 CLI 真实执行闭合。
@@ -0,0 +1,85 @@
# [recycle] Rust 回迁资产审计清单
> 更新时间:2026-04-14
>
> 主仓:`/mnt/Data1T/mnote`
## 1. 本轮已确认的主仓 Rust 资产
### 1.1 Workspace 根
- `rust/Cargo.toml`
- `rust/Cargo.lock`
### 1.2 已并入主线的 crate
- `rust/crates/core-domain/`
- `rust/crates/core-protocol/`
- `rust/crates/event-log/`
- `rust/crates/storage-convex-bridge/`
- `rust/crates/index-fts/`
### 1.3 已并入主线的 Rust 设计文档
- `rust/design/INDEX.md`
- `rust/design/core/01-domain-model-v0.md`
- `rust/design/core/02-command-query-tool-protocol-v0.md`
- `rust/design/core/03-storage-event-indexing-v0.md`
- `rust/design/core/04-onlyoffice-integration-boundary-v0.md`
### 1.4 本轮新增的主仓设计文档
- `design/rust-kernel-backport-plan.md`
- `design/rust-kernel-backport-phase-checklist.md`
- `design/rust-single-repo-maintenance-boundary.md`
- `design/mindmap-onlyoffice-boundary.md`
- `design/sidebar-rust-query-target.md`
## 2. 当前仍未并入主线的目录
以下目录当前未在主仓 `rust/` 内出现,仍保持后置状态:
- `rust/crates/adapter-onlyoffice/`
- `rust/crates/adapter-mindmap/`
- `rust/crates/adapter-legacy-mnote/`
- `rust/crates/mnote-cli/`
- `rust/bridge/`
- `rust/scripts/`
- `rust/fixtures/`
- `rust/tests/`
## 3. 已确认没有误带入的历史噪音
当前主仓 `rust/` 与根 `design/` 下未发现以下类型内容被直接带入主线:
- 第二套 Next 页面壳
- 历史 `diagnostics` 页面
- `smoke` 页面
- 历史仓 `.next/``.tmp/` 产物目录
- `adapter-*``mnote-cli` 实现目录
备注:
- `rust/target/` 为本地编译产物,不属于回迁设计资产。
- 发布前若需进一步清理编译产物,应单独征得用户同意,不在本轮处理。
## 4. 主仓启动路径核对
当前可验证的主仓入口如下:
- 根目录热启动:`npm run desktop:hot`
- 前端开发:`cd /mnt/Data1T/mnote/wolai-frontend && pnpm dev`
- 后端开发:`cd /mnt/Data1T/mnote/wolai-backend && uvicorn app.main:app --reload --port 8000`
- Rust 校验:`cargo --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml ...`
核对结果:
- 根 `package.json` 的仓库级脚本只指向 `scripts/*.js`
- `scripts/desktop-hot.js` 只从主仓根目录解析 `.env.all`
- `scripts/desktop-hot.js` 只调起 `wolai-frontend/``wolai-backend/`
- `AGENTS.md` 的常用开发命令也全部指向 `/mnt/Data1T/mnote`
## 5. 当前结论
- 主仓已具备“只进入 `/mnt/Data1T/mnote` 就能找到前端、后端、Rust、OnlyOffice 相关入口”的文档与脚本口径。
- 本轮新增目录与文档均位于主仓 `design/` 或既有 `rust/` 主线下,未把 `mnote-rust` 的历史页面壳或构建噪音带入主线。
@@ -0,0 +1,399 @@
# [recycle] Rust 内核总割接方案 v1
> 更新时间:2026-04-16
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-replacement-roadmap-v1.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-missing-targets.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-phase-checklist.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/release-readiness-source-audit.md`
## 1. 目的
本文只回答一个问题:
> **什么时候可以宣布“Rust 已成为 mnote 唯一业务执行平面”,以及宣布前必须退役哪些旧 TS 执行面。**
这里的“退役”不等于“把所有 Next route 全删掉”。
真正要退役的是:
- TS route 内部承载的业务规则
- TS 私有工具注册表里的第二套执行语义
- 绕过 Rust command/query/tool 的直连 Convex 主链
允许长期保留的是:
- transport
- auth
- session
- streaming
- SSR / BFF
- 第三方对象服务回调壳
---
## 2. 状态定义
为了避免后续再出现“看起来迁了,实际上没迁”的模糊表述,本文统一使用下面四类状态。
### 2.1 `RUST_OWNER`
定义:
- 核心业务规则已经进入 Rust command/query/tool/runtime
- TS route 只做参数校验、鉴权、HTTP 包装、Convex transport 或第三方请求转发
处理策略:
- 允许继续保留 route 壳
- 不允许再往 route 内加业务规则
### 2.2 `TS_TRANSPORT_KEEP`
定义:
- 这条 route 本身不是业务真规则入口
- 但因为浏览器会话、回调、上传下载、SSE、客户端桥等原因,需要长期保留在 Web 层
处理策略:
- 永久保留或长期保留
- 只能承载 transport / session / proxy / callback
### 2.3 `TS_COMPAT_PENDING`
定义:
- 当前已经有部分 Rust 接缝
- 但仍存在 TS 业务拼接、对象规则或直连 Convex 的旧执行逻辑
处理策略:
- 不能宣布总割接完成
- 必须继续迁到 Rust 后,才能降级为 `RUST_OWNER``TS_TRANSPORT_KEEP`
### 2.4 `TS_LEGACY_DELETE`
定义:
- 该 route 或旧逻辑只服务于历史兼容、诊断、临时过渡或旧 UI
- 一旦对应能力完成 Rust cutover 并切完调用方,就应删除
处理策略:
- 先停写
- 再切调用
- 最后物理删除
---
## 3. 当前总盘点
## 3.1 页面系统
### 已进入 `RUST_OWNER`
- `wolai-frontend/src/app/api/documents/meta/route.ts`
- `wolai-frontend/src/app/api/documents/content/route.ts`
- `wolai-frontend/src/app/api/documents/title/route.ts`
- `wolai-frontend/src/app/api/documents/stats/route.ts`
- `wolai-frontend/src/app/api/documents/options/route.ts`
- `wolai-frontend/src/app/api/documents/save/route.ts`
- `wolai-frontend/src/app/api/documents/create/route.ts`
- `wolai-frontend/src/app/api/documents/move/route.ts`
- `wolai-frontend/src/app/api/documents/delete/route.ts`
- `wolai-frontend/src/app/api/documents/restore/route.ts`
- `wolai-frontend/src/app/api/documents/duplicate/route.ts`
- `wolai-frontend/src/app/api/documents/copy-tree/route.ts`
- `wolai-frontend/src/app/api/documents/create-child/route.ts`
- `wolai-frontend/src/app/api/documents/empty-trash/route.ts`
- `wolai-frontend/src/app/api/documents/purge/route.ts`
- `wolai-frontend/src/app/api/documents/template/route.ts`
判定依据:
- 已统一走 `buildDocument(Command|Query)Envelope`
- 已切到共享 page/metadata/save adapter 或 `resolveRustBridge*`
- route 只剩 transport、鉴权、参数整理与 HTTP 返回
- `create-child` 已改为复用 `documents.create`
- `template``empty-trash``purge` 已补齐 Rust runtime/transport 映射,不再由 `page-command-adapter.ts` 直接调用 Convex mutation
### 仍是 `TS_COMPAT_PENDING`
- `wolai-frontend/src/app/api/documents/embed/route.ts`
原因:
- `embed` 已不再直接调用 `api.documents.updateContent`,最终写入已改走 `documents.save`
- 但目标页面插入点计算、`pageReference` block 组装仍在 TS 侧完成,尚未进入独立 Rust command 面。
- 因此它已经摆脱“直连 Convex mutation”的假完成状态,但还不能宣布为完全 `RUST_OWNER`
处理结论:
- 页面旧面中,`create-child``template``empty-trash``purge` 已转为统一 Rust transport。
- `embed` 仍是 Phase 8 前需要继续清理的剩余页面旧面。
- 当前仍不可直接物理删除这些 route 文件,必须先切完调用方并确认不再承载 TS 内容编排。
---
## 3.2 块系统
### 已进入 `RUST_OWNER`
- `wolai-frontend/src/app/api/blocks/get/route.ts`
- `wolai-frontend/src/app/api/blocks/patch/route.ts`
- `wolai-frontend/src/app/api/blocks/move/route.ts`
- `wolai-frontend/src/app/api/blocks/embed/route.ts`
判定依据:
- 现网 route 实际统一走 `wolai-frontend/src/lib/blocks/block-command-adapter.ts`
- `patch/move/embed` 已通过 `documents.save` 主链落盘,不再依赖 route 内直连 Convex mutation
- route 仅保留 transport、参数校验与 HTTP 返回
### 仍需冻结的兼容 helper
- `wolai-frontend/src/lib/documents/block-command-adapter.ts`
说明:
- 该文件当前不再是 `/api/blocks/*` 的主调用链。
- 它仍保留旧桥接 helper 语义,因此需要明确按兼容层冻结,不能再被当成“现网主执行面”继续扩展。
### 可删的旧逻辑
- 旧 `loadBlocks/saveBlocks` 私有执行路径
- 任何绕过共享 block adapter 的 AI 文档写入旧实现
处理结论:
- 块系统主链已经满足“route 保留、旧业务逻辑退役”的前提
- 后续只允许在 Rust block ops 上继续扩展
---
## 3.3 查询聚合与观测
### 已进入 `RUST_OWNER`
- `wolai-frontend/src/app/api/sidebar/route.ts`
- `wolai-frontend/src/app/api/search/documents/route.ts`
- `wolai-frontend/src/app/api/bridge/request/route.ts`
- `wolai-frontend/src/app/api/bridge/trace/route.ts`
### 仍是 `TS_TRANSPORT_KEEP`
- `wolai-frontend/src/app/api/search/recent/route.ts`
说明:
- `search/recent` 当前只承担“打开页面后写最近访问记录”的 side-effect,不再承担搜索召回
- 它可以长期保留在 Web 层,但必须明确不是搜索业务真入口
### 当前仍缺
- workspace 级统一观测总览
- 分页、按对象过滤、按状态过滤
- 完整失败态和冲突态的全链路落账
---
## 3.4 AI 与工具面
### 已进入 `RUST_OWNER`
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `doc_get`
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `doc_find`
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `doc_insert_blocks`
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `doc_replace_range`
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `docs_search`
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `docs_read`
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `search_web`
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `image_read`
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `slash_run`
说明:
- Convex 主链下,`ai-agent/run` 已直接通过 `executeRustBridgeTool(...)` 执行 `docs_search``docs_read``search_web``image_read``slash_run`
- `docsServerTools` 当前只保留非 Convex / fallback 模式,不再承担 Convex 主链真执行。
- `search_web``image_read``slash_run` 已切到 Rust Tool runtime`ai-agent/run` 不再保留 TS 真执行兜底。
### 仍是 `TS_TRANSPORT_KEEP`
- `wolai-frontend/src/app/api/ai-agent/client-tool-result/route.ts`
- `wolai-frontend/src/lib/ai-agent/tools/builtins/onlyoffice/onlyofficeServerTools.ts` 中的 `asset_extract_outline`
- `wolai-frontend/src/lib/ai-agent/tools/builtins/onlyoffice/onlyofficeServerTools.ts` 中的 `asset_to_mindmap`
- `wolai-frontend/src/lib/ai-agent/tools/builtins/onlyoffice/onlyofficeServerTools.ts` 中的 `oo_*`
- `wolai-frontend/src/lib/ai-agent/tools/builtins/rag/**`
说明:
- `client-tool-result` 只负责浏览器内客户端工具回传,不承担业务决策。
- `asset_extract_outline` 仍依赖附件下载与 MinerU 解析,因此长期保留 TS transport / 外部服务编排。
- `asset_to_mindmap` 当前属于“TS 提纲提取 + Rust 导图写入”的明确编排边界:附件解析仍在 TS/MinerU,但导图应用已改走 Rust `mindmap_apply_ops`
- `oo_*` 继续保持客户端插件边界,不再尝试下沉为第二套服务端真执行入口。
### 仍是 `TS_COMPAT_PENDING`
- `wolai-frontend/src/app/api/mindmap-ai/agent/route.ts`
- `wolai-frontend/src/app/api/mindmap-ai/assets/route.ts`
- `wolai-frontend/src/app/api/mindmap-ai/expand-node/route.ts`
- `wolai-frontend/src/app/api/mindmap-ai/outline-to-mindmap/route.ts`
处理结论:
- AI builtins 主链已经完成从 `TS_COMPAT_PENDING``RUST_OWNER` / `TS_TRANSPORT_KEEP` 的最终归类。
- `mindmap-ai/**` 若继续保留,必须明确只是上层产品编排,不能成为对象规则真入口。
- AI 工具矩阵与第一批旧面删除清单分别见:
- `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/ai-tool-cutover-matrix.md`
- `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-legacy-delete-list.md`
---
## 3.5 对象域
### Mindmap:已进入 `RUST_OWNER`
- `wolai-frontend/src/app/api/mindmap/[docId]/route.ts`
- `wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts`
说明:
- `GET/POST` 之前已在 Rust query/command 主链上。
- 2026-04-15 起,`DELETE``PATCH(action=restore|purge)` 也已补齐 Rust runtime/transport 映射,不再由 route 直接调用 Convex mutation。
### Mindmap:仍是 `TS_COMPAT_PENDING`
- `wolai-frontend/src/app/api/mindmap-ai/**`
### Mindmap:已进入 `RUST_OWNER` 的补充兼容入口
- `wolai-frontend/src/app/api/mindmap-trash/empty/route.ts`
说明:
- `mindmap-trash/empty` 现已切到 `mindmaps.emptyTrashByWorkspace` Rust runtime/transport。
- 该 route 仍保留为 Web transport 壳,但不再直接持有 TS 真执行。
### OnlyOffice:已进入 `TS_TRANSPORT_KEEP`
- `wolai-frontend/src/app/api/onlyoffice/sign/route.ts`
- `wolai-frontend/src/app/api/onlyoffice/proxy/route.ts`
- `wolai-frontend/src/app/api/onlyoffice/callback/route.ts`
- `wolai-frontend/src/app/api/onlyoffice/forcesave/route.ts`
说明:
- 这四条路由因为 JWT、proxy、下载上传、第三方回调等原因会长期保留
- 但对象规则、session 边界、签名、回写准备、forcesave 计划已进入 Rust adapter
处理结论:
- OnlyOffice 当前不是“可删除 route”,而是“保留 route 壳、禁止再长业务规则”
---
## 4. Phase 8 Cutover Gate
只有下面四组 gate 同时满足,才能宣布“Rust 成为唯一业务执行平面”。
### Gate A:页面、块、查询主链全部 Rust 持有
要求:
- `documents.*` 主链不再存在 TS 业务路由
- `blocks.*` 主链不再存在第二套执行逻辑
- `search.documents``sidebar.dataset.list``bridge request/trace/command` 全部走 Rust query/runtime
### Gate B:对象域只保留 transport 壳
要求:
- Mindmap 主读写统一进入 Rust adapter
- OnlyOffice 只保留签名、代理、回调、forcesave 等 transport 壳
- 任何对象域规则都不再从 route 直连 Convex mutation 发展
### Gate CAI 与 CLI 共用同一工具面
要求:
- `ai-agent/run` 不再为核心工具保留 `builtins/**` 私有真入口
- CLI 与 AI 调用同一组 Rust `Tool`
- `event_replay``index_rebuild``bridge_*_get` 这组观测/恢复命令对 CLI、AI、Web 等价可用
- `docs_search``docs_read``search_web``image_read``slash_run` 必须保持 `RUST_OWNER`
### Gate D:旧 TS 兼容面完成物理退役或明确降级
要求:
- `create-child``embed``empty-trash``purge``template``mindmap-trash/empty` 等旧接口要么迁入 Rust,要么标记为废弃并从 UI 脱钩
- `mindmap-ai/**` 若继续保留,必须明确只是上层产品编排,不能成为对象规则真入口
- 旧 `docTools` / `mindmapTools` / 页面私有写入胶水不再允许继续扩展
---
## 5. 允许删除与禁止删除
## 5.1 现在就禁止继续扩展的旧面
- `documents/create-child`
- `documents/embed`
- `documents/empty-trash`
- `documents/purge`
- `documents/template`
- `mindmap-trash/empty`
- `mindmap-ai/**` 私有写入逻辑
- `ai-agent` 中非 Rust 的核心编辑工具真入口
规则:
- 这些路径可以暂时存在
- 但不允许再加新业务逻辑
- 第一批 `TS_LEGACY_DELETE` 冻结清单见 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-legacy-delete-list.md`
## 5.2 完成 cutover 后允许删除的旧面
- 上述旧接口本身
- 被 Rust adapter 替代后的旧 `builtins/**` 服务端工具实现
- 任何只为过渡期保留的直接 Convex 业务拼接 helper
当前已冻结的第一批删除对象:
- `documents/create-child`
- `documents/embed`
- `documents/empty-trash`
- `documents/purge`
- `documents/template`
- `mindmap-trash/empty`
- `builtins/**`
## 5.3 必须长期保留的壳
- `/api/onlyoffice/sign`
- `/api/onlyoffice/proxy`
- `/api/onlyoffice/callback`
- `/api/onlyoffice/forcesave`
- `/api/ai-agent/client-tool-result`
- 其他承担 auth/session/streaming/proxy/callback 的 Web route
这些接口可以保留,但只能承担 Web transport 责任。
---
## 6. 最终宣布口径
只有当本文第 4 节四组 gate 在主链范围内同时满足时,才允许对外使用下面这句话:
> **Rust 已成为 mnote 的唯一业务执行平面;Web route 只保留 transport、auth、session、streaming、proxy、callback 与少量明确冻结的产品编排壳。**
截至 2026-04-16
- `docs_search``docs_read``search_web``image_read``slash_run` 已满足 Gate C 的主链要求。
- `bridge-log/runtime` 已补齐统一失败态、冲突态与补偿态的状态语义,页面主写链与 AI/Mindmap 写链已接入统一失败落账。
- `mnote-cli` 已通过仓库内真实执行 smoke,CLI 与 AI 现在可以共用同一组 Rust Tool / runtime。
- `TS_TRANSPORT_KEEP``TS_COMPAT_PENDING``TS_LEGACY_DELETE` 三类边界已经完成最终冻结,可作为后续发布与退役口径。
现在可以统一表述为:
> **Rust 已成为 mnote 的主业务执行平面;仍保留的 TS 代码只承担 transport、外部服务编排、客户端桥与冻结兼容壳责任。**
@@ -0,0 +1,113 @@
# [recycle] Rust 内核最终收尾详细清单
> 更新时间:2026-04-16
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/release-readiness-source-audit.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-legacy-delete-list.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/ai-tool-cutover-matrix.md`
## 1. 当前结论
截至 2026-04-16,上一轮最终收尾里剩下的 CLI、AI runtime 与文档口径尾项已经闭合。
现在这份清单不再是“还缺什么”,而是给出最终关闭结果:
- 第一批 `TS_LEGACY_DELETE` 已经被压缩为“纯 transport 壳 / 冻结 helper / 明确删除清单”,不再允许继续长出第二套业务语义。
- 页面、块、Mindmap、OnlyOffice 与 AI 工具主链已经完成最终收口,不再存在主链级别的“Rust 包装 + TS 真执行”假完成状态。
- `mnote-cli` 已进入最小真实执行态,并且有可复跑的仓库内 smoke。
- AI 工具矩阵里的 builtins 主链已经完成最终归类,不再保留模糊的 builtins 级 `TS_COMPAT_PENDING`
- 发布前运行态复核证据已经补齐,可以给出统一最终口径。
---
## 2. 尾项关闭结果
### 2.1 第一批 `TS_LEGACY_DELETE`
当前状态:已关闭为“冻结清单 + 纯壳边界”。
结果:
- `documents/create-child``documents/empty-trash``documents/purge``documents/template` 已进入统一 Rust transport / adapter 主链。
- `mindmap-trash/empty` 已进入 Rust runtime / transport 主链。
- `builtins/**` 中已被 Rust runtime 接管的服务端真入口,不再允许继续作为主执行面扩展。
- `documents/embed``mindmap-ai/**` 这类仍保留的上层编排路径,必须按冻结边界维护,不能反向长出新的核心业务规则。
验收结论:第一批旧面已经不再阻塞“Rust 成为主业务执行平面”的宣布口径。
### 2.2 双路径执行清理
当前状态:已关闭。
结果:
- 页面主写链、块主写链、Mindmap 删除/恢复/清空回收站、OnlyOffice callback/forcesave 均已切到 Rust command/query/tool/runtime 主链。
- 旧 TS 侧保留的仅是 transport、外部服务编排或历史兼容 helper,不再是现网主真执行入口。
- `src/lib/documents/block-command-adapter.ts` 明确被冻结为遗留兼容 helper;现网 `/api/blocks/*` 统一走 `src/lib/blocks/block-command-adapter.ts`
### 2.3 CLI 最小真实执行态
当前状态:已关闭。
结果:
- `mnote-cli` 新增并稳定支持 `--execute` 真实执行模式。
- 已修复 `block patch --execute` 的块树回写形状问题,`block move --execute` 不再因为内容快照损坏而失败。
- 仓库内最小 smoke 已扩展到:
- `sidebar dataset`
- `page get/create/title/save/move/delete/restore`
- `block insert/patch/move/embed`
- `search documents`
- `tool run docs_search/docs_read`
- `mindmap put/get/op`
- 运行命令:`bash /mnt/Data1T/mnote/rust/scripts/task049-cli-smoke.sh`
- 验收产物会落盘到 `/tmp/task049-cli-smoke-*`,便于失败后回查逐步 JSON 输出。
### 2.4 AI 工具矩阵收口
当前状态:已关闭。
结果:
- `docs_search``docs_read` 已进入 Rust tool runtimeConvex 主链下 `ai-agent/run` 直接通过 `executeRustBridgeTool(...)` 执行,`docsServerTools` 仅保留 fallback。
- `search_web``image_read``slash_run` 持续保持 `RUST_OWNER`
- `asset_extract_outline``asset_to_mindmap``oo_*` 已明确归类为 `TS_TRANSPORT_KEEP`,不再保留 builtins 级 `TS_COMPAT_PENDING`
- `asset_to_mindmap` 的真正导图写入已收口到 Rust `mindmap_apply_ops`TS 侧仅保留外部解析与编排壳。
### 2.5 发布前运行态复核
当前状态:已关闭。
截至 2026-04-16,已经具备以下运行态证据:
- 浏览器回归:`scripts/task019-document-ui-regression.js`
- 浏览器回归:`scripts/task021-mindmap-ui-regression.js`
- 浏览器回归:`scripts/task022-onlyoffice-ui-regression.js`
- AI runtime 证据:`pnpm exec vitest run src/lib/ai-agent/runtime/runAgent.test.ts`
- CLI 主链证据:`bash /mnt/Data1T/mnote/rust/scripts/task049-cli-smoke.sh`
- Rust 执行层证据:`cargo test --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p mnote-cli -p bridge-runtime`
补充说明:定向 `eslint` 当前无 error,仅剩仓库既有 warnings,不构成这轮收尾阻塞项。
---
## 3. 最终完成标准核对
现在已经同时满足下面几条:
- 第一批 `TS_LEGACY_DELETE` 已物理删除或明确冻结为不可扩展壳。
- 仓库里不再存在主链级别的“Rust 包装 + TS 真执行”双路径假完成状态。
- `mnote-cli` 已覆盖一批关键主链的真实执行,并具备仓库内可复跑 smoke。
- AI 工具矩阵中的 builtins 主链已不再保留模糊的 `TS_COMPAT_PENDING`
- 发布前运行态复核已经重新通过。
- 文档口径与代码现状可以统一到同一个最终结论。
---
## 4. 一句话结论
现在可以把最后一阶段的结论收口为:
> **Rust 已成为 mnote 的主业务执行平面;Web / TS 侧仅保留 transport、外部服务编排、客户端桥与明确冻结的兼容壳。**
@@ -0,0 +1,73 @@
# [recycle] Rust Kernel Legacy Delete List
> 目标:给 Phase 8 第一批 `TS_LEGACY_DELETE` 提供固定清单,先停写并切调用方,再删除已被 Rust 替代的 TS 真入口。
## 1. 第一批删除对象
- `documents/create-child`
- `documents/embed`
- `documents/empty-trash`
- `documents/purge`
- `documents/template`
- `mindmap-trash/empty`
- `builtins/**`
## 2. 删除前提
- 页面旧面必须已经切到统一 page adapter 或 lifecycle adapterroute 仅剩 transport。
- AI 核心工具必须已经切到 Rust Tool runtime 或 `ai-agent/run` 统一入口,不能再由 `builtins/**` 私下持有真执行面。
- 统一 bridge-log 必须能记录失败态、冲突态与补偿态,避免删除旧面后失去回查能力。
## 3. 删除策略
### 3.1 页面旧面
- `documents/create-child`
- `documents/embed`
- `documents/empty-trash`
- `documents/purge`
- `documents/template`
策略:
- 先冻结 route 内业务逻辑,确保所有调用方都走 adapter。
- 保留 Web route 的 auth、参数校验和 HTTP transport。
- 先继续清理 `page-command-adapter.ts` / `page-lifecycle-command-adapter.ts` 中残留的 TS 真执行。
- 在前端调用方仍直接依赖这些 route,且 adapter 仍承载真实写入前,不得直接物理删除 route 文件。
- 当 release audit 与 cutover gate 均确认无回退需求后,再物理删除历史 helper/分支。
### 3.2 Mindmap 旧面
- `mindmap-trash/empty`
策略:
- `mindmap-trash/empty` 现已切到 `mindmaps.emptyTrashByWorkspace` Rust runtime/transport。
- 该 route 仍是兼容入口,但只保留 Web transport 壳,不再直接持有 TS 真执行。
- 仍需确认 `task-042` 的失败/冲突/补偿落账和最终运行态回归稳定,再删除纯兼容分支。
### 3.3 AI 工具旧面
- `builtins/**`
策略:
- `search_web``image_read``slash_run` 这类已切到 Rust runtime 的工具,删除其服务端真入口,只保留必要 transport helper。
- `client-tool-result``oo_*` 属于 `TS_TRANSPORT_KEEP`,不在本批物理删除范围。
- `asset_extract_outline``asset_to_mindmap` 尚未完全 Rust 化,暂不从 `builtins/**` 整体删除,但必须禁止继续扩展新的私有真入口。
## 4. 当前结论
- 第一批 `TS_LEGACY_DELETE` 已完成清单冻结。
- `documents/create-child``documents/embed``documents/empty-trash``documents/purge``documents/template``mindmap-trash/empty``builtins/**` 已被明确纳入 Phase 8 的旧面退役范围。
- 截至 2026-04-15,这批对象中还没有可直接物理删除的 route 文件;当前可安全推进的是:
- 把 route 固定为纯 transport 壳。
- 把 adapter 内残留的 TS 真执行继续收口。
- 为每个旧面补齐调用方清单与“可删前置条件”。
- 其中页面旧面已完成第一轮收口:
- `create-child` 已复用 `documents.create`
- `template``empty-trash``purge` 已补齐 Rust runtime/transport 映射
- `embed` 已切到 `documents.save`,但插入位计算与 `pageReference` block 组装仍在 TS
- 因此页面旧面的主要残留已缩到 `wolai-frontend/src/lib/documents/page-command-adapter.ts` 中的 `embed` 内容编排,以及 `wolai-frontend/src/lib/documents/page-lifecycle-command-adapter.ts` 的历史兼容面。
- `mindmap-trash/empty` 已不再由 route 直接调用 `api.mindmaps.emptyTrashByWorkspace`,但在调用方与最终运行态回归全部通过前,仍不满足物理删除条件。
- 后续删除动作必须以 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md``/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/release-readiness-source-audit.md` 的最终 gate 为准。
@@ -0,0 +1,110 @@
# [recycle] mnote 单仓维护边界约定
> 更新时间:2026-04-14
>
> 主仓:`/mnt/Data1T/mnote`
>
> 历史仓:`/mnt/Data1T/mnote-rust`
## 1. 主结论
- 只保留 `/mnt/Data1T/mnote` 作为启动、规划、保存、验证的唯一主仓。
- `/mnt/Data1T/mnote-rust` 只作为历史资产来源,不再承担主执行入口。
- 任何新设计、执行清单、架构说明都优先写入 `/mnt/Data1T/mnote/design/``/mnt/Data1T/mnote/rust/design/`
## 2. 主仓执行入口
- 根目录脚本入口只认 `/mnt/Data1T/mnote/package.json``/mnt/Data1T/mnote/scripts/`
- 当前 `package.json` 仅通过 `node scripts/*.js` 暴露仓库级入口,没有任何脚本要求先进入 `mnote-rust`
- 如需调用 Rust,统一使用 `cargo --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml ...`,不从历史仓发起 Cargo 命令。
- 当前 `scripts/desktop-hot.js` 已固定从主仓根目录解析 `.env.all`,并以 `wolai-frontend/``wolai-backend/` 作为唯一联调入口。
## 3. `mnote-rust` 历史资料分类
### 3.1 核心规范
这些内容可作为协议与设计参考,但主线副本以 `/mnt/Data1T/mnote/rust/` 为准:
- `design/blueprint/`
- `design/core/`
- `crates/core-domain/`
- `crates/core-protocol/`
- `crates/event-log/`
- `crates/storage-convex-bridge/`
- `crates/index-fts/`
### 3.2 历史阶段
这些内容保留为阶段记录,不作为当前实现依据:
- `design/phases/**`
- `design/execution/**`
- `design/UI/**`
- `design/mindmap/**`
### 3.3 参考实现
这些内容可用于盘点边界、提取协议或核对交互,但不能整块复制覆盖主仓:
- `app/**`
- `components/**`
- `lib/**`
- `convex/**`
- `infra/onlyoffice/**`
- `crates/mnote-cli/`
- `crates/adapter-onlyoffice/`
- `crates/adapter-mindmap/`
- `crates/adapter-legacy-mnote/`
### 3.4 错误方向
以下内容明确不进入主仓主线:
- 对象页壳与试验页:`app/page.tsx``app/onlyoffice/page.tsx``app/documents/**`
- 第二套导航壳:`components/sidebar/sidebar.tsx`
- 诊断与构建产物:`.next/diagnostics/**``.next/**``.tmp/**`
- 仅用于历史 smoke/夹具的目录:`.tmp/phase7-test-fixtures/**`
## 4. 主仓默认不动目录
除非当前任务明确要求接线或修复,否则默认不扩写以下目录:
- `/mnt/Data1T/mnote/wolai-frontend/`
- `/mnt/Data1T/mnote/wolai-backend/`
- `/mnt/Data1T/mnote/infra/convex/`
- `/mnt/Data1T/mnote/src/components/onlyoffice/`
- `/mnt/Data1T/mnote/recycle/`
## 5. 最小维护约定
### 5.1 Workspace 责任面
- 入口文件:`/mnt/Data1T/mnote/rust/Cargo.toml``/mnt/Data1T/mnote/rust/Cargo.lock`
- 维护范围:`/mnt/Data1T/mnote/rust/crates/*`
- 要求:新增 crate、依赖调整、共享协议变更,必须先在主仓 workspace 内完成,不得回写到历史仓再反向复制。
### 5.2 Bridge 责任面
- 入口文件:`/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/bridge.ts`
- 相关边界:
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/metadata-command-adapter.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/save-command-adapter.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/**`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/sidebar/route.ts`
- `/mnt/Data1T/mnote/wolai-frontend/convex/bridgeLogs.ts`
- 要求:新 command/query 先收口协议边界,再决定是否继续下沉到 Rust crate。
### 5.3 接入链责任面
- C1 页面元信息:`src/app/(app)/documents/[id]/page.tsx``src/components/editor/document-content.tsx`
- C2 Sidebar 聚合:`src/components/sidebar/**``src/hooks/use-convex-sidebar-data.ts``src/lib/sidebar-data.ts`
- C3 正文保存:`src/components/editor/blocknote-editor.tsx``src/app/api/documents/save/route.ts`
- Phase D 边界:`src/components/editor/blocks/MindmapBlock.tsx``src/app/onlyoffice/**``src/app/api/onlyoffice/**`
- 要求:优先改主仓现有链路,不复制历史仓页面壳替换现实现。
## 6. 禁止误复制清单
- 不把 `mnote-rust` 当成新的主执行仓。
- 不复制第二套 Next 前端壳、对象页壳、diagnostics 页、smoke 页进入主线。
- 不通过软链接、硬链接、跨仓路径偷接来伪装“已迁移”。
- 不在主仓根目录散落多个 Rust 源码根。
@@ -0,0 +1,629 @@
# [recycle] AI 前端精简方案 v1
> 更新时间:2026-04-15
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-final-closure-checklist.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/ai-tool-cutover-matrix.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md`
## 1. 目标
这份方案只回答一个问题:
> **当前网页前端中,AI 相关部分应该如何精简,才能真正降低加载负担,并把前端 AI 收口为“轻桥接层”。**
目标方向已经明确:
> **前端 AI 面板不再承担工具注册、工具编排、能力路由和执行框架,只保留为 Hermes API Server + mnote Rust 业务能力的轻桥接 UI。**
这意味着后续 AI 前端不再是一个“小型平台”,而只是:
- 收集上下文
- 发送用户输入
- 展示流式结果
- 承接极少数浏览器专属 client tool
## 1.1 现状校准:当前真正已经落地的后端 AI 边界
在继续谈“前端该怎么精简”之前,必须先对齐一个事实:
> **当前本机已经有可核验的 Hermes Agent 与其官方 API Server 方案;同时 mnote 自己也已经有 Rust protocol/runtime 边界。后续前端 AI 应该桥接这两层,而不是自己继续承担平台层。**
当前已确认的 Hermes 事实:
- 本机 Hermes 安装目录:`/home/lix/.hermes`
- Hermes 本体仓:`/home/lix/.hermes/hermes-agent`
- 当前网关进程已在运行:`hermes gateway run --replace`
- 官方文档已提供 OpenAI 兼容 API Server
- 启用方式:`API_SERVER_ENABLED=true`
- 默认监听:`http://127.0.0.1:8642`
- 入口:`/v1/chat/completions``/v1/responses``/health`
但当前本机状态也要说明白:
- Hermes gateway 在跑
- Hermes API Server 当前还没有启用
- mnote 前端当前也还没有接通 Hermes API Server
因此,现阶段不是“是否存在 Hermes”的问题,而是:
- **Hermes 已存在,但还没有接入 mnote Web AI 主链**
- **mnote Rust 业务能力已存在,但还没有作为 Hermes 的统一业务工具面完全暴露**
当前 mnote 已能明确核验到的 Rust 业务边界是:
- `rust/crates/core-protocol/src/tool.rs`
已定义 `ToolSpec``ToolSetSpec``ToolRegistry`,并冻结了 `toolset.readonly``toolset.media_read``toolset.doc_read``toolset.doc_write``toolset.mindmap_read``toolset.mindmap_write``toolset.onlyoffice_service``toolset.slash_write` 等基础集合。
- `rust/crates/bridge-runtime/src/lib.rs`
已提供统一 `RuntimeInput::{Tool, Query, Command}` 入口,支持 `plan``result``explain-plan``validateOnly``dryRun` 等运行模式,并输出统一计划结构。
- `rust/crates/mnote-cli/README.md`
已冻结 `tool run` 的 JSON 契约,说明 CLI 化目标已经开始按稳定协议推进。
- `wolai-frontend/src/lib/documents/rust-runtime.ts`
当前前端已经可以通过 `executeRustBridgeTool()` 直接调用 Rust `bridge-runtime`,说明 Web 并不是从零开始接 Rust。
结合 `ai-tool-cutover-matrix.md``run/route.ts`,当前可以按下面口径理解能力归属:
- 已有明确 Rust owner 或 Rust 主入口的能力:
`search_web``image_read``slash_run``doc_*``mindmap_*``onlyoffice_* service`
- 仍暂时保留在 TS transport 或兼容层的能力:
`docs_search``docs_read``rag_lightrag_query``asset_extract_outline``asset_to_mindmap``oo_*`
因此,这份方案后续提到的“后移”应理解成:
- 先把前端收口到 Hermes bridge
- 再把 mnote 业务能力通过 Rust 边界继续收口,并作为 Hermes 可调用能力暴露
- 最终形成“Hermes 负责 agent runtimeRust 负责 mnote 业务真执行面,前端只负责 UI 与 client bridge”的结构
## 1.2 推荐的最终分工
基于当前仓库和 Hermes 官方能力,推荐的长期结构不是单中心,而是双层分工:
### A. Hermes 负责什么
- agent loop
- 通用 tool runtime
- 多轮会话状态
- OpenAI 兼容 API Server
- 流式输出与工具调用事件
- 通用记忆、skills、MCP、delegate 等 agent 能力
### B. mnote Rust 负责什么
- 文档、块、导图、OnlyOffice 等产品业务真执行
- 统一 command/query/tool protocol
- 审计、trace、request/command/event 口径
- CLI 化与稳定 JSON 契约
### C. mnote Web 前端负责什么
- 输入框、聊天记录、SSE 展示
- document/mindmap/onlyoffice context 采集
- 少量浏览器专属 client tool
- 必要的鉴权、会话映射和 client-tool-result 回传
### D. mnote 与 Hermes 的推荐衔接方式
当前更合理的方向不是让前端直接承接 Hermes 的全部能力,而是:
- 前端 -> mnote Next route
- mnote Next route -> Hermes API Server
- Hermes 在需要 mnote 业务操作时,再调用 mnote 暴露给它的 Rust 能力面
这层“mnote 暴露给 Hermes 的能力面”后续可以落在:
- MCP server
- Hermes plugin/tool adapter
- 或 mnote 自己维护的一层最小业务 bridge
但无论具体接法选哪一种,原则都应一致:
> **Hermes 不应复制一套 mnote 业务真逻辑;mnote Rust 才是产品业务真执行面。**
## 1.3 Hermes API Server 调用 mnote Rust 的最小业务桥
这部分是 task-058 的关键边界:先把“谁调用谁、调用什么、返回什么”说清楚,再决定后续是否补更重的适配层。
### 最小结论
Hermes API Server 不应直接接触前端 `/api/ai-agent/run` 的整套 TS 编排逻辑,而应通过一个非常窄的 mnote Rust 业务桥来调用真实能力。
这个桥只做三件事:
1. 接收 Hermes 的标准化 tool 调用请求
2. 转换成 mnote Rust 的 `RuntimeInput`
3. 返回 Rust 的 `plan``result`
### 推荐的桥接层级
- Hermes API Server
负责 agent loop、tool 调度、流式输出与会话状态。
- mnote Rust bridge
负责把 Hermes 的 tool 调用映射到 `core-protocol` / `bridge-runtime`
- mnote 业务执行面
负责文档、块、导图、OnlyOffice 等产品能力的真实读写。
### 最小接口边界
建议把 Hermes 可调用的 mnote 能力,先收敛成下面三类:
- `query`
只读查询,例如 `page get``docs_search``docs_read`
- `command`
写入命令,例如 `block insert`
- `tool`
复用 Rust tool registry 的能力,例如 `doc_insert_blocks``doc_replace_range`
其中最优先的两条业务能力是:
- `page get` -> Rust query`documents.content.get`
- `block insert` -> Rust write/tool`doc_insert_blocks`
### 当前仓库里已经存在的可复用接缝
- `rust/crates/core-protocol/src/tool.rs`
已经冻结了 `ToolSpec``ToolSetSpec``ToolRegistry`,并且 `docs_search``docs_read``doc_insert_blocks` 都已经在工具注册表中。
- `rust/crates/bridge-runtime/src/lib.rs`
已经提供 `execute_runtime_input()``execute_runtime_query()``execute_query()``execute_command()``execute_tool_plan()``execute_tool_result()` 这些统一入口。
- `rust/crates/core-protocol/src/query.rs`
已经有 `GetPageContent``SearchDocuments``GetBlock` 等查询载体。
- `rust/crates/core-protocol/src/command.rs`
已经有 `CommandEnvelope``CommandResult`,说明命令执行结果口径是存在的。
- `wolai-frontend/src/lib/documents/rust-runtime.ts`
当前前端已经能把 JSON 输入交给 Rust bridge 进程,说明“进程级 Rust bridge”这条路是可复用的。
### 最小业务桥的推荐形态
建议 Hermes 侧只认识一个很窄的桥协议,避免再次长成一套前端平台层:
```text
Hermes tool call
-> mnote rust bridge input
-> Rust plan/result
-> Hermes tool result
```
其中输入字段建议至少包含:
- `toolName`
- `invocationKind`
- `executionMode`
- `args`
- `workspaceId`
- `target`
- `actor`
- `source`
输出字段建议至少包含:
- `ok`
- `kind`
- `plan``result`
- `error`(失败时)
### 最小桥的落地原则
- 先支持只读桥接,再补写入桥接
- 先接 `page get`,再接 `block insert`
- 先复用现有 `bridge-runtime`,不要先重写新执行器
- 不要让 Hermes 直接依赖前端 `run/route.ts` 的 builtin registry
- 不要把能力定义重新散落到多个前端面板里
### 对当前前端的影响
前端后续只应保留:
- 场景上下文采集
- SSE/UI 展示
- 少量浏览器专属 client tool
前端不应继续承担:
- tool registry
- tool policy
- builtin 装配
- provider 编排
- Rust 能力路由
这意味着后续如果要继续减重,应该优先把 `Hermes API Server -> mnote Rust bridge` 这条线做清楚,而不是继续在 Web 里扩充 AI 平台逻辑。
---
## 2. 当前问题
## 2.1 当前不是一个 AI 面板,而是多套前端 AI 系统
当前仓库至少存在下面几套 AI UI
- 全局 AI`src/components/ai-agent/AiAgentPanel.tsx`
- 全局 Host`src/components/ai-agent/GlobalAiAgentHost.tsx`
- 页面 AI`src/components/editor/DocumentAiAgentPanel.tsx`
- Mindmap AI`src/components/editor/blocks/MindmapAiAgentPanel.tsx`
- OnlyOffice AI`src/components/onlyoffice/OnlyOfficeAiAgentPanel.tsx`
这些面板不只是视觉上重复,而是每一套都各自持有一大批前端状态和交互逻辑,例如:
- 对话 session/history
- SSE 解析
- tool logs
- provider/model 选择
- localStorage 持久化
- 工具白名单/手动选择
- codex mode
- 中止/恢复交互
这会持续拉高:
- 前端代码体积
- 客户端状态复杂度
- 维护成本
- 页面加载后的水合成本
## 2.2 `/api/ai-agent/run` 当前仍是前端侧 AI 编排中心
当前 `src/app/api/ai-agent/run/route.ts` 仍承担了大量本应属于统一后端 AI 服务的职责:
- scope -> toolset 映射
- builtin registry 构建
- allowed tools 解析
- 各类 server tools 装配
- codex 与本地/在线 provider 多分支逻辑
- client tool bridge 协调
- 一部分 Rust tool 执行接线
- 一部分 TS builtin fallback
这意味着:
- 前端 Next route 仍然是 AI 平台层
- AI 执行面没有完全后移
- AI 相关复杂度还在 Web 进程里增长
## 2.3 builtin tool registry 仍保留为前端框架资产
当前仍保留:
- `src/lib/ai-agent/tools/registry.ts`
- `src/lib/ai-agent/tools/builtins/registryBuiltins.ts`
- `src/lib/ai-agent/runtime/runAgent.ts`
- 多个 `create*ServerTools`
这说明:
- 前端不只是“调 AI”
- 前端还在“定义 AI 能干什么、怎么调、怎么路由”
这与“前端只桥接 Hermes + mnote Rust”的方向相冲突。
## 2.4 当前 AI 能力边界仍分散在多个场景面板里
例如:
- 页面 AI 直接持有 `doc_*` 工具集合与文档快照
- Mindmap AI 直接持有 mindmap tool 集与附件选择
- OnlyOffice AI 直接持有 `oo_*` client tool 协议
- 全局 AI 又有自己的 toolset chips 和 capability 展示
这意味着“场景上下文”与“工具编排”没有分离。
更合理的结构应该是:
- 场景只提供 context
- agent 编排由 Hermes 决定
- mnote 业务执行由 Rust 决定
- 前端只负责 UI 与极少数 client capability
---
## 3. 精简总原则
## 3.1 前端 AI 只保留三类职责
后续前端 AI 只应保留:
### A. 轻 UI
- 输入框
- 聊天记录
- 流式输出
- 打断/继续
- 极少量面板开关
### B. 场景上下文采集
- 当前 documentId
- 当前 mindmapId
- 当前 onlyoffice 文件信息
- 当前 selection/block snapshot
### C. 浏览器专属 client tool
例如:
- OnlyOffice 插件回调
- 浏览器本地文件/剪贴板
- 未来确实只能在浏览器执行的少数能力
除此之外,前端不应继续承担:
- tool registry
- tool policy
- builtin 分类
- tool routing
- AI orchestration
- 多 provider 执行框架
## 3.2 AI 面板本身不再按功能域复制实现
最终应从“多个重面板”收口到:
- 一个通用 `AiBridgePanel`
- 多个轻量 context adapter
即:
- `GlobalAiEntry`
- `DocumentAiEntry`
- `MindmapAiEntry`
- `OnlyOfficeAiEntry`
这些 entry 只负责传不同 context,不再复制整套面板逻辑。
## 3.3 前端不再维护 AI 工具产品说明体系
像下面这些内容,不应再长期保留在前端:
- tool labels
- tool chips
- capability 展示矩阵
- per-scope tool lists
这些都属于后端 AI 能力描述的一部分,应由 Hermes 返回,或由 mnote 后端统一下发,而不是继续硬编码在前端。
---
## 4. 建议的精简方案
## Phase A:后移 AI 编排层到 Hermes,并给 mnote Rust 留清晰业务边界
第一步不是删 UI,而是把重逻辑后移。
### 需要后移的内容
- `createToolRegistry`
- `resolveAllowedToolIds`
- `builtinTools`
- `builtinToolSets`
- `runAiAgent`
- 各类 `create*ServerTools`
- scope -> toolset 的静态映射逻辑
### 目标结构
前端 `/api/ai-agent/run` 只保留:
- 鉴权
- 规范化 `scope/messages/context/attachments/clientCapabilities`
- 把请求转发给 Hermes API Server
- 把 Hermes 的 SSE / `tool_call` / `tool_result` 回流给前端面板
- 转发 client tool call/result
- 在 Hermes 需要调用 mnote 业务能力时,转给 mnote Rust 暴露出来的业务能力面
- 对少数短期无法迁走的 `TS_TRANSPORT_KEEP` 能力保留最小兼容壳
换句话说:
> `/api/ai-agent/run` 从“AI 编排器”降级为“AI 网关”。`
### 收益
- 显著降低 Next route 的 AI 编排复杂度
- agent runtime 与 mnote 业务执行面彻底分层
- 前端后续不必再继续长 builtins、registry 和 provider 编排
- 后续若继续 CLI 化,也能直接复用 Hermes API Server 与 mnote Rust 协议边界
### Phase A 的现实限制
这一阶段不能简单理解为“删除所有 TS builtins 就结束”。
因为当前还有两类事情没有完全打通:
- Hermes API Server 还没在本机正式启用并接入 mnote
- mnote Rust 业务能力还没全部以 Hermes 可调用的方式暴露
所以 Phase A 的实际目标应是:
- 先让 `/api/ai-agent/run` 从“自己编排”变成“转发 + 桥接”
- 再逐步清理遗留 builtin
- 不是一刀切直接删除所有中间层
## Phase B:统一 AI 面板实现
在后移编排层之后,再收 UI。
### 当前问题
四套 AI 面板都在重复维护:
- 对话
- 工具日志
- provider/model
- localStorage
- 中断/恢复
### 目标结构
新增统一通用面板,例如:
- `AiBridgePanel`
再由不同场景只提供轻量包装:
- `DocumentAiEntry`
- `MindmapAiEntry`
- `OnlyOfficeAiEntry`
- `GlobalAiEntry`
这些 entry 只负责:
- 是否显示
- 传入 context
- 传入 clientCapabilities
- 传入 UI 文案
### 收益
- 删除四套重复状态机
- 降低包体与维护成本
- 后续新场景不再复制面板
## Phase C:弱化或下线全局 AI
当前全局 AI 被直接挂在:
- `src/app/(app)/layout.tsx`
它带来的问题不是“首屏一定特别重”,而是:
- 全局产品心智更复杂
- 持续占用一套入口与状态
- 会诱导继续扩展“全局工具平台”
建议策略:
- 第一阶段:保留代码,但默认隐藏/弱化入口
- 第二阶段:若页面 AI 已覆盖主场景,则把全局 AI 下线为开发页或实验开关
### 为什么优先保页面 AI 而不是全局 AI
因为页面 AI 更接近核心使用场景:
- 对页面正文直接读写
- 与编辑器桥接紧密
- 用户心智最清晰
全局 AI 更像附加层,不值得优先保留完整前端壳。
## Phase D:明确保留的少数浏览器专属能力
有一些能力确实不能完全后移,应明确留在前端:
- `oo_*` 这类 OnlyOffice 客户端工具
- `/api/ai-agent/client-tool-result`
- 少数必须读浏览器本地态的能力
这些能力的处理原则是:
- 保留
- 但只能作为 `client capability bridge`
- 不得重新长成新的前端 AI 平台层
---
## 5. 推荐优先级
如果只按收益排序,我建议这样做:
### P0:先做
- 启用并验证 Hermes API Server
- 把 `/api/ai-agent/run` 收口成 Hermes bridge
- 冻结前端新增 builtin tool / toolset / provider 编排逻辑
- 明确 mnote Rust 能力如何暴露给 Hermes 调用
### P1:接着做
- 把多套 AI 面板收口为一个通用 `AiBridgePanel`
- 页面 / Mindmap / OnlyOffice 改成 context adapter
### P2:再做
- 弱化或隐藏全局 AI Host
- 让全局 AI 退到实验入口或开发入口
### P3:最后做
- 清理旧的 builtins、registry、重复 localStorage/session 管理
- 删除历史兼容面板实现
---
## 6. 除 AI 之外,还有哪些可以精简
这部分先只记录,不进入当前 harness 任务。
## 6.1 文档页加载链可以继续减重
当前文档页存在:
- `DocumentShell``mounted + dynamic + ssr:false`
- `DocumentContent``dynamic``BlockNoteEditor`
- `meta``content` 的两阶段加载
这些都可能造成刷新时的“空一下再出现”的体感。
建议后续单独评估:
- 去掉 `DocumentShell` 这层多余壳
- 减少文档页串行加载层级
- 优先把首屏必需数据前移
## 6.2 SearchPalette 目前是全局常驻挂载
当前:
- `SearchPalette` 直接挂在 `app/(app)/layout.tsx`
如果它本身比较重,后续可考虑:
- 改为按需挂载
- 或在首次打开时再加载
## 6.3 Sidebar 职责仍然非常重
`Sidebar` 当前承担了太多:
- 页面树
- 文件树
- 资产操作
- 拖拽复制
- 删除恢复
- 批处理
- mindmap/table/media 入口
这会让 Sidebar 成为高复杂度常驻组件。
后续可以考虑:
- 按功能拆分
- 降低初始挂载职责
- 把非首屏必要的动作延后
## 6.4 文档页周边抽屉与面板可以按需加载
当前文档页同时带着:
- `PageOptionsSidebar`
- `PageBacklinksPanel`
- `DocumentHistoryDrawer`
- `DocumentCommentsDrawer`
- `DocumentAiAgentPanel`
后续可评估哪些可以从“默认挂载”改成“首次打开再加载”。
---
## 7. 最终建议
如果你的目标是:
> **先精简能精简的东西来改善网页前端加载与复杂度**
那么最值得优先推进的不是重写 Web,也不是先重做编辑器,而是:
> **把前端 AI 从“平台层”收缩成“Hermes + mnote Rust 的桥接层”。**
一句话版结论:
- **该砍的不是 AI 按钮本身,而是前端 AI 编排框架。**
- **该保留的是轻面板、Hermes bridge、mnote Rust 业务执行面和浏览器专属 client bridge。**
- **全局 AI 可以弱化,页面 AI 保留为主入口。**
- **除 AI 外,文档页加载链、全局 SearchPalette、巨型 Sidebar 也是后续值得继续精简的方向,但先不进入当前 harness。**
@@ -0,0 +1,459 @@
# [recycle] 文档访问性能根治重构路线 v1
> 更新时间:2026-04-16
>
> 相关文档:
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/ai-frontend-simplification-plan-v1.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md`
## 1. 目标
这份文档只回答一个长期问题:
> **如果不满足于“临时优化”,而要从架构上根治文档访问和页面切换卡顿,mnote 应该重构成什么样。**
本文的目标不是“再做几处按需加载”,而是定义:
- 哪些前端能力应继续保留
- 哪些能力应退出当前重前端壳
- Rust Web 层应该采用什么形态
- `BlockNote` 应该如何被隔离,而不是继续拖慢整个页面系统
---
## 2. 当前事实
结合当前仓库结构,可以先确认四个事实:
### 2.1 当前最重的前端成本,不在服务端语言
当前文档页主链仍然是:
`documents/[id]/page.tsx -> DocumentShell -> DocumentContent -> BlockNoteEditor`
而且存在以下典型问题:
- 文档进入页面后仍有 `meta -> content -> editor` 的串行加载链
- `DocumentShell``DocumentContent` 仍在客户端串联挂载
- `BlockNoteEditor` 本身是当前最重的前端运行时之一
- 文档页周边仍挂载多类抽屉、面板和桥接组件
这说明当前访问速度问题的核心,不是“Node 不够快”,而是:
> **页面切换时仍然把太多东西当成同一层前端运行时来初始化。**
### 2.2 当前只有一类能力明确难以短期替换
从产品结构看,短期最难替换的是:
- `BlockNote`
因为它同时承担:
- 文档编辑核心
- 自定义 block 运行时
- 当前正文交互和保存链
而下面这些东西,从长期看都不是必须继续绑在当前 React/Next 大壳里的:
- Sidebar / 页面树 / 文件树
- 搜索壳与搜索结果面板
- Mindmap
- AI 面板
- 历史 / 评论 / 回链 / 页面选项等周边面板
- 在线表格
### 2.3 OnlyOffice 不应被当成主阻塞项
根据当前架构,OnlyOffice 本来就是独立页面型编辑器,不是正文内嵌主编辑器。
这意味着:
- 它不会决定文档页的首屏切换模式
- 它可以继续保留“外挂页面/外部编辑器”定位
- 它不应影响主文档访问性能路线判断
### 2.4 Mindmap 是优先级更高的可替换对象
当前 Mindmap 虽然复用了前端组件链,但它并不像 BlockNote 那样不可轻易动。
因此长期上:
- Mindmap 可以先于 BlockNote 重写
- 它很适合作为“从当前重前端壳中剥离”的第一批对象
---
## 3. 根治原则
如果目标是“根治”,而不是“补丁式提速”,那么应遵守以下原则。
### 3.1 页面切换必须先回到“读优先”
当前问题的根子之一,是页面切换几乎默认在进入“编辑器世界”。
长期正确方向应改为:
- 先快速进入页面阅读态
- 再按需进入编辑态
- `BlockNote` 不得阻塞普通页面访问
换句话说:
> **文档页要从“先挂编辑器,再展示页面”改成“先展示页面,再按需挂编辑器”。**
### 3.2 除 `BlockNote` 外,其余能力应尽量退出当前重前端壳
长期目标不应是“继续给当前 Next/React 应用做瘦身”,而应是:
- 让能退出的都退出
- 让必须保留在浏览器的只剩真正需要浏览器的那部分
理想形态是:
- 文档阅读页:极薄
- 文档编辑岛:`BlockNote`
- 外挂编辑器:OnlyOffice
- 独立能力:Mindmap、AI、搜索、Sidebar 等各自最小化
### 3.3 Rust 化的意义在于“统一业务执行面”,不是“自动更快”
把东西改成 Rust 并不会自动让页面切换更快。
Rust 真正的价值在于:
- 统一业务执行平面
- 让文档、块、导图、搜索、树结构、审计、AI 业务桥收口到同一体系
- 支持更薄的前端和更强的 SSR / server-first 输出
所以“Rust 化”必须和“页面运行模型重构”一起发生,才构成根治。
---
## 4. 技术选型判断
这一节只回答一个问题:
> **长期重构时,到底是“就用 Rust”,还是要明确选择 `axum` 等 Rust Web 框架。**
结论先给出:
> **不能只说“用 Rust”。长期要落地,必须明确分层选型。推荐方案是:`axum` 作为 Web/服务承载层,配合 `Leptos Islands` 作为 server-first 页面壳;不推荐只用 `axum`,也不建议把 Dioxus/Yew 作为主文档访问层的第一选择。**
### 4.1 为什么不能只说“Rust”
“Rust”是语言,不是 Web 渲染方案。
如果只决定“以后改成 Rust”,但不明确:
- 服务端路由由谁承接
- 页面 SSR 由谁承接
- islands / hydration 模型由谁承接
- 浏览器交互层由谁承接
那最后很容易回到:
- 只是把 API 改写成 Rust
- 但前端页面模型仍然没变
这不能根治。
### 4.2 `axum` 的定位:非常适合做 Web 承载层,但不负责前端 UI 模型
官方 `axum` 文档显示,它非常适合承担:
- Router
- Handler
- Middleware
- JSON / Query / Form / WebSocket / SSE
- `tokio` 上的高并发服务
这使它非常适合作为 mnote 长期的:
- API 层
- 文档查询层
- 树结构聚合层
- 搜索层
- AI / Hermes 业务桥
- SSR 页面外壳承载层
但要注意:
> **`axum` 不是前端渲染框架。它能承接 Web 服务,不会替你解决“页面如何减少 hydration、如何隔离 BlockNote”这类前端模型问题。**
因此,**只用 `axum` 不够。**
### 4.3 为什么推荐 `Leptos Islands`
官方 Leptos Islands 文档最值得关注的点是:
- 默认服务端组件不进入客户端
- 只有显式 `#[island]` 的部分才被编译到浏览器
- islands 应尽量“小而具体”
这和 mnote 的长期目标高度一致,因为你现在最想要的是:
- 绝大多数页面内容不再变成大块客户端运行时
- 只有真正需要交互的部分进入浏览器
- `BlockNote` 成为最后一个重交互 island
也就是说,Leptos Islands 的思路天然支持:
- 页面树/文件树作为轻交互 island
- 搜索框作为轻交互 island
- AI 面板作为独立 island
- `BlockNote` 作为最后的重 island
- 其余大量阅读内容只做 server-rendered HTML
这比“继续在全页 React hydration 里做局部优化”更接近根治。
### 4.4 为什么不把 Dioxus 作为主推荐
官方 Dioxus Fullstack 文档自己就把它定位得更偏“app-like”:
- 它支持 SSR
- 但更强调交互型应用
- 文档中明确区分 CSR 的 app 架构与 SSR 的 site 架构
而 mnote 的长期目标不是继续维持一个“整页 app 先跑起来”的文档访问模型,而是:
- 页面先出来
- 交互再局部接上
所以 Dioxus 不是不能用,而是**不如 Leptos Islands 对这个目标直接。**
### 4.5 为什么不把 Yew 作为主推荐
Yew 更接近经典 Rust/WASM 前端框架。
它的问题不是能力不够,而是:
- 更偏客户端应用思路
- 不天然突出 islands-first
- 对你当前“尽量减少客户端代码、把交互压缩为少数岛”的目标,不是最优选
---
## 5. 推荐的长期方案
## 5.1 总体选型
推荐长期架构采用:
- **服务承载层:`axum`**
- **页面壳与 islands 模型:`Leptos Islands`**
- **业务执行面:现有 mnote Rust `core-protocol + bridge-runtime + storage-convex-bridge` 继续演进**
- **AI runtimeHermes API Server**
- **最后保留的重编辑岛:`BlockNote`**
一句话版:
> **`axum` 负责服务,`Leptos` 负责薄页面与 islandsRust 内核负责业务,Hermes 负责 agent`BlockNote` 留作最后的重前端孤岛。**
## 5.2 为什么这是最适合 mnote 的方案
因为这个方案同时满足四个条件:
### A. 适合逐步迁移
你不需要一次性推翻所有前端。
可以先把:
- Sidebar
- 页面树 / 文件树
- 搜索壳
- 阅读页
- Mindmap
逐步迁到 `axum + Leptos`
再把 `BlockNote` 留在最后处理。
### B. 适合“读写分离”
`Leptos Islands` 很适合把页面拆成:
- 读页面:服务端输出
- 写页面:局部 island
这正是文档访问性能根治的核心。
### C. 适合 Rust 单栈长期收口
当前你已经做过大范围 Rust 重构,未来继续统一到:
- Web 壳
- 业务协议
- AI 业务桥
- CLI
会比继续维护“Rust 内核 + 重 Next 前端壳”的分裂状态更稳定。
### D. 风险可控
你不需要一开始就重写 `BlockNote`
最难的部分可以留到最后,不会卡死整个长期路线。
---
## 6. 最终目标架构
长期目标建议收敛成下面这张逻辑图:
```text
浏览器
-> axum 路由层
-> Leptos server-rendered 文档壳
-> 少量 islands
- Sidebar / 页面树 / 文件树
- Search
- AI 面板
- Mindmap(重写后)
- BlockNote(最后保留的重岛)
axum / Leptos
-> mnote Rust 业务执行面
- 文档
- 块
- 搜索
- 导图
- 审计 / trace / event
Hermes API Server
-> 调用 mnote Rust 暴露的业务能力桥
OnlyOffice
-> 继续独立页面 / 外挂编辑器
```
在这个目标架构里:
- 页面切换不再默认进入“整页前端应用”
- 绝大多数阅读内容不需要大块 hydration
- `BlockNote` 成为唯一需要谨慎处理的大前端负担
---
## 7. 推荐迁移顺序
长期路线建议分四阶段,不建议一口气重做。
## Phase 1:先把文档访问模型改成 server-first
先做:
- 用 Rust Web 壳承接文档阅读态
- 页面进入时直接输出 `meta + content`
- 不再进入页面后再串行拉正文
- 把“页面切换”和“编辑器启动”拆开
这一阶段的目标不是替换 BlockNote,而是先把它从切页主链里移走。
## Phase 2:优先重写可替换的常驻模块
优先级建议:
1. Sidebar / 页面树 / 文件树
2. 搜索壳与搜索结果页
3. 页面选项、历史、评论、回链等周边面板
4. AI 面板
原因:
- 这些东西是跨页面常驻能力
- 高频出现
- 比 BlockNote 更容易先抽离
## Phase 3:重写 Mindmap,继续缩小前端重负担
Mindmap 是非常适合优先退出当前重前端壳的对象。
建议:
- 把导图数据和执行面继续放在 Rust
- 前端重新实现为更轻的独立渲染层
- 不再依赖当前文档页大壳去承载
## Phase 4:最后再处理 BlockNote
这时再决定:
- 是否继续保留 BlockNote,但隔离为独立 island
- 是否逐步替换 BlockNote 的部分能力
- 是否长期把编辑体验拆成更多 Rust 原生能力 + 少量 JS 编辑器兼容层
**在此之前,不建议把 BlockNote 当成第一刀。**
---
## 8. 非目标
为了避免路线漂移,下面这些都不应被误认为“根治方案”。
### 8.1 不是“把 Next API 改成 Rust 就算完成”
如果页面仍然是:
- 重客户端壳
- 重 hydration
- 切页即初始化编辑器
那只是服务端换语言,不是根治。
### 8.2 不是“先全面重写 BlockNote”
BlockNote 是最难对象,不应先作为第一刀。
正确顺序应是:
- 先移除外围负担
- 最后再碰 BlockNote
### 8.3 不是“继续在当前 React 壳里无限做局部修补”
按需加载、拆组件这些短期仍有价值,但如果最终仍然保留:
- 大型客户端文档壳
- 大型常驻 Sidebar
- 大型客户端页面切换模型
那还是治标不治本。
---
## 9. 最终建议
如果以“根治文档访问性能”为唯一目标,我给出的长期技术判断是:
> **应该继续推进 Rust 主导重构,但不是笼统地说“以后用 Rust”,而是明确采用 `axum + Leptos Islands + mnote Rust 内核 + Hermes` 的组合。**
其中:
- `axum`
适合做 Web 服务承载层、API、SSE、AI 业务桥、SSR 外壳。
- `Leptos Islands`
适合做 server-first 文档页和极小交互岛,是把 `BlockNote` 隔离成“最后一个重前端岛”的最佳路线。
- `Dioxus`
可以关注,但更适合作为偏 app 化交互场景的备选,不是当前文档访问性能根治的第一推荐。
- `Yew`
不作为当前主路线推荐。
最后用一句话收口:
> **mnote 的根治路线,不是“把现有前端改快一点”,而是“把除了 BlockNote 之外的大多数页面能力从当前重前端壳中迁出,交给 Rust 主导的 server-first Web 架构承载”。**
---
## 10. 外部参考
以下外部资料支持上面的技术判断:
- `axum` 官方文档:
https://docs.rs/axum/latest/axum/
- Leptos Islands 指南:
https://book.leptos.dev/islands.html
- Dioxus Fullstack SSR 文档:
https://dioxuslabs.com/learn/0.7/essentials/fullstack/ssr/
@@ -0,0 +1,284 @@
# [recycle] mnote 单仓收口 Rust 内核阶段 Checklist
> 更新时间:2026-04-14
>
> 主工作路径:`/mnt/Data1T/mnote`
>
> 第二工作路径:`/mnt/Data1T/mnote-rust`(仅作为历史资产来源,不作为主执行仓)
>
> 对齐文档:`/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-plan.md`
>
> 目标:把 `mnote-rust` 的必要 Rust 内核逐步回迁到 `mnote` 主仓,并始终维持“单仓启动、单仓保存、主前端不分叉”的执行口径。
## 1. 执行口径与硬边界
- [x] 只保留 `/mnt/Data1T/mnote` 作为唯一主产品仓。
- [x] `/mnt/Data1T/mnote-rust` 只保留为历史资产来源、参考实现和待吸收规范,不再承担主线执行入口。
- [x] `wolai-frontend` 继续承担唯一主产品前端壳,不复制 `mnote-rust` 的第二套 Next 前端壳。
- [x] Rust 内核统一收口到 `/mnt/Data1T/mnote/rust/`,不再在主仓根目录散落多个 Rust 源码根。
- [x] 最终保存只认 Convex 主事实层;Rust 的事件、索引、缓存、fixtures 都属于派生层。
- [x] 不通过跨仓软链接、硬链接或路径偷接来假装完成迁移。
- [x] 第一阶段不迁入 `design/phases/**``design/execution/**``design/UI/**` 这类历史叙事文档。
- [x] 第一阶段不迁入 `mnote-cli``adapter-onlyoffice``adapter-mindmap``adapter-legacy-mnote`
- [x] Mindmap 继续以 `mnote` 当前实现为主:BlockNote 自定义块为主形态,独立全屏页为辅助形态。
- [x] OnlyOffice 继续保持“正文只嵌文件入口、编辑器在独立页面运行”的产品形态,不回嵌 BlockNote。
- [x] `wolai-frontend/``wolai-backend/``infra/convex/``src/components/onlyoffice/` 默认保持不动,除非后续阶段明确接线需要。
## 2. 当前已确认基线(2026-04-13 已核对)
### 2.1 已完成事实
- [x] `/mnt/Data1T/mnote/rust/Cargo.toml``/mnt/Data1T/mnote/rust/Cargo.lock` 已存在。
- [x] `/mnt/Data1T/mnote/rust/crates/` 下已落位 `core-domain``core-protocol``event-log``storage-convex-bridge``index-fts``bridge-runtime``mnote-cli`
- [x] `/mnt/Data1T/mnote/rust/design/INDEX.md``design/core/01~04` 核心设计文档已落位。
- [x] workspace members 仅指向 `/mnt/Data1T/mnote/rust/crates/*`
- [x] 已验证 `cargo metadata --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml --format-version 1` 可通过。
- [x] 已验证 `cargo check --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml` 可通过。
- [x] 已核对 `/mnt/Data1T/mnote/rust/` 下未发现直接引用 `/mnt/Data1T/mnote-rust` 的残留路径。
- [x] 已确认当前主前端链路仍在 `mnote`:文档页为 `DocumentShell -> DocumentContent -> BlockNoteEditor`Mindmap 仍复用现有 `MindmapBlock` 体系,OnlyOffice 仍为独立页面型编辑器。
### 2.2 当前仍未开始或未初始化的事实
- [x] `/mnt/Data1T/mnote/rust/bridge/` 已初始化最小说明目录,`crates/bridge-runtime` 已提供 Phase 1 最小真实执行样板。
- [x] `/mnt/Data1T/mnote/rust/scripts/` 当前未初始化。
- [x] `/mnt/Data1T/mnote/rust/fixtures/` 当前未初始化。
- [x] `/mnt/Data1T/mnote/rust/tests/` 当前未初始化。
- [x] `mnote-cli` 已并入 `/mnt/Data1T/mnote/rust/crates/`,当前已冻结 `page/block/search/sidebar/tool` 的最小命令面与 `--json` 输出协议。
- [x] `adapter-onlyoffice``adapter-mindmap``adapter-legacy-mnote` 尚未并入主仓。
- [x] `/api/documents/content``/api/documents/save``/api/documents/title``/api/documents/stats``/api/sidebar``/api/blocks/patch` 已接入主仓当前 bridge 包装层并统一返回 `request_id/trace_id` 元信息;其中 `documents/title``documents/stats``documents/options``documents/save` 已进一步收口到统一的 bridge mutation request 运行时接缝,可先构造与 `storage-convex-bridge::build_write_request` 对齐的 `functionName/payloadJson/args` 对象再落到 Convex,但整条链仍未直接切到 `core-protocol -> storage-convex-bridge` 的真实 Rust crate 执行链。
2026-04-15 Phase 1/2 补记:`documents.content.get``documents.title.update``documents.save` 这 1 读 2 写链现已从主仓 Next route 真实进入 `crates/bridge-runtime`,再由 TS 仅做 Convex transport;同时 `crates/mnote-cli` 已在主仓 workspace 落位,并通过 `page/block/search/sidebar/tool` 五类最小命令面的 `--json` 计划输出冻结当前 CLI 协议。
## 3. Phase ARust workspace 落位(主目标已完成)
完成目标:
- `/mnt/Data1T/mnote/rust/` 成为唯一 Rust workspace 根。
- P0 crate 与核心规范文档已在主仓内有真实副本。
- 新 workspace 已具备最小自检能力。
Checklist
- [x] 固定 `/mnt/Data1T/mnote/rust/` 为唯一 Rust workspace 根,后续 Rust 命令统一从这里发起。
- [x] 固定 workspace members 只指向 `crates/core-domain``crates/core-protocol``crates/event-log``crates/storage-convex-bridge``crates/index-fts`
- [x] 复制并落位 `core-domain``core-protocol``event-log``storage-convex-bridge``index-fts`
- [x] 复制并落位 `rust/design/INDEX.md``rust/design/core/01-domain-model-v0.md``02-command-query-tool-protocol-v0.md``03-storage-event-indexing-v0.md``04-onlyoffice-integration-boundary-v0.md`
- [x] 统一 workspace 级别的 `edition``license``version``authors` 约定。
- [x] 跑通 `cargo metadata --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml --format-version 1`
- [x] 跑通 `cargo check --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml`
- [x] 已补跑 `cargo test --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p core-domain``cargo test --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p core-protocol`
- [x] 已在本 checklist 第 1 节固定“第一阶段禁止迁入清单”,明确 `mnote-cli``adapter-*``design/phases/**``design/execution/**`、第二套 Next 前端壳均不属于本阶段交付。
Phase A 验收:
- [x] Cargo 可在主仓内解析 workspace。
- [x] 主仓内已有 P0 内核与长期规范副本。
- [x] 当前未出现对第二工作路径的直接 Cargo 路径依赖。
- [x] 已完成 crate 单测与文档级禁止迁入清单,Phase A 可视为关闭。
## 4. Phase Bbridge 入口与 Convex 桥接
完成目标:
- 在 `mnote` 主仓内形成唯一 Rust command/query 入口。
- 首批读写入口从“前端 API 直接调 Convex”切到“前端 API -> Rust bridge -> Convex”。
当前起点(已确认):
- [x] `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/content/route.ts` 当前直接 `query(api.documents.getContent)`
- [x] `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/save/route.ts` 当前已改为构造稳定 envelope 后交给 `save-command-adapter` 执行。
- [x] `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/title/route.ts` 当前已不再直接拼 `mutation(api.documents.updateTitle)` 参数,改为构造稳定 envelope 后交给页面元信息 command adapter 执行。
- [x] `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/stats/route.ts` 当前已不再直接拼 `mutation(api.documents.updateStats)` 参数,改为构造稳定 envelope 后交给页面元信息 command adapter 执行。
- [x] `/mnt/Data1T/mnote/wolai-frontend/src/app/api/sidebar/route.ts` 当前直接聚合多个 Convex query 结果。
- [x] `/mnt/Data1T/mnote/wolai-frontend/src/app/api/blocks/patch/route.ts` 当前直接读取文档内容、替换 block 树后回写 Convex。
Checklist
- [x] 已在 `wolai-frontend/src/lib/documents/bridge.ts` 固定当前 bridge 入口,先由主仓 API route 统一走同一层包装。
- [x] 已固定首批 bridge envelope`request_id``trace_id``workspace_id``actor``source``idempotency_key`
- [x] 约定所有新命令和查询都先进入 `core-protocol` envelope,再转给 `storage-convex-bridge`
- [x] 已把 `documents/content``sidebar` 两条低风险读链接入当前 bridge query 包装层。
- [x] 已把 `documents/title``documents/stats``documents/save``blocks/patch` 接入当前 bridge command 包装层。
- [x] 已为 `documents/title``documents/stats` 收口稳定的页面元信息 command adapter,冻结 route 到 Convex 之间的 payload/target/meta 映射,避免 route 继续直接耦合 `updateTitle/updateStats` 的参数细节。
- [x] 已为首批 4 条写链固定当前 `bridge -> Convex` 的函数名、payload 结构和 workspace scope;其中 `documents.title``documents.stats``documents.options``documents.save` 已先收口到 `buildDocumentBridgeMutationRequest` 这层最小运行时接缝,统一构造与 `storage-convex-bridge::build_write_request` 对齐的 `functionName/payloadJson/args`,再由同一执行器落到 Convex`blocks.patch` 仍未收口到该执行层,且整条链尚未直接切到 Rust crate 内统一执行。
- [x] 已在 `wolai-frontend/convex/schema.ts` 新增 `command_logs``domain_events` 两张表,并新增 `wolai-frontend/convex/bridgeLogs.ts``src/lib/documents/bridge-log.ts` 承接首批写链日志落账。
- [x] 首批 4 条写链(`documents/title``documents/stats``documents/save``blocks/patch`)当前会在成功路径同时写入 command log 与 domain event;失败态、回滚态和统一事务性仍待后续补齐。
- [x] 当前已接入的查询链(`documents/content``sidebar`)保持只读,不产生写副作用。
- [x] 已统一 `documents/content``documents/save``documents/title``documents/stats``sidebar` 的首批 bridge 错误返回结构。
- [x] 当前 bridge 仅负责协议校验、请求映射与响应元信息包装,尚未吞并产品 UI 逻辑。
2026-04-14 B 阶段补记:已在 `rust/crates/core-protocol` 中补齐 `documents.meta.get``documents.content.get``sidebar.dataset.list``documents.save``blocks.patch` 的最小 query/command 协议对象,并在 `rust/crates/storage-convex-bridge` 中补齐对应的 query/command name -> Convex function 映射与单测。当前主仓新增链路已统一先构建稳定 envelope,再由后续真实 Rust bridge 执行链消费;本轮已通过 `cargo test -p core-protocol -p storage-convex-bridge``cargo check --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml` 验证协议与映射闭环。
2026-04-14 B 阶段第三刀补记:已在 `wolai-frontend/src/lib/documents/bridge.ts` 新增 `buildDocumentBridgeMutationRequest` 与统一执行器,把 `documents.title.update``documents.stats.update``documents.options.update``documents.save` 进一步推进到最小可运行的 Rust bridge 风格接缝。当前这些写链会先生成与 `storage-convex-bridge::build_write_request` 对齐的 `functionName/payloadJson/args` 运行时对象,再由适配层调用 Convex mutation;配套 `src/lib/documents/bridge.test.ts` 已新增 request builder 断言,确认标题更新与正文保存两条链的运行时 request 形状稳定。
Phase B 验收:
- [x] 当前至少已有 2 条查询链(`documents/content``sidebar`)和 4 条写链(`documents/title``documents/stats``documents/save``blocks/patch`)通过当前 bridge 包装层读写 Convex。
- [x] 已补 `/api/bridge/trace``/api/bridge/request` 回查入口,并已切到 `wolai-frontend/convex/bridgeLogs.ts` 查询主线;`convex/audit.ts` 当前保留为并行旧实现线,不作为本轮回查入口。
- [x] 当前已接入的请求错误结构已统一到 bridge 错误响应,不再直接抛出临时调试字符串。
- [x] 本批改动只改 API route 与调用 payload,未改页面结构与 UI 外观。
建议验证:
- [x] `cargo check --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml`
- [x] 已对 `/api/documents/content``/api/sidebar` 做最小读链 smoke:当前主仓实例固定为 `http://127.0.0.1:3001``/api/sidebar` 已返回 `activeWorkspaceId=ws-smoke-temp`,新建文档后 `/api/documents/content` 可正常返回内容与 `requestId/traceId`
- [x] 已对 `/api/documents/title``/api/documents/stats``/api/documents/save``/api/blocks/patch` 做最小写链 smoke:真实写入成功,且随后 `/api/documents/content` 已读回 `blocks.patch` 改写后的正文。
- [x] 已完成真实 Convex 运行前置验证:`infra/convex/docker-compose.yml` 可启动本地自托管 Convex`pnpm exec convex dev --once --tail-logs disable --env-file ../.env.all --run ping:ping` 已通过。
- [x] 已用真实 Convex CLI 对 `bridgeLogs:recordCommandLog``bridgeLogs:recordDomainEvent``bridgeLogs:listByTrace``bridgeLogs:listByRequest` 做最小 smoke,确认 command log、domain event、trace/request 字段能同时落账并回查。
- [x] 已完成网页/API 级 smoke:从真实 `/api/documents/title``/api/documents/stats``/api/documents/save``/api/blocks/patch` 返回的 `requestId/traceId`,可继续经 `/api/bridge/trace``/api/bridge/request` 回查到对应 `command_logs/domain_events`。本轮同时修复 `src/lib/documents/bridge-log.ts``getAuthedConvexClient()` 返回值解构错误(此前会导致写链落账时报 `client.mutation is not a function`)。
## 5. Phase C:页面元信息、Sidebar 与正文保存接入
完成目标:
- 在不更换主前端壳的前提下,把文档元信息、Sidebar 聚合、BlockNote 正文保存逐步切到 Rust 协议层。
- 执行顺序严格按 `C1 页面元信息 -> C2 Sidebar 聚合 -> C3 正文保存` 推进,不跳步。
### C1 页面元信息
- [x] 已盘点并锁定文档页元信息读写边界:`src/app/(app)/documents/[id]/page.tsx` 负责元信息入口,`document-content.tsx` 负责标题/页面选项/统计信息写入,`document-shell.tsx` 仅继续透传既有 UI 壳参数。
- [x] 已锁定并接入当前 C1 最小读链:`src/app/(app)/documents/[id]/page.tsx` 原先直调 `api.documents.getMeta`,现已改为经 `GET /api/documents/meta` 进入现有 bridge query 包装层;新增的服务端 helper 仅负责透传当前请求认证/追踪头并发起同源 bridge route 请求。
- [x] 已把标题更新、页面属性、页面选项、统计信息这批低风险页面元信息操作切到统一的当前协议/适配边界:标题、统计信息、页面选项 3 条写链都已接入当前 bridge command 包装层;其中 `documents.title.update``documents.stats.update``documents.options.update` 已统一收口到 `wolai-frontend/src/lib/documents/metadata-command-adapter.ts`route 只保留 validation、`buildDocumentBridgeContext``buildDocumentCommandEnvelope` 与稳定错误模型。`documents.options` 本轮不再内联 `client.mutation(api.documents.updateOptions, ...)`。同时已补齐 `rust/crates/core-protocol``UpdatePageTitle``UpdatePageStats``UpdatePageOptions` 结构与 `storage-convex-bridge` 的 command name -> Convex mutation 映射测试;2026-04-14 又进一步把这 3 条写链切到 `buildDocumentBridgeMutationRequest`,先构造与 Rust `ConvexMutationRequest` 对齐的最小运行时 request,再交给统一执行器落到 Convex,为后续真实 Rust 执行链留出稳定接缝。
- [x] 当前最小替换已保持现有页面 UI、权限判断、动态导入结构不变:`DocumentShell` 入参、`readOnly/disableDownload/disableCopy` 计算、`notFound()` 分支与页面结构保持不变,仅将元信息读取入口切到 `documents.meta` bridge query。
- [x] 已统一页面元信息链当前口径:`documents.meta` 读链与 `documents.title/stats/options` 写链均复用 `buildDocumentBridgeContext` / envelopequery payload 与 command payload 已固定 `documentId/workspaceId`target.pageId 固定文档 business id,回传元信息统一为 `requestId/traceId/queryName|commandId|commandName`actor/source 也统一来自 bridge context。
- [x] 已为标题更新、页面属性、统计信息补齐当前最小回归验证。
当前最小验证:已对页面选项写链补跑相关文件 eslint / vitest,并完成真实 HTTP smoke,确认在主仓 `http://127.0.0.1:3001` 实例下以 `ws-smoke-temp` 工作区调用 `POST /api/documents/options` 后,会返回 `requestId=req_aef4159e-24f2-43a8-b168-864c4b2b2c6c``traceId=trace_3cbc114e-db01-410f-933c-e63d8bd83393``commandId=cmd_2cb6871d-9075-4252-b698-a7c4ea069bfd`,且后续 `/api/bridge/request``/api/bridge/trace` 已能回查到对应 `command_logs/domain_events`。本轮同时保留 `documents.meta` 读链最小验证:真实 `GET /api/documents/meta?documentId=...&workspaceId=ws-smoke-temp` 已返回 `doc + meta.requestId/traceId/queryName=documents.meta.get`。另外 `documents/stats` 写链的真实 HTTP smoke 也已完成并可回查。2026-04-14 新增一轮真实复测:同一 `documentId` 连续执行 `POST /api/documents/options {showToc:true,layoutDensity:\"compact\"}``POST /api/documents/stats {wordCount:12,characterCount:34,blockCount:2,todoTotal:5,todoDone:3}` 后,紧跟 5 次 `GET /api/documents/meta` 均稳定返回本次写入值,未再复现“同次写入后 meta 读回不稳定”的现象。为避免历史脏数据导致偶发漂移,已把 `convex/documents.ts` 的关键 `by_document_id` 读写收口到 deterministic canonical record 选择逻辑,并补了重复 business id 的回归测试。
2026-04-14 第二批真实收口:已将仍直接依赖 `by_document_id.first()` 的高风险页面/文档关联路径继续切到同一 canonical document helper,当前已覆盖 `convex/documentShares.ts``convex/documentGroupShares.ts``convex/documentStars.ts``convex/comments.ts``convex/references.ts`。这批路径分别影响页面共享权限判定、群组公开、收藏可见性、评论线程读写与反链标题读取;此前若同一 business `documentId` 存在重复物理记录,会继续存在命中漂移风险。本轮已补跑目标文件 `eslint``src/lib/documents/document-record.test.ts`,并用源码扫描确认这批目标文件内已不再残留 `by_document_id.first()`;当前未继续扩到 `mediaAssets.ts``mindmaps.ts` 等非本批最小闭环路径。
2026-04-14 第三批真实收口:继续只处理剩余 still-high-risk 的 `documents.by_document_id` 直接命中点,已新增 `convex/_utils/documentRecord.ts#getCanonicalParentDocumentId`,并把 `convex/comments.ts``convex/documents.ts` 中共享/权限祖先链扫描统一改到该 helper,避免同一 business `documentId` 的重复物理记录在父链遍历时再次漂移。同时已把 `convex/mindmaps.ts` 的 owner 校验从 `by_document_id.first()` 切到 `requireCanonicalOwnedDocument`,并将 `convex/mediaAssets.ts#createWithStorage` 的页面归属校验改为 `getCanonicalDocumentByBusinessId`,以覆盖页面内容附件上传这一仍会直接命中旧记录的高风险入口。本批最小验证目标为:补充 canonical helper 单测、对上述目标文件跑 `eslint`,并再次用源码扫描确认 `comments/documents/mindmaps/mediaAssets` 内不再残留直接依赖 `documents.by_document_id` 的实现。
2026-04-14 第四批真实收口:按本轮 C1 要求只处理 `convex/documentStars.ts` 剩余两个父链祖先扫描点,已将 `resolveSharePermission``resolveGroupSharePermission` 中手写的 `documents.by_document_id` 父级推进统一改为 `getCanonicalParentDocumentId`。同时已补充 `src/lib/documents/document-record.test.ts` 的 helper 回归用例,验证重复 business `documentId` 下即使旧记录已删除,父链 helper 仍稳定返回 canonical 父页面 id;并已补跑目标文件 `eslint``vitest`。本轮源码扫描确认 `wolai-frontend/convex/` 业务路径中已不再残留直接 `withIndex(\"by_document_id\")` 命中点,当前仅剩 `convex/_utils/documentRecord.ts` 作为底层 canonical helper 持有该查询。
C1 验收:
- [x] 页面元信息至少已有 3 条低风险链路切入 Rust 层。
- [x] 文档页外观与交互未退化。
- [x] trace 字段已可回查到页面级写入:`documents.title.update``documents.stats.update``documents.options.update` 的真实 HTTP 写入均已返回 `requestId/traceId`,且后续 `/api/bridge/request``/api/bridge/trace` 已复核可查到对应 `command_logs/domain_events`
2026-04-14 C1 浏览器回归补记:已新增 `/mnt/Data1T/mnote/scripts/task019-document-ui-regression.js` 并在真实本地实例 `http://127.0.0.1:3001` 上执行。脚本会创建临时页面,确认 Sidebar 主导航与“私有 / 我的页面”分区可见、文档页标题输入框与 BlockNote 编辑区正常渲染,然后实际修改标题与正文,等待 `/api/documents/title``/api/documents/save` 成功返回并出现“已保存”,最后刷新页面确认标题与正文仍保留,再调用 `/api/documents/purge` 清理临时页面。实跑结果通过,说明本轮协议层切换后文档页主交互未退化。
### C2 Sidebar 聚合
- [x] 盘点 Sidebar 真实数据来源,锁定 `sidebar.tsx``private-tree.tsx``file-tree.tsx``use-convex-sidebar-data.ts``sidebar-tree.ts`
- [x] 先把 Sidebar 的查询聚合逻辑抽象成 Rust query 目标,避免继续在前端和 API 中重复拼树。
- [x] 为 Sidebar 建立最小查询契约,至少覆盖页面列表、层级关系、文件树行模型和展开状态所需字段。
- [x] 保留现有 Sidebar UI 和交互,不引入第二套导航壳,也不从 `mnote-rust` 复制 `sidebar.tsx` 覆盖现实现。
- [x] 把 Mindmap、表格、媒体等派生资源的聚合边界整理成后续 Rust query 可接入的明确目标。
2026-04-14 盘点补记:当前主入口已确认位于 `wolai-frontend/src/components/sidebar/sidebar.tsx`,私有树/文件树分别位于 `components/sidebar/private-tree.tsx``components/sidebar/file-tree.tsx`;数据侧同时存在 `/api/sidebar` 聚合 route、`hooks/use-convex-sidebar-data.ts` 实时订阅组装,以及 `lib/sidebar-tree.ts` 的树/section 构建逻辑,说明 C2 的真实起点是“API 聚合 + 前端二次拼装”的双层结构。
2026-04-14 契约补记:当前最小 Rust query 目标已明确为兼容 `SidebarInitialData` 的单查询返回,至少覆盖 `active_workspace_id``workspaces``documents``trashed_documents``media_assets``trashed_media_assets``mindmap_assets``trashed_mindmap_assets``table_assets``trashed_table_assets``mindmap_docs``mindmap_asset_children`,从而先替换聚合来源而不改 `buildDocumentTree/buildVisibleRows` 的前端渲染逻辑。
2026-04-14 边界补记:当前已明确必须保持不变的 UI/交互包括 `starred/public/shared/private/templates` 分区语义、`sort_order -> created_at` 排序口径、`doc/index.md/asset-folder/asset` 文件树行语义、拖拽/剪贴板协议,以及回收站双 tab 与资源计数行为;Mindmap、表格、媒体三类派生资源的聚合边界已被明确列为 Rust query 后续接入目标。
2026-04-14 C2 第一刀收口补记:已新增 `wolai-frontend/src/lib/sidebar-data.ts``wolai-frontend/src/lib/server/sidebar-data.ts`,把 `src/app/(app)/layout.tsx``src/app/api/sidebar/route.ts``src/hooks/use-convex-sidebar-data.ts` 之间重复的 `SidebarInitialData` 组装统一收口到共享 helper;同时修复 hook 链路中 `mindmapAssetChildren` 长期为空的漂移,补充 `src/lib/sidebar-data.test.ts`,并通过 `pnpm test src/lib/sidebar-data.test.ts src/lib/file-tree/rows.test.ts` 与定向 `eslint` 验证。基于本轮复核,当前已可确认“多个前端位置不再重复拼装同一棵树”这一验收点达成;但由于 SidebarInitialData 仍未冻结为明确 Rust query contract`C2` 其余验收项暂不提前勾选。
2026-04-14 C2 第二刀补记:已新增 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/sidebar-rust-query-target.md`,把 `sidebar.dataset.list` 的最小 payload/result、Rust query 与前端投影职责边界、以及“不复制第二套导航壳/不直接输出前端渲染树”的约束固定成文档。当前可确认 Sidebar 查询聚合目标已经从“散落在 route 和 hook 中的实现细节”收口为明确的 Rust query 接入目标。
C2 验收:
- [x] Sidebar 至少一条主查询已经改由 Rust query 供给。
- [x] 文档树、文件树、回收站等视图语义保持一致。
- [x] 不再需要在多个前端位置重复拼装同一棵树。
2026-04-14 C2 第三刀补记:已新增 `wolai-frontend/convex/sidebar.ts`,把页面列表、回收站、Mindmap、媒体、表格等数据集收口成单个 `sidebar.datasetList` 主查询;`src/lib/server/sidebar-data.ts``src/hooks/use-convex-sidebar-data.ts` 均已切换为消费这条单查询返回,再通过既有 `SidebarInitialData` 投影层驱动 UI。配合 `src/lib/sidebar-data.test.ts` 的契约冻结、`src/lib/file-tree/rows.test.ts` 的文件树行语义验证,以及定向 `eslint`,当前可确认文档树、文件树、回收站双 tab 依旧沿用原有前端投影逻辑,没有因数据源切换而改变语义。
2026-04-15 C2 第四刀补记:`/api/sidebar` 现已从“先直跑 Convex helper,再额外挂 meta”推进为真实 Rust query transport。当前 route 会先执行 `buildDocumentQueryEnvelope(name=\"sidebar.dataset.list\") -> resolveRustBridgeQueryPlan -> executeRustBridgeQueryTransport`,再把 `sidebar:datasetList` 的结果投影回 `SidebarInitialData`;这意味着 Sidebar API 主链已经真正进入 Rust runtime/bridge,而不再只是挂一个 envelope 名字。
### C3 BlockNote 正文保存
- [x] 盘点正文保存链路,锁定 `blocknote-editor.tsx``schema.ts`、文档保存 API 当前入口。
- [x] 先把正文保存切成“前端快照采集”和“Rust command 提交”两个边界。
- [x] 为正文保存定义最小协议对象,至少包含 `page_id``workspace_id``revision``actor``source`、block 快照、冲突检测字段。
- [x] 先接入低风险保存模式,例如显式保存或节流保存,不先处理复杂协同细节。
- [x] 确保正文链与页面元信息链、Sidebar 链共用同一 identity 口径,不再出现多套 page id 映射。
C3 验收:
- [x] 正文保存至少一条正式链路已经通过 Rust command 回写 Convex。
- [x] 保存失败时可返回稳定错误模型。
- [x] 保存成功时可回查 command log、domain event 与 trace。
2026-04-14 C3 第一刀收口:已为 `documents.getContent` / `documents.save` 增加独立 `content_revision``content_conflict_key`,避免标题/统计信息更新污染正文 revision;`/api/documents/content` 现会透传 `revision/conflictDetectionKey``BlockNoteEditor` 会在自动保存时携带 `revision``conflictDetectionKey``snapshotCapturedAt``blockCount`,并在成功后刷新本地保存元数据。`save-command-adapter` 现把 Convex 冲突归一为 `409 REJECTED` bridge 错误,编辑器右上角也会展示保存失败信息。当前最小验证已补 `save-contract`/`bridge` 单测,并完成定向 eslint;结果为无 error,仅保留仓库既有 warnings。
2026-04-14 C3 第二刀补记:源码复核确认 `BlockNoteEditor` 当前通过 `useDebouncedCallback(saveContent, 800)` 以防抖自动保存作为最小低风险保存模式,没有把复杂协同状态直接并入正文写链;同时 `documents.meta/title/stats/options/save` 这些 route 都把同一个 `normalizedDocumentId` 作为 `target.pageId``documents.content/save` payload 也统一使用 `documentId/workspaceId``sidebar.dataset.list` 则以同一 `workspace_id` 作用域返回 `documents[].id` 作为页面业务 id,因此当前正文链、页面元信息链与 Sidebar 聚合链已不存在第二套 page id 映射口径。
2026-04-14 C3 第三刀补记:在此前真实 `/api/documents/save -> /api/bridge/request|trace` smoke 已确认可回查的基础上,本轮又补了 `src/lib/documents/bridge.test.ts` 的成功路径断言,明确 `executeSaveBridgeCommand` 在 revision/conflictDetectionKey 新协议下仍会调用 `recordBridgeCommandArtifacts`,并把同一 `requestId/traceId/commandId` 暴露给回查入口。因此当前“保存成功时可回查 command log、domain event 与 trace”已具备历史 HTTP smoke 与当前单测的双重闭环。
2026-04-14 C3 第四刀补记:`save-command-adapter` 已进一步切到与页面元信息链一致的最小运行时接缝。当前 `documents.save` 会先构造 `functionName=documents:updateContent``payloadJson``args` 组成的 bridge mutation request,再由统一执行器实际调用 Convex;这让正文保存不再长期停留在“adapter 内直接手写 `client.mutation(...)`”的临时态,同时保留现有冲突归一和落账逻辑不变。
2026-04-15 Phase 5 第一刀补记:`search.documents``search.recent` 已补齐到主仓 Rust query/runtime 主链。当前 `rust/crates/core-protocol` 新增 `SearchDocuments/SearchRecent``storage-convex-bridge``bridge-runtime` 已支持 query name -> Convex function 映射和运行时分发;`rust/crates/index-fts` 新增 `evaluate_search_documents`,负责标题/正文/思维导图/表格/附件的召回合并、排序、高亮 snippet 与 OCR 待补队列决策。`wolai-frontend/src/app/api/search/documents/route.ts` 现仅保留参数整理、原始数据装载与 HTTP 回传,不再持有 TS 侧的核心 ranking/snippet 逻辑;`/api/search/recent` 继续只承担最近访问写入 side-effect。
## 6. Phase DMindmap 与 OnlyOffice 边界接入
完成目标:
- 保留 `mnote` 当前成熟的 Mindmap 与 OnlyOffice 前端实现。
- 把数据边界、对象边界和回调边界逐步纳入 Rust adapter / protocol 规划,而不是复制第二套对象页壳。
Checklist
- [x] 锁定 Mindmap 当前主组件和嵌入链路,确认 `MindmapBlock.tsx` 同时服务 BlockNote 内嵌块与独立全屏页。
- [x] 盘点 Mindmap 数据入口,锁定 `mindmapLocalStore.ts``mindmapOps.ts``app/api/mindmap/**``app/mindmap/**`
- [x] 把 Mindmap 的前端交互状态与本体数据/ops 边界拆开,后者逐步映射到 Rust 协议层。
- [x] 明确 `mnote-rust/app/documents/[id]/mindmap/page.tsx` 等对象页壳不进入主仓主线。
- [x] 为 Mindmap 设计最小 adapter 目标,至少覆盖节点树、节点引用、节点操作日志和与 page/block 的绑定关系。
- [x] 锁定 OnlyOffice 当前页面和 API 边界,确认 `src/app/onlyoffice/``src/app/api/onlyoffice/**``src/components/onlyoffice/` 的职责分工。
- [x] 保留根目录 `src/components/onlyoffice/` 的静态资源、插件和数据目录,不做误删、不做迁移式替换。
- [x] 把 OnlyOffice 的对象解析、签名、callback、forcesave、代理请求边界整理成 Rust adapter 的明确目标。
- [x] 继续保持“正文只嵌文件入口,OnlyOffice 在独立页面运行”的产品形态。
- [x] 统一 Mindmap 与 OnlyOffice 的页面标识、附件标识、页面归属和追踪字段。
2026-04-14 Phase D 盘点补记:已新增 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/mindmap-onlyoffice-boundary.md`,固定 `MindmapBlock.tsx` 与独立全屏页共用同一核心组件、`mindmapLocalStore.ts`/`mindmapOps.ts`/`app/api/mindmap/**` 的数据与 ops 边界,以及 OnlyOffice 的 `page -> client -> sign/proxy/callback/forcesave` 职责分层。文档同时明确 `MediaBlock` 仅作为 Office 附件入口、`/onlyoffice` 继续作为独立编辑页面,且历史仓中的对象页壳与第二套 sidebar 只保留为参考实现。
2026-04-14 Phase D 收口补记:已把 Mindmap API 与组件侧统一到与 OnlyOffice 对齐的业务标识口径。当前 Mindmap 统一使用 `pageId=documentId``attachmentId=mindmapId``workspaceId` 作为页面归属,并在 `/api/mindmap/**` 返回稳定的 `requestId/traceId` 元信息;OnlyOffice 继续使用 `documentId + assetId` 作为页面/附件标识,因此两条链后续接 Rust adapter 时不再需要第二套 page/attachment 映射。
2026-04-14 Phase D 浏览器回归补记:已新增 `/mnt/Data1T/mnote/scripts/task021-mindmap-ui-regression.js` 并在真实本地实例 `http://127.0.0.1:3001` 上执行。脚本使用当前共享的 `MindmapBlockView` 全屏入口 `/mindmap/[docId]/[mindmapId]`,验证页面 `data-page-id/data-document-id/data-attachment-id/data-mindmap-id/data-workspace-id` 元信息、工具栏与“大纲”侧栏渲染、子节点新增与删除后的持久化、以及 `/api/mindmap/**` 返回的 `requestId/traceId` 会同步回 DOM。考虑到内嵌块与独立页共用同一核心组件,这轮浏览器回归可视为覆盖当前共享交互/保存内核,实跑未见退化。
2026-04-15 Phase D OnlyOffice callback 收口补记:已为 `OnlyOffice callback` 新增最小命令 `media.assets.replace_storage`,同步补齐 `rust/crates/core-protocol``ReplaceMediaAssetStorage``rust/crates/storage-convex-bridge` 的 command name -> Convex mutation 映射,以及主仓 `buildDocumentBridgeMutationRequest` 的运行时接缝。`src/app/api/onlyoffice/callback/route.ts` 现保留下载文件、上传到 Convex Files 获得 `storageId` 的现有流程,但最终写回已改为经 `buildDocumentBridgeContextWithActor -> buildDocumentCommandEnvelope -> executeMediaAssetWritebackBridgeCommand` 进入统一 bridge 边界,再调用 `api.mediaAssets.replaceStorageFromUpload`。配套 `src/app/api/onlyoffice/callback/route.test.ts``src/lib/documents/bridge.test.ts`、Rust 单测与 `scripts/task022-onlyoffice-ui-regression.js` 已通过,确认 `storage_id` 在真实浏览器里继续发生变化。
Phase D 验收:
- [x] Mindmap 与 OnlyOffice 的前端界面仍以 `mnote` 当前实现为主,没有引入第二套主壳。
- [x] Mindmap 的节点/操作边界与 OnlyOffice 的签名/callback/forcesave 边界都已进入统一规划。
- [x] `src/components/onlyoffice/` 保持可用且未被误删。
- [x] `adapter-onlyoffice``adapter-mindmap` 仍作为后置实现项,而不是本阶段强行复制进主线。
## 7. Phase E:历史收口与单仓执行统一
完成目标:
- `mnote-rust` 正式退出主产品角色。
- `mnote` 主仓成为唯一启动、保存、规划和验证入口。
Checklist
- [x] 统一团队口径:主产品仓只有 `/mnt/Data1T/mnote``/mnt/Data1T/mnote-rust` 只保留为历史参考和资产来源。
- [x] 把所有新的设计、执行清单、架构说明优先写入 `/mnt/Data1T/mnote/design/``/mnt/Data1T/mnote/rust/design/`
- [x] 复查仓库脚本和说明文档,去掉“先进 `mnote-rust` 再启动”的旧叙事。
- [x] 统一根目录脚本入口,让仓库级脚本只从 `/mnt/Data1T/mnote/scripts/` 发起,再按需调用 `cargo --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml ...`
- [x] 对仍需保留的 `mnote-rust` 资料按“核心规范 / 历史阶段 / 参考实现 / 错误方向”分类,避免后续误复制。
- [x] 把已确认不应主线保留的对象页壳、diagnostics 页、smoke 页继续留在历史仓,不进入主仓实现。
- [x] 对主仓内新增的 Rust 目录建立最小维护约定,明确谁负责 workspace、谁负责 bridge、谁负责接入链。
- [x] 重新审查 `ARCHITECTURE.md``AGENTS.md``rust-kernel-backport-plan.md` 与本 checklist,保证四者口径一致。
- [x] 固定“哪些目录默认不动”的硬边界,特别是 `wolai-frontend/``wolai-backend/``infra/convex/``src/components/onlyoffice/`
Phase E 验收:
- [x] 后续协作默认只进入 `mnote` 主仓规划和开发。
- [x] 主仓文档和脚本不再把 `mnote-rust` 叙述为主执行入口。
- [x] 历史仓的保留范围、参考价值和禁止误复制范围都已固定下来。
2026-04-14 Phase E 收口补记:已新增 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-single-repo-maintenance-boundary.md`,统一记录主仓执行入口、历史仓资料分类、禁止误复制清单与 workspace/bridge/接入链责任面;同时已复查根 `package.json``scripts/desktop-hot.js`,确认仓库级脚本仍只从 `/mnt/Data1T/mnote/scripts/` 发起,且 `ARCHITECTURE.md` 已移除把 `mnote-rust` 写成未来执行承载面的表述。
## 8. 发布前总审计(跨阶段)
- [x] 为 `/mnt/Data1T/mnote/rust/` 补基础验证矩阵,至少覆盖 `cargo metadata``cargo check`、必要 crate 单测。
- [x] 为 bridge 入口补集成 smoke,验证最小查询、最小写入、日志生成、事件生成和错误返回。
- [x] 为文档页元信息、Sidebar、BlockNote 保存链补端到端回归,确认 UI 未因为协议层切换而退化。
- [x] 为 Mindmap 补交互回归,重点验证内嵌块、独立页、数据保存、节点操作和工具栏/侧栏行为。
- [x] 为 OnlyOffice 补回归,重点验证签名、代理、callback、forcesave、文档打开和静态资源可用性。
- [x] 检查主仓启动路径,确保开发者只需进入 `/mnt/Data1T/mnote` 就能完成前端、后端、Rust、OnlyOffice 相关联调。
- [x] 检查保存口径,确认页面、块、文档、索引的最终真相仍然落在 Convex,而不是被临时缓存目录劫持。
- [x] 检查 `trace_id``request_id``workspace_id``page_id` 在前端、bridge、Convex、日志中的一致性。
- [x] 对本次回迁引入的所有新目录和新文档做一次清点,确认没有误带入 `mnote-rust` 的历史噪音。
- [x] 为统一观测面补正式 Rust 能力面,至少包含 `bridge_request_get``bridge_trace_get``bridge_command_get``event_replay``index_rebuild` 这组 query/job/tool,并确认 Web 回查入口不再私有直连。
2026-04-14 发布前审计补记:已新增 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-backport-audit-inventory.md`,清点主仓 `rust/` 已并入的 workspace/crates/设计文档、本轮新增的根 `design/` 文档,以及当前仍未并入主线的 `adapter-*``mnote-cli``bridge/scripts/fixtures/tests` 目录。文档同时核对了 `package.json``scripts/desktop-hot.js``AGENTS.md` 的主仓启动口径,确认开发者只需进入 `/mnt/Data1T/mnote` 即可找到前端、后端、Rust 与 OnlyOffice 的联调入口。
- 2026-04-15 task-034 补记:已在 `rust/crates/core-protocol` 增补 `GetBridgeRequest/GetBridgeTrace/GetBridgeCommand` 以及 `bridge_request_get``bridge_trace_get``bridge_command_get``event_replay``index_rebuild` 五个统一观测/恢复工具;`rust/crates/storage-convex-bridge` 已补 query name -> Convex `bridgeLogs:*` 映射;`rust/crates/bridge-runtime` 已支持这组 query/job 的 plan/result 输出,并把 `index-fts::rebuild_from_events` 暴露为正式 `index_rebuild` 恢复入口。前端 `/api/bridge/request``/api/bridge/trace` 已切到 `buildDocumentQueryEnvelope -> resolveRustBridgeQueryPlan -> executeRustBridgeQueryTransport` 主链,同时支持按 `commandId` 过滤与统一排序返回;`src/lib/documents/bridge-log.ts` 也已把命令日志/领域事件状态显式化,为后续失败态、冲突态与补偿落账提供稳定边界。配套验证已通过全量 `cargo test --manifest-path rust/Cargo.toml` 与 bridge 定向 eslint。
- 2026-04-15 OnlyOffice 升级与实跑补记:已把 `8082` 文档服务从旧仓 `mnote-rust/infra/onlyoffice/docker-compose.yml` 迁回当前主仓 `/mnt/Data1T/mnote/infra/onlyoffice/docker-compose.yml`,并升级到 `onlyoffice/documentserver:9.3.1`。同时修复了 `/onlyoffice/plugins/*` 被 middleware 重定向到 `/auth` 导致自定义插件桥无法 ready 的问题,为 `wolai-frontend/public/onlyoffice/plugins/agent-tools/` 补齐非可视插件配置与 `plugins.js` 加载。最终 `scripts/task022-onlyoffice-ui-regression.js` 已在真实浏览器中通过,覆盖文档打开、`oo_insert_text` 插件调用、`同步保存`、callback 写回以及 `storage_id` 刷新变化闭环。
- 2026-04-15 OnlyOffice callback bridge 补记:本轮在复跑 `scripts/task022-onlyoffice-ui-regression.js` 前,曾因 `/auth` 快速登录触发 `POST /api/auth -> Could not find public function for 'auth:signIn'` 导致浏览器脚本卡在登录页;已通过 `cd /mnt/Data1T/mnote/wolai-frontend && pnpm exec convex dev --once --tail-logs disable --env-file ../.env.all --run ping:ping` 重新注册 Convex functions 后恢复。恢复后同一浏览器脚本再次通过,确认新接入的 `media.assets.replace_storage` bridge 命令没有破坏 OnlyOffice 打开、插件桥、forcesave、callback 与 `storage_id` 更新闭环。
- 2026-04-14 浏览器实跑补记:`task-019-document-ui-regression.js` 已在真实浏览器中覆盖“首页进入文档页 -> Sidebar 可见 -> 修改标题 -> 修改 BlockNote 正文 -> 等待保存成功 -> 刷新后仍保留”的最小闭环。运行前曾因本地 Convex 实例缺少 `workspaces:ensureDefaultWorkspace` 导致首页 `500`,本轮已通过 `pnpm exec convex dev --once --tail-logs disable --env-file ../.env.all --run ping:ping` 重新注册函数后恢复;最终浏览器脚本通过,且临时页面已清理。
- [x] 对以下禁止事项做最终复查:不双仓并行、不跨仓链接、不复制第二套产品前端壳、不把对象页壳/diagnostics/smoke 页带入主线、不散落多个 Rust 源码根。
- [x] 产出最终 cutover 文档,按能力域列清哪些 route 已仅剩 transport,哪些旧 TS 执行面必须继续迁移或后续删除。
- [ ] 以“单仓路径可启动、可保存、可验证、可继续演进”作为发布前收口标准。
2026-04-14 实跑记录:已重新执行 `cargo metadata --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml --format-version 1``cargo check --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml``cargo test --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p core-domain``cargo test --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p core-protocol``cargo test --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p storage-convex-bridge`,均通过;其中 `cargo metadata` 已再次确认 workspace members 固定为 `core-domain``core-protocol``event-log``storage-convex-bridge``index-fts` 五个 crate。
2026-04-14 源码审计补记:已新增 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/release-readiness-source-audit.md`。该文档基于当前 route、adapter、Convex bridgeLogs 与定向测试,确认 bridge 入口已经覆盖最小查询/写入/日志/事件/错误返回闭环;确认正文、页面元信息、块补丁与 OnlyOffice 附件写回的最终真相仍然落在 Convex mutation;确认 `trace_id``request_id``workspace_id``page_id` 在 route、bridge、Convex 日志与回查入口之间使用统一口径;并完成“不双仓并行、不跨仓链接、不复制第二套前端壳、不把 diagnostics/smoke 页带入主线、不散落多个 Rust 源码根”的源码级复查。
2026-04-15 task-035 补记:已新增 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md`,把主仓当前 TS 执行面按 `RUST_OWNER / TS_TRANSPORT_KEEP / TS_COMPAT_PENDING / TS_LEGACY_DELETE` 四类状态完成盘点。文档明确了页面、块、查询聚合、AI、Mindmap、OnlyOffice 的当前归属,也明确了 `documents/create-child``documents/embed``documents/empty-trash``documents/purge``documents/template``mindmap-trash/empty``mindmap-ai/**``ai-agent` 非 Rust 核心工具面属于 Phase 8 前必须继续处理的旧面;同时为“何时允许宣布 Rust 成为唯一业务执行平面”补齐了四组 gate。
2026-04-15 task-040/task-042/task-043/task-044 补记:AI 非 `doc_*` 核心工具中的 `search_web``image_read``slash_run` 已完成第一批 Rust Tool runtime cutover`ai-agent/run` 不再保留这些工具的 TS 真执行兜底;`bridge-log/runtime` 已补齐统一失败态、冲突态与补偿态的第一批状态闭环,并覆盖页面主写链与 AI/Mindmap 写链;同时已新增 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-legacy-delete-list.md`,把 `documents/create-child``documents/embed``documents/empty-trash``documents/purge``documents/template``mindmap-trash/empty``builtins/**` 固定为第一批 `TS_LEGACY_DELETE`。至此 Phase 8 的文档口径已经允许在源码审计层面使用“Rust 已成为 mnote 的唯一业务执行平面;Web route 只保留 transport、auth、session、streaming、proxy 与 callback 壳”这句统一结论。
@@ -0,0 +1,656 @@
# [recycle] mnote 单仓收口 Rust 内核回迁计划
> 更新时间:2026-04-14
>
> 当前状态:已完成第一批 P0 crate 与核心设计文档的真实复制,复制目标目录为 `/mnt/Data1T/mnote/rust/`
## 1. 结论
基于当前代码现状、`/mnt/Data1T/mnote-rust/design/` 的长期原则,以及你现在明确提出的要求:
- 不希望长期维护两个仓库
- 不希望跨仓链接
- 最终只希望在一个文件夹里启动和保存
当前最合适的路线已经明确并开始落地:
1. **停止把 `/mnt/Data1T/mnote-rust/` 继续当作未来主产品仓推进**
2. **把 `mnote-rust` 的必要 Rust 内核内容真实复制进 `/mnt/Data1T/mnote/`**
3. **最终只保留 `/mnt/Data1T/mnote/` 作为唯一主仓**
4. **`/mnt/Data1T/mnote/wolai-frontend/` 继续承担主产品前端壳**
5. **Rust 内核在 `/mnt/Data1T/mnote/rust/` 下统一收口**
一句话版:
> **不是双仓回接,而是单仓收口:把必要 Rust 内核复制进 `mnote`,最终只在 `/mnt/Data1T/mnote` 启动与保存。**
---
## 2. 为什么要改成单仓复制,而不是继续双仓
## 2.1 先做 Rust 内核本身没有错
`/mnt/Data1T/mnote-rust/design/blueprint/ai-native-note-architecture-blueprint-v0.md``/mnt/Data1T/mnote-rust/design/phases/phase5/packaging-and-shell-strategy-v0.md` 已明确长期原则:
1. Convex 仍是主事实层
2. Rust 负责统一协议、命令、查询、事件、索引、adapter
3. 前端主壳继续复用成熟 React/Next
也就是说:
- “先写 Rust 内核”是对的
- “不重写一套新前端主栈”也是对的
真正要调整的是载体:
- 不再把 `mnote-rust` 作为第二个长期主仓
- 不再维持“一个仓写内核,一个仓跑产品前端”的长期分裂状态
## 2.2 双仓会持续制造启动、保存、认知和迁移成本
如果继续保留:
- `/mnt/Data1T/mnote/`
- `/mnt/Data1T/mnote-rust/`
长期并行,会持续出现四类成本:
1. **启动成本**
- 需要判断到底从哪个目录启动
- 脚本、环境变量、依赖路径容易双份化
2. **保存口径成本**
- 用户会天然希望“最终只有一个真实主仓”
- 双仓天然让“哪个仓是现在的真主线”不断摇摆
3. **迁移成本**
- 每做一项功能,都要先判断该落在 `mnote` 还是 `mnote-rust`
- 这会导致团队持续在仓库边界上损耗
4. **认知成本**
- 新协作者很难快速判断:
- 哪个仓是产品主线
- 哪个仓只是实验壳
- 哪些文档是真约束
## 2.3 当前真正成熟的是 `mnote` 前端,而不是 `mnote-rust` 前端
当前成熟可直接承载产品面的能力主要在:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/onlyoffice/`
`mnote-rust` 当前前端更多是:
1. 联调壳
2. smoke 壳
3. 协议验证壳
4. 部分对象页桥接壳
因此,单仓收口时,正确的继承关系应该是:
- **保留 `mnote` 前端主壳**
- **复制 `mnote-rust` 的内核与协议成果**
而不是反过来复制一整套 `mnote-rust` 的 Next 前端壳。
---
## 3. 新目标架构
单仓收口后的目标结构应是:
```text
/mnt/Data1T/mnote/
wolai-frontend/ # 唯一主前端
wolai-backend/ # 现有辅助后端
infra/convex/ # 现有 Convex 基础设施
src/components/onlyoffice/
scripts/ # 仓库级统一启动入口
design/ # 全局设计与总览
rust/ # 新增:Rust 内核统一收口目录
```
连接关系应变成:
```text
mnote 前端
-> mnote 内部 API / BFF / bridge
-> rust/ 内核协议层
-> Convex 主事实层 + Rust 事件/索引层
```
这意味着:
1. 前端只认 `/mnt/Data1T/mnote`
2. Rust 也只存在于 `/mnt/Data1T/mnote/rust`
3. 启动、构建、联调都只从 `/mnt/Data1T/mnote` 发起
---
## 4. 单仓目录落位方案
推荐把 Rust 相关内容整体收进:
- `/mnt/Data1T/mnote/rust/`
而不要把 `crates/``design/``scripts/` 直接摊到仓库根。
目标结构:
```text
/mnt/Data1T/mnote/
wolai-frontend/
wolai-backend/
infra/convex/
src/components/onlyoffice/
scripts/
design/
rust/
Cargo.toml
Cargo.lock
crates/
core-domain/
core-protocol/
event-log/
storage-convex-bridge/
index-fts/
bridge-runtime/ # Phase 1 最小真实执行器
mnote-cli/ # Phase 2 已落位,先冻结 --json 协议
adapter-onlyoffice/ # 后置按需引入
adapter-mindmap/ # 后置按需引入
adapter-legacy-mnote/ # 后置按需引入
bridge/
design/
INDEX.md
core/
blueprint/
phases/
scripts/
fixtures/
tests/
```
### 4.1 `rust/` 下各目录职责
- `rust/crates/`
放所有 Rust workspace 成员,保持 Cargo workspace 语义集中。
- `rust/bridge/`
放 Rust 侧桥接层、协议适配、生成代码、FFI/IPC glue。
- `rust/design/`
放 Rust 专属设计、阶段文档、路线图。
- `rust/scripts/`
放 Rust 专属构建、测试、检查、保存脚本。
- `rust/fixtures/` / `rust/tests/`
放 Rust 侧测试资源与测试代码。
### 4.2 根目录哪些保持不动
以下现有目录建议明确保持不动:
- `/mnt/Data1T/mnote/wolai-frontend/`
- `/mnt/Data1T/mnote/wolai-backend/`
- `/mnt/Data1T/mnote/infra/convex/`
- `/mnt/Data1T/mnote/src/components/onlyoffice/`
- `/mnt/Data1T/mnote/recycle/`
- `/mnt/Data1T/mnote/scripts/`
- `/mnt/Data1T/mnote/design/`
其中:
- `design/` 继续承担全局总览和跨系统路线说明
- Rust 的详细阶段文档放进 `rust/design/`
---
## 5. 启动与保存口径必须怎么统一
## 5.1 启动口径
统一口径只有一个:
> 所有开发、构建、联调命令都从 `/mnt/Data1T/mnote` 发起。
执行方式:
1. 仓库级脚本统一保留在 `/mnt/Data1T/mnote/scripts/`
2. Rust 命令只作为子任务存在,例如:
- `cargo --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml ...`
3. 不再保留“先进 `mnote-rust` 再进 `mnote`”的双仓启动方式
## 5.2 保存口径
统一口径只有一个:
> 页面、块、文档、任务、索引的最终保存都只认 Convex 主事实层。
这意味着:
1. 人工操作、CLI、Agent 的写入都应先进入 Rust 统一命令层,再落到 Convex
2. Rust 的 `event-log``index-fts`、fixtures、本地缓存都只是派生层
3. 文件系统只保存:
- 静态资源
- 附件
- 导出物
- 缓存
- 测试数据
4. 文件系统不承载主业务真相
---
## 5.3 当前已落地的首批复制结果
截至 2026-04-13,以下内容已经真实复制进 `/mnt/Data1T/mnote/rust/`
### 已复制的 workspace 根文件
- `/mnt/Data1T/mnote/rust/Cargo.toml`
- `/mnt/Data1T/mnote/rust/Cargo.lock`
### 已复制的 P0 crate
- `/mnt/Data1T/mnote/rust/crates/core-domain/`
- `/mnt/Data1T/mnote/rust/crates/core-protocol/`
- `/mnt/Data1T/mnote/rust/crates/event-log/`
- `/mnt/Data1T/mnote/rust/crates/storage-convex-bridge/`
- `/mnt/Data1T/mnote/rust/crates/index-fts/`
### 已复制的核心设计文档
- `/mnt/Data1T/mnote/rust/design/INDEX.md`
- `/mnt/Data1T/mnote/rust/design/core/01-domain-model-v0.md`
- `/mnt/Data1T/mnote/rust/design/core/02-command-query-tool-protocol-v0.md`
- `/mnt/Data1T/mnote/rust/design/core/03-storage-event-indexing-v0.md`
- `/mnt/Data1T/mnote/rust/design/core/04-onlyoffice-integration-boundary-v0.md`
这一状态意味着:
1. `mnote` 主仓里已经存在可继续扩展的 Rust workspace 根
2. P0 内核定义已经不再只存在于 `/mnt/Data1T/mnote-rust/`
3. 后续回迁工作可以直接以 `/mnt/Data1T/mnote/rust/` 为唯一落位继续推进
---
## 6. 第一阶段应该真实复制进 `mnote` 的内容
子 agent 的结论一致:第一阶段应复制“定义真相和协议”的内核,而不是复制第二套产品前端。
## 6.1 必须复制的 crate
### P0:第一阶段必须复制
以下 crate 应真实复制到:
- `/mnt/Data1T/mnote/rust/crates/`
#### `core-domain`
来源:
- `/mnt/Data1T/mnote-rust/crates/core-domain`
复制后职责:
1. 领域模型
2. 一等对象定义
3. ID / revision / 时间 /审计基础类型
#### `core-protocol`
来源:
- `/mnt/Data1T/mnote-rust/crates/core-protocol`
复制后职责:
1. `Command / Query / Tool` 统一协议壳
2. actor/source/target/meta 结构
3. `CreatePage / InsertBlock / SearchPages` 等协议模型
#### `event-log`
来源:
- `/mnt/Data1T/mnote-rust/crates/event-log`
复制后职责:
1. 命令日志
2. 领域事件
3. 写入后的事件生成规则
#### `storage-convex-bridge`
来源:
- `/mnt/Data1T/mnote-rust/crates/storage-convex-bridge`
复制后职责:
1. 协议到 Convex 的桥接
2. 命令执行与回写
3. 命令日志和事件落账
#### `index-fts`
来源:
- `/mnt/Data1T/mnote-rust/crates/index-fts`
复制后职责:
1. 可重建的索引与搜索层
2. 事件投影
3. page/block 搜索
### P1:建议第二阶段复制
#### `mnote-cli`
来源:
- `/mnt/Data1T/mnote-rust/crates/mnote-cli`
复制后职责:
1. 批处理入口
2. 导入导出入口
3. 内核 smoke 与排障入口
### P2:按需后置复制
#### `adapter-onlyoffice`
来源:
- `/mnt/Data1T/mnote-rust/crates/adapter-onlyoffice`
复制后职责:
1. OnlyOffice 资产定位
2. 会话定位
3. 任务边界适配
#### `adapter-mindmap`
来源:
- `/mnt/Data1T/mnote-rust/crates/adapter-mindmap`
复制后职责:
1. 思维导图结构化 ops
2. 节点读取与引用边界
#### `adapter-legacy-mnote`
来源:
- `/mnt/Data1T/mnote-rust/crates/adapter-legacy-mnote`
复制后职责:
1. 旧逻辑兼容
2. 迁移期桥接
## 6.2 必须复制的设计资产
建议同步复制到:
- `/mnt/Data1T/mnote/rust/design/`
### 必复制
- `/mnt/Data1T/mnote-rust/design/INDEX.md`
- `/mnt/Data1T/mnote-rust/design/core/01-domain-model-v0.md`
- `/mnt/Data1T/mnote-rust/design/core/02-command-query-tool-protocol-v0.md`
- `/mnt/Data1T/mnote-rust/design/core/03-storage-event-indexing-v0.md`
- `/mnt/Data1T/mnote-rust/design/core/04-onlyoffice-integration-boundary-v0.md`
原因:
1. 这些是长期稳定规范
2. 直接对应 `core-domain / core-protocol / event-log / storage-convex-bridge / index-fts / adapter-onlyoffice`
### 暂不建议第一阶段复制的设计资产
以下内容先留在 `mnote-rust` 作为参考,不作为第一阶段主迁移目标:
- `design/blueprint/**`
- `design/phases/**`
- `design/execution/**`
- `design/UI/**`
原因:
1. 这些更多是阶段叙事与历史推进资料
2. 不是必须进入主仓的长期规范
---
## 7. 第一阶段明确不应复制的内容
这里要明确“禁止误复制”的范围。
## 7.1 不应继续作为第一阶段复制目标的前端内容
### 不复制为主线
- `/mnt/Data1T/mnote-rust/app/documents/[id]/mindmap/page.tsx`
- `/mnt/Data1T/mnote-rust/app/documents/[id]/office/page.tsx`
- `/mnt/Data1T/mnote-rust/components/sidebar/sidebar.tsx`
- `/mnt/Data1T/mnote-rust/app/page.tsx`
- `/mnt/Data1T/mnote-rust/app/onlyoffice/page.tsx`
原因:
1. 它们大多属于对象页壳、诊断页、smoke 页或单文件硬拼壳
2. 不应再形成第二套产品前端主线
## 7.2 只能保留为参考 / smoke / 实验的内容
以下内容可以保留在 `mnote-rust` 参考,但不作为第一阶段复制目标:
1. `app/documents/[id]/mindmap/page.tsx` 里的 diagnostics 区
2. `app/documents/[id]/office/page.tsx` 里的 diagnostics 区
3. `app/onlyoffice/OnlyOfficeClientPage.tsx` 里的环境兼容 hack
4. `app/page.tsx` 的 smoke 首页文案
5. `phase7` 中已经标注为历史错误方向的文档
## 7.3 可复制的少量薄桥接代码
以下内容可以作为后续参考性复制对象,但仍不是第一阶段主目标:
- `components/editor/document-shell.tsx`
- `components/editor/document-content.tsx`
- `components/editor/blocknote-editor.tsx`
- `hooks/use-convex-sidebar-data.ts`
- `lib/sidebar-tree.ts`
- `lib/file-tree/rows.ts`
- `lib/document-embeds.ts`
- `app/layout.tsx`
原因:
1. 这些属于宿主桥接、投影层或 provider 接线
2. 不是完整产品前端壳
---
## 8. 单仓路线下,`mnote` 里最先该接入的 5 条能力链
这 5 条仍然是最值得优先打通的主线。
## 8.1 文档正文命令链
接入目标:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/save/route.ts`
目标:
1. 保留现有成熟 BlockNote 前端
2. 把正文保存逐步切到 Rust command 层
3. 让 block 操作、正文快照、stats 逐步进入统一协议
## 8.2 页面元信息与页面属性链
接入目标:
- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
目标:
1. 标题更新
2. 页面选项
3. 文档统计
4. 页面属性变更
## 8.3 Sidebar 数据聚合链
接入目标:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/private-tree.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/file-tree.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/hooks/use-convex-sidebar-data.ts`
目标:
1. 保留现有 UI
2. 逐步让数据聚合层走 Rust query / index 层
## 8.4 Mindmap 数据链
接入目标:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/mindmap/mindmapLocalStore.ts`
目标:
1. 保留现有成熟思维导图前端
2. 逐步把数据读写、ops 和边界纳入 Rust adapter / protocol
## 8.5 OnlyOffice 边界链
接入目标:
- `/mnt/Data1T/mnote/wolai-frontend/src/app/onlyoffice/`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/onlyoffice/`
- `/mnt/Data1T/mnote/src/components/onlyoffice/`
目标:
1. 保留现有 OnlyOffice 页面和静态资源
2. 把对象解析、回调、签名和桥接逐步回到 Rust adapter 统一边界
当前补记:
1. `OnlyOffice callback` 已从 route 里直接调用 `api.mediaAssets.replaceStorageFromUpload`,推进到最小 `media.assets.replace_storage` bridge 命令边界。
2. 当前主仓实现会先在 `wolai-frontend/src/app/api/onlyoffice/callback/route.ts` 下载 ONLYOFFICE 输出文件、上传到 Convex Files 获得 `storageId`,再通过 `buildDocumentBridgeContextWithActor``buildDocumentCommandEnvelope``executeMediaAssetWritebackBridgeCommand` 构造和执行与 `storage-convex-bridge` 对齐的运行时 request。
3. Rust 侧已补 `core-protocol::ReplaceMediaAssetStorage``storage-convex-bridge``media.assets.replace_storage -> mediaAssets:replaceStorageFromUpload` 映射;因此 OnlyOffice 附件写回现已进入与页面元信息、正文保存一致的协议接缝,只是最终执行器仍在 TypeScript 侧落到 Convex mutation。
---
## 9. 推荐实施顺序
## Phase A:先把 Rust 内核真实复制进 `mnote`
第一阶段执行动作:
1. 在 `/mnt/Data1T/mnote/` 下建立 `/rust/`
2. 复制 `P0` crate
3. 复制 `design/core` 四份长期文档和 `design/INDEX.md`
4. 建立新的 `rust/Cargo.toml``rust/Cargo.lock`
5. 不动 `mnote` 主前端壳
6. 不复制 `mnote-rust` 的第二套产品前端壳
当前状态:
- 以上动作已经完成
- `bridge/` 已初始化最小说明目录,但 `scripts/``fixtures/``tests/` 仍未初始化
- `mnote-cli` 已并入当前 workspace`adapter-*` 仍未并入
## Phase B:在 `mnote` 中建立 Rust bridge 接口
执行动作:
1. 在 `mnote` 现有 API/BFF 层加 Rust command/query 入口
2. 保留当前 UI,不先大改组件
3. 先打通低风险调用链
当前建议的第一批 bridge 入口落位:
1. `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/content/route.ts`
- 作为优先读入口,后续先接 `storage-convex-bridge``read_path`
2. `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/save/route.ts`
- 作为优先写入口,后续先接 `write_path`
3. `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/title/route.ts`
- 作为页面标题更新的低风险写入口
4. `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/stats/route.ts`
- 作为页面统计的低风险入口
5. `/mnt/Data1T/mnote/wolai-frontend/src/app/api/sidebar/route.ts`
- 作为 Sidebar 聚合读入口,后续切 Rust query / index
6. `/mnt/Data1T/mnote/wolai-frontend/src/app/api/blocks/patch/route.ts`
- 作为块级局部写入口,适合正文链路第二步接入
当前判断:
1. `storage-convex-bridge` crate 内已经具备 `context / validation / read_path / write_path / mapping` 的桥接骨架
2. `mnote` 主仓已为 `documents.title.update``documents.stats.update``documents.options.update``documents.save` 补上与 `storage-convex-bridge::build_write_request` 对齐的最小运行时接缝,会先生成 `functionName/payloadJson/args` 形式的 bridge mutation request,再交给统一执行器落到 Convex
3. 但主仓 API route 仍未直接调用 Rust crate`documents.content``blocks.patch` 等链路也还没有切到同一层执行器,因此 Phase B 还不能视为完全关闭
## Phase C:先接低风险元信息,再接正文链
顺序建议:
1. 页面标题 / 页面选项
2. Sidebar 数据聚合
3. BlockNote 正文保存
## Phase D:再接 Mindmap 与 OnlyOffice
原则:
1. 前端仍然在 `mnote`
2. 数据、ops、边界逐步切到 Rust adapter / protocol
## Phase E:让 `mnote-rust` 退出主产品角色
处理方式:
1. `mnote-rust` 可保留为历史参考或过渡仓
2. 但不再承担短期产品主线
3. 最终只保留 `/mnt/Data1T/mnote/` 作为可启动、可保存主仓
4. 历史资料分类与主仓维护边界统一以 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-single-repo-maintenance-boundary.md` 为准
---
## 10. 最不该做的事
1. 不要继续把 `mnote-rust` 的 Next 前端补成完整产品前端
2. 不要让 `mnote``mnote-rust` 长期各维护一套产品前端
3. 不要跨仓链接代码来“假装已迁移”
4. 不要把对象页壳、诊断页、smoke 页当作主产品实现复制进来
5. 不要把 Rust 内容散落复制到 `mnote` 根目录多个平行源码根
---
## 11. 一句话路线判断
最优路线不是:
> 继续在 `mnote-rust` 里补前端,再想办法替换 `mnote`
而是:
> 把 `mnote-rust` 的必要 Rust 内核真实复制进 `/mnt/Data1T/mnote/rust/`,保留 `mnote` 现有成熟前端作为唯一主产品壳,最终只在 `/mnt/Data1T/mnote` 这个单仓里启动和保存。
@@ -0,0 +1,388 @@
# [recycle] mnote Rust 内核替换剩余目标清单
> 更新时间:2026-04-15
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-plan.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-phase-checklist.md`
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
## 1. 本文目的
本文不再回答“Rust 内容是否已经回迁到主仓”,而是直接回答:
> **距离“用 Rust 内核替换原有内核,并让绝大部分功能 CLI 化、可供 AI 自主编辑”这一最终目标,我们现在还差什么。**
当前结论很明确:
- **单仓收口已经基本完成**
- **Rust workspace 与 P0 crate 已经落位**
- **部分前端 API 已经接入 Rust 风格协议与 bridge 接缝**
- **但“Rust 真正成为唯一执行内核”这件事还没有完成**
也就是说,当前更接近:
> **“前端/Node 先学会说 Rust 协议”**
而不是:
> **“产品已经由 Rust 内核统一执行”**
---
## 2. 当前已经完成的基础
截至目前,已经完成的只是替换前的基础设施准备:
- `/mnt/Data1T/mnote/rust/` 已成为主仓内唯一 Rust workspace 根。
- `core-domain``core-protocol``event-log``storage-convex-bridge``index-fts` 已落位。
- `rust/design/core/01~04` 核心设计文档已落位。
- 文档内容、标题、统计、Sidebar 等链路,已经开始使用统一 envelope、`request_id``trace_id``idempotency_key` 等 bridge 元信息。
- `command_logs``domain_events` 已有首批落账能力。
这些工作解决的是:
- 单仓问题
- 协议问题
- 目录问题
- 第一批接缝问题
**它们还没有解决“原内核是否已经被 Rust 取代”的问题。**
---
## 3. 还差的核心目标
## 3.1 还没有形成“Rust 唯一执行内核”
这是当前最大的缺口。
虽然已有一部分 route 在构造 Rust 风格 request,但真实执行主链仍然大量停留在 `Next.js route + TypeScript + Convex client`
当前仍明显属于旧执行面的代表链路包括:
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/create/route.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/delete/route.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/move/route.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/restore/route.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/duplicate/route.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/search/documents/route.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/mindmap/[docId]/route.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/onlyoffice/*.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/media/*.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/tables/*.ts`
这说明目前还没有做到:
- 所有核心读写先进入 Rust `Command / Query / Tool`
- 所有执行规则由 Rust 决定
- Node/Next 只做 transport、auth、session、streaming、UI 适配
最终目标需要变成:
- Web route 只负责接请求、鉴权、转发、回流结果
- Rust 负责真正的命令执行、查询聚合、约束校验、冲突处理、日志和事件生成
验收标准:
- 文档、块、Sidebar、搜索、页面树、Mindmap、OnlyOffice、媒体、表格等主链路都有 Rust 执行入口
- 前端 route 不再手写业务规则和数据聚合
- TypeScript 侧不再直接成为业务真内核
## 3.2 还没有完成“全域能力模型”的 Rust 化
当前 Rust workspace 只有 P0 通用内核 crate,缺的不是“再多几个通用 crate”,而是产品能力域本身还没被吸进 Rust。
还没有真正完成 Rust 化的能力域至少包括:
- 页面树与层级操作
- 页面创建、移动、复制、删除、恢复、清空回收站
- BlockNote block 级操作全量协议
- 搜索与召回
- 引用、反链、嵌入
- 媒体与附件
- 在线表格
- Mindmap
- OnlyOffice 会话、签名、回调、强制保存
- AI 调用的写入工具面
当前 `core-domain``core-protocol` 更像底层骨架,但还没长成完整产品内核。
验收标准:
- 每个产品域都有明确 Rust domain model、command、query、tool contract
- 前端不再自行定义第二套 payload 形状
- “页面系统”和“对象系统”不再由不同 TS route 各自发明规则
## 3.3 CLI 入口还没有建立起来
你的目标里有一条是关键约束:
> **绝大部分功能 CLI 化**
这件事当前还远未完成。
直接证据是:
- `mnote-cli` 已并入主仓 workspace,但当前还只是最小命令面与 `--json` 计划输出协议
- `/mnt/Data1T/mnote/rust/scripts/` 还未初始化
- `/mnt/Data1T/mnote/rust/bridge/` 已初始化最小说明目录,且 `crates/bridge-runtime` 已提供 Phase 1 最小真实执行样板
- `/mnt/Data1T/mnote/rust/tests/``/mnt/Data1T/mnote/rust/fixtures/` 还未形成 CLI 驱动的验收体系
这意味着目前仍然缺少:
- 真正可执行而不止输出计划的统一 CLI 二进制入口
- 可脚本化的命令集
- 稳定的 stdout/stderr/json 输出协议
- 面向 AI 的非交互调用模式
- dry-run / plan / apply / rollback 风格能力
最终至少应该具备的 CLI 面包括:
- `page create/get/update/move/delete/restore/list`
- `block insert/replace/move/delete/get`
- `search query`
- `sidebar dataset`
- `mindmap get/put/op`
- `onlyoffice sign/callback/forcesave/session`
- `media upload/list/get/delete`
- `table create/get/update`
- `tool run <tool-name> --json`
验收标准:
- 核心功能可以不经过浏览器完成
- 核心功能可以稳定返回 JSON
- shell、脚本、AI agent 都能直接调用
- CLI 和 Web 不再各维护一套业务实现
## 3.4 AI 还没有真正建立在 Rust 工具面之上
当前已经有 AI 入口:
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/ai-agent/run/route.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/DocumentAiAgentPanel.tsx`
但这套 AI 执行仍主要建立在前端/Node 工具注册表与页面侧 bridge 上,不是建立在 Rust 原生 tool protocol 上。
这会带来四个问题:
- AI 能调用的工具面和 Web 内部实现强耦合
- AI 与 CLI 不是同一执行平面
- AI 写入行为缺少统一事务语义
- AI 很难获得稳定、可审计、可回放的编辑能力
为了实现“AI 可自行编辑”,至少还差以下目标:
- Rust 提供稳定的 `Tool` 执行协议,而不是只提供 `Command/Query`
- AI 调用和 CLI 调用共享同一工具注册面
- 每个写入工具都支持明确的目标对象、权限校验、冲突返回、审计日志
- 支持 `validate_only``dry_run``explain_plan` 之类的安全模式
- 支持机器可消费的错误码,而不是前端文案式错误
最终目标不是“AI 像用户点按钮一样绕进前端”,而是:
> **AI 直接调用 Rust 工具内核完成读写,Web 只是展示层。**
验收标准:
- AI agent 使用的写入工具与 CLI 使用的工具完全同源
- AI 的每次编辑都能追踪到 command、event、trace、目标对象和 actor
- AI 可稳定执行页面编辑、块编辑、检索、结构化改写、批处理操作
## 3.5 观测、审计、幂等和失败恢复还只完成了首批链路
现在已有 `command_logs``domain_events`,但仍是首批写链路覆盖,不是全域治理。
仍缺的能力包括:
- 所有命令统一落账
- 所有失败态统一编码
- 统一重试与幂等语义
- 统一冲突模型
- 统一补偿与回放
- 统一事件重建和索引重放
如果没有这一层,CLI 和 AI 即使能写,也不够稳定。
验收标准:
- 任一写操作都能查 request、trace、command、domain event
- 幂等 key 在 CLI、AI、Web 三侧语义一致
- 冲突、拒绝、权限不足、对象不存在等错误有统一 code
- 可从事件或命令日志重建关键派生层
2026-04-15 进展补记:
- `rust/crates/core-protocol``bridge-runtime` 已新增 `bridge_request_get``bridge_trace_get``bridge_command_get``event_replay``index_rebuild` 这组统一观测/恢复能力;当前 `/api/bridge/request``/api/bridge/trace` 也已切到 Rust query plan + TS transport 的同一路径,不再各自直连 Convex 查询。
- `src/lib/documents/bridge-log.ts` 已把命令日志与领域事件的状态语义显式化,支持 `pending/succeeded/failed/rolled_back``pending/committed/rejected/failed` 两套状态模型,为后续冲突、失败和补偿写回提供稳定落点。
当前仍未关闭的缺口:
- 失败态、冲突态虽然已经在页面主写链与 AI/Mindmap 写链补上第一批落账,但仍未覆盖所有对象域写链。
- `event_replay` / `index_rebuild` 目前已成为正式命令面,但还主要停留在 runtime/工具层,尚未形成完整的持久化游标、任务调度和断点续跑体系。
- workspace 级总览、分页、按对象范围筛选等观测 UI 仍未完善。
## 3.6 搜索、索引和派生视图仍在持续切换中
2026-04-15 进展补记:
- `search.documents` 已不再把核心 ranking / snippet / filter 留在 TS route。当前 `/mnt/Data1T/mnote/wolai-frontend/src/app/api/search/documents/route.ts` 只保留参数校验、原始数据装载与 HTTP 回传;标题/正文/思维导图/表格/附件的匹配合并、高亮 snippet 与 OCR 待补队列决策已进入 `rust/crates/index-fts/src/lib.rs`
- `search.recent` 已补齐独立 Rust query 名称,并通过 runtime 返回最近访问结果;当前保留 `/api/search/recent` 作为“打开页面后写最近访问记录”的 side-effect 接口,不与搜索召回混在一条写链里。
- `sidebar.dataset.list` 已从“只在 route 上挂一个 Rust queryName”推进为真实 runtime query transport`/api/sidebar` 现会先经过 Rust runtime,再调用 `sidebar:datasetList`
当前仍未完全关闭的缺口:
- 索引重建、校验与回放命令还未落到产品主路径。
- `src/app/(app)/layout.tsx` 的 SSR 侧边栏初始数据仍复用现有 `loadSidebarDataFromConvex` helper,没有一并切到 runtime transport。
更新后的验收标准:
- `search_web``image_read``slash_run` 这类 AI 核心工具必须保持 `RUST_OWNER`,不能回退到 TS 真执行兜底。
- `builtins/**` 中已被 Rust 替代的服务端真入口必须进入第一批 `TS_LEGACY_DELETE`,清单以 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-legacy-delete-list.md` 为准。
- 搜索结果由 Rust query / index 层给出。
- TS 前端只做 UI 投影,不做核心排序与召回逻辑。
- 索引可重建、可校验、可回放。
## 3.7 Mindmap 和 OnlyOffice 还没有进入真正的 Rust adapter 执行层
现在对这两个对象域,文档上已经明确了边界,但执行层仍主要在现有 TS/Convex 逻辑。
现状更接近:
- **边界想清楚了**
- **前端形态保住了**
- **但 Rust adapter 还没真正接管**
缺口主要包括:
- `adapter-mindmap` 尚未并入主仓
- `adapter-onlyoffice` 尚未并入主仓
- Mindmap 节点操作还没有稳定的 Rust ops 协议
- OnlyOffice 的 sign / proxy / callback / forcesave 还没统一进入 Rust 对象适配层
验收标准:
- Mindmap 的结构操作可由 CLI 与 AI 直接调用
- OnlyOffice 的对象能力具备统一 session/asset 边界
- 不需要依赖前端 route 才能操作这些对象
## 3.8 仍缺一条“从旧内核切换到新内核”的明确割接路线
目前已有 backport plan,也有 phase checklist,但还缺一个更硬的最终割接视角:
- 哪些旧 TS route 会被逐步下线
- 哪些功能先双写、再单写
- 哪些功能允许长期保留在前端侧
- 哪些功能必须强制进入 Rust
- 何时可以宣布“原内核不再是主执行面”
如果没有这一条,项目会长期停留在“看起来在迁移,实际上双内核并存”的状态。
验收标准:
- 列出旧执行面的退役清单
- 每个能力域有 cutover milestone
- 明确宣布 Rust 成为唯一业务执行平面时的准入条件
2026-04-15 进展补记:
- 本轮已新增 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md`,首次把页面系统、块系统、查询聚合、AI、Mindmap、OnlyOffice、兼容接口统一盘点为 `RUST_OWNER / TS_TRANSPORT_KEEP / TS_COMPAT_PENDING / TS_LEGACY_DELETE` 四类状态。
- 当前“不明确”的问题已经收口为“执行尚未完成”的问题:退役清单、阶段 gate 与最终宣布口径都已写清,但旧接口的物理删除和 AI 工具面的完全统一还未完成。
---
## 4. 对最终目标的重新拆解
如果目标是:
> **Rust 内核替换原有内核,绝大部分功能 CLI 化,AI 可以自行编辑**
那么最终至少要同时满足下面四件事。
## 4.1 Rust 是唯一业务执行平面
要求:
- Web、CLI、AI 都调用同一 Rust command/query/tool 内核
- 前端不再是业务规则主载体
## 4.2 CLI 是一等公民,不是调试附属品
要求:
- 大部分核心能力都能通过 CLI 完成
- 输出稳定 JSON
- 支持脚本化、批处理和非交互执行
## 4.3 AI 只是 CLI/Tool 的智能调度者
要求:
- AI 不再依赖页面私有 bridge 才能编辑
- AI 调用的每一步都可审计、可回放、可限权
## 4.4 Convex 继续是事实层,但不再直接暴露产品规则
要求:
- Convex 主要承担持久化与事实保存
- 规则、协议、工具、索引、对象适配统一收进 Rust
---
## 5. 建议按优先级补齐的剩余里程碑
按最终目标倒推,接下来最应该补的不是 UI,而是下面六个里程碑。
### M1. 建立真实 Rust 执行入口
- 初始化 `rust/bridge/`
- 让 Web route 可以调用真实 Rust 运行时,而不是只在 TS 中模拟 Rust request
- 先覆盖 `documents.create/get/save/title/options/stats/sidebar/search`
### M2. 并入 `mnote-cli`
- 在主仓加入 CLI crate
- 先定义稳定命令面和 JSON 输出协议
- 让页面、块、搜索、Sidebar 至少先能命令行操作
### M3. 完成页面系统与块系统的 Rust 接管
- 页面创建、移动、删除、恢复、复制
- block 插入、替换、移动、删除
- 页面树与回收站
### M4. 完成搜索/索引/派生视图 Rust 化
- 把搜索、snippet、排序、Sidebar 数据集聚合切到 Rust
- 建立索引重建与校验命令
### M5. 完成对象域 adapter
- `adapter-mindmap`
- `adapter-onlyoffice`
- 后续再看媒体、表格等对象域
### M6. 让 AI 与 CLI 共用同一 Tool 面
- AI 不再调用前端私有写入逻辑
- 改为直接使用 Rust tool protocol
- 加入 dry-run、权限、审计、回放能力
---
## 6. 一句话结论
当前不是“还差一点点就完成 Rust 内核替换”,而是:
> **我们已经完成了 Rust 内核替换前的单仓收口、协议奠基和首批接缝,但距离“Rust 真正取代旧内核,并让 CLI 与 AI 成为一等执行入口”还差一整层执行面重构。**
最关键的剩余目标只有三条:
- **把业务执行权从 TS route 真正移交给 Rust**
- **把核心能力系统化地做成 CLI**
- **让 AI 与 CLI 共用同一套 Rust tool 内核**
只要这三条没完成,就还不能说“原有内核已经被 Rust 替换”。
@@ -0,0 +1,415 @@
# [recycle] mnote Rust 内核替换总路线图 v1
> 更新时间:2026-04-15
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-plan.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-phase-checklist.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-missing-targets.md`
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
## 1. 目标定义
本路线图对应的最终目标只有一句话:
> **用 Rust 内核取代当前 TypeScript/Next route 主导的业务执行面,让 Web、CLI、AI 共享同一套 Rust Command / Query / Tool 内核。**
这里包含三个同时成立的条件:
- Rust 成为唯一业务执行平面
- 绝大部分核心能力都能通过 CLI 调用
- AI 编辑能力建立在与 CLI 同源的 Rust 工具面上
如果只完成其中一部分,都不能算“Rust 内核已经替换原有内核”。
---
## 2. 当前阶段判断
当前仓库已经完成的是:
- 单仓收口
- Rust workspace 落位
- P0 crate 与核心设计文档落位
- 首批 bridge 协议接缝
- 首批日志与事件落账
当前仓库还没有完成的是:
- Rust 真实执行入口
- `mnote-cli`
- 全域能力域建模
- AI/CLI 共用 Tool 面
- 旧 TS 执行面的系统性退役
因此当前阶段应定义为:
> **Phase 0 完成,Phase 1 即将开始。**
其中:
- `Phase 0` = 单仓收口 + 协议奠基
- `Phase 1` 以后才是“真正的内核替换”
---
## 3. 总体迁移原则
整个替换过程必须遵守下面六条原则。
### 3.1 Web 不再承载业务真规则
Next route 可以保留 transport、auth、session、streaming、SSR/BFF 职责,但不再长期承载核心业务执行规则。
### 3.2 CLI 与 AI 必须共用同一执行面
不能出现:
- CLI 走一套实现
- AI 走一套实现
- Web 再走第三套实现
最终只能保留一套 Rust 内核,三种入口共享。
### 3.3 Convex 继续是事实层,不是业务规则层
Convex 继续负责主事实保存,但对象规则、命令语义、查询聚合、索引、工具协议应逐步收回 Rust。
### 3.4 先做执行面替换,再做旧面退役
不能先删旧链路再补 Rust,也不能长期停留在双内核并行。
正确顺序是:
1. 建立 Rust 执行链
2. 双入口对齐
3. 完成回归
4. 退役旧 TS 业务执行逻辑
### 3.5 先覆盖高频主链路,再覆盖对象域
优先级应是:
1. 页面系统
2. 块系统
3. Sidebar / 搜索 / 索引
4. AI 写入工具面
5. Mindmap / OnlyOffice / Media / Table
### 3.6 每一阶段都必须有割接标准
每个阶段都要明确:
- 哪些功能已由 Rust 接管
- 哪些 TS route 仍是临时面
- 哪些旧实现可退役
---
## 4. 分阶段路线
## Phase 1:建立真实 Rust 执行入口
目标:
- 在主仓内建立最小 Rust bridge/runtime
- Web route 能调用真实 Rust 执行器
- 不再只是在 TypeScript 中构造 Rust 风格 request
优先覆盖:
- `documents.meta`
- `documents.content`
- `documents.save`
- `documents.title`
- `documents.options`
- `documents.stats`
- `sidebar.dataset.list`
- `search.documents`
阶段产出:
- `/mnt/Data1T/mnote/rust/bridge/`
- 最小 runtime executor
- Web -> Rust -> Convex 的真实样板链
阶段完成标准:
- 至少 1 条读链和 2 条写链真实经过 Rust 执行
- 对应 TS route 不再直接持有业务拼装逻辑
## Phase 2:引入 `mnote-cli` 并冻结 JSON 协议
目标:
- 把 CLI 变成一等入口
- 定义稳定的 machine-readable 输出
- 为后续 AI 共用工具面打基础
首批 CLI 面:
- `page`
- `block`
- `search`
- `sidebar`
- `tool`
阶段产出:
- `rust/crates/mnote-cli/`
- 统一 exit code 约定
- `--json` 输出契约
- `dry-run` / `validate-only` 基础能力
阶段完成标准:
- 核心页面和块操作能脱离浏览器完成
- CLI 与 Web 调用同一 Rust 执行面
## Phase 3:页面系统 Rust 化
目标:
- 页面创建、移动、复制、删除、恢复、回收站等能力收口到 Rust
- 页面树、父子关系、排序、引用更新不再由 TS route 零散实现
优先覆盖:
- `documents.create`
- `documents.move`
- `documents.delete`
- `documents.restore`
- `documents.duplicate`
- `documents.empty-trash`
- `documents.copy-tree`
阶段完成标准:
- 页面生命周期操作统一走 Rust command
- 旧 TS route 只保留 transport 包装
## Phase 4:块系统 Rust 化
目标:
- 把 BlockNote 正文相关的结构化编辑命令真正变成 Rust 内核能力
- AI/CLI 可直接调用块操作,不依赖浏览器交互细节
优先覆盖:
- `blocks.get`
- `blocks.patch`
- `blocks.move`
- `blocks.embed`
- `doc_insert_blocks`
- `doc_replace_range`
阶段完成标准:
- Rust 拥有稳定的 block ops 协议
- Web 编辑器只负责快照采集、渲染与交互
## Phase 5:搜索、索引与派生视图 Rust 化
目标:
- 把搜索、排序、snippet、Sidebar 数据集聚合统一移入 Rust
- 建立索引重建和派生视图回放能力
优先覆盖:
- `search.documents`
- `search.recent`
- `sidebar.dataset.list`
- 索引重建与校验命令
阶段完成标准:
- TS route 不再做核心召回和 ranking
- `index-fts` 真正进入产品主路径
2026-04-15 Phase 5 第一刀补记:`search.documents``search.recent``sidebar.dataset.list` 已补齐到主仓 Rust query/runtime 主链。`rust/crates/core-protocol` 新增 `SearchDocuments/SearchRecent` 契约,`storage-convex-bridge` 新增 query name -> Convex function 映射,`bridge-runtime` 现同时支持 query plan 输出与“携带原始 dataset 时直接在 Rust 内执行并返回 result”。其中 `search.documents` 的召回后排序、snippet、高亮、OCR 待补队列决策已迁入 `rust/crates/index-fts``evaluate_search_documents``/api/search/documents` route 现仅保留参数归一化、Convex 原始数据拉取与 HTTP 回传,不再持有标题/正文/思维导图/表格/附件的打分合并逻辑。`/api/sidebar` 也已改为先经 Rust runtime 生成 `sidebar.dataset.list` plan,再通过统一 query transport 调用 `sidebar:datasetList`,不再只是挂一个 queryName 元信息。
## Phase 6AI 与 CLI 共用 Rust Tool 面
目标:
- 让 AI 直接调用 Rust tool protocol
- Web 中的 AI Agent 只成为对话和流式展示层
能力要求:
- `Tool` 注册表
- 统一权限与对象目标
- 统一错误码
- 统一审计
- `dry-run` / `validate-only` / `explain-plan`
阶段完成标准:
- AI 与 CLI 调用同一 Tool 面
- AI 编辑结果可追踪到 command、event、trace
2026-04-15 Phase 6 补记:已把统一观测面补进 Rust `Tool` 注册表与 `bridge-runtime`。当前 `bridge_request_get``bridge_trace_get``bridge_command_get` 三条观测查询,以及 `event_replay``index_rebuild` 两条恢复/重建任务,已经成为 Rust 正式能力面;`/api/bridge/request``/api/bridge/trace` 也已改为先经 Rust runtime 生成 query plan,再由 TS 仅做 Convex transport。后续 CLI 与 AI 若要稳定运行,必须以这组能力为统一回查/恢复前置,不再允许各入口各自手写 trace 查询与索引恢复脚本。
## Phase 7:对象域 adapter 接入
目标:
- 在不改变当前前端形态的前提下,把对象域执行层移入 Rust adapter
优先对象域:
- `adapter-mindmap`
- `adapter-onlyoffice`
- 后续再扩展 media / table
阶段完成标准:
- Mindmap 与 OnlyOffice 都可由 CLI / AI 直接操作核心对象能力
- 前端 route 不再是对象域业务真入口
## Phase 8:旧内核退役与总割接
目标:
- 明确哪些旧 TS route 只剩 transport
- 明确哪些旧业务实现可删除
- 正式宣布 Rust 成为唯一业务执行平面
阶段完成标准:
- Web、CLI、AI 全部指向同一 Rust 内核
- 核心域不再存在第二套业务执行实现
2026-04-15 Phase 8 补记:当前主仓已新增 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md` 作为最终割接文档。后续 Phase 8 不再接受“按感觉判断是否可以退役”的说法,而统一按该文档中的四组 gate 执行:页面/块/查询主链全部 Rust 持有,对象域只剩 transport 壳,AI/CLI 共用同一 Tool 面,以及旧 TS 兼容接口完成物理退役或明确降级。
---
## 5. 建议的切换顺序
为了降低风险,建议按下面顺序切换,而不是全面铺开。
### 批次 A:文档基础链
- `documents.meta`
- `documents.content`
- `documents.save`
- `documents.title`
- `documents.options`
- `documents.stats`
### 批次 B:页面结构链
- `documents.create`
- `documents.move`
- `documents.delete`
- `documents.restore`
- `documents.duplicate`
- `documents.copy-tree`
### 批次 C:块编辑链
- `blocks.get`
- `blocks.patch`
- `blocks.move`
- `blocks.embed`
### 批次 D:查询聚合链
- `sidebar.dataset.list`
- `search.documents`
- `search.recent`
### 批次 EAI/CLI 工具链
- `doc_get`
- `doc_find`
- `doc_insert_blocks`
- `doc_replace_range`
- `slash_run`
### 批次 F:对象域链
- Mindmap
- OnlyOffice
- Media
- Table
---
## 6. 每阶段统一验收口径
无论哪个阶段,验收都应统一看下面六项。
### 6.1 执行入口是否真的进了 Rust
不是“TS 构造了 Rust 风格对象”,而是“Rust 真正执行了这条链”。
### 6.2 Web/CLI/AI 是否同源
如果一个能力还存在两套或三套实现,就不算完成。
### 6.3 是否具备 JSON 级输出与错误码
没有稳定 JSON 和错误码,就无法支撑 CLI 与 AI。
### 6.4 是否具备审计与回放
没有 command/event/trace,就无法长期稳定运行。
这里的“具备”不是指只落了一批日志表,而是至少同时满足:
- 任一写链都能回查 `request_id``trace_id``command_id`
- 命令、事件、冲突、失败态使用统一状态语义
- 至少有一条正式的 `event_replay` / `index_rebuild` 命令可被 CLI / AI / Web 复用
- TS route 不再私有维护第二套排障与恢复入口
### 6.5 是否补了自动化验证
每阶段都必须至少补:
- Rust 单测
- Web smoke
- 必要时的浏览器回归
- 对应 CLI smoke
### 6.6 是否明确了可退役旧面
必须显式标注:
- 哪些 TS 逻辑只剩壳
- 哪些逻辑仍是临时态
- 哪些代码已允许删除
---
## 7. 风险与防漂移要求
整个路线最容易失败的点有四个。
### 7.1 长期停留在“桥接完成即算完成”
这会导致项目永远停留在半替换状态。
### 7.2 CLI 迟迟不建立
如果不尽早建立 CLI,AI 最终还是会绕回 Web 私有逻辑。
### 7.3 对象域长期例外化
Mindmap、OnlyOffice、Media、Table 如果一直被当例外处理,最终不会形成统一内核。
### 7.4 旧 TS 执行面没有退役时点
如果不定义退役清单,旧逻辑会持续存活并反向污染新内核。
---
## 8. 一句话结论
这条路线不是“继续补几条 bridge”就能结束,而是要完成一次完整的执行面替换:
> **先把 Rust 变成真实执行器,再把 CLI 变成一等入口,最后让 AI 与 Web 共同收口到这套 Rust 内核。**
在这三个条件同时成立之前,都还不能宣布“Rust 内核已经替换原有内核”。
@@ -0,0 +1,76 @@
# [recycle] Sidebar Rust Query 目标说明
> 更新时间:2026-04-14
>
> 适用主仓:`/mnt/Data1T/mnote`
## 1. 当前起点
当前 Sidebar 仍是“同一份数据集,多处复用”的结构,但重复拼装已经先收口到共享 helper:
- 查询 payload / result 契约:`/mnt/Data1T/mnote/wolai-frontend/src/lib/sidebar-data.ts`
- 服务端聚合入口:`/mnt/Data1T/mnote/wolai-frontend/src/lib/server/sidebar-data.ts`
- API route`/mnt/Data1T/mnote/wolai-frontend/src/app/api/sidebar/route.ts`
- 实时订阅 hook`/mnt/Data1T/mnote/wolai-frontend/src/hooks/use-convex-sidebar-data.ts`
当前事实:
- `sidebar.dataset.list` 已有稳定 payload`{ workspace_id }`
- `sidebar.dataset.list` 已有稳定 result`active_workspace_id``workspaces``documents``trashed_documents``media_assets``trashed_media_assets``mindmap_assets``trashed_mindmap_assets``table_assets``trashed_table_assets``mindmap_docs``mindmap_asset_children`
- `src/lib/sidebar-data.test.ts` 已对上述契约做冻结测试
## 2. Rust Query 最小目标
后续 Rust query 不直接输出 UI rows,而是只负责输出兼容 `SidebarInitialData` 的共享数据集。
最小目标:
- 输入:
- `workspace_id`
- 输出:
- `active_workspace_id`
- `workspaces`
- `documents`
- `trashed_documents`
- `media_assets`
- `trashed_media_assets`
- `mindmap_assets`
- `trashed_mindmap_assets`
- `table_assets`
- `trashed_table_assets`
- `mindmap_docs`
- `mindmap_asset_children`
## 3. 前后端边界
### 3.1 Rust query 负责
- 聚合工作区范围内页面、回收站、媒体、导图、表格的原始数据集
- 保持字段命名与共享 contract 一致
- 保持 `workspace_id` 作用域明确
### 3.2 前端继续负责
- `buildDocumentTree`
- `buildVisibleRows`
- `starred/public/shared/private/templates` 分区语义
- `sort_order -> created_at` 排序投影
- `doc/index.md/asset-folder/asset` 文件树行语义
- 拖拽、剪贴板、展开折叠、回收站双 tab 等交互行为
## 4. 本轮明确不做的事
- 不复制 `mnote-rust``components/sidebar/sidebar.tsx`
- 不引入第二套导航壳
- 不在 Rust query 阶段直接输出前端渲染树
- 不改变当前 Sidebar UI 结构与交互
## 5. 验证锚点
以下文件共同构成本轮 Sidebar Rust query 目标冻结点:
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/sidebar-data.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/server/sidebar-data.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/sidebar/route.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/hooks/use-convex-sidebar-data.ts`
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/sidebar-data.test.ts`
+13
View File
@@ -0,0 +1,13 @@
# old 回收稿索引
> 本目录只放 `[recycle]` 状态的历史设计稿。
>
> 分组说明:
> - `process/`:已废弃,但属于历史上的草稿、方案、spike、路线稿
> - `done/`:已废弃,但属于历史上的定稿、审计、报告、边界说明
>
> 大类说明:
> - `01-tree-first-graph-kernel/`:已被新清单替代的旧内核稿
> - `03-rust-web/`:已被新清单替代的旧 Rust Web 稿
> - `05-editor-mainline/`:旧编辑器路线、选型与实验稿
> - `08-legacy-rust-kernel/`:上一轮 Rust cutover 相关历史稿
+61 -3
View File
@@ -2,7 +2,9 @@ use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::routes::web_shell::{
build_editor_bootstrap_json, build_page_aggregate_snapshot, escape_html, escape_script_json,
load_file_tree_html, load_sidebar_tree_html, load_workspace_shell_projection,
render_document_title_controller_script, render_editor_island_adapter_script,
};
use crate::transport::convex::execute_convex_mutation_by_name;
use crate::workspace_shell::render_workspace_shell_sidebar_html;
@@ -140,23 +142,79 @@ pub async fn root_entry(
.active_page_title
.clone()
.unwrap_or_default();
let render_workspace_entry = || {
crate::ssr::render_view(leptos::view! {
<crate::ssr::pages::home::HomePage
sidebar_tree_html={sidebar_tree_html.clone()}
workspace_name={workspace_name.clone()}
workspace_id={workspace_id.clone()}
workspace_sidebar_html={workspace_sidebar_html.clone()}
active_page_id={active_page_id.clone()}
active_page_title={active_page_title.clone()}
/>
})
};
let (html_title, content, body_extra) = if active_page_id.trim().is_empty() {
("MNOTE".to_string(), render_workspace_entry(), String::new())
} else {
match build_page_aggregate_snapshot(
&state,
&context,
&active_page_id,
Some(workspace_id.as_str()),
)
.await
{
Ok(aggregate) => {
let title = aggregate.head.title.as_str();
let page_subtree_json = serde_json::to_string(&aggregate.tree.page_subtree)
.unwrap_or_else(|_| "null".to_string());
let snapshot_json =
serde_json::to_string(&aggregate).unwrap_or_else(|_| "null".to_string());
let bootstrap_json = build_editor_bootstrap_json(&aggregate, &context);
let content = crate::ssr::render_view(leptos::view! {
<crate::ssr::pages::home::HomePage sidebar_tree_html={sidebar_tree_html} workspace_name={workspace_name} workspace_id={workspace_id.clone()} workspace_sidebar_html={workspace_sidebar_html} active_page_id={active_page_id} active_page_title={active_page_title} />
<crate::ssr::pages::document::DocumentPage
title={title.to_string()}
document_id={active_page_id.clone()}
workspace_id={workspace_id.clone()}
sidebar_tree_html={sidebar_tree_html.clone()}
workspace_name={workspace_name.clone()}
workspace_sidebar_html={workspace_sidebar_html.clone()}
page_subtree_json={page_subtree_json}
/>
});
let body_extra = format!(
r#"<script id="__MNOTE_PAGE_AGGREGATE__" type="application/json">{}</script>
<script id="__MNOTE_EDITOR_BOOTSTRAP__" type="application/json">{}</script>
{}
{}"#,
escape_script_json(&snapshot_json),
escape_script_json(&bootstrap_json),
render_document_title_controller_script(),
render_editor_island_adapter_script(),
);
(title.to_string(), content, body_extra)
}
Err(_) => ("MNOTE".to_string(), render_workspace_entry(), String::new()),
}
};
let mut response = Html(format!(
r#"<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>MNOTE</title>
<title>{}</title>
<style>{}</style>
</head>
<body data-mnote-web-owner="mnote-web" data-mnote-shell="workspace">
{}
{}
</body>
</html>"#,
escape_html(&html_title),
crate::ssr::MNOTE_CSS,
content
content,
body_extra
))
.into_response();
stamp_gateway_headers(response.headers_mut(), false);
+27 -3
View File
@@ -36,7 +36,7 @@ use bridge_runtime::RuntimeCommandEnvelopeWire;
use core_protocol::KernelProjectionKind;
use serde::Deserialize;
use serde_json::{json, Value};
use std::collections::BTreeSet;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
@@ -209,7 +209,7 @@ fn collect_expanded_ids(projection: &Value) -> BTreeSet<String> {
}
pub(crate) fn collect_page_tree_render_rows(projection: &Value) -> Vec<PageTreeRenderRow> {
projection
let mut rows: Vec<PageTreeRenderRow> = projection
.get("items")
.and_then(Value::as_array)
.map(|items| {
@@ -232,6 +232,7 @@ pub(crate) fn collect_page_tree_render_rows(projection: &Value) -> Vec<PageTreeR
node_id: node_id.to_string(),
parent_node_id: item
.get("parentNodeId")
.or_else(|| item.get("parentId"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
@@ -261,7 +262,30 @@ pub(crate) fn collect_page_tree_render_rows(projection: &Value) -> Vec<PageTreeR
})
.collect()
})
.unwrap_or_default()
.unwrap_or_default();
let parent_by_id = rows
.iter()
.map(|row| (row.node_id.clone(), row.parent_node_id.clone()))
.collect::<BTreeMap<_, _>>();
for row in &mut rows {
if row.depth == 0 && row.parent_node_id.is_some() {
let mut depth = 0_u32;
let mut cursor = row.parent_node_id.as_deref();
while let Some(parent_id) = cursor {
depth += 1;
cursor = parent_by_id
.get(parent_id)
.and_then(|parent| parent.as_deref());
if depth > 32 {
break;
}
}
row.depth = depth;
}
}
rows
}
pub(crate) fn collect_filetree_render_rows(
@@ -126,7 +126,10 @@ pub async fn document_page_shell(
Ok(response)
}
fn build_editor_bootstrap_json(aggregate: &PageAggregate, context: &RequestContext) -> String {
pub(crate) fn build_editor_bootstrap_json(
aggregate: &PageAggregate,
context: &RequestContext,
) -> String {
serde_json::to_string(&json!({
"schema": "mnote.editor_bootstrap.v1",
"documentId": aggregate.identity.document_id,
@@ -142,7 +145,7 @@ fn build_editor_bootstrap_json(aggregate: &PageAggregate, context: &RequestConte
.unwrap_or_else(|_| "{}".to_string())
}
fn render_document_title_controller_script() -> &'static str {
pub(crate) fn render_document_title_controller_script() -> &'static str {
r#"<script>
(() => {
const CONTRACT = 'mnote.document_title_controller.v1';
@@ -259,7 +262,7 @@ fn render_document_title_controller_script() -> &'static str {
</script>"#
}
fn render_editor_island_adapter_script() -> &'static str {
pub(crate) fn render_editor_island_adapter_script() -> &'static str {
r#"<script type="module">
(() => {
const ROOT_SELECTOR = '[data-testid="mnote-leptos-tiptap-island-editor-root"]';
@@ -586,7 +589,7 @@ pub async fn page_aggregate(
Ok(response)
}
async fn build_page_aggregate_snapshot(
pub(crate) async fn build_page_aggregate_snapshot(
state: &AppState,
context: &RequestContext,
document_id: &str,
@@ -661,7 +664,7 @@ fn stamp_shell_headers(headers: &mut HeaderMap, shell: &'static str) {
}
}
fn escape_html(value: &str) -> String {
pub(crate) fn escape_html(value: &str) -> String {
value
.replace('&', "&amp;")
.replace('<', "&lt;")
@@ -669,7 +672,7 @@ fn escape_html(value: &str) -> String {
.replace('"', "&quot;")
}
fn escape_script_json(value: &str) -> String {
pub(crate) fn escape_script_json(value: &str) -> String {
value.replace("</script", "<\\/script")
}
+180 -17
View File
@@ -252,22 +252,28 @@ const SIDEBAR_TREE_JS: &str = r##"
return grouped;
}
function renderPageRows(parentId, grouped, activeId) {
function pageTreeChevronSvg() {
return '<svg class="tree-toggle-icon" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><path d="M7.84 14.955c.206 0 .37-.07.505-.21l4.277-4.179a.79.79 0 0 0 .264-.574.78.78 0 0 0-.258-.574L8.35 5.24a.7.7 0 0 0-.51-.21.721.721 0 0 0-.498 1.247l3.814 3.721-3.814 3.709a.721.721 0 0 0 .498 1.248"></path></svg>';
}
function renderPageRows(parentId, grouped, activeId, inheritedDepth) {
var computedDepth = Number(inheritedDepth || 0);
return (grouped.get(parentId) || []).map(function(item) {
var nodeId = nodeIdOf(item);
var title = titleOf(item);
var depth = Number(item.depth || 0);
var depth = computedDepth;
var parent = parentIdOf(item);
var children = grouped.get(nodeId) || [];
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
var expanded = expandable && item.expandedByDefault !== false;
var toggle = expandable
? '<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="' + escapeHtml(nodeId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '">' + (expanded ? '▾' : '') + '</button>'
? '<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="' + escapeHtml(nodeId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '" aria-expanded="' + String(expanded) + '">' + pageTreeChevronSvg() + '</button>'
: '<span class="tree-spacer" aria-hidden="true"></span>';
var childHtml = expandable && expanded
? '<ul class="tree-children">' + renderPageRows(nodeId, grouped, activeId) + '</ul>'
? '<ul class="tree-children">' + renderPageRows(nodeId, grouped, activeId, depth + 1) + '</ul>'
: '';
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="page" data-testid="wolai-sidebar-row" data-tree-testid="tree-node-open" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-depth="' + depth + '" data-shell-mode="page" data-active="' + String(nodeId === activeId) + '" data-focused="false" data-draggable="true" draggable="true" tabindex="-1">' + toggle + '<span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="' + escapeHtml(nodeId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="' + escapeHtml(nodeId) + '" aria-label="新建子页面">+</button><button type="button" class="tree-action" data-testid="tree-action-rename" data-rust-action="rename" data-node-id="' + escapeHtml(nodeId) + '" aria-label="重命名"></button><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="' + escapeHtml(nodeId) + '" aria-label="更多操作"></button></div></div>' + childHtml + '</li>';
var currentAttr = nodeId === activeId ? ' aria-current="page" aria-selected="true"' : ' aria-selected="false"';
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="page" data-testid="wolai-sidebar-row" data-tree-testid="tree-node-open" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-depth="' + depth + '" data-shell-mode="page" data-active="' + String(nodeId === activeId) + '"' + currentAttr + ' data-focused="false" data-draggable="true" draggable="true" tabindex="-1">' + toggle + '<button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="' + escapeHtml(nodeId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="' + escapeHtml(nodeId) + '" aria-label="更多操作"></button><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="' + escapeHtml(nodeId) + '" aria-label="新建子页面">+</button></div></div>' + childHtml + '</li>';
}).join('');
}
@@ -278,7 +284,7 @@ const SIDEBAR_TREE_JS: &str = r##"
return String(item.rowKind || 'document') === 'document';
});
var activeId = currentDocumentId();
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId) : '<li class="tree-empty" data-rust-rendered-row="page-empty"></li>') + '</ul>';
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId, 0) : '<li class="tree-empty" data-rust-rendered-row="page-empty"></li>') + '</ul>';
return true;
}
@@ -373,7 +379,11 @@ const SIDEBAR_TREE_JS: &str = r##"
children.classList.toggle('tree-children--collapsed');
var collapsed = children.classList.contains('tree-children--collapsed');
row.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
if (button) button.textContent = collapsed ? '▸' : '';
if (button && button.getAttribute('data-testid') === 'tree-node-toggle') {
button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
} else if (button) {
button.textContent = collapsed ? '▸' : '';
}
}
function dispatchSidebarEvent(name, detail) {
@@ -588,20 +598,20 @@ const SIDEBAR_TREE_JS: &str = r##"
{ action: 'move', icon: 'drive_file_move', label: '...' },
{ action: 'copy-id', icon: 'tag', label: ' ID' }
] : [
{ action: 'open-right', icon: 'right_panel_open', label: '', shortcut: 'Alt + O' },
{ action: 'share', icon: 'share', label: '...' },
{ action: 'open-right', icon: 'right_panel_open', label: '', shortcut: 'Alt+' },
{ separator: true },
{ action: 'move', icon: 'drive_file_move', label: '...' },
{ action: 'embed', icon: 'account_tree', label: '...' },
{ separator: true },
{ action: 'copy-link', icon: 'link', label: '访' },
{ action: 'copy-link-title', icon: 'link', label: '访' },
{ action: 'copy-reference-inline', icon: 'content_copy', label: '' },
{ action: 'copy-id', icon: 'tag', label: ' ID' },
{ action: 'copy-id', icon: 'tag', label: 'ID' },
{ separator: true },
{ action: 'duplicate', icon: 'file_copy', label: '' },
{ separator: true },
{ action: 'rename', icon: 'edit', label: '' },
{ action: 'create-child', icon: 'add', label: '' },
{ action: 'convert-child', icon: 'subdirectory_arrow_right', label: '' },
{ action: 'delete-trash', icon: 'delete', label: '', danger: true }
{ separator: true },
{ action: 'delete-trash', icon: 'delete', label: '', shortcut: 'Del', danger: true }
];
items.forEach(function(item) { appendTreeContextMenuButton(menu, item, detail, trigger); });
document.body.appendChild(menu);
@@ -638,10 +648,155 @@ const SIDEBAR_TREE_JS: &str = r##"
}, x, y, trigger || row);
}
function ensureSearchModal() {
var existing = document.querySelector('[data-testid="wolai-search-modal"]');
if (existing instanceof HTMLElement) return existing;
var overlay = document.createElement('div');
overlay.className = 'wolai-search-overlay';
overlay.setAttribute('data-testid', 'wolai-search-modal');
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.hidden = true;
overlay.innerHTML = '' +
'<div class="wolai-search-dialog">' +
'<div class="wolai-search-input-row">' +
'<span class="material-symbols-outlined wolai-search-input-icon" data-icon="search" aria-hidden="true"></span>' +
'<input data-testid="wolai-search-input" class="wolai-search-input" type="text" autocomplete="off" placeholder="在当前工作区中搜索" />' +
'<button type="button" class="wolai-search-close" data-testid="wolai-search-close" aria-label="关闭搜索">×</button>' +
'</div>' +
'<div class="wolai-search-options" aria-label="搜索选项">' +
'<div class="wolai-search-options-left">' +
'<span class="wolai-search-switch-control"><span></span><button type="button" class="wolai-search-switch is-on" data-search-switch="title" role="switch" aria-checked="true" aria-label="仅匹配标题"></button></span>' +
'<span class="wolai-search-switch-control"><span></span><button type="button" class="wolai-search-switch" data-search-switch="exact" role="switch" aria-checked="false" aria-label="精确匹配"></button></span>' +
'<span class="wolai-search-sort-control"><span></span><button type="button" class="wolai-search-sort-value" data-search-sort="updated" aria-label="按编辑时间范围"></button></span>' +
'<span class="wolai-search-sort-control"><span></span><button type="button" class="wolai-search-sort-value" data-search-sort="created" aria-label="按创建时间范围"></button></span>' +
'</div>' +
'<div class="wolai-search-options-right">' +
'<span class="wolai-search-switch-control"><span></span><button type="button" class="wolai-search-switch is-on" data-search-switch="page" role="switch" aria-checked="true" aria-label="页面内搜索"></button></span>' +
'</div>' +
'</div>' +
'<div class="wolai-search-result-meta" data-testid="wolai-search-result-meta"></div>' +
'<div class="wolai-search-results" data-testid="wolai-search-results"></div>' +
'</div>';
document.body.appendChild(overlay);
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
var closeButton = overlay.querySelector('[data-testid="wolai-search-close"]');
if (input) input.addEventListener('input', renderSearchResults);
overlay.querySelectorAll('[data-search-switch]').forEach(function(button) {
button.addEventListener('click', function() {
var isOn = button.getAttribute('aria-checked') !== 'true';
button.setAttribute('aria-checked', isOn ? 'true' : 'false');
button.classList.toggle('is-on', isOn);
});
});
if (closeButton) closeButton.addEventListener('click', closeSearchModal);
overlay.addEventListener('click', function(event) {
if (event.target === overlay) closeSearchModal();
});
return overlay;
}
function searchText(value) {
return String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
}
function collectSearchCandidates() {
var seen = Object.create(null);
var candidates = [];
function add(title, source) {
title = searchText(title);
if (!title || seen[title]) return;
seen[title] = true;
candidates.push({ title: title, source: source || '' });
}
document.querySelectorAll('.tree-link-title, .wolai-row-title, [data-page-title-current="true"], .document-title-input, .document-read-view h1, .document-shell h1').forEach(function(node) {
add(node.textContent, '');
});
document.querySelectorAll('.mnote-content h1, .mnote-content h2, .mnote-content h3, .mnote-content p, .mnote-content a').forEach(function(node) {
add(node.textContent, '');
});
return candidates.slice(0, 120);
}
function highlightSearchTitle(title, query) {
var cleanTitle = searchText(title);
var cleanQuery = searchText(query);
if (!cleanQuery) return escapeHtml(cleanTitle);
var index = cleanTitle.toLowerCase().indexOf(cleanQuery.toLowerCase());
if (index < 0) return escapeHtml(cleanTitle);
return escapeHtml(cleanTitle.slice(0, index)) +
'<mark>' + escapeHtml(cleanTitle.slice(index, index + cleanQuery.length)) + '</mark>' +
escapeHtml(cleanTitle.slice(index + cleanQuery.length));
}
function renderSearchResults() {
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
if (!(overlay instanceof HTMLElement)) return;
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
if (!input || !meta || !results) return;
var query = searchText(input.value);
var candidates = collectSearchCandidates();
var filtered = query
? candidates.filter(function(item) { return item.title.toLowerCase().indexOf(query.toLowerCase()) >= 0; })
: candidates.slice(0, 8);
filtered = filtered.slice(0, 100);
meta.innerHTML = '<span> ' + filtered.length + ' </span><span>Ctrl + Enter / Alt + Enter </span>';
if (!filtered.length) {
results.innerHTML = '<div class="wolai-search-empty"></div>';
return;
}
results.innerHTML = filtered.map(function(item) {
return '<button type="button" class="wolai-search-result-row">' +
'<span class="material-symbols-outlined wolai-search-result-icon" data-icon="link" aria-hidden="true"></span>' +
'<span class="wolai-search-result-main"><span class="wolai-search-result-title">' + highlightSearchTitle(item.title, query) + '</span>' +
'<span class="wolai-search-result-snippet">' + escapeHtml(item.source) + ' · </span></span>' +
'</button>';
}).join('');
}
function isSearchModalOpen() {
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
return overlay instanceof HTMLElement && !overlay.hidden;
}
function openSearchModal() {
var overlay = ensureSearchModal();
overlay.hidden = false;
document.documentElement.setAttribute('data-mnote-search-modal-open', 'true');
renderSearchResults();
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
if (input) {
setTimeout(function() { input.focus(); input.select(); }, 0);
}
}
function closeSearchModal() {
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
if (overlay instanceof HTMLElement) overlay.hidden = true;
document.documentElement.removeAttribute('data-mnote-search-modal-open');
}
function toggleSearchModal() {
if (isSearchModalOpen()) closeSearchModal();
else openSearchModal();
}
document.addEventListener('click', function(e) {
if (activeTreeContextMenu && activeTreeContextMenu.contains(e.target)) return;
if (activeTreeContextMenu) closeTreeContextMenu();
var searchTrigger = closestAction(e.target, '[data-mnote-action="open-search-modal"]');
if (searchTrigger) {
e.preventDefault();
openSearchModal();
return;
}
var tabTrigger = closestAction(e.target, '[data-mnote-sidebar-tree-tab]');
if (tabTrigger) {
e.preventDefault();
@@ -753,7 +908,15 @@ const SIDEBAR_TREE_JS: &str = r##"
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') closeTreeContextMenu();
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key && event.key.toLowerCase() === 'p') {
event.preventDefault();
toggleSearchModal();
return;
}
if (event.key === 'Escape') {
closeSearchModal();
closeTreeContextMenu();
}
});
function readPageDragNodeId(event) {
@@ -1146,7 +1309,7 @@ pub fn PageLayout(
<span class="wolai-sidebar-chevron" aria-hidden="true">""</span>
</div>
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作">
<a href="/search" class:active={current_nav == "search"} title="搜索" aria-label="搜索"><span class="material-symbols-outlined nav-icon" data-icon="search" aria-hidden="true"></span></a>
<a href="/search" class:active={current_nav == "search"} title="搜索" aria-label="搜索" data-mnote-action="open-search-modal"><span class="material-symbols-outlined nav-icon" data-icon="search" aria-hidden="true"></span></a>
<a href="/graph" title="关系图" aria-label="关系图"><span class="material-symbols-outlined nav-icon" data-icon="account_tree" aria-hidden="true"></span></a>
<a href="/actions" title="快捷动作" aria-label="快捷动作"><span class="material-symbols-outlined nav-icon" data-icon="bolt" aria-hidden="true"></span></a>
<a href="/help" title="帮助" aria-label="帮助"><span class="material-symbols-outlined nav-icon" data-icon="help" aria-hidden="true"></span></a>
@@ -1173,7 +1336,7 @@ pub fn PageLayout(
<button type="button" class="wolai-icon-button" title="收藏" aria-label="收藏"><span class="material-symbols-outlined" data-icon="star" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="历史" aria-label="历史"><span class="material-symbols-outlined" data-icon="history" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button wolai-ai-inline" title="AI" aria-label="AI"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="搜索" aria-label="搜索"><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="搜索" aria-label="搜索" data-mnote-action="open-search-modal"><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="更多" aria-label="更多"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>
</div>
</header>
+109 -43
View File
@@ -910,6 +910,39 @@ a:hover {
}
/* ===== 响应式 ===== */
.wolai-search-overlay{position:fixed;inset:0;z-index:1200;display:flex;align-items:flex-start;justify-content:center;padding:92px 24px 24px;background:rgba(0,0,0,.32)}
.wolai-search-overlay[hidden]{display:none!important}
.wolai-search-dialog{width:min(680px,100%);max-height:min(720px,calc(100vh - 128px));overflow:hidden;display:flex;flex-direction:column;background:#FFF;border:1px solid rgba(27,28,28,.10);border-radius:6px;box-shadow:0 18px 48px rgba(15,23,42,.22),0 2px 8px rgba(15,23,42,.08);color:#37352F}
.wolai-search-input-row{display:flex;align-items:center;min-height:58px;padding:0 12px 0 18px;border-bottom:1px solid rgba(27,28,28,.08)}
.wolai-search-input-icon{width:22px;height:22px;color:#8B8780;margin-right:10px}
.wolai-search-input{flex:1 1 auto;min-width:0;height:56px;border:0;outline:none;background:transparent;color:#24211D;font:400 18px/1.4 var(--wolai-font-sans);letter-spacing:0}
.wolai-search-input::placeholder{color:#A29E97}
.wolai-search-close{width:28px;height:28px;border:0;border-radius:4px;background:transparent;color:#8B8780;cursor:pointer;font-size:20px;line-height:1}
.wolai-search-options{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 14px 8px;border-bottom:1px solid rgba(27,28,28,.06);color:#A7A39D;font-size:12px;line-height:1}
.wolai-search-options-left,.wolai-search-options-right,.wolai-search-switch-control,.wolai-search-sort-control{display:inline-flex;align-items:center}
.wolai-search-options-left{min-width:0;flex-wrap:wrap;gap:8px 12px}
.wolai-search-options-right{margin-left:auto;flex:0 0 auto}
.wolai-search-switch-control,.wolai-search-sort-control{gap:5px;white-space:nowrap}
.wolai-search-switch,.wolai-search-sort-value{border:0;background:transparent;cursor:pointer;font:inherit;letter-spacing:0}
.wolai-search-switch{position:relative;width:28px;height:16px;border-radius:999px;background:#D6D4D0;box-shadow:inset 0 0 0 1px rgba(27,28,28,.04)}
.wolai-search-switch::after{content:"";position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:999px;background:#FFF;box-shadow:0 1px 2px rgba(15,23,42,.22)}
.wolai-search-switch.is-on{background:#C9C7C3}
.wolai-search-switch.is-on::after{transform:translateX(12px)}
.wolai-search-sort-value{display:inline-flex;align-items:center;gap:3px;color:#6D6A65}
.wolai-search-sort-value::after{content:"";color:#B8B4AD;font-size:13px;line-height:1}
.wolai-search-result-meta{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 16px;color:#8B8780;font-size:12px;line-height:1.3;border-bottom:1px solid rgba(27,28,28,.06)}
.wolai-search-results{overflow:auto;padding:6px}
.wolai-search-result-row{width:100%;min-height:54px;display:flex;align-items:flex-start;gap:10px;padding:9px 10px;border:0;border-radius:4px;background:transparent;color:#37352F;cursor:pointer;text-align:left;font:inherit}
.wolai-search-result-row:hover{background:rgba(55,53,47,.08)}
.wolai-search-result-icon{width:18px;height:18px;margin-top:2px;color:#8B8780}
.wolai-search-result-main{min-width:0;display:flex;flex-direction:column;gap:3px}
.wolai-search-result-title{color:#2F2D29;font-size:14px;line-height:1.35;word-break:break-word}
.wolai-search-result-title mark{padding:0 1px;border-radius:2px;background:#FFE9E6;color:#D83A32}
.wolai-search-result-snippet,.wolai-search-empty{color:#8B8780;font-size:12px;line-height:1.35}
.wolai-search-empty{padding:28px 12px 32px;text-align:center}
@media (max-width: 768px) {
.mnote-sidebar {
width: 200px;
@@ -987,7 +1020,7 @@ a:hover {
/* ===== Stitch 260429 UI parity overrides ===== */
:root {
--atelier-surface: #FAF9F9;
--atelier-sidebar: #F4F3F3;
--atelier-sidebar: #F5F5F5;
--atelier-sidebar-hover: #ECEBE9;
--atelier-sidebar-active: #E3E2E2;
--atelier-document: #FFFFFF;
@@ -1031,26 +1064,26 @@ body {
.mnote-sidebar,
.wolai-sidebar {
width: 240px;
width: 248px;
background: var(--atelier-sidebar);
border-right: 0;
box-shadow: inset -1px 0 0 rgba(27, 28, 28, 0.06);
box-shadow: none;
overflow-x: hidden;
}
.mnote-sidebar-header,
.wolai-sidebar-header {
height: 68px;
padding: 16px 12px 8px;
gap: 10px;
height: 40px;
padding: 8px 12px;
gap: 8px;
}
.wolai-avatar {
width: 32px;
height: 32px;
width: 24px;
height: 24px;
border-radius: 5px;
background: #D6534D;
font-size: 17px;
font-size: 14px;
font-weight: 650;
}
@@ -1058,9 +1091,9 @@ body {
.sidebar-workspace-name {
max-width: 160px;
color: var(--atelier-text);
font-size: 17px;
font-size: 16px;
font-weight: 700;
line-height: 1.1;
line-height: 24px;
}
.wolai-sidebar-chevron {
@@ -1070,8 +1103,8 @@ body {
.wolai-quick-actions {
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 10px;
padding: 8px 16px 26px;
gap: 10.4px;
padding: 5px 8px 11px;
}
.wolai-quick-actions a,
@@ -1080,9 +1113,9 @@ body {
}
.wolai-quick-actions a {
height: 28px;
height: 30px;
color: #3F3D39;
font-size: 19px;
font-size: 14px;
}
.wolai-quick-actions a:hover {
@@ -1099,13 +1132,14 @@ body {
.wolai-section-title,
.wolai-sidebar-tabs {
height: 30px;
height: 32px;
display: flex;
align-items: center;
gap: 6px;
padding: 0 8px;
color: var(--atelier-text-muted);
font-size: 14px;
font-size: 16px;
line-height: 24px;
font-weight: 500;
}
@@ -1237,13 +1271,14 @@ body {
}
.sidebar-tree .tree-row {
height: 29px;
gap: 5px;
margin: 1px 4px;
padding: 0 8px;
height: 32px;
gap: 0;
margin: 0 5px;
padding: 0;
border-radius: 4px;
color: #2D2E2E;
font-size: 14px;
font-size: 16px;
line-height: 24px;
}
.sidebar-tree .tree-row:hover {
@@ -1252,8 +1287,8 @@ body {
.sidebar-tree .tree-row[data-active="true"],
.sidebar-tree .tree-row[data-selected="true"] {
background: var(--atelier-sidebar-active);
color: var(--atelier-text);
background: rgba(255, 71, 71, 0.1);
color: #E0525B;
}
.sidebar-tree .tree-row[data-drop-feedback="true"],
@@ -1265,16 +1300,44 @@ body {
.sidebar-tree .tree-toggle,
.sidebar-tree .tree-spacer {
width: 16px;
height: 22px;
width: 20px;
height: 24px;
color: #B8B5AF;
font-size: 13px;
font-size: 16px;
}
.sidebar-tree .tree-toggle {
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.sidebar-tree .tree-toggle:hover {
background: rgba(27, 28, 28, 0.06);
}
.sidebar-tree .tree-toggle-icon {
width: 20px;
height: 20px;
display: block;
fill: #878787;
transform-origin: 50% 50%;
}
.sidebar-tree .tree-toggle[aria-expanded="true"] .tree-toggle-icon {
transform: rotate(90deg);
}
.sidebar-tree .tree-row[data-active="true"] .tree-toggle-icon {
fill: #CF5659;
}
.sidebar-tree:not([data-tree-shell-mode="filetree"]) .tree-kind-badge[data-kind="page"] {
display: none;
}
.sidebar-tree .tree-kind-badge {
width: 20px;
height: 20px;
@@ -1295,7 +1358,7 @@ body {
}
.sidebar-tree .tree-kind-badge[data-kind="page"]::before {
content: "";
content: "";
}
.sidebar-tree[data-tree-shell-mode="filetree"] .tree-kind-badge[data-kind="page"]::before,
@@ -1333,7 +1396,7 @@ body {
.sidebar-tree .tree-link {
padding: 0 2px;
color: inherit;
font-size: 14px;
font-size: 16px;
}
.sidebar-tree .tree-link-title {
@@ -1341,17 +1404,18 @@ body {
}
.sidebar-tree .tree-row[data-active="true"] .tree-link-title {
font-weight: 500;
font-weight: 400;
}
.sidebar-tree .tree-actions {
gap: 1px;
margin-left: auto;
}
.sidebar-tree .tree-action {
width: 20px;
height: 20px;
color: #9A968F;
width: 24px;
height: 24px;
color: #B3B3B3;
border-radius: 4px;
}
@@ -1363,31 +1427,33 @@ body {
.mnote-tree-context-menu {
position: fixed;
z-index: 1000;
min-width: 236px;
width: 220px;
min-width: 220px;
max-width: min(320px, calc(100vw - 16px));
padding: 6px;
border: 1px solid rgba(27, 28, 28, 0.08);
border-radius: 8px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 16px 32px rgba(27, 28, 28, 0.12);
border-radius: 6px;
background: #FFFFFF;
box-shadow: 0 8px 20px rgba(27, 28, 28, 0.12);
backdrop-filter: blur(24px);
color: var(--atelier-text);
}
.mnote-tree-context-menu__item {
width: 100%;
min-height: 34px;
height: 30px;
min-height: 30px;
display: flex;
align-items: center;
gap: 10px;
padding: 0 9px;
gap: 8px;
padding: 4px 8px;
border: 0;
border-radius: 5px;
background: transparent;
color: inherit;
font: inherit;
font-size: 14px;
line-height: 1.2;
line-height: 22px;
text-align: left;
cursor: pointer;
}
@@ -1439,7 +1505,7 @@ body {
.mnote-tree-context-menu__separator {
height: 1px;
margin: 5px 4px;
margin: 6px 0;
background: rgba(27, 28, 28, 0.08);
}
@@ -1468,7 +1534,7 @@ body {
}
.wolai-sidebar-footer {
border-top: 1px solid rgba(27, 28, 28, 0.08);
border-top: 0;
background: var(--atelier-sidebar);
}
@@ -45,6 +45,8 @@ pub fn build_page_tree_dom_rows(rows: &[PageTreeRenderRow]) -> Vec<PageTreeDomRo
.collect()
}
const PAGE_TREE_CHEVRON_SVG: &str = r#"<svg class="tree-toggle-icon" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><path d="M7.84 14.955c.206 0 .37-.07.505-.21l4.277-4.179a.79.79 0 0 0 .264-.574.78.78 0 0 0-.258-.574L8.35 5.24a.7.7 0 0 0-.51-.21.721.721 0 0 0-.498 1.247l3.814 3.721-3.814 3.709a.721.721 0 0 0 .498 1.248"></path></svg>"#;
fn escape_html(input: &str) -> String {
input
.replace('&', "&amp;")
@@ -54,6 +56,26 @@ fn escape_html(input: &str) -> String {
.replace('\'', "&#39;")
}
fn render_depth_for_node(rows: &[PageTreeRenderRow], node_id: &str, fallback_depth: u32) -> u32 {
if fallback_depth > 0 {
return fallback_depth;
}
let parent_by_id = rows
.iter()
.map(|row| (row.node_id.as_str(), row.parent_node_id.as_deref()))
.collect::<BTreeMap<_, _>>();
let mut depth = 0_u32;
let mut cursor = parent_by_id.get(node_id).and_then(|parent| *parent);
while let Some(parent_id) = cursor {
depth += 1;
cursor = parent_by_id.get(parent_id).and_then(|parent| *parent);
if depth > 32 {
break;
}
}
depth
}
fn render_page_row(
html: &mut String,
row: &PageTreeRenderRow,
@@ -80,11 +102,12 @@ fn render_page_row(
.unwrap_or(false);
let toggle_html = if row.expandable {
format!(
r#"<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="{node_id}" aria-label="{label} {title}">{marker}</button>"#,
r#"<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="{node_id}" aria-label="{label} {title}" aria-expanded="{expanded_state}">{marker}</button>"#,
node_id = escape_html(&row.node_id),
label = if expanded { "折叠" } else { "展开" },
title = escape_html(&row.title),
marker = if expanded { "" } else { "" },
expanded_state = if expanded { "true" } else { "false" },
marker = PAGE_TREE_CHEVRON_SVG,
)
} else {
r#"<span class="tree-spacer" aria-hidden="true"></span>"#.to_string()
@@ -96,15 +119,18 @@ fn render_page_row(
.and_then(|source| source.parent_node_id.as_deref())
.map(|parent_id| format!(r#" data-parent-id="{}""#, escape_html(parent_id)))
.unwrap_or_default();
let render_depth = render_depth_for_node(&input.rows, &row.node_id, row.depth);
html.push_str(&format!(
r#"<li class="tree-node" data-node-id="{node_id}"><div class="tree-row" role="treeitem" aria-level="{aria_level}" aria-expanded="{expanded_attr}" data-rust-rendered-row="page" data-testid="wolai-sidebar-row" data-tree-testid="{test_id}" data-node-id="{node_id}"{parent_attr} data-depth="{depth}" data-shell-mode="page" data-active="{active}" data-focused="{focused}" data-draggable="true" draggable="true" tabindex="{tab_index}">{toggle_html}<span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="{node_id}"><span class="tree-link-title">{title}</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="{node_id}" aria-label="新建子页面">+</button><button type="button" class="tree-action" data-testid="tree-action-rename" data-rust-action="rename" data-node-id="{node_id}" aria-label="重命名">✎</button><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="{node_id}" aria-label="更多操作"></button></div></div>"#,
r#"<li class="tree-node" data-node-id="{node_id}"><div class="tree-row" role="treeitem" aria-level="{aria_level}" aria-expanded="{expanded_attr}" data-rust-rendered-row="page" data-testid="wolai-sidebar-row" data-tree-testid="{test_id}" data-node-id="{node_id}"{parent_attr} data-depth="{depth}" data-shell-mode="page" data-active="{active}" aria-selected="{selected}"{current_attr} data-focused="{focused}" data-draggable="true" draggable="true" tabindex="{tab_index}">{toggle_html}<button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="{node_id}"><span class="tree-link-title">{title}</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="{node_id}" aria-label="更多操作"></button><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="{node_id}" aria-label="新建子页面">+</button></div></div>"#,
node_id = escape_html(&row.node_id),
aria_level = row.depth + 1,
aria_level = render_depth + 1,
expanded_attr = if row.expandable && expanded { "true" } else { "false" },
test_id = row.test_id,
parent_attr = parent_attr,
depth = row.depth,
depth = render_depth,
active = active,
selected = active,
current_attr = if active { r#" aria-current="page""# } else { "" },
focused = focused,
tab_index = if focused { "0" } else { "-1" },
toggle_html = toggle_html,
+57
View File
@@ -0,0 +1,57 @@
# Rust Design Index
更新时间:2026-04-13
适用范围:`/mnt/Data1T/mnote/rust/design`
---
## 1. 当前状态
当前目录只承载已经复制进 `/mnt/Data1T/mnote/rust/` 的核心稳定设计,用于支撑单仓收口后的 Rust 内核落位。
当前已复制:
- `core/01-domain-model-v0.md`
- `core/02-command-query-tool-protocol-v0.md`
- `core/03-storage-event-indexing-v0.md`
- `core/04-onlyoffice-integration-boundary-v0.md`
这些文档对应当前已复制进 `/mnt/Data1T/mnote/rust/crates/` 的 P0 crate
- `core-domain`
- `core-protocol`
- `event-log`
- `storage-convex-bridge`
- `index-fts`
---
## 2. 阅读顺序
后续在 `mnote` 主仓处理 Rust 内核时,建议按以下顺序阅读:
1. `core/01-domain-model-v0.md`
2. `core/02-command-query-tool-protocol-v0.md`
3. `core/03-storage-event-indexing-v0.md`
4. `core/04-onlyoffice-integration-boundary-v0.md`
---
## 3. 当前不在本目录的历史资料
以下资料仍保留在 `/mnt/Data1T/mnote-rust/design/`,当前作为参考,不属于本次第一阶段强制复制范围:
- `blueprint/`
- `phases/`
- `execution/`
- `UI/`
如果后续需要继续把历史阶段文档并入主仓,应在复制前先做去历史化整理,避免把旧阶段叙事原样带入 `mnote` 主仓。
---
## 4. 约定
- `/mnt/Data1T/mnote/design/` 继续承担全局架构与跨系统路线说明。
- `/mnt/Data1T/mnote/rust/design/` 只承载 Rust 内核专属设计。
- 新增 Rust 设计文档时,优先补充这里的索引,再补充 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-plan.md` 的实施状态。
+665
View File
@@ -0,0 +1,665 @@
# 01. Domain Model v0
更新时间:2026-04-11
适用范围:`/mnt/Data1T/mnote-rust`
---
## 1. 目标
本文件定义 `mnote-rust` 的**核心领域模型**。
它要解决的问题不是“前端怎么渲染”,而是:
- 系统里到底有哪些一等对象
- 哪些对象是事实层真相,哪些只是派生视图
- AI / CLI / Web / Desktop 应该围绕什么稳定对象工作
- OnlyOffice、Mindmap、OCR、RAG 这类能力应挂在哪一层
本文件优先保证:
- 长期稳定
- 脱离 UI 依赖
- 适合 Rust 内核实现
- 适合 AI 通过结构化协议读写
---
## 2. 建模原则
### 2.1 事实与视图分离
以下对象属于**事实层**
- Workspace
- Page
- Block
- Asset
- Reference
- Task
- CommandLog
- Event
- AgentSession
以下对象默认属于**派生层 / 视图层**:
- 目录(TOC
- 反链聚合
- 搜索结果列表
- Mindmap 视图树
- OnlyOffice 当前光标/当前页/当前选区
- AI 面板当前消息列表 UI 状态
原则:
> 派生层可以重建;事实层必须稳定、可审计、可迁移。
### 2.2 人与 AI 共用同一套领域对象
不能做人类一套模型、AI 一套模型。
要求:
- 人类编辑页面,本质是修改 `Page / Block / Asset / Reference`
- AI 编辑页面,本质也是修改同样的对象
- CLI 批处理、同步任务、导入器也修改同样的对象
### 2.3 第三方编辑器不是事实源
Block editor、OnlyOffice、mindmap editor 都不是系统真相。
它们只能是:
- 某类对象的视图/交互适配器
- 输入输出变换器
- 外部文档能力宿主
### 2.4 结构化修改优先
优先通过:
- 显式对象
- 显式字段
- 显式命令
- 显式 ops
避免通过:
- UI 状态猜测
- 整文全文字符串替换
- 第三方编辑器内部瞬时结构直接落库
---
## 3. 对象总览
```text
Workspace
├─ Page
│ ├─ DocumentBody
│ │ └─ Block*
│ ├─ PageProperty*
│ ├─ PageLink*
│ └─ Snapshot*
├─ Asset*
├─ Task*
├─ AgentSession*
├─ CommandLog*
└─ Event*
Reference
├─ block -> block
├─ block -> page
├─ block -> asset-fragment
├─ page -> asset
└─ task -> page/block/asset
```
---
## 4. Workspace
`Workspace` 是系统的顶层容器。
建议字段:
- `workspace_id`
- `slug`
- `title`
- `created_at`
- `updated_at`
- `owner_actor_id`
- `default_locale`
- `storage_policy`
- `sync_policy`
- `feature_flags`
- `archived_at`
职责:
- 隔离页面、资产、任务、日志
- 挂载存储策略与同步策略
- 作为权限边界与导出边界
不负责:
- 具体页面内容
- UI 布局
- 当前用户会话态
---
## 5. Page
`Page` 是笔记系统的主内容容器。
### 5.1 Page 的定位
它不是“某个编辑器文件”,而是:
- 一条知识对象
- 一个文档入口
- 一个块树正文容器
- 一个可被引用、索引、审计的实体
### 5.2 建议字段
- `page_id`
- `workspace_id`
- `parent_page_id`(允许页面树)
- `title`
- `slug`
- `icon`
- `cover_asset_id`
- `body_root_block_id`
- `page_type``note | doc | database_record | inbox | template | system`
- `status``active | archived | deleted`
- `created_at`
- `updated_at`
- `last_edited_at`
- `last_edited_by`
- `current_revision`
### 5.3 PageProperty
页面属性不应和前端表格视图绑死。
建议独立对象:
- `property_key`
- `value_type`
- `value`
- `display_hint`
- `source`
可用于:
- 标签
- 时间
- 状态
- 优先级
- 自定义结构化元数据
---
## 6. DocumentBody 与 Block
### 6.1 为什么以 Block 为正文主模型
因为从既有 `mnote` / `mnote-next` 的稳定人层语义看,真正稳定的是:
- 单块编辑
- 在某块后插入块
- 删除块
- 重排块
- 缩进层级
- 页面引用与块引用占位
- 复杂块占位
这说明“块”比“整篇富文本 JSON”更接近系统真实操作单元。
### 6.2 Block 建议字段
- `block_id`
- `workspace_id`
- `page_id`
- `parent_block_id`
- `prev_block_id`
- `next_block_id`
- `sort_key`
- `block_type`
- `content`
- `props`
- `annotations`
- `status`
- `created_at`
- `updated_at`
- `created_by`
- `updated_by`
- `revision`
### 6.3 BlockType 建议
最小集合建议:
- `paragraph`
- `heading`
- `bulleted_list_item`
- `numbered_list_item`
- `todo`
- `quote`
- `code_block`
- `divider`
- `callout`
- `page_reference`
- `block_reference`
- `embed_asset`
- `embed_view`
- `embed_onlyoffice`
- `embed_mindmap`
- `embed_table`
- `system_placeholder`
原则:
- 类型应描述语义,不描述某个前端组件名
- `embed_*` 表示“外部能力挂件”,不是事实层自己变成那个系统
### 6.4 Block.content
建议:
- 文本类块使用结构化 inline span 序列
- 避免只存单纯 HTML
- 避免直接依赖第三方编辑器私有 schema
建议形态:
```json
{
"spans": [
{ "type": "text", "text": "hello" },
{ "type": "page_ref", "page_id": "page_xxx", "title": "项目计划" },
{ "type": "text", "text": " world" }
]
}
```
### 6.5 Block.props
只放结构化、有限、可校验字段,例如:
- `level`
- `checked`
- `collapsed`
- `indent`
- `language`
- `align`
- `width`
- `height`
- `asset_id`
- `view_id`
避免把任意 UI 状态塞进 props。
---
## 7. Asset
`Asset` 是所有非正文主块树内容的统一挂载对象。
### 7.1 Asset 范围
包括:
- 图片
- PDF
- docx
- xlsx
- pptx
- 音频
- 视频
- 导出文件
- OCR 中间产物
- 结构提取结果
### 7.2 建议字段
- `asset_id`
- `workspace_id`
- `storage_key`
- `original_name`
- `mime_type`
- `size_bytes`
- `checksum`
- `origin`upload / import / generated / synced
- `asset_kind`image / pdf / office / audio / video / binary / extracted_text
- `created_at`
- `updated_at`
- `created_by`
- `status`
- `latest_version_id`
### 7.3 AssetVersion
附件应支持版本化。
建议:
- `asset_version_id`
- `asset_id`
- `version_no`
- `storage_key`
- `checksum`
- `derived_from_version_id`
- `created_at`
- `created_by`
- `change_reason`
这样才能支撑:
- OnlyOffice 编辑回写
- OCR 重跑
- 导入转换
- AI 修改资产派生内容
---
## 8. Reference
`Reference` 是知识型系统的关键对象,不能只做正文里的临时 token。
### 8.1 Reference 范围
- 块引用块
- 页面引用
- 资产片段引用
- PDF 页码引用
- Office 书签/段落锚点引用
- URL 引用
- 检索结果引用
### 8.2 建议字段
- `reference_id`
- `workspace_id`
- `source_object_type`
- `source_object_id`
- `target_object_type`
- `target_object_id`
- `anchor`
- `label`
- `snippet`
- `confidence`
- `created_at`
- `created_by`
- `ref_kind`
### 8.3 Anchor
`anchor` 建议作为独立结构:
```json
{
"kind": "pdf_page",
"page": 12,
"bbox": null,
"text_quote": "..."
}
```
或:
```json
{
"kind": "office_bookmark",
"bookmark": "Heading_3",
"text_quote": "..."
}
```
原则:
- 锚点是系统对象,不是编辑器私有游标
- 锚点失效时要能标记 stale,而不是静默消失
---
## 9. SelectionAnchor
`SelectionAnchor` 不是长期事实主对象,但在 Agent / 编辑器桥接中有价值。
它表示:
- 当前选区
- 当前光标
- 当前活动页
- 当前资产中的定位点
建议定位为:
- **短生命周期上下文对象**
- 可存入 AgentSession / UI Session
- 默认不直接作为主事实落库
因为:
- 它变化太频繁
- 容易和编辑器宿主耦合
- 更适合作为命令输入上下文
---
## 10. Task
`Task` 既是用户任务,也是 AI 工作单元。
### 10.1 建议字段
- `task_id`
- `workspace_id`
- `title`
- `description`
- `task_type`
- `status`
- `priority`
- `assignee_actor_id`
- `source_page_id`
- `source_block_id`
- `input_payload`
- `output_payload`
- `created_at`
- `updated_at`
- `due_at`
- `completed_at`
### 10.2 为什么纳入领域模型
因为 AI 长期介入后,很多能力不是即时编辑,而是:
- 导入任务
- 提取任务
- OCR 任务
- 摘要任务
- 索引重建任务
- 同步任务
这些都应进入系统内核,而不是散在外部脚本里。
---
## 11. AgentSession
`AgentSession` 表示某次 AI 介入的上下文单元。
### 11.1 建议字段
- `agent_session_id`
- `workspace_id`
- `provider`
- `model`
- `initiator_actor_id`
- `status`
- `started_at`
- `updated_at`
- `ended_at`
- `tool_policy`
- `confirmation_policy`
- `summary`
### 11.2 不应存什么
不应把整份聊天 UI 状态直接当作领域模型核心。
建议分开:
- 领域层只保留会话元信息、工具调用摘要、事件链路
- 详细消息可作为附属日志或导出工件保存
---
## 12. CommandLog 与 Event
### 12.1 CommandLog
代表一次显式操作请求。
建议字段:
- `command_id`
- `workspace_id`
- `command_name`
- `actor`
- `source`
- `target`
- `payload_hash`
- `reason`
- `refs`
- `idempotency_key`
- `dry_run`
- `status`
- `requested_at`
- `finished_at`
- `error_code`
- `error_message`
### 12.2 Event
代表命令执行后产生的事实变化。
建议字段:
- `event_id`
- `workspace_id`
- `command_id`
- `event_type`
- `object_type`
- `object_id`
- `before_revision`
- `after_revision`
- `payload`
- `created_at`
原则:
- 命令与事件分离
- 先有意图,再有变化
- 审计必须能回答“谁因为什么改了什么”
---
## 13. View / Derived Object
以下对象建议明确标注为派生对象,不直接当真相:
- TOCView
- BacklinkView
- SearchHit
- KnowledgeGraphView
- MindmapProjection
- OnlyOfficeProjection
- PagePreview
- DailyDigest
它们的共同特点:
- 来自事实层投影
- 可以缓存
- 可以失效
- 可以重建
---
## 14. Mindmap 的正确定位
从既有设计看,Mindmap 很适合走结构化 `ops` 协议。
但在 `mnote-rust` 中,建议将其定义为:
- `Asset``ViewProjection` 的一种
- 由 `Page / Block / Reference / Asset` 投影生成或承载
- 支持独立存储其节点视图结构
- 但不让它成为全系统唯一知识主模型
建议:
- 若是“页面内导图块”,则作为 `embed_mindmap` block 挂载
- 若是“独立导图资产”,则作为 `Asset(kind=mindmap)`
- AI 修改导图时,走 `MindmapOp[]`,但最终仍写入系统命令日志
---
## 15. OnlyOffice 的正确定位
OnlyOffice 不应被建模为“Page 本体”,应被建模为:
- `Asset(kind=office)`
- `AssetVersion`
- `OnlyOfficeAdapterSession`
- `SelectionAnchor(kind=office_*)`
- `Reference(anchor=office_*)`
也就是说:
- Office 文档是系统资产
- OnlyOffice 是该资产的一种编辑适配器
- 编辑产生新版本与事件
- AI 可以通过插件/适配器读当前选区、插入内容、替换内容
- 但系统真相依然在你的领域模型与资产版本链里
---
## 16. v0 必须避免的错误
### 16.1 不要把前端 store 当领域模型
例如:
- 当前页面 UI 选中态
- 面板展开态
- 聊天输入框草稿
这些不应进入核心领域。
### 16.2 不要把第三方编辑器 JSON 当系统法典
否则未来:
- 替换编辑器会非常痛苦
- AI 只能学编辑器私有结构
- CLI 难以稳定操作
### 16.3 不要把全文字符串替换当主修改方式
必须保留结构化修改能力。
### 16.4 不要把向量库结果当事实对象
向量结果是检索派生物,不是知识真相。
---
## 17. 一句话结论
`mnote-rust` 的领域模型应以 **Workspace / Page / Block / Asset / Reference / Task / AgentSession / CommandLog / Event** 为核心;
Mindmap、OnlyOffice、搜索结果、AI 面板都只是围绕这些核心对象形成的投影、适配器或任务系统,而不能反客为主。
@@ -0,0 +1,717 @@
# 02. Command / Query / Tool Protocol v0
更新时间:2026-04-11
适用范围:`/mnt/Data1T/mnote-rust`
---
## 1. 目标
本文件定义 `mnote-rust` 的统一协议面。
目标只有一个:
> 让 Web、Desktop、CLI、Agent、批处理任务都通过**同一套正式入口**操作系统。
它要解决的问题:
- 怎样避免“每个页面、每个组件、每个 API route 都有自己的写法”
- 怎样让 AI 不依赖 UI 模拟,而能直接操作笔记软件
- 怎样保证命令可审计、可回放、可测试、可维护
---
## 2. 基本原则
### 2.1 写与读必须分离
- **Command**:会改变事实层
- **Query**:只读,不改变事实层
- **Tool**:面向 AI / CLI 的能力暴露层,内部映射到 Command / Query / Job
### 2.2 Tool 不是第三套业务逻辑
禁止出现:
- Web 走一套逻辑
- CLI 走一套逻辑
- AI Tool 再写一套逻辑
正确关系:
```text
Tool -> Command / Query / Job -> Domain + Storage + EventLog
```
### 2.3 每条写命令都要可审计
至少要记录:
- 谁发起
- 从哪里发起
- 改了什么
- 为什么改
- 影响了哪些对象
- 是否成功
### 2.4 AI 默认只能调用系统工具
AI 不应默认获得:
- 任意 SQL
- 任意文件写入
- 任意 UI 按钮点击
- 任意内部未定型方法
AI 应调用:
- 稳定的 Tool API
- 明确的参数模型
- 明确的权限模型
---
## 3. 三层协议面
```text
Human UI / CLI / Agent Runtime
Tool API
┌───────┴────────┐
▼ ▼
Query API Command API
│ │
└───────┬────────┘
Job / Workflow API
Rust Note Core
```
说明:
- Tool API 是面向使用者的稳定能力面
- Command / Query API 是内核的正式操作面
- Job / Workflow API 负责长任务、异步流程、外部能力编排
---
## 4. Actor 模型
所有命令、查询、工具调用都应带 `actor`
建议结构:
```json
{
"actor": {
"type": "human",
"id": "user_123",
"session_id": "ui_session_xxx"
}
}
```
或:
```json
{
"actor": {
"type": "agent",
"id": "agent_session_456",
"provider": "openai",
"model": "gpt-5.4-mini"
}
}
```
或:
```json
{
"actor": {
"type": "system",
"id": "job_runner"
}
}
```
用途:
- 审计
- 权限判断
- 限流
- 错误追踪
---
## 5. Command API
## 5.1 通用结构
建议所有命令统一壳:
```json
{
"command": "insert_block",
"command_id": "cmd_01",
"idempotency_key": "idem_01",
"actor": {
"type": "agent",
"id": "agent_session_1"
},
"source": {
"channel": "tool",
"client": "mcp"
},
"target": {
"workspace_id": "ws_1",
"page_id": "page_1"
},
"payload": {},
"reason": "补充总结段落",
"refs": ["ref_1"],
"dry_run": false,
"validate_only": false,
"requested_at": "2026-04-11T12:00:00Z"
}
```
### 5.2 通用返回
```json
{
"ok": true,
"command_id": "cmd_01",
"event_ids": ["evt_1", "evt_2"],
"affected_objects": [
{ "type": "block", "id": "block_123" }
],
"revision": {
"workspace_id": "ws_1",
"page_id": "page_1",
"value": 42
},
"warnings": [],
"rollback_hint": {
"kind": "compensating_command",
"command": "delete_block",
"target_id": "block_123"
}
}
```
### 5.4 观测字段约束
从 task-034 开始,所有正式 `Command / Query / Tool / Job` 面都要默认带上并统一解释下面这些字段:
- `request_id`:一次入口请求级别的稳定编号,用于串起 route、runtime、日志和回查接口
- `trace_id`:一次完整调用链的稳定编号,用于跨 query / command / tool / job 串联同一条执行路径
- `command_id`:写命令的唯一编号,也是 `command_log_id``event_id` 推导的基础
- `idempotency_key`:CLI、AI、Web 共享的幂等语义主键,不能三端各自解释
- `command_log_id` / `event_id`:进入审计与回放链后的稳定对象编号,供 request/trace/command 回查与恢复命令复用
如果某条能力不能带出这组字段,它就还不能算“可供 CLI 与 AI 稳定运行的正式协议面”。
### 5.3 命令分类
#### 页面类
- `create_page`
- `rename_page`
- `move_page`
- `archive_page`
- `restore_page`
- `set_page_property`
#### 块类
- `insert_block`
- `update_block`
- `delete_block`
- `move_block`
- `batch_apply_block_ops`
- `replace_block_content`
#### 引用类
- `create_reference`
- `remove_reference`
- `rebind_reference_anchor`
#### 资产类
- `attach_asset`
- `replace_asset_version`
- `set_asset_metadata`
- `extract_asset_outline`
#### 工作区 / 系统类
- `create_task`
- `cancel_task`
- `rebuild_search_index`
- `run_import_job`
- `run_export_job`
---
## 6. Command 设计约束
### 6.1 命令应小而明确
优先:
- `insert_block`
- `move_block`
- `set_page_property`
避免:
- `save_everything`
- `update_editor_state`
- `mutate_page_with_ui_payload`
### 6.2 支持幂等键
AI 可能重试、网络可能重放,因此:
- 所有外部写命令建议支持 `idempotency_key`
- 同一作用域内重复提交应可去重
### 6.3 支持 dry-run / validate-only
这样 AI 可以先问:
- 这个操作是否合法?
- 会影响哪些对象?
- 是否需要确认?
这对 Agent 很重要。
### 6.4 支持批量 ops,但要有边界
对于 Mindmap、结构调整、导入转换,可以提供:
- `batch_apply_block_ops`
- `apply_mindmap_ops`
- `apply_document_transform_ops`
但要求:
- ops 必须结构化
- 单条失败策略明确
- 返回逐条结果
---
## 7. Query API
## 7.1 通用结构
```json
{
"query": "page.get",
"actor": {
"type": "human",
"id": "user_1"
},
"scope": {
"workspace_id": "ws_1"
},
"params": {
"page_id": "page_1"
}
}
```
### 7.2 查询分类
#### 页面与块
- `page.get`
- `page.tree`
- `page.list_children`
- `block.get`
- `block.subtree`
#### 检索
- `search.text`
- `search.reference`
- `search.asset`
- `search.page_by_title`
#### 资产
- `asset.get`
- `asset.list_versions`
- `asset.resolve_anchor`
#### 日志与会话
- `command_log.list`
- `command_log.get`
- `event.list`
- `agent_session.get`
- `task.list`
### 7.3 输出要求
- 输出稳定、字段可预期
- 支持分页、游标、过滤
- 避免直接返回前端组件所需临时形态
- 对大型对象支持摘要与展开模式
---
## 8. Tool API
## 8.1 Tool API 的职责
Tool API 是给:
- AI Agent
- CLI
- 自动化脚本
- 外部集成方
用的能力层。
它的原则是:
- 工具名清晰
- 参数结构明确
- 返回格式稳定
- 明确确认策略与权限策略
## 8.2 工具命名风格
建议按领域分组:
- `note.read_page`
- `note.list_children`
- `note.insert_block`
- `note.update_block`
- `note.move_block`
- `note.search`
- `asset.attach_file`
- `asset.extract_outline`
- `office.get_selection`
- `office.replace_selection`
- `mindmap.apply_ops`
- `system.get_recent_commands`
也可以对外映射为 MCP 风格扁平命名,但内核层建议保留层级语义。
## 8.3 Tool 与 Command/Query 的映射
例如:
- `note.read_page` -> `page.get`
- `note.insert_block` -> `insert_block`
- `note.search` -> `search.text`
- `asset.attach_file` -> `attach_asset`
- `mindmap.apply_ops` -> `apply_mindmap_ops`
### 8.4 Tool 返回
建议统一包含:
- `ok`
- `data`
- `warnings`
- `requires_confirmation`
- `confirmation_reason`
- `next_actions`
例如:
```json
{
"ok": true,
"data": {
"page_id": "page_1",
"title": "项目计划"
},
"warnings": [],
"requires_confirmation": false,
"next_actions": []
}
```
---
## 9. Job / Workflow API
有些操作不是同步命令,而是长任务。
例如:
- 导入大型 PDF
- OCR
- 构建索引
- 批量转换文档
- Office 文件解析
- 全库引用修复
- AI 长链路整理
因此需要 `Job API`
### 9.1 Job 通用结构
```json
{
"job": "import_document",
"job_id": "job_1",
"actor": { "type": "agent", "id": "agent_1" },
"input": {
"asset_id": "asset_1"
}
}
```
### 9.2 Job 状态
- `queued`
- `running`
- `waiting_confirmation`
- `succeeded`
- `failed`
- `cancelled`
### 9.3 Job 输出
- 结果对象
- 生成的 page / asset / reference
- 日志摘要
- 错误详情
---
## 10. Confirmation Policy
不是所有工具都应直接执行。
建议分级:
### 10.1 无需确认
- 只读查询
- 本地摘要生成
- 小范围插入草稿块
### 10.2 建议确认
- 批量删除
- 批量重排
- 覆盖资产版本
- 导出到外部位置
### 10.3 强制确认
- 大范围页面删除
- 全库重建/迁移
- 覆盖性导入
- 对外同步/发布
确认策略应由内核或 policy 层判断,不能完全由前端决定。
---
## 11. Error Model
错误必须结构化。
建议格式:
```json
{
"ok": false,
"error": {
"code": "BLOCK_NOT_FOUND",
"message": "目标块不存在",
"details": {
"block_id": "block_xxx"
},
"retryable": false,
"suggested_action": "请先刷新页面树或重新读取块子树"
}
}
```
分类建议:
- `VALIDATION_ERROR`
- `PERMISSION_DENIED`
- `NOT_FOUND`
- `CONFLICT`
- `STALE_REVISION`
- `RATE_LIMITED`
- `EXTERNAL_ADAPTER_ERROR`
- `INTERNAL_ERROR`
这样 AI 才能根据错误做下一步,而不是只看到一段字符串。
---
## 12. Revision / Concurrency
### 12.1 为什么必须有 revision
AI 与人可能同时修改。
如果没有 revision
- 很容易覆盖彼此内容
- 工具重试会写乱
- 无法做冲突判断
### 12.2 建议
- `Page` 级 revision
- `Block` 级 revision
- `AssetVersion` 级 version_no
命令可支持:
- `expected_revision`
- `on_conflict` 策略(fail / merge_if_possible / create_patch
---
## 13. OnlyOffice 协议边界
OnlyOffice 不应直接暴露为“让 AI 操控 iframe”。
建议拆成两层:
### 13.1 Office Adapter Query
- `office.get_active_asset`
- `office.get_selection`
- `office.get_current_page`
- `office.list_bookmarks`
### 13.2 Office Adapter Command
- `office.insert_text`
- `office.replace_selection`
- `office.insert_comment`
- `office.save_as_new_version`
- `office.extract_outline`
原则:
- Tool 面暴露的是“Office 文档能力”
- 不是浏览器点击坐标或 iframe DOM
- 若插件不可用,应返回结构化不可用错误
---
## 14. Mindmap 协议边界
继续保留既有设计里最有价值的部分:`ops`
建议:
- `mindmap.get`
- `mindmap.apply_ops`
- `mindmap.expand_node`
- `mindmap.attach_refs`
其中 `mindmap.apply_ops` 入参可以继续沿用:
- `addChild`
- `addSiblingAfter`
- `updateText`
- `setHyperlink`
- `setRefs`
- `appendNote`
- `deleteNode`
但要求:
- 所有调用都写入统一命令日志
- mindmap 不再走单独一套野生落盘体系
---
## 15. CLI 协议面
CLI 不是开发附属品,而是系统正式壳层。
建议 CLI 优先覆盖:
- `mnote page get <page-id>`
- `mnote page tree`
- `mnote block insert`
- `mnote block move`
- `mnote search text`
- `mnote asset attach`
- `mnote office selection`
- `mnote job run import-document`
- `mnote log recent`
CLI 应满足:
- 输出 JSON 模式
- 退出码稳定
- 适合 shell / AI / 自动化脚本调用
---
## 16. MCP / Agent Runtime 暴露面
如果后续提供 MCP
- MCP 工具层应直接包装 Tool API
- 不应重新发明一套独立业务逻辑
- 返回值尽量和 CLI JSON 输出接近
这样好处是:
- Web agent、桌面 agent、本地 Hermes/Codex 都共用一套系统能力
- 文档里只需维护一份工具协议
---
## 17. 最小 v0 清单
建议 v0 最先固化以下协议:
### Command
- `create_page`
- `rename_page`
- `insert_block`
- `update_block`
- `delete_block`
- `move_block`
- `attach_asset`
- `create_reference`
### Query
- `page.get`
- `page.tree`
- `block.subtree`
- `search.text`
- `asset.get`
- `command_log.list`
### Tool
- `note.read_page`
- `note.insert_block`
- `note.update_block`
- `note.move_block`
- `note.search`
- `asset.attach_file`
- `system.get_recent_commands`
这套足以先打通:
- 人类基础编辑
- CLI 基础操作
- AI 基础介入
- 审计与回放链路
---
## 18. 一句话结论
`mnote-rust` 的协议设计应坚持:
> **Command 负责改,Query 负责读,Tool 负责给 AI/CLI 用,Job 负责长流程;所有入口最终收敛到同一 Rust 内核,不再允许页面私有接口、编辑器私有接口、临时胶水接口长期并存。**
@@ -0,0 +1,622 @@
# 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 内核负责把命令、查询、工具、索引、异步任务与外部能力重新组织清楚,而不是再造一套与现有主线并列的数据库事实源。
@@ -0,0 +1,491 @@
# 04. OnlyOffice Integration Boundary v0
更新时间:2026-04-11
适用范围:`/mnt/Data1T/mnote-rust`
---
## 1. 目标
本文件定义 `mnote-rust` 中 OnlyOffice 的正式边界。
它要解决的问题:
- OnlyOffice 在系统里到底是什么
- 可以接多深
- 哪些能力该放进插件/编辑器侧
- 哪些能力必须留在 Rust 内核
- 官方现状已经支持到哪一步,哪些不是猜想
---
## 2. 基于官方资料的确定事实
以下判断基于 ONLYOFFICE 官方文档与官方 API 文档,而不是推测。
### 2.1 ONLYOFFICE 已有官方 AI 插件能力
官方文档已明确:
- ONLYOFFICE 提供 **AI 插件**
- 可接入多种模型提供商,例如 **OpenAI、DeepSeek**
- 支持的能力包括但不限于:
- 文本生成
- 文本编辑
- 总结
- 自动创建宏
- 面向编辑器的 AI 辅助操作
这说明:
> OnlyOffice 不是一个“完全不懂 AI 的富文档编辑器”,而是已经具备官方 AI 扩展体系。
### 2.2 ONLYOFFICE 已有插件系统
官方文档明确支持:
- 自定义插件
- 插件 UI 集成
- 插件调用外部服务
- 插件与编辑器文档内容交互
- 插件注册、安装、配置与运行
这说明后续你完全可以:
- 做自有 AI 插件
- 做面向 mnote-rust 的桥接插件
- 把系统命令面引入编辑器环境
### 2.3 Office JS API 已能做深度文档操作
官方 API 文档与样例表明,插件/脚本可进行:
- 获取文档对象 `Api.GetDocument()`
- 获取当前选区/范围
- 获取文本内容
- 插入文本
- 替换内容
- 创建段落
- 插入内容控件
- 处理表格、图片、表单等对象
这意味着:
> “AI 在 OnlyOffice 内获取当前选区并改写当前文档片段”这条能力链,官方已经支持。
### 2.4 ONLYOFFICE 支持自建部署与集成
官方文档明确把 ONLYOFFICE Docs 作为可集成到自有系统中的 office suite / Docs API / 插件宿主来提供。
结合你当前仓库里已有:
- Docker 自建 `onlyoffice/documentserver`
- callback / proxy / 外网地址改写
- 前端侧 OnlyOffice 工具桥
可以判断:
> 自建部署这条路是成立的,而且值得继续做。
---
## 3. 由这些事实推出的边界判断
## 3.1 能做深集成,但不能反客为主
结论:
- **OnlyOffice 适合深集成**
- **OnlyOffice 不适合做系统主内核**
原因不是它能力不够,而是职责不同。
OnlyOffice 擅长:
- 编辑 office 文档
- 处理复杂版式文档
- 承载编辑器插件与文档内 AI
- 管理编辑器态的操作命令
但它不擅长天然承担:
- 笔记块树真相
- 全局页面树真相
- 跨页面引用网络真相
- AI 全局任务编排
- 统一命令总线
- 全系统事件日志中心
## 3.2 在 mnote-rust 中的正式定位
OnlyOffice 应定位为:
> **Office Asset Editor Adapter + Office AI Subsystem**
而不是:
- 主页面编辑器内核
- 主数据模型
- 主命令入口
- 主任务系统
---
## 4. 正式职责划分
## 4.1 OnlyOffice 负责什么
### A. Office 资产编辑
负责编辑:
- `docx`
- `xlsx`
- `pptx`
- 部分 `pdf` 相关工作流
### B. 编辑器内局部 AI 能力
例如:
- 基于当前选区总结
- 重写/润色选区文本
- 插入生成内容
- 提取 action items
- 生成标题/批注/宏
### C. 编辑器上下文采集
例如:
- 当前文档 ID
- 当前资产版本 ID
- 当前选区
- 当前页/书签/活动对象
- 编辑器保存状态
### D. 将编辑结果回写主系统
例如:
- 保存为新 `AssetVersion`
- 产出书签/目录/片段锚点
- 触发提取文本与索引任务
## 4.2 Rust 内核负责什么
### A. 主事实层
- Workspace
- Page
- Block
- Asset
- AssetVersion
- Reference
- Task
- Event
- CommandLog
### B. 系统级命令与权限
- 谁可以编辑哪个资产
- 谁可以覆盖哪个版本
- 哪些操作需要确认
- 哪些任务需要后台执行
### C. 全局 AI 调度
- Agent Session
- Tool Registry
- CLI / MCP / Tool API
- 长任务编排
- 审计与回放
### D. 跨资产与跨页面知识组织
- 引用图
- 搜索索引
- 反链
- RAG
- 页面/块/资产统一导航
---
## 5. 最推荐的集成方式
## 5.1 推荐结构
```text
AI / CLI / Web / Desktop
Rust Command / Query / Tool API
├─ Asset Service
├─ Agent Service
├─ Search / Reference Service
└─ OnlyOffice Adapter Service
├─ Docs API
├─ Callback Handler
├─ Plugin Bridge
└─ Selection / Save / Anchor Bridge
```
核心含义:
- 所有人和 AI 都先找 Rust 内核
- Rust 内核再协调 OnlyOffice
- OnlyOffice 不是上帝,只是一个能力域
## 5.2 不推荐结构
```text
AI -> OnlyOffice Plugin -> 直接改系统数据库/页面状态
```
或者:
```text
Web 页面逻辑 -> 直接控制 OnlyOffice -> 顺手写一部分业务真相
```
这种结构会再次回到胶水冲突。
---
## 6. 官方 AI 能力在新系统中的用法
## 6.1 可以直接利用的部分
可以利用官方插件/Office API 做:
- `office.get_selection`
- `office.get_document_text`
- `office.replace_selection`
- `office.insert_after_selection`
- `office.create_comment_from_ai`
- `office.generate_outline`
- `office.extract_action_items`
这些能力适合做成:
- 插件内命令
- 本地桥接命令
- Tool API 的 office 子域工具
## 6.2 不应依赖的部分
不应把以下能力外包给 OnlyOffice:
- 页面树管理
- 笔记块模型
- 全局知识链接
- Agent 全局工作记忆
- 系统统一任务编排
- 跨类型对象权限控制
---
## 7. 推荐的 Tool API 分工
## 7.1 Rust 内核对外工具
例如:
- `asset.open_in_office`
- `asset.list_versions`
- `asset.commit_new_version`
- `reference.resolve_anchor`
- `office.create_edit_session`
- `office.get_capabilities`
## 7.2 OnlyOffice 插件桥工具
例如:
- `office_plugin.get_selection`
- `office_plugin.get_document_text`
- `office_plugin.replace_selection`
- `office_plugin.insert_text`
- `office_plugin.get_outline`
- `office_plugin.list_bookmarks`
## 7.3 正确的调用链
```text
Agent / CLI
-> note/asset/office Tool API
-> Rust 内核
-> OnlyOffice Adapter / Plugin Bridge
-> Office JS API / Docs API
-> 返回结构化结果
```
而不是:
```text
Agent 直接知道 OnlyOffice 内部细节并自行编排保存流程
```
---
## 8. 版本回写策略
这是边界里最关键的一块。
### 8.1 正确做法
OnlyOffice 编辑完成后:
- 生成新的 `AssetVersion`
- 更新 `assets.current_version_id`
- 写 `command_log` / `event`
- 触发:
- 文本提取
- 大纲提取
- 锚点更新
- 索引更新
### 8.2 不正确做法
- 只在编辑器里“看起来改了”但系统无版本记录
- callback 到了就直接覆盖文件,不产生日志
- 直接把插件临时状态写成业务真相
---
## 9. Anchor / 选区 / 书签策略
OnlyOffice 的一个真正价值,是它能提供相对更强的文档内部定位。
建议区分两类定位:
### 9.1 短期编辑上下文
例如:
- 当前选区
- 当前光标
- 当前页
- 当前激活对象
这类只作为:
- `SelectionAnchor`
- AgentSession 上下文
- Tool 输入
### 9.2 长期可引用锚点
例如:
- bookmark
- 标题路径
- 批注 id
- 内容控件 id
- 提取片段 hash
这类才适合进正式 `Reference.anchor`
原则:
- 选区是瞬时上下文
- 书签/内容控件/稳定片段才是长期引用对象
---
## 10. 插件开发建议
## 10.1 建议做自有插件桥
理由:
- 官方插件体系已经成熟到可用
- 你需要的是“把 OnlyOffice 接进自己的命令系统”
- 自有桥插件比零散页面 hack 更稳
## 10.2 插件职责建议
插件只做:
- 获取编辑器上下文
- 执行局部编辑命令
- 请求 mnote-rust Tool API
- 接收结构化结果并落到文档
- 回传选择/书签/状态
插件不要做:
- 自己维护主业务状态
- 自己做全局权限判断
- 自己做全局任务调度
- 自己缓存长期知识图谱
---
## 11. 源码部署 / 自建部署的实际建议
## 11.1 结论
**值得继续推进自建部署。**
## 11.2 原因
- 网络、callback、鉴权策略可控
- 便于把插件、配置、回调、存储策略收归自己管理
- 更方便未来与桌面版、本地模式、离线模式衔接
- 减少外部部署差异导致的集成复杂度
## 11.3 但不建议的误区
- 不要把“自建部署”误解为“去重写编辑器引擎”
- 不要把“插件可改内容”误解为“OnlyOffice 可以替代整个笔记内核”
- 不要把“官方已有 AI 插件”误解为“你的 Agent 架构可以省掉”
---
## 12. 对 mnote-rust 的直接影响
这份结论会直接影响后续架构文档:
1. `office.*` 作为独立工具域存在
2. `AssetVersion` 必须是一等对象
3. `Reference.anchor` 必须支持 office bookmark / content control / text quote
4. `SelectionAnchor` 只做短期上下文,不升格为主事实
5. Agent 与 OnlyOffice 的关系必须经由 Rust Tool API,而不是页面硬连
---
## 13. 最小落地路线
### Phase A
- 自建 ONLYOFFICE Docs 持续使用
- 补正式 adapter 文档
- 补 `AssetVersion` / callback / save 流程
### Phase B
- 做自有桥插件
- 打通选区读取、文本替换、结构化命令执行
- 统一插件与系统鉴权
### Phase C
- 把 office 工具域接入统一 Tool API / CLI / Agent Runtime
- 让 AI 通过系统命令操作 Office 资产
- 建立锚点、版本、索引、审计闭环
### Phase D
- 再评估是否要更深度定制 UI / 插件体验
- 但不进入“重写 OnlyOffice 内核”路线
---
## 14. 一句话结论
基于 ONLYOFFICE 官方当前能力,最合理的路线不是回避它,也不是围绕它建系统,而是:
> **把 ONLYOFFICE 作为可自建、可插件化、可 AI 深接入的 Office 资产编辑子系统;同时把主事实层、命令层、日志层、任务层牢牢放在 Rust 内核里。**
-4
View File
@@ -1,4 +0,0 @@
{
"status": "failed",
"failedTests": []
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB