diff --git a/.gitignore b/.gitignore index 276c84a3..71a65d25 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/AGENTS.md b/AGENTS.md index abf91a85..df0b4b18 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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。 diff --git a/design/01-05-current-priority-overview.md b/design/01-05-current-priority-overview.md new file mode 100644 index 00000000..3e8a0e95 --- /dev/null +++ b/design/01-05-current-priority-overview.md @@ -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 和树域产品执行面。** diff --git a/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md b/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md new file mode 100644 index 00000000..2f5dc6c4 --- /dev/null +++ b/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md @@ -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 Kernel;Mindmap 只是其中一种挂件、投影和操作器。** + +这意味着: + +- 页面树、文件树、Mindmap、RAG 结构索引、AI 结构操作,本质上都应收口到同一结构内核 +- `BlockNote` 不再是系统定义页面的唯一方式 +- Rust 最终不只是承接 API 或导图对象,而是承接整个统一结构真相 + +如果后续继续推进,真正该优先做的不是“先重写导图 UI”,而是: + +1. 定义统一 kernel node / edge 模型 +2. 定义 subtree / projection / reference 查询协议 +3. 让 Sidebar、搜索、AI、Mindmap 开始直接消费 kernel +4. 最后再逐步边缘化 `BlockNote` diff --git a/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md b/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md new file mode 100644 index 00000000..cc7ead33 --- /dev/null +++ b/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md @@ -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 主链。 + +### P1:tree 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 中重复的主路径树拼装已继续清理,避免再把旧树真相带回主链。 + +### P3:Rust 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 接住统一主链。** diff --git a/design/03-rust-web/process/3-11-rust-web-legacy-next-retirement-gates-v1.md b/design/03-rust-web/process/3-11-rust-web-legacy-next-retirement-gates-v1.md new file mode 100644 index 00000000..e6acb0cd --- /dev/null +++ b/design/03-rust-web/process/3-11-rust-web-legacy-next-retirement-gates-v1.md @@ -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 SSE,stream owner `rust-web`。 +- `/api/hermes/bridge`:Rust Web Hermes bridge,AI 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 面板默认请求 Hermes,task126 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 +``` diff --git a/design/03-rust-web/process/3-4-undo.md b/design/03-rust-web/process/3-4-undo.md new file mode 100644 index 00000000..707a198d --- /dev/null +++ b/design/03-rust-web/process/3-4-undo.md @@ -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`,除非真实代码已完成并通过对应验收命令。 + +## 阶段 A:Page 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/` 能看到 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,不再被主链调用。 + +## 阶段 C:3000 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 contract:workspace 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] 创建、重命名、移动、归档页面后,页面树和文件树都能在不刷新的情况下更新。 + +## 阶段 D:Search 收口为 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=` 时首屏 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=` 的 HTML 中包含 server-rendered result list。 +- [x] `rust/crates/mnote-web/src/routes/search.rs` 的 contract 不再声明主 runtime 为 `react_search_palette`。 +- [x] React 搜索组件只负责交互增强,不拥有检索真相或排序真相。 + +## 阶段 E:AI 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 主动调用。 + +## 阶段 F:Mindmap 成为 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 读回。 + +## 阶段 G:Legacy 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] 先执行阶段 A:Page Aggregate 协议上移,避免页面壳继续拼第二份页面真相。 +- [x] 再执行阶段 B:`page.*` 写命令切主链,确保编辑器保存、标题、设置都落到同一命令族。 +- [x] 再执行阶段 C:tree realtime 消费闭环,解决页面树/文件树 runtime 不一致问题。 +- [x] 然后并行准备阶段 D、E、F,但实施时分别通过独立 smoke 验收,避免 Search/AI/Mindmap 互相牵连。 +- [x] 最后执行阶段 G、H:legacy gate 与设计文档状态治理必须以真实功能验收为前提。 + +## 全局验收矩阵 + +| 能力 | 关键证明 | 命令 | +| --- | --- | --- | +| Page Aggregate | core-protocol 持有 projection,3000 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] 阶段 C:Rust 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: node scripts/task123-rust-web-tree-live-stream-consumer-smoke.js +MNOTE_UI_BASE_URL=http://127.0.0.1: node scripts/task125-rust-web-search-server-first-smoke.js +MNOTE_UI_BASE_URL=http://127.0.0.1: node scripts/task126-rust-web-ai-bridge-structured-write-smoke.js +MNOTE_UI_BASE_URL=http://127.0.0.1: node scripts/task127-rust-web-mindmap-kernel-projection-smoke.js +``` diff --git a/design/04-tree-domain/done/4-11-tree-rust-family-final-renderer-and-host-thinning-checklist-v1.md b/design/04-tree-domain/done/4-11-tree-rust-family-final-renderer-and-host-thinning-checklist-v1.md new file mode 100644 index 00000000..92302e53 --- /dev/null +++ b/design/04-tree-domain/done/4-11-tree-rust-family-final-renderer-and-host-thinning-checklist-v1.md @@ -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 plan,3000 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 F:final 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 HTML;runtime 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` artifact;legacy 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 model(focused id normalize / next / previous / home / end) + - [x] selection model(filetree selection state family) + - [x] picker state model(高亮 / Enter / exclude) + - [x] keyboard model(导航 / 打开 / expand-collapse / context menu intent) + - [x] drag/drop model(payload normalize / copy-move effect) + - [x] action registry(page / filetree / picker action set) + - [x] runtime facade(`TreeShellRuntimeRequest / TreeShellRuntimeResult` 可序列化 API,输出统一 DOM patch / host event / command event) +- [x] `page tree` 先切 final renderer。 + - 当前状态:默认 page tree 由 `TreeShellRustDomShellHost` 渲染 DOM,focus / keyboard / expand / collapse / toggle / open / context menu / move command dispatch 均通过 `TreeShellRuntimeRequest/Result` 返回结果驱动。 +- [x] `picker` 以轻量模式复用同一 renderer state family。 + - 当前状态:默认 picker 由 `TreeShellRustDomShellHost` 渲染 DOM,keyboard command、hover focus、click pick 均通过 runtime state 与 hostEvents 驱动,测试已禁止默认 iframe postMessage 成功路径。 +- [x] `file tree` 后切 final renderer,并保留 `doc / index / asset-folder / asset` 能力。 + - 当前状态:默认 file tree 由 `TreeShellRustDomShellHost` 渲染 DOM,selection、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 renderer;host 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 H:Rust 搜索 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 I:move 排序执行下沉 + +### 目标 + +把 `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 schema(TS artifact 写入边界与 Rust artifact writer 均固定 `mnote.tree.domain_event` v1) +- [x] Rust 侧由 command plan 生成 delta / event hint,Next 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 继续扩写长期树命令语义。 diff --git a/design/04-tree-domain/done/4-12-task-003-page-tree-final-renderer-execution-checklist-v1.md b/design/04-tree-domain/done/4-12-task-003-page-tree-final-renderer-execution-checklist-v1.md new file mode 100644 index 00000000..579fc91b --- /dev/null +++ b/design/04-tree-domain/done/4-12-task-003-page-tree-final-renderer-execution-checklist-v1.md @@ -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 验证边界。 diff --git a/design/04-tree-domain/done/4-13-task-004-picker-final-renderer-state-family-checklist-v1.md b/design/04-tree-domain/done/4-13-task-004-picker-final-renderer-state-family-checklist-v1.md new file mode 100644 index 00000000..8d5ca61c --- /dev/null +++ b/design/04-tree-domain/done/4-13-task-004-picker-final-renderer-state-family-checklist-v1.md @@ -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-26:3000 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`。 diff --git a/design/04-tree-domain/done/4-14-task-005-file-tree-final-renderer-capability-checklist-v1.md b/design/04-tree-domain/done/4-14-task-005-file-tree-final-renderer-capability-checklist-v1.md new file mode 100644 index 00000000..21a00e12 --- /dev/null +++ b/design/04-tree-domain/done/4-14-task-005-file-tree-final-renderer-capability-checklist-v1.md @@ -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-26:task-005 完整验证通过,未发现需要新增生产改动的缺口;本清单作为能力门槛记录。 + +## 非目标 + +- 本清单不拆除 compat host 的所有 debug fallback;对应 `task-006`。 +- 本清单不新增业务命令语义;file tree drop/delete/paste/upload preflight 已在前置任务中下沉 Rust。 diff --git a/design/04-tree-domain/done/4-15-task-006-compat-host-thinning-execution-checklist-v1.md b/design/04-tree-domain/done/4-15-task-006-compat-host-thinning-execution-checklist-v1.md new file mode 100644 index 00000000..1ad9fc69 --- /dev/null +++ b/design/04-tree-domain/done/4-15-task-006-compat-host-thinning-execution-checklist-v1.md @@ -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__` 注入第二份主路径 items;page/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 与状态机;当前已把本批可验证主路径收薄记录为阶段完成。 diff --git a/design/04-tree-domain/done/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md b/design/04-tree-domain/done/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md new file mode 100644 index 00000000..0b0ba315 --- /dev/null +++ b/design/04-tree-domain/done/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md @@ -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 host;legacy 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。 diff --git a/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md b/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md new file mode 100644 index 00000000..37d43854 --- /dev/null +++ b/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md @@ -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 不再依赖协议裂缝或壳内临时拼装来维持主路径 diff --git a/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md b/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md new file mode 100644 index 00000000..5a51e66d --- /dev/null +++ b/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md @@ -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:` + +要求: + +- `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 改造不再新增破坏性字段 diff --git a/design/04-tree-domain/done/4-5-tree-command-envelope-cutover-stage1-v1.md b/design/04-tree-domain/done/4-5-tree-command-envelope-cutover-stage1-v1.md new file mode 100644 index 00000000..5e35b720 --- /dev/null +++ b/design/04-tree-domain/done/4-5-tree-command-envelope-cutover-stage1-v1.md @@ -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。 diff --git a/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md b/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md new file mode 100644 index 00000000..d8971e0c --- /dev/null +++ b/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md @@ -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` 等兼容命名不再是新增主语义入口 diff --git a/design/04-tree-domain/done/4-7-tree-shell-ui-state-boundary-v1.md b/design/04-tree-domain/done/4-7-tree-shell-ui-state-boundary-v1.md new file mode 100644 index 00000000..2ebda9ac --- /dev/null +++ b/design/04-tree-domain/done/4-7-tree-shell-ui-state-boundary-v1.md @@ -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 使用同一份边界说明 diff --git a/design/04-tree-domain/done/4-8-tree-shell-cutover-performance-report-v1.md b/design/04-tree-domain/done/4-8-tree-shell-cutover-performance-report-v1.md new file mode 100644 index 00000000..fcb24afb --- /dev/null +++ b/design/04-tree-domain/done/4-8-tree-shell-cutover-performance-report-v1.md @@ -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 结论的前提下,把这份报告升级为多样本趋势报表。 diff --git a/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md b/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md new file mode 100644 index 00000000..a4685119 --- /dev/null +++ b/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md @@ -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 B:Rust route 与 projection 输出 + +这一阶段的目标是让 `mnote-web` 成为树域 projection 与 command 的正式出口,而不是继续让前端自己拼树。 + +**当前状态:`COMPLETED(Rust 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 中剥离为独立、可验证、可持续替换的正式执行面。 + +**当前状态:`COMPLETED(Leptos 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 D:Next 中挂载并切流 + +这一阶段的目标是把新树域壳真正挂到当前产品里,而不是停留在独立 demo。 + +**当前状态:`COMPLETED(Sidebar / 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。** diff --git a/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md b/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md new file mode 100644 index 00000000..3139b595 --- /dev/null +++ b/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md @@ -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 写入口仍未完全完成。** diff --git a/design/05-editor-mainline/process/5-2-tiptap-notion-like-template-adoption-v1.md b/design/05-editor-mainline/process/5-2-tiptap-notion-like-template-adoption-v1.md new file mode 100644 index 00000000..989b554f --- /dev/null +++ b/design/05-editor-mainline/process/5-2-tiptap-notion-like-template-adoption-v1.md @@ -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 数量当作当前阶段目标。** diff --git a/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md b/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md new file mode 100644 index 00000000..91ccdc72 --- /dev/null +++ b/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md @@ -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 直接写入主编辑区三者真正对齐的正确底层方向。 diff --git a/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md b/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md new file mode 100644 index 00000000..f2dd0b69 --- /dev/null +++ b/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md @@ -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 J:AI 写入口对齐 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 单一真源过渡的过程中。** diff --git a/design/05-editor-mainline/process/5-7-wolai-page-tree-main-editor-experience-restoration-v1.md b/design/05-editor-mainline/process/5-7-wolai-page-tree-main-editor-experience-restoration-v1.md new file mode 100644 index 00000000..224acfb1 --- /dev/null +++ b/design/05-editor-mainline/process/5-7-wolai-page-tree-main-editor-experience-restoration-v1.md @@ -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 收口。** diff --git a/design/05-editor-mainline/process/5-8-wolai-editor-state-automation-test-plan-v1.md b/design/05-editor-mainline/process/5-8-wolai-editor-state-automation-test-plan-v1.md new file mode 100644 index 00000000..0e911f60 --- /dev/null +++ b/design/05-editor-mainline/process/5-8-wolai-editor-state-automation-test-plan-v1.md @@ -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` + +不要继续扩写本文作为新测试方案。 diff --git a/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md b/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md new file mode 100644 index 00000000..0d8de28f --- /dev/null +++ b/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-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//` | +| 本地截图 | 同视口、同动作链截图 | +| 入口 | 按钮、快捷键、菜单或 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 B:Sidebar / 页面树 + +| ID | 状态 | 任务 | 验收要点 | +| --- | --- | --- | --- | +| B1 | PARITY | 侧栏宽度与背景 | `task131` 覆盖宽 `248px`、背景 `rgb(245,245,245)`、无右侧 inset、topbar.x=sidebar.right;subagent 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 action;subagent 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 B:Sidebar / 页面树。执行口径按 `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 对齐,运行时递归 depth;page tree 展开箭头改为 20x20 SVG chevron,page 行不渲染空 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 chevron,page 行不渲染空 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/GREEN,subagent 最终对标复核通过:根页不再停在“打开当前页面”入口,直接出现 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 F:Page 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`。 diff --git a/design/06-mindmap/process/6-mindmap-kernel-phase6-projection-editor-v1.md b/design/06-mindmap/process/6-mindmap-kernel-phase6-projection-editor-v1.md new file mode 100644 index 00000000..be611830 --- /dev/null +++ b/design/06-mindmap/process/6-mindmap-kernel-phase6-projection-editor-v1.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`。** diff --git a/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md b/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md new file mode 100644 index 00000000..7ae7053e --- /dev/null +++ b/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md @@ -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//`。 + +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// + +请验证: +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 文本结论当作最终证据。 diff --git a/design/90-reference/90-1-filetree.md b/design/90-reference/90-1-filetree.md new file mode 100644 index 00000000..09c49d26 --- /dev/null +++ b/design/90-reference/90-1-filetree.md @@ -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.5(2026年3月) +- 核心特性: + - VSCode风格界面,支持文件/目录层级展示与折叠 + - Git状态集成(修改、未跟踪、忽略文件颜色标记) + - Vim键绑定(hjkl导航)与鼠标支持 + - 系统剪贴板集成,支持文件复制/剪切/粘贴 + - 可通过cargo直接安装:`cargo install filetree` + +#### 2. fileview - 轻量级VSCode风格文件树TUI +- 仓库:https://crates.io/crates/fileview +- 版本:v1.8.1(2026年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应用中的文件树实现 +- Tauri(Rust+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 风格单面板文件树的最小可运行代码示例吗? diff --git a/design/90-reference/90-2-yemianshu.md b/design/90-reference/90-2-yemianshu.md new file mode 100644 index 00000000..aa704234 --- /dev/null +++ b/design/90-reference/90-2-yemianshu.md @@ -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, + 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 风格页面树的最小可运行示例吗? diff --git a/design/README.md b/design/README.md new file mode 100644 index 00000000..ddf06903 --- /dev/null +++ b/design/README.md @@ -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/` 分层 diff --git a/design/design/stitch/260429/DESIGN.md b/design/design/stitch/260429/DESIGN.md new file mode 100644 index 00000000..3a71cc6c --- /dev/null +++ b/design/design/stitch/260429/DESIGN.md @@ -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 | + +*Director’s 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. Do’s and Don’ts + +### 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. \ No newline at end of file diff --git a/design/design/stitch/260429/code.html b/design/design/stitch/260429/code.html new file mode 100644 index 00000000..0efb8d6d --- /dev/null +++ b/design/design/stitch/260429/code.html @@ -0,0 +1,399 @@ + + + + + +个人 | The Digital Atelier + + + + + + + +
+ + + +
+ +
+
+menu +
+The Digital Atelier +/ + +home + 个人 + +
+
+
+
+ + Public +
+
+star +history +auto_awesome +search +more_horiz +
+
+
+ +
+ +
+
+home +
+

个人

+
+face 个人空间 +calendar_today 2024年5月20日 +
+
+ +
+ +description +我的文件 + + +filter_vintage +灵感库 + + +sensor_door +工作台 + + +lock +私人保险箱 + + +book +阅读清单 + + +favorite +健康追踪 + + +laptop_mac +项目管理 + +
+ +
+

+ 欢迎来到你的数字工作室。这里是你整理思绪、捕获灵感并将其转化为现实的私人圣殿。每一个方块都承载着无限的可能性。 +

+
+
+lightbulb +
+

设计原则

+

+ 我们追求的是“垂直节奏”。在你的数字化工坊中,呼吸感和平衡比任何繁杂的装饰都更为重要。请在此处记录你的第一条笔记。 +

+
+
+
+
+
+ 输入 '/' 以插入块 +
+
+
+
+ +
+ + +
+
+
+ + \ No newline at end of file diff --git a/design/design/stitch/260429/screen.png b/design/design/stitch/260429/screen.png new file mode 100644 index 00000000..5aa1d8c2 Binary files /dev/null and b/design/design/stitch/260429/screen.png differ diff --git a/design/old/01-tree-first-graph-kernel/process/tree-first-graph-kernel-checklist-v1.md b/design/old/01-tree-first-graph-kernel/process/tree-first-graph-kernel-checklist-v1.md new file mode 100644 index 00000000..92f8d4be --- /dev/null +++ b/design/old/01-tree-first-graph-kernel/process/tree-first-graph-kernel-checklist-v1.md @@ -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 1:Node / Edge / Projection 基础模型落地 +- Kernel Phase 2:Kernel Query / Command / Subtree / Graph Traversal 协议落地 +- Kernel Phase 3:Rust Web 接入 kernel,成为主承载层 +- Kernel Phase 4:Sidebar / 页面树 / 文件树切到 kernel projection +- Kernel Phase 5:结构知识刷新与 kernel-aware 检索 +- Kernel Phase 6:Mindmap 降级为 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 1:Node / 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 2:Kernel 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 3:Rust 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 4:Sidebar / 页面树 / 文件树切到 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 6:Mindmap 降级为 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 8:BlockNote 退化为内容编辑挂件 + +**当前状态:`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/projection,Sidebar/搜索/Mindmap/阅读页/AI 都逐步切到 kernel projection,最后再让 `BlockNote` 与旧前端壳退场。** diff --git a/design/old/03-rust-web/process/rust-web-long-term-checklist-v1.md b/design/old/03-rust-web/process/rust-web-long-term-checklist-v1.md new file mode 100644 index 00000000..d3a58bed --- /dev/null +++ b/design/old/03-rust-web/process/rust-web-long-term-checklist-v1.md @@ -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 1:Rust Web 基础层落地 +- Phase 2:文档阅读页 server-first 化 +- Phase 3:Sidebar / 页面树 / 文件树 Rust 化与 island 化 +- Phase 4:搜索系统 Rust 化与 island 化 +- Phase 5:AI 面板进一步收口为纯桥接 island +- Phase 6:Mindmap 独立对象化与独立页面化 +- 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 1:Rust 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 3:Sidebar / 页面树 / 文件树 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 5:AI 面板进一步收口为纯桥接 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 island,Hermes / Rust bridge / client-tool-result 主链继续复用现有接缝。 +- [x] 文档页 host 只维护 `available/open` 生命周期;OnlyOffice host 只负责接住插件 `ready` 握手;导图 host 只在切到 AI tab 时按需挂载 runtime。 +- [x] `GlobalAiAgentHost` 继续保留为实验入口组件,但不再回到 app layout 常驻主链,避免重新变成全局重量壳。 + +--- + +## 11. Phase 6:Mindmap 独立对象化与独立页面化 + +### 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`;稳定后再清旧壳,而不是反过来。** diff --git a/design/old/04-tree-domain/process/4-1-sidebar-pagetree-filetree-product-gap-analysis-v1.md b/design/old/04-tree-domain/process/4-1-sidebar-pagetree-filetree-product-gap-analysis-v1.md new file mode 100644 index 00000000..68a46525 --- /dev/null +++ b/design/old/04-tree-domain/process/4-1-sidebar-pagetree-filetree-product-gap-analysis-v1.md @@ -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 中的新树实现,定义为**过渡验证层** +- 下一阶段任务,定义为**产品级树域控件重建** + +只有这样,团队后续的修改方向才不会继续发散。 diff --git a/design/old/04-tree-domain/process/4-10-tree-rust-family-cutover-checklist-v1.md b/design/old/04-tree-domain/process/4-10-tree-rust-family-cutover-checklist-v1.md new file mode 100644 index 00000000..f856f1f9 --- /dev/null +++ b/design/old/04-tree-domain/process/4-10-tree-rust-family-cutover-checklist-v1.md @@ -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 SSE;Next 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 hint;3000 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` route,Sidebar 搜索态只消费该 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 JSON,compat 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__` 注入第二份 items;React 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 家族。** diff --git a/design/old/04-tree-domain/process/4-9-tree-rust-family-cutover-remaining-architecture-and-capability-preservation-v1.md b/design/old/04-tree-domain/process/4-9-tree-rust-family-cutover-remaining-architecture-and-capability-preservation-v1.md new file mode 100644 index 00000000..ed3c1be4 --- /dev/null +++ b/design/old/04-tree-domain/process/4-9-tree-rust-family-cutover-remaining-architecture-and-capability-preservation-v1.md @@ -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 家族。** diff --git a/design/old/05-editor-mainline/done/appflowy.md b/design/old/05-editor-mainline/done/appflowy.md new file mode 100644 index 00000000..a97939f0 --- /dev/null +++ b/design/old/05-editor-mainline/done/appflowy.md @@ -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同步功能。 \ No newline at end of file diff --git a/design/old/05-editor-mainline/done/rust-block-editor-adoption-matrix-v0.md b/design/old/05-editor-mainline/done/rust-block-editor-adoption-matrix-v0.md new file mode 100644 index 00000000..f00bf800 --- /dev/null +++ b/design/old/05-editor-mainline/done/rust-block-editor-adoption-matrix-v0.md @@ -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` 明确区分状态、块解析、命令执行。 +- `Command` 模式非常适合把当前分散在 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 接缝处补最小胶水。 diff --git a/design/old/05-editor-mainline/done/rust-block-editor-ai-tool-contract-v0.md b/design/old/05-editor-mainline/done/rust-block-editor-ai-tool-contract-v0.md new file mode 100644 index 00000000..f1ceb8c7 --- /dev/null +++ b/design/old/05-editor-mainline/done/rust-block-editor-ai-tool-contract-v0.md @@ -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 结果包含最小审计信息 diff --git a/design/old/05-editor-mainline/done/rust-block-editor-command-contract-v0.md b/design/old/05-editor-mainline/done/rust-block-editor-command-contract-v0.md new file mode 100644 index 00000000..2e49634f --- /dev/null +++ b/design/old/05-editor-mainline/done/rust-block-editor-command-contract-v0.md @@ -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` 成为第一阶段必须可用的基础能力 diff --git a/design/old/05-editor-mainline/done/rust-block-editor-interaction-samples-v1.md b/design/old/05-editor-mainline/done/rust-block-editor-interaction-samples-v1.md new file mode 100644 index 00000000..a0f060e4 --- /dev/null +++ b/design/old/05-editor-mainline/done/rust-block-editor-interaction-samples-v1.md @@ -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` 风格封装 `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 的第一批回归样例。 diff --git a/design/old/05-editor-mainline/done/tiptap.md b/design/old/05-editor-mainline/done/tiptap.md new file mode 100644 index 00000000..64078d53 --- /dev/null +++ b/design/old/05-editor-mainline/done/tiptap.md @@ -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集成步骤),你直接复制就能用? \ No newline at end of file diff --git a/design/old/05-editor-mainline/done/tiptaplogin.md b/design/old/05-editor-mainline/done/tiptaplogin.md new file mode 100644 index 00000000..3d6d95e4 --- /dev/null +++ b/design/old/05-editor-mainline/done/tiptaplogin.md @@ -0,0 +1,4 @@ +# [recycle] tiptaplogin 记录 + +email:liaibo@yeah.net +key:Liaibo95540245 diff --git a/design/old/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md b/design/old/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md new file mode 100644 index 00000000..3e36c9d6 --- /dev/null +++ b/design/old/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md @@ -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` 控制阅读态/编辑态 +- 只有进入编辑态后才挂载 `` + +这意味着: + +> **`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`,直到真实文档页完成切流为止。** diff --git a/design/old/05-editor-mainline/process/5-3-tiptap-leptos-rust-migration-checklist-v1.md b/design/old/05-editor-mainline/process/5-3-tiptap-leptos-rust-migration-checklist-v1.md new file mode 100644 index 00000000..48aa62d8 --- /dev/null +++ b/design/old/05-editor-mainline/process/5-3-tiptap-leptos-rust-migration-checklist-v1.md @@ -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 对标体验增强。** diff --git a/design/old/05-editor-mainline/process/5-editor-baseline-reset-v2.md b/design/old/05-editor-mainline/process/5-editor-baseline-reset-v2.md new file mode 100644 index 00000000..7d09e0e7 --- /dev/null +++ b/design/old/05-editor-mainline/process/5-editor-baseline-reset-v2.md @@ -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 页面体验” + +### 阶段 B:Leptos-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 主线 + +### 阶段 C:Rust 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,再映射回编辑器展示 + +### 阶段 D:AI-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 全自研编辑器”的文档口径为准。 diff --git a/design/old/05-editor-mainline/process/ai-first-rust-block-editor-baseline-v1.md b/design/old/05-editor-mainline/process/ai-first-rust-block-editor-baseline-v1.md new file mode 100644 index 00000000..6371db21 --- /dev/null +++ b/design/old/05-editor-mainline/process/ai-first-rust-block-editor-baseline-v1.md @@ -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` 的更完整形态收敛。 + +## 阶段 4:AI-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 完成后,再执行依赖清理与最终删除主线路径 | diff --git a/design/old/05-editor-mainline/process/blockselect.md b/design/old/05-editor-mainline/process/blockselect.md new file mode 100644 index 00000000..1acc3fc7 --- /dev/null +++ b/design/old/05-editor-mainline/process/blockselect.md @@ -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! { +
+ } + } + /> +
+ } +} +``` + +--- + +# 七、避坑(非常重要) +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 + egui(Rust 原生 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** +- 前端:Leptos(Rust 编译 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. yrs(CRDT 协作,必用) +- **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-tungstenite(WebSocket)**: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** 最小块编辑器模板吗? diff --git a/design/old/05-editor-mainline/process/runtime-shell-to-wolai-page-correction-checklist-v1.md b/design/old/05-editor-mainline/process/runtime-shell-to-wolai-page-correction-checklist-v1.md new file mode 100644 index 00000000..e4223ae1 --- /dev/null +++ b/design/old/05-editor-mainline/process/runtime-shell-to-wolai-page-correction-checklist-v1.md @@ -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。** diff --git a/design/old/05-editor-mainline/process/rust-block-editor-blocknote-migration-v1.md b/design/old/05-editor-mainline/process/rust-block-editor-blocknote-migration-v1.md new file mode 100644 index 00000000..0db328b7 --- /dev/null +++ b/design/old/05-editor-mainline/process/rust-block-editor-blocknote-migration-v1.md @@ -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 或待删除对象 diff --git a/design/old/05-editor-mainline/process/rust-block-editor-blocks-spike-v0.md b/design/old/05-editor-mainline/process/rust-block-editor-blocks-spike-v0.md new file mode 100644 index 00000000..85c4b47c --- /dev/null +++ b/design/old/05-editor-mainline/process/rust-block-editor-blocks-spike-v0.md @@ -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` 为主。 + +而 `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 +- 缺口 + +当前文档已满足这些项。 diff --git a/design/old/05-editor-mainline/process/rust-block-editor-edita-spike-v0.md b/design/old/05-editor-mainline/process/rust-block-editor-edita-spike-v0.md new file mode 100644 index 00000000..45b08a19 --- /dev/null +++ b/design/old/05-editor-mainline/process/rust-block-editor-edita-spike-v0.md @@ -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` 的价值不在 UI,而在: + +- 持有统一 `state` +- 提供统一命令入口 +- 提供 block registry +- 允许 fallback block + +这和 `mnote` 第一阶段要的“无头编辑器容器”是对齐的。 + +对 `mnote` 的映射建议: + +- `Node` 不直接映射前端 DOM 节点 +- `State` 映射 `EditorDocument` +- `Input` 第一阶段不必暴露给外部 UI,可先收口成 `EditorCommandEnvelope` + +### 3.2 `Command` + +`Command` 很适合当 `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 diff --git a/design/old/05-editor-mainline/process/rust-block-editor-model-v0.md b/design/old/05-editor-mainline/process/rust-block-editor-model-v0.md new file mode 100644 index 00000000..2f883328 --- /dev/null +++ b/design/old/05-editor-mainline/process/rust-block-editor-model-v0.md @@ -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 的长期依赖 diff --git a/design/old/05-editor-mainline/process/rust-block-editor-phase0-baseline-v1.md b/design/old/05-editor-mainline/process/rust-block-editor-phase0-baseline-v1.md new file mode 100644 index 00000000..16742df6 --- /dev/null +++ b/design/old/05-editor-mainline/process/rust-block-editor-phase0-baseline-v1.md @@ -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 参考。 diff --git a/design/old/08-legacy-rust-kernel/done/ai-tool-cutover-matrix.md b/design/old/08-legacy-rust-kernel/done/ai-tool-cutover-matrix.md new file mode 100644 index 00000000..9870828b --- /dev/null +++ b/design/old/08-legacy-rust-kernel/done/ai-tool-cutover-matrix.md @@ -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 registry,TS 仅保留最小 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 runtime,TS 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。 diff --git a/design/old/08-legacy-rust-kernel/done/mindmap-onlyoffice-boundary.md b/design/old/08-legacy-rust-kernel/done/mindmap-onlyoffice-boundary.md new file mode 100644 index 00000000..95d5b34b --- /dev/null +++ b/design/old/08-legacy-rust-kernel/done/mindmap-onlyoffice-boundary.md @@ -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` 继续作为后置实现项,而不是本阶段直接迁入主线。 diff --git a/design/old/08-legacy-rust-kernel/done/phase8-cutover-log-2026-04-15.md b/design/old/08-legacy-rust-kernel/done/phase8-cutover-log-2026-04-15.md new file mode 100644 index 00000000..8f383381 --- /dev/null +++ b/design/old/08-legacy-rust-kernel/done/phase8-cutover-log-2026-04-15.md @@ -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` 已收口到统一 adapter,route 仅保留 transport。 +- Mindmap 剩余旧面已接到 Rust mindmap adapter/tool,AI/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 工具链做一轮发布前运行态回归复核。 diff --git a/design/old/08-legacy-rust-kernel/done/release-readiness-source-audit.md b/design/old/08-legacy-rust-kernel/done/release-readiness-source-audit.md new file mode 100644 index 00000000..31677dbe --- /dev/null +++ b/design/old/08-legacy-rust-kernel/done/release-readiness-source-audit.md @@ -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 真实执行闭合。 diff --git a/design/old/08-legacy-rust-kernel/done/rust-backport-audit-inventory.md b/design/old/08-legacy-rust-kernel/done/rust-backport-audit-inventory.md new file mode 100644 index 00000000..48f7ac7b --- /dev/null +++ b/design/old/08-legacy-rust-kernel/done/rust-backport-audit-inventory.md @@ -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` 的历史页面壳或构建噪音带入主线。 diff --git a/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md b/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md new file mode 100644 index 00000000..69a952f5 --- /dev/null +++ b/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md @@ -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 C:AI 与 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、外部服务编排、客户端桥与冻结兼容壳责任。** diff --git a/design/old/08-legacy-rust-kernel/done/rust-kernel-final-closure-checklist.md b/design/old/08-legacy-rust-kernel/done/rust-kernel-final-closure-checklist.md new file mode 100644 index 00000000..4c6d77e2 --- /dev/null +++ b/design/old/08-legacy-rust-kernel/done/rust-kernel-final-closure-checklist.md @@ -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 runtime;Convex 主链下 `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、外部服务编排、客户端桥与明确冻结的兼容壳。** diff --git a/design/old/08-legacy-rust-kernel/done/rust-kernel-legacy-delete-list.md b/design/old/08-legacy-rust-kernel/done/rust-kernel-legacy-delete-list.md new file mode 100644 index 00000000..0aea6578 --- /dev/null +++ b/design/old/08-legacy-rust-kernel/done/rust-kernel-legacy-delete-list.md @@ -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 adapter,route 仅剩 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 为准。 diff --git a/design/old/08-legacy-rust-kernel/done/rust-single-repo-maintenance-boundary.md b/design/old/08-legacy-rust-kernel/done/rust-single-repo-maintenance-boundary.md new file mode 100644 index 00000000..51ae8f55 --- /dev/null +++ b/design/old/08-legacy-rust-kernel/done/rust-single-repo-maintenance-boundary.md @@ -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 源码根。 diff --git a/design/old/08-legacy-rust-kernel/process/ai-frontend-simplification-plan-v1.md b/design/old/08-legacy-rust-kernel/process/ai-frontend-simplification-plan-v1.md new file mode 100644 index 00000000..15da5416 --- /dev/null +++ b/design/old/08-legacy-rust-kernel/process/ai-frontend-simplification-plan-v1.md @@ -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 runtime,Rust 负责 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。** diff --git a/design/old/08-legacy-rust-kernel/process/document-access-performance-root-cure-v1.md b/design/old/08-legacy-rust-kernel/process/document-access-performance-root-cure-v1.md new file mode 100644 index 00000000..759095ad --- /dev/null +++ b/design/old/08-legacy-rust-kernel/process/document-access-performance-root-cure-v1.md @@ -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 runtime:Hermes API Server** +- **最后保留的重编辑岛:`BlockNote`** + +一句话版: + +> **`axum` 负责服务,`Leptos` 负责薄页面与 islands,Rust 内核负责业务,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/ diff --git a/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-phase-checklist.md b/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-phase-checklist.md new file mode 100644 index 00000000..baf9f70e --- /dev/null +++ b/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-phase-checklist.md @@ -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 A:Rust 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 B:bridge 入口与 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` / envelope;query 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 D:Mindmap 与 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 壳”这句统一结论。 diff --git a/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-plan.md b/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-plan.md new file mode 100644 index 00000000..fc292c96 --- /dev/null +++ b/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-plan.md @@ -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` 这个单仓里启动和保存。 diff --git a/design/old/08-legacy-rust-kernel/process/rust-kernel-missing-targets.md b/design/old/08-legacy-rust-kernel/process/rust-kernel-missing-targets.md new file mode 100644 index 00000000..9a045768 --- /dev/null +++ b/design/old/08-legacy-rust-kernel/process/rust-kernel-missing-targets.md @@ -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 --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 替换”。 diff --git a/design/old/08-legacy-rust-kernel/process/rust-kernel-replacement-roadmap-v1.md b/design/old/08-legacy-rust-kernel/process/rust-kernel-replacement-roadmap-v1.md new file mode 100644 index 00000000..9b1f9767 --- /dev/null +++ b/design/old/08-legacy-rust-kernel/process/rust-kernel-replacement-roadmap-v1.md @@ -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 6:AI 与 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` + +### 批次 E:AI/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 内核已经替换原有内核”。 diff --git a/design/old/08-legacy-rust-kernel/process/sidebar-rust-query-target.md b/design/old/08-legacy-rust-kernel/process/sidebar-rust-query-target.md new file mode 100644 index 00000000..93132b16 --- /dev/null +++ b/design/old/08-legacy-rust-kernel/process/sidebar-rust-query-target.md @@ -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` diff --git a/design/old/README.md b/design/old/README.md new file mode 100644 index 00000000..d327e567 --- /dev/null +++ b/design/old/README.md @@ -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 相关历史稿 diff --git a/rust/crates/mnote-web/src/routes/gateway.rs b/rust/crates/mnote-web/src/routes/gateway.rs index 717e210a..4335ec2e 100644 --- a/rust/crates/mnote-web/src/routes/gateway.rs +++ b/rust/crates/mnote-web/src/routes/gateway.rs @@ -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 content = crate::ssr::render_view(leptos::view! { - - }); + let render_workspace_entry = || { + crate::ssr::render_view(leptos::view! { + + }) + }; + 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! { + + }); + let body_extra = format!( + r#" + + {} + {}"#, + 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#" - MNOTE + {} {} + {} "#, + escape_html(&html_title), crate::ssr::MNOTE_CSS, - content + content, + body_extra )) .into_response(); stamp_gateway_headers(response.headers_mut(), false); diff --git a/rust/crates/mnote-web/src/routes/tree.rs b/rust/crates/mnote-web/src/routes/tree.rs index 1d257930..a5766aa3 100644 --- a/rust/crates/mnote-web/src/routes/tree.rs +++ b/rust/crates/mnote-web/src/routes/tree.rs @@ -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 { } pub(crate) fn collect_page_tree_render_rows(projection: &Value) -> Vec { - projection + let mut rows: Vec = 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 Vec>(); + 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( diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs index 4fe01e60..432c52b7 100644 --- a/rust/crates/mnote-web/src/routes/web_shell.rs +++ b/rust/crates/mnote-web/src/routes/web_shell.rs @@ -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#""# } -fn render_editor_island_adapter_script() -> &'static str { +pub(crate) fn render_editor_island_adapter_script() -> &'static str { r#"