对齐 Wolai 侧栏体验并收拢设计入库
This commit is contained in:
+13
-1
@@ -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/
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# 01-05 当前主线与优先级总览
|
||||
|
||||
> 更新时间:2026-04-22
|
||||
|
||||
这份总览只做一件事:
|
||||
|
||||
> **给 `design/01-*` 到 `design/05-*` 当前仍然有效的主线稿排优先级,并明确哪些 `process` 已经过时或应降级为历史参考。**
|
||||
|
||||
## 1. 当前仍然有效的上位主线
|
||||
|
||||
下面三份仍然是当前架构判断的上位依据:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
|
||||
它们分别固定了三件事:
|
||||
|
||||
1. 长期事实源是 `tree-first graph kernel`
|
||||
2. `Convex` 保留为当前自托管存储 / 实时底座,不先拆
|
||||
3. `mnote-web` 是 Rust Web 承载层,长期继续承担 transport、projection 分发与切流
|
||||
|
||||
## 2. 当前第一优先级
|
||||
|
||||
当前最优先的不是继续扩 UI,也不是继续大规模重写执行面,而是把页面域和树域的真相边界先收口。
|
||||
|
||||
### 2.1 Page Aggregate
|
||||
|
||||
当前第一优先级固定为:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
|
||||
|
||||
原因:
|
||||
|
||||
- 默认主编辑器已经切到页面内 `leptos-tiptap` island
|
||||
- 标题单一真源已经开始收口
|
||||
- 文档页入口已经消费前端侧 `PageAggregateProjection`
|
||||
- 但 Rust 侧还没有原生 `Page Aggregate` 契约
|
||||
- 标题 / 正文 / 页面设置仍未统一成同一组 page aggregate command family
|
||||
|
||||
### 2.2 Tree Command Cutover
|
||||
|
||||
当前第二优先级固定为:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
|
||||
|
||||
原因:
|
||||
|
||||
- 前端已经开始以 `tree.*` 作为 preferred command name
|
||||
- 但 route / adapter / bridge / CLI 仍保留大量 `documents.*`
|
||||
- 如果不先完成命令面统一,后续 page aggregate 与 tree realtime 都会持续停留在兼容双轨
|
||||
|
||||
### 2.3 Tree Realtime 主链
|
||||
|
||||
当前第三优先级固定为:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
|
||||
原因:
|
||||
|
||||
- 当前 sidebar 已有 query snapshot、tree stream、preferred snapshot 选择链
|
||||
- 但正式的 snapshot + delta 主链还没有完全成为唯一实时事实来源
|
||||
- 这会持续带来 refetch 补偿、freshness 选择和旧快照回闪问题
|
||||
|
||||
## 3. 当前仍应保留但不在第一线的 process
|
||||
|
||||
下面这些文档仍然有效,但当前不应排在第一优先:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
|
||||
- `/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`
|
||||
- `/mnt/Data1T/mnote/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-2-tiptap-notion-like-template-adoption-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md`
|
||||
|
||||
保留原因:
|
||||
|
||||
- 方向仍对
|
||||
- 仍然是局部主线的有效合同
|
||||
- 但它们不应压过 `Page Aggregate`、`tree command`、`tree realtime` 三条当前主战线
|
||||
|
||||
## 4. 当前已降级为历史参考的 05 主线稿
|
||||
|
||||
下面这些稿件已被后续实现和更新文档覆盖,不再作为当前活跃 `process`:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/5-3-tiptap-leptos-rust-migration-checklist-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/5-editor-baseline-reset-v2.md`
|
||||
- `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-1-sidebar-pagetree-filetree-product-gap-analysis-v1.md`
|
||||
|
||||
降级原因:
|
||||
|
||||
- 它们的重要判断已经被吸收到 `5-4 / 5-5 / 5-6`
|
||||
- 文档中的事实基线已经落后于当前默认主编辑器、page aggregate 入口与 island 主链
|
||||
- 继续保留在活跃 `process` 容易让后续工作按旧阶段推进
|
||||
|
||||
## 5. 当前执行顺序
|
||||
|
||||
当前推荐顺序固定为:
|
||||
|
||||
1. `Page Aggregate`
|
||||
2. `Tree Command Cutover Stage 2`
|
||||
3. `Tree Realtime Event Stream`
|
||||
4. 树域产品交互合同补齐
|
||||
5. 编辑器官方模板行为对齐
|
||||
|
||||
## 6. 一句话收口
|
||||
|
||||
当前 `design/01-05` 的真实主线,不是“继续证明 Rust Web 值不值得做”,也不是“继续证明 `leptos-tiptap` 能不能用”,而是:
|
||||
|
||||
> **先把页面域和树域收口到 Rust 主导的单一真源,再推进 tree realtime 和树域产品执行面。**
|
||||
@@ -0,0 +1,719 @@
|
||||
# 1 [process] Tree-First Graph 内核方案 v1
|
||||
|
||||
> 更新时间:2026-04-22
|
||||
>
|
||||
> 当前优先级入口:
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文回答的不是“页面如何加速”,而是更底层的问题:
|
||||
|
||||
> **mnote 长期到底应该以什么作为统一对象内核。**
|
||||
|
||||
在前一轮讨论里,方向经历了一个重要修正:
|
||||
|
||||
- 不是让 `Mindmap` 成为系统主投影
|
||||
- 也不是让“导图页面”变成新的系统中心
|
||||
- 而是让 **`tree-first graph` 成为统一结构内核**
|
||||
- `Mindmap` 只是这个内核的一种可视化挂件
|
||||
|
||||
这份文档的目标,是把这个判断正式固定下来。
|
||||
|
||||
---
|
||||
|
||||
## 2. 先给结论
|
||||
|
||||
结论只有一句:
|
||||
|
||||
> **mnote 的长期核心不应是 `BlockNote-first`,也不应是 `Mindmap-first`,而应是 `tree-first graph kernel`。**
|
||||
|
||||
也就是说:
|
||||
|
||||
- **树** 是主骨架
|
||||
- **图** 是横向引用扩展
|
||||
- **Mindmap** 是树/图的一种空间化视图
|
||||
- **Sidebar / 页面树 / 文件树 / 文档阅读页 / 搜索结果 / AI 面板** 都只是同一内核的不同投影
|
||||
|
||||
因此,长期正确方向不是:
|
||||
|
||||
- “把所有东西都画成导图”
|
||||
|
||||
而是:
|
||||
|
||||
- “让所有对象共享同一份结构内核,再让不同视图各自投影”
|
||||
|
||||
---
|
||||
|
||||
## 3. 为什么不是 Mindmap-first
|
||||
|
||||
虽然 Mindmap 在结构表达上很强,但它不适合被定义为主中心。
|
||||
|
||||
原因有四个。
|
||||
|
||||
### 3.1 导图 UI 不是所有场景的最佳交互
|
||||
|
||||
下面这些场景不适合被强行导图化:
|
||||
|
||||
- Sidebar 导航
|
||||
- 文件树浏览
|
||||
- 文档阅读
|
||||
- 搜索结果浏览
|
||||
- 历史版本查看
|
||||
- AI 工具面板
|
||||
|
||||
这些场景里,很多时候:
|
||||
|
||||
- 列表更合适
|
||||
- 树表更合适
|
||||
- 阅读流更合适
|
||||
- 搜索结果卡片更合适
|
||||
|
||||
所以:
|
||||
|
||||
> **导图是一种强表达能力的视图,不是所有结构都应默认进入的主视图。**
|
||||
|
||||
### 3.2 如果 Mindmap 成为主中心,系统会被导图交互绑架
|
||||
|
||||
一旦把 Mindmap 当主投影,后面很容易出现:
|
||||
|
||||
- 结构建模被导图控件的数据形状反向约束
|
||||
- 页面/文件/章节/引用都被迫适配导图编辑器
|
||||
- 视图层规则污染对象层规则
|
||||
|
||||
这会让“结构内核”被“某种 UI 控件”夺走主导权。
|
||||
|
||||
### 3.3 当前本地代码已经说明 Mindmap 更像重操作壳
|
||||
|
||||
当前:
|
||||
|
||||
- [`MindmapBlock.tsx`](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx)
|
||||
|
||||
同时承担:
|
||||
|
||||
- 内嵌块
|
||||
- 独立页
|
||||
- 工具栏
|
||||
- 右键菜单
|
||||
- 导航器
|
||||
- 缩略图
|
||||
- 本地全屏视图
|
||||
|
||||
这说明它当前的本质更接近:
|
||||
|
||||
- 一个前端重交互操作层
|
||||
|
||||
而不是:
|
||||
|
||||
- 一个稳定、极简、内核级结构模型
|
||||
|
||||
### 3.4 Mindmap 已经适合退到“挂件层”
|
||||
|
||||
真正更合理的位置是:
|
||||
|
||||
- 它继续保留
|
||||
- 但作为 `tree-first graph kernel` 的挂件和投影
|
||||
- 而不是事实源
|
||||
|
||||
---
|
||||
|
||||
## 4. 为什么是 Tree-First Graph
|
||||
|
||||
这是因为 mnote 当前最稳定、最通用、最可渐进迁移的共同语义,本质上是:
|
||||
|
||||
- **父子层级**
|
||||
- **局部子树**
|
||||
- **对象引用**
|
||||
|
||||
这正对应:
|
||||
|
||||
- 树
|
||||
- 子树
|
||||
- 图边
|
||||
|
||||
### 4.1 树是最自然的主骨架
|
||||
|
||||
下面这些天然就是树:
|
||||
|
||||
- 工作区结构
|
||||
- 页面树
|
||||
- 文件树
|
||||
- 文档大纲
|
||||
- PDF 章节结构
|
||||
- 页面内部结构
|
||||
- 导图节点结构
|
||||
|
||||
因此“树优先”不是一种美学偏好,而是数据现实。
|
||||
|
||||
### 4.2 图是必须存在的扩展层
|
||||
|
||||
但系统又不可能只有树,因为还存在:
|
||||
|
||||
- 双向引用
|
||||
- 页面引用页面
|
||||
- 块引用块
|
||||
- 摘要引用原文
|
||||
- PDF 章节引用页码/附件
|
||||
- AI 生成节点引用证据节点
|
||||
|
||||
这些都不是父子关系,而是横向边。
|
||||
|
||||
所以系统最终一定是:
|
||||
|
||||
- 树作为主骨架
|
||||
- 图作为引用扩展
|
||||
|
||||
也就是:
|
||||
|
||||
> **tree-first graph**
|
||||
|
||||
### 4.3 这种结构最适合 Rust 内核化
|
||||
|
||||
因为它天然适合:
|
||||
|
||||
- typed node
|
||||
- typed edge
|
||||
- subtree query
|
||||
- graph traversal
|
||||
- object projection
|
||||
- CLI / AI tool 直接操作
|
||||
|
||||
这比“以某个前端编辑器数据格式为真相”更适合进入 Rust core。
|
||||
|
||||
---
|
||||
|
||||
## 5. 内核定义
|
||||
|
||||
## 5.1 统一内核
|
||||
|
||||
长期建议把 mnote 的结构真相定义为:
|
||||
|
||||
- `WorkspaceKernel`
|
||||
- `Node`
|
||||
- `Edge`
|
||||
- `Projection`
|
||||
|
||||
### 5.1.1 当前已落地的 Rust 类型
|
||||
|
||||
2026-04-16 这轮已经在 Rust `core-protocol` 中补入统一 kernel 类型定义,入口文件为:
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/crates/core-protocol/src/kernel.rs`
|
||||
|
||||
当前已落下的主类型包括:
|
||||
|
||||
- `KernelNode`
|
||||
- `KernelEdge`
|
||||
- `KernelProjectionRequest`
|
||||
- `KernelProjectionResult`
|
||||
- `KernelSubtreeRef`
|
||||
- `KernelSubtreeResult`
|
||||
- `KernelGetNode`
|
||||
- `KernelGetSubtree`
|
||||
- `KernelListChildren`
|
||||
- `KernelListEdges`
|
||||
- `KernelTraverseGraph`
|
||||
- `KernelCreateNode`
|
||||
- `KernelUpdateNode`
|
||||
- `KernelMoveSubtree`
|
||||
- `KernelAttachEdge`
|
||||
- `KernelDetachEdge`
|
||||
|
||||
这意味着这份文档里的 kernel 语义已经不再只是概念,而是进入了 Rust 可复用协议层。
|
||||
|
||||
### 5.2 Node
|
||||
|
||||
每个对象都是带类型的节点。
|
||||
|
||||
候选节点类型包括:
|
||||
|
||||
- `workspace`
|
||||
- `folder`
|
||||
- `page`
|
||||
- `section`
|
||||
- `paragraph`
|
||||
- `asset`
|
||||
- `book`
|
||||
- `pdf`
|
||||
- `mindmap`
|
||||
- `mindmap_node`
|
||||
- `table`
|
||||
- `query_view`
|
||||
- `summary`
|
||||
- `ai_note`
|
||||
- `reference_anchor`
|
||||
|
||||
当前 Rust 中的最小首批节点类型已经固定为:
|
||||
|
||||
- `workspace`
|
||||
- `folder`
|
||||
- `page`
|
||||
- `section`
|
||||
- `asset`
|
||||
- `book`
|
||||
- `pdf`
|
||||
- `mindmap`
|
||||
- `mindmap_node`
|
||||
- `summary`
|
||||
- `ai_note`
|
||||
- `reference_anchor`
|
||||
- `content_node`
|
||||
- `index_node`
|
||||
|
||||
注意:
|
||||
|
||||
> **这里的关键不是名字,而是“页面、文件、书籍、摘要、AI 结果不再分属不同系统,而是同一内核里的 typed node”。**
|
||||
|
||||
### 5.3 Edge
|
||||
|
||||
边分成两类:
|
||||
|
||||
#### A. 骨架边
|
||||
|
||||
- `parent_of`
|
||||
- `child_of`
|
||||
- `contains`
|
||||
|
||||
#### B. 扩展边
|
||||
|
||||
- `references`
|
||||
- `backlinks_to`
|
||||
- `source_of`
|
||||
- `derived_from`
|
||||
- `summarizes`
|
||||
- `indexes`
|
||||
- `points_to`
|
||||
|
||||
当前 Rust 中的最小首批边类型已经固定为:
|
||||
|
||||
- `parent_of`
|
||||
- `child_of`
|
||||
- `contains`
|
||||
- `references`
|
||||
- `backlinks_to`
|
||||
- `source_of`
|
||||
- `derived_from`
|
||||
- `summarizes`
|
||||
- `indexes`
|
||||
- `points_to`
|
||||
|
||||
这样:
|
||||
|
||||
- 树结构靠骨架边维持
|
||||
- 网状关系靠扩展边表达
|
||||
|
||||
### 5.4 Projection
|
||||
|
||||
Projection 不是数据真相,只是同一内核的不同投影。
|
||||
|
||||
长期主要投影包括:
|
||||
|
||||
- Sidebar tree
|
||||
- 页面树
|
||||
- 文件树
|
||||
- 文档阅读流
|
||||
- Mindmap
|
||||
- 搜索结果页
|
||||
- AI 操作视图
|
||||
- 未来可能的关系图 / 时间线 / 表格视图
|
||||
|
||||
当前 Rust 中已经固定的投影种类包括:
|
||||
|
||||
- `sidebar_tree`
|
||||
- `page_tree`
|
||||
- `file_tree`
|
||||
- `mindmap`
|
||||
- `read_view`
|
||||
- `search_results`
|
||||
- `rag_index`
|
||||
|
||||
### 5.5 四层边界
|
||||
|
||||
为了避免后续又把前端页面壳误当事实源,当前主线边界固定为四层:
|
||||
|
||||
1. **事实源**
|
||||
- 统一 `tree-first graph kernel`
|
||||
- 只承载 `node / edge / subtree / audit`
|
||||
- 不承载具体前端 UI 状态
|
||||
2. **投影**
|
||||
- `sidebar tree`
|
||||
- `page tree`
|
||||
- `file tree`
|
||||
- `mindmap projection`
|
||||
- `read view`
|
||||
- `search / rag projection`
|
||||
3. **编辑器**
|
||||
- `BlockNote`
|
||||
- `Mindmap canvas`
|
||||
- `OnlyOffice`
|
||||
- 未来其他专用内容编辑器
|
||||
4. **外挂 / 挂件**
|
||||
- AI 面板
|
||||
- 评论
|
||||
- 历史
|
||||
- 回链
|
||||
- 右侧辅助信息面板
|
||||
|
||||
固定规则是:
|
||||
|
||||
- 事实源只在 kernel
|
||||
- 投影不拥有对象真相
|
||||
- 编辑器不等于对象模型
|
||||
- 外挂只消费 kernel 或 projection,不再私自定义第二套对象真相
|
||||
|
||||
### 5.6 术语冻结
|
||||
|
||||
这一轮同时把后续文档和任务要共用的术语冻结如下:
|
||||
|
||||
- `node`
|
||||
指统一内核中的 typed object
|
||||
- `edge`
|
||||
指节点之间的 typed relation
|
||||
- `projection`
|
||||
指从 kernel 派生出的视图结果,不是事实源
|
||||
- `subtree`
|
||||
指从某个 root node 出发的一段有界层级结构
|
||||
- `content node`
|
||||
指以正文载荷为主的节点,适合交给 BlockNote 之类编辑器处理
|
||||
- `reference edge`
|
||||
指非父子关系的引用边,例如页面引用、证据引用、来源引用
|
||||
- `summary node`
|
||||
指对某段 subtree 或 source node 做摘要后的节点
|
||||
- `index node`
|
||||
指为搜索/RAG 建立的结构索引节点
|
||||
|
||||
### 5.7 现阶段并入策略
|
||||
|
||||
当前主线按下面的口径迁移:
|
||||
|
||||
- 暂时继续存在,但应逐步并入 kernel 的对象:
|
||||
- 页面
|
||||
- Sidebar 数据集
|
||||
- 搜索结果对象
|
||||
- 导图树数据
|
||||
- 当前明确不是事实源、只保留为编辑或展示壳:
|
||||
- BlockNote 文档结构
|
||||
- Mindmap 前端画布状态
|
||||
- OnlyOffice 页面壳
|
||||
- 各类 AI host / panel 本地状态
|
||||
|
||||
---
|
||||
|
||||
## 6. 与当前系统的关系
|
||||
|
||||
## 6.1 与 Sidebar / 页面树 / 文件树的关系
|
||||
|
||||
这些都不应再视为独立系统。
|
||||
|
||||
长期应改成:
|
||||
|
||||
- 同一份结构内核
|
||||
- 在 Sidebar 中投影为导航树
|
||||
- 在文件页中投影为文件树
|
||||
- 在某些对象页中投影为结构树
|
||||
|
||||
所以:
|
||||
|
||||
> **Sidebar 不是“一个前端导航组件”,而是 kernel 的树投影。**
|
||||
|
||||
## 6.2 与 Mindmap 的关系
|
||||
|
||||
Mindmap 不再是中心,而是:
|
||||
|
||||
- kernel 的空间化图形视图
|
||||
- 适合做结构浏览、重组、章节展开、节点重排
|
||||
|
||||
但它不再是:
|
||||
|
||||
- 唯一主视图
|
||||
- 唯一对象真相
|
||||
|
||||
### 6.3 与 BlockNote 的关系
|
||||
|
||||
长期上,`BlockNote` 应从“系统底座”降级为:
|
||||
|
||||
- 内容编辑挂件
|
||||
- 某类页面内容编辑器
|
||||
|
||||
而不是:
|
||||
|
||||
- 页面结构本体
|
||||
- 工作区结构内核
|
||||
|
||||
也就是说,未来不是:
|
||||
|
||||
- 页面 = BlockNote 文档
|
||||
|
||||
而更接近:
|
||||
|
||||
- 页面 = kernel 子树
|
||||
- BlockNote = 某类内容节点的编辑器
|
||||
|
||||
### 6.4 与 AI 的关系
|
||||
|
||||
AI 不应直接面对“页面壳”和“前端控件”,而应直接面对 kernel。
|
||||
|
||||
长期上 AI 更适合操作:
|
||||
|
||||
- 节点
|
||||
- 子树
|
||||
- 引用边
|
||||
- 结构索引
|
||||
- 节点摘要
|
||||
|
||||
例如:
|
||||
|
||||
- 创建节点
|
||||
- 拆分章节为子树
|
||||
- 为节点补 refs
|
||||
- 生成 summary 节点
|
||||
- 把 PDF 章节树挂到 book 节点下
|
||||
|
||||
这比“模拟导图 UI 操作”或“模拟 BlockNote 操作”更稳定。
|
||||
|
||||
### 6.5 与 RAG 的关系
|
||||
|
||||
RAG 不再只是:
|
||||
|
||||
- 文本块检索
|
||||
|
||||
而应演进为:
|
||||
|
||||
- 结构索引检索 + 正文证据回查
|
||||
|
||||
例如:
|
||||
|
||||
- 一本书先变成书籍节点
|
||||
- 再生成章节树
|
||||
- 再生成章节子树摘要
|
||||
- 再用节点 refs 指回原始页码、附件、正文块
|
||||
|
||||
这样检索时可以:
|
||||
|
||||
1. 先命中结构层
|
||||
2. 再下钻子树
|
||||
3. 再回查原文证据
|
||||
|
||||
这正适合复杂文档。
|
||||
|
||||
---
|
||||
|
||||
## 7. 结合 BookRAG 的启发
|
||||
|
||||
用户提到的 `BookRAG` 给出的核心启发,不是“做一个导图页面”,而是:
|
||||
|
||||
> **复杂文档应该先被抽成层级结构索引,再做检索与生成。**
|
||||
|
||||
这和 mnote 非常契合。
|
||||
|
||||
### 7.1 书籍对象的建议形态
|
||||
|
||||
长期上可以采用:
|
||||
|
||||
- 一个 `book` 节点
|
||||
- 一个 canonical 章节树
|
||||
- 每章是一个 subtree
|
||||
- 每小节是更深层节点
|
||||
- 节点 refs 指向:
|
||||
- 页码
|
||||
- PDF
|
||||
- 原文块
|
||||
- 摘要
|
||||
- AI 生成说明
|
||||
|
||||
### 7.2 不建议直接复制很多份 chapter mindmap 实体
|
||||
|
||||
更好的方式是:
|
||||
|
||||
- 逻辑上是一棵 canonical tree
|
||||
- 章节导图只是 subtree projection
|
||||
- 需要时再做缓存或派生视图
|
||||
|
||||
否则会出现:
|
||||
|
||||
- 多份结构副本
|
||||
- 同步成本
|
||||
- 版本冲突
|
||||
|
||||
### 7.3 这和当前 Rust Mindmap 协议是兼容的
|
||||
|
||||
当前 Rust 已经有:
|
||||
|
||||
- `MindmapTreeNode`
|
||||
- `MindmapOp`
|
||||
- `mindmap_get_subtree`
|
||||
- `mindmap_outline_to_mindmap`
|
||||
|
||||
这意味着:
|
||||
|
||||
- 以树为真相
|
||||
- 以子树为检索和投影单位
|
||||
|
||||
并不是从零开始。
|
||||
|
||||
---
|
||||
|
||||
## 8. 这条路线和“主 Mindmap”有什么本质差异
|
||||
|
||||
两者差异很大。
|
||||
|
||||
### 8.1 错误路线
|
||||
|
||||
错误路线是:
|
||||
|
||||
- 整个工作区变成一个超级导图页面
|
||||
- 所有东西都围绕导图控件组织
|
||||
- UI 形态决定对象语义
|
||||
|
||||
### 8.2 正确路线
|
||||
|
||||
正确路线是:
|
||||
|
||||
- 整个工作区共享一份结构内核
|
||||
- Mindmap 只是可视化挂件
|
||||
- 列表、树表、阅读流、搜索结果也都是合法投影
|
||||
- 对象语义先于 UI 形态存在
|
||||
|
||||
---
|
||||
|
||||
## 9. 长期分层建议
|
||||
|
||||
## 9.1 Kernel Layer
|
||||
|
||||
Rust 内核负责:
|
||||
|
||||
- typed node
|
||||
- typed edge
|
||||
- subtree query
|
||||
- graph traversal
|
||||
- projection query
|
||||
- 权限
|
||||
- trace
|
||||
- 版本
|
||||
- 审计
|
||||
|
||||
## 9.2 Service Layer
|
||||
|
||||
Rust Web 层负责:
|
||||
|
||||
- API
|
||||
- SSR 页面壳
|
||||
- SSE / WS
|
||||
- 结构查询
|
||||
- AI bridge
|
||||
- projection 请求分发
|
||||
|
||||
## 9.3 Projection Layer
|
||||
|
||||
不同前端视图负责:
|
||||
|
||||
- Sidebar tree projection
|
||||
- 阅读页 projection
|
||||
- Mindmap projection
|
||||
- 搜索 projection
|
||||
- AI 操作 projection
|
||||
|
||||
## 9.4 Editor Layer
|
||||
|
||||
编辑器只是挂件:
|
||||
|
||||
- BlockNote
|
||||
- Mindmap canvas
|
||||
- OnlyOffice
|
||||
- 未来别的专用编辑器
|
||||
|
||||
它们都不再是系统底座。
|
||||
|
||||
---
|
||||
|
||||
## 10. 为什么这条路线更适合替代 BlockNote 世界
|
||||
|
||||
因为它不是“再造一个更大的前端编辑器”,而是:
|
||||
|
||||
- 先把页面结构、对象结构和引用结构收口
|
||||
- 再让 BlockNote 退化成一个专用内容编辑挂件
|
||||
|
||||
长期上,页面不再被定义成:
|
||||
|
||||
- 一个 block 文档
|
||||
|
||||
而更接近:
|
||||
|
||||
- 一个子树容器
|
||||
|
||||
这样未来才可能逐步实现:
|
||||
|
||||
- 页面结构独立于 BlockNote
|
||||
- 页面中的某些内容节点仍可用 BlockNote 编辑
|
||||
- 某些结构节点则改用别的编辑/操作方式
|
||||
|
||||
这比一次性整体替掉 BlockNote 更现实。
|
||||
|
||||
---
|
||||
|
||||
## 11. 风险与约束
|
||||
|
||||
### 11.1 不要把整个 workspace 真存成一条超大 JSON 树
|
||||
|
||||
逻辑上统一成一棵树,不等于物理上只能是一条大对象。
|
||||
|
||||
长期更合理的是:
|
||||
|
||||
- 逻辑统一
|
||||
- 物理分片
|
||||
- 子树加载
|
||||
- 局部版本
|
||||
- 局部缓存
|
||||
|
||||
### 11.2 不要让 Projection 反向定义内核
|
||||
|
||||
例如:
|
||||
|
||||
- Mindmap 控件的数据格式
|
||||
- BlockNote 的块数据结构
|
||||
- Sidebar 某次渲染需要的 rows
|
||||
|
||||
这些都不能反过来定义 kernel 真相。
|
||||
|
||||
### 11.3 不要过早把所有内容节点都树化成同一种文本节点
|
||||
|
||||
结构树适合表达:
|
||||
|
||||
- 层级
|
||||
- 目录
|
||||
- 引用
|
||||
- 摘要
|
||||
- 索引
|
||||
|
||||
但富文本正文仍然可能需要自己的内容模型。
|
||||
|
||||
所以长期更合理的是:
|
||||
|
||||
- 树/图内核负责结构
|
||||
- 内容节点负责正文
|
||||
- 两者通过 typed node 接口连接
|
||||
|
||||
---
|
||||
|
||||
## 12. 最终结论
|
||||
|
||||
最终结论可以固定成下面这句话:
|
||||
|
||||
> **mnote 的长期方向不是 Mindmap-first,而是 Tree-First Graph Kernel;Mindmap 只是其中一种挂件、投影和操作器。**
|
||||
|
||||
这意味着:
|
||||
|
||||
- 页面树、文件树、Mindmap、RAG 结构索引、AI 结构操作,本质上都应收口到同一结构内核
|
||||
- `BlockNote` 不再是系统定义页面的唯一方式
|
||||
- Rust 最终不只是承接 API 或导图对象,而是承接整个统一结构真相
|
||||
|
||||
如果后续继续推进,真正该优先做的不是“先重写导图 UI”,而是:
|
||||
|
||||
1. 定义统一 kernel node / edge 模型
|
||||
2. 定义 subtree / projection / reference 查询协议
|
||||
3. 让 Sidebar、搜索、AI、Mindmap 开始直接消费 kernel
|
||||
4. 最后再逐步边缘化 `BlockNote`
|
||||
+500
@@ -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 接住统一主链。**
|
||||
@@ -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
|
||||
```
|
||||
@@ -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/<document_id>` 能看到 Rust/kernel owner 头;使用真实 id 验证。
|
||||
|
||||
## 阶段 B:`page.*` 成为页面默认写命令族
|
||||
|
||||
**目标:** 页面标题、页面设置、正文保存默认通过 `page.*` 命令进入 Rust kernel/bridge;`documents.*` 只作为兼容 alias 继续被测试覆盖。
|
||||
|
||||
**涉及文件:**
|
||||
- Modify: `rust/crates/mnote-web/src/routes/documents.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/routes/tree.rs`
|
||||
- Modify: `rust/crates/bridge-runtime/src/lib.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/routes/web_shell.rs`
|
||||
- Modify: `wolai-frontend/src/components/editor/DocumentEditorIsland.runtime.tsx`
|
||||
- Modify: `wolai-frontend/src/components/editor/DocumentTitle*.tsx`
|
||||
- Test: `rust/crates/bridge-runtime/src/lib.rs`
|
||||
- Test: `rust/crates/mnote-web/src/routes/documents.rs`
|
||||
- Test: `scripts/task121-rust-web-editor-island-hydration-smoke.js`
|
||||
- Test: `scripts/task122-rust-web-create-page-ui-smoke.js`
|
||||
|
||||
**Checklist:**
|
||||
- [x] 将 `rust/crates/mnote-web/src/routes/documents.rs` 的正文保存默认命令从 `documents.save` 改为 `page.body.save`。
|
||||
- [x] 保留 `/api/documents/save` HTTP route 名称作为迁移期兼容入口,但响应里标注 executed command 为 `page.body.save`。
|
||||
- [x] 为标题保存写面确认或新增 Rust Web route,内部命令统一使用 `page.head.updateTitle`。
|
||||
- [x] 为页面布局/设置写面确认或新增 Rust Web route,内部命令统一使用 `page.layout.updateOptions`。
|
||||
- [x] 在 `bridge-runtime` tests 中保留 `documents.save`、`documents.title.update`、`documents.options.update` alias 映射测试,证明旧命名不会静默断链。
|
||||
- [x] 在 3000 文档页 island 保存路径中加入 command family observability,页面保存后可从响应或调试日志确认 `page.*`。
|
||||
- [x] 新增 smoke:创建页面、改标题、输入正文、刷新页面后标题和正文仍一致。
|
||||
- [x] 新增 smoke:旧 compat alias 请求仍返回兼容成功,但响应里声明 canonical command 为 `page.*`。
|
||||
- [x] 将设计文档中仍要求 `documents.*` 作为主链的段落移动到对应 `old/process`,并标记为 `[recycle]`。
|
||||
|
||||
**验收命令:**
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/rust
|
||||
cargo test -p bridge-runtime page_body_save
|
||||
cargo test -p bridge-runtime page_head_update_title
|
||||
cargo test -p bridge-runtime page_layout_update_options
|
||||
cargo test -p mnote-web documents_save
|
||||
cd /mnt/Data1T/mnote
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task121-rust-web-editor-island-hydration-smoke.js
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task122-rust-web-create-page-ui-smoke.js
|
||||
```
|
||||
|
||||
**完成定义:**
|
||||
- [x] 新页面创建、标题保存、正文保存、刷新恢复均走 3000。
|
||||
- [x] `rg -n "documents\.save|documents\.title\.update|documents\.options\.update" rust/crates/mnote-web/src` 只剩兼容入口、alias 测试或明确迁移注释。
|
||||
- [x] `bridge-runtime` 对 `documents.*` 的支持被标记为 compat alias,不再被主链调用。
|
||||
|
||||
## 阶段 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=<keyword>` 时首屏 HTML 已包含匹配结果。
|
||||
- [x] 增加 smoke:键盘打开搜索、输入关键字、选择结果后跳转到 3000 文档页。
|
||||
|
||||
**验收命令:**
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/rust
|
||||
cargo test -p core-protocol search
|
||||
cargo test -p bridge-runtime search_documents_query
|
||||
cargo test -p mnote-web search
|
||||
cd /mnt/Data1T/mnote
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task125-rust-web-search-server-first-smoke.js
|
||||
```
|
||||
|
||||
**完成定义:**
|
||||
- [x] `curl http://127.0.0.1:3000/search?q=<keyword>` 的 HTML 中包含 server-rendered result list。
|
||||
- [x] `rust/crates/mnote-web/src/routes/search.rs` 的 contract 不再声明主 runtime 为 `react_search_palette`。
|
||||
- [x] React 搜索组件只负责交互增强,不拥有检索真相或排序真相。
|
||||
|
||||
## 阶段 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:<temp-port> node scripts/task123-rust-web-tree-live-stream-consumer-smoke.js
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:<temp-port> node scripts/task125-rust-web-search-server-first-smoke.js
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:<temp-port> node scripts/task126-rust-web-ai-bridge-structured-write-smoke.js
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:<temp-port> node scripts/task127-rust-web-mindmap-kernel-projection-smoke.js
|
||||
```
|
||||
+328
@@ -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 继续扩写长期树命令语义。
|
||||
+32
@@ -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 验证边界。
|
||||
+37
@@ -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`。
|
||||
+37
@@ -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。
|
||||
+31
@@ -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 与状态机;当前已把本批可验证主路径收薄记录为阶段完成。
|
||||
@@ -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。
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
# 4-2 [done] Sidebar / 页面树 / 文件树 产品交互合同 v1
|
||||
|
||||
> 更新时间:2026-04-17
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-1-sidebar-pagetree-filetree-product-gap-analysis-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档不是继续讨论“是否要做 Rust tree shell”。
|
||||
|
||||
这份文档要冻结的是:
|
||||
|
||||
- 页面树对标 Wolai / Notion 的最小产品交互合同
|
||||
- 文件树对标 VS Code Explorer 的最小产品交互合同
|
||||
- 当前旧树已经具备的能力基线
|
||||
- 新 Rust tree shell 必须补齐的能力矩阵
|
||||
- 后续 `projection / command / row model / selection model / focus model / keyboard / DnD / context menu` 的最低验收口径
|
||||
|
||||
也就是说,这份文档是下一阶段多人并行推进时的共同合同,而不是描述性分析。
|
||||
|
||||
---
|
||||
|
||||
## 2. 基本原则
|
||||
|
||||
- 页面树 / 文件树都不是事实源,它们都只是 `tree-first graph kernel` 的 projection。
|
||||
- Sidebar 是壳,不是树真相。
|
||||
- 新实现不能以“能显示树结构”作为完成标准,而要以“不比旧交互与 UI 差”作为最低标准。
|
||||
- 文件树与页面树允许在 UI 上不同,但必须共享同一套 projection 与 command 主骨架。
|
||||
- 所有新增能力都应优先落到可测试的 `row model / selection model / focus model / keyboard / DnD` 层,而不是先堆散落 UI 事件。
|
||||
|
||||
---
|
||||
|
||||
## 3. 页面树合同
|
||||
|
||||
### 3.1 对标目标
|
||||
|
||||
- 功能对标:Wolai / Notion 页面树
|
||||
- 视觉与节奏对标:轻量、低干扰、hover 才显动作、不是调试面板
|
||||
|
||||
### 3.2 页面树必须具备的最低能力
|
||||
|
||||
- 稳定的页面层级展开 / 折叠
|
||||
- 当前页高亮与祖先自动展开
|
||||
- 行级 hover 动作区
|
||||
- 新建子页面
|
||||
- 重命名
|
||||
- 页面移动
|
||||
- 上下文菜单入口
|
||||
- 焦点与键盘导航
|
||||
- 基础拖拽排序
|
||||
- 搜索过滤后仍保持树层级可理解
|
||||
|
||||
### 3.3 页面树必须保留的旧能力基线
|
||||
|
||||
- 右键菜单不是只有重命名/删除,而应保留工作区级高频动作入口
|
||||
- 页面树不能退化成纯按钮列表
|
||||
- 大树场景不能因切流而失去稳定滚动体验
|
||||
- 主树 consumer 不能重新持有第二套结构真相
|
||||
|
||||
---
|
||||
|
||||
## 4. 文件树合同
|
||||
|
||||
### 4.1 对标目标
|
||||
|
||||
- 功能对标:VS Code Explorer
|
||||
- 视觉密度对标:资源管理器,而不是文档树换皮
|
||||
|
||||
### 4.2 文件树必须具备的最低能力
|
||||
|
||||
- 文件树专用 row model
|
||||
- 页面、`index.md`、附件、mindmap 文件夹、导图子附件的复合资源层级
|
||||
- 单选
|
||||
- 多选
|
||||
- Shift 范围选
|
||||
- 右键菜单入口
|
||||
- 双击打开资源
|
||||
- 基础键盘导航
|
||||
- 目录拖拽骨架
|
||||
- 外部文件拖入上传骨架
|
||||
- 资源图标语义
|
||||
- 资源类型菜单分支
|
||||
|
||||
### 4.3 文件树必须保留的旧能力基线
|
||||
|
||||
- 不能退化成“页面树加附件列表”
|
||||
- 不能丢掉多选与范围选
|
||||
- 不能丢掉资产级操作入口
|
||||
- 不能丢掉内部拖拽/复制与外部文件拖入的扩展空间
|
||||
- mindmap 相关资源不能被拍平成普通附件列表
|
||||
|
||||
---
|
||||
|
||||
## 5. 统一协议合同
|
||||
|
||||
### 5.1 Projection 合同
|
||||
|
||||
所有树 consumer 必须能明确声明自己消费哪一种 projection:
|
||||
|
||||
- `sidebar_tree`
|
||||
- `page_tree`
|
||||
- `file_tree`
|
||||
|
||||
最小字段基线:
|
||||
|
||||
- `row_id`
|
||||
- `node_id`
|
||||
- `parent_node_id`
|
||||
- `node_type`
|
||||
- `projection_kind`
|
||||
- `depth`
|
||||
- `position`
|
||||
- `title`
|
||||
- `capabilities`
|
||||
- `resource_meta`
|
||||
|
||||
文件树扩展字段:
|
||||
|
||||
- `resource_kind`
|
||||
- `asset_kind`
|
||||
- `icon_hint`
|
||||
- `expandable`
|
||||
- `expanded_by_default`
|
||||
|
||||
### 5.2 Command 合同
|
||||
|
||||
命令面至少要为后续产品交互预留稳定口径:
|
||||
|
||||
- `tree.node.create`
|
||||
- `tree.node.rename`
|
||||
- `tree.subtree.move`
|
||||
- `tree.node.archive`
|
||||
- `tree.node.restore`
|
||||
- `tree.asset.attach`
|
||||
- `tree.asset.detach`
|
||||
|
||||
### 5.3 本地 UI 状态合同
|
||||
|
||||
以下状态不应回流为结构真相,只能留在 UI 本地状态层:
|
||||
|
||||
- `expanded`
|
||||
- `selected`
|
||||
- `hover`
|
||||
- `focus`
|
||||
- `dragging`
|
||||
- `drop target`
|
||||
|
||||
---
|
||||
|
||||
## 6. 状态骨架合同
|
||||
|
||||
### 6.1 row model
|
||||
|
||||
必须单独存在,不能散落在 renderer 中。
|
||||
|
||||
最低要求:
|
||||
|
||||
- 页面树与文件树都能从 projection 映射到稳定 row model
|
||||
- row model 可以独立测试
|
||||
- row model 不再重新定义结构真相
|
||||
|
||||
### 6.2 selection model
|
||||
|
||||
最低要求:
|
||||
|
||||
- 单选
|
||||
- 多选
|
||||
- Shift 范围选
|
||||
- 右键选中
|
||||
- 可见行变化后的选择归一化
|
||||
|
||||
### 6.3 focus model
|
||||
|
||||
最低要求:
|
||||
|
||||
- 当前焦点行稳定可追踪
|
||||
- 焦点与选中不完全等价
|
||||
- 页面树与文件树都能共享焦点层定义
|
||||
|
||||
### 6.4 keyboard
|
||||
|
||||
最低要求:
|
||||
|
||||
- 上下导航
|
||||
- 左右展开/折叠
|
||||
- Enter 打开
|
||||
- 文件树预留 copy / paste / delete 的按键入口
|
||||
|
||||
### 6.5 DnD
|
||||
|
||||
最低要求:
|
||||
|
||||
- 页面树支持基础排序/移动
|
||||
- 文件树支持目录拖放骨架
|
||||
- 文件树支持外部文件拖入的扩展点
|
||||
- 非法投放校验必须可测试
|
||||
|
||||
### 6.6 context menu
|
||||
|
||||
最低要求:
|
||||
|
||||
- 页面树与文件树都有统一的 context menu 入口模型
|
||||
- 菜单本身可按资源类型分支
|
||||
- shell 与宿主之间要有稳定动作协议,而不是只靠壳内 `prompt/alert`
|
||||
|
||||
---
|
||||
|
||||
## 7. 当前判定
|
||||
|
||||
截至 2026-04-28 final DOM shell 收口后:
|
||||
|
||||
- `page_tree`:默认主路径已进入 `rust_wasm_dom_shell_host + dom_wasm`,展开/折叠、当前页高亮、祖先展开、行级动作、重命名、移动、上下文菜单、键盘与基础拖拽均由 Rust runtime contract 驱动或回放。
|
||||
- `picker`:轻量选择器已复用同一 renderer state family,键盘高亮、Enter 选中、根节点与排除项逻辑已进入默认 DOM host。
|
||||
- `file_tree`:默认主路径已进入 `rust_wasm_dom_shell_host + dom_wasm`,页面、`index.md`、附件、`mindmap` 文件夹、资源图标、单选/多选/范围选、右键、双击打开、内部拖放与外部文件拖入均保留。
|
||||
|
||||
本合同的产品交互最低门槛已由 `4-11` 与 `4-18` 收口;剩余 `normalizedMove` fallback、`document.snapshot.saved` 独立事件、`tree.node.embed pageReference` 等属于后续 Page Aggregate / runtime 深层收口,不再阻塞本文。
|
||||
|
||||
- `filetree` 模式闭环已由 Rust projection、runtime state family、preflight 与默认 DOM host 共同承接。
|
||||
- 页面树 / 文件树正式交互合同已固定,并由 `task112/task113` smoke 与组件/Rust 测试覆盖。
|
||||
- row model / selection model / focus model / keyboard / DnD / context menu 骨架已进入 renderer state family 与 runtime facade。
|
||||
|
||||
---
|
||||
|
||||
## 8. 完成判定
|
||||
|
||||
只有同时满足下面几条,才可以宣称“新树不比旧交互与 UI 差”:
|
||||
|
||||
- 页面树满足本合同第 3 节最低能力
|
||||
- 文件树满足本合同第 4 节最低能力
|
||||
- projection / command 合同不再漂移
|
||||
- `row model / selection model / focus model / keyboard / DnD / context menu` 有独立实现与测试
|
||||
- filetree 不再依赖协议裂缝或壳内临时拼装来维持主路径
|
||||
@@ -0,0 +1,218 @@
|
||||
# 4-4 [done] Tree Projection Protocol Contract v1
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
>
|
||||
> 关联:
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
|
||||
## 1. 目的
|
||||
|
||||
这份文档用于冻结树域 projection contract,避免 `sidebar_tree`、`page_tree`、`file_tree` 在后续 Rust route、Leptos tree shell、Next 挂载切流阶段继续各自长字段。
|
||||
|
||||
这里固定三条原则:
|
||||
|
||||
- 树域只消费 projection,不消费前端自己拼出来的结构真相
|
||||
- Rust 与前端共享同一组 projection 字段语义
|
||||
- `sidebar_tree`、`page_tree`、`file_tree` 是同一协议家族,不是三套无关返回值
|
||||
|
||||
## 2. 协议分层
|
||||
|
||||
### 2.1 Rust canonical contract
|
||||
|
||||
Rust 侧的 canonical contract 统一表达为:
|
||||
|
||||
- `projection_id`
|
||||
- `projection`
|
||||
- `root_node_id`
|
||||
- `items`
|
||||
- `edges`
|
||||
|
||||
`items` 里的字段语义冻结如下。
|
||||
|
||||
### 2.2 TypeScript transport contract
|
||||
|
||||
前端 TypeScript 侧继续使用 camelCase 字段,但语义必须与 Rust 一致:
|
||||
|
||||
- `projectionId`
|
||||
- `projectionKind`
|
||||
- `rootNodeId`
|
||||
- `parentNodeId`
|
||||
- `resourceMeta`
|
||||
- `iconHint`
|
||||
- `expandedByDefault`
|
||||
|
||||
也就是说:
|
||||
|
||||
- Rust 是 canonical truth
|
||||
- TypeScript 只是命名风格映射,不允许再引入第二套语义
|
||||
|
||||
## 3. 共享字段
|
||||
|
||||
下面这些字段属于 `sidebar_tree`、`page_tree`、`file_tree` 的共享基线。
|
||||
|
||||
| canonical | TS | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `node_id` | `nodeId` | 当前 projection item 对应的 kernel node |
|
||||
| `parent_node_id` | `parentNodeId` | 上级 node,根节点为 `null` |
|
||||
| `node_type` | `nodeType` | `workspace/folder/page/section/asset/book/pdf/mindmap/table/index` 等语义类型 |
|
||||
| `projection_kind` | `projectionKind` | 当前 item 属于哪种 projection:`sidebar_tree/page_tree/file_tree` |
|
||||
| `title` | `title` | 展示标题,允许兜底为“无标题” |
|
||||
| `depth` | `depth` | 当前树深度 |
|
||||
| `position` | `position` | 同级排序位置 |
|
||||
| `child_count` | `childCount` | 子项数量 |
|
||||
| `expandable` | `expandable` | 当前 item 是否理论上可展开 |
|
||||
| `expanded_by_default` | `expandedByDefault` | 默认展开建议 |
|
||||
| `capabilities` | `capabilities` | 当前行允许的交互能力 |
|
||||
| `resource_meta` | `resourceMeta` | 绑定到业务资源的元信息 |
|
||||
| `icon_hint` | `iconHint` | 给 shell / renderer 的图标提示 |
|
||||
|
||||
## 4. `capabilities` 冻结口径
|
||||
|
||||
当前允许的共享 `capabilities` 为:
|
||||
|
||||
- `expand`
|
||||
- `open`
|
||||
- `drag`
|
||||
- `drop`
|
||||
- `select`
|
||||
- `create-child`
|
||||
- `rename`
|
||||
- `archive`
|
||||
- `restore`
|
||||
- `context-menu`
|
||||
- `reorder`
|
||||
- `open-asset`
|
||||
- `pick`
|
||||
|
||||
约束:
|
||||
|
||||
- `capabilities` 只表达“允许做什么”
|
||||
- 它不表达局部 UI 状态
|
||||
- 它不表达 hover、selected、dragging、drop target 这类临时态
|
||||
|
||||
## 5. `resource_meta` 冻结口径
|
||||
|
||||
`resource_meta` 统一用于表达 projection item 背后的真实资源。
|
||||
|
||||
共享字段:
|
||||
|
||||
- `resource_kind`
|
||||
- `document_id`
|
||||
- `asset_id`
|
||||
- `workspace_id`
|
||||
- `asset_kind`
|
||||
- `icon_hint`
|
||||
- `extra`
|
||||
|
||||
### 5.1 `resource_kind`
|
||||
|
||||
当前冻结为:
|
||||
|
||||
- `workspace`
|
||||
- `document`
|
||||
- `index`
|
||||
- `asset`
|
||||
- `asset_folder`
|
||||
- `mindmap`
|
||||
- `table`
|
||||
- `book`
|
||||
- `pdf`
|
||||
|
||||
### 5.2 `asset_kind`
|
||||
|
||||
当前冻结为:
|
||||
|
||||
- `file`
|
||||
- `mindmap`
|
||||
- `table`
|
||||
- `book`
|
||||
- `pdf`
|
||||
- `image`
|
||||
- `video`
|
||||
- `audio`
|
||||
- `unknown`
|
||||
|
||||
## 6. 三类 projection 的差异字段
|
||||
|
||||
### 6.1 `sidebar_tree`
|
||||
|
||||
`sidebar_tree` 是最轻的导航 projection。
|
||||
|
||||
要求:
|
||||
|
||||
- 保留共享字段
|
||||
- 不引入 `row_id`
|
||||
- 资源主语义通常落在 `resource_kind=document`
|
||||
- 允许后续继续作为 workspace 首屏 / stream snapshot 主链
|
||||
|
||||
### 6.2 `page_tree`
|
||||
|
||||
`page_tree` 是页面树 projection。
|
||||
|
||||
它在共享字段基础上新增:
|
||||
|
||||
- `row_id`
|
||||
|
||||
当前 `row_id` 命名约束:
|
||||
|
||||
- `page:<nodeId>`
|
||||
|
||||
要求:
|
||||
|
||||
- `page_tree` 必须可直接生成 picker 轻量列表
|
||||
- `page_tree` 必须可直接生成可见 rows
|
||||
- `page_tree` 不允许重新回退成前端自行 flatten 嵌套树
|
||||
|
||||
### 6.3 `file_tree`
|
||||
|
||||
`file_tree` 是页面树骨架上的更宽对象投影。
|
||||
|
||||
它在共享字段基础上强调这些字段必须稳定存在:
|
||||
|
||||
- `resource_kind`
|
||||
- `asset_kind`
|
||||
- `icon_hint`
|
||||
- `expandable`
|
||||
- `expanded_by_default`
|
||||
|
||||
要求:
|
||||
|
||||
- `file_tree` 不能继续由前端用 `asset-folder` / `asset` / `index` 临时猜语义作为长期真相
|
||||
- `file_tree` 必须由 Rust 直接输出 `document/index/asset/asset_folder/mindmap/table/book/pdf` 等对象投影
|
||||
|
||||
## 7. 当前稳定项与过渡项
|
||||
|
||||
### 7.1 已稳定
|
||||
|
||||
- `sidebar_tree` 基础 contract
|
||||
- `page_tree` 的 `row_id/node_id/parent_node_id/depth/position/capabilities/resource_meta`
|
||||
- `expanded_by_default`
|
||||
- `child_count`
|
||||
|
||||
### 7.2 仍处于过渡
|
||||
|
||||
- `file_tree` 里的 `asset_folder/asset/index` 仍有前端 adapter 残留
|
||||
- `icon_hint` 还没有完全由 Rust 主导
|
||||
- `asset_kind` 仍需要从更宽对象类型扩展到 `book/pdf`
|
||||
|
||||
## 8. Fixture 与 Contract Test 要求
|
||||
|
||||
后续所有树域 fixture / contract tests 至少覆盖:
|
||||
|
||||
- `sidebar_tree` 基础 fixture
|
||||
- `page_tree` fixture
|
||||
- `file_tree` fixture
|
||||
- `resource_meta` 差异字段
|
||||
- `capabilities`
|
||||
- `icon_hint`
|
||||
- `expanded_by_default`
|
||||
|
||||
## 9. 完成判定
|
||||
|
||||
当以下条件同时成立时,视为本 contract 冻结完成:
|
||||
|
||||
- `sidebar_tree`、`page_tree`、`file_tree` 的共享字段与差异字段都已写成文档与共享类型
|
||||
- Rust 与前端共享同一组字段语义
|
||||
- `file_tree` 不再依赖前端“补字段猜语义”
|
||||
- 后续 shell / renderer / route 改造不再新增破坏性字段
|
||||
@@ -0,0 +1,174 @@
|
||||
# 4-5 [done] Tree Command Envelope 第一批收口方案 v1
|
||||
|
||||
> 更新时间:2026-04-17
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
这份文档用于固定 Stage B-2 的真实边界:
|
||||
|
||||
- 前端已经不应该再扩散树命令语义。
|
||||
- 但当前仓库里,浏览器组件层仍然散落着大量 `fetch("/api/documents/...")` 入口。
|
||||
- 第一批要先收口 `create / rename / move`,把前端限制为 optimistic UI 与用户交互壳。
|
||||
|
||||
这里的核心口径是:
|
||||
|
||||
> 前端只保留 optimistic UI,真语义和 command envelope 收口到 Rust/bridge。
|
||||
|
||||
## 2. 当前命令入口盘点
|
||||
|
||||
### 2.1 Sidebar 主入口
|
||||
|
||||
当前树命令入口主要集中在:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/menus/CustomSideMenu.tsx`
|
||||
|
||||
其中:
|
||||
|
||||
- `sidebar.tsx`
|
||||
- `create` 走 `/api/documents/create`
|
||||
- `rename` 走 `/api/documents/title`
|
||||
- `move` 走 `/api/documents/move`
|
||||
- `delete` 走 `/api/documents/delete`
|
||||
- `restore` 走 `/api/documents/restore`
|
||||
- `purge` 走 `/api/documents/purge`
|
||||
- `embed` 走 `/api/documents/embed`
|
||||
- `copy-tree` 走 `/api/documents/copy-tree`
|
||||
- `document-content.tsx`
|
||||
- 文档标题更新走 `/api/documents/title`
|
||||
- 移动页走 `/api/documents/move`
|
||||
- 页面嵌入走 `/api/documents/embed`
|
||||
- `CustomSideMenu.tsx`
|
||||
- block 转子页面走 `/api/documents/create-child`
|
||||
- `pageReference` 删除走 `/api/documents/delete`
|
||||
|
||||
### 2.2 当前哪些 route 已接到 envelope
|
||||
|
||||
第一批最关键的三条主路径,已经接入 `buildDocumentCommandEnvelope`:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/create/route.ts`
|
||||
- 命令名:`documents.create`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/title/route.ts`
|
||||
- 命令名:`documents.title.update`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/move/route.ts`
|
||||
- 命令名:`documents.move`
|
||||
|
||||
当前真实执行链路是:
|
||||
|
||||
1. Next route 构造 `buildDocumentCommandEnvelope`
|
||||
2. `resolveRustBridgeCommandPlan`
|
||||
3. TS runtime 负责 transport dispatch
|
||||
4. Convex mutation 负责实际持久化
|
||||
|
||||
因此当前形态是:
|
||||
|
||||
- `Convex substrate`
|
||||
- `Rust semantic owner`
|
||||
- `TS runtime transport dispatcher`
|
||||
|
||||
不是“Rust 已经直接替代 Convex 落库”。
|
||||
|
||||
### 2.3 当前仍留在前端的树语义
|
||||
|
||||
虽然 route 已接入 envelope,但浏览器端仍残留较多树语义:
|
||||
|
||||
- `sidebar.tsx`
|
||||
- 本地 `insertNode` / `moveLocalNode`
|
||||
- 非法拖拽判断
|
||||
- `targetParentId` / `position` 推导
|
||||
- `copy-tree` 的递归策略与拖放目标推导
|
||||
- `document-content.tsx`
|
||||
- 移动页时仍由前端给出固定 `position`
|
||||
- `CustomSideMenu.tsx`
|
||||
- block 转子页面的 UI 级决策仍在前端
|
||||
|
||||
这说明当前主问题已经不是 route 是否接 bridge,而是:
|
||||
|
||||
> 命令 transport 已经收口,但命令入口和部分树策略仍散落在前端组件层。
|
||||
|
||||
## 3. 第一批 cutover 范围
|
||||
|
||||
Stage B-2 第一批只处理:
|
||||
|
||||
- `create`
|
||||
- `rename`
|
||||
- `move`
|
||||
|
||||
原因:
|
||||
|
||||
- 这三条覆盖 sidebar 主交互、文档页主交互、block 转页入口。
|
||||
- 它们已经具备稳定的 route -> envelope -> Rust bridge -> Convex 主链。
|
||||
- 继续向前推进时,前端只需要保留 optimistic UI 和错误提示,不需要继续扩散 transport 细节。
|
||||
|
||||
## 4. 第一批 cutover 顺序
|
||||
|
||||
推荐固定为两段:
|
||||
|
||||
### 4.1 第一段:先 `create / rename / move`
|
||||
|
||||
目标:
|
||||
|
||||
- 浏览器组件不再直接散落 `fetch("/api/documents/create|title|move")`
|
||||
- 收成共享的 tree command client
|
||||
- 让 `sidebar.tsx`、`document-content.tsx`、`CustomSideMenu.tsx` 只表达交互和 optimistic UI
|
||||
|
||||
这一步已经开始落地:
|
||||
|
||||
- 新增共享入口:
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tree-command-client.ts`
|
||||
- 第一批已接入的组件:
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/menus/CustomSideMenu.tsx`
|
||||
|
||||
### 4.2 第二段:再 `delete / restore / purge / embed`
|
||||
|
||||
原因:
|
||||
|
||||
- `delete / restore / purge` 已接 envelope,但仍有多处页面级分叉入口。
|
||||
- `embed` 当前最特殊,它不是独立 `documents.embed` 命令,而是 TS adapter 里拼 `pageReference` 后走 `documents.save`。
|
||||
- `embed` 必须等 Rust kernel 定义正式页面嵌入语义后,再从“TS adapter 语义”切成“Rust tree command 语义”。
|
||||
|
||||
## 5. 不纳入第一批的内容
|
||||
|
||||
以下内容先不纳入 Stage B-2 第一批:
|
||||
|
||||
- `copy-tree`
|
||||
- 当前仍强依赖前端拖放和递归策略
|
||||
- block 级 `blocks.move / blocks.embed`
|
||||
- 这是块域命令,不是页面树命令主链
|
||||
- 树排序算法本身
|
||||
- 当前仍有一部分位置推导在前端,后续要继续收回 Rust kernel
|
||||
|
||||
## 6. 长期边界说明
|
||||
|
||||
长期固定如下:
|
||||
|
||||
- 前端负责:
|
||||
- 用户交互
|
||||
- optimistic UI
|
||||
- 选择器、确认框、拖拽体验
|
||||
- Next route / shared client 负责:
|
||||
- 稳定 transport 边界
|
||||
- 请求格式统一
|
||||
- Rust kernel / bridge 负责:
|
||||
- `tree command` 语义
|
||||
- `page_tree / sidebar_tree / file_tree` 真相
|
||||
- 审计、trace、版本口径
|
||||
- Convex 负责:
|
||||
- 持久化
|
||||
- 订阅
|
||||
- 实时底座
|
||||
|
||||
## 7. 下一步建议
|
||||
|
||||
Stage B-2 后续应继续做三件事:
|
||||
|
||||
1. 把 `delete / restore / purge` 也收成同一层 shared tree command client。
|
||||
2. 为 `embed` 定义独立的 Rust page-tree 命令,而不是继续停留在 TS adapter + `documents.save`。
|
||||
3. 把 `targetParentId / sortOrder / subtree move legality` 等策略继续从前端移回 Rust kernel。
|
||||
@@ -0,0 +1,102 @@
|
||||
# 4-6 [done] Tree Command Protocol Cutover Stage 2 v1
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
>
|
||||
> 目的:冻结树域 command protocol 的长期命名面,并明确 `documents.*` 到 `tree.*` 的兼容迁移口径。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
长期命名面不再继续扩大 `documents.*`。
|
||||
|
||||
固定迁移方向为:
|
||||
|
||||
- 兼容层继续保留 `documents.*`
|
||||
- 长期正式协议统一切到 `tree.*`
|
||||
|
||||
## 2. 长期正式命名
|
||||
|
||||
冻结如下:
|
||||
|
||||
- `tree.node.create`
|
||||
- `tree.node.rename`
|
||||
- `tree.node.archive`
|
||||
- `tree.node.restore`
|
||||
- `tree.node.purge`
|
||||
- `tree.subtree.move`
|
||||
- `tree.subtree.copy`
|
||||
- `tree.asset.attach`
|
||||
- `tree.asset.detach`
|
||||
- `tree.node.embed`
|
||||
|
||||
## 3. 当前兼容映射
|
||||
|
||||
| 兼容命名 | 长期命名 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `documents.create` | `tree.node.create` | 新建页面 |
|
||||
| `documents.title.update` | `tree.node.rename` | 重命名页面 |
|
||||
| `documents.move` | `tree.subtree.move` | 移动子树 |
|
||||
| `documents.delete` | `tree.node.archive` | 软删除进入回收站 |
|
||||
| `documents.restore` | `tree.node.restore` | 从回收站恢复 |
|
||||
| `documents.purge` | `tree.node.purge` | 永久删除 |
|
||||
| `documents.embed` | `tree.node.embed` | 嵌入页面 |
|
||||
| `documents.copy_tree` | `tree.subtree.copy` | 复制树 |
|
||||
|
||||
## 4. Cutover 顺序
|
||||
|
||||
### Stage 2A
|
||||
|
||||
- 先在 Rust route / bridge 上接受 `tree.*`
|
||||
- 同时保留 `documents.*` alias
|
||||
- 前端 command client 开始显式知道两套名字的对应关系
|
||||
|
||||
### Stage 2B
|
||||
|
||||
- 主调用路径默认发 `tree.*`
|
||||
- 兼容入口只用于旧 route / 旧测试 / 旧调试脚本
|
||||
|
||||
### Stage 2C
|
||||
|
||||
- 删除主路径对 `documents.*` 的依赖
|
||||
- 仅保留极薄 alias,或在最终阶段删除 alias
|
||||
|
||||
## 5. 命名边界
|
||||
|
||||
### 5.1 `tree.node.*`
|
||||
|
||||
用于单节点生命周期命令:
|
||||
|
||||
- `tree.node.create`
|
||||
- `tree.node.rename`
|
||||
- `tree.node.archive`
|
||||
- `tree.node.restore`
|
||||
- `tree.node.purge`
|
||||
- `tree.node.embed`
|
||||
|
||||
### 5.2 `tree.subtree.*`
|
||||
|
||||
用于结构级命令:
|
||||
|
||||
- `tree.subtree.move`
|
||||
- `tree.subtree.copy`
|
||||
|
||||
### 5.3 `tree.asset.*`
|
||||
|
||||
用于资源挂接:
|
||||
|
||||
- `tree.asset.attach`
|
||||
- `tree.asset.detach`
|
||||
|
||||
## 6. 当前约束
|
||||
|
||||
- 前端组件不再自己扩散树语义
|
||||
- tree command client 是唯一稳定入口
|
||||
- route / bridge / kernel command 的映射必须集中管理
|
||||
|
||||
## 7. 完成判定
|
||||
|
||||
当以下条件同时成立时,视为 Stage 2 完成:
|
||||
|
||||
- 文档明确记录 `documents.*` 到 `tree.*` 的映射
|
||||
- Rust / 前端都接受 `tree.*`
|
||||
- 主调用路径默认走 `tree.*`
|
||||
- `documents.create` 等兼容命名不再是新增主语义入口
|
||||
@@ -0,0 +1,110 @@
|
||||
# 4-7 [done] Tree Shell UI State Boundary v1
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
>
|
||||
> 目的:明确哪些状态属于 projection,哪些状态只能留在 tree shell / renderer 本地。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
树域必须严格区分:
|
||||
|
||||
- projection truth
|
||||
- local UI state
|
||||
|
||||
否则 Rust projection、Leptos tree shell、Next 挂载切流会继续把状态缠回旧前端壳。
|
||||
|
||||
## 2. 属于 projection 的状态
|
||||
|
||||
这些状态必须来自上游 projection 或 command 结果,而不是由 UI 自行猜测:
|
||||
|
||||
- `nodeId`
|
||||
- `parentNodeId`
|
||||
- `projectionKind`
|
||||
- `nodeType`
|
||||
- `title`
|
||||
- `depth`
|
||||
- `position`
|
||||
- `childCount`
|
||||
- `expandable`
|
||||
- `expandedByDefault`
|
||||
- `capabilities`
|
||||
- `resourceMeta`
|
||||
- `iconHint`
|
||||
|
||||
说明:
|
||||
|
||||
- projection 只描述“结构真相”和“允许做什么”
|
||||
- projection 不承载 hover、selected、dragging 等临时交互态
|
||||
|
||||
## 3. 只能留在 UI 本地的状态
|
||||
|
||||
这些状态只能存在于 tree shell / renderer 本地:
|
||||
|
||||
- `expanded`
|
||||
- `selected`
|
||||
- `hover`
|
||||
- `focus`
|
||||
- `dragging`
|
||||
- `drop target`
|
||||
- `context menu open`
|
||||
- `keyboard navigation anchor`
|
||||
|
||||
说明:
|
||||
|
||||
- `expandedByDefault` 是 projection 建议值
|
||||
- `expanded` 是本地会话态,不直接回写 projection
|
||||
|
||||
## 4. 边界规则
|
||||
|
||||
### 4.1 `expanded`
|
||||
|
||||
- 初始值可由 `expandedByDefault` 推导
|
||||
- 运行期展开/折叠必须只保留在本地状态
|
||||
- 不能把 `expanded` 反向写回 projection item
|
||||
|
||||
### 4.2 `selected`
|
||||
|
||||
- `selected` 属于当前用户当前会话的局部状态
|
||||
- 不能作为 projection 的共享字段
|
||||
|
||||
### 4.3 `focus`
|
||||
|
||||
- `focus` 属于 keyboard / accessibility 层本地状态
|
||||
- 不能参与结构真相
|
||||
|
||||
### 4.4 `dragging` / `drop target`
|
||||
|
||||
- `dragging` 与 `drop target` 是瞬时状态
|
||||
- 只能存在于 DnD 状态机
|
||||
- drop 成功后,真正持久化的是 `tree.subtree.move` 等命令结果
|
||||
|
||||
## 5. Rust 与前端共识
|
||||
|
||||
Rust 负责:
|
||||
|
||||
- 输出 projection
|
||||
- 接收 command
|
||||
- 返回 command result / resync snapshot
|
||||
|
||||
前端或 Leptos shell 负责:
|
||||
|
||||
- `expanded`
|
||||
- `selected`
|
||||
- `hover`
|
||||
- `focus`
|
||||
- `dragging`
|
||||
- `drop target`
|
||||
|
||||
## 6. 对 `page_tree` / `file_tree` / picker 的影响
|
||||
|
||||
- `page_tree`、`file_tree`、picker 必须共用同一套本地状态边界
|
||||
- picker 只是交互能力更窄,不是另一套状态模型
|
||||
- `keyboard`、`selection`、`focus`、`dragging` 的命名与语义必须一致
|
||||
|
||||
## 7. 完成判定
|
||||
|
||||
当以下条件同时成立时,视为状态边界冻结完成:
|
||||
|
||||
- 文档明确列出 projection 与本地状态
|
||||
- Rust 和前端都不再把 `expanded/selected/focus/dragging/drop target` 写进 projection
|
||||
- 后续 Leptos shell 与 Next consumer 使用同一份边界说明
|
||||
@@ -0,0 +1,75 @@
|
||||
# 4-8 [done] Tree Shell Cutover Performance Report v1
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
>
|
||||
> 测试环境:
|
||||
> - 前端主站:`http://127.0.0.1:3000`
|
||||
> - Rust Web:`http://127.0.0.1:3104`
|
||||
> - 浏览器:Playwright Chromium headless
|
||||
> - 数据口径:同一工作区下真实 Sidebar + 临时父/子页面样本 + move/embed picker 对话框
|
||||
|
||||
## 1. 测量口径
|
||||
|
||||
- `首包`
|
||||
- 指主树或 picker 打开后,到对应 surface 首次可见的时间。
|
||||
- `首次可交互`
|
||||
- 指 surface 已出现,且首个可点击行/按钮已可交互。
|
||||
- `切页延迟`
|
||||
- 指点击页面树或文件树行,到浏览器 URL 切到目标页面的时间。
|
||||
- `大树展开延迟`
|
||||
- 指点击页面树真实可展开节点的展开/收起往返显隐完成时间。
|
||||
- `fallback`
|
||||
- 指 `/api/stream/events` 被阻断或失败时,Sidebar 仍能在主站内回到可见可用树视图的结果。
|
||||
|
||||
## 2. 实测结果
|
||||
|
||||
### 2.1 首页与切流 smoke
|
||||
|
||||
- `task097-homepage-entry-smoke.js`
|
||||
- `/auth` 返回 `200 text/html`
|
||||
- `/` 返回 `307 -> /auth`
|
||||
- `3104 /health` 返回 `200`
|
||||
|
||||
### 2.2 Sidebar 主树 cutover
|
||||
|
||||
- `task101-tree-shell-cutover-smoke.js`
|
||||
- `首包 / 首次可交互`:`9ms`
|
||||
- `切页延迟`:`205ms`
|
||||
- `stream`:`/api/stream/events?stream=workspace&projection=sidebar_tree&workspaceId=...`
|
||||
- `fallback`:阻断 stream 后仍在 `22ms` 内显示可用主树
|
||||
|
||||
### 2.3 filetree / picker 回归
|
||||
|
||||
- `task102-tree-shell-regression.js`
|
||||
- `filetree 首包 / 首次可交互`:`6ms`
|
||||
- `filetree 切页延迟`:`179ms`
|
||||
- `picker 首包 / 首次可交互`:`264ms`
|
||||
- `picker 搜索输入后结果/空态稳定出现`:`1005ms`
|
||||
|
||||
### 2.4 页面树展开样本
|
||||
|
||||
- 真实可展开节点往返展开/收起延迟:`124ms`
|
||||
|
||||
## 3. 对照与结论
|
||||
|
||||
- 旧 iframe/postMessage 实验壳已退出 Sidebar / filetree / picker 主路径。
|
||||
- 当前默认主路径已经是 Next 内统一 tree surface + stream/projection 主链。
|
||||
- `3104` 与 Convex 连接正常时,主树按 stream 路径工作;`3104`/stream 失败时,主树仍能通过 fallback 保持可见。
|
||||
- 当前开发机口径下,主路径交互均在亚秒级,未再出现此前 `3000` 因 Convex/tree shell 卡死而无响应的问题。
|
||||
|
||||
## 4. fallback 结果
|
||||
|
||||
- `fallback` 触发条件:
|
||||
- 主动阻断 `/api/stream/events`
|
||||
- 观测结果:
|
||||
- 页面不空白
|
||||
- Sidebar 主树仍渲染
|
||||
- 可继续看到目标页面树节点
|
||||
- 结论:
|
||||
- `fallback` 已从“实验性兜底”变成正式切流护栏
|
||||
|
||||
## 5. 残余风险
|
||||
|
||||
- 当前指标来自本地开发环境与小样本 smoke,不代表生产环境大规模树深/大附件工作区的长期 p95。
|
||||
- picker 搜索链路仍受搜索索引可见性影响;本轮已验证“搜索后结果或空态稳定出现”,但未把索引收敛时间当作树域切流 blocker。
|
||||
- 后续如继续增强 Rust-first renderer,可在不改变当前 cutover 结论的前提下,把这份报告升级为多样本趋势报表。
|
||||
@@ -0,0 +1,944 @@
|
||||
# 4 [done] Sidebar / 页面树 / 文件树 Rust Web 重构方案 v1
|
||||
|
||||
> 更新时间:2026-04-22
|
||||
>
|
||||
> 当前优先级入口:
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/done/3-2-tree-first-graph-kernel-phase3-task-breakdown-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/90-reference/90-2-yemianshu.md`
|
||||
> - `/mnt/Data1T/mnote/design/90-reference/90-1-filetree.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-1-sidebar-pagetree-filetree-product-gap-analysis-v1.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档回答的问题不是:
|
||||
|
||||
- “当前 Sidebar 再怎么局部优化一下”
|
||||
|
||||
而是:
|
||||
|
||||
> **在 `tree-first graph kernel` 前提下,是否应该把 Sidebar / 页面树 / 文件树直接重构为一个独立的 Rust Web 子系统。**
|
||||
|
||||
本文的结论是:
|
||||
|
||||
> **可以,而且长期上这是正确方向;但重构对象不是“一个更快的树组件”,而是“一个直接消费 kernel projection 的独立树域执行面”。**
|
||||
|
||||
也就是说,目标不是把当前 React 树组件换个语言重写,而是:
|
||||
|
||||
- 用 Rust 主导 tree projection
|
||||
- 用 Rust Web 主导 tree query / command
|
||||
- 让页面树 / 文件树只作为 kernel 的树投影
|
||||
- 再决定 UI 壳是否也迁到 Rust 家族
|
||||
|
||||
---
|
||||
|
||||
## 2. 必须遵守的前提:树不是 UI 数据,而是 kernel 投影
|
||||
|
||||
这份方案必须完全服从:
|
||||
|
||||
- [tree-first-graph-kernel-v1.md](/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md)
|
||||
|
||||
里面已经固定的几条原则。
|
||||
|
||||
### 2.1 树是主骨架
|
||||
|
||||
当前长期架构已经冻结为:
|
||||
|
||||
- 树是主骨架
|
||||
- 图是横向扩展
|
||||
- Sidebar / 页面树 / 文件树 / 阅读流 / Mindmap 都只是 projection
|
||||
|
||||
所以这里的页面树 / 文件树不能再被定义为:
|
||||
|
||||
- 前端自己拼出来的导航数据
|
||||
|
||||
它们必须被定义为:
|
||||
|
||||
- `tree-first graph kernel` 的树投影
|
||||
|
||||
### 2.2 页面树和文件树不是两套真相
|
||||
|
||||
在新架构里:
|
||||
|
||||
- 页面树不是独立系统
|
||||
- 文件树也不是独立系统
|
||||
|
||||
两者都来自同一个 kernel,只是投影范围不同:
|
||||
|
||||
- `page_tree`
|
||||
- 以 `page` / `section` / 页面层级为主
|
||||
- `file_tree`
|
||||
- 在页面层级基础上,把 `asset` / `mindmap` / `table` / 未来 `book` / `pdf` 一起投影出来
|
||||
|
||||
### 2.3 Sidebar 是壳,不是事实源
|
||||
|
||||
Sidebar 长期不应再被理解为:
|
||||
|
||||
- “一个左侧导航 React 组件”
|
||||
|
||||
而应理解为:
|
||||
|
||||
- “tree projection 的承载壳”
|
||||
|
||||
固定边界应是:
|
||||
|
||||
- kernel 持有真相
|
||||
- projection 输出树
|
||||
- Sidebar 只负责显示和交互
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前现状
|
||||
|
||||
### 3.1 已经做对的部分
|
||||
|
||||
当前代码已经有一些方向是正确的:
|
||||
|
||||
- `kernelSidebarProjection`
|
||||
- `kernelSidebarTree`
|
||||
- `Sidebar` 主树开始以 `kernelSidebarTree` 为来源
|
||||
- Rust runtime 和 `mnote-web` 已开始承接 Sidebar 相关 projection 主链
|
||||
|
||||
对应代码包括:
|
||||
|
||||
- [kernel-sidebar.ts](/mnt/Data1T/mnote/wolai-frontend/src/lib/kernel-sidebar.ts)
|
||||
- [sidebar-data.ts](/mnt/Data1T/mnote/wolai-frontend/src/lib/sidebar-data.ts)
|
||||
- [sidebar.tsx](/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx)
|
||||
- [kernel.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/kernel.rs)
|
||||
|
||||
### 3.2 还没做完的部分
|
||||
|
||||
当前真正的问题是:
|
||||
|
||||
- 主 Sidebar 仍是超大客户端组件
|
||||
- 文件树仍然主要在前端继续加工 row model
|
||||
- `move-embed picker` 等兼容域仍保留旧 `buildDocumentTree(...)`
|
||||
- 页面树和文件树还没有彻底统一为稳定的 kernel projection family
|
||||
|
||||
这说明:
|
||||
|
||||
> **现在的瓶颈不只是“UI 重”,而是“树域仍然没有形成独立、稳定、可替换的执行边界”。**
|
||||
|
||||
---
|
||||
|
||||
## 4. 对参考资料的判断
|
||||
|
||||
### 4.1 `/design/cankao/yemianshu.md` 和 `/design/cankao/filetree.md` 能参考什么
|
||||
|
||||
这两份参考有价值,但要分层使用。
|
||||
|
||||
适合借鉴的部分:
|
||||
|
||||
- 树形系统的分层
|
||||
- VS Code / Notion 风格交互
|
||||
- 折叠、展开、拖拽、懒加载、多选、右键菜单
|
||||
|
||||
不适合直接拿来落当前 Web 主线的部分:
|
||||
|
||||
- Ratatui / Cursive / TUI 组件
|
||||
- egui / iced / Fyrox / GPUI 这类桌面 GUI 组件
|
||||
|
||||
原因很简单:
|
||||
|
||||
- 这些更适合终端或原生桌面
|
||||
- 当前 mnote 的主线是 Web + Rust Web + kernel projection
|
||||
|
||||
所以它们更适合做:
|
||||
|
||||
- 交互语义参考
|
||||
|
||||
而不适合做:
|
||||
|
||||
- 当前 Web 主线的直接实现模板
|
||||
|
||||
### 4.2 更适合作为直接参考的方向
|
||||
|
||||
如果这次真要把 Sidebar / 页面树 / 文件树往 Rust 家族重构,应该看两类参考:
|
||||
|
||||
#### A. Rust Web 前端框架
|
||||
|
||||
优先关注:
|
||||
|
||||
- `Leptos`
|
||||
- `Dioxus`
|
||||
- `Yew`
|
||||
|
||||
本文的建议顺序是:
|
||||
|
||||
1. `Leptos`
|
||||
2. `Dioxus`
|
||||
3. `Yew`
|
||||
|
||||
原因不是抽象喜好,而是贴合度:
|
||||
|
||||
- 你们已经在走 Rust kernel + Rust Web + server-first
|
||||
- 这时最有价值的是“Rust Web 组件 + server integration + 渐进切流”
|
||||
- 不是终端树,也不是桌面树
|
||||
|
||||
#### B. 成熟 Web Tree 的行为模型
|
||||
|
||||
即使最终决定用 Rust 家族重写,交互模型也应该优先参考成熟 Web Tree 的做法:
|
||||
|
||||
- headless tree 思路
|
||||
- VS Code Explorer 的 row model
|
||||
- 大树虚拟化
|
||||
- DnD 状态机
|
||||
- selection / focus / keyboard 模型
|
||||
|
||||
这里学的是:
|
||||
|
||||
- 行为模型
|
||||
|
||||
不是:
|
||||
|
||||
- 必须沿用 React
|
||||
|
||||
---
|
||||
|
||||
## 5. 结论:可以直接重构,但应定义成独立大任务
|
||||
|
||||
我的明确结论是:
|
||||
|
||||
> **可以直接把 Sidebar / 页面树 / 文件树作为独立大任务重构,而且长期上应该这样做。**
|
||||
|
||||
但这个重构不能被理解为:
|
||||
|
||||
- 把 `sidebar.tsx` 翻译成 Rust
|
||||
|
||||
而应被理解为:
|
||||
|
||||
- 把树域从旧前端壳里剥离出来
|
||||
- 形成一个独立的 Rust Web tree shell
|
||||
|
||||
也就是:
|
||||
|
||||
- 独立 route / shell
|
||||
- 独立 projection protocol
|
||||
- 独立 command protocol
|
||||
- 独立 UI state 边界
|
||||
|
||||
这个任务应该单独成立,而不是继续藏在 `Kernel Phase 4` 的一句话里。
|
||||
|
||||
---
|
||||
|
||||
## 6. 目标架构
|
||||
|
||||
## 6.1 新的树域分层
|
||||
|
||||
长期建议把树域拆成五层:
|
||||
|
||||
### 1. Kernel Truth
|
||||
|
||||
只承载:
|
||||
|
||||
- `node`
|
||||
- `edge`
|
||||
- `subtree`
|
||||
- `audit`
|
||||
|
||||
### 2. Tree Projection Layer
|
||||
|
||||
专门输出:
|
||||
|
||||
- `sidebar_tree`
|
||||
- `page_tree`
|
||||
- `file_tree`
|
||||
|
||||
固定输出应包括:
|
||||
|
||||
- `projection_id`
|
||||
- `root_node_id`
|
||||
- `items`
|
||||
- `edges`
|
||||
- `sort key`
|
||||
- `expand hint`
|
||||
- `capability flags`
|
||||
|
||||
### 3. Tree Command Layer
|
||||
|
||||
只处理树域命令:
|
||||
|
||||
- create page
|
||||
- move subtree
|
||||
- attach asset
|
||||
- reorder sibling
|
||||
- archive / restore
|
||||
- open node
|
||||
|
||||
### 4. Tree Shell
|
||||
|
||||
树域的独立承载壳,只负责:
|
||||
|
||||
- 拉 projection
|
||||
- 发 command
|
||||
- 维护局部 UI 状态
|
||||
|
||||
### 5. Tree Renderer
|
||||
|
||||
最终的可视组件,只负责:
|
||||
|
||||
- row 渲染
|
||||
- 虚拟化
|
||||
- 选中
|
||||
- 展开
|
||||
- 右键菜单
|
||||
- DnD feedback
|
||||
|
||||
---
|
||||
|
||||
## 6.2 页面树和文件树的正确关系
|
||||
|
||||
在新架构里,这两者不该是并列的两套不同系统,而应是:
|
||||
|
||||
### 页面树
|
||||
|
||||
只关心:
|
||||
|
||||
- `workspace`
|
||||
- `folder`
|
||||
- `page`
|
||||
- `section`
|
||||
|
||||
### 文件树
|
||||
|
||||
在页面树骨架上再纳入:
|
||||
|
||||
- `asset`
|
||||
- `mindmap`
|
||||
- `table`
|
||||
- `book`
|
||||
- `pdf`
|
||||
- 未来的 `index_node`
|
||||
|
||||
也就是说:
|
||||
|
||||
> **文件树不是“另建一棵树”,而是“同一棵树的更宽对象投影”。**
|
||||
|
||||
这非常符合 `tree-first graph kernel` 的定义。
|
||||
|
||||
---
|
||||
|
||||
## 7. 为什么建议用 Rust Web 子系统,而不是继续堆在当前 Sidebar 里
|
||||
|
||||
### 7.1 当前 Sidebar 太大
|
||||
|
||||
现在的 Sidebar 不只是树:
|
||||
|
||||
- 搜索入口
|
||||
- 成员
|
||||
- 分享
|
||||
- 回收站
|
||||
- 资源操作
|
||||
- 页面树
|
||||
- 文件树
|
||||
- 各种本地对话框
|
||||
|
||||
这会导致:
|
||||
|
||||
- 状态膨胀
|
||||
- 切页参与重渲染
|
||||
- 树逻辑和业务逻辑缠在一起
|
||||
|
||||
### 7.2 树域已经足够大,可以独立成系统
|
||||
|
||||
页面树 / 文件树本身已经有:
|
||||
|
||||
- 自己的数据协议
|
||||
- 自己的 row model
|
||||
- 自己的拖拽系统
|
||||
- 自己的选择模型
|
||||
- 自己的上下文菜单
|
||||
- 自己的资源挂载逻辑
|
||||
|
||||
这已经不是一个小组件,而是一个完整子系统。
|
||||
|
||||
### 7.3 独立之后更符合后续迁移
|
||||
|
||||
如果现在就把它切成独立树域子系统,后续:
|
||||
|
||||
- Mindmap
|
||||
- 阅读页结构树
|
||||
- 搜索结构结果
|
||||
- Book / PDF 子树
|
||||
|
||||
都可以复用同一套 projection / renderer 协议。
|
||||
|
||||
---
|
||||
|
||||
## 8. 技术路线选择
|
||||
|
||||
## 8.1 方案对比
|
||||
|
||||
### 方案 A:继续 React,只换数据层
|
||||
|
||||
优点:
|
||||
|
||||
- 风险最低
|
||||
- 最快收口旧 helper
|
||||
|
||||
缺点:
|
||||
|
||||
- 树域仍留在旧前端壳内
|
||||
- 不能完成“Rust 主执行面”这一步
|
||||
|
||||
### 方案 B:独立 Rust Web tree shell,当前主站只挂载它
|
||||
|
||||
优点:
|
||||
|
||||
- 可以保留整体产品双栈过渡
|
||||
- 树域先行 Rust 化
|
||||
- 与 `tree-first graph kernel` 最一致
|
||||
|
||||
缺点:
|
||||
|
||||
- 需要额外处理嵌入、路由、样式、事件桥接
|
||||
|
||||
### 方案 C:直接整站前端重写
|
||||
|
||||
优点:
|
||||
|
||||
- 理论上最终最纯
|
||||
|
||||
缺点:
|
||||
|
||||
- 范围失控
|
||||
- 风险过高
|
||||
- 与当前阶段目标不匹配
|
||||
|
||||
## 8.2 当前建议
|
||||
|
||||
本文明确建议:
|
||||
|
||||
> **选方案 B:把 Sidebar / 页面树 / 文件树做成独立 Rust Web tree shell。**
|
||||
|
||||
---
|
||||
|
||||
## 8.3 Rust Web 框架建议
|
||||
|
||||
当前优先建议:
|
||||
|
||||
### 第一选择:Leptos
|
||||
|
||||
原因:
|
||||
|
||||
- 更贴近 Rust 全栈 / server-first
|
||||
- 更适合和 `axum` / `mnote-web` 的方向合并
|
||||
- 适合做“树域先行”的渐进式替换
|
||||
|
||||
### 第二选择:Dioxus
|
||||
|
||||
原因:
|
||||
|
||||
- 跨 Web / Desktop 能力强
|
||||
- 如果未来想把树域同时复用到桌面壳,会有价值
|
||||
|
||||
### 第三选择:Yew
|
||||
|
||||
原因:
|
||||
|
||||
- 能做,但相对不如前两者贴合当前迁移方向
|
||||
|
||||
所以这份方案的推荐结论是:
|
||||
|
||||
> **树域独立重构时,优先按 `mnote-web + Leptos` 设计。**
|
||||
|
||||
---
|
||||
|
||||
## 8.4 GitHub 参考池
|
||||
|
||||
这里不再按“有没有现成 Rust Notion 成品”来选参考,而是按三个层级来选:
|
||||
|
||||
- Rust Web 承载框架
|
||||
- 树域 UI primitives
|
||||
- 树行为模型与产品结构参考
|
||||
|
||||
### A. 直接可参考:Rust Web 主路线
|
||||
|
||||
#### `leptos-rs/leptos`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/leptos-rs/leptos
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- `Rust + SSR + islands` 的 Web 承载方式
|
||||
- 与 `axum` 风格服务端组合
|
||||
- 渐进式切流,而不是一次性整站替换
|
||||
|
||||
为什么适合当前方案:
|
||||
|
||||
- 当前 `mnote-web` 已经是 Rust Web 接入点
|
||||
- 树域后续如果独立成 shell,最需要的是“Rust Web 承载能力”,不是单独一个树控件
|
||||
|
||||
结论:
|
||||
|
||||
- **这是树域 Rust Web 重构的第一参考。**
|
||||
|
||||
#### `DioxusLabs/dioxus`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/DioxusLabs/dioxus
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- Rust 组件化 UI
|
||||
- Web / Desktop 共享思路
|
||||
|
||||
限制:
|
||||
|
||||
- 更偏多端壳能力
|
||||
- 与当前 `mnote-web + axum` 路线的贴合度仍低于 `Leptos`
|
||||
|
||||
结论:
|
||||
|
||||
- **可作为备选路线参考,但不是当前首选。**
|
||||
|
||||
#### `yewstack/yew`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/yewstack/yew
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- Rust Web 组件化基本能力
|
||||
|
||||
限制:
|
||||
|
||||
- 能做,但对你们当前 server-first 与渐进切流路线支持感不如 `Leptos`
|
||||
|
||||
结论:
|
||||
|
||||
- **保留为第三选择,不作为当前主实现模板。**
|
||||
|
||||
### B. 直接可参考:树域 UI primitives / 组件层
|
||||
|
||||
#### `cloud-shuttle/radix-leptos`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/cloud-shuttle/radix-leptos
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- `Leptos` 生态下的 UI primitives 组合方式
|
||||
- `collapsible`、`scroll area`、`menu`、`overlay`、可访问性细节
|
||||
- 树域壳层需要的基础交互组件
|
||||
|
||||
限制:
|
||||
|
||||
- 它不是完整树组件
|
||||
- 不能直接替代页面树 / 文件树的 row model 与状态机
|
||||
|
||||
结论:
|
||||
|
||||
- **适合作为树域 shell 的基础件参考。**
|
||||
|
||||
#### `thaw-ui/thaw`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/thaw-ui/thaw
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- `Leptos` 组件组织方式
|
||||
- 通用面板、按钮、菜单等基础 UI
|
||||
|
||||
限制:
|
||||
|
||||
- 更像通用组件库
|
||||
- 对树域协议、树行为模型帮助有限
|
||||
|
||||
结论:
|
||||
|
||||
- **适合作为辅助 UI 库参考,不是树域核心参考。**
|
||||
|
||||
#### `KoVal177/leptos-column-browser`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/KoVal177/leptos-column-browser
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- Rust Web 下的层级导航
|
||||
- 异步懒加载子节点
|
||||
- 多层树浏览器的交互拆分
|
||||
|
||||
限制:
|
||||
|
||||
- 项目较新、体量小
|
||||
- 更接近 column browser,不是当前 Sidebar 单栏树的完整模板
|
||||
|
||||
结论:
|
||||
|
||||
- **适合借鉴 provider / async loading / column navigation 思路,不适合直接照搬。**
|
||||
|
||||
### C. 高价值参考:树行为模型与产品结构
|
||||
|
||||
#### `lukasbach/headless-tree`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/lukasbach/headless-tree
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- row model
|
||||
- selection / focus / keyboard 模型
|
||||
- DnD 状态机
|
||||
- 虚拟化树行为拆分
|
||||
|
||||
为什么值得看:
|
||||
|
||||
- 你们后续真正难的不是“画一棵树”,而是把树行为从具体 UI 框架中抽出来
|
||||
- 这正对应 `tree-first graph kernel -> projection -> shell -> renderer` 的分层思想
|
||||
|
||||
限制:
|
||||
|
||||
- 不是 Rust
|
||||
- 不能直接进入实现层
|
||||
|
||||
结论:
|
||||
|
||||
- **非常适合作为树行为模型参考。**
|
||||
|
||||
#### `AppFlowy-IO/AppFlowy`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/AppFlowy-IO/AppFlowy
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- “工作区 / 页面树 / 文档”这类产品结构
|
||||
- Rust 统一管理部分内核能力的思路
|
||||
- Notion 类产品如何收敛页面树语义
|
||||
|
||||
限制:
|
||||
|
||||
- 主前端不是你们要走的 `Leptos Web` 路线
|
||||
- 不能作为当前树域 Web 重构的直接模板
|
||||
|
||||
结论:
|
||||
|
||||
- **适合作为产品结构与边界参考,不适合作为实现模板。**
|
||||
|
||||
#### `toeverything/AFFiNE`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/toeverything/AFFiNE
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- Notion / knowledge base 类产品的页面树体验
|
||||
- 页面、知识库、画布等多视图并存时的交互组织
|
||||
|
||||
限制:
|
||||
|
||||
- 技术栈不是 Rust
|
||||
- 更适合借鉴交互与产品层结构
|
||||
|
||||
结论:
|
||||
|
||||
- **适合作为页面树 / 知识库产品交互参考。**
|
||||
|
||||
## 8.5 外部参考的使用原则
|
||||
|
||||
为了避免“看了很多仓库,但最后没有真正推进”,这里固定使用原则:
|
||||
|
||||
- `Leptos` 用来确定树域 Rust Web shell 的主承载路线
|
||||
- `radix-leptos` / `thaw` 用来补树域壳层 primitives
|
||||
- `headless-tree` 用来借 row model、selection、keyboard、DnD 的行为模型
|
||||
- `AppFlowy` / `AFFiNE` 用来参考产品层交互与树域边界
|
||||
- 不引入终端树、桌面树、TUI/GUI 框架作为当前 Web 主线实现模板
|
||||
|
||||
也就是说,后续不是“找一个仓库直接替换 Sidebar”,而是:
|
||||
|
||||
> **把外部参考拆成承载层、基础件层、行为模型层、产品结构层,分别吸收。**
|
||||
|
||||
---
|
||||
|
||||
## 9. 重构范围
|
||||
|
||||
## 9.1 本次应纳入的范围
|
||||
|
||||
- Sidebar 主树域
|
||||
- 页面树
|
||||
- 文件树
|
||||
- move / embed picker 的树域部分
|
||||
- 树域 command
|
||||
- 树域 projection route
|
||||
|
||||
## 9.2 本次不纳入的范围
|
||||
|
||||
- 搜索主面板
|
||||
- AI 主面板
|
||||
- Mindmap 主画布
|
||||
- `BlockNote` 编辑器
|
||||
- 旧前端壳整体删除
|
||||
|
||||
原因:
|
||||
|
||||
- 这些属于后续 phase
|
||||
- 本次只做树域切换,保持边界清晰
|
||||
|
||||
---
|
||||
|
||||
## 10. 新协议定义建议
|
||||
|
||||
## 10.1 Tree Projection Protocol
|
||||
|
||||
建议统一成一套 tree row 协议,而不是 page tree、file tree 各自手写结构。
|
||||
|
||||
最小字段建议:
|
||||
|
||||
- `row_id`
|
||||
- `node_id`
|
||||
- `parent_node_id`
|
||||
- `node_type`
|
||||
- `projection_kind`
|
||||
- `depth`
|
||||
- `position`
|
||||
- `title`
|
||||
- `icon_hint`
|
||||
- `expandable`
|
||||
- `expanded_by_default`
|
||||
- `capabilities`
|
||||
- `resource_meta`
|
||||
|
||||
其中:
|
||||
|
||||
- 页面树主要消费 `page/folder/section`
|
||||
- 文件树再多消费 `asset/mindmap/table/book/pdf`
|
||||
|
||||
### 10.2 Tree Command Protocol
|
||||
|
||||
建议统一成:
|
||||
|
||||
- `tree.node.create`
|
||||
- `tree.subtree.move`
|
||||
- `tree.node.rename`
|
||||
- `tree.node.archive`
|
||||
- `tree.node.restore`
|
||||
- `tree.asset.attach`
|
||||
- `tree.asset.detach`
|
||||
|
||||
这些 command 最终应映射到 kernel command,而不是直接绑在前端 UI 行为上。
|
||||
|
||||
---
|
||||
|
||||
## 11. 分阶段实施建议
|
||||
|
||||
下面这组 phase 不再按“最初方案假设”维护,而按 **2026-04-18 当前仓库代码状态** 重写。
|
||||
|
||||
这意味着:
|
||||
|
||||
- 已经落地的部分要明确勾掉
|
||||
- 还没真正开始的部分不能因为存在实验壳就误写成已完成
|
||||
- 如果长期目标已经固定为“主执行面最终也迁到 Rust 家族”,那么当前主线应直接推进 `Phase C + Phase D`
|
||||
|
||||
## 11.1 Tree Shell Phase A:协议冻结
|
||||
|
||||
这一阶段的目标不是开始写 UI,而是把树域协议冻结到后续不会反复返工。
|
||||
|
||||
**当前状态:`COMPLETED(协议、共享类型、状态边界与 contract tests 已完成封板)`**
|
||||
|
||||
### 完成 checklist
|
||||
|
||||
- [x] 把 `page_tree` 的现有字段提升为当前协议基线:
|
||||
- `row_id`
|
||||
- `node_id`
|
||||
- `parent_node_id`
|
||||
- `node_type`
|
||||
- `projection_kind`
|
||||
- `depth`
|
||||
- `position`
|
||||
- `title`
|
||||
- `capabilities`
|
||||
- `resource_meta`
|
||||
- [x] 树域主路径已统一到 `kernelSidebarTree -> page_tree projection -> visible rows` 这一协议家族
|
||||
- [x] 把 `sidebar_tree`、`page_tree`、`file_tree` 的共用字段与差异字段正式写成共享类型/文档,不再只散落在前端映射代码里
|
||||
- [x] 定义并冻结 `file_tree` 扩展字段:
|
||||
- `resource_kind`
|
||||
- `asset_kind`
|
||||
- `icon_hint`
|
||||
- `expandable`
|
||||
- `expanded_by_default`
|
||||
- [x] 第一批树域命令已经收口到共享 command client:
|
||||
- `documents.create`
|
||||
- `documents.title.update`
|
||||
- `documents.move`
|
||||
- `documents.delete`
|
||||
- `documents.restore`
|
||||
- `documents.purge`
|
||||
- `documents.embed`
|
||||
- `documents.copy_tree`
|
||||
- [x] 冻结树域 command protocol 的长期命名面,并补齐 `documents.* -> tree.*` 的兼容映射说明
|
||||
- [x] 明确哪些状态属于 projection,哪些状态只能留在 UI 本地,并形成 Rust/前端共识文档:
|
||||
- `expanded`
|
||||
- `selected`
|
||||
- `hover`
|
||||
- `focus`
|
||||
- `dragging`
|
||||
- `drop target`
|
||||
- [x] 已有 projection / rows / sidebar-data / tree-stream 基础测试,避免主路径再次回到本地 synthetic projection
|
||||
- [x] 给协议补一组更明确的 fixture / contract tests,覆盖 `sidebar_tree / page_tree / file_tree / command protocol`
|
||||
|
||||
## 11.2 Tree Shell Phase 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。**
|
||||
@@ -0,0 +1,232 @@
|
||||
# 5-5-1 [done] Page Aggregate Contract v1
|
||||
|
||||
> 更新时间:2026-04-22
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档只定义一件事:
|
||||
|
||||
> **文档页在前端与 Rust 主链之间,当前最小可落地的 `Page Aggregate` 契约。**
|
||||
|
||||
它的作用不是取代长期 Rust kernel contract,而是先冻结当前主文档页的聚合边界,阻止:
|
||||
|
||||
- 标题、正文、页面设置、page subtree 继续各自散长字段
|
||||
- 页面壳继续在 props 链上拼第二份页面真相
|
||||
- `leptos-tiptap` island 继续只接正文而不接页面布局语义
|
||||
|
||||
## 2. 当前最小契约
|
||||
|
||||
当前最小 `Page Aggregate` 固定为五层:
|
||||
|
||||
- `page_identity`
|
||||
- `page_head`
|
||||
- `page_layout`
|
||||
- `page_body`
|
||||
- `page_tree`
|
||||
|
||||
以及一组附属统计:
|
||||
|
||||
- `page_stats`
|
||||
|
||||
### 2.1 `page_identity`
|
||||
|
||||
作用:
|
||||
|
||||
> **标识这份页面聚合属于哪一个 page aggregate。**
|
||||
|
||||
当前最小字段:
|
||||
|
||||
- `documentId`
|
||||
- `workspaceId`
|
||||
|
||||
说明:
|
||||
|
||||
- 这层是聚合身份,不是 UI 展示字段。
|
||||
- 后续如果 Rust kernel 侧补 `nodeId / aggregateId / projectionVersion`,应继续落在这一层,而不是散回页面 props。
|
||||
|
||||
### 2.2 `page_head`
|
||||
|
||||
作用:
|
||||
|
||||
> **承载页面头部的正式真相。**
|
||||
|
||||
当前最小字段:
|
||||
|
||||
- `title`
|
||||
- `updatedAt`
|
||||
- `permissions.readOnly`
|
||||
- `permissions.disableDownload`
|
||||
- `permissions.disableCopy`
|
||||
|
||||
说明:
|
||||
|
||||
- 页头标题属于 `page_head truth`,不是组件局部输入框自己的真相。
|
||||
- 前端仍可保留一个“输入中”的临时编辑态,但提交后必须回到 `page_head.title`。
|
||||
|
||||
### 2.3 `page_layout`
|
||||
|
||||
作用:
|
||||
|
||||
> **承载页面布局与编辑器 runtime 相关设置。**
|
||||
|
||||
当前最小字段:
|
||||
|
||||
- `pageOptions.wideLayout`
|
||||
- `pageOptions.smallText`
|
||||
- `pageOptions.layoutDensity`
|
||||
- `pageOptions.showHeadingNumbers`
|
||||
- `pageOptions.showToc`
|
||||
- `pageOptions.showStructure`
|
||||
- `pageOptions.protectEditing`
|
||||
- `pageOptions.showWordCount`
|
||||
- `pageOptions.collapseBacklinks`
|
||||
- `pageOptions.pageFont`
|
||||
- `pageOptions.hideChildPages`
|
||||
- `pageOptions.showBlockRefCount`
|
||||
- `pageOptions.embedDefaultBlockId`
|
||||
|
||||
说明:
|
||||
|
||||
- 这层不是“页面设置面板 UI state”,而是页面设置的持久化真相。
|
||||
- 其中只有一部分已经进入 `leptos-tiptap` island 运行时:
|
||||
- `wideLayout`
|
||||
- `smallText`
|
||||
- `layoutDensity`
|
||||
- 下面两项当前只完成字段贯通,不可描述成“正式支持”:
|
||||
- `showHeadingNumbers`
|
||||
- `embedDefaultBlockId`
|
||||
|
||||
### 2.4 `page_body`
|
||||
|
||||
作用:
|
||||
|
||||
> **承载正文内容与保存元数据。**
|
||||
|
||||
当前最小字段:
|
||||
|
||||
- `content`
|
||||
- `revision`
|
||||
- `conflictDetectionKey`
|
||||
|
||||
说明:
|
||||
|
||||
- 正文相关命令都应收口到 `page_body`,而不是继续发明新的“页面内临时写回”路径。
|
||||
- `/api/documents/save -> documents.save -> documents:updateContent` 是当前正式 page body 保存入口。
|
||||
- 2026-04-28 更新:`page.body.saved` 与 `document.snapshot.saved` 已拆成两条正式 domain event。`page.body.saved` 只表达正文块集合保存,`document.snapshot.saved` 承载 snapshot version / content hash / updatedAt,避免正文事件 payload 继续夹带快照语义。
|
||||
- 2026-04-28 更新:`tree.node.embed` 的 `pageReference` block 结构与插入位置由 Rust `pageAggregateEmbedPlan` 产出;3000 route 只提供源页面标题、目标内容、anchor block 等 substrate preflight,不再自行拼装页面聚合正文结构。
|
||||
|
||||
### 2.5 `page_tree`
|
||||
|
||||
作用:
|
||||
|
||||
> **承载当前页面对应的子树投影。**
|
||||
|
||||
当前最小字段:
|
||||
|
||||
- `pageSubtree`
|
||||
|
||||
说明:
|
||||
|
||||
- 当前 `pageSubtree` 仍是兼容过渡态 projection,不应夸大为最终 Rust page aggregate tree contract。
|
||||
- 但在文档页入口和阅读态/TOC 消费层,它已经归属于 `page_tree`,不再继续散成独立 props。
|
||||
|
||||
### 2.6 `page_stats`
|
||||
|
||||
作用:
|
||||
|
||||
> **承载页面统计的附属投影。**
|
||||
|
||||
当前最小字段:
|
||||
|
||||
- `wordCount`
|
||||
- `characterCount`
|
||||
- `blockCount`
|
||||
- `todoTotal`
|
||||
- `todoDone`
|
||||
|
||||
说明:
|
||||
|
||||
- 统计不是页面身份,也不是布局或正文真相,但它是 page aggregate 的附属部分。
|
||||
|
||||
## 3. 哪些字段是 aggregate truth,哪些只是 UI state
|
||||
|
||||
### 3.1 Aggregate Truth
|
||||
|
||||
下面这些字段属于 page aggregate truth:
|
||||
|
||||
- `page_identity.*`
|
||||
- `page_head.*`
|
||||
- `page_layout.pageOptions.*`
|
||||
- `page_body.*`
|
||||
- `page_tree.pageSubtree`
|
||||
- `page_stats.*`
|
||||
|
||||
### 3.2 UI State
|
||||
|
||||
下面这些只能算前端临时态,不是正式真相:
|
||||
|
||||
- 标题输入框当前未提交文本
|
||||
- 当前是否处于编辑态/阅读态
|
||||
- host fallback banner / runtime observability
|
||||
- inspector 当前 tab
|
||||
- history drawer / comments drawer / AI panel 的打开状态
|
||||
- slash 菜单、浮动工具条、块手柄 hover 等 island 交互态
|
||||
|
||||
约束:
|
||||
|
||||
> **UI state 允许存在,但不能再反客为主冒充 page aggregate truth。**
|
||||
|
||||
## 4. 哪些字段必须由 Rust 主导,哪些当前允许前端临时持有
|
||||
|
||||
### 4.1 必须由 Rust 主导
|
||||
|
||||
- `page_head.title` 的持久化结果
|
||||
- `page_body.content/revision/conflictDetectionKey`
|
||||
- `page_layout.pageOptions` 的持久化结果
|
||||
- `page_tree.pageSubtree` 的正式投影来源
|
||||
|
||||
### 4.2 当前允许前端临时持有
|
||||
|
||||
- 标题输入中的 debounce 缓冲态
|
||||
- 正文 host 内的未保存 dirty 态
|
||||
- 只影响单次交互的面板/菜单开关
|
||||
- `showHeadingNumbers/embedDefaultBlockId` 的“字段已贯通,但深语义未完成”阶段性桥接逻辑
|
||||
|
||||
约束:
|
||||
|
||||
> **允许前端临时持有,不等于允许前端成为第二真相。**
|
||||
|
||||
## 5. 当前命令收口口径
|
||||
|
||||
当前最小命令口径:
|
||||
|
||||
- `page.head.updateTitle`
|
||||
- 当前前端别名映射到 `documents.title.update`
|
||||
- `page.layout.updateOptions`
|
||||
- 当前前端别名映射到 `documents.options.update`
|
||||
- `page.body.save`
|
||||
- 当前正式入口映射到 `documents.save`
|
||||
|
||||
说明:
|
||||
|
||||
- 这轮只是把命名与调用口径收口到 page aggregate 子域。
|
||||
- 底层 bridge / Convex / Rust mutation 名称暂不要求一次性全部重命名。
|
||||
|
||||
## 6. 当前未完成边界
|
||||
|
||||
以下事项当前仍未完成,不应在 checklist 中超前打钩:
|
||||
|
||||
- 树标题与页头标题已经证明消费同一份更新后的 projection
|
||||
- AI 写入口已经完全脱离 editor bridge,正式执行 page body command
|
||||
- `showHeadingNumbers` 真正进入 heading 渲染语义
|
||||
- `embedDefaultBlockId` 真正进入嵌入默认落点逻辑
|
||||
|
||||
## 7. 当前可对外口径
|
||||
|
||||
当前可以准确描述为:
|
||||
|
||||
> **文档页已开始消费统一 `Page Aggregate`,标题/页面设置/正文保存也开始按 `page_head / page_layout / page_body` 收口;但树域投影统一与 AI 正式 page body 写入口仍未完全完成。**
|
||||
@@ -0,0 +1,236 @@
|
||||
# 5-2 [process] Tiptap Notion-Like 模板裁剪与 `leptos-tiptap` 映射分析 v1
|
||||
|
||||
> 更新时间:2026-04-19
|
||||
>
|
||||
> 这份文档已经按当前进展修正:
|
||||
>
|
||||
> 现在的问题不再是“这套模板能不能迁进来”,而是:
|
||||
>
|
||||
> **我们已经做出了一版 `leptos-tiptap` 编辑器,接下来应该从官方模板中保留什么行为模型,并优先把它接入主编辑器。**
|
||||
|
||||
## 1. 修正后的总判断
|
||||
|
||||
当前对官方模板的使用方式应当明确为:
|
||||
|
||||
1. 它是体验 benchmark,不是直接照搬对象。
|
||||
2. 它是行为模型参考,不是 React 代码迁移目标。
|
||||
3. 现阶段最值钱的不是再扩更多模板功能,而是把已实现能力接入主编辑器。
|
||||
|
||||
因此,这份模板的正确用途是:
|
||||
|
||||
> **借它定义“官方行为应当是什么”,再用 `leptos-tiptap` + Rust 把这些行为落到 `mnote` 的主链。**
|
||||
|
||||
## 2. 官方模板里当前最该保留的参考
|
||||
|
||||
### 2.1 主编辑器扩展组合
|
||||
|
||||
核心参考文件:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-templates/notion-like/components/notion-like-editor.tsx`
|
||||
|
||||
当前值得保留的不是整份文件,而是这些决策:
|
||||
|
||||
- 以 Tiptap 正式 schema 组织常用块
|
||||
- 常用块优先于低频复杂块
|
||||
- `task list / task item` 使用正式扩展,不做 HTML 占位
|
||||
- `UniqueID` 作为块身份辅助机制
|
||||
- `Indent` 作为普通块缩进增强的参考扩展
|
||||
|
||||
### 2.2 slash 菜单行为模型
|
||||
|
||||
核心参考文件:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-ui/slash-dropdown-menu/slash-dropdown-menu.tsx`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-ui/slash-dropdown-menu/use-slash-dropdown-menu.ts`
|
||||
|
||||
当前最该保留的是:
|
||||
|
||||
- slash 入口就是块编辑器的主入口之一
|
||||
- 菜单项的分组与命令组织方式
|
||||
- 菜单锚点应跟随 caret,而不是随便找一个固定位置
|
||||
|
||||
### 2.3 浮动工具条行为模型
|
||||
|
||||
核心参考文件:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-templates/notion-like/components/notion-like-editor-toolbar-floating.tsx`
|
||||
|
||||
当前最该保留的是:
|
||||
|
||||
- 工具条只在文本 selection 语境下出现
|
||||
- 文本格式命令和 `turn into` 要分清语境
|
||||
- 不要把固定显示工具条误当成“功能已完成”
|
||||
|
||||
### 2.4 左侧手柄与块菜单行为模型
|
||||
|
||||
核心参考文件:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-ui/drag-context-menu/drag-context-menu.tsx`
|
||||
|
||||
当前最该保留的是:
|
||||
|
||||
- hover 先只显露手柄,不自动弹块菜单
|
||||
- 块菜单应由左侧手柄点击触发
|
||||
- `turn into / duplicate / delete / drag` 是块菜单的核心动作
|
||||
|
||||
这点非常关键,因为它直接影响我们后续对标 Wolai/Notion 的交互正确性。
|
||||
|
||||
## 3. 结合当前代码后的现实判断
|
||||
|
||||
### 3.1 已经有的东西
|
||||
|
||||
从当前实现看,下面这些已经不是“要不要做”,而是“如何迁进主链”:
|
||||
|
||||
- `paragraph / heading / list / todo / quote / code block / divider`
|
||||
- slash 菜单
|
||||
- 浮动工具条
|
||||
- 左侧手柄
|
||||
- 块菜单
|
||||
- `turn into`
|
||||
- 顶层拖拽
|
||||
- refresh 后结构保留
|
||||
|
||||
对应当前实现入口:
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/spikes/leptos-tiptap-spike/src/main.rs`
|
||||
|
||||
### 3.2 当前真正的缺口
|
||||
|
||||
当前真正的缺口不是模板 feature 数量,而是下面这些:
|
||||
|
||||
- 它还在 spike 里,不在主编辑器里
|
||||
- 保存还没接上正式 Rust truth
|
||||
- `block_id` 还没成为正式合同
|
||||
- 页面引用 / 块引用还没打通
|
||||
- 图片上传与附件桥还没接
|
||||
- 表格还没做
|
||||
|
||||
所以现阶段不该继续把注意力平均分配到:
|
||||
|
||||
- Markdown 导入导出回归
|
||||
- `kode` 组件分层整理
|
||||
- 表格
|
||||
- AI Cloud
|
||||
- 协作链
|
||||
|
||||
## 4. `P0.5` 该如何采用官方模板
|
||||
|
||||
### 4.1 `P0.5` 要保留的部分
|
||||
|
||||
`P0.5` 应保留的不是一堆零散 feature,而是以下几类“官方行为”:
|
||||
|
||||
- 主编辑器使用正式 Tiptap 扩展,而不是临时 HTML 拼接
|
||||
- slash 菜单锚定 caret
|
||||
- 选中文字才出现浮动工具条
|
||||
- hover 只显手柄,点击手柄才弹块菜单
|
||||
- `turn into` 作为块菜单和工具条共享的一组块类型切换动作
|
||||
- `UniqueID` 作为 runtime 辅助,但最终块 id 仍归 Rust
|
||||
|
||||
### 4.2 `P0.5` 不要再继续扩的部分
|
||||
|
||||
`P0.5` 不应该继续向下扩这些模板能力:
|
||||
|
||||
- 协作
|
||||
- AI
|
||||
- TOC
|
||||
- 完整表格
|
||||
- 图片上传整链
|
||||
- 移动端全量工具条
|
||||
|
||||
原因很简单:
|
||||
|
||||
这些都不会帮助我们更快完成“接入主编辑器”。
|
||||
|
||||
## 5. 官方模板到 `mnote` 的映射口径
|
||||
|
||||
### 5.1 直接借行为,不借代码
|
||||
|
||||
以下部分应当“借行为模型”,不应当尝试直接照搬 React 代码:
|
||||
|
||||
- `slash-dropdown-menu`
|
||||
- `drag-context-menu`
|
||||
- `notion-like-editor-toolbar-floating`
|
||||
|
||||
原因:
|
||||
|
||||
- React hooks / context / portal 不能直接进入 Leptos
|
||||
- 真正有价值的是交互时机、状态边界、动作分组
|
||||
- 不是 `tsx` 组件本身
|
||||
|
||||
### 5.2 `block id` 需要现在就纳入主线
|
||||
|
||||
官方模板里的 `UniqueID.configure(...)` 很重要,但口径要修正成:
|
||||
|
||||
- 官方参考:
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-templates/notion-like/components/notion-like-editor.tsx`
|
||||
- Rust 真相字段:
|
||||
`/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs`
|
||||
|
||||
最终原则:
|
||||
|
||||
1. Rust `EditorBlock.block_id` 是持久化真相。
|
||||
2. Tiptap `UniqueID` 只负责浏览器 runtime 内的节点身份辅助。
|
||||
3. 不允许把前端临时 id 倒灌成正式文档真相。
|
||||
|
||||
### 5.3 `Indent` 值得做,但不在这一轮前排
|
||||
|
||||
官方缩进扩展很值得参考:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-extension/indent-extension.ts`
|
||||
|
||||
但当前它不应排在主编辑器接入之前。
|
||||
|
||||
更合理的顺序是:
|
||||
|
||||
1. 先完成主编辑器接入
|
||||
2. 再完成 Rust 保存边界
|
||||
3. 再考虑普通块缩进增强
|
||||
|
||||
### 5.4 图片上传与表格都先后置
|
||||
|
||||
官方模板确实有图片与表格能力,但现在不应让它们进入主 checklist 前排。
|
||||
|
||||
原因:
|
||||
|
||||
- 图片上传不是当前切流主编辑器的阻塞项
|
||||
- 表格更不是当前高频刚需
|
||||
- 两者都容易把工作重新带到复杂 UI / 服务桥接细节
|
||||
|
||||
## 6. 哪些旧 checklist 应从主线降级
|
||||
|
||||
下面这些项不删,但不应继续放在当前主线前排:
|
||||
|
||||
- Markdown 导入导出回归
|
||||
参考 `blocks` 仍然有价值,但它更像切流后的回归与兼容工作。
|
||||
|
||||
- 借 `kode` 做 Leptos 组件分层
|
||||
这个参考可以保留,但它属于实现注记,不是当前交付里程碑。
|
||||
|
||||
- 完整表格
|
||||
继续后置。
|
||||
|
||||
- 完整图片上传
|
||||
继续后置,只保留后续最小桥接预留。
|
||||
|
||||
## 7. 对接下来工作的直接建议
|
||||
|
||||
基于当前代码和官方模板,接下来最合理的顺序是:
|
||||
|
||||
1. 先把当前 `leptos-tiptap` 编辑器接入主编辑器。
|
||||
2. 同时建立正式的 Rust load/save 边界。
|
||||
3. 在这个过程中引入稳定 block id 策略。
|
||||
4. 等它真正成为主编辑器后,再规划 `P1 / P1.5` 的 Wolai 对标体验增强。
|
||||
|
||||
这也意味着:
|
||||
|
||||
当前官方模板对我们的最大价值,不是再提供更多可抄的 feature,而是帮助我们定义:
|
||||
|
||||
- 哪些交互已经够了
|
||||
- 哪些交互时机必须修正
|
||||
- 哪些功能现在根本不该做
|
||||
|
||||
## 8. 最终结论
|
||||
|
||||
当前对这份模板的正确使用方式是:
|
||||
|
||||
> **以后续主编辑器开发继续以 Tiptap 官方能力模型为体验上限,以 `leptos-tiptap` 为运行时接入层;先完成主编辑器接入和 Rust truth 落地,再单独规划 Wolai 对标增强,不再把模板 feature 数量当作当前阶段目标。**
|
||||
@@ -0,0 +1,591 @@
|
||||
# 5-5 [process] 主编辑区与树域单一真源对齐方案 v1
|
||||
|
||||
> 更新时间:2026-04-22
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/5-3-tiptap-leptos-rust-migration-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档只回答一个当前已经暴露出来的主线问题:
|
||||
|
||||
> **当 `leptos-tiptap` 已经成为页面内正式主编辑区后,页面树 / 文件树 / 页面标题 / 页面设置 / 页面正文,是否已经统一成 Rust 主导的单一真源架构。**
|
||||
|
||||
当前结论是:
|
||||
|
||||
> **还没有。**
|
||||
|
||||
当前系统已经不再是:
|
||||
|
||||
- `iframe runtime`
|
||||
- 纯前端假接入
|
||||
- 纯 `BlockNote` 主链
|
||||
|
||||
但也还不是:
|
||||
|
||||
- Rust 持有页面聚合真相
|
||||
- Leptos island 只消费 Rust projection
|
||||
- 树域与主编辑区共享同一份 page aggregate
|
||||
|
||||
它现在更接近于:
|
||||
|
||||
> **`Rust-aware + Convex-backed + 前端本地组装` 的混合态。**
|
||||
|
||||
因此,当前看到的这些“小问题”:
|
||||
|
||||
- 标题单一真源问题已经开始收口,但页面聚合仍未完全统一
|
||||
- 页面宽度和主编辑区宽度看起来不是一套规则
|
||||
- 页面设置大部分只改了壳层,没有真正进入主编辑器
|
||||
|
||||
并不是孤立 UI bug,而是同一个底层断层的表现:
|
||||
|
||||
> **树域 projection 与主编辑区 page runtime 之间,还缺一个统一的 page aggregate 真相层。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 先给结论
|
||||
|
||||
### 2.1 当前不能说已经实现了 Rust 单一真源
|
||||
|
||||
虽然当前文档页已经:
|
||||
|
||||
- 默认使用页面内正式 `leptos-tiptap` island
|
||||
- 不再依赖外部 iframe bridge 作为主编辑器
|
||||
- 正文保存已经能按 `workspaceId/documentId` 绑定到正确页面
|
||||
|
||||
但以下事实仍然成立:
|
||||
|
||||
- 标题链路虽已开始与树域 canonical snapshot 对齐,但还没有和正文、页面设置统一成同一组 page aggregate command family
|
||||
- 页面正文是一条独立保存链
|
||||
- 页面设置又是一条独立更新链
|
||||
- 页面树 / 文件树与页头标题的一致性已明显改善,但当前 page aggregate projection 仍主要由前端装配而不是 Rust 原生提供
|
||||
- `pageOptions` 已部分进入 `leptos-tiptap` 运行时语义层,但还没有整体收口完成
|
||||
|
||||
所以当前不能把这条线描述成:
|
||||
|
||||
> **“主编辑区、页面树、文件树已经统一为 Rust 下唯一真源”。**
|
||||
|
||||
更准确的表述应该是:
|
||||
|
||||
> **树域已经在向 Rust canonical contract 靠拢,但文档页主编辑区仍处于页面聚合尚未完成的混合切流阶段。**
|
||||
|
||||
### 2.2 当前最该收口的不是换编辑器,而是统一页面聚合边界
|
||||
|
||||
当前不该重新争论:
|
||||
|
||||
- 要不要 `Tiptap`
|
||||
- 要不要 `leptos-tiptap`
|
||||
- 要不要继续保留 Convex
|
||||
|
||||
这些结论都已经足够明确:
|
||||
|
||||
- `Tiptap` 继续作为浏览器输入 runtime
|
||||
- `leptos-tiptap` 继续作为 Leptos 内正式 editor island 接缝
|
||||
- `Convex` 继续作为当前存储 / 实时 / 协作底座
|
||||
|
||||
当前真正要收口的是:
|
||||
|
||||
> **页面这一层,到底由谁持有聚合语义,前端到底应该消费什么,编辑器到底应该回发什么。**
|
||||
|
||||
### 2.3 后续主线应固定为 Page Aggregate,而不是继续零散补洞
|
||||
|
||||
从长期架构看,当前主线不应再描述成:
|
||||
|
||||
- “继续补几个 `leptos-tiptap` 细节”
|
||||
- “再把标题同步修一下”
|
||||
- “再把页面设置接一点进去”
|
||||
|
||||
后续主线应改写成:
|
||||
|
||||
> **建立 Rust 主导的 `page aggregate`:让页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区 / AI 写入,都围绕同一组 page projection 与 page command family 运转。**
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前问题的精确判断
|
||||
|
||||
## 3.1 当前页面数据并不是一个统一聚合
|
||||
|
||||
当前文档页在加载时,实际上是把几类数据拆开读取,再由前端壳层重新拼装:
|
||||
|
||||
- 页面 meta
|
||||
- 页面标题
|
||||
- 页面设置
|
||||
- 页面正文
|
||||
- 页面子树快照
|
||||
|
||||
这意味着当前不是:
|
||||
|
||||
- Rust 返回一份稳定的 page aggregate projection
|
||||
|
||||
而是:
|
||||
|
||||
- Next route 先查 meta
|
||||
- 再查 content
|
||||
- 再把 `title/options/content/pageSubtree` 手动组成页面 props
|
||||
|
||||
这条链本身已经说明:
|
||||
|
||||
> **当前页面不是消费一份 canonical page projection,而是消费几份分裂的子结果。**
|
||||
|
||||
### 3.1.1 标题链已开始收口,但尚未完成页面聚合统一
|
||||
|
||||
当前页头标题已经不再单纯由 `DocumentContent` 内部的长期本地 `pageTitle` 真相驱动,而是开始依赖与 sidebar / breadcrumb 共享的 preferred sidebar snapshot。
|
||||
|
||||
这带来两个直接后果:
|
||||
|
||||
1. 标题不同步这一类“前端本地状态长期漂移”问题已经明显减少
|
||||
2. 但标题仍然没有和正文、页面设置一起统一到单一 page aggregate command / projection 边界中
|
||||
|
||||
于是:
|
||||
|
||||
- 页头、breadcrumb、sidebar、page tree、file tree 的标题一致性已有一轮真实收口
|
||||
- 但从聚合语义上看,标题仍是一条相对独立的更新链,不等于 page aggregate 已经完成
|
||||
|
||||
这类问题不是“防抖没配好”,而是:
|
||||
|
||||
> **标题更新仍然是页面聚合之外的一条独立 side effect 链。**
|
||||
|
||||
### 3.1.2 正文和页面树仍然不是同一份 projection family
|
||||
|
||||
当前正文编辑器使用的是:
|
||||
|
||||
- `leptos-tiptap` runtime
|
||||
- `/api/documents/save`
|
||||
- `EditorBlockDocument / Tiptap / blocks` 的转换边界
|
||||
|
||||
而页面子树 / outline / evidence 使用的是:
|
||||
|
||||
- `pageSubtree`
|
||||
- 前端本地定义的 TS 结构
|
||||
- “内容与标题未变化时复用旧快照”的策略
|
||||
|
||||
这说明当前主编辑区与树域虽然已经能同时工作,但它们仍然不是:
|
||||
|
||||
- 同一个 Rust projection family 的不同视图
|
||||
|
||||
而是:
|
||||
|
||||
- 一边是编辑器保存链
|
||||
- 一边是页面子树附带快照
|
||||
|
||||
因此当前 `pageSubtree` 更像:
|
||||
|
||||
- 页面阅读态和 TOC 的辅助投影
|
||||
|
||||
而不是:
|
||||
|
||||
- 主编辑区与树域共享的 canonical page aggregate
|
||||
|
||||
### 3.1.3 页面设置已部分进入 island,但还不是完整 editor runtime 语义
|
||||
|
||||
当前页面设置里至少有三类配置:
|
||||
|
||||
#### A. 页面壳层布局类
|
||||
|
||||
- `wideLayout`
|
||||
- `smallText`
|
||||
- `layoutDensity`
|
||||
|
||||
#### B. 页面视图类
|
||||
|
||||
- `showToc`
|
||||
- `collapseBacklinks`
|
||||
- `hideChildPages`
|
||||
|
||||
#### C. 编辑器语义类
|
||||
|
||||
- `showHeadingNumbers`
|
||||
- `showBlockRefCount`
|
||||
- `embedDefaultBlockId`
|
||||
|
||||
现在的主要问题不是“这些字段没有存下来”,而是:
|
||||
|
||||
> **它们虽然能持久化到 documents 表,也已有一部分进入 `leptos-tiptap` island,但还没有统一进入正式 page aggregate,更没有完整地进入 editor runtime 语义层。**
|
||||
|
||||
结果就是:
|
||||
|
||||
- 一部分设置已经作用于 island 布局或排版
|
||||
- 一部分设置只影响阅读态
|
||||
- 一部分设置被展示出来,但主编辑器内部几乎不消费
|
||||
|
||||
这也是为什么当前会出现:
|
||||
|
||||
> **页面设置看起来像是真的,但很多只是 UI 层局部生效。**
|
||||
|
||||
### 3.1.4 所谓“Rust 路径”里仍有明显 compat 痕迹
|
||||
|
||||
当前 rename / move / archive 等页面操作,虽然已经开始声明:
|
||||
|
||||
- `preferredCommandName`
|
||||
|
||||
但真实落地仍大量停留在:
|
||||
|
||||
- `documents.title.update`
|
||||
- `documents.move`
|
||||
- `documents.delete`
|
||||
|
||||
这说明当前并不能说:
|
||||
|
||||
- 树域命令已经完整切到 Rust tree command family
|
||||
|
||||
更不能说:
|
||||
|
||||
- 页面编辑域已经和树域命令收敛到同一个 page aggregate contract
|
||||
|
||||
因此现状应判定为:
|
||||
|
||||
> **Rust bridge 已经进入主链,但 page aggregate command cutover 仍未完成。**
|
||||
|
||||
---
|
||||
|
||||
## 4. 为什么这些问题会直接影响长期方向
|
||||
|
||||
## 4.1 它会削弱 Rust + Leptos 的真正优势
|
||||
|
||||
你们要的不是:
|
||||
|
||||
- “一个能跑的 Tiptap 页面”
|
||||
|
||||
而是:
|
||||
|
||||
- Rust 掌握页面与树的真相
|
||||
- Leptos 成为页面内正式 editor island
|
||||
- AI 最终直接接入主编辑区写作
|
||||
|
||||
如果标题、正文、设置、树节点标题分别走不同链路,那么 Rust + Leptos 的长期优势就会被削弱成:
|
||||
|
||||
- 只是“比纯前端多了一层 bridge”
|
||||
|
||||
而不是:
|
||||
|
||||
- “由 Rust 持有统一对象语义,前端只消费真相投影”
|
||||
|
||||
### 4.1.1 AI 直接写主编辑区会缺乏稳定入口
|
||||
|
||||
如果后续 AI 要直接进入主编辑区编写,而当前系统没有统一的 page aggregate command family,那么 AI 最终只能选下面几条坏路:
|
||||
|
||||
- 直接操作前端 DOM
|
||||
- 直接操作 Tiptap JSON
|
||||
- 直接调用不同的标题 / 正文 / 设置接口拼写入
|
||||
|
||||
这三条路都不符合长期目标。
|
||||
|
||||
长期正确路线必须是:
|
||||
|
||||
> **AI 先操作 Rust page aggregate 的命令与块语义,再由 editor island 把结果投影到 live editing surface。**
|
||||
|
||||
### 4.1.2 树域与页面域会继续出现“双真相漂移”
|
||||
|
||||
只要页面树 / 文件树消费的还是一套资源 meta,而主编辑区头部和页面设置是另一套前端状态,那么后续还会不断出现:
|
||||
|
||||
- 标题单一真源已开始收口,但当前尚未和正文、页面设置统一成同一 page aggregate 命令边界
|
||||
- 页面设置变更只影响一部分视图
|
||||
- 阅读态与编辑态宽度规则不一致
|
||||
- 新增一个页面能力时,要改三四条链路
|
||||
|
||||
这类问题越到后面越难收。
|
||||
|
||||
---
|
||||
|
||||
## 5. 正确的目标重写
|
||||
|
||||
当前阶段的目标不应再写成:
|
||||
|
||||
- “把 `leptos-tiptap` 再打磨得更像官方模板”
|
||||
|
||||
而应重写为:
|
||||
|
||||
> **建立 Rust 主导的 `page aggregate`,让页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区 / AI 写入共享同一份页面真相。**
|
||||
|
||||
这句话拆开后,意味着下面五点。
|
||||
|
||||
### 5.1 Page Aggregate Projection
|
||||
|
||||
Rust 侧需要提供一个统一的页面聚合投影,至少包含:
|
||||
|
||||
- `page_identity`
|
||||
- `workspace_id`
|
||||
- `document_id`
|
||||
- `node_id`
|
||||
- `page_head`
|
||||
- `title`
|
||||
- `icon`
|
||||
- `cover`
|
||||
- `updated_at`
|
||||
- `permissions`
|
||||
- `page_layout`
|
||||
- `wide_layout`
|
||||
- `small_text`
|
||||
- `layout_density`
|
||||
- `show_toc`
|
||||
- `show_structure`
|
||||
- `page_body`
|
||||
- `editor_document`
|
||||
- `revision`
|
||||
- `conflict_detection_key`
|
||||
- `stats`
|
||||
- `page_tree`
|
||||
- `outline`
|
||||
- `child_pages`
|
||||
- `resource_meta`
|
||||
|
||||
当前不要求一次把所有字段做满,但必须先把口径冻结。
|
||||
|
||||
### 5.2 Page Command Family
|
||||
|
||||
后续页面域的操作不应继续被拆成互不统属的零散 mutation,而应在语义上统一到 page aggregate 之下。
|
||||
|
||||
最小命令族至少包括:
|
||||
|
||||
- `page.head.rename`
|
||||
- `page.layout.update`
|
||||
- `page.body.save`
|
||||
- `page.body.insert_reference`
|
||||
- `page.body.set_embed_default_block`
|
||||
- `page.tree.refresh_projection`
|
||||
|
||||
这不意味着现在立刻把所有接口名都改掉,而是:
|
||||
|
||||
> **从现在开始,所有页面能力都应先判断它属于哪个 page aggregate 子域,而不是继续新增离散 mutation。**
|
||||
|
||||
### 5.3 Leptos Island 的正式边界
|
||||
|
||||
`leptos-tiptap` island 的职责应收敛为:
|
||||
|
||||
- 消费 `page_body`
|
||||
- 消费必要的 `page_layout` 编辑器相关配置
|
||||
- 回发正文与块级命令
|
||||
- 暴露可供 AI / 页面壳使用的稳定 editor handle
|
||||
|
||||
它不应再负责:
|
||||
|
||||
- 定义页面总真相
|
||||
- 自己发明页面设置语义
|
||||
- 在页面壳之外另持有一套页面元信息状态
|
||||
|
||||
### 5.4 Tree Domain 的接缝
|
||||
|
||||
页面树 / 文件树不需要知道编辑器内部细节,但必须与页面聚合共享:
|
||||
|
||||
- 同一份 `document_id`
|
||||
- 同一份页面标题
|
||||
- 同一份 `resource_meta`
|
||||
- 同一份页面可见状态与能力语义
|
||||
|
||||
也就是说:
|
||||
|
||||
- 树域保持 tree-first
|
||||
- 页面域保持 page aggregate
|
||||
- 两者通过稳定 projection contract 对齐
|
||||
|
||||
而不是:
|
||||
|
||||
- 树域一套标题来源
|
||||
- 页面域另一套标题来源
|
||||
|
||||
### 5.5 Convex 的正确定位
|
||||
|
||||
当前不应该先拆 Convex。
|
||||
|
||||
正确口径是:
|
||||
|
||||
- Convex 继续作为存储 / 实时 / 协作底座
|
||||
- Rust 持有 canonical contract 与语义编排
|
||||
- Leptos / Next 负责消费 projection 与呈现 island
|
||||
|
||||
因此,“Rust 单一真源”在当前阶段的正确含义不是:
|
||||
|
||||
- 只有 Rust 数据库
|
||||
|
||||
而是:
|
||||
|
||||
- **Rust 持有语义单一真源,Convex 持有当前持久化底座。**
|
||||
|
||||
---
|
||||
|
||||
## 6. 当前哪些地方需要纠偏
|
||||
|
||||
## 6.1 需要纠偏的不是“页面里还有前端状态”
|
||||
|
||||
页面壳中存在一些本地临时状态本身没有问题。
|
||||
|
||||
问题在于:
|
||||
|
||||
- 哪些状态是临时 UI 状态
|
||||
- 哪些状态却在冒充对象真相
|
||||
|
||||
后续必须把两者分清。
|
||||
|
||||
### 6.1.1 可以继续留在前端壳的状态
|
||||
|
||||
- inspector 开关
|
||||
- comments drawer 开关
|
||||
- history drawer 开关
|
||||
- 当前是否在编辑态
|
||||
- 当前 host observability
|
||||
|
||||
这些都是 UI state,不必进入 Rust 真相层。
|
||||
|
||||
### 6.1.2 必须退出前端壳真相地位的状态
|
||||
|
||||
- 页面标题
|
||||
- 页面宽度 / 页面布局正式选项
|
||||
- 编辑器默认嵌入位置
|
||||
- 页面 outline 的正式来源
|
||||
- 页面 stats 的正式来源
|
||||
|
||||
这些都不应继续被前端 `useState` 长期定义为页面事实。
|
||||
|
||||
## 6.2 需要纠偏的不是继续换保存底座
|
||||
|
||||
当前正文保存链已经基本可用,真正的问题不是“保存不到页面”,而是:
|
||||
|
||||
- 保存成功不等于 page aggregate 已建立
|
||||
|
||||
因此:
|
||||
|
||||
- 现在不该推翻 `documents.save`
|
||||
- 也不该另造一套临时保存格式
|
||||
|
||||
而应做的是:
|
||||
|
||||
> **把标题 / 正文 / 页面设置的语义边界对齐到同一 page aggregate 之下。**
|
||||
|
||||
## 6.3 需要纠偏的是页面设置的产品口径
|
||||
|
||||
当前页面设置里已经混入了三类字段:
|
||||
|
||||
- 真正属于页面布局的
|
||||
- 真正属于阅读壳的
|
||||
- 真正属于编辑器 runtime 的
|
||||
|
||||
后续必须先分类,再决定哪些继续保留在页面设置面板里,哪些推迟支持,哪些进入 editor island。
|
||||
|
||||
否则只会继续出现:
|
||||
|
||||
- UI 有按钮
|
||||
- 数据能存
|
||||
- 但主编辑器不真正消费
|
||||
|
||||
---
|
||||
|
||||
## 7. 分阶段落地
|
||||
|
||||
下面的阶段不是完整开发清单,而是主线纠偏顺序。
|
||||
|
||||
## 7.1 Phase F: 冻结 Page Aggregate Contract
|
||||
|
||||
目标:
|
||||
|
||||
- 在设计层明确页面聚合的 canonical contract
|
||||
- 不再让页面页头 / 正文 / 设置 / 子树各自长字段
|
||||
|
||||
完成标准:
|
||||
|
||||
- 有单独的 page aggregate contract 文档
|
||||
- 明确 `page_head / page_layout / page_body / page_tree` 的最小字段
|
||||
- 明确哪些字段是 UI state,哪些字段是 aggregate truth
|
||||
|
||||
## 7.2 Phase G: 主文档页改为消费统一聚合
|
||||
|
||||
目标:
|
||||
|
||||
- 文档页不再手工拼 `title + options + content + pageSubtree`
|
||||
- 改为消费统一页面聚合返回值
|
||||
|
||||
完成标准:
|
||||
|
||||
- 页面头部使用聚合中的 `page_head`
|
||||
- 页面设置初始值使用聚合中的 `page_layout`
|
||||
- 主编辑区 bootstrap 使用聚合中的 `page_body`
|
||||
- 树与 TOC 使用聚合中的 `page_tree`
|
||||
|
||||
## 7.3 Phase H: `leptos-tiptap` 正式消费 editor-related page options
|
||||
|
||||
目标:
|
||||
|
||||
- 把真正属于编辑器 runtime 的设置接入 `leptos-tiptap`
|
||||
|
||||
最小优先级建议:
|
||||
|
||||
1. `wideLayout`
|
||||
2. `smallText`
|
||||
3. `layoutDensity`
|
||||
4. `showHeadingNumbers`
|
||||
5. `embedDefaultBlockId`
|
||||
|
||||
完成标准:
|
||||
|
||||
- inspector 改动后,主编辑区表现真实变化
|
||||
- 不再出现“设置能存,但编辑器内部几乎没变化”
|
||||
|
||||
## 7.4 Phase I: 树域与页面域标题统一
|
||||
|
||||
目标:
|
||||
|
||||
- rename 后页头、sidebar row、page tree、file tree 一致刷新
|
||||
|
||||
完成标准:
|
||||
|
||||
- 标题变更只经过一套 page aggregate command 语义
|
||||
- 所有标题消费者都来自同一份更新后的 projection
|
||||
|
||||
## 7.5 Phase J: AI 写入入口对齐 page aggregate
|
||||
|
||||
目标:
|
||||
|
||||
- AI 不再绕过 page aggregate 直接拼前端对象
|
||||
|
||||
完成标准:
|
||||
|
||||
- AI 能通过统一页面命令把内容写入主编辑区
|
||||
- 人工编辑与 AI 编辑共享同一块语义边界
|
||||
|
||||
---
|
||||
|
||||
## 8. 当前阶段的完成判定
|
||||
|
||||
只有当下面这些条件同时成立时,才能把这条线描述成:
|
||||
|
||||
> **“主编辑区与树域已经基本统一到 Rust 主导的单一真源架构”。**
|
||||
|
||||
- 文档页消费统一 `page aggregate projection`
|
||||
- 标题 / 正文 / 页面设置不再是三条分裂真相链
|
||||
- `leptos-tiptap` island 真正消费 editor-related `page_layout`
|
||||
- 树标题与页头标题来自同一份 projection
|
||||
- AI 写入入口开始围绕 page aggregate command family 设计
|
||||
|
||||
在此之前,正确口径都应保持为:
|
||||
|
||||
> **`leptos-tiptap` 主编辑区已基本可用,但页面聚合仍未收口,当前仍处于从混合态向 Rust 单一真源过渡的过程中。**
|
||||
|
||||
---
|
||||
|
||||
## 9. 本文结论
|
||||
|
||||
这次暴露出的页面宽度不统一、页面设置只部分生效,以及标题链虽已收口但聚合仍未统一,并不是坏消息。
|
||||
|
||||
它们的价值在于:
|
||||
|
||||
> **它们准确暴露了当前真正缺的不是“再修几个细节”,而是“把页面域提升成一个与树域对齐的 Rust page aggregate”。**
|
||||
|
||||
因此,后续主线不应继续散落成:
|
||||
|
||||
- 修标题
|
||||
- 修宽度
|
||||
- 修页面设置
|
||||
|
||||
而应统一收口为:
|
||||
|
||||
> **Page Aggregate Contract**
|
||||
>
|
||||
> **Page Aggregate Projection**
|
||||
>
|
||||
> **Page Aggregate Command Family**
|
||||
|
||||
这才是让 `tree-first graph kernel`、`leptos-tiptap` 主编辑区、以及后续 AI 直接写入主编辑区三者真正对齐的正确底层方向。
|
||||
@@ -0,0 +1,269 @@
|
||||
# 5-6 [process] Page Aggregate 单一真源对齐执行清单 v1
|
||||
|
||||
> 更新时间:2026-04-22
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份清单不是再讨论“问题是不是存在”,而是把:
|
||||
|
||||
- `5-5` 里对当前混合态的判断
|
||||
- 后续 `Page Aggregate` 主线
|
||||
|
||||
整理成一份能持续勾选、持续验收、持续防止跑偏的执行清单。
|
||||
|
||||
这份清单只围绕一个目标:
|
||||
|
||||
> **让页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区 / AI 写入口,逐步收口到 Rust 主导的 page aggregate 单一真源。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前阶段结论
|
||||
|
||||
当前可以确认的事实有两类。
|
||||
|
||||
### 2.1 已经成立的事实
|
||||
|
||||
- [x] `leptos-tiptap` 已经成为页面内正式主编辑区,不再是 iframe bridge。
|
||||
- [x] 正文保存已经能按 `workspaceId/documentId` 正确落到对应页面。
|
||||
- [x] 当前主编辑区已具备可继续推进的基础交互能力。
|
||||
- [x] 当前问题已经不再是“能不能接入主编辑器”,而是“接入后如何收口为单一真源”。
|
||||
|
||||
### 2.2 还没有成立的事实
|
||||
|
||||
- [ ] 页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区还没有消费同一份 page aggregate projection。
|
||||
- [x] 标题 / 正文 / 页面设置已经开始统一到同一组 page aggregate command family。
|
||||
- [ ] `pageOptions` 还没有整体收口到 `leptos-tiptap` island 的正式运行时语义层。
|
||||
- [ ] AI 写入口还没有完整对齐 page aggregate command family。
|
||||
|
||||
补充:本轮已新增前端统一 `page-command-client`,并把 `DocumentContent` 的标题 / 页面设置写入、AI 正文写回、以及 `BlockNote` / `leptos-tiptap` 各 host 的正文保存统一到 `page.head.updateTitle / page.layout.updateOptions / page.body.save`。同时,Next route 侧已新增统一 `page-write-command-adapter`,`/api/documents/title`、`/api/documents/options`、`/api/documents/save` 三条页面写链路已开始共用同一层执行器。这里勾选的是“命令执行面开始收口”,不等于页面域真相链已经完全统一。
|
||||
|
||||
补充:2026-04-28 树域剩余 runtime 收口中,`page.body.saved` 与 `document.snapshot.saved` 已完成职责拆分;`tree.node.embed` 的 `pageReference` block 结构与插入位置已由 Rust `pageAggregateEmbedPlan` 生成,3000 route 只保留源/目标内容读取作为 substrate preflight。这使正文保存快照语义和页面嵌入正文 patch 都开始落在 Page Aggregate / Rust artifact 边界内。
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前明确不做什么
|
||||
|
||||
为了避免再次回到“零散补 UI”的老路,下面这些项当前不进入主线前排:
|
||||
|
||||
- [ ] 不把当前问题重新降级成“修两个样式 bug”。
|
||||
- [ ] 不继续通过新增前端本地状态来掩盖 page aggregate 缺失。
|
||||
- [ ] 不先重做一轮页面设置 UI。
|
||||
- [ ] 不先扩一批和 page aggregate 无关的编辑器花活。
|
||||
- [ ] 不先争论替换 `Tiptap`、替换 `leptos-tiptap`、移除 Convex。
|
||||
|
||||
---
|
||||
|
||||
## 4. Phase F:冻结 Page Aggregate Contract
|
||||
|
||||
目标:
|
||||
|
||||
> **先把页面聚合的 canonical contract 写清楚,阻止页面头部、正文、设置、子树继续各自长字段。**
|
||||
|
||||
### 4.1 设计层完成标准
|
||||
|
||||
- [x] 单独补一份 page aggregate contract 文档。
|
||||
- [x] 文档中固定 `page_identity / page_head / page_layout / page_body / page_tree` 的最小字段。
|
||||
- [x] 文档中固定哪些字段属于 page aggregate truth,哪些字段只是前端 UI state。
|
||||
- [x] 文档中固定哪些字段必须由 Rust 主导,哪些字段允许作为前端临时态存在。
|
||||
|
||||
### 4.2 代码层完成标准
|
||||
|
||||
- [x] 前端与 Rust 侧都出现统一命名的 page aggregate 类型或等价契约。
|
||||
- [x] 不再继续给 `DocumentPageProps`、`DocumentContentProps` 零散加字段来扩页面真相。
|
||||
- [x] 文档页加载入口能明确区分“聚合读取结果”和“局部 UI 临时态”。
|
||||
|
||||
补充:当前已新增 `page-aggregate-builder.ts`、`page-aggregate-loader.ts` 与 `/api/documents/page`,文档页 SSR 入口和 `DocumentContent` 的内容重试补拉都已改为消费同一份 `PageAggregateProjection`,不再由 `page.tsx` 手工拼 `meta + content`。Rust 侧仍未直接暴露同名 `Page Aggregate` projection route,但 `storage-convex-bridge` 与 `bridge-runtime` 已开始接受 `page.head.updateTitle / page.layout.updateOptions / page.body.save` 这组 page command family 的命名口径,因此这里先按“等价契约已出现”勾选完成。
|
||||
|
||||
### 4.3 退出标准
|
||||
|
||||
- [x] 后续再讨论标题、页面设置、正文保存时,能够直接定位到它属于 `page_head / page_layout / page_body / page_tree` 的哪一层。
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase G:主文档页改为消费统一聚合
|
||||
|
||||
目标:
|
||||
|
||||
> **让 `/documents/[id]` 不再手工拼 `title + options + content + pageSubtree`,而是消费统一页面聚合。**
|
||||
|
||||
### 5.1 加载链路收口
|
||||
|
||||
- [x] 页面 SSR / route 入口改为优先读取统一 page aggregate,而不是分别读取 meta 和 content。
|
||||
- [x] `DocumentShell` / `DocumentContent` 的入参改为围绕统一聚合对象组织。
|
||||
- [x] 页面头部、页面设置初值、主编辑区 bootstrap、TOC/outline 都来自同一份聚合结果。
|
||||
|
||||
### 5.2 当前散落 props 的收口
|
||||
|
||||
- [x] `title` 不再作为独立真相字段散落传递。
|
||||
- [x] `initialOptions` 不再作为独立真相字段散落传递。
|
||||
- [x] `initialContent`、`initialRevision`、`initialConflictDetectionKey` 归入统一 `page_body`。
|
||||
- [x] `initialPageSubtree` 归入统一 `page_tree`,并明确其是正式 projection 还是仅兼容过渡项。
|
||||
|
||||
### 5.3 退出标准
|
||||
|
||||
- [x] 页面加载时不再能明显看出“这是几份子结果拼出来的页面”。
|
||||
- [ ] 后续页面新增字段时,不再需要继续向外层 props 链同时塞多种局部真相。
|
||||
|
||||
补充:当前首屏 SSR 与客户端内容重试补拉都已经走 `/api/documents/page -> page aggregate loader -> PageAggregateProjection`,因此“页面明显由 `meta + content` 两次查询拼起来”的入口级痕迹已经消失;但新增字段仍可能需要继续补 loader / route / island 消费链,所以第二项继续保留未完成。
|
||||
|
||||
补充:本轮已新增 `page-aggregate-client-state.ts`,并让 `DocumentContent` 把原先分散维护的 `options / content / serverContentSnapshot / serverPageSubtreeSnapshot / serverPageSubtreeTitle / contentRevision / conflictDetectionKey` 开始收口为同一份 client aggregate state reducer。这里代表“页面本地 `body/layout/tree` 真相已经开始统一”,不等于标题链、页面设置运行时分类、AI 对页面设置面的正式入口都已闭环,因此本阶段不提前宣称聚合完成。对应最小回归测试为 `page-aggregate-client-state.test.ts`。
|
||||
|
||||
---
|
||||
|
||||
## 6. Phase H:让 `pageOptions` 真正进入 `leptos-tiptap` island
|
||||
|
||||
目标:
|
||||
|
||||
> **把当前“能展示、能保存、但主编辑区不真正消费”的页面设置,按优先级接入 island。**
|
||||
|
||||
### 6.1 必须先分类
|
||||
|
||||
- [x] 把当前页面设置分成:页面壳布局类、阅读视图类、编辑器 runtime 语义类。
|
||||
- [ ] 明确哪些项应继续保留在页面设置面板中,哪些项应降级或隐藏,哪些项必须进入 island。
|
||||
|
||||
补充:本轮已新增 `page-option-semantics.ts`,把 `pageOptions` 的运行时语义正式收口为代码契约,不再让这套规则继续散落在 `page-options-sidebar.tsx`、`editor-host-types.ts` 与 `leptos-tiptap-island-editor-host.tsx` 里各写一份。当前至少已经明确:
|
||||
|
||||
- `wideLayout / smallText / layoutDensity` 属于 `page_shell_layout + editor_runtime`
|
||||
- `showHeadingNumbers` 属于 `read_view + editor_runtime`
|
||||
- `showToc` 属于 `read_view`
|
||||
- `protectEditing / showBlockRefCount` 属于 `editor_runtime`,但当前仍是 `planned`
|
||||
- `embedDefaultBlockId` 已进入 `editor_runtime`
|
||||
|
||||
对应最小回归测试为 `page-option-semantics.test.ts`、`leptos-tiptap-island-editor-host.test.tsx`、`page-options-sidebar.test.tsx`。第二项继续保留未完成,因为“哪些项应降级/隐藏/保留”的产品面纠偏还没完全落到 inspector 与 AI tool surface。
|
||||
|
||||
### 6.2 最小接入优先级
|
||||
|
||||
- [x] `wideLayout` 真正进入 island 布局语义,而不是只改外层壳宽度。
|
||||
- [x] `smallText` 真正进入主编辑区排版语义。
|
||||
- [x] `layoutDensity` 真正进入块间距 / 正文密度语义。
|
||||
- [x] `showHeadingNumbers` 真正进入 heading 展示语义,或明确标注暂不支持。
|
||||
- [x] `embedDefaultBlockId` 真正进入主编辑器引用 / 嵌入默认位置逻辑,或明确标注暂不支持。
|
||||
|
||||
### 6.3 页面设置面板纠偏
|
||||
|
||||
- [x] 没有真正接入 island 的编辑器语义项,不再继续以“已开启/已关闭”假装正式完成。
|
||||
- [x] 对未支持项给出显式降级说明,而不是仅保存字段。
|
||||
- [ ] 让“页面设置有值但编辑器内部没变化”这类状态在产品上消失。
|
||||
|
||||
### 6.4 退出标准
|
||||
|
||||
- [ ] inspector 中至少最小优先级项修改后,主编辑区可见结果真实变化。
|
||||
- [ ] 用户不再需要猜“这个设置到底有没有真正作用到编辑器”。
|
||||
|
||||
---
|
||||
|
||||
## 7. Phase I:树域与页面域标题统一
|
||||
|
||||
目标:
|
||||
|
||||
> **rename 后页头、sidebar row、page tree、file tree 使用同一份标题结果。**
|
||||
|
||||
### 7.1 标题真相纠偏
|
||||
|
||||
- [x] 页面头部标题不再长期由前端 `pageTitle` 本地状态冒充正式真相。
|
||||
- [x] rename 命令在语义上进入统一 page aggregate command family。
|
||||
- [x] 树域消费的标题与页面头部消费的标题来自同一份更新后的 projection。
|
||||
|
||||
### 7.2 回归验证
|
||||
|
||||
- [x] 当前页重命名后,页头即时更新。
|
||||
- [x] sidebar 对应节点标题同步更新。
|
||||
- [x] page tree / file tree 对应节点标题同步更新。
|
||||
- [x] 刷新后标题一致,不依赖额外手动刷新。
|
||||
- [x] 切页往返后标题一致,不出现旧快照回闪。
|
||||
|
||||
注:当前已在标题持久化成功后广播 `emitDocumentsChanged(documentId)`,并补了 `sidebar-events.test.ts` 覆盖 `documents-changed -> sidebarRefetch` 的刷新链路。
|
||||
|
||||
补充:已新增 `usePreferredSidebarSnapshot`,当 `tree stream` 已连上但快照落后、`documents-changed` 触发的 query/refetch 先拿到新标题时,会优先消费更新后的 canonical sidebar snapshot;待 stream 追平后再回到 live stream。对应回归测试为 `use-preferred-sidebar-snapshot.test.tsx`。这修掉了“刷新链发出去了,但 stale stream 仍把 sidebar 标题压回旧值”的一类问题;浏览器层 `sidebar row / page tree / file tree` 真实渲染验收仍待补齐。
|
||||
|
||||
补充:已新增 `AppLayoutShell`,把 layout 顶栏 `Breadcrumb` 从 SSR 注入的静态 `documents` 挪到与 `Sidebar` 共享的同一条 live sidebar snapshot 管线;并且 layout shell 会把“已选中的 preferred snapshot”同一对象同时透传给 `Sidebar` 与 `Breadcrumb`,不再各自独立选择。对应回归测试为 `app-layout-shell.test.tsx`。这意味着 breadcrumb / sidebar 现在至少共享同一份工作区树 canonical snapshot,不再是 layout 一条静态链、sidebar 一条 live 链并行。页头 `page.head.title` 与这条工作区树链之间的最终统一验收仍待补齐,因此本阶段继续不提前打满。
|
||||
|
||||
补充:已新增 `PreferredSidebarSnapshotProvider` 与 `usePageHeadTitle`,把文档页头标题从 `DocumentContent` 内部长期持有的 `pageTitle` 本地真相,改为“同一份 preferred sidebar snapshot 的 committed title + 短暂 draft”。同时修正 `useSidebarData.refetch()`,在 Convex live 模式下收到 `documents-changed` 也会主动拉取一份新的 `/api/sidebar` snapshot,再与 tree stream 做 freshness 选择,避免“页头草稿是新的,但 breadcrumb / sidebar / page tree / file tree 还卡在旧快照”。对应单测为 `use-page-head-title.test.tsx`、`use-sidebar-data.test.tsx`。浏览器烟测 `scripts/task110-page-title-single-truth-smoke.js` 已验证:页头重命名后,breadcrumb、默认 sidebar、page tree、file tree、切页往返、刷新均保持一致,因此本阶段与“标题来自同一份更新后的 projection”相关的勾选正式保留。
|
||||
|
||||
### 7.3 退出标准
|
||||
|
||||
- [x] 标题不同步问题不再以“前端本地状态漂移”的形式重复出现。
|
||||
|
||||
---
|
||||
|
||||
## 8. Phase 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 单一真源过渡的过程中。**
|
||||
+935
@@ -0,0 +1,935 @@
|
||||
# 5-7 [process] Wolai 页面树与主编辑器体验复刻方案 v1
|
||||
|
||||
> 更新时间:2026-04-30
|
||||
>
|
||||
|
||||
> **Wolai-aline 执行口径更新(2026-04-30):** 本文只保留体验复刻任务拆解和产品目标。所有 Wolai 对标测试、浏览器取证、编辑权限、安全边界、subagent 使用、截图复核和 smoke 补齐,统一以 `/home/lix/.codex/skills/wolai-aline` 与 `/mnt/Data1T/mnote/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md` 为准。若本文旧段落与该 skill 冲突,以 skill 为准。
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-2-tiptap-notion-like-template-adoption-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份方案只处理一件事:
|
||||
|
||||
> **以真实 Wolai 页面为视觉和交互 benchmark,继续还原 mnote 的页面树与主编辑器体验。**
|
||||
|
||||
本轮目标不是重新定义系统架构,也不是把 Wolai 的全部协作能力完整复制过来,而是把当前 `3000` 页面在视觉密度、布局节奏、页面树反馈、主编辑器输入体验上继续向 Wolai 靠近。
|
||||
|
||||
约束如下:
|
||||
|
||||
- UI 以像素级复刻为目标。
|
||||
- 功能以“高频路径可用、复杂能力可降级”为原则。
|
||||
- 当前项目保留“文件树 / Explorer”创新,不要求删除。
|
||||
- 默认主编辑器继续是页面内 `leptos-tiptap` island。
|
||||
- `BlockNote` 只作为 fallback / 对照链,不重新变成默认方向。
|
||||
- 树、页面结构、标题、正文、页面设置不能在 UI 层再拼第二份真相。
|
||||
|
||||
这份文档应放在 `05-editor-mainline/process`,因为它的核心不是 Rust Web 壳复刻,也不是单独的树命令合同,而是:
|
||||
|
||||
> **围绕 Page Aggregate,把页面树、页头、页面设置、正文编辑器统一成接近 Wolai 的产品体验。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 本轮取证基线
|
||||
|
||||
### 2.1 真实 Wolai 参考
|
||||
|
||||
本轮参考页面:
|
||||
|
||||
- `https://www.wolai.com/wolai/xhqeop8UHpVTMUSVmgz8nq`
|
||||
- `https://www.wolai.com/wolai/qN1Bh9YjLAXs8bxCJoAJ6C`
|
||||
- `https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd`
|
||||
|
||||
本轮已保存的截图与快照:
|
||||
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-page1.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-page1-snapshot.md`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-page2.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-page2-snapshot.md`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-target-1392x1213.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-target-snapshot.md`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-public-search-overlay.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-public-search-results-skill.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-public-presentation-mode.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-public-start-edit-overlay.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-public-comment-panel.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-public-good-night-mode.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-help-first-viewport-current.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-help-sidebar-filter-page-reference.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-help-search-overlay.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-help-search-results-page-reference.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-help-search-result-navigated-block-reference.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-help-embedded-page-reference-hover.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/wolai-help-block-reference-page-snapshot.md`
|
||||
- `/mnt/Data1T/mnote/wolai-basic-editing-reference.png`
|
||||
- `/mnt/Data1T/mnote/wolai-basic-editing-reference-snapshot.md`
|
||||
- `/mnt/Data1T/mnote/wolai-hermes-public-hover-block.png`
|
||||
- `/mnt/Data1T/mnote/wolai-hermes-start-edit-login-dialog.png`
|
||||
- `/mnt/Data1T/mnote/wolai-hermes-comment-panel.png`
|
||||
- `/mnt/Data1T/mnote/wolai-hermes-presentation-mode.png`
|
||||
- `/mnt/Data1T/mnote/wolai-hermes-good-night-mode.png`
|
||||
|
||||
用户在内置浏览器中提供的登录态截图也作为本轮重要视觉证据:该截图展示了真实个人空间的长页面树、当前页 `Hermes` 选中态、完整顶部图标区、底部垃圾桶 / 模板中心入口、右下角 AI / 帮助浮动入口。
|
||||
|
||||
补充说明:历史截图和用户提供的登录态截图只作为现有证据池,不再作为后续任务的唯一验收依据。该 owner 态菜单不是单纯样式:它包含“在右侧边栏打开 / 移动到 / 嵌入到 / 复制访问链接 / 复制页面引用链接 / 复制页面 ID / 拷贝副本 / 重命名 / 删除”等页面树动作,应进入后续交互合同,而不是只当成截图复刻。
|
||||
|
||||
2026-04-30 口径更新:后续取证统一从 `wolai-aline` skill 启动。已知技术路径是 `Playwright Node + /opt/google/chrome/chrome + /mnt/Data1T/mnote/tmp/wolai-playwright-profile`,但本文不再维护独立自动化方案。当前 Hermes 测试页已授权用于最小编辑验证;owner 首屏、搜索浮层、顶部更多菜单、正文块 hover、`.hover-block-menu` 块菜单等行为,都必须通过 subagent 浏览器取证、主线程截图复核和差异矩阵重新确认。`dokobot doko read --local --reuse-tab` 可作为登录态 read evidence,但不能替代键鼠操作验收。
|
||||
|
||||
### 2.2 当前 mnote 参考
|
||||
|
||||
当前本地 `3000` 截图:
|
||||
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/local-3000-playwright-1392x1213.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/local-3000-first-viewport.png`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/local-3000-dom-snapshot.txt`
|
||||
|
||||
当前本地页面已经具备接近 Wolai 的基本壳:
|
||||
|
||||
- 左侧 workspace / 快捷操作 / 页面树 / 底部入口。
|
||||
- 右侧顶部 breadcrumb / 页面操作。
|
||||
- 中央文档页标题和正文区。
|
||||
- 右下角帮助与 AI 入口。
|
||||
|
||||
但与真实 Wolai 仍有显著差距:
|
||||
|
||||
- 左侧树的视觉密度、选中态、图标系统、滚动条和层级缩进仍不够像 Wolai。
|
||||
- 文档主内容的起始位置、标题尺寸、正文列宽、页头留白和默认元信息展示不一致。
|
||||
- 顶栏按钮体系与 Wolai 的 owner / published 两种状态还没有明确分型。
|
||||
- 主编辑器的块级 hover 手柄、slash、浮动工具条和块菜单需要按 Wolai / Notion 风格继续收口。
|
||||
- 当前页面树、页头、正文、页面设置仍必须继续服从 Page Aggregate 单一真源主线,不能为了 UI 快速复刻重新制造局部状态。
|
||||
|
||||
### 2.3 2026-04-30 subagent 基线复核
|
||||
|
||||
本轮已按 `wolai-aline` skill 派 subagent 同时操作 Wolai Hermes 与本地 `3000`。工具链结论:`Node.js Playwright + /opt/google/chrome/chrome` 可操作两个目标;Wolai 通过 `/mnt/Data1T/mnote/tmp/wolai-playwright-profile` 进入 owner 登录态,本地 `3000` 返回可用页面。无源码改动,证据写入:
|
||||
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-editor-parity/doc-baseline-mini/`
|
||||
|
||||
主线程已复核关键截图:
|
||||
|
||||
- Wolai 首屏:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/doc-baseline-mini/wolai-initial.png`
|
||||
- 本地首屏:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/doc-baseline-mini/local-initial.png`
|
||||
- Wolai 搜索:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/wolai-03-search-modal-controls-filled.png`
|
||||
- 本地搜索:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/local-03-search-modal-controls-filled.png`
|
||||
- Wolai 块 hover:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/wolai-08-block-hover-insert-entry.png`
|
||||
- 本地块 hover:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/local-14-block-hover-insert-entry.png`
|
||||
- Wolai 编辑清理后:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/wolai-14-title-suffix-after.png`
|
||||
|
||||
当前差异矩阵:
|
||||
|
||||
| 项 | Wolai Hermes | 本地 `3000` | 后续处理 |
|
||||
| --- | --- | --- | --- |
|
||||
| 首屏入口 | owner 登录态直接显示 `Hermes` 正文页,标题、正文、页面树同屏 | 默认是聚合 / 预览壳,正文编辑器需点击 `打开当前页面` 才出现 | 拆成 P0/P1 独立任务;先用 smoke 固化“普通文档首屏直接进入正文体验”或明确产品降级 |
|
||||
| 侧栏数据与选中态 | 真实空间树,当前页在 `个人 / 软件开发 / Hermes` 路径下,红色选中态明显 | anonymous 示例树,当前页为 `1111`,行密度和图标体系仍偏本地化 | Sidebar checklist 必须同时验视觉和真实 active path |
|
||||
| 搜索入口 | 左侧放大镜可打开全局搜索;本轮 `Ctrl+K` 未稳定打开,`Ctrl+P` 在另一轮可 toggle | 顶栏搜索可打开;侧栏搜索曾修为 modal;`Ctrl+P` 可 toggle | 搜索任务不得只测一个入口;需记录每个入口、快捷键和 URL |
|
||||
| 搜索控件 | modal 居中,遮罩、结果行、快捷键提示完整;switch 视觉可见 | modal 结构接近,`role="switch"` 可测;不同输入下结果为空或本地数据结果 | switch 默认状态必须同时记录截图和 `aria-checked`,结果态需拆数据差异与控件差异 |
|
||||
| 搜索结果 | 输入 `Hermes` 命中页面和块,结果有红色高亮和路径 | 本地同词可出现空态或示例数据结果,取决于当前页面数据 | 建立固定本地 fixture 或 smoke 种子,不能用随机工作区数据验视觉 |
|
||||
| 正文编辑区 | 首屏正文 `contenteditable=true`,标题也可编辑 | 首屏无可见编辑区;点击后出现 title textarea 与 `.tiptap.ProseMirror` | 文档画布任务必须先消除“打开当前页面”入口语义差异或显式记录降级 |
|
||||
| 编辑焦点风险 | Wolai 标题和正文均可编辑,测试输入曾误落到标题,已清理 | 本地输入 / 撤销可完成,但页面结构不同 | editable-test mode 前必须先断言焦点 block 类型,优先在正文末尾新建唯一测试块 |
|
||||
| 块 hover | 正文块左侧出现轻量块控制入口,视觉很克制 | 本地显示 `+` 与拖拽/块句柄按钮,但位置、密度、内容列差异明显 | E2-E4 必须以截图和 hover 点位复核,不可只看按钮存在 |
|
||||
|
||||
这轮基线说明:当前最大偏差不是单个按钮,而是本地普通文档首屏仍像“聚合入口 / 预览页”,Wolai 则直接进入可读写的页面正文。后续 P0/P1 checklist 必须优先处理这个入口语义,否则搜索、块 hover、编辑器测试都会测到不同页面状态。
|
||||
|
||||
### 2.4 Wolai-aline 取证与验收统一口径
|
||||
|
||||
本节旧版“published / owner readonly / sandbox mutation”分层已经收口到 `wolai-aline` skill。后续执行不再从本文推导测试策略,统一遵循:
|
||||
|
||||
- Codex skill:`/home/lix/.codex/skills/wolai-aline`
|
||||
- 流程文档:`/mnt/Data1T/mnote/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md`
|
||||
- 失败模式:`/home/lix/.codex/skills/wolai-aline/references/failure-patterns.md`
|
||||
|
||||
现行要点:
|
||||
|
||||
- 每个 Wolai-aline 小任务都必须先取 Wolai 基线,再做本地 RED smoke,再实现,再由 subagent 浏览器复测,最后主线程复核截图和差异矩阵。
|
||||
- 浏览器取证必须使用 subagent;subagent 默认只读,但当任务明确需要编辑器行为时,可在当前 Hermes 测试页进入 `Hermes editable-test mode` 做最小编辑验证。
|
||||
- 当前 Hermes 测试页 `https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd` 已授权用于最小编辑验证;可以创建带唯一标记的临时测试块、输入少量测试文本、验证 slash / toolbar / block menu / 快捷键等编辑器行为。
|
||||
- 编辑验证必须记录编辑前后截图、动作链、输入内容和清理状态。若安全清理有风险,不扩大操作,报告残留测试内容。
|
||||
- 即使在 Hermes 测试页,也禁止未经确认地删除、移动、归档、发布、评论、改权限或批量修改既有非测试内容。
|
||||
- 其他 Wolai 页面仍默认只读;写入仍需用户提供沙盒页 URL 和明确授权。
|
||||
- 对标不能只看文案或 DOM 是否存在;必须主动检查截图中的控件形态、状态、hover/active、快捷键 toggle、关闭路径和 URL 跳转。
|
||||
|
||||
硬门槛:
|
||||
|
||||
- 不能把 subagent 文本结论当作最终对齐证据;主线程必须复核截图。
|
||||
- 如果截图里有肉眼可见差异,先补 smoke 捕获差异,再改实现。
|
||||
- 任何新发现的稳定失败模式,都要沉淀到 `wolai-aline` skill 的 `failure-patterns.md`。
|
||||
|
||||
### 2.5 `基本编辑能力` 操作矩阵
|
||||
|
||||
`https://www.wolai.com/wolai/qN1Bh9YjLAXs8bxCJoAJ6C` 是 Wolai 对“主编辑器基础操作”的官方内容页。本页不只是说明文字,它给出了后续 `3000` 必须能复现的主编辑器操作矩阵:
|
||||
|
||||
| 操作 | Wolai 参考页说明 | 当前对 `Hermes` 可自动化验证性 | `3000` 后续验收要求 |
|
||||
| --- | --- | --- | --- |
|
||||
| 普通文本输入 / 回车 / 光标移动 | 像普通文本编辑器一样输入、换行、移动光标 | 可用 Hermes editable-test mode 取基线 | 在本地编辑态 smoke 中验证输入、换行、保存后刷新保持 |
|
||||
| 块模型 | 文本、列表项、图片、文件等都是“块” | owner 态只读已可观察块 hover 手柄和 block menu | 本地每个块需有稳定 block id 与 hover target |
|
||||
| 拖动块 | 块左侧 `::` 图标可拖动,并显示辅助线 | owner 态只读可观察手柄;真实拖动是写入动作,按 Hermes editable-test mode 最小验证 | 本地必须验证拖拽预览、横向 drop line、分栏 drop line |
|
||||
| Esc 选中块 | 输入态按 `esc` 选中当前块,上下方向键切换,`shift + 上/下` 多选,`enter` 回编辑 | 可用 Hermes editable-test mode 取基线 | 本地必须有 block selection state、keyboard smoke |
|
||||
| `cmd/ctrl + A` | 输入态第一次选中当前块文字,第二次选中所有块;块选中态直接选中所有块 | 可用 Hermes editable-test mode 取基线 | 本地必须区分 text selection 与 block selection |
|
||||
| 上 / 下插入块 | hover 左侧 `::` 上方 / 下方点击 `+`;或 `esc` 选中块后按 `a` / `b` | owner 态可观察手柄;真实插入可用 Hermes editable-test mode 做最小验证 | Hermes editable-test mode 必须验证 before / after 插入与 `Esc, a` / `Esc, b` |
|
||||
| 块布局显示 | `cmd/ctrl + shift + U` 显示 / 隐藏块布局虚线框 | 可用 Hermes editable-test mode 取基线 | 本地需有布局 overlay smoke,至少验证虚线框开关 |
|
||||
| 分栏 | 拖块到另一个块左 / 右,辅助线由横线变竖线,松开形成分栏 | 可用 Hermes editable-test mode 取拖拽基线 | 本地可先做 drop preview,不要求完整分栏持久化一次到位 |
|
||||
| 转换块 | 通过块菜单 `转换为`、快捷键、或 slash 命令转换块类型 | owner 态已可打开块菜单并看到 `转换为`;执行转换可用 Hermes editable-test mode 做最小验证 | 本地必须验证块菜单中的 `转换为` 入口与至少 paragraph/headings/list 的转换 |
|
||||
| 复制 / 粘贴 | 文本或块选中后 `cmd/ctrl + C/V`,匹配样式为 `cmd/ctrl + shift + V` | 可用 Hermes editable-test mode 取基线 | 本地需验证纯文本粘贴与块复制的最小路径 |
|
||||
| 缩进 / 取消缩进 | `tab` 缩进成为上一块子元素,`shift + tab` 取消缩进 | 可用 Hermes editable-test mode 取基线 | 本地需验证 list / paragraph 缩进语义与 tree projection 不冲突 |
|
||||
| 文本样式工具条 | 选中文字出现工具条,支持粗体、斜体、下划线、删除线、行内代码、颜色、链接、页面引用、转换块类型 | 可用 Hermes editable-test mode 取基线 | 本地需验证 selection toolbar 出现、按钮视觉与 command dispatch |
|
||||
| slash 菜单 | 输入 `/` 唤起快捷命令菜单;空行左侧 `+` 也可创建块 | 可用 Hermes editable-test mode 取基线 | 本地需验证 `/` 菜单、搜索过滤、选择条目插入块 |
|
||||
|
||||
本轮已能在目标页 `Hermes` 自动化验证的 published 操作:
|
||||
|
||||
- 顶栏搜索:打开搜索 modal,输入 `技能`,出现 `共1条匹配结果`。
|
||||
- 开始编辑:点击后出现 `登录以编辑` 对话框,文案为“登录后,您才可以编辑该页面,是否继续?”,包含 `取消` 和 `继续`。
|
||||
- 评论:点击后打开右侧评论面板,tab 为 `未解决(0)`,空态为 `还没有评论`。
|
||||
- 演示模式:点击后进入 presentation view,隐藏侧栏 / 顶栏,画面只保留大字号内容和演示控件。
|
||||
- Good Night:右下角主题按钮切换暗色 token。
|
||||
- 正文块 hover:owner 态可在块左侧看到手柄,并可点击 `.hover-block-menu` 打开块菜单;上 / 下插入块等写入路径按 `wolai-aline` 的 Hermes editable-test mode 验收。
|
||||
|
||||
因此,后续 `3000` 的主编辑器验收不能只做“截图像”。每个主编辑器改动都必须绑定一个可执行操作:
|
||||
|
||||
- 如果是 published 态能力,必须先在 Wolai published 目标页执行同类操作,再在 `3000` 执行同类操作。
|
||||
- 如果是 owner/edit 态能力,必须先通过 `wolai-aline` 流程在 Wolai 目标页执行同类操作,再在 `3000` 执行同类操作。
|
||||
- 如果是 owner/edit 写入能力,当前 Hermes 测试页允许最小编辑验证;其他 Wolai 页面写入必须先取得沙盒页 URL 与明确授权。
|
||||
- 如果动作是在当前 Hermes 测试页做最小编辑验证,例如输入测试文本、创建临时测试块、验证 slash / toolbar / block menu / 快捷键,可按 `Hermes editable-test mode` 执行并记录前后证据;删除、移动、归档、发布、评论、改权限或批量修改既有非测试内容仍必须在动作发生前单独确认。
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前代码落点
|
||||
|
||||
### 3.1 工作区壳与页面树
|
||||
|
||||
相关入口:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/layout.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/app-layout-shell.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/server/sidebar-data.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/hooks/use-sidebar-data.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/tree-shell-surface.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/tree-shell-host.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/tree-shell-dom-host.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/tree-projection.ts`
|
||||
|
||||
判断:
|
||||
|
||||
- `Sidebar` 外壳和 `tree-shell-*` 是页面树像素复刻的主战场。
|
||||
- `tree-shell-dom-host` 承担当前树行渲染与本地展开/选中/拖拽反馈,不应再回到旧 React 树渲染器上做长期改造。
|
||||
- `tree-projection` 是页面树 UI 的直接 projection 输入,不应在 UI 层重新推导排序、层级或合法性。
|
||||
|
||||
### 3.2 文档页与主编辑器
|
||||
|
||||
相关入口:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-aggregate-loader.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-aggregate-builder.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-shell.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-read-view.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-island-editor-host.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-option-semantics.ts`
|
||||
|
||||
判断:
|
||||
|
||||
- `document-content.tsx` 决定 Wolai 页面顶部留白、标题、正文容器、页面元信息、右侧 inspector 和 fallback banner。
|
||||
- `document-read-view.tsx` 决定阅读态块样式、任务块、标题层级、附件 / 引用 / 子页面等展示。
|
||||
- `leptos-tiptap-island-editor-host.tsx` 决定编辑态输入 surface、保存、slash、工具条、块菜单和 editor runtime page options 的接缝。
|
||||
- `page-option-semantics.ts` 是页面设置能否真实进入主编辑器运行时的判断边界,不能只改设置面板外观。
|
||||
|
||||
### 3.3 全局视觉 token
|
||||
|
||||
相关入口:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/globals.css`
|
||||
|
||||
判断:
|
||||
|
||||
- 像素复刻必须先固定字体、字号、行高、颜色、圆角、hover/active token。
|
||||
- 当前全局字体口径有冲突:前面偏 Wolai 系统字体,后面 `html` 又偏 `Inter`。若不先收口字体栈,后续所有标题宽度、行高、树行密度都会偏。
|
||||
|
||||
---
|
||||
|
||||
## 4. Wolai 视觉目标合同
|
||||
|
||||
### 4.1 字体与基础排版
|
||||
|
||||
目标字体栈:
|
||||
|
||||
- 优先中文系统字体:`PingFang SC`、`Noto Sans CJK SC`、`Microsoft YaHei UI`。
|
||||
- 英文字体跟随系统 sans-serif,不单独强行使用 `Inter` 作为全局主字体。
|
||||
|
||||
目标字号与行高:
|
||||
|
||||
| 区域 | 字号 | 行高 | 字重 |
|
||||
| --- | --- | --- | --- |
|
||||
| 页面主标题 | 34px | 44px | 600 |
|
||||
| 首页 / 帮助页大分组标题 | 28px | 36px | 600 |
|
||||
| 次级分组标题 | 18px | 26px | 600 |
|
||||
| 正文基础文字 | 16px | 24px | 400 |
|
||||
| 左侧树条目 | 14px | 24px | 400 |
|
||||
| 顶栏文字按钮 | 14px | 21px | 400 |
|
||||
| 搜索框文字 | 14px | 22px | 400 |
|
||||
|
||||
目标颜色:
|
||||
|
||||
| 语义 | 颜色 |
|
||||
| --- | --- |
|
||||
| 主标题 | `rgb(30, 30, 30)` |
|
||||
| 正文 | `rgba(0, 0, 0, 0.85)` |
|
||||
| 次级文字 | `rgba(0, 0, 0, 0.65)` |
|
||||
| 弱提示 | `rgba(0, 0, 0, 0.35)` |
|
||||
| 左侧背景 | `rgb(245, 245, 245)` |
|
||||
| 选中强调文字 | `rgb(207, 86, 89)` |
|
||||
| 选中强调背景 | `rgba(207, 86, 89, 0.15)` |
|
||||
| 顶栏 hover 背景 | `rgb(245, 245, 245)` |
|
||||
|
||||
### 4.2 左侧页面树
|
||||
|
||||
Wolai 目标特征:
|
||||
|
||||
- 左侧栏宽度约 `248px - 260px`,浅灰底。
|
||||
- 顶部空间名行高度约 `40px`,头像为 28px 左右方形圆角块。
|
||||
- 顶部快捷图标横排,图标弱色,hover 只加浅灰底。
|
||||
- 搜索框尺寸约 `236px x 32px`,圆角 `4px`,内边距约 `5px 32px 5px 12px`。
|
||||
- 树行高度约 `32px`,内容行内 icon / arrow / title 对齐。
|
||||
- 当前项使用红字 + 淡红底,背景覆盖整行可点击区域。
|
||||
- 未选中 hover 使用同一红色体系,但透明度更轻。
|
||||
- 缩进按层级稳定递增,不靠文本空格。
|
||||
- 滚动条细、贴右侧、弱灰色,不抢视觉。
|
||||
- 底部垃圾桶 / 模板中心固定在侧栏底部,边界线轻。
|
||||
|
||||
当前 mnote 允许保留:
|
||||
|
||||
- `我的页面 / Explorer` 双模式。
|
||||
- 文件树作为创新入口。
|
||||
- AI / Graph / 文件等自有入口。
|
||||
|
||||
但这些创新必须满足:
|
||||
|
||||
- 视觉密度服从 Wolai 左侧栏。
|
||||
- 选中态和 hover 态与页面树统一。
|
||||
- 不因为多了文件树而破坏 Wolai 的 32px 行节奏。
|
||||
|
||||
### 4.3 顶部导航与页面操作区
|
||||
|
||||
Wolai 目标特征:
|
||||
|
||||
- 顶栏高度约 `40px`。
|
||||
- 左侧是菜单按钮 + breadcrumb。
|
||||
- breadcrumb 使用图标 / 文本 / 分隔符,字号 `14px`,颜色弱。
|
||||
- 右侧按钮是弱文本 / 图标按钮,padding 约 `2px 7px`,圆角 `3px`。
|
||||
- owner 态可见:公开状态、收藏、演示、评论、关系、邀请、历史、更多。
|
||||
- published 态可见:演示模式、搜索、开始编辑、评论、复制、开始使用 wolai。
|
||||
- 顶栏 hover 不用重色按钮,只加浅灰底。
|
||||
|
||||
mnote 当前差距:
|
||||
|
||||
- 顶栏右侧已经有 Public、收藏、历史、AI、搜索、更多,但与 Wolai owner 态的 icon 序列和视觉轻重不同。
|
||||
- published 视角与 owner 视角没有明确组件分型。
|
||||
- breadcrumb 与页面内容标题之间的垂直节奏还需要更接近 Wolai。
|
||||
|
||||
### 4.4 主文档画布
|
||||
|
||||
Wolai 目标特征:
|
||||
|
||||
- 主内容列在大屏中不贴左,普通文档正文列宽约 `760px`。
|
||||
- 登录态 `Hermes` 页面首屏标题大约从 y=118px 开始。
|
||||
- 标题为 `34px / 44px / 600`,颜色接近 `rgb(30,30,30)`。
|
||||
- 标题下方正文块紧跟,不默认显示额外元信息行和横向分割线。
|
||||
- 普通正文块使用 `16px / 24px`。
|
||||
- 任务块 checkbox 轻边框,小尺寸,和文本基线对齐。
|
||||
- 空白区域非常克制,不用额外卡片、阴影或说明文案。
|
||||
|
||||
mnote 当前差距:
|
||||
|
||||
- 当前本地文档页标题偏靠左且偏低,内容列与真实 Wolai 相比没有稳定对齐。
|
||||
- 当前显示“个人空间 / 工作区首页”元信息和横向分割线,这不是 Wolai 普通文档页默认观感。
|
||||
- 当前正文首块是“打开当前页面”链接,和目标 `Hermes` 的普通块 / 任务块 / 段落展示差异较大。
|
||||
- 右下角绿色 AI 浮动按钮比 Wolai 更重,真实 Wolai 是更轻的 AI 方形入口和问号入口。
|
||||
|
||||
### 4.5 主编辑器块级体验
|
||||
|
||||
Wolai / Notion-like 目标:
|
||||
|
||||
- slash 是主编辑入口之一,菜单跟随 caret。
|
||||
- 选中文本才出现浮动工具条。
|
||||
- hover 块时只露出左侧手柄,不自动弹菜单。
|
||||
- 点击左侧手柄才出现块菜单。
|
||||
- 块菜单核心动作是 `turn into / duplicate / delete / drag`。
|
||||
- `turn into` 同时存在于文本工具条语境和块菜单语境,但命令上下文不同。
|
||||
- 块 identity 由 Rust `EditorBlock.block_id` 持有;Tiptap `UniqueID` 只作为浏览器 runtime 辅助。
|
||||
- 页面引用、块引用、嵌入默认位置最终必须回到 Page Aggregate / Rust artifact 边界。
|
||||
|
||||
#### 4.5.1 hover 上 / 下插入块
|
||||
|
||||
用户补充的登录态截图显示,Wolai 的块 hover 体验还有一个非常关键的细节:当鼠标停在块左侧手柄区域时,当前块行会出现很轻的淡红背景,手柄上方和下方分别出现一条短横线;继续 hover 短横线时,短横线变成 `+`,并显示黑色 tooltip:
|
||||
|
||||
- `在上方插入块`,快捷键提示为 `Esc, a`。
|
||||
- `在下方插入块`,快捷键提示为 `Esc, b`。
|
||||
|
||||
这个交互不是装饰性按钮,而是 Wolai 块编辑器的“块级插入光标”。它要和块选中、拖拽手柄、块菜单、slash 菜单、快捷键一起设计,不能只在正文左侧放一个常驻加号。
|
||||
|
||||
当前仓库已有可复用参考:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx` 中,旧 `BlockNoteView` 通过 `SideMenuController` 注入自定义 `CustomSideMenu`。
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/menus/CustomSideMenu.tsx` 中,`WolaiDragHandleWithInsert` 已实现上方插入、下方插入、手柄菜单、菜单冻结、空段落加号打开 slash、截图失焦状态重置。
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-inline-editor-host.tsx` 当前已有较简化的块手柄和菜单,但没有复刻上 / 下短横线插入态。
|
||||
- `/mnt/Data1T/mnote/rust/spikes/leptos-tiptap-spike/src/lib.rs` 已有 leptos/tiptap 手柄雏形,但当前样式偏重,并且 `insert_top_level_paragraph_after_html` 只支持 after 插入。
|
||||
|
||||
旧 BlockNote 实现中最值得迁移的机制:
|
||||
|
||||
- 插入控件不是放在块容器上下边界,而是围绕手柄中心点计算位置:上方插入、手柄、下方插入三者垂直对齐。
|
||||
- 非 hover 状态显示 `12px` 左右短横线,hover 状态切换为 `Plus` 图标。
|
||||
- 插入按钮尺寸约 `16px`,手柄按钮约 `22px`,按钮之间保持小间距,避免插入按钮和拖拽手柄互相遮挡 pointer event。
|
||||
- `hoverArea` 要比按钮视觉范围更大,避免鼠标从手柄移到插入按钮时控件闪烁。
|
||||
- 手柄菜单打开时需要冻结 hover 状态,否则菜单会因鼠标离开块行而消失。
|
||||
- 空段落的中心 `+` 点击应等同打开 slash 菜单,而不是直接创建第二个空段落。
|
||||
- 截图、窗口失焦、页面隐藏时要重置 hover / frozen 状态,避免手柄卡住或消失。
|
||||
- 思维导图、在线表格、附件这类高块或内部接管鼠标事件的块,需要允许常驻或更稳定的插入控件显示策略。
|
||||
|
||||
迁移到 `leptos-tiptap` 时,目标行为如下:
|
||||
|
||||
- hover 普通块:左侧只显示三段控件,上短横线 / 拖拽手柄 / 下短横线;不自动打开块菜单。
|
||||
- hover 上短横线:短横线变 `+`,tooltip 显示 `在上方插入块` 和 `Esc, a`。
|
||||
- hover 下短横线:短横线变 `+`,tooltip 显示 `在下方插入块` 和 `Esc, b`。
|
||||
- 点击上方 `+`:在当前块前插入空段落,焦点进入新段落。
|
||||
- 点击下方 `+`:在当前块后插入空段落,焦点进入新段落。
|
||||
- 按 `Esc` 进入块选择 / 块聚焦状态后,按 `a` 执行 before 插入,按 `b` 执行 after 插入。
|
||||
- 点击拖拽手柄才打开块菜单;拖拽手柄本身不和上 / 下插入按钮抢 hover。
|
||||
|
||||
实现边界:
|
||||
|
||||
- `leptos-tiptap` 需要补齐 before / after 两个编辑器命令;不能继续只有 after HTML 插入。
|
||||
- 第一阶段可以先在 runtime 内插入空段落并保存正文,但正式命令口径应回到 Page Aggregate / Rust editor block model,例如 `page.body.insertBlockBefore` / `page.body.insertBlockAfter` 或等价 `EditorCommand::InsertBlock { position: Before | After }`。
|
||||
- UI 层可以持有当前 hover block、active block、tooltip、menu open 这些临时状态,但不能把插入后的块 id 只存在前端临时结构里。
|
||||
- 视觉上应优先复刻 Wolai:轻灰短横线、浅灰 hover 背景、黑色小 tooltip、淡红块 hover 底色;不要沿用 spike 中 32px、大圆角、重阴影、绿色 accent 的手柄样式。
|
||||
|
||||
首轮不追求:
|
||||
|
||||
- 完整协作光标。
|
||||
- 完整评论系统。
|
||||
- 完整表格编辑体验。
|
||||
- 完整图片 / 附件上传链。
|
||||
- 完整权限和发布链路。
|
||||
- 完整全局搜索索引。
|
||||
|
||||
### 4.6 操作态菜单与浮层
|
||||
|
||||
真实 Wolai 的体验不能只看静态首屏,必须把操作后出现的浮层也纳入复刻合同。
|
||||
|
||||
#### 4.6.1 页面树 owner 态菜单
|
||||
|
||||
用户提供的登录态截图显示,页面树当前页 `Hermes` 的管理菜单为白色浮层,宽约 220px,圆角和阴影都很轻,菜单项按语义分组:
|
||||
|
||||
- 打开位置:`在右侧边栏打开`,带快捷键提示。
|
||||
- 页面组织:`移动到...`、`嵌入到...`。
|
||||
- 引用复制:`复制访问链接`、`复制页面引用链接`、`复制页面ID`。
|
||||
- 页面副本:`拷贝副本`。
|
||||
- 危险或编辑动作:`重命名`、`删除`。
|
||||
|
||||
这说明页面树行不只是导航节点,还承载页面操作入口。mnote 复刻时必须把这些动作映射到正式命令:
|
||||
|
||||
- `在右侧边栏打开`:UI state + route / side panel state。
|
||||
- `移动到...`:`tree.subtree.move`。
|
||||
- `嵌入到...`:`tree.node.embed` 或 Page Aggregate embed plan。
|
||||
- `复制页面引用链接`:生成 page reference artifact,不应只复制 URL。
|
||||
- `重命名`:`page.head.updateTitle`,并同步 tree projection。
|
||||
- `删除`:必须走归档 / 删除确认链,不能前端直接移除节点。
|
||||
|
||||
首轮可以先复刻菜单壳、分组、hover、禁用态和快捷键提示;真实移动、嵌入、删除等高风险动作按后续命令合同逐步接入。
|
||||
|
||||
#### 4.6.2 搜索弹层
|
||||
|
||||
公开页搜索和帮助中心搜索都使用居中 modal overlay:
|
||||
|
||||
- 背景整体变暗,侧栏和正文保持原位但失焦。
|
||||
- 搜索框宽约 680px,高约 56px,白底,圆角轻,阴影明显但不厚重。
|
||||
- 输入后出现选项行:`仅匹配标题`、`精确匹配`、`页面内搜索`。
|
||||
- 结果区显示匹配数量和快捷键说明:`Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开`。
|
||||
- 结果行左侧是页面 / 块图标,中间是标题与命中摘要,命中词使用红色高亮,右侧显示所在页面或空间。
|
||||
|
||||
mnote 需要把搜索拆成两层:
|
||||
|
||||
- 左侧栏 `快速筛选页面`:只过滤当前页面树,结果直接替换树列表。
|
||||
- 顶栏搜索 modal:跨页面 / 页面内搜索入口,支持结果列表、快捷键提示、打开方式提示。
|
||||
|
||||
首轮可以用本地 projection / 当前页面块快照生成结果,不必先接全局索引;但视觉结构、选项行和打开方式提示应先复刻。
|
||||
|
||||
#### 4.6.3 published 态操作
|
||||
|
||||
真实 published 态存在几类弱操作入口:
|
||||
|
||||
- `演示模式`:点击后隐藏侧栏和顶栏,画布变成大字号演示页,只保留主题切换入口。
|
||||
- `开始编辑`:未登录时弹出“登录以编辑”确认框,红色主按钮为继续,取消为弱按钮。
|
||||
- `评论`:右侧滑出评论面板,顶部有 `未解决(0)` tab、筛选下拉、关闭按钮,空态居中。
|
||||
- `Good Night`:右下角主题按钮直接切换暗色主题,暗色主题不重排页面,只替换 token。
|
||||
|
||||
mnote 首轮不必做完整发布权限,但应该保留这套状态分型:
|
||||
|
||||
- owner 编辑态。
|
||||
- published 阅读态。
|
||||
- presentation 演示态。
|
||||
- comment side panel。
|
||||
- light / dark token 切换。
|
||||
|
||||
这些状态大多是 UI state,但 `开始编辑` 是否可用、评论权限、发布状态必须来自页面权限或发布 projection,不应在前端硬猜。
|
||||
|
||||
### 4.7 帮助中心页的内在逻辑
|
||||
|
||||
`https://www.wolai.com/wolai/xhqeop8UHpVTMUSVmgz8nq` 不是普通内容页,它展示了 Wolai 页面系统的内在组织方式。
|
||||
|
||||
观察结论:
|
||||
|
||||
- 左侧页面树是帮助主题的完整目录,根页面下挂大量子页面,既是导航也是信息架构。
|
||||
- 正文入口页不是自由排版海报,而是由页面链接、分组标题、四列目录块、子页面引用组成的“文档门户”。
|
||||
- `快速上手 / 常见问题 / 视频教程 / 使用技巧短视频` 是高频入口。
|
||||
- “让我们先掌握一些基础”下的四列目录把页面按能力分组:基础操作、基础块类型、进阶块类型、媒体与文件。
|
||||
- 目录项本质上是页面引用 / 页面链接,不是普通文本。
|
||||
- 搜索可以跨整个帮助中心返回页面和块级结果,命中词红色高亮。
|
||||
- 搜索结果可以按快捷键在新窗口或右侧边栏打开,说明“右侧边栏打开”是 Wolai 页面导航模型的一等入口。
|
||||
|
||||
更重要的是,帮助中心中的“块引用”页直接定义了编辑器的语义模型:
|
||||
|
||||
- 行内块引用:在文字中间引用另一个块,底部有圆点虚线,不能直接编辑,点击跳转原始出处。
|
||||
- 嵌入块引用:以独立块引用另一个块,左侧有圆点虚线,可以直接查看或部分编辑,但不能删除被引用块本身。
|
||||
- 页面引用:当对象是页面时,菜单从“复制块引用链接”变为“复制页面引用链接”。
|
||||
- 行内页面引用:在文本内显示页面标题。
|
||||
- 嵌入页面引用:独立引用块显示页面图标和标题。
|
||||
- 复制粘贴路径:块菜单复制引用链接,再粘贴生成引用。
|
||||
- 快捷键路径:`esc` 选中块,`H` 复制行内块引用链接,`Q` 复制嵌入块引用链接。
|
||||
- 嵌入到路径:`cmd/alt + shift + G` 打开“嵌入到”弹窗,搜索目标页面,把块嵌入到目标页面。
|
||||
- 当前页面位置快速引用:编辑时输入 `[[` 搜索页面,添加页面行内引用。
|
||||
- 右侧边栏拖拽引用:从右侧边栏拖拽产生引用。
|
||||
- 引用预览和别名:悬浮行内块引用出现预览,小窗顶部可以设置别名,别名末尾显示箭头。
|
||||
|
||||
因此,mnote 不应把“引用”当成一个简单链接样式。它至少需要在 Page Aggregate / editor block model 中区分:
|
||||
|
||||
- `inline_page_reference`
|
||||
- `inline_block_reference`
|
||||
- `embedded_page_reference`
|
||||
- `embedded_block_reference`
|
||||
- `reference_alias`
|
||||
- `reference_preview`
|
||||
- `reference_source_block_id`
|
||||
- `reference_target_page_id`
|
||||
|
||||
首轮实现可以降级,但语义模型不能降级成普通 URL。
|
||||
|
||||
---
|
||||
|
||||
## 5. 复刻范围分级
|
||||
|
||||
### 5.1 P0:先做像素基线
|
||||
|
||||
P0 只做最影响首屏观感的部分:
|
||||
|
||||
- 全局字体栈与基础字号行高收口。
|
||||
- Sidebar 宽度、背景、顶部栏、树行高度、选中态、hover 态。
|
||||
- 顶栏高度、breadcrumb、右侧动作按钮轻量化。
|
||||
- 文档标题位置、字号、行高、正文列宽。
|
||||
- 默认隐藏普通文档页中的额外元信息行和分割线。
|
||||
- 右下角浮动入口减重。
|
||||
- 在 `1392 x 1213` 视口下对齐真实 Wolai 截图。
|
||||
|
||||
### 5.2 P1:补齐高频交互体验
|
||||
|
||||
P1 做用户会立刻感知的交互:
|
||||
|
||||
- 页面树展开 / 折叠动画和 hover action。
|
||||
- 当前页祖先链展开和选中态保持。
|
||||
- 搜索框视觉与本地过滤体验。
|
||||
- 树行上下文菜单的视觉壳和核心动作入口。
|
||||
- 顶栏搜索 modal 的选项行、结果行、命中高亮和快捷键提示。
|
||||
- 评论侧栏、登录编辑确认框、演示模式的基础状态切换。
|
||||
- 文档页进入编辑后,slash、浮动工具条、左侧手柄、上 / 下插入块、块菜单的基础可用体验。
|
||||
- 页面标题编辑后,标题 / breadcrumb / sidebar / page tree / file tree 保持一致。
|
||||
|
||||
### 5.3 P2:接近 Wolai 的产品完整感
|
||||
|
||||
P2 做复杂但可逐步推进的部分:
|
||||
|
||||
- 拖拽排序与 drop feedback。
|
||||
- page reference / block reference 的正式插入体验。
|
||||
- `[[` 页面引用搜索、`嵌入到...` 弹窗、右侧边栏打开 / 拖拽引用。
|
||||
- 引用预览和别名。
|
||||
- 右侧评论、历史、关系图入口的真实面板。
|
||||
- published / owner / edit 三态顶栏完整切换。
|
||||
- 页面设置项与 editor runtime 的完整联动。
|
||||
- 帮助中心类多列目录块的静态展示和后续编辑支持。
|
||||
|
||||
### 5.4 明确降级项
|
||||
|
||||
以下功能可以先只做视觉入口或最小实现:
|
||||
|
||||
- 评论协作。
|
||||
- 权限分享。
|
||||
- 发布与侵权投诉链路。
|
||||
- 全局搜索索引。
|
||||
- 完整演示模式。
|
||||
- 完整夜间主题。
|
||||
- 完整表格、图表、数据库。
|
||||
- 完整文件上传和媒体块管理。
|
||||
- 完整引用别名编辑器和跨页面引用预览缓存。
|
||||
|
||||
---
|
||||
|
||||
## 6. 单一真源边界
|
||||
|
||||
### 6.1 可以留在前端展示层的内容
|
||||
|
||||
以下内容允许作为前端展示或临时 UI state:
|
||||
|
||||
- 色彩、字号、行高、间距、圆角、阴影。
|
||||
- Sidebar 宽度与布局。
|
||||
- 树行 hover、focus、临时 selection、drag preview。
|
||||
- 顶栏按钮排列与 hover。
|
||||
- 文档画布宽度、标题留白、正文显示密度。
|
||||
- slash 菜单开合状态。
|
||||
- 浮动工具条开合状态。
|
||||
- 块手柄 hover 状态。
|
||||
- 右下角浮动入口显示状态。
|
||||
|
||||
### 6.2 必须回到 Rust / projection / Page Aggregate 的内容
|
||||
|
||||
以下内容不能只在前端复刻:
|
||||
|
||||
- 页面树真实层级。
|
||||
- 排序真相。
|
||||
- 当前页 active path。
|
||||
- 拖拽是否合法。
|
||||
- 新建、重命名、移动、归档、恢复结果。
|
||||
- 页面标题与 breadcrumb / sidebar / file tree 的一致性。
|
||||
- 正文保存与 conflict key。
|
||||
- 页面设置对 editor runtime 的正式语义。
|
||||
- page reference / block reference / embed 默认位置。
|
||||
- 引用类型、引用源、引用目标、引用别名、引用预览数据。
|
||||
- AI 写入标题、正文、页面设置的语义边界。
|
||||
|
||||
原则:
|
||||
|
||||
> **UI 可以复刻 Wolai,事实源不能复刻成第二份前端状态。**
|
||||
|
||||
### 6.3 命令口径
|
||||
|
||||
页面树动作继续沿 `tree.*`:
|
||||
|
||||
- `tree.node.create`
|
||||
- `tree.node.rename`
|
||||
- `tree.subtree.move`
|
||||
- `tree.node.archive`
|
||||
- `tree.node.restore`
|
||||
- `tree.node.embed`
|
||||
|
||||
页面动作继续沿 `page.*`:
|
||||
|
||||
- `page.head.updateTitle`
|
||||
- `page.layout.updateOptions`
|
||||
- `page.body.save`
|
||||
|
||||
兼容 `documents.*` 可以继续存在,但不应作为新增体验的正式命令面。
|
||||
|
||||
---
|
||||
|
||||
## 7. 推荐实施路线
|
||||
|
||||
### Phase A:建立 Wolai 视觉 token
|
||||
|
||||
目标:
|
||||
|
||||
> **先让字体、字号、行高、颜色和基础密度可被统一复用。**
|
||||
|
||||
主要文件:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/globals.css`
|
||||
|
||||
工作内容:
|
||||
|
||||
- 统一全局字体栈,避免 `Inter` 覆盖中文系统字体。
|
||||
- 建立 Wolai-like token:`--wolai-sidebar-bg`、`--wolai-active-fg`、`--wolai-active-bg`、`--wolai-text-primary`、`--wolai-text-secondary`。
|
||||
- 固定标题、正文、sidebar、topbar 的字号 / 行高。
|
||||
- 用 token 替代散落硬编码颜色。
|
||||
|
||||
验收:
|
||||
|
||||
- `local-3000` 截图中标题、树行和顶栏文字宽度明显接近真实 Wolai。
|
||||
- 中文字体不再出现 Inter 优先导致的字宽偏差。
|
||||
|
||||
### Phase B:复刻 Sidebar / 页面树首屏
|
||||
|
||||
目标:
|
||||
|
||||
> **让左侧页面树在密度、选中态、hover、滚动条、底部入口上接近 Wolai。**
|
||||
|
||||
主要文件:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/tree-shell-surface.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/tree-shell-host.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/tree-shell-dom-host.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/tree-projection.ts`
|
||||
|
||||
工作内容:
|
||||
|
||||
- 固定侧栏宽度和背景。
|
||||
- 调整 workspace header 高度、头像尺寸、空间名截断方式。
|
||||
- 调整快捷图标行尺寸与 hover。
|
||||
- 调整页面树行高到 32px 附近。
|
||||
- 当前页改为 Wolai 红字 + 淡红底。
|
||||
- hover 态与 active 态同色系但层级更轻。
|
||||
- 缩进只由层级和 icon slot 决定。
|
||||
- 底部垃圾桶 / 模板中心与 Wolai 对齐,同时保留 mnote 自有入口时不破坏密度。
|
||||
- `Explorer` 作为 mnote 创新保留,但视觉上降噪,避免抢过“我的页面”主路径。
|
||||
|
||||
验收:
|
||||
|
||||
- 对照用户提供的登录态截图,`Hermes` 选中行的颜色、行高、左侧缩进、滚动条位置接近 Wolai。
|
||||
- 页面树长列表滚动时不出现行高抖动。
|
||||
- 文件树创新入口仍可访问。
|
||||
|
||||
### Phase C:复刻顶栏与 breadcrumb
|
||||
|
||||
目标:
|
||||
|
||||
> **让顶栏在 owner / published 两种状态下都接近 Wolai 的轻量工具条。**
|
||||
|
||||
主要文件:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/app-layout-shell.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
|
||||
|
||||
工作内容:
|
||||
|
||||
- 固定顶栏高度约 40px。
|
||||
- breadcrumb 文本、图标、分隔符弱化。
|
||||
- 右侧按钮统一使用轻量 icon/text button。
|
||||
- 区分 owner 态和 published 态的按钮组合。
|
||||
- 公开状态 pill 对齐 Wolai 的“全网公开 / Public”视觉。
|
||||
- 顶栏 hover 只加浅灰底,不使用重边框或大面积按钮。
|
||||
|
||||
验收:
|
||||
|
||||
- 登录态参考图中的顶栏图标序列能在 mnote 中找到对应视觉位置。
|
||||
- published 公共页参考图中的“演示模式 / 搜索 / 开始编辑 / 评论 / 复制 / 开始使用 wolai”可先作为视觉分型,不要求一次接齐功能。
|
||||
|
||||
### Phase D:复刻文档画布与阅读态
|
||||
|
||||
目标:
|
||||
|
||||
> **让普通文档页首屏像 Wolai,而不是像调试页或二级详情页。**
|
||||
|
||||
主要文件:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-read-view.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-option-semantics.ts`
|
||||
|
||||
工作内容:
|
||||
|
||||
- 标题使用 `34px / 44px / 600`。
|
||||
- 普通正文列宽约 760px。
|
||||
- 调整首屏 top padding,使标题起点接近 Wolai。
|
||||
- 默认隐藏普通文档页的“个人空间 / 工作区首页”元信息行和横向分割线。
|
||||
- 保留页面设置控制,但把“显示元信息 / 显示结构”这类调试或增强项作为显式选项,不默认露出。
|
||||
- 阅读态任务块、段落、标题、列表、引用、代码块按 Wolai 行高和颜色重调。
|
||||
- 右下角 AI / 帮助入口减重,避免绿色大按钮抢主编辑区视觉。
|
||||
|
||||
验收:
|
||||
|
||||
- `Hermes` 类普通文档页首屏只突出标题和正文块。
|
||||
- 页面不再默认出现明显不像 Wolai 的横线、meta row 或 debug outline。
|
||||
- 阅读态和编辑态在基础排版上不出现明显跳动。
|
||||
|
||||
### Phase E:复刻主编辑器高频块交互
|
||||
|
||||
目标:
|
||||
|
||||
> **让 `leptos-tiptap` 的输入体验从“能编辑”推进到“像 Wolai 的块编辑器”。**
|
||||
|
||||
主要文件:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-island-editor-host.tsx`
|
||||
- `/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs`
|
||||
- `/mnt/Data1T/mnote/rust/crates/bridge-runtime/`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/`
|
||||
|
||||
工作内容:
|
||||
|
||||
- slash 菜单锚定 caret。
|
||||
- selection 存在时显示浮动工具条。
|
||||
- hover 块只显示左侧手柄区,上方和下方短横线在 hover 时变成插入 `+`。
|
||||
- 点击上方 / 下方插入控件分别创建 before / after 空段落,并把焦点移动到新段落。
|
||||
- 支持 `Esc, a` 和 `Esc, b` 的块级插入快捷键。
|
||||
- 点击手柄打开块菜单。
|
||||
- 块菜单至少提供 `turn into / duplicate / delete` 的视觉与基础命令入口。
|
||||
- `turn into` 不直接绕过 Rust block model。
|
||||
- Tiptap `UniqueID` 只用于 runtime 辅助,最终块 id 对齐 Rust `EditorBlock.block_id`。
|
||||
- 页面引用 / 块引用可先做最小插入体验,复杂搜索和预览后置。
|
||||
|
||||
验收:
|
||||
|
||||
- 用户能在主编辑区通过 slash 插入常见块。
|
||||
- 用户选中文本时看到 Wolai-like 浮动工具条。
|
||||
- 用户 hover 块时不会被重工具条打扰,并能看到 Wolai-like 上 / 下插入短横线。
|
||||
- 用户可以通过鼠标或 `Esc, a` / `Esc, b` 在当前块前后插入空段落。
|
||||
- 块操作不会制造前端临时 id 作为持久化真相。
|
||||
|
||||
### Phase F:用 Page Aggregate 守住一致性
|
||||
|
||||
目标:
|
||||
|
||||
> **复刻体验时不牺牲单一真源。**
|
||||
|
||||
主要文件:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-aggregate-loader.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-aggregate-builder.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-command-client.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/page/route.ts`
|
||||
- `/mnt/Data1T/mnote/rust/crates/core-protocol/`
|
||||
- `/mnt/Data1T/mnote/rust/crates/bridge-runtime/`
|
||||
- `/mnt/Data1T/mnote/rust/crates/mnote-web/`
|
||||
|
||||
工作内容:
|
||||
|
||||
- 新增视觉状态时,先判断它是否只是 UI state。
|
||||
- 新增页面字段时,必须归入 `page_identity / page_head / page_layout / page_body / page_tree`。
|
||||
- 标题变更必须继续让页头、breadcrumb、sidebar、page tree、file tree 同步。
|
||||
- 正文保存继续走 `page.body.save`。
|
||||
- 页面设置继续按 `page-option-semantics` 判断是否进入 island runtime。
|
||||
- 页面树动作继续走 `tree.*`,不新增 `documents.*` 长期动作。
|
||||
|
||||
验收:
|
||||
|
||||
- 页面重命名后,所有消费者保持一致。
|
||||
- 切页、刷新、实时更新不会把 UI 压回旧标题。
|
||||
- 视觉复刻代码里没有新增第二套树排序或页面标题真相。
|
||||
|
||||
### Phase G:截图回归与像素验收
|
||||
|
||||
目标:
|
||||
|
||||
> **把“像 Wolai”变成可复查、可操作、可重复的截图基线。**
|
||||
|
||||
建议新增或复用:
|
||||
|
||||
- `/mnt/Data1T/mnote/scripts/task*-smoke.js`
|
||||
- `/mnt/Data1T/mnote/tmp/wolai-compare/`
|
||||
|
||||
验收视口:
|
||||
|
||||
- 桌面:`1392 x 1213`
|
||||
- 宽屏:`1440 x 900`
|
||||
- 窄屏:`390 x 844`
|
||||
|
||||
截图基线:
|
||||
|
||||
- 真实 Wolai 登录态长页面树。
|
||||
- 真实 Wolai `Hermes` 普通文档页。
|
||||
- 当前 mnote 同类页面。
|
||||
- mnote 文件树创新入口展开态。
|
||||
|
||||
验收方式:
|
||||
|
||||
- 每次验收必须按 `wolai-aline` skill 形成差异矩阵,并附 Wolai 截图、本地截图、动作链、DOM/ARIA 线索和剩余差异。
|
||||
- 浏览器取证必须由 subagent 执行;主线程必须复核截图,不能只接受 subagent 文本结论。
|
||||
- published 阅读态、owner 登录态、editable-test mode 都只表示取证上下文,不再作为三套独立测试方案维护。
|
||||
- 先用 Wolai 基线写出本地 RED smoke,再实现,再跑本地 smoke,再派 subagent 复测。
|
||||
- 若截图肉眼可见差异但 smoke 未捕获,先增强 smoke 或 checklist 断言,再继续实现。
|
||||
- 后续可加入截图 diff,但不能用截图 diff 替代真实交互烟测。
|
||||
- 每次改 Sidebar、topbar、document canvas、editor runtime 后都要重新截首屏,并同步更新连续 checklist 的状态。
|
||||
- 连续执行清单见 `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md`。
|
||||
|
||||
owner 态交互验收清单:
|
||||
|
||||
- 自动化打开 `https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd` 后确认进入登录 owner 态,而不是 published 态。
|
||||
- hover 页面树当前项 `Hermes`,确认 hover action 可见。
|
||||
- 打开页面树菜单,截图菜单项和分组。
|
||||
- hover 正文块 `事实上`,确认块背景、左侧手柄、上短横线、下短横线可见。
|
||||
- hover 上短横线,确认 tooltip 为 `在上方插入块` / `Esc, a`。
|
||||
- hover 下短横线,确认 tooltip 为 `在下方插入块` / `Esc, b`。
|
||||
- 打开块菜单,截图 `AI 助理 / 转换为 / 拷贝副本 / 删除 / 复制链接 / 移动/嵌入到... / 块历史... / 评论 / 颜色 / 文字居中 / 文字翻译 / 生成海报` 等入口。
|
||||
- 在 `3000` 执行同类操作并保存同视口截图。
|
||||
- 验收报告中列出 Wolai 截图、本地截图、差异结论和不可测项。
|
||||
|
||||
---
|
||||
|
||||
## 8. 验证命令建议
|
||||
|
||||
前端运行:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/wolai-frontend && pnpm dev
|
||||
```
|
||||
|
||||
前端相关测试:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/components/sidebar/tree-shell-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx src/components/editor/document-content.test.ts src/components/editor/leptos-tiptap-island-editor-host.test.tsx
|
||||
```
|
||||
|
||||
projection / aggregate 测试:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/wolai-frontend && pnpm test src/lib/tree-projection.test.ts src/lib/tree-projection-contract.test.ts src/lib/documents/page-aggregate-builder.test.ts src/lib/documents/tree-command-client.test.ts
|
||||
```
|
||||
|
||||
Rust 侧测试:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote && cargo test -p mnote-web workspace_shell
|
||||
cd /mnt/Data1T/mnote && cargo test -p bridge-runtime
|
||||
```
|
||||
|
||||
浏览器验收:
|
||||
|
||||
- 先按 `wolai-aline` skill 派 subagent 打开真实 Wolai 参考页和 `http://127.0.0.1:3000/`。
|
||||
- 统一视口至少覆盖 `1392 x 1213`;涉及响应式时补 `1440 x 900` 和 `390 x 844`。
|
||||
- 每个小任务都要保存 Wolai 与本地截图,并记录入口、关闭路径、URL 变化、控件形态、控件状态、快捷键、hover/active、DOM/ARIA 线索。
|
||||
- 主线程复核截图后,把结论写入差异矩阵;发现差异先补 smoke,再改实现。
|
||||
- 对照范围至少包含左侧栏宽度、树行高度、选中态、正文标题位置、顶栏按钮密度、搜索 modal、块 hover/插入入口、右下角浮动入口。
|
||||
|
||||
---
|
||||
|
||||
## 9. 禁止事项
|
||||
|
||||
为避免这条线跑偏,后续实现中禁止:
|
||||
|
||||
- 不把 `BlockNote` 写回默认主编辑器。
|
||||
- 不把 Next App Router 恢复成 `3000` 主入口。
|
||||
- 不在 UI 层重新拼第二份页面树、排序、标题或正文真相。
|
||||
- 不把 compat route 或 debug shell 当正式主链。
|
||||
- 不为了像素复刻绕开 `tree.*` / `page.*` 命令族。
|
||||
- 不把 Tiptap runtime 临时 id 倒灌成 Rust 持久化块 id。
|
||||
- 不把页面设置做成“保存了字段但 editor runtime 不消费”的假功能。
|
||||
- 不为了复刻 Wolai 顶栏而提前接入真实权限 / 分享 / 评论复杂链路。
|
||||
|
||||
---
|
||||
|
||||
## 10. 当前退出标准
|
||||
|
||||
这份方案只有在下面条件同时满足后,才能移入 `done`:
|
||||
|
||||
- `3000` 首屏在左侧页面树、顶栏、文档画布三块视觉上接近真实 Wolai。
|
||||
- 页面树保留 mnote 文件树创新,但不破坏 Wolai 主路径密度。
|
||||
- 普通文档页默认不再显得像调试页或二级详情页。
|
||||
- `leptos-tiptap` 主编辑器具备 slash、浮动工具条、左侧手柄、上 / 下插入块、块菜单的基础 Wolai-like 行为。
|
||||
- 标题 / 正文 / 页面设置 / 页面树仍服从 Page Aggregate 与 tree-first graph kernel 主线。
|
||||
- 相关 smoke / 单测 / 截图验收有记录。
|
||||
- 连续 checklist 中 P0/P1 任务均有 Wolai 基线、本地 RED/GREEN smoke、subagent 复测截图和主线程复核结论。
|
||||
|
||||
当前结论:
|
||||
|
||||
> **下一步应先做 P0 像素基线和 Sidebar / 文档画布首屏对齐,再推进编辑器块级交互。功能复杂项可以降级,但页面树和主编辑器的视觉节奏必须优先向真实 Wolai 收口。**
|
||||
@@ -0,0 +1,41 @@
|
||||
# 5-8 [process] Wolai 编辑态自动化测试方案 v1(已收口)
|
||||
|
||||
> 更新时间:2026-04-30
|
||||
>
|
||||
> 状态:本文件原有的工具筛选、只读边界、沙盒页要求和脚本草案已经被统一的 `wolai-aline` skill 与 `08-wolai-aline-test-flow` 流程取代。后续不要再按本文旧版测试方案执行。
|
||||
|
||||
## 当前唯一口径
|
||||
|
||||
后续所有 Wolai 对标、复刻、aline/alignment、编辑器状态自动化测试任务,统一使用:
|
||||
|
||||
- Codex skill:`/home/lix/.codex/skills/wolai-aline`
|
||||
- 项目流程:`/mnt/Data1T/mnote/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md`
|
||||
- 项目规则:`/mnt/Data1T/mnote/AGENTS.md` 的 `Wolai-aline 对标流程`
|
||||
|
||||
## 取代的旧结论
|
||||
|
||||
以下旧结论不再作为执行依据:
|
||||
|
||||
- “Hermes 页面只能只读,写入必须另找沙盒页”。
|
||||
- “published 态脚本可以代表主编辑器完成验收”。
|
||||
- “subagent 文本结论或文案断言足以证明对齐”。
|
||||
- “先实现再由用户肉眼指出差异”。
|
||||
- 本文旧版 `wolai-reference-*` 脚本草案、分阶段 checklist、工具通道优先级。
|
||||
|
||||
## 现行规则摘要
|
||||
|
||||
- 当前 Hermes 测试页 `https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd` 已授权用于 Wolai-aline 编辑器对标中的最小编辑验证。
|
||||
- 编辑验证必须记录前后截图、动作链、输入内容和清理状态。
|
||||
- 即使在 Hermes 测试页,也禁止未经确认地删除、移动、归档、发布、评论、改权限或批量修改既有非测试内容。
|
||||
- 其他 Wolai 页面仍默认只读;写入仍需用户提供沙盒页 URL 和明确授权。
|
||||
- 浏览器对标测试必须使用 subagent 执行;主线程必须复核截图并形成差异矩阵。
|
||||
- 发现差异后先补本地 smoke,让差异可复现失败,再实现修复。
|
||||
|
||||
## 后续维护
|
||||
|
||||
若后续 Wolai-aline 任务发现新的稳定失败模式,应更新:
|
||||
|
||||
- `/home/lix/.codex/skills/wolai-aline/references/failure-patterns.md`
|
||||
- 必要时同步更新 `/mnt/Data1T/mnote/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md`
|
||||
|
||||
不要继续扩写本文作为新测试方案。
|
||||
@@ -0,0 +1,273 @@
|
||||
# 5-9 [process] Wolai-aline 连续执行 checklist v1
|
||||
|
||||
> 更新时间:2026-04-30
|
||||
>
|
||||
> 本清单拆自 `5-7-wolai-page-tree-main-editor-experience-restoration-v1.md`。它不是新的测试方案;执行口径统一服从 `/home/lix/.codex/skills/wolai-aline` 与 `/mnt/Data1T/mnote/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md`。
|
||||
|
||||
## 1. 使用方式
|
||||
|
||||
每次只领取一个最小可对标任务,按下面固定闭环推进:
|
||||
|
||||
1. 用一句话定义行为,例如“侧栏搜索入口打开全局搜索 modal 且 URL 不变”。
|
||||
2. 派 subagent 对 Wolai Hermes 取基线,必要时进入 Hermes editable-test mode。
|
||||
3. 主线程查看 Wolai 截图,填写差异矩阵,不接受只有文字的结论。
|
||||
4. 写或更新本地 `scripts/task*-smoke.js`,先让当前 `3000` 暴露差异。
|
||||
5. 小范围实现,不新增第二套页面树、标题、正文或排序真相。
|
||||
6. 运行本地 smoke 和改动范围内单测。
|
||||
7. 再派 subagent 同链路复测 Wolai 与 `3000`。
|
||||
8. 主线程复核最终截图;若肉眼仍有差异,先补 smoke 或 checklist 断言,再继续修。
|
||||
9. 更新本清单状态和截图路径。
|
||||
|
||||
状态标记:`TODO` 未开始,`BASELINE` 已取 Wolai 基线,`RED` 已有本地失败 smoke,`GREEN` 本地实现和 smoke 通过,`PARITY` subagent 复测与主线程截图复核通过,`BLOCKED` 有阻塞。
|
||||
|
||||
|
||||
## 2. 当前基线证据
|
||||
|
||||
2026-04-30 已由 subagent 使用 Playwright 同时操作 Wolai Hermes 与本地 `3000`,主线程已复核关键截图。后续任务可沿用这些基线,但每个具体小任务仍需重新截同动作链证据。
|
||||
|
||||
| 项 | Wolai 证据 | 本地证据 | 当前结论 |
|
||||
| --- | --- | --- | --- |
|
||||
| 首屏 | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/doc-baseline-mini/wolai-initial.png` | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/doc-baseline-mini/local-initial.png` | 本地首屏仍是聚合/预览壳,需点击 `打开当前页面` 才进入正文,和 Wolai 直接正文页不一致 |
|
||||
| 搜索 modal | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/wolai-03-search-modal-controls-filled.png` | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/local-03-search-modal-controls-filled.png` | 容器接近;结果态、数据、入口矩阵仍需拆开验;switch 状态必须截图 + `aria-checked` 双证据 |
|
||||
| 块 hover | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/wolai-08-block-hover-insert-entry.png` | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/local-14-block-hover-insert-entry.png` | 本地已有 `+` 与手柄入口,但位置、密度、页面状态不同,不可判定已对齐 |
|
||||
| 编辑清理 | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/wolai-14-title-suffix-after.png` | `/mnt/Data1T/mnote/tmp/wolai-editor-parity/baseline-20260430/local-16-editor-after-undo-cleanup.png` | Wolai 标题和正文都可编辑,editable-test 前必须先确认焦点 block 类型 |
|
||||
|
||||
当前必须优先拆解的差异:
|
||||
|
||||
1. 普通文档首屏入口语义:Wolai 直接显示正文页,本地仍需点击 `打开当前页面`。
|
||||
2. 搜索入口矩阵:Wolai 左侧搜索可打开,`Ctrl+K` 本轮未稳定;本地顶栏搜索可打开,侧栏入口和快捷键要继续按 smoke 验证。
|
||||
3. 搜索结果数据:Wolai 对 `Hermes` 有真实结果,本地依赖当前工作区示例数据;后续需要固定 fixture 或种子。
|
||||
4. 编辑器测试焦点:写入前必须确认当前焦点在正文测试块,不在标题或 breadcrumb 渲染副本。
|
||||
|
||||
|
||||
### 2.1 task129 Phase A 执行记录
|
||||
|
||||
2026-04-30 已新增并执行 `scripts/task129-wolai-aline-baseline-smoke.js`,用于固化 Phase A 的只读基线取证入口。该脚本不编辑 Wolai,只做双端首屏取证、DOM 摘要和差异矩阵输出。
|
||||
|
||||
主线程验证:
|
||||
|
||||
- 命令:`WOLAI_ALINE_RUN_ID=main-20260430-122309 node scripts/task129-wolai-aline-baseline-smoke.js`
|
||||
- 结果:通过,`ok: true`,确认 `simultaneous: true`
|
||||
- 矩阵:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/main-20260430-122309/comparison-matrix.md`
|
||||
- Wolai 截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/main-20260430-122309/wolai-1392x1213-first-screen.png`
|
||||
- 本地截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/main-20260430-122309/local-1392x1213-first-screen.png`
|
||||
|
||||
主线程多视口验证:
|
||||
|
||||
- 命令:`WOLAI_ALINE_RUN_ID=main-viewports-20260430-122418 WOLAI_ALINE_VIEWPORTS=1392x1213,1440x900,390x844 node scripts/task129-wolai-aline-baseline-smoke.js`
|
||||
- 结果:通过,生成 `1392x1213`、`1440x900`、`390x844` 三组 Wolai / 本地截图
|
||||
- 矩阵:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/main-viewports-20260430-122418/comparison-matrix.md`
|
||||
|
||||
subagent 复测:
|
||||
|
||||
- 命令:`WOLAI_ALINE_RUN_ID=subagent-20260430-122359 node scripts/task129-wolai-aline-baseline-smoke.js`
|
||||
- 结果:通过,`ok: true`,确认 `simultaneous: true`,本次不需要 `xvfb-run`
|
||||
- 矩阵:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/subagent-20260430-122359/comparison-matrix.md`
|
||||
- Wolai 截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/subagent-20260430-122359/wolai-1392x1213-first-screen.png`
|
||||
- 本地截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/subagent-20260430-122359/local-1392x1213-first-screen.png`
|
||||
|
||||
主线程截图复核结论:
|
||||
|
||||
- Wolai Hermes 为 owner 登录态,直接显示正文页和 `全网公开` 状态。
|
||||
- 本地 `3000` 工作区可见,但首屏仍停留在聚合 / 预览入口,需要点击 `打开当前页面` 才进入正文。
|
||||
- `task129` 只证明取证工具链可用,不证明 UI 已对齐;后续首个实现任务应优先处理 `D9 普通文档首屏入口语义`。
|
||||
|
||||
### 2.2 task130 D9 执行记录
|
||||
|
||||
2026-04-30 已新增并执行 `scripts/task130-wolai-aline-root-document-entry-smoke.js`,用于固化 `D9 普通文档首屏入口语义`。该脚本启动临时 `mnote-web` 端口验证新代码路径,不修改现有 `3000` 进程。
|
||||
|
||||
RED:
|
||||
|
||||
- 初次运行失败:`AssertionError: 根页不应停在需要点击“打开当前页面”的聚合入口`
|
||||
- 失败原因:Rust Web root route 在有 active page 时只渲染 workspace entry 和 `打开当前页面` 链接,没有内嵌 Page Aggregate、editor bootstrap 和 `leptos-tiptap` island。
|
||||
|
||||
GREEN:
|
||||
|
||||
- 命令:`node scripts/task130-wolai-aline-root-document-entry-smoke.js`
|
||||
- 结果:通过
|
||||
- 本地截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task130-root-document-entry/local-root-document-entry.png`
|
||||
- smoke 断言:根页不再出现 `打开当前页面`;存在 `document-shell[data-editor-host="leptos_tiptap_island"]`;存在 `__MNOTE_PAGE_AGGREGATE__`、`__MNOTE_EDITOR_BOOTSTRAP__`;存在 `.ProseMirror[contenteditable="true"]`。
|
||||
|
||||
实现摘要:
|
||||
|
||||
- root route 在 active page 可加载 Page Aggregate 时,直接复用 `DocumentPage`、Page Aggregate snapshot、editor bootstrap、title controller 和 island adapter。
|
||||
- 若 active page 不能加载 Page Aggregate,回退旧 workspace entry,避免 recent cookie 指向失效页面时 root 直接 400。
|
||||
- 现有 `3000` 进程仍是旧实例;需要重启 `mnote-web` 后,`3000` 才会体现该改动。
|
||||
|
||||
验证:
|
||||
|
||||
- `cargo fmt -p mnote-web --check`
|
||||
- `cargo check -p mnote-web`
|
||||
- `cargo test -p mnote-web root_entry -- --nocapture`
|
||||
- `node scripts/task130-wolai-aline-root-document-entry-smoke.js`
|
||||
|
||||
|
||||
最终对标复核:
|
||||
|
||||
- subagent 命令:`node scripts/task130-wolai-aline-root-document-entry-smoke.js`
|
||||
- 结果:通过
|
||||
- Wolai 基线:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task129-baseline/main-20260430-122309/wolai-1392x1213-first-screen.png`
|
||||
- 本地截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task130-root-document-entry/local-root-document-entry.png`
|
||||
- 结论:D9 目标行为一致;剩余侧栏数据、图标细节、具体字号属于后续 Sidebar / visual token checklist。
|
||||
## 3. 通用证据矩阵
|
||||
|
||||
每个 checklist 项都必须保留以下字段:
|
||||
|
||||
| 字段 | 要求 |
|
||||
| --- | --- |
|
||||
| Wolai 截图 | 绝对路径,保存在 `/mnt/Data1T/mnote/tmp/wolai-editor-parity/<task>/` |
|
||||
| 本地截图 | 同视口、同动作链截图 |
|
||||
| 入口 | 按钮、快捷键、菜单或 hover 区域 |
|
||||
| 关闭路径 | Esc、再次快捷键、遮罩、关闭按钮、入口 toggle |
|
||||
| URL | 操作前后是否变化 |
|
||||
| 控件类型 | switch、button、dropdown、menu、input、panel、modal |
|
||||
| 控件状态 | checked、selected、disabled、focused、hover、active |
|
||||
| 视觉形态 | 尺寸、位置、间距、阴影、颜色、图标、遮罩 |
|
||||
| DOM/ARIA | role、aria、data-testid、可聚焦性 |
|
||||
| smoke | 对应 `scripts/task*-smoke.js` 或测试文件 |
|
||||
| 剩余差异 | 不允许空泛写“基本一致” |
|
||||
|
||||
## 4. Phase A:基线和全局 token
|
||||
|
||||
| ID | 状态 | 任务 | 验收要点 |
|
||||
| --- | --- | --- | --- |
|
||||
| A1 | GREEN | 固定基线视口与截图目录 | `task129` 已支持 `WOLAI_ALINE_VIEWPORTS`;主线程已生成 `1392x1213`、`1440x900`、`390x844` 三组截图;subagent 已复测默认视口 |
|
||||
| A2 | PARITY | Wolai owner 首屏基线 | `task129` 主线程与 subagent 均确认 Wolai owner 登录态,截图见 2.1 执行记录 |
|
||||
| A3 | PARITY | 本地 `3000` 首屏基线 | `task129` 主线程与 subagent 均确认本地 3000 可打开,截图见 2.1 执行记录 |
|
||||
| A4 | TODO | 字体栈与基础 token | smoke 或 DOM 断言全局字体、标题字号、正文行高、sidebar 行高;截图确认中文字宽接近 Wolai |
|
||||
| A5 | PARITY | 截图差异表模板落地 | `task129` 自动输出 `comparison-matrix.md`,主线程已复核截图,subagent 已独立复测 |
|
||||
| A6 | PARITY | 同时操作工具链固化 | `task129` 固定 `Playwright + Chrome persistent profile`;主线程与 subagent 均确认可同时操作两个目标,本次 subagent 不需要 `xvfb-run` |
|
||||
|
||||
## 5. Phase 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`。
|
||||
@@ -0,0 +1,934 @@
|
||||
# 6 [process] Mindmap Kernel Phase 6:降级为 Projection / Editor v1
|
||||
|
||||
> 更新时间:2026-04-22
|
||||
>
|
||||
> 当前主线依据:
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
>
|
||||
> 历史参考:
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/ai-first-rust-block-editor-baseline-v1.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于重写 `Kernel Phase 6` 的落地口径。
|
||||
|
||||
这里仍沿用 `Phase 6` 命名,是为了保留与旧阶段拆分的一致性;当前执行依据应以 `design/01-05-current-priority-overview.md` 和相关现行主线文档为准,不再以 `1-1-tree-first-graph-kernel-checklist-v2.md` 作为唯一推进入口。
|
||||
|
||||
这里回答的不是:
|
||||
|
||||
- “要不要马上把导图 UI 全量重写成 Rust”
|
||||
|
||||
而是:
|
||||
|
||||
> **在当前仓库代码现实下,Mindmap 如何从独立对象中心,降级成 `tree-first graph kernel` 的一种 projection / editor。**
|
||||
|
||||
这份文档必须同时满足三件事:
|
||||
|
||||
- 服从 `tree-first graph kernel` 主线
|
||||
- 对齐当前仓库里的真实实现,而不是抽象设想
|
||||
- 给出可迁移、可双写、可分阶段切流的方案
|
||||
|
||||
---
|
||||
|
||||
## 2. 先给结论
|
||||
|
||||
结论固定如下:
|
||||
|
||||
> **Mindmap 后续必须进行 Rust 内核化重构,但重构重点不是先替换 `simple-mind-map` 画布,而是先把“导图真相、导图命令、导图 projection、AI 工具入口”切到 Rust kernel。**
|
||||
|
||||
换句话说:
|
||||
|
||||
- **必须 Rust 化的部分**
|
||||
- 导图事实源
|
||||
- 导图命令层
|
||||
- 导图 projection 层
|
||||
- AI / CLI 直接调用的导图工具层
|
||||
- **不必第一阶段 Rust 化的部分**
|
||||
- 具体画布渲染器
|
||||
- 工具栏、缩略图、拖拽动画、局部 DOM 细节
|
||||
|
||||
因此:
|
||||
|
||||
> **Phase 6 的正确目标不是“把导图换个前端库”,而是“让导图不再以 `simple-mind-map` JSON 为系统真相”。**
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前代码现实
|
||||
|
||||
当前导图主链已经部分接入 Rust runtime,但整体仍然是“前端重交互壳 + blob 持久化 + Rust compat 工具”的形态,还不是 kernel truth。
|
||||
|
||||
### 3.1 前端主壳仍然由 `simple-mind-map` 驱动
|
||||
|
||||
当前主组件:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx`
|
||||
|
||||
现状特征:
|
||||
|
||||
- 直接加载 `simple-mind-map` 与大量插件
|
||||
- 同时承担内嵌块、独立页、全屏态、工具栏、缩略图、右键菜单、图片处理、导入导出
|
||||
- 直接调用:
|
||||
- `mindmap.setData(...)`
|
||||
- `mindmap.getData(...)`
|
||||
- `mindmap.execCommand(...)`
|
||||
- 直接请求:
|
||||
- `fetch(/api/mindmap/${docId}/${mindmapId})`
|
||||
|
||||
这说明当前导图编辑器仍然以画布库数据结构为第一现场。
|
||||
|
||||
### 3.2 当前所谓 projection 仍然是前端摘要层,不是 kernel projection
|
||||
|
||||
当前文件:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/mindmap/mindmap-projection.ts`
|
||||
|
||||
当前 `buildMindmapProjection(...)` 的本质是:
|
||||
|
||||
- 接受一份 raw mindmap blob
|
||||
- 做 `simple-mind-map` 兼容归一化
|
||||
- 输出一个前端摘要对象
|
||||
- 同时保留 `data` 原始树
|
||||
|
||||
这意味着当前 projection 还是:
|
||||
|
||||
- **blob 的摘要**
|
||||
|
||||
而不是:
|
||||
|
||||
- **kernel subtree / graph 的正式投影**
|
||||
|
||||
### 3.3 当前持久化仍然是整棵导图 blob
|
||||
|
||||
当前持久化主链:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/convex/mindmaps.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts`
|
||||
|
||||
现状特征:
|
||||
|
||||
- `mindmaps.get` 返回整棵 `data`
|
||||
- `mindmaps.put` 写回整棵 `data`
|
||||
- `mindmaps` 表持有的是一份完整导图 JSON
|
||||
- 独立页服务端入口仍然走:
|
||||
- `mindmaps.get`
|
||||
- 然后在前端侧 `buildMindmapProjection(...)`
|
||||
|
||||
这说明当前“导图真相”依然是:
|
||||
|
||||
- **可直接存取的一整棵导图 blob**
|
||||
|
||||
而不是:
|
||||
|
||||
- **kernel node / edge / subtree**
|
||||
|
||||
### 3.4 Rust runtime 已经介入,但仍是 compat mindmap 树语义
|
||||
|
||||
当前 Rust 侧相关实现:
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/crates/core-protocol/src/mindmap.rs`
|
||||
- `/mnt/Data1T/mnote/rust/crates/bridge-runtime/src/lib.rs`
|
||||
|
||||
已存在的事实:
|
||||
|
||||
- Rust 侧已经有:
|
||||
- `MindmapTreeNode`
|
||||
- `MindmapOp`
|
||||
- `mindmap_get`
|
||||
- `mindmap_get_subtree`
|
||||
- `mindmap_put`
|
||||
- `mindmap_apply_ops`
|
||||
- `mindmap_outline_to_mindmap`
|
||||
- `bridge-runtime` 已能在 Rust 内部应用 `MindmapOp`
|
||||
|
||||
但这些能力当前本质上仍然是:
|
||||
|
||||
- 对一棵 `MindmapTreeNode` JSON 树做读取、修改、返回
|
||||
|
||||
不是:
|
||||
|
||||
- 对 `KernelNode` / `KernelEdge` / `KernelSubtree` 做正式操作
|
||||
|
||||
### 3.5 Kernel 协议已具备导图进入统一内核的入口
|
||||
|
||||
当前 kernel 类型:
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/crates/core-protocol/src/kernel.rs`
|
||||
|
||||
已经有:
|
||||
|
||||
- `KernelNodeType::Mindmap`
|
||||
- `KernelNodeType::MindmapNode`
|
||||
- `KernelProjectionKind::Mindmap`
|
||||
|
||||
这说明协议层已经承认:
|
||||
|
||||
> **导图应该属于统一 kernel,而不是永远停留在专用 blob 协议。**
|
||||
|
||||
问题不在方向,而在主线尚未切换完成。
|
||||
|
||||
---
|
||||
|
||||
## 4. 当前架构问题
|
||||
|
||||
如果继续维持当前模式,会有四类问题。
|
||||
|
||||
### 4.1 导图真相仍被视图库数据结构绑架
|
||||
|
||||
现在真正被保存、被读取、被回写的是:
|
||||
|
||||
- `simple-mind-map` 兼容树
|
||||
|
||||
这会导致:
|
||||
|
||||
- 领域模型被 UI 库字段形状反向约束
|
||||
- 导图语义无法稳定进入 kernel
|
||||
- 不同视图无法共享统一对象真相
|
||||
|
||||
### 4.2 AI 仍然不是直接操作 kernel
|
||||
|
||||
虽然已经有 `mindmap_apply_ops` 等工具,但它们当前语义仍然是:
|
||||
|
||||
- 取出一棵树
|
||||
- 在 runtime 里改树
|
||||
- 返回新树
|
||||
|
||||
这仍然不是:
|
||||
|
||||
- 直接创建 `mindmap_node`
|
||||
- 直接移动 subtree
|
||||
- 直接挂接 reference edge
|
||||
|
||||
因此 AI 还没有真正“直接写导图内核”。
|
||||
|
||||
### 4.3 导图还没有进入统一 projection 家族
|
||||
|
||||
当前 Sidebar / 页面树 / 文件树 正在往 kernel projection 收敛。
|
||||
|
||||
但导图仍然主要是:
|
||||
|
||||
- 专用 route
|
||||
- 专用 blob
|
||||
- 专用前端大组件
|
||||
|
||||
这会让导图继续成为一个旁路系统。
|
||||
|
||||
### 4.4 前端壳过重,迁移边界不清
|
||||
|
||||
`MindmapBlock.tsx` 当前同时承担:
|
||||
|
||||
- 读取
|
||||
- 兼容转换
|
||||
- 渲染
|
||||
- 编辑
|
||||
- 保存
|
||||
- 资产替换
|
||||
- 导入导出
|
||||
- 页面模式切换
|
||||
|
||||
这意味着:
|
||||
|
||||
> **只要导图真相还留在这里,Phase 6 就不会真正成立。**
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 6 的正确目标
|
||||
|
||||
Phase 6 的正确目标固定为:
|
||||
|
||||
> **让 Mindmap 从“独立对象中心 + blob 真相 + 前端重壳”降级成“kernel subtree / graph 的一种 projection 与 editor”。**
|
||||
|
||||
再收口成一句:
|
||||
|
||||
> **Mindmap 不是对象真相层,Mindmap 只是树/图真相的一种空间化编辑视图。**
|
||||
|
||||
这意味着:
|
||||
|
||||
- 导图不能再拥有第二份对象真相
|
||||
- 导图不能再以整棵 blob 作为长期 canonical model
|
||||
- 导图编辑器只能操作 kernel
|
||||
- 导图 route 只能消费 kernel projection
|
||||
|
||||
---
|
||||
|
||||
## 6. 目标分层
|
||||
|
||||
长期建议把导图域拆成四层。
|
||||
|
||||
### 6.1 Kernel Truth
|
||||
|
||||
这一层只存系统真相。
|
||||
|
||||
建议对象:
|
||||
|
||||
- `mindmap`
|
||||
- 作为导图根容器节点
|
||||
- `mindmap_node`
|
||||
- 作为导图树节点
|
||||
- `reference edge`
|
||||
- 作为节点到页面、附件、PDF anchor、summary、ai_note 的横向关系
|
||||
|
||||
这层只负责:
|
||||
|
||||
- 节点存在性
|
||||
- 父子结构
|
||||
- sibling 顺序
|
||||
- 节点稳定 ID
|
||||
- 节点元信息
|
||||
- 引用边
|
||||
- 审计与版本
|
||||
|
||||
### 6.2 Mindmap Projection Layer
|
||||
|
||||
这一层负责把 kernel truth 投影为导图视图可消费的数据。
|
||||
|
||||
长期至少应有两类 projection:
|
||||
|
||||
- `mindmap_preview`
|
||||
- 给文档内嵌卡片、轻量预览使用
|
||||
- `mindmap_editor`
|
||||
- 给导图独立页、沉浸编辑器使用
|
||||
|
||||
这层输出的应是:
|
||||
|
||||
- 稳定 node id
|
||||
- parent / child 关系
|
||||
- sibling order
|
||||
- depth
|
||||
- child count
|
||||
- 引用摘要
|
||||
- capability flags
|
||||
- 视图提示信息
|
||||
|
||||
而不是直接把前端控件内部状态回吐出去。
|
||||
|
||||
### 6.3 Editor Adapter Layer
|
||||
|
||||
这一层负责:
|
||||
|
||||
- 把 kernel projection 转成 `simple-mind-map` 当前所需的数据形状
|
||||
- 把画布交互翻译成 kernel command
|
||||
|
||||
这一层是 compat adapter,不是事实源。
|
||||
|
||||
### 6.4 View Shell
|
||||
|
||||
这一层只负责:
|
||||
|
||||
- 画布
|
||||
- 工具栏
|
||||
- 右键菜单
|
||||
- 缩略图
|
||||
- 全屏壳
|
||||
- 交互状态
|
||||
|
||||
这里可以继续用 `simple-mind-map` 过渡,但不能再持有对象真相。
|
||||
|
||||
---
|
||||
|
||||
## 7. 哪些语义必须进入 kernel
|
||||
|
||||
下面这些语义必须进入 kernel,而不是继续停留在 `simple-mind-map` blob 中。
|
||||
|
||||
### 7.1 节点级稳定语义
|
||||
|
||||
- `mindmap` 根节点
|
||||
- `mindmap_node` 子节点
|
||||
- `title / text`
|
||||
- `note`
|
||||
- `hyperlink`
|
||||
- `refs`
|
||||
- `collapsed`
|
||||
- sibling 排序键
|
||||
- 节点归档/删除状态
|
||||
|
||||
### 7.2 树语义
|
||||
|
||||
- 创建子节点
|
||||
- 创建同级节点
|
||||
- 重命名
|
||||
- 移动 subtree
|
||||
- 重排顺序
|
||||
- 删除 subtree
|
||||
|
||||
### 7.3 图语义
|
||||
|
||||
- 节点引用页面
|
||||
- 节点引用块
|
||||
- 节点引用附件
|
||||
- 节点引用 PDF 页码/anchor
|
||||
- AI 生成节点引用证据节点
|
||||
|
||||
### 7.4 审计语义
|
||||
|
||||
- command id
|
||||
- trace id
|
||||
- actor id
|
||||
- revision / version
|
||||
|
||||
---
|
||||
|
||||
## 8. 哪些语义不应进入 kernel
|
||||
|
||||
下面这些内容不应进入 kernel truth。
|
||||
|
||||
- 当前缩放比例
|
||||
- 当前视口位置
|
||||
- 当前选中节点
|
||||
- minimap 展开状态
|
||||
- 文本编辑框 DOM 状态
|
||||
- 鼠标拖拽中的临时态
|
||||
- 纯视图级动画状态
|
||||
- `simple-mind-map` 内部 history 栈
|
||||
- 仅服务当前画布库的缓存字段
|
||||
|
||||
这些内容属于:
|
||||
|
||||
- editor session state
|
||||
- view shell state
|
||||
|
||||
不是 kernel truth。
|
||||
|
||||
---
|
||||
|
||||
## 9. 命令层重构口径
|
||||
|
||||
当前 `mindmap_apply_ops` 和 `mindmap_put` 不能再继续被当作长期真相接口。
|
||||
|
||||
### 9.1 `mindmap_put`
|
||||
|
||||
长期应降级为:
|
||||
|
||||
- 导入整图
|
||||
- 替换快照
|
||||
- 迁移回放
|
||||
- 故障恢复
|
||||
|
||||
不应再作为常规编辑主路径。
|
||||
|
||||
### 9.2 `mindmap_apply_ops`
|
||||
|
||||
长期应保留,但语义要改。
|
||||
|
||||
它应变成:
|
||||
|
||||
- **compat facade**
|
||||
|
||||
内部行为应是:
|
||||
|
||||
- 把 `MindmapOp` 翻译成 kernel command 序列
|
||||
|
||||
例如:
|
||||
|
||||
- `addChild`
|
||||
- `kernel.node.create`
|
||||
- `addSiblingAfter`
|
||||
- `kernel.node.create` + sibling order 调整
|
||||
- `updateText`
|
||||
- `kernel.node.update`
|
||||
- `setHyperlink`
|
||||
- `kernel.node.update`
|
||||
- `setRefs`
|
||||
- `kernel.edge.attach` / `kernel.edge.detach`
|
||||
- `deleteNode`
|
||||
- `kernel.subtree.delete` 或 `kernel.node.archive`
|
||||
|
||||
也就是说:
|
||||
|
||||
> **`mindmap_apply_ops` 可以继续存在,但它不能继续直接修改一棵 compat 树。**
|
||||
|
||||
### 9.3 新的正式命令面
|
||||
|
||||
Phase 6 后导图应以 kernel command 为正式写面:
|
||||
|
||||
- `create_node`
|
||||
- `update_node`
|
||||
- `move_subtree`
|
||||
- `reorder_siblings`
|
||||
- `archive_node`
|
||||
- `restore_node`
|
||||
- `attach_edge`
|
||||
- `detach_edge`
|
||||
|
||||
如果 kernel 当前缺少某些命令,就应在 Phase 6 补入,而不是继续把缺口留给前端 blob。
|
||||
|
||||
---
|
||||
|
||||
## 10. Projection 重构口径
|
||||
|
||||
当前导图 route 仍然主要走:
|
||||
|
||||
- `mindmaps.get`
|
||||
|
||||
然后前端:
|
||||
|
||||
- `buildMindmapProjection(...)`
|
||||
|
||||
这条链必须调整。
|
||||
|
||||
### 10.1 正式读路径
|
||||
|
||||
长期正式读路径应是:
|
||||
|
||||
- `kernel.project_view`
|
||||
- projection=`mindmap`
|
||||
|
||||
而不是:
|
||||
|
||||
- `mindmaps.get`
|
||||
- 返回 raw blob
|
||||
|
||||
### 10.2 Projection 输出要求
|
||||
|
||||
导图 projection 输出至少应包括:
|
||||
|
||||
- `projection_id`
|
||||
- `root_node_id`
|
||||
- `items`
|
||||
- `edges`
|
||||
- `depth`
|
||||
- `sort_key`
|
||||
- `expand_hint`
|
||||
- `capability_flags`
|
||||
- `resource_meta`
|
||||
|
||||
### 10.3 前端 adapter 的角色
|
||||
|
||||
前端可以继续存在一个:
|
||||
|
||||
- `projection -> simple-mind-map` adapter
|
||||
|
||||
但这层只能是 view adapter。
|
||||
|
||||
它不能再同时承担:
|
||||
|
||||
- canonical model
|
||||
- 持久化模型
|
||||
- AI 写入模型
|
||||
|
||||
---
|
||||
|
||||
## 11. AI / CLI 口径
|
||||
|
||||
如果目标是“AI 能直接编写思维导图,而不是外挂组件”,那 Phase 6 的 AI 路线必须明确。
|
||||
|
||||
### 11.1 AI 读取导图
|
||||
|
||||
AI 应读取:
|
||||
|
||||
- kernel subtree
|
||||
- mindmap projection
|
||||
- node refs / evidence
|
||||
|
||||
而不是只读取一棵视图库 JSON。
|
||||
|
||||
### 11.2 AI 修改导图
|
||||
|
||||
AI 应直接发 kernel command,或通过 compat facade 发命令。
|
||||
|
||||
正确路径应是:
|
||||
|
||||
- AI 意图
|
||||
- tool / command plan
|
||||
- kernel command
|
||||
- projection 刷新
|
||||
|
||||
不是:
|
||||
|
||||
- AI 输出一整棵导图 JSON
|
||||
- 再整体覆盖保存
|
||||
|
||||
### 11.3 AI 生成导图
|
||||
|
||||
`mindmap_outline_to_mindmap` 这类能力可以保留,但输出应优先写入:
|
||||
|
||||
- `mindmap`
|
||||
- `mindmap_node`
|
||||
- `reference edge`
|
||||
|
||||
而不是先生成一棵孤立 blob 再把它塞进存储。
|
||||
|
||||
---
|
||||
|
||||
## 12. 与当前代码对应的重构任务
|
||||
|
||||
### 12.1 Rust 协议层
|
||||
|
||||
需要重构或补充:
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/crates/core-protocol/src/kernel.rs`
|
||||
- `/mnt/Data1T/mnote/rust/crates/core-protocol/src/mindmap.rs`
|
||||
|
||||
建议动作:
|
||||
|
||||
- 保留 `MindmapOp` 作为 compat DTO
|
||||
- 不再把 `MindmapTreeNode` 当长期 canonical model
|
||||
- 为导图补齐正式 projection / command 契约
|
||||
|
||||
### 12.2 Rust runtime
|
||||
|
||||
需要重构:
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/crates/bridge-runtime/src/lib.rs`
|
||||
|
||||
建议动作:
|
||||
|
||||
- 增加正式 `mindmap` projection builder
|
||||
- 把 `mindmap_apply_ops` 改成 command translator
|
||||
- 让 `mindmaps.get` 逐步降级为 compat query
|
||||
- 新增或补齐导图相关 kernel command
|
||||
|
||||
### 12.3 前端 mindmap adapter
|
||||
|
||||
需要重构:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/mindmap/mindmap-projection.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/mindmap/mindmapOps.ts`
|
||||
|
||||
建议动作:
|
||||
|
||||
- `mindmap-projection.ts`
|
||||
- 从“blob 摘要器”转成“kernel projection adapter”
|
||||
- `mindmapOps.ts`
|
||||
- 从“本地真相修改器”降级为 compat / fallback 层
|
||||
|
||||
### 12.4 前端视图壳
|
||||
|
||||
需要重构:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/mindmap/[docId]/[mindmapId]/page.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/mindmap/[docId]/[mindmapId]/mindmap-page-client.tsx`
|
||||
|
||||
建议动作:
|
||||
|
||||
- 将读取从 `mindmaps.get` 切到 kernel projection
|
||||
- 将写入从 `getData()/POST whole blob` 切到命令流
|
||||
- 将 `MindmapBlock.tsx` 缩成 view shell + adapter
|
||||
|
||||
### 12.5 持久化与兼容层
|
||||
|
||||
需要重构:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/convex/mindmaps.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts`
|
||||
|
||||
建议动作:
|
||||
|
||||
- 迁移期允许双写
|
||||
- 长期让 `mindmaps` 表退到 compat snapshot / import-export 层
|
||||
- 正式真相切到 kernel backing store
|
||||
|
||||
---
|
||||
|
||||
## 13. 迁移分期
|
||||
|
||||
### 13.1 Phase 6-A:冻结边界
|
||||
|
||||
目标:
|
||||
|
||||
- 停止在前端和 compat blob 上继续追加长期业务语义
|
||||
|
||||
详细 checklist:
|
||||
|
||||
- [ ] 冻结口径:
|
||||
- 明确 `MindmapTreeNode` 只作为 compat DTO
|
||||
- 明确 `mindmaps` blob 只作为过渡存储,不再新增长期业务语义
|
||||
- 明确 `simple-mind-map` 只作为 renderer / editor adapter,不再定义系统真相
|
||||
- [ ] 协议边界盘点:
|
||||
- 盘点 `/mnt/Data1T/mnote/rust/crates/core-protocol/src/mindmap.rs` 中哪些字段属于 compat 语义
|
||||
- 盘点 `/mnt/Data1T/mnote/rust/crates/core-protocol/src/kernel.rs` 中已有 `mindmap` / `mindmap_node` / `projection` 能力
|
||||
- 列出 Phase 6 后必须新增的 kernel command / projection 契约缺口
|
||||
- [ ] 前端边界盘点:
|
||||
- 盘点 `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx` 中哪些逻辑属于真相层
|
||||
- 将这些逻辑标记为后续待迁移:
|
||||
- 读主链
|
||||
- 写主链
|
||||
- 本地摘要
|
||||
- 引用回写
|
||||
- 资产 URL 反写
|
||||
- 明确哪些逻辑继续保留在 view shell:
|
||||
- 画布渲染
|
||||
- 右键菜单
|
||||
- 工具栏
|
||||
- 缩略图
|
||||
- 全屏壳
|
||||
- [ ] 持久化边界盘点:
|
||||
- 盘点 `/mnt/Data1T/mnote/wolai-frontend/convex/mindmaps.ts` 的现有字段与索引
|
||||
- 盘点 `/mnt/Data1T/mnote/wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts` 的现有读写语义
|
||||
- 明确哪些接口后续降级为 compat:
|
||||
- `mindmaps.get`
|
||||
- `mindmaps.put`
|
||||
- `mindmaps.delete`
|
||||
- `mindmaps.restore`
|
||||
- [ ] AI / CLI 边界盘点:
|
||||
- 盘点 `mindmap_get` / `mindmap_get_subtree` / `mindmap_put` / `mindmap_apply_ops`
|
||||
- 标注哪些工具后续保留为 facade,哪些应切到 kernel command
|
||||
- 明确 AI 不再以“整图 JSON 覆盖”作为长期主路径
|
||||
- [ ] 文档口径冻结:
|
||||
- 当前文档作为 Phase 6 主文档继续维护
|
||||
- checklist / architecture / 后续任务拆分不再把导图描述为独立事实源
|
||||
- 新增导图相关设计时默认引用本文件,而不是继续围绕 `simple-mind-map` 数据结构展开
|
||||
- [ ] 代码约束冻结:
|
||||
- 在未完成 Phase 6-B 之前,不再给 `MindmapTreeNode` 增加新的长期业务字段
|
||||
- 在未完成 Phase 6-C 之前,不再新增新的“整图读取后本地改树再整体提交”的主路径
|
||||
- 在未完成 Phase 6-D 之前,不再新增绕过 kernel 的 AI 写入链路
|
||||
|
||||
出阶段判定:
|
||||
|
||||
- [ ] 新导图语义默认先判断是否落 kernel,而不是先落前端 blob
|
||||
- [ ] `MindmapBlock.tsx` 不再继续承担新的长期对象真相职责
|
||||
- [ ] 团队已统一接受:
|
||||
- compat blob 不是长期真相
|
||||
- `simple-mind-map` 不是对象中心
|
||||
- Phase 6 后正式主线是 kernel truth + projection + adapter
|
||||
|
||||
### 13.2 Phase 6-B:先切读路径
|
||||
|
||||
目标:
|
||||
|
||||
- 独立页与内嵌预览先读取正式 kernel projection
|
||||
|
||||
详细 checklist:
|
||||
|
||||
- [ ] 定义 projection 家族:
|
||||
- 定义 `mindmap_preview` projection
|
||||
- 定义 `mindmap_editor` projection
|
||||
- 明确两者共享的稳定字段:
|
||||
- `projection_id`
|
||||
- `root_node_id`
|
||||
- `items`
|
||||
- `edges`
|
||||
- `capability_flags`
|
||||
- `resource_meta`
|
||||
- [ ] 定义 projection item 结构:
|
||||
- 稳定 node id
|
||||
- `parent_id`
|
||||
- `sort_key`
|
||||
- `depth`
|
||||
- `child_count`
|
||||
- `title/text`
|
||||
- `collapsed`
|
||||
- `refs summary`
|
||||
- `style hint`
|
||||
- [ ] Rust runtime 补 projection builder:
|
||||
- 在 `/mnt/Data1T/mnote/rust/crates/bridge-runtime/src/lib.rs` 中增加正式 `mindmap` projection builder
|
||||
- 让 `kernel.project_view` 可返回 `KernelProjectionKind::Mindmap`
|
||||
- 明确 preview 与 editor 的输出差异,不再直接返回 compat 整树
|
||||
- [ ] 前端 adapter 改造:
|
||||
- 将 `/mnt/Data1T/mnote/wolai-frontend/src/lib/mindmap/mindmap-projection.ts` 从“blob 摘要器”改为“kernel projection adapter”
|
||||
- 把 adapter 输出限定为 `simple-mind-map` 所需最小数据形状
|
||||
- 不再让 adapter 同时承担 canonical model 职责
|
||||
- [ ] 独立页读路径切换:
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/mindmap/[docId]/[mindmapId]/page.tsx`
|
||||
改为优先读取 kernel projection
|
||||
- SSR 初始数据不再以 `mindmaps.get` raw blob 为主
|
||||
- `mindmap-page-client.tsx` 仅接收 projection / adapter 输出
|
||||
- [ ] 文档内嵌预览切换:
|
||||
- 内嵌卡片优先消费 `mindmap_preview`
|
||||
- 预览态不再默认依赖整棵 blob
|
||||
- 轻量预览和沉浸编辑使用不同 projection,避免主编辑壳过早挂载
|
||||
- [ ] 兼容回退策略:
|
||||
- kernel projection 不可用时,可临时回退到 compat `mindmaps.get`
|
||||
- 回退路径必须显式标记为 compat/fallback
|
||||
- 回退逻辑不得反向成为新主路径
|
||||
- [ ] 可观测性与追踪:
|
||||
- projection 响应带 `request_id` / `trace_id`
|
||||
- 前端记录当前页面命中的 projection 来源:
|
||||
- kernel
|
||||
- compat fallback
|
||||
- 为后续切流留出观测点
|
||||
- [ ] 测试与验收:
|
||||
- 增加 runtime 单测:`mindmap_preview` / `mindmap_editor` 输出稳定
|
||||
- 增加 adapter 单测:projection -> `simple-mind-map` data
|
||||
- 增加独立页 smoke:首屏读 projection 成功
|
||||
- 增加内嵌态 smoke:文档页不再依赖整图 blob 才能显示摘要
|
||||
|
||||
出阶段判定:
|
||||
|
||||
- [ ] 独立页正式主读链已经是 kernel projection
|
||||
- [ ] 文档内嵌预览正式主读链已经是 preview projection
|
||||
- [ ] compat `mindmaps.get` 只作为 fallback,而不是默认主链
|
||||
- [ ] 前端已有清晰的 `projection -> adapter -> renderer` 三层边界
|
||||
|
||||
### 13.3 Phase 6-C:再切写路径
|
||||
|
||||
目标:
|
||||
|
||||
- 常规编辑改为命令流
|
||||
|
||||
详细 checklist:
|
||||
|
||||
- [ ] 明确正式写面:
|
||||
- `create_node`
|
||||
- `update_node`
|
||||
- `move_subtree`
|
||||
- `reorder_siblings`
|
||||
- `archive_node`
|
||||
- `restore_node`
|
||||
- `attach_edge`
|
||||
- `detach_edge`
|
||||
- [ ] 补齐 kernel command 缺口:
|
||||
- 若当前 kernel 缺少 sibling reorder 命令,需补齐
|
||||
- 若当前 kernel 缺少 subtree archive/delete 语义,需补齐
|
||||
- 若当前 kernel 缺少导图节点 refs 的 attach/detach 语义,需补齐
|
||||
- [ ] compat `MindmapOp` 到 kernel command 的翻译表落地:
|
||||
- `addChild`
|
||||
- `addSiblingAfter`
|
||||
- `updateText`
|
||||
- `setHyperlink`
|
||||
- `setRefs`
|
||||
- `appendNote`
|
||||
- `deleteNode`
|
||||
- 为每个 op 指定唯一的 kernel command 映射与错误语义
|
||||
- [ ] Rust runtime 改造:
|
||||
- `/mnt/Data1T/mnote/rust/crates/bridge-runtime/src/lib.rs`
|
||||
中的 `mindmap_apply_ops` 改为 command translator
|
||||
- 不再直接对 compat 树做 canonical 修改
|
||||
- 执行完成后返回:
|
||||
- command 执行结果
|
||||
- 最新 projection 或 refresh token
|
||||
- trace / audit 信息
|
||||
- [ ] 前端保存链改造:
|
||||
- `MindmapBlock.tsx` 中工具栏操作不再默认走 `getData()/POST whole blob`
|
||||
- 节点编辑、移动、删除、引用更新都发命令,而不是整体回传 snapshot
|
||||
- 本地只保留短暂 optimistic state,不再作为长期真相
|
||||
- [ ] 导图 route 改造:
|
||||
- `/api/mindmap/[docId]/[mindmapId]`
|
||||
的 `POST` 从常规编辑主入口降级
|
||||
- 新增或切换到专用 command route / command envelope
|
||||
- `mindmap_put` 只保留:
|
||||
- 导入
|
||||
- 替换快照
|
||||
- 恢复
|
||||
- 迁移回放
|
||||
- [ ] 双写与迁移策略:
|
||||
- 迁移期允许 kernel truth + compat snapshot 双写
|
||||
- 双写失败时要有清晰告警,不得静默漂移
|
||||
- 明确哪一侧是主真相,哪一侧只是镜像
|
||||
- [ ] 冲突与审计:
|
||||
- 所有命令返回 revision / version
|
||||
- 写入链保留 `command_id` / `request_id` / `trace_id`
|
||||
- 并发冲突时优先按 kernel command 冲突规则处理,而不是前端最后一次整图覆盖
|
||||
- [ ] 测试与验收:
|
||||
- runtime 单测:各类 `MindmapOp` 均正确翻译为 kernel command
|
||||
- route 单测:常规编辑不再依赖整图 `put`
|
||||
- UI smoke:增删改拖拽节点后可稳定刷新 projection
|
||||
- 回归测试:导入/恢复仍可通过 `mindmap_put` 正常工作
|
||||
|
||||
出阶段判定:
|
||||
|
||||
- [ ] 常规编辑链路已经以 kernel command 为主
|
||||
- [ ] `mindmap_put` 已从常规编辑主链退出
|
||||
- [ ] 前端不再依赖 `getData()` 作为长期提交真相
|
||||
- [ ] compat snapshot 即使保留,也只是双写镜像或导入导出格式
|
||||
|
||||
### 13.4 Phase 6-D:统一 AI / CLI
|
||||
|
||||
目标:
|
||||
|
||||
- AI / CLI 改为直接操作 kernel truth
|
||||
|
||||
详细 checklist:
|
||||
|
||||
- [ ] AI 工具口径统一:
|
||||
- 明确 AI 读导图优先读取 kernel projection / subtree
|
||||
- 明确 AI 写导图优先发送 kernel command
|
||||
- 明确 AI 不再以“输出完整 mindmap JSON 并整体覆盖”作为主模式
|
||||
- [ ] compat tool 重构:
|
||||
- `mindmap_get`
|
||||
降级为 compat read facade
|
||||
- `mindmap_get_subtree`
|
||||
降级为 compat read facade
|
||||
- `mindmap_apply_ops`
|
||||
降级为 compat write facade
|
||||
- `mindmap_put`
|
||||
降级为导入/恢复工具
|
||||
- [ ] 新 kernel-aware tool 面补齐:
|
||||
- 面向 node / subtree / edge 的导图工具定义
|
||||
- 工具参数默认使用:
|
||||
- `nodeId`
|
||||
- `rootNodeId`
|
||||
- `workspaceId`
|
||||
- `pageId`
|
||||
- `edgeType`
|
||||
- 避免继续以 compat `uid + whole tree` 为中心
|
||||
- [ ] AI host/runtime 接缝改造:
|
||||
- 导图 agent route 优先走 kernel-aware tool
|
||||
- `mindmap_outline_to_mindmap` 的输出优先落 kernel truth
|
||||
- AI 修改后的刷新结果优先返回 projection,而不是 raw blob
|
||||
- [ ] CLI 改造:
|
||||
- CLI 新增或切换到导图 kernel command 子命令
|
||||
- 现有 `mindmap get/put/op` 标记 compat/legacy 语义
|
||||
- CLI smoke 优先验证 kernel command 与 projection 输出
|
||||
- [ ] 权限与审计:
|
||||
- AI / CLI 导图写入保留 actor / trace / command id
|
||||
- 区分:
|
||||
- 用户直接编辑
|
||||
- AI 代理修改
|
||||
- 导入/恢复
|
||||
- 确保后续 bridge log / audit 能区分来源
|
||||
- [ ] 结果返回规范:
|
||||
- AI / CLI 执行导图写命令后,默认返回:
|
||||
- command 执行结果
|
||||
- 受影响 node / edge
|
||||
- 最新 projection 摘要
|
||||
- 非必要不再回传整棵 compat 树
|
||||
- [ ] 迁移与兼容:
|
||||
- 迁移期 compat tools 仍可保留
|
||||
- 但默认优先级必须低于 kernel-aware tools
|
||||
- 新增 AI 能力时不得再优先扩写 compat blob 工具
|
||||
- [ ] 测试与验收:
|
||||
- AI tool 单测:至少一条创建节点、修改节点、挂接 refs 的链路走 kernel command
|
||||
- CLI smoke:至少一条真实导图操作链不依赖整图覆盖
|
||||
- 回归测试:compat tool 仍可用于迁移和紧急 fallback
|
||||
|
||||
出阶段判定:
|
||||
|
||||
- [ ] AI 已能直接创建、修改、移动导图节点与引用边
|
||||
- [ ] CLI 已能直接操作导图 kernel truth
|
||||
- [ ] compat tools 只剩 facade / import-export / fallback 职责
|
||||
- [ ] 导图 AI / CLI 主链已经和 Sidebar / 页面树 / 阅读页一样,正式回到统一 kernel command / projection 体系
|
||||
|
||||
### 13.5 Phase 6-E:压缩旧壳
|
||||
|
||||
目标:
|
||||
|
||||
- 把 `MindmapBlock.tsx` 收缩为可替换 view shell
|
||||
|
||||
要求:
|
||||
|
||||
- 真相、命令、projection 已完全外移
|
||||
- 前端只剩渲染与交互适配
|
||||
|
||||
---
|
||||
|
||||
## 14. 完成判定
|
||||
|
||||
只有同时满足下面几条,才能说 `Kernel Phase 6` 完成。
|
||||
|
||||
- [ ] 导图正式事实源已经进入 kernel node / edge / subtree
|
||||
- [ ] 导图独立页读取的是 kernel projection,而不是 `mindmaps.get` raw blob
|
||||
- [ ] 文档内嵌导图读取的是 preview projection,而不是前端直接拼整棵树
|
||||
- [ ] 常规编辑操作已经回写 kernel command,而不是 `POST` 整棵 `getData()`
|
||||
- [ ] AI / CLI 可以直接创建、修改、移动导图节点,而不依赖整图覆盖
|
||||
- [ ] `simple-mind-map` 已经退到 renderer / adapter 层
|
||||
- [ ] `mindmaps` blob 存储已降级为 compat snapshot 或导入导出用途
|
||||
|
||||
---
|
||||
|
||||
## 15. 非目标
|
||||
|
||||
这阶段不以这些事情为主目标:
|
||||
|
||||
- 立即把整个导图画布改写成 Rust 前端
|
||||
- 立即替换全部导图 UI 细节
|
||||
- 把 `simple-mind-map` 的所有样式字段完整提升为 kernel 语义
|
||||
- 在第一阶段就清空全部历史兼容接口
|
||||
|
||||
Phase 6 的重点只有一个:
|
||||
|
||||
> **先把导图真相、导图命令、导图 projection 从前端 blob 体系里拔出来,正式并入 `tree-first graph kernel`。**
|
||||
@@ -0,0 +1,137 @@
|
||||
# Wolai-aline 对标测试流程 v1
|
||||
|
||||
> 本流程用于所有以 Wolai 体验复刻、对标、aline/alignment 为目标的任务。目标不是只做出类似文案,而是用真实 Wolai 行为和本地 `3000` 行为做可复现对比,形成可回归的 smoke 与截图证据。
|
||||
|
||||
## 适用范围
|
||||
|
||||
凡任务描述包含以下任一意图,默认进入本流程:
|
||||
|
||||
- `wolai-aline`、`wolai-align`、`Wolai 对标`、`复刻 Wolai`、`恢复 Wolai 体验`。
|
||||
- 需要把本地 `http://127.0.0.1:3000` 的页面、Sidebar、搜索、编辑器、浮层、菜单、快捷键或交互状态对齐 Wolai。
|
||||
- 需要比较 Wolai 页面与本地实现的截图、DOM、键鼠行为或浏览器状态。
|
||||
|
||||
## 固定资源
|
||||
|
||||
- 本地入口:`http://127.0.0.1:3000/`。
|
||||
- Wolai 参考页 / 当前授权 Hermes 测试页:`https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd`。
|
||||
- Wolai owner 登录态 Chrome profile:`/mnt/Data1T/mnote/tmp/wolai-playwright-profile`。
|
||||
- Chrome 可执行文件:`/opt/google/chrome/chrome`。
|
||||
- 对标证据输出根目录:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/`。
|
||||
- 当前可复用 smoke 示例:`/mnt/Data1T/mnote/scripts/task128-rust-web-wolai-search-modal-smoke.js`。
|
||||
|
||||
## 安全边界
|
||||
|
||||
- 默认优先对 Wolai 做只读操作:打开页面、点击入口、hover、打开/关闭菜单、截图、DOM 读取、输入搜索关键词。
|
||||
- 当前 Hermes 测试页 `https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd` 已授权用于 Wolai-aline 编辑器对标;当任务需要真实编辑行为时,可以执行最小范围编辑测试。
|
||||
- 编辑测试必须记录编辑前后截图、动作链、输入内容和是否已清理;测试内容优先使用唯一标记,例如 `[mnote-aline-test-时间戳]`。
|
||||
- 即使在 Hermes 测试页,也禁止未经确认地删除、移动、归档、发布、评论、改权限或批量修改既有非测试内容。
|
||||
- 其他 Wolai 页面仍默认只读;如需写入,必须先由用户提供沙盒页 URL 和明确授权。
|
||||
- 遇到登录、滑块验证或登录态失效,不绕过验证;记录阻塞并让用户介入。
|
||||
- 登录态 profile 只放在 `tmp/` 这类 git ignored 路径,不提交、复制或打印敏感 cookie/token。
|
||||
|
||||
## Codex skill
|
||||
|
||||
后续 Wolai-aline 任务必须启用本机 skill:`/home/lix/.codex/skills/wolai-aline`。该 skill 是本流程的执行入口,负责强制差异矩阵、截图复核、subagent 浏览器取证和失败模式沉淀。
|
||||
|
||||
## 标准流程
|
||||
|
||||
1. 明确小任务
|
||||
|
||||
从设计稿、缺陷反馈或用户描述中抽出一个可验证的小任务。任务必须能用具体行为描述,例如“侧栏搜索图标打开全局搜索 modal 且不跳转”“Ctrl+P 在 modal 打开时关闭 modal”。
|
||||
|
||||
2. 先取 Wolai 基线(必要时编辑)
|
||||
|
||||
浏览器测试必须交给 subagent 执行。subagent 默认只读;当任务明确需要编辑器行为时,可在当前 Hermes 测试页进入受控 editable-test mode,记录 Wolai 的真实行为、DOM 线索、截图路径、编辑内容和不可验证项。
|
||||
|
||||
Wolai 基线至少记录:
|
||||
- 打开入口:哪个按钮、菜单、快捷键或区域触发。
|
||||
- 关闭入口:快捷键、Esc、遮罩、关闭按钮、再次触发等是否有效。
|
||||
- URL 是否变化。
|
||||
- 关键控件形态和状态:开关、下拉、按钮、菜单、输入框、焦点态、选中态、disabled 态。
|
||||
- 关键文本只是辅助证据,不能替代控件形态和交互状态。
|
||||
- 截图保存到 `/mnt/Data1T/mnote/tmp/wolai-editor-parity/<task>/`。
|
||||
|
||||
3. 写 RED smoke
|
||||
|
||||
在本地实现前,先新增或扩展 `scripts/task*-smoke.js`。smoke 必须在当前实现上失败,并且失败原因要对应缺失行为。
|
||||
|
||||
smoke 断言优先级:
|
||||
- 行为断言:打开、关闭、URL 不跳转、焦点、快捷键、遮罩点击。
|
||||
- DOM 语义断言:`role`、`aria-*`、`data-testid`、`data-*` 状态。
|
||||
- 视觉状态代理断言:开关开启/关闭、下拉当前值、结果行存在、hover/active class。
|
||||
- 文案断言:作为补充,不能单独作为通过依据。
|
||||
|
||||
4. 小范围实现
|
||||
|
||||
只改当前任务直接相关的文件。涉及 `3000` 当前主入口时优先检查 `rust/crates/mnote-web/`;不要把长期语义塞进临时 compat 或前端第二份真相。
|
||||
|
||||
若修改 Rust SSR 常量或样式,需要重启 `npm run desktop:hot` 或对应 `mnote-web` 进程后再跑浏览器 smoke,避免测到旧二进制。
|
||||
|
||||
5. 本地验证
|
||||
|
||||
至少运行:
|
||||
- 当前任务 smoke,例如 `node scripts/task128-rust-web-wolai-search-modal-smoke.js`。
|
||||
- 与改动范围匹配的格式化/单测,例如 `cd rust && cargo fmt -p mnote-web --check && cargo test -p mnote-web`。
|
||||
|
||||
如有 CSS 尺寸、SSR 输出、页面入口等护栏测试失败,优先修实现或压缩新增样式,不随意放宽阈值。
|
||||
|
||||
6. subagent 对标复测
|
||||
|
||||
实现后再次派 subagent 做浏览器对标。subagent 负责:
|
||||
- 用 Wolai owner/profile 或公开态取参考截图。
|
||||
- 用本地 `3000` 执行同一动作链。
|
||||
- 记录 DOM 状态、URL、截图路径、剩余差异。
|
||||
- 不修改源码,不清理截图,不还原文件。
|
||||
|
||||
7. 主线程复核截图
|
||||
|
||||
主线程必须查看或复核 subagent 产出的 Wolai 与本地截图。不能只根据 subagent 的“通过”结论或文本断言宣布对齐。
|
||||
|
||||
复核重点:
|
||||
- 控件类型是否一致,例如 switch 不能误做成普通 pill button。
|
||||
- 开启/关闭、选中/未选中、hover/active 等状态是否一致。
|
||||
- 打开/关闭路径是否一致,例如同一入口不应跳转到另一页面。
|
||||
- 快捷键是否是 toggle 还是单向打开。
|
||||
- 结果列表、命中高亮、空态、占位符是否符合当前任务验收范围。
|
||||
|
||||
8. 汇报
|
||||
|
||||
最终汇报必须包含:
|
||||
- 改动文件。
|
||||
- RED/GREEN 验证命令。
|
||||
- Wolai 与本地截图绝对路径。
|
||||
- 已对齐项和剩余差异。
|
||||
- 未执行或受阻的验证原因。
|
||||
|
||||
## Subagent 浏览器测试模板
|
||||
|
||||
```text
|
||||
请执行 Wolai-aline 浏览器对标测试,不要修改源码。默认只读;如任务明确要求编辑器行为,可在当前 Hermes 测试页做最小编辑验证。禁止删除、移动、归档、发布、评论、改权限或批量修改既有非测试内容。
|
||||
|
||||
任务:<一句话描述当前对标行为>
|
||||
Wolai URL: https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd
|
||||
Wolai profile: /mnt/Data1T/mnote/tmp/wolai-playwright-profile
|
||||
本地 URL: http://127.0.0.1:3000/
|
||||
输出目录: /mnt/Data1T/mnote/tmp/wolai-editor-parity/<task>/
|
||||
|
||||
请验证:
|
||||
1. Wolai 中该行为的打开入口、关闭入口、URL 变化、关键 DOM/视觉状态。
|
||||
2. 本地 3000 中同一行为是否一致。
|
||||
3. 保存 Wolai 和本地截图。
|
||||
4. 回报截图绝对路径、通过/失败结论、剩余差异。
|
||||
|
||||
遇到登录/滑块不要绕过,直接报告需要用户介入。
|
||||
```
|
||||
|
||||
## 当前已固化样例:搜索 modal
|
||||
|
||||
当前实现已用 `task128-rust-web-wolai-search-modal-smoke.js` 固化以下行为:
|
||||
|
||||
- 顶栏搜索按钮打开同一个搜索 modal。
|
||||
- 侧栏左上搜索入口打开同一个搜索 modal,不跳转 `/search`。
|
||||
- 搜索选项包含 `仅匹配标题`、`精确匹配`、`按编辑时间`、`按创建时间`、`页面内搜索`。
|
||||
- `仅匹配标题` 和 `页面内搜索` 是 `role="switch"` 且默认 `aria-checked="true"`。
|
||||
- `Ctrl+P` 在 modal 打开时关闭 modal。
|
||||
- 结果数量、快捷键提示、空态/结果区域可被 smoke 捕获。
|
||||
|
||||
历史教训:不能只检查 modal 文案。上一次搜索 modal 先误把 Wolai 的 switch 做成普通 pill button,截图复核后才发现。因此后续 Wolai-aline 任务必须把截图复核写入验收,而不是把 subagent 文本结论当作最终证据。
|
||||
@@ -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 风格单面板文件树的最小可运行代码示例吗?
|
||||
@@ -0,0 +1,147 @@
|
||||
# 90-2 参考资料:页面树 Rust 生态调研
|
||||
|
||||
是的,Rust生态中有多种类似Notion页面树的实现,覆盖**数据结构库**、**UI组件库**(终端/桌面)和**完整应用**三类,均支持层次化文档管理、折叠展开、节点操作等核心功能。Notion页面树的关键特性是**块级编辑+嵌套页面+树形导航**,以下是Rust中的对应方案。
|
||||
|
||||
---
|
||||
|
||||
### 一、核心数据结构库:构建页面树的基础
|
||||
|
||||
这些库提供树状数据结构,可作为Notion风格页面树的底层存储,支持节点增删改查、层级管理等操作。
|
||||
|
||||
| 库名称 | 核心特点 | 适用场景 |
|
||||
|--------|----------|----------|
|
||||
| **easy-tree** | 轻量级树结构,深度优先遍历,支持并行迭代 | 快速构建简单页面树,文件系统映射 |
|
||||
| **tree-ds** | 支持节点插入/删除/移动,整树枚举,子树修剪与嫁接 | 复杂层次结构管理,需要灵活节点操作 |
|
||||
| **treelog** | 自定义树渲染,支持多种样式(Unicode/ASCII/Box) | 命令行工具中展示页面树结构 |
|
||||
| **ptree** | 美观的树状结构打印,支持自定义节点显示 | 调试或展示页面树层级关系 |
|
||||
|
||||
---
|
||||
|
||||
### 二、UI组件库:实现页面树的可视化与交互
|
||||
|
||||
这些组件库提供现成的树形UI,支持折叠展开、拖拽、键盘导航等Notion页面树关键交互功能。
|
||||
|
||||
#### 1. 终端(TUI)组件库
|
||||
|
||||
| 组件库 | 适配框架 | 核心特性 |
|
||||
|--------|----------|----------|
|
||||
| **tui-file-explorer** | Ratatui | 双面板布局,文件/目录层级展示,支持Git状态标记 |
|
||||
| **ratatui-toolkit** | Ratatui | 包含树视图组件,支持自定义渲染与交互 |
|
||||
| **cursive-tree** | Cursive | 支持"<>"/"↑↓"导航,鼠标点击切换折叠状态,多选择模式 |
|
||||
|
||||
#### 2. 桌面(GUI)组件库
|
||||
|
||||
| 组件库 | 适配框架 | 核心特性 |
|
||||
|--------|----------|----------|
|
||||
| **gpui-component::tree** | GPUI | 支持折叠/展开、键盘导航、自定义项渲染,适合文件浏览器和导航菜单 |
|
||||
| **fyrox::gui::tree** | Fyrox | 内置选择控制,支持Ctrl+Click多选,Alt+Click拖拽 |
|
||||
| **iced_file_tree** | Iced | 适配Iced跨平台GUI框架,支持文件系统导航与层级展示 |
|
||||
| **egui-tree-view** | egui | 纯Rust即时模式GUI,支持嵌套节点,折叠展开,拖拽排序 |
|
||||
|
||||
---
|
||||
|
||||
### 三、完整应用:Notion风格页面树的直接体验
|
||||
|
||||
这些应用提供完整的Notion-like体验,包含页面树导航+块级编辑器,可直接使用或作为参考实现。
|
||||
|
||||
| 应用名称 | 技术栈 | 页面树特性 |
|
||||
|----------|--------|------------|
|
||||
| **Rustree** | Rust | 分层结构存储HTML文本,支持节点引用,类似Treepad |
|
||||
| **TauriNote** | Tauri(Rust+WebView) | 仿Notion块级编辑器,文件树管理,支持创建/删除笔记 |
|
||||
| **Ferrite** | Rust+egui | 轻量级文本编辑器,支持Markdown,内置文档结构导航 |
|
||||
| **ylin** | Tauri 2+Rust | 文件树侧边栏,支持Markdown编辑,实时预览 |
|
||||
|
||||
---
|
||||
|
||||
### 四、Notion页面树核心功能的Rust实现方案
|
||||
|
||||
#### 1. 块级编辑+页面树结合
|
||||
|
||||
Notion的核心是**块级内容+嵌套页面**,Rust中可通过以下方式实现:
|
||||
- 后端:使用`tree-ds`或`easy-tree`存储块/页面层次结构,每个节点包含内容类型(文本/标题/列表等)和子节点列表
|
||||
- 前端:使用GPUI/egui/Iced的树组件渲染页面树,块编辑器可集成`rust-markdown`或自定义块解析器
|
||||
|
||||
#### 2. 拖拽调整页面顺序
|
||||
|
||||
拖拽是Notion页面树的重要交互,Rust GUI框架中实现方式:
|
||||
- **egui**:通过`egui::DragValue`和自定义状态管理实现节点拖拽排序
|
||||
- **Fyrox**:内置`Tree`组件支持Alt+Click拖拽,无需额外实现
|
||||
- **Tauri**:结合Rust后端与前端拖拽库(如SortableJS),通过IPC同步节点位置
|
||||
|
||||
#### 3. 动态加载与懒渲染
|
||||
|
||||
大型页面树需要懒加载优化,Rust中可通过:
|
||||
- 使用`TreeBackend`按需加载子节点(如`cursive-tree`的实现)
|
||||
- 在Tauri应用中,Rust后端监听节点展开事件,读取子页面数据并返回给前端
|
||||
|
||||
---
|
||||
|
||||
### 五、使用场景与选择建议
|
||||
|
||||
| 使用场景 | 推荐选择 | 理由 |
|
||||
|----------|----------|------|
|
||||
| 快速体验Notion风格页面树 | TauriNote/Rustree | 开箱即用,完整UI体验,支持块编辑与页面树导航 |
|
||||
| 集成到Ratatui终端应用 | tui-file-explorer | 专为Ratatui设计,双面板布局,功能完善 |
|
||||
| 构建跨平台GUI应用 | egui-tree-view/iced_file_tree | 纯Rust实现,原生GUI体验,无需WebView |
|
||||
| 开发Tauri桌面应用 | 自定义实现+前端组件 | 结合Rust后端性能与前端交互灵活性,最接近Notion体验 |
|
||||
| 仅需页面树数据结构 | easy-tree/tree-ds | 轻量级,API简单,适合快速开发 |
|
||||
|
||||
---
|
||||
|
||||
### 六、快速上手示例:egui-tree-view实现Notion风格页面树
|
||||
|
||||
```rust
|
||||
use egui::{CentralPanel, Context, TopBottomPanel};
|
||||
use egui_tree_view::{TreeView, TreeViewState};
|
||||
|
||||
struct PageNode {
|
||||
title: String,
|
||||
children: Vec<PageNode>,
|
||||
expanded: bool,
|
||||
}
|
||||
|
||||
impl PageNode {
|
||||
fn new(title: &str) -> Self {
|
||||
Self {
|
||||
title: title.to_string(),
|
||||
children: Vec::new(),
|
||||
expanded: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result<()> {
|
||||
let mut tree_state = TreeViewState::default();
|
||||
let mut root_node = PageNode::new("Notion Style Page Tree");
|
||||
root_node.children.push(PageNode::new("Getting Started"));
|
||||
root_node.children.push({
|
||||
let mut node = PageNode::new("Advanced Features");
|
||||
node.children.push(PageNode::new("Block Editing"));
|
||||
node.children.push(PageNode::new("Page Nesting"));
|
||||
node
|
||||
});
|
||||
|
||||
eframe::run_simple_native("Notion Page Tree Demo", Default::default(), move |ctx, _frame| {
|
||||
TopBottomPanel::top("menu").show(ctx, |ui| {
|
||||
ui.heading("Notion Style Editor");
|
||||
});
|
||||
|
||||
CentralPanel::default().show(ctx, |ui| {
|
||||
TreeView::new("page_tree", &mut tree_state)
|
||||
.with_nodes(&mut root_node.children, |ui, node, _depth| {
|
||||
ui.checkbox(&mut node.expanded, &node.title);
|
||||
node.expanded
|
||||
})
|
||||
.show(ui);
|
||||
});
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 七、总结
|
||||
|
||||
Rust生态中已有丰富的工具可构建Notion风格页面树,从底层数据结构到完整应用全覆盖。若需快速开发,可选择现成组件库或应用;若需高度定制,可基于树结构库+UI框架组合实现,充分发挥Rust的性能与安全优势。
|
||||
|
||||
需要我给你一份在 Tauri 中结合 Rust 后端与前端实现可拖拽 Notion 风格页面树的最小可运行示例吗?
|
||||
@@ -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/` 分层
|
||||
@@ -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.
|
||||
@@ -0,0 +1,399 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="zh-CN"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>个人 | The Digital Atelier</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"on-secondary-fixed-variant": "#003ea7",
|
||||
"on-surface": "#2d2e2e",
|
||||
"surface-tint": "#006e28",
|
||||
"primary": "#006e28",
|
||||
"on-secondary-container": "#fefcff",
|
||||
"inverse-surface": "#2f3031",
|
||||
"surface-container-high": "#f5f5f5",
|
||||
"on-surface-variant": "#5a5a5a",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-background": "#1b1c1c",
|
||||
"on-tertiary-container": "#2d2e2e",
|
||||
"surface-container-low": "#fafafa",
|
||||
"on-primary": "#ffffff",
|
||||
"surface": "#ffffff",
|
||||
"primary-container": "#31aa4d",
|
||||
"tertiary-fixed-dim": "#c7c6c6",
|
||||
"error-container": "#ffdad6",
|
||||
"secondary-container": "#286af9",
|
||||
"surface-bright": "#faf9f9",
|
||||
"surface-container-highest": "#e3e2e2",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-primary-fixed": "#002107",
|
||||
"secondary": "#0051d5",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-error-container": "#93000a",
|
||||
"error": "#ba1a1a",
|
||||
"on-primary-fixed-variant": "#00531c",
|
||||
"tertiary": "#5e5e5e",
|
||||
"on-primary-container": "#003610",
|
||||
"on-secondary-fixed": "#00174b",
|
||||
"secondary-fixed": "#dbe1ff",
|
||||
"inverse-on-surface": "#f2f0f0",
|
||||
"tertiary-fixed": "#e3e2e2",
|
||||
"outline": "#d1d5db",
|
||||
"surface-container": "#f7f7f7",
|
||||
"on-error": "#ffffff",
|
||||
"inverse-primary": "#69de7a",
|
||||
"background": "#ffffff",
|
||||
"surface-variant": "#f3f4f6",
|
||||
"primary-fixed-dim": "#69de7a",
|
||||
"surface-dim": "#dbdad9",
|
||||
"tertiary-container": "#959595",
|
||||
"secondary-fixed-dim": "#b4c5ff",
|
||||
"on-tertiary-fixed": "#1b1c1c",
|
||||
"on-tertiary-fixed-variant": "#464747",
|
||||
"primary-fixed": "#86fb93",
|
||||
"outline-variant": "#bdcab9",
|
||||
"wolai-selected": "#fce4e4"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "4px",
|
||||
"lg": "6px",
|
||||
"xl": "8px",
|
||||
"full": "9999px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"headline": ["Inter", "sans-serif"],
|
||||
"body": ["Inter", "sans-serif"],
|
||||
"label": ["Inter", "sans-serif"]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; -webkit-font-smoothing: antialiased; color: #37352f; }
|
||||
.material-symbols-outlined { font-size: 18px; vertical-align: middle; }
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
.sidebar-item:hover .item-actions { opacity: 1; }
|
||||
.triangle-toggle { transition: transform 0.2s; cursor: pointer; font-size: 14px !important; }
|
||||
.triangle-toggle.open { transform: rotate(90deg); }
|
||||
.block-spacing { margin-bottom: 2px; }
|
||||
|
||||
/* Sidebar View Switching */
|
||||
.view-content { display: none; }
|
||||
.view-active { display: block; }
|
||||
.tab-active { color: #2d2e2e; font-weight: 600; border-bottom: 2px solid #006e28; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-white text-on-surface font-body">
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
<!-- SideNavBar -->
|
||||
<aside class="flex flex-col h-full bg-[#f7f7f5] dark:bg-stone-950 w-60 border-r border-stone-200/50 z-30 select-none">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-2 mb-2 cursor-pointer transition-colors pt-2"><div class="flex flex-col w-full">
|
||||
<!-- Workspace Header -->
|
||||
<div class="flex items-center gap-2 px-3 py-2 cursor-pointer group">
|
||||
<div class="w-8 h-8 rounded bg-[#d3544e] flex items-center justify-center shrink-0 text-white font-medium text-lg">L</div>
|
||||
<span class="text-[17px] font-bold text-stone-800 truncate">liaibo的个人空间</span>
|
||||
<span class="material-symbols-outlined text-stone-600 text-[20px] ml-auto">expand_more</span>
|
||||
</div>
|
||||
<!-- Toolbar Icons -->
|
||||
<div class="flex items-center px-4 py-3 text-stone-600 w-full bg-[#f7f7f5]">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span class="material-symbols-outlined text-[24px] cursor-pointer hover:bg-stone-200/50 rounded-sm p-0.5">search</span>
|
||||
<span class="material-symbols-outlined text-[24px] cursor-pointer hover:bg-stone-200/50 rounded-sm p-0.5">account_tree</span>
|
||||
<span class="material-symbols-outlined text-[24px] cursor-pointer hover:bg-stone-200/50 rounded-sm p-0.5">bolt</span>
|
||||
<span class="material-symbols-outlined text-[24px] cursor-pointer hover:bg-stone-200/50 rounded-sm p-0.5">help</span>
|
||||
<span class="material-symbols-outlined text-[24px] cursor-pointer hover:bg-stone-200/50 rounded-sm p-0.5">inventory_2</span>
|
||||
<span class="material-symbols-outlined text-[24px] cursor-pointer hover:bg-stone-200/50 rounded-sm p-0.5">more_horiz</span>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
<!-- Main Tabs -->
|
||||
<nav class="flex-1 overflow-y-auto no-scrollbar px-2 space-y-0.5">
|
||||
<div class="mt-2">
|
||||
<!-- Starred Section -->
|
||||
<div class="flex items-center gap-1 px-3 py-1 text-stone-400 group cursor-pointer hover:bg-stone-200/50">
|
||||
<span class="material-symbols-outlined text-[16px] text-orange-400">star</span>
|
||||
<span class="text-[13px] font-medium text-stone-500">星标置顶</span>
|
||||
<span class="material-symbols-outlined text-[16px] ml-0.5">expand_more</span>
|
||||
</div>
|
||||
<div class="px-1 space-y-[1px] mt-1 mb-6">
|
||||
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
|
||||
<span class="text-stone-300 font-bold ml-1">·</span>
|
||||
<span class="material-symbols-outlined text-stone-500 text-[18px]">sticky_note_2</span>
|
||||
<span class="text-[14px]">记录</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
|
||||
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
|
||||
<span class="w-5 h-5 flex items-center justify-center overflow-hidden rounded-[2px]">
|
||||
<img alt="calendar" class="w-full h-full object-contain" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDPgwtm4gouecfvHJ5gaIuLbl9hm82rutzZUhlxiM8_jyV_QiMLixw_ZWHlStFPooQgE6NpMVdOKuuV3LrNJbAuLGA_WBjHnfhP0NhZxpuysUjt_unG-_rzrfEWr-kspQ2pzJqK9nROJOz0DkSHoP6F-Y5y94TegpSKGg8Q3sWp-g3qDNQzBV5pK7h6jU26EIY9UtZDzCOM0Vel8YpfBIRKt-BbyFvvaJiITXMNKHJNHr2bf6Ciry1Ms1mj-mdZ18bjVgp7eojOfyc"/>
|
||||
</span>
|
||||
<span class="text-[14px]">日报</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
|
||||
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
|
||||
<span class="material-symbols-outlined text-green-600 text-[20px]">account_balance_wallet</span>
|
||||
<span class="text-[14px]">阳阳钱包</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
|
||||
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
|
||||
<span class="w-5 h-5 flex items-center justify-center overflow-hidden rounded-[2px]">
|
||||
<img alt="calendar" class="w-full h-full object-contain" src="https://lh3.googleusercontent.com/aida-public/AB6AXuB_jLmyrcqa9V9ShAaaFQtg4-H2_dirQ6z7nw_0a0sJvU8dbeLxV1uHAlwUxg8cyvojv7beX8Y9faeT4SOHn0EBMsv5XUiOsAh735lJcVuY-Tlkwv3zH8BavU-fEnHpCvtSounrGJ1uooJInzjY37ypbdeHhkQbeGMBbzupSc_dQXzISN2XG31ug4s3_9u4fDfVxKg0J92Ap-50Kc7STQgQDe5m1rZo71_1I7oP5en9N3gLgWlaRCtq5o7dHzwB5WbZpJhYVTQZC4M"/>
|
||||
</span>
|
||||
<span class="text-[14px]">My LifeOS</span>
|
||||
</a>
|
||||
</div>
|
||||
<!-- Section with Tabs Toggle -->
|
||||
<div class="flex items-center justify-between px-3 py-1 mt-4">
|
||||
<div class="flex gap-4">
|
||||
<button class="text-[13px] font-medium text-stone-400 hover:text-stone-700 transition-colors tab-active cursor-pointer pb-0.5" id="tab-pages" onclick="switchSidebarView('pages')">我的页面</button>
|
||||
<button class="flex items-center gap-1 text-[13px] font-medium text-stone-400 hover:text-stone-700 transition-colors cursor-pointer pb-0.5" id="tab-files" onclick="switchSidebarView('files')">
|
||||
<span class="material-symbols-outlined text-[16px]">folder_open</span>
|
||||
Explorer
|
||||
</button>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-[16px] text-stone-400 ml-0.5 cursor-pointer">expand_more</span>
|
||||
</div>
|
||||
<!-- View: Page View (Default Wolai) -->
|
||||
<div class="view-content view-active px-1 space-y-[1px] mt-2" id="view-pages">
|
||||
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
|
||||
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
|
||||
<span class="material-symbols-outlined text-stone-800 text-[20px] font-bold">content_cut</span>
|
||||
<span class="text-[14px]">剪藏</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
|
||||
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
|
||||
<span class="material-symbols-outlined text-stone-800 text-[20px] font-variation-fill">home</span>
|
||||
<span class="text-[14px]">个人</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
|
||||
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
|
||||
<span class="material-symbols-outlined text-stone-800 text-[20px]">domain</span>
|
||||
<span class="text-[14px]">知识</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
|
||||
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
|
||||
<span class="material-symbols-outlined text-stone-800 text-[20px]">grid_view</span>
|
||||
<span class="text-[14px]">项目</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
|
||||
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
|
||||
<span class="material-symbols-outlined text-stone-800 text-[20px] font-variation-fill">book</span>
|
||||
<span class="text-[14px]">工作资料</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-2 px-3 py-1.5 text-stone-700 hover:bg-stone-200/60 rounded" href="#">
|
||||
<span class="material-symbols-outlined text-stone-400 text-[14px]">chevron_right</span>
|
||||
<span class="w-5 h-5 flex items-center justify-center overflow-hidden rounded-[2px]">
|
||||
<img alt="calendar" class="w-full h-full object-contain" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCPR9Mr19xWn9DuKwKZpoJ1tqeR0Y4FE1fMyBlvjZEYCkYOh1NowiLbxo7fSRF1cQBgV51cEwjMIB3HrjmsNtmvzYRdtnHDOse0Z77UzXg7ksBGpKrzJ48KxQ5wi-IKKzzqmc19EDDYRU997VaWrFWJdS2hI5MXjDqAOW9W1d8UfA2QRBmTtYhPfZr9dQb6jSnVaMxjNTchVEKJic6g8FSLbirEusH-O88rEdVehgQiLSu0NtV_QusN_BD98oEBqQt9L6VAiVy0byc"/>
|
||||
</span>
|
||||
<span class="text-[14px]">日常</span>
|
||||
</a>
|
||||
</div>
|
||||
<!-- View: File View (VS Code Style) -->
|
||||
<div class="view-content px-1 mt-2 mb-4" id="view-files">
|
||||
<!-- Folder: Public -->
|
||||
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600">
|
||||
<span class="material-symbols-outlined text-[16px] text-stone-400">expand_more</span>
|
||||
<span class="material-symbols-outlined text-[18px] text-amber-500">folder</span>
|
||||
<span class="text-[13px] font-medium">public</span>
|
||||
</div>
|
||||
<div class="ml-4 border-l border-stone-200 pl-1">
|
||||
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600">
|
||||
<span class="material-symbols-outlined text-[18px] text-blue-400">html</span>
|
||||
<span class="text-[13px]">index.html</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600">
|
||||
<span class="material-symbols-outlined text-[18px] text-indigo-400">image</span>
|
||||
<span class="text-[13px]">favicon.ico</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Folder: Src -->
|
||||
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600 mt-1">
|
||||
<span class="material-symbols-outlined text-[16px] text-stone-400">expand_more</span>
|
||||
<span class="material-symbols-outlined text-[18px] text-amber-500">folder</span>
|
||||
<span class="text-[13px] font-medium">src</span>
|
||||
</div>
|
||||
<div class="ml-4 border-l border-stone-200 pl-1">
|
||||
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600">
|
||||
<span class="material-symbols-outlined text-[18px] text-sky-500">javascript</span>
|
||||
<span class="text-[13px]">App.js</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600">
|
||||
<span class="material-symbols-outlined text-[18px] text-blue-500">css</span>
|
||||
<span class="text-[13px]">styles.css</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600">
|
||||
<span class="material-symbols-outlined text-[16px] text-stone-400">chevron_right</span>
|
||||
<span class="material-symbols-outlined text-[18px] text-amber-500">folder</span>
|
||||
<span class="text-[13px]">assets</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Root Files -->
|
||||
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600 mt-1">
|
||||
<span class="material-symbols-outlined text-[18px] text-stone-400">description</span>
|
||||
<span class="text-[13px]">README.md</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 px-3 py-0.5 hover:bg-stone-200/50 cursor-pointer rounded text-stone-600">
|
||||
<span class="material-symbols-outlined text-[18px] text-amber-600">settings</span>
|
||||
<span class="text-[13px]">package.json</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Footer -->
|
||||
<div class="mt-auto border-t border-stone-200">
|
||||
<div class="flex w-full border-t border-stone-200 bg-[#f7f7f5]">
|
||||
<button class="flex-1 flex items-center justify-center gap-2 py-3 border-r border-stone-200 hover:bg-stone-200/30 text-stone-600 transition-colors">
|
||||
<span class="material-symbols-outlined text-[20px]">delete</span>
|
||||
<span class="text-[14px]">垃圾箱</span>
|
||||
</button>
|
||||
<button class="flex-1 flex items-center justify-center gap-2 py-3 hover:bg-stone-200/30 text-stone-600 transition-colors">
|
||||
<span class="w-4 h-4">
|
||||
<svg fill="none" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L3 7V17L12 22L21 17V7L12 2Z" fill="#fcd34d" fill-opacity="0.6" stroke="#f59e0b" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
|
||||
<path d="M3 7L12 12L21 7" stroke="#f59e0b" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
|
||||
<path d="M12 22V12" stroke="#f59e0b" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="text-[14px]">模板中心</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main Content Area -->
|
||||
<main class="flex-1 overflow-y-auto bg-white relative">
|
||||
<!-- TopAppBar -->
|
||||
<header class="flex justify-between items-center px-4 h-[44px] w-full sticky top-0 bg-white/90 backdrop-blur-sm z-40">
|
||||
<div class="flex items-center gap-1.5 text-stone-500">
|
||||
<span class="material-symbols-outlined hover:bg-stone-100 p-1 rounded cursor-pointer" data-icon="menu">menu</span>
|
||||
<div class="flex items-center text-[13px] font-normal px-1">
|
||||
<span class="hover:bg-stone-100 px-1.5 py-0.5 rounded cursor-pointer">The Digital Atelier</span>
|
||||
<span class="mx-0.5 text-stone-300">/</span>
|
||||
<span class="hover:bg-stone-100 px-1.5 py-0.5 rounded cursor-pointer text-stone-900 font-medium flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-[16px]" data-icon="home">home</span>
|
||||
个人
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<div class="flex items-center px-2 py-1 bg-green-50 text-green-700 rounded-full text-[11px] font-medium mr-2 gap-1 border border-green-100">
|
||||
<span class="w-1.5 h-1.5 bg-green-500 rounded-full"></span>
|
||||
Public
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5 text-stone-500">
|
||||
<span class="material-symbols-outlined hover:bg-stone-100 p-1.5 rounded cursor-pointer" data-icon="star">star</span>
|
||||
<span class="material-symbols-outlined hover:bg-stone-100 p-1.5 rounded cursor-pointer" data-icon="history">history</span>
|
||||
<span class="material-symbols-outlined hover:bg-stone-100 p-1.5 rounded cursor-pointer text-green-600" data-icon="auto_awesome">auto_awesome</span>
|
||||
<span class="material-symbols-outlined hover:bg-stone-100 p-1.5 rounded cursor-pointer" data-icon="search">search</span>
|
||||
<span class="material-symbols-outlined hover:bg-stone-100 p-1.5 rounded cursor-pointer" data-icon="more_horiz">more_horiz</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Document Canvas -->
|
||||
<article class="max-w-4xl mx-auto pt-[80px] pb-40 px-12 md:px-24">
|
||||
<!-- Page Icon & Title -->
|
||||
<div class="mb-8">
|
||||
<div class="group relative inline-block mb-4">
|
||||
<span class="material-symbols-outlined text-[72px] text-stone-800 cursor-default" data-icon="home">home</span>
|
||||
</div>
|
||||
<h1 class="text-4xl font-bold text-stone-900 tracking-tight leading-tight mb-2">个人</h1>
|
||||
<div class="flex items-center gap-4 text-stone-400 text-[13px] border-b border-stone-100 pb-6 mb-8">
|
||||
<span class="flex items-center gap-1.5 hover:text-stone-600 cursor-pointer"><span class="material-symbols-outlined text-[16px]" data-icon="face">face</span> 个人空间</span>
|
||||
<span class="flex items-center gap-1.5"><span class="material-symbols-outlined text-[16px]" data-icon="calendar_today">calendar_today</span> 2024年5月20日</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Vertical Sub-page Links with Wolai Spacing -->
|
||||
<section class="mb-10 space-y-[2px]">
|
||||
<a class="group flex items-center gap-2 py-1 px-1 -ml-1 hover:bg-stone-100 rounded transition-colors text-stone-700" href="#">
|
||||
<span class="material-symbols-outlined text-stone-400 group-hover:text-primary" data-icon="description">description</span>
|
||||
<span class="text-[15px] font-normal underline decoration-stone-200 underline-offset-4">我的文件</span>
|
||||
</a>
|
||||
<a class="group flex items-center gap-2 py-1 px-1 -ml-1 hover:bg-stone-100 rounded transition-colors text-stone-700" href="#">
|
||||
<span class="material-symbols-outlined text-red-400" data-icon="filter_vintage">filter_vintage</span>
|
||||
<span class="text-[15px] font-normal underline decoration-stone-200 underline-offset-4">灵感库</span>
|
||||
</a>
|
||||
<a class="group flex items-center gap-2 py-1 px-1 -ml-1 hover:bg-stone-100 rounded transition-colors text-stone-700" href="#">
|
||||
<span class="material-symbols-outlined text-amber-500" data-icon="sensor_door">sensor_door</span>
|
||||
<span class="text-[15px] font-normal underline decoration-stone-200 underline-offset-4">工作台</span>
|
||||
</a>
|
||||
<a class="group flex items-center gap-2 py-1 px-1 -ml-1 hover:bg-stone-100 rounded transition-colors text-stone-700" href="#">
|
||||
<span class="material-symbols-outlined text-blue-500" data-icon="lock">lock</span>
|
||||
<span class="text-[15px] font-normal underline decoration-stone-200 underline-offset-4">私人保险箱</span>
|
||||
</a>
|
||||
<a class="group flex items-center gap-2 py-1 px-1 -ml-1 hover:bg-stone-100 rounded transition-colors text-stone-700" href="#">
|
||||
<span class="material-symbols-outlined text-green-500" data-icon="book">book</span>
|
||||
<span class="text-[15px] font-normal underline decoration-stone-200 underline-offset-4">阅读清单</span>
|
||||
</a>
|
||||
<a class="group flex items-center gap-2 py-1 px-1 -ml-1 hover:bg-stone-100 rounded transition-colors text-stone-700" href="#">
|
||||
<span class="material-symbols-outlined text-rose-500" data-icon="favorite">favorite</span>
|
||||
<span class="text-[15px] font-normal underline decoration-stone-200 underline-offset-4">健康追踪</span>
|
||||
</a>
|
||||
<a class="group flex items-center gap-2 py-1 px-1 -ml-1 hover:bg-stone-100 rounded transition-colors text-stone-700" href="#">
|
||||
<span class="material-symbols-outlined text-indigo-400" data-icon="laptop_mac">laptop_mac</span>
|
||||
<span class="text-[15px] font-normal underline decoration-stone-200 underline-offset-4">项目管理</span>
|
||||
</a>
|
||||
</section>
|
||||
<!-- Editorial Content Blocks -->
|
||||
<div class="space-y-6">
|
||||
<p class="text-[16px] text-stone-800 leading-[1.8]">
|
||||
欢迎来到你的数字工作室。这里是你整理思绪、捕获灵感并将其转化为现实的私人圣殿。每一个方块都承载着无限的可能性。
|
||||
</p>
|
||||
<div class="p-6 bg-[#f7f7f5] rounded-lg border-l-4 border-stone-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="material-symbols-outlined text-primary mt-0.5" data-icon="lightbulb">lightbulb</span>
|
||||
<div class="space-y-2">
|
||||
<h3 class="font-bold text-stone-800 text-[15px]">设计原则</h3>
|
||||
<p class="text-[14px] text-stone-600 leading-relaxed">
|
||||
我们追求的是“垂直节奏”。在你的数字化工坊中,呼吸感和平衡比任何繁杂的装饰都更为重要。请在此处记录你的第一条笔记。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-4 border-t border-stone-100">
|
||||
<div class="text-stone-300 text-[15px] cursor-text select-none py-1 hover:bg-stone-50 rounded px-1 -mx-1">
|
||||
输入 '/' 以插入块
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<!-- Floating Action Buttons -->
|
||||
<div class="fixed bottom-6 right-6 flex flex-col gap-2 z-50">
|
||||
<button class="bg-white border border-stone-200 text-stone-500 rounded-full w-10 h-10 flex items-center justify-center shadow-md hover:bg-stone-50 transition-all">
|
||||
<span class="material-symbols-outlined" data-icon="help">help</span>
|
||||
</button>
|
||||
<button class="bg-primary text-white rounded-full w-12 h-12 flex items-center justify-center shadow-lg hover:opacity-90 transition-all scale-100 active:scale-95">
|
||||
<span class="material-symbols-outlined" data-icon="auto_awesome" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script>
|
||||
function switchSidebarView(view) {
|
||||
const pagesView = document.getElementById('view-pages');
|
||||
const filesView = document.getElementById('view-files');
|
||||
const pagesTab = document.getElementById('tab-pages');
|
||||
const filesTab = document.getElementById('tab-files');
|
||||
|
||||
if (view === 'pages') {
|
||||
pagesView.classList.add('view-active');
|
||||
filesView.classList.remove('view-active');
|
||||
pagesTab.classList.add('tab-active');
|
||||
filesTab.classList.remove('tab-active');
|
||||
} else {
|
||||
filesView.classList.add('view-active');
|
||||
pagesView.classList.remove('view-active');
|
||||
filesTab.classList.add('tab-active');
|
||||
pagesTab.classList.remove('tab-active');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body></html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
+538
@@ -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` 与旧前端壳退场。**
|
||||
@@ -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`;稳定后再清旧壳,而不是反过来。**
|
||||
+526
@@ -0,0 +1,526 @@
|
||||
# 4-1 [recycle] Sidebar / 页面树 / 文件树 产品级差距分析 v1
|
||||
|
||||
> 更新时间:2026-04-17
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档不回答:
|
||||
|
||||
- “当前新树能不能先凑合继续用”
|
||||
- “再补一点样式是不是就够了”
|
||||
|
||||
这份文档回答的是:
|
||||
|
||||
> **当前 git 中已经接入的 Rust Web tree shell,距离“文件树对标 VS Code、页面树对标 Wolai / Notion”的产品目标还差什么,以及下一步应该如何推进。**
|
||||
|
||||
结论先固定:
|
||||
|
||||
- 当前方向没有跑偏
|
||||
- 当前实现仍然只是过渡 shell,不是产品级树控件
|
||||
- 下一步重点不是继续修饰过渡 shell,而是进入“产品级树域控件重建”
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前实现所处阶段
|
||||
|
||||
根据当前 git 中未提交改动,现状应被定义为:
|
||||
|
||||
- 已完成 tree shell 挂载位
|
||||
- 已完成 `mnote-web /tree` 路由与基本 command 回写
|
||||
- 已完成 Sidebar / 文件树 / picker 的渐进切流入口
|
||||
- 尚未完成产品级树交互能力重建
|
||||
|
||||
也就是说,当前完成的是:
|
||||
|
||||
- **协议验证**
|
||||
- **切流验证**
|
||||
- **最小可运行壳验证**
|
||||
|
||||
而不是:
|
||||
|
||||
- **VS Code 级文件树**
|
||||
- **Wolai / Notion 级页面树**
|
||||
|
||||
### 2.1 当前代码中已经成立的部分
|
||||
|
||||
- `wolai-frontend` 已经可以把树域挂到 `mnote-web tree shell`
|
||||
- `mnote-web` 已经可以输出基础 tree projection 并承接 create / rename / move
|
||||
- 新旧树之间已经有 feature flag 与 fallback
|
||||
|
||||
对应实现可参考:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/MnoteWebTreeShell.tsx`
|
||||
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/tree.rs`
|
||||
|
||||
### 2.2 当前实现不能被误判为“已完成重构”的原因
|
||||
|
||||
当前 `mnote-web tree shell` 仍然具有明显过渡壳特征:
|
||||
|
||||
- 通过 `iframe + postMessage + fallback` 接入主站
|
||||
- 壳内 UI 仍是手写 HTML / CSS / DOM 逻辑
|
||||
- 交互动作仍大量依赖 `prompt` / `alert`
|
||||
- 没有完成稳定的 row model / focus model / selection model / keyboard model / DnD state machine
|
||||
|
||||
这类实现适合做:
|
||||
|
||||
- 协议对齐
|
||||
- route 验证
|
||||
- 真机切流
|
||||
|
||||
不适合直接作为:
|
||||
|
||||
- 最终产品树控件
|
||||
|
||||
---
|
||||
|
||||
## 3. 现阶段的关键判断
|
||||
|
||||
### 3.1 方向没有错
|
||||
|
||||
当前路线与长期架构是一致的:
|
||||
|
||||
- 树真相继续下沉到 kernel / projection
|
||||
- 页面树与文件树继续作为 projection family
|
||||
- Sidebar 继续降级为树域承载壳
|
||||
|
||||
所以,“把树域逐步从旧 React 组件中剥离出来”这个方向是对的。
|
||||
|
||||
### 3.2 体验回退是阶段性真实现象
|
||||
|
||||
用户现在觉得新树在操作和界面上与目标产品差距很大,这个判断是正确的。
|
||||
|
||||
原因不是:
|
||||
|
||||
- 目标错了
|
||||
|
||||
而是:
|
||||
|
||||
- 当前切进去的是过渡 shell
|
||||
- 旧 React 树实际上已经沉淀了一部分成熟交互
|
||||
- 新 Rust Web 壳还没有把这些产品交互能力重新建起来
|
||||
|
||||
### 3.3 当前最危险的误区
|
||||
|
||||
当前最应该避免的,不是“改慢一点”,而是下面两个误区:
|
||||
|
||||
- 误区 A:把过渡 shell 继续当最终产品打磨
|
||||
- 误区 B:看到效果差,就退回“继续长期维持旧树 + 新协议双轨”
|
||||
|
||||
正确做法是:
|
||||
|
||||
- 承认当前只是过渡壳
|
||||
- 用它验证协议与切流
|
||||
- 然后进入产品级树控件重建
|
||||
|
||||
---
|
||||
|
||||
## 4. 当前实现与目标产品的差距矩阵
|
||||
|
||||
## 4.1 文件树:目标应对标 VS Code Explorer
|
||||
|
||||
这里的“对标”不是抄界面,而是对标:
|
||||
|
||||
- 行为模型
|
||||
- 信息密度
|
||||
- 交互反馈
|
||||
- 结构表达
|
||||
|
||||
### 当前已经具备的部分
|
||||
|
||||
- 已有文件树入口
|
||||
- 已有页面与附件的基础层级表达
|
||||
- 已有打开页面 / 打开附件的最小动作链路
|
||||
- 主站旧文件树中已沉淀部分 VS Code 风格拖放语义
|
||||
|
||||
### 当前缺失的关键能力
|
||||
|
||||
- 缺少稳定的文件树专用 row model
|
||||
- 缺少真实的 `filetree` 模式闭环
|
||||
- 缺少多选与连续选择
|
||||
- 缺少键盘导航
|
||||
- 缺少目录型拖放状态机
|
||||
- 缺少重命名内联编辑
|
||||
- 缺少右键菜单体系
|
||||
- 缺少 hover 工具动作
|
||||
- 缺少大树虚拟化与增量展开策略
|
||||
- 缺少图标语义与资源类型区分
|
||||
|
||||
### 当前与 VS Code 的本质差距
|
||||
|
||||
当前新壳更像:
|
||||
|
||||
- “能渲染树结构的调试页”
|
||||
|
||||
而 VS Code Explorer 是:
|
||||
|
||||
- “高密度、可键盘驱动、可多选、可拖放、可重命名、可上下文操作的文件资源管理器”
|
||||
|
||||
所以文件树下一阶段不应再以“补几个按钮”为目标,而应以“重建 Explorer 行为模型”为目标。
|
||||
|
||||
## 4.2 页面树:目标应对标 Wolai / Notion
|
||||
|
||||
这里的“对标”不是纯视觉复刻,而是对标:
|
||||
|
||||
- 页面层级表达方式
|
||||
- hover 操作节奏
|
||||
- 新建 / 展开 / 拖拽 / 上下文动作的一致性
|
||||
- 页面树作为知识库导航入口的轻量感
|
||||
|
||||
### 当前已经具备的部分
|
||||
|
||||
- 已有页面树 projection 主链
|
||||
- 已有基础展开 / 新建 / 重命名 / 上下移动作
|
||||
- 已有主站切流挂载位
|
||||
|
||||
### 当前缺失的关键能力
|
||||
|
||||
- 缺少 hover 暴露的轻量动作区
|
||||
- 缺少更细的页面类型 / 状态表达
|
||||
- 缺少更自然的层级拖拽交互
|
||||
- 缺少键盘导航与 focus 管理
|
||||
- 缺少右键菜单与上下文操作体系
|
||||
- 缺少行级局部状态管理
|
||||
- 缺少高密度列表的稳定渲染与滚动体验
|
||||
- 缺少与搜索、跳转、最近访问状态的联动边界
|
||||
|
||||
### 当前与 Wolai / Notion 的本质差距
|
||||
|
||||
当前新壳更像:
|
||||
|
||||
- “展示树数据并支持几个命令”
|
||||
|
||||
而 Wolai / Notion 页面树更接近:
|
||||
|
||||
- “低干扰导航器 + 轻量页面管理器”
|
||||
|
||||
所以页面树下一阶段的重心不是“增加更多按钮”,而是:
|
||||
|
||||
- 让操作默认隐藏、按需显现
|
||||
- 让层级结构更轻
|
||||
- 让拖拽、展开、新建、上下文菜单进入一致的节奏
|
||||
|
||||
## 4.3 Sidebar:目标不是单独对标某产品,而是成为稳定壳
|
||||
|
||||
在长期架构中,Sidebar 的任务不是持有树真相,而是:
|
||||
|
||||
- 承载 projection
|
||||
- 承载搜索入口
|
||||
- 承载快速切换
|
||||
- 承载少量工作区级操作
|
||||
|
||||
所以 Sidebar 的核心问题不是“左栏长什么样”,而是:
|
||||
|
||||
- 树域壳与主站其它能力之间的耦合是否被切干净
|
||||
|
||||
当前 Sidebar 仍然偏重,说明下一阶段除了树控件本身,还要继续做:
|
||||
|
||||
- Sidebar 壳职责收敛
|
||||
- 树域与其它面板解耦
|
||||
- 搜索 / AI / 树域三者的边界整理
|
||||
|
||||
---
|
||||
|
||||
## 5. 当前代码中的具体偏差
|
||||
|
||||
## 5.1 `filetree` 模式闭环仍不完整
|
||||
|
||||
当前主站已向新壳传入 `mode=\"filetree\"`,但 Rust route 与壳内脚本对模式的识别还没有完整闭环。
|
||||
|
||||
这意味着当前“文件树已切到新壳”不能简单视为已经完成。
|
||||
|
||||
这一点必须优先修正,因为它会直接影响:
|
||||
|
||||
- 文件树功能判断
|
||||
- 测试结果判断
|
||||
- 后续重构任务拆分
|
||||
|
||||
## 5.2 新壳仍是协议验证页,不是产品树控件
|
||||
|
||||
当前树壳使用:
|
||||
|
||||
- 手写 HTML 模板
|
||||
- 手写 DOM 生成树节点
|
||||
- 手写按钮动作
|
||||
|
||||
这在协议验证阶段是合理的,但不应继续长期积累。
|
||||
|
||||
如果继续在这一层追加:
|
||||
|
||||
- hover 细节
|
||||
- 菜单
|
||||
- 多选
|
||||
- DnD
|
||||
- keyboard
|
||||
|
||||
最终只会把过渡壳演化成难维护的第二套前端。
|
||||
|
||||
## 5.3 旧树的成熟交互能力尚未被系统迁移
|
||||
|
||||
旧文件树与旧页面树中,已经沉淀出一部分成熟能力:
|
||||
|
||||
- 文件树的拖放反馈
|
||||
- 文件树的多选与内部拖放
|
||||
- 页面树的虚拟化
|
||||
- 页面树的拖拽排序
|
||||
|
||||
这些能力现在还没有以“协议化行为模型”的方式迁移进新树域,而是仍然留在旧 React 实现里。
|
||||
|
||||
这意味着下一阶段不能只盯新壳,还要做一件关键工作:
|
||||
|
||||
- 把旧树中已经证明有效的交互经验抽象成正式产品合同
|
||||
|
||||
---
|
||||
|
||||
## 6. 下一阶段应如何指导修改
|
||||
|
||||
## 6.1 原则一:停止把过渡 shell 当最终实现打磨
|
||||
|
||||
接下来不应继续以如下方式推进:
|
||||
|
||||
- “再补几个按钮”
|
||||
- “再修一版样式”
|
||||
- “再把这个 iframe 页面做像一点”
|
||||
|
||||
这些动作只能缓解表面问题,不能得到产品级树控件。
|
||||
|
||||
正确方式是:
|
||||
|
||||
- 把当前壳明确标记为过渡验证层
|
||||
- 只修协议、切流、阻塞性错误
|
||||
- 不在这里继续堆复杂交互
|
||||
|
||||
## 6.2 原则二:先冻结产品交互合同,再进入 Rust Web 正式实现
|
||||
|
||||
下一阶段首先要做的,不是直接写更多 UI,而是冻结两份合同:
|
||||
|
||||
- 文件树产品交互合同
|
||||
- 页面树产品交互合同
|
||||
|
||||
这两份合同至少应明确:
|
||||
|
||||
- 行模型
|
||||
- 层级缩进规则
|
||||
- active / selected / focused / dragging 的区别
|
||||
- hover 暴露策略
|
||||
- 右键菜单入口
|
||||
- 键盘导航规则
|
||||
- 多选规则
|
||||
- 拖放语义
|
||||
- 内联重命名规则
|
||||
- 空白区与容器区行为
|
||||
|
||||
## 6.3 原则三:旧树不是要照抄,而是要提炼成熟行为
|
||||
|
||||
当前仓库里旧树实现仍然有直接参考价值:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/file-tree.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/private-tree.tsx`
|
||||
|
||||
它们的价值不在于:
|
||||
|
||||
- 继续长期保留 React 版本
|
||||
|
||||
而在于:
|
||||
|
||||
- 可以作为“已在当前产品中验证过的交互基线”
|
||||
|
||||
下一阶段应从它们中提炼:
|
||||
|
||||
- 哪些交互是必须保留的
|
||||
- 哪些只是过渡做法
|
||||
- 哪些要升级成正式协议字段或 UI 状态机
|
||||
|
||||
## 6.4 原则四:文件树与页面树应共用一套树行为骨架
|
||||
|
||||
下一阶段不要再走两套完全独立实现。
|
||||
|
||||
应采用:
|
||||
|
||||
- 同一套 tree row model
|
||||
- 同一套 selection / focus / keyboard / drag state
|
||||
- 同一套 command dispatch
|
||||
|
||||
然后在 projection 层区分:
|
||||
|
||||
- `page_tree`
|
||||
- `file_tree`
|
||||
|
||||
这样才能保持:
|
||||
|
||||
- 文件树不是孤立系统
|
||||
- 页面树不是孤立系统
|
||||
- 两者继续服从 `tree-first graph kernel`
|
||||
|
||||
## 6.5 原则五:最终目标应是产品级 Rust Web tree shell,而不是长期 iframe 壳
|
||||
|
||||
长期上,目标应当是:
|
||||
|
||||
- Rust Web 正式树控件
|
||||
- 直接消费 projection protocol
|
||||
- 直接发 command protocol
|
||||
- 主站以内嵌挂载或原生整合方式承载
|
||||
|
||||
而不是:
|
||||
|
||||
- 长期保留 `iframe + postMessage + fallback` 作为正式方案
|
||||
|
||||
`iframe` 在当前阶段的价值是:
|
||||
|
||||
- 降风险切流
|
||||
- 降低对主站的侵入
|
||||
|
||||
它不应成为最终交付形态。
|
||||
|
||||
---
|
||||
|
||||
## 7. 建议采用的参考体系
|
||||
|
||||
## 7.1 最值得参考:行为模型参考
|
||||
|
||||
优先级最高的不是某个“现成树组件”,而是行为模型参考。
|
||||
|
||||
推荐优先参考:
|
||||
|
||||
- `headless-tree`
|
||||
|
||||
主要借鉴内容:
|
||||
|
||||
- row model
|
||||
- selection
|
||||
- focus
|
||||
- keyboard
|
||||
- drag and drop 状态拆分
|
||||
|
||||
这类参考最适合解决:
|
||||
|
||||
- 为什么树一旦复杂就开始失控
|
||||
- 为什么文件树与页面树经常需要重复写交互
|
||||
|
||||
## 7.2 产品结构参考
|
||||
|
||||
推荐参考:
|
||||
|
||||
- `AppFlowy`
|
||||
- `AFFiNE`
|
||||
|
||||
主要借鉴内容:
|
||||
|
||||
- Sidebar 与页面树的产品层边界
|
||||
- 工作区 / 页面 / 文档之间的协作关系
|
||||
- 页面树如何作为知识产品的导航层
|
||||
|
||||
不建议直接借:
|
||||
|
||||
- 大量样式实现细节
|
||||
- 与当前架构不一致的事实源模型
|
||||
|
||||
## 7.3 Rust Web UI 参考
|
||||
|
||||
推荐参考:
|
||||
|
||||
- `Leptos`
|
||||
- `radix-leptos`
|
||||
- `thaw`
|
||||
|
||||
主要用途:
|
||||
|
||||
- 实现正式 Rust Web tree shell
|
||||
- 补齐 collapsible / menu / scroll area / overlay 等 primitives
|
||||
|
||||
## 7.4 不建议作为主导参考的对象
|
||||
|
||||
以下可以看,但不应成为主导路线:
|
||||
|
||||
- 终端树组件
|
||||
- 桌面 GUI 树组件
|
||||
- 纯列浏览器方案
|
||||
|
||||
原因是它们无法直接回答当前最核心的问题:
|
||||
|
||||
- 如何在 Web 主站中实现产品级页面树 / 文件树
|
||||
|
||||
---
|
||||
|
||||
## 8. 下一阶段的任务拆分建议
|
||||
|
||||
## 8.1 阶段 A:修正当前过渡壳中的协议闭环问题
|
||||
|
||||
只处理阻塞项:
|
||||
|
||||
- 修正 `filetree` 模式闭环
|
||||
- 校正 projection / mode / consumer 对应关系
|
||||
- 补齐最小测试
|
||||
|
||||
这一阶段不做:
|
||||
|
||||
- 大规模 UI 打磨
|
||||
- 新交互堆叠
|
||||
|
||||
## 8.2 阶段 B:产出页面树 / 文件树产品交互合同
|
||||
|
||||
必须单独落文档,至少包含:
|
||||
|
||||
- 文件树对标 VS Code 的功能矩阵
|
||||
- 页面树对标 Wolai / Notion 的功能矩阵
|
||||
- 当前已实现 / 未实现 / 不做 的判定
|
||||
- 对 projection 和 command 的新增要求
|
||||
|
||||
## 8.3 阶段 C:实现正式 tree row model 与状态骨架
|
||||
|
||||
这一阶段要优先完成:
|
||||
|
||||
- row model
|
||||
- selection model
|
||||
- focus model
|
||||
- keyboard model
|
||||
- drag state
|
||||
- context menu entry model
|
||||
|
||||
这一层最好先独立,再挂 UI。
|
||||
|
||||
## 8.4 阶段 D:实现产品级 Rust Web tree shell
|
||||
|
||||
这一阶段才进入正式 UI:
|
||||
|
||||
- 页面树 renderer
|
||||
- 文件树 renderer
|
||||
- picker renderer
|
||||
- 统一 shell 挂载
|
||||
|
||||
## 8.5 阶段 E:收敛旧树实现
|
||||
|
||||
最后再做:
|
||||
|
||||
- 旧文件树 helper 清理
|
||||
- 旧页面树 helper 清理
|
||||
- 旧 Sidebar 树域状态清理
|
||||
- 旧 consumer 收口
|
||||
|
||||
---
|
||||
|
||||
## 9. 最终结论
|
||||
|
||||
当前实现与设计之间,不存在“方向性错误”,但存在明显的“阶段性落差”。
|
||||
|
||||
这个落差的本质不是:
|
||||
|
||||
- 少几个按钮
|
||||
- 样式还不够像
|
||||
|
||||
而是:
|
||||
|
||||
- 当前完成的是树域过渡 shell
|
||||
- 目标要求的是产品级树控件系统
|
||||
|
||||
因此,接下来应明确口径:
|
||||
|
||||
- 当前 git 中的新树实现,定义为**过渡验证层**
|
||||
- 下一阶段任务,定义为**产品级树域控件重建**
|
||||
|
||||
只有这样,团队后续的修改方向才不会继续发散。
|
||||
@@ -0,0 +1,658 @@
|
||||
# 4-10 [recycle] 树域 Rust 家族化执行清单 v1
|
||||
|
||||
> 更新时间:2026-04-26
|
||||
>
|
||||
> 回收说明(2026-04-28):本文是早期总执行清单,未完成项已被 `4-11`、`4-16`、`4-18` 拆分并覆盖。本文保留为历史执行口径,不再作为当前 `process` 入口;后续以 `design/04-tree-domain/process/4-16-tree-rust-family-remaining-final-runtime-checklist-v1.md` 及 `done/4-11`、`done/4-18` 为准。
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-9-tree-rust-family-cutover-remaining-architecture-and-capability-preservation-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-11-tree-rust-family-final-renderer-and-host-thinning-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份清单只服务一件事:
|
||||
|
||||
> **把树域从“Rust 持有语义、React 持有主执行面”的过渡态,推进到“Rust 家族同时持有语义与主执行面”的下一阶段。**
|
||||
|
||||
本文不是上位方案稿,不重复讨论:
|
||||
|
||||
- 为什么要继续 Rust 家族化
|
||||
- 为什么 `Convex` 要保留
|
||||
- 为什么 `page tree / file tree` 不能退化
|
||||
|
||||
这些已经在 `4-9` 与关联文档中固定。
|
||||
|
||||
本文只给:
|
||||
|
||||
- 分阶段执行顺序
|
||||
- 每阶段 checklist
|
||||
- 每阶段阻塞项
|
||||
- 每阶段验收口径
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前基线
|
||||
|
||||
当前默认真实状态(2026-04-25):
|
||||
|
||||
- 树域 truth 已收口到 `tree-first graph kernel`
|
||||
- `Convex` 继续提供存储、同步、内容与实时底座
|
||||
- `mnote-web` 已具备 projection / command / transport 基础出口
|
||||
- `page tree / file tree / picker` 已开始消费 projection family
|
||||
- 主站默认 `treeRendererFamily` 已切到 `rust_family`
|
||||
- `page tree / file tree / picker` 默认主路径已进入 same-origin iframe compat host
|
||||
- 但当前切主的是 `compat host`,不是 final Rust renderer;宿主与 fallback 仍保留
|
||||
|
||||
因此本清单的目标不是“再做一轮 projection 收口”,而是:
|
||||
|
||||
- 补齐 Rust 侧剩余契约
|
||||
- 迁掉主路径 React 树 renderer
|
||||
- 保住 `page tree / file tree` 现有能力
|
||||
|
||||
---
|
||||
|
||||
## 3. 总体执行顺序
|
||||
|
||||
建议固定为下面五段:
|
||||
|
||||
1. `Phase A`:补齐 `file_tree` Rust 直接输出
|
||||
2. `Phase B`:完成 `tree.*` 命令主路径切换
|
||||
3. `Phase C`:补齐树域正式 realtime contract
|
||||
4. `Phase D`:迁移 `page tree` Rust 家族 renderer
|
||||
5. `Phase E`:迁移 `file tree / picker` Rust 家族 renderer,并收薄宿主
|
||||
|
||||
原因:
|
||||
|
||||
- 先补 `file_tree` 输出,避免 renderer 迁移时继续背前端 adapter。
|
||||
- 先收命令与 realtime,避免 UI 壳迁完后还卡在旧协议。
|
||||
- `page tree` 先迁,能先验证主行 renderer 与交互骨架。
|
||||
- `file tree` 后迁,避免最复杂对象面成为第一块落地风险点。
|
||||
|
||||
---
|
||||
|
||||
## 4. Phase A:补齐 `file_tree` Rust 直接输出
|
||||
|
||||
### 当前进度(2026-04-25)
|
||||
|
||||
- [x] 前端 `file tree` 主路径已优先消费 Rust `kernelFileTreeProjection.items`
|
||||
- [x] `/api/sidebar`、SSR loader、`/api/mnote-web/stream` 已补 `kernelFileTreeProjection`
|
||||
- [x] 非过滤主路径不再默认依赖 `pageRows + assetsByDoc` 拼装
|
||||
- [x] `file tree` 同源 host 的 inline override 已收口到 `kernel file_tree items`
|
||||
- `inlineFileTreeRows` 旧注入边界已移除
|
||||
- `TreeShellIframeHost` 不再接受 `FileTreeRow[]` 作为正式 inline renderer 输入
|
||||
- [x] 搜索过滤态不再回退 `pageRows + assets` 二次重建
|
||||
- `buildVisibleRows` 缺少 `kernel file_tree items` 时稳定返回空列表
|
||||
- 旧 `filterKernelFileTreeProjectionItems` 宿主裁剪 helper 已移除
|
||||
- 正式 Rust 搜索 projection 已在 `4-11 Phase H` 接入 3000 同源主链
|
||||
|
||||
### 目标
|
||||
|
||||
让 `file_tree` 不再依赖当前前端在 `pageRows + assets` 上继续拼主路径可见行语义。
|
||||
|
||||
### checklist
|
||||
|
||||
- [x] 盘点当前 `file_tree` 仍由前端 adapter 补齐的对象语义:
|
||||
- `index.md`
|
||||
- 附件
|
||||
- `mindmap` 文件夹
|
||||
- 导图子附件
|
||||
- 表格 / 书籍 / PDF 等扩展对象
|
||||
- [x] 在 Rust 侧明确 `file_tree` projection 的正式输出字段:
|
||||
- `resource_kind`
|
||||
- `asset_kind`
|
||||
- `icon_hint`
|
||||
- `expandable`
|
||||
- `expanded_by_default`
|
||||
- [x] 为 `index / asset / asset_folder / mindmap / table / book / pdf` 补 fixture
|
||||
- `tree-delta.test.ts` 已覆盖 delta 重建后的 `mindmap / pdf / book / table`
|
||||
- `route.test.ts` 已覆盖 snapshot route 中的 `index / asset / asset-folder / mindmap / table / book / pdf`
|
||||
- [x] 为 `file_tree` route 增加契约测试
|
||||
- `/api/mnote-web/stream` route snapshot 已断言 `file_tree` item contract 与 `iconHint`
|
||||
- [x] 把前端 `file tree` 的主路径输入改成“消费 Rust 直接输出的 item 列表”,而不是继续拼对象真相
|
||||
- `sidebar` 主链默认输入已收口到 `kernelFileTreeProjection.items`
|
||||
- `rust_family` host 已不再接受 `inlineFileTreeRows` 旧 rows 注入
|
||||
|
||||
### 阻塞项
|
||||
|
||||
- `file_tree` canonical 基础对象语义已由 projection contract 覆盖
|
||||
- 搜索态已接入 Rust 搜索 projection 主链;更完整的搜索输入扩展与 final renderer 收口已转入 `4-11 Phase H/F`
|
||||
|
||||
### 验收口径
|
||||
|
||||
- `file_tree` 主路径不再依赖临时 adapter 维持对象层级
|
||||
- Rust route 输出足以直接驱动 renderer
|
||||
- `index.md / asset-folder / asset` 行语义在 fixture 与 route test 中稳定存在
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase B:完成 `tree.*` 命令主路径切换
|
||||
|
||||
### 当前进度(2026-04-25)
|
||||
|
||||
- [x] 已确认正式命令名:
|
||||
- `create -> tree.node.create`
|
||||
- `rename -> tree.node.rename`
|
||||
- `move -> tree.subtree.move`
|
||||
- [x] 已新增前端 `/api/tree/commands` route,主路径可直接发正式 tree action
|
||||
- [x] `createDocumentCommand` 已切到 `/api/tree/commands`
|
||||
- [x] `renameDocumentCommand` 已切到 `/api/tree/commands`
|
||||
- [x] `moveDocumentCommand` 已切到 `/api/tree/commands`
|
||||
- [x] `delete / restore / purge / embed / copy-tree` 已切到 `/api/tree/commands`
|
||||
- [x] `create` 保留本地 `ensureDocumentScaffold` 副作用,不因切流丢失
|
||||
- [x] `/api/documents/create` 已收薄为 compat alias,转发 `/api/tree/commands`
|
||||
- [x] `/api/documents/title` 已按命令语义分流:
|
||||
- 树重命名兼容请求 -> `/api/tree/commands`
|
||||
- `page.head.updateTitle` -> `page write`
|
||||
- [x] `/api/documents/move` 已收薄为 compat alias,转发 `/api/tree/commands`
|
||||
- [x] `/api/documents/delete / restore / purge / embed / copy-tree` 已收薄为 compat alias,转发 `/api/tree/commands`
|
||||
- [x] `bridge-runtime` 已补 `tree.node.archive / restore / purge / embed` 与 `tree.subtree.copy` 别名执行计划
|
||||
- [x] `tree.*` 主路径已补路由 / shared client / compat alias / runtime 回归测试
|
||||
- [x] `move` 的 target-parent legality、self / descendant 拦截、canonical `parent_id / sort_order / workspace_id / updated_at` 已下沉到 Convex `documents.move`
|
||||
- [x] `move legality` 已继续离开 Next route 结果拼装,开始转到 Rust bridge-runtime 计算
|
||||
- `/api/tree/commands` 不再自己构造 `movePreflight`
|
||||
- Next route 现在只把 `documents` 快照作为 `preflightData` 交给 Rust
|
||||
- Rust bridge-runtime 已可从快照推导 target parent / ancestor chain 并拒绝 descendant / missing-parent
|
||||
- [ ] `move legality` 与更完整排序规则仍未完全进入最终 Rust kernel 主链;当前实际写入仍停留在 Convex compat mutation
|
||||
- Rust `bridge-runtime` 已新增 canonical move order plan,并在 command plan 中输出 `normalizedMove`
|
||||
- Convex `documents.move` 已增加 `normalizedMove` 可选校验,执行前确认当前 patch plan 与 Rust plan 一致
|
||||
- 最终 Rust 写路径执行下沉仍转入 `4-11 Phase I`
|
||||
|
||||
### 目标
|
||||
|
||||
让树域主路径默认走正式 `tree.*` 命令面,`documents.*` 退到 compat。
|
||||
|
||||
### checklist
|
||||
|
||||
- [x] 盘点当前主路径仍在调用的 `documents.*` 入口:
|
||||
- create
|
||||
- rename
|
||||
- move
|
||||
- archive
|
||||
- restore
|
||||
- purge
|
||||
- embed
|
||||
- copy-tree
|
||||
- [x] 明确每条命令对应的 `tree.*` 正式名
|
||||
- [x] 前端 shared command client 默认发 `tree.*`
|
||||
- [x] `documents.*` 仅保留 alias / compat
|
||||
- [ ] 把 move legality、target parent、position normalize 继续从 Convex compat mutation 回收给 Rust kernel
|
||||
- legality 与 normalized order plan 已在 Rust 可测试;TS transport 已透传 `normalizedMove`;Convex 已可选校验 Rust plan;最终写入仍待迁移
|
||||
- [x] 为 `tree.*` 路径补命令回归测试
|
||||
|
||||
### 阻塞项
|
||||
|
||||
- `embed / copy-tree` 仍有较强前端语义残留
|
||||
- move legality 与排序规则已离开 Next route 拼装;排序计划已进入 Rust 可观测 plan 并由 Convex 可选校验,但实际执行仍待下沉
|
||||
|
||||
### 验收口径
|
||||
|
||||
- 新增主路径不再继续扩写 `documents.*`
|
||||
- 主调用路径默认发 `tree.*`
|
||||
- 组件层不再承担长期树命令语义解释
|
||||
|
||||
---
|
||||
|
||||
## 6. Phase C:补齐树域正式 realtime contract
|
||||
|
||||
### 当前进度(2026-04-24)
|
||||
|
||||
- [x] `tree stream` 前端主链已从“freshness 仲裁”收口到“stream live 优先,query/fallback 兜底”
|
||||
- [x] `usePreferredSidebarSnapshot` 已按 `treeStream.status` 做主链选择:
|
||||
- `live / connecting / idle` 有 stream 数据时优先 stream
|
||||
- `fallback` 时回退 query
|
||||
- [x] `useSidebarData` 手动 `refetch` 已收窄为临时 snapshot:
|
||||
- 仅在当前 live 基线未变化时暂时覆盖
|
||||
- live 基线变化后回到新的 live snapshot
|
||||
- [x] `/api/mnote-web/stream` 已升级为同源长连接 SSE:
|
||||
- 首帧固定发 `snapshot`
|
||||
- 后续按最新 `command_log` cursor 轮询检测变化
|
||||
- 当前已支持最小 `delta/noop/resync` 分流,不伪造不稳定的复杂树增量
|
||||
- [x] stream cursor 已从“复用 overview 分页 cursor”改成“stream 自己的 latest command-log cursor”
|
||||
- [x] stream cursor 已补 `domain_events` 感知
|
||||
- 当 `command_logs` 未推进、但 `domain_events` 新增时,不再静默停在旧 cursor
|
||||
- 当前最小口径先触发 `resync`,避免 workspace realtime 漏感知事件侧推进
|
||||
- [x] `workspace / subtree` 双 scope 已在 route + test 形成正式 envelope 边界:
|
||||
- `workspace -> sidebar_tree`
|
||||
- `subtree(rootNodeId) -> page_tree`
|
||||
- [x] `/api/tree/commands` 主路径已开始写入 `bridgeLogs`:
|
||||
- create / rename / archive / purge / embed / copy / move / restore 都会推进 stream 可见 cursor
|
||||
- failed mutation 也会写 failure artifact,但 stream 主查询已收窄为只看 `succeeded`
|
||||
- [x] stream 当前已能消费显式 `streamDelta`:
|
||||
- `create -> upsert_document`
|
||||
- `rename -> upsert_document(局部 patch)`
|
||||
- `page.head.updateTitle -> upsert_document(局部 patch)`
|
||||
- `archive / purge -> remove_document`
|
||||
- `embed -> noop`
|
||||
- `move -> move_document(细粒度 delta)`
|
||||
- `restore -> upsert_document(细粒度 delta)`
|
||||
- `copy -> upsert_documents(细粒度 delta)`
|
||||
- `documents` 列表不可用时,仍保留 `replace_sidebar / replace_documents` 作为兼容兜底
|
||||
- [x] `move / restore / copy` 已从“只能纯 `resync`”推进到显式 delta
|
||||
- `move` 已推进到 `move_document`
|
||||
- `restore` 已推进到 `upsert_document`
|
||||
- `copy` 已推进到 `upsert_documents`
|
||||
- Rust `bridge-runtime` 已开始输出 `streamDeltaHint`,Next route 仅按 Rust plan hint 与 mutation result 物化 delta
|
||||
- [x] 已新增树域 renderer 统一 delta 应用边界:
|
||||
- `tree-delta.ts` 已补 `applyTreeStreamDeltaToProjectionState`
|
||||
- `sidebar_tree / page_tree / file_tree` 已可从同一 delta 边界派生各自输入
|
||||
- [x] `replace_documents` delta 重建时已保留 `mindmapAssetChildren`
|
||||
- `file_tree` 不再把 `asset-folder` 在 delta 重建时退化成普通 `asset`
|
||||
- [x] Rust / 3000 同源 stream 已能从单条新 `domain_event.payload.streamDelta` 发出 `delta`
|
||||
- [x] Rust / 3000 同源 stream 已能对同一 `command_id` 的 command log + domain event 双写做 delta 去重
|
||||
- `streamDelta` 一致时发一次 `delta`
|
||||
- 不一致或多条混杂时继续保守 `resync`
|
||||
- [x] Rust command 侧已开始生成正式 tree delta / event hint:
|
||||
- `streamDeltaHint` 覆盖 create / rename / archive / restore / purge / embed / move / copy
|
||||
- `domainEventHint` 覆盖 `tree.node.created / renamed / archived / restored / purged / embedded / duplicated` 与 `tree.subtree.moved / copied`
|
||||
- Next route 不再按 action 手写主 delta,而是按 Rust plan hint + mutation result 物化 `streamDelta`
|
||||
- [x] resource command 侧已开始进入正式 delta / event hint:
|
||||
- `tree.resource.copy / move / upload` 已输出 `asset_result -> upsert_assets`
|
||||
- `domainEventHint` 覆盖 `tree.resource.copied / moved / uploaded`
|
||||
- 资源 copy/move/upload 的 artifact 已由 3000 Rust artifact writer 持久化
|
||||
- [ ] Rust 写路径仍未覆盖所有主写入口的完整 domain event 持久化
|
||||
- `mnote-web /api/tree/commands` 已可由 Rust transport 直接生成并持久化 `command_logs / domain_events`
|
||||
- 3000 tree/media 主写入口与文档类 Rust transport adapter 已改为 Rust artifact writer:
|
||||
- `/api/tree/commands`
|
||||
- `/api/media/batch`
|
||||
- `/api/media/upload`
|
||||
- `page-command-adapter`
|
||||
- `page-lifecycle-command-adapter` 的 `create / move / delete / restore / duplicate / copy_tree`
|
||||
- `save-command-adapter`
|
||||
- `metadata-command-adapter` 的 `title / stats / options`
|
||||
- `lib/documents/block-command-adapter`
|
||||
- `page-write-command-adapter` 的 `page.head.updateTitle / page.layout.updateOptions / page.body.save`
|
||||
- 当前显式 adapter 层已不再直接调用 TS `recordBridgeCommandArtifacts(...)`
|
||||
- [ ] `3000` 仍是浏览器入口,但 polling transport 主体已在 Rust 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 家族。**
|
||||
+514
@@ -0,0 +1,514 @@
|
||||
# 4-9 [recycle] 树域 Rust 家族化剩余架构事项与能力保留方案 v1
|
||||
|
||||
> 更新时间:2026-04-23
|
||||
>
|
||||
> 回收说明(2026-04-28):本文是 Rust 家族化下一阶段的早期架构拆解稿,后续已由 `4-10` 执行清单、`4-11` final renderer / host thinning、`4-16` remaining final runtime、`4-18` final DOM shell hard gate 继续拆分和覆盖。本文保留为历史能力保留口径,不再作为当前活跃 `process` 入口。
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-7-tree-shell-ui-state-boundary-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档只回答两个问题:
|
||||
|
||||
1. 在当前长期方向已经固定为 `Rust kernel + Rust web + Leptos island + Convex substrate` 的前提下,树域继续朝 Rust 家族化推进,还剩哪些真正的架构事项。
|
||||
2. 在 `page tree / file tree` 从当前 `TypeScript + Next + React` 主壳向 Rust 家族执行面迁移时,哪些现有能力必须被完整保留,不能为了“换语言”而退化。
|
||||
|
||||
这份文档不是为了重复证明:
|
||||
|
||||
- `Convex` 是否还保留
|
||||
- 树域是否已经具备 projection contract
|
||||
- `Page Aggregate` 是否应该存在
|
||||
|
||||
这些上位结论已经在关联文档中固定。
|
||||
|
||||
本文只做当前阶段最需要的补充收口:
|
||||
|
||||
> **树域已经完成 truth / projection / command 边界的第一轮收口,但主渲染壳仍主要停留在 `TypeScript + Next + React`。下一阶段要解决的,不再是“树是不是 kernel projection”,而是“树域主执行面怎样继续向 Rust 家族迁移,同时不丢掉现有交互能力”。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前判断
|
||||
|
||||
## 2.1 已经成立的部分
|
||||
|
||||
当前已经成立且不应回退的事实:
|
||||
|
||||
- `tree-first graph kernel` 继续是树域真相层。
|
||||
- `Convex` 继续承担存储、实时同步、内容与协作底座。
|
||||
- `mnote-web` 已经具备树域 projection / command / transport 的正式入口能力。
|
||||
- `sidebar_tree / page_tree / file_tree` 已经形成同一 projection family。
|
||||
- 树域主路径不应再回到“前端自己定义第二套树真相”。
|
||||
|
||||
也就是说,当前真正成立的结构是:
|
||||
|
||||
- `Rust kernel` 持有树语义。
|
||||
- `Convex` 提供 substrate。
|
||||
- `Rust web` 已开始承接 route / projection / command / stream。
|
||||
- 当前主站中的树 consumer 已经开始消费 projection family。
|
||||
|
||||
## 2.2 仍未完成的部分
|
||||
|
||||
当前仍未完成、且正是下一阶段主任务的部分是:
|
||||
|
||||
- `page tree / file tree / picker` 的主渲染壳仍主要是 `TypeScript + Next + React`。
|
||||
- `file_tree` 的一部分 row 语义仍由前端 adapter 继续补齐,而不是完全由 Rust 直接输出。
|
||||
- `tree.*` 的长期命令面虽已冻结,但主调用路径仍未完全退出 `documents.*` 兼容入口。
|
||||
- 树域 realtime 仍未完全收口为 Rust Web 正式 `snapshot + delta + resync` 主链。
|
||||
- Sidebar 整体仍是超大客户端壳,树域虽然已收 truth,但执行面与宿主边界还不够薄。
|
||||
|
||||
因此当前正确判断不是:
|
||||
|
||||
- “树域 Rust 家族化已经完成”
|
||||
|
||||
而是:
|
||||
|
||||
> **树域已经完成“语义与协议收口”,但还没有完成“主执行面 Rust 家族化”。**
|
||||
|
||||
---
|
||||
|
||||
## 3. 为什么继续 Rust 家族化有真实意义
|
||||
|
||||
## 3.1 不是语言洁癖,而是减少第二套运行时语义
|
||||
|
||||
如果长期保持下面这种结构:
|
||||
|
||||
- Rust 负责 kernel / query / command / projection
|
||||
- Next/React 继续负责主树 UI、主交互壳、主状态骨架
|
||||
|
||||
那么树域会长期同时维护两套复杂度:
|
||||
|
||||
1. Rust 侧的 projection / command / stream 语义
|
||||
2. 前端壳里的 row model / selection / focus / keyboard / DnD / fallback / adapter 语义
|
||||
|
||||
这会带来:
|
||||
|
||||
- 协议在两侧重复解释
|
||||
- 调试时要同时跨 TS / Rust 两套执行面
|
||||
- tree shell 的边界始终无法真正稳定
|
||||
- 后续一切性能优化都要先穿过旧前端壳
|
||||
|
||||
因此这里的“继续 Rust 家族化”真正要减少的,不是文件后缀的种类,而是:
|
||||
|
||||
> **树域存在两套长期执行语义的状态。**
|
||||
|
||||
## 3.2 对加载速度与运行成本有真实潜力
|
||||
|
||||
树域是高频常驻 UI,不是偶尔打开一次的边缘面板。
|
||||
|
||||
如果它继续作为大型 `use client` 组件族存在,就会长期保留这些成本:
|
||||
|
||||
- 客户端组件挂载
|
||||
- 虚拟列表初始化
|
||||
- DnD 状态机初始化
|
||||
- 菜单与选择状态初始化
|
||||
- 浏览器侧 row model 构建
|
||||
- 兼容 fallback 与 stream 选择逻辑
|
||||
|
||||
这些成本不会因为“数据真相已经交给 Rust”而自动消失。
|
||||
|
||||
把树域主壳继续迁向 Rust 家族,真实收益主要来自:
|
||||
|
||||
- 进一步压低浏览器端常驻 JS
|
||||
- 让 projection -> renderer 的路径更短
|
||||
- 减少 hydration 与挂载层数
|
||||
- 降低 Sidebar 切页与刷新时的重渲染放大
|
||||
- 为后续更彻底的 server-first 壳收口创造条件
|
||||
|
||||
这里必须强调:
|
||||
|
||||
- Rust 化 **不自动等于** 更快
|
||||
- 但在当前项目的长期方向里,Rust 家族化是“继续减轻旧前端壳”的必要手段
|
||||
|
||||
## 3.3 对协作冲突与长期维护也有真实意义
|
||||
|
||||
这里的“冲突”不只指 git merge 冲突,更主要是:
|
||||
|
||||
- projection contract 与 renderer 行为漂移
|
||||
- TS adapter 与 Rust route 的边界反复变化
|
||||
- 同一个交互在两侧同时维护 legality / normalization / fallback
|
||||
|
||||
当树域主执行面仍留在 React 壳里时:
|
||||
|
||||
- Rust 改协议
|
||||
- 前端就要继续补 adapter
|
||||
- 测试也要跨两族运行时拼接验证
|
||||
|
||||
继续 Rust 家族化的意义,是让下面这条链尽量收敛为一条语言家族更统一的主链:
|
||||
|
||||
- kernel truth
|
||||
- projection
|
||||
- tree command
|
||||
- tree shell
|
||||
- renderer
|
||||
|
||||
---
|
||||
|
||||
## 4. 下一阶段剩余架构事项
|
||||
|
||||
下面这些事项,才是树域继续 Rust 家族化时真正还没完成的主任务。
|
||||
|
||||
## 4.1 正式定义“树域主执行面”的完成标准
|
||||
|
||||
首先必须明确:
|
||||
|
||||
- “树域已经吃 projection” 不等于 “树域已经 Rust 家族化完成”
|
||||
- “存在 Leptos scaffold / Rust tree_shell 模块” 不等于 “主路径 renderer 已迁完”
|
||||
|
||||
下一阶段完成标准应固定为:
|
||||
|
||||
- 至少一条真实默认用户流量,主树 renderer 不再依赖当前 React `PrivateTree` / `FileTree`
|
||||
- `page tree / file tree / picker` 的核心交互骨架不再由 Next/React 主壳承担
|
||||
- TS 宿主只保留挂载、局部桥接、页面路由与极薄 compat
|
||||
|
||||
如果这一标准不先冻结,后续很容易再次把“有 Rust route”误当成“已经 Rust 化完成”。
|
||||
|
||||
## 4.2 把 page tree renderer 真正迁入 Rust 家族
|
||||
|
||||
`page tree` 下一阶段不是继续补前端 helper,而是把以下能力迁成 Rust 家族正式 renderer 能力:
|
||||
|
||||
- 行渲染
|
||||
- 展开/折叠
|
||||
- 当前页高亮
|
||||
- hover 动作区
|
||||
- 键盘导航
|
||||
- 拖拽反馈
|
||||
- 上下文菜单入口
|
||||
|
||||
这里的重点不是“把视觉样式翻译一遍”,而是:
|
||||
|
||||
- 把行级行为骨架从 React 组件迁走
|
||||
- 让 page tree 只围绕正式 projection item 工作
|
||||
- 不再由现有 TS 组件长期持有主路径交互状态机
|
||||
|
||||
## 4.3 把 file tree 从“前端 adapter 文件树”收口为“Rust 直接输出 + Rust 家族 renderer”
|
||||
|
||||
`file_tree` 是当前最关键也最容易退化的一段。
|
||||
|
||||
下一阶段必须同时做两件事:
|
||||
|
||||
1. Rust 侧继续把 `file_tree` 输出补完整。
|
||||
2. renderer 不再依赖当前前端在 `pageRows + assets` 上继续拼 visible rows。
|
||||
|
||||
必须继续向 Rust 收口的内容包括:
|
||||
|
||||
- `index.md`
|
||||
- 附件
|
||||
- `mindmap` 文件夹
|
||||
- 导图子附件
|
||||
- 表格、书籍、PDF 等对象提示
|
||||
- `resource_kind / asset_kind / icon_hint`
|
||||
|
||||
否则文件树即使换了新壳,也仍然只是:
|
||||
|
||||
- “页面树 + 前端附件补丁”
|
||||
|
||||
这不符合长期目标。
|
||||
|
||||
## 4.4 把树域交互骨架拆成可替换的正式模块
|
||||
|
||||
下一阶段不应继续把交互都堆在单一大组件里,而应显式收口为几类正式模块:
|
||||
|
||||
- row model
|
||||
- selection model
|
||||
- focus model
|
||||
- keyboard model
|
||||
- DnD state machine
|
||||
- context action registry
|
||||
|
||||
无论这些模块最终落在:
|
||||
|
||||
- `Leptos`
|
||||
- 或极薄 Rust/TS bridge
|
||||
|
||||
都必须满足两条:
|
||||
|
||||
- 它们不再定义结构真相
|
||||
- 它们可以独立测试与演进
|
||||
|
||||
## 4.5 完成 tree command cutover Stage 2
|
||||
|
||||
树域继续 Rust 家族化时,命令面不能长期继续停在双轨。
|
||||
|
||||
下一阶段必须完成:
|
||||
|
||||
- 主路径默认发 `tree.*`
|
||||
- `documents.*` 退到 compat
|
||||
- legality / normalize / move target 等树策略继续回收给 Rust
|
||||
|
||||
否则会出现一个长期问题:
|
||||
|
||||
- UI 壳换成 Rust 家族了
|
||||
- 但命令面仍主要经由旧 `documents.*` 兼容语义运行
|
||||
|
||||
这样并没有完成真正的主执行面收口。
|
||||
|
||||
## 4.6 完成 Rust Web realtime contract
|
||||
|
||||
树域如果要继续往正式主链推进,realtime 必须从“可用”进入“单一正式合同”。
|
||||
|
||||
下一阶段需要补齐:
|
||||
|
||||
- `snapshot`
|
||||
- `delta`
|
||||
- `resync`
|
||||
- cursor 漂移处理
|
||||
- 局部 subtree 与 workspace scope 的统一
|
||||
|
||||
目标不是简单保住 fallback,而是让树域主路径不再依赖:
|
||||
|
||||
- 查询快照
|
||||
- 本地 freshness 选择
|
||||
- React 侧补偿式同步
|
||||
|
||||
## 4.7 收薄宿主边界
|
||||
|
||||
长期目标不是让主站彻底消失,而是让主站对树域只保留最小宿主职责:
|
||||
|
||||
- 页面布局挂载位
|
||||
- 路由跳转
|
||||
- 样式容器
|
||||
- 极薄 feature flag / compat
|
||||
- 与其他页面面板的最小桥接
|
||||
|
||||
不应继续保留在宿主中的内容:
|
||||
|
||||
- 树结构重建
|
||||
- 主交互状态骨架
|
||||
- 文件树资源行语义补丁
|
||||
- 主要 keyboard / DnD / selection 逻辑
|
||||
|
||||
---
|
||||
|
||||
## 5. page tree 迁移时必须保留的能力
|
||||
|
||||
`page tree` 在迁移到 Rust 家族主执行面时,必须显式保证下面这些能力不退化。
|
||||
|
||||
## 5.1 结构与导航
|
||||
|
||||
必须保留:
|
||||
|
||||
- 稳定的层级展开/折叠
|
||||
- 当前页高亮
|
||||
- 祖先自动展开
|
||||
- 搜索/过滤后层级仍可理解
|
||||
|
||||
禁止退化为:
|
||||
|
||||
- 纯扁平列表
|
||||
- 只有打开功能、没有层级语义的按钮组
|
||||
|
||||
## 5.2 行级动作
|
||||
|
||||
必须保留:
|
||||
|
||||
- 新建子页面
|
||||
- 重命名
|
||||
- 移动
|
||||
- 归档/恢复类上下文操作入口
|
||||
- 行级 hover 动作区
|
||||
|
||||
要求:
|
||||
|
||||
- 不能为了换 renderer 把动作缩减到“点开页面 + 更多菜单”
|
||||
- 不能丢失当前工作区级高频动作入口
|
||||
|
||||
## 5.3 键盘与焦点
|
||||
|
||||
必须保留:
|
||||
|
||||
- 上下移动
|
||||
- 左右展开/折叠
|
||||
- Enter 打开
|
||||
- 稳定的 focus 行
|
||||
|
||||
要求:
|
||||
|
||||
- `focus` 不能重新混成 `selected`
|
||||
- 展开与选择变化后焦点归一化必须稳定
|
||||
|
||||
## 5.4 拖拽与移动
|
||||
|
||||
必须保留:
|
||||
|
||||
- 基础拖拽排序
|
||||
- 父节点切换
|
||||
- 非法拖拽校验
|
||||
|
||||
要求:
|
||||
|
||||
- drop feedback 不能退化成不可预测的闪烁
|
||||
- 目标父节点与位置推导必须有稳定、可测试的协议
|
||||
|
||||
## 5.5 大树体验
|
||||
|
||||
必须保留:
|
||||
|
||||
- 稳定滚动
|
||||
- 大树下的可见区域性能
|
||||
- 展开/折叠与切页不会出现明显回闪
|
||||
|
||||
这部分是迁移时的硬验收项,不是可选优化项。
|
||||
|
||||
---
|
||||
|
||||
## 6. file tree 迁移时必须保留的能力
|
||||
|
||||
`file_tree` 比 `page tree` 更容易因迁移而退化。
|
||||
|
||||
下一阶段必须把“功能保留”明确写成硬门槛。
|
||||
|
||||
## 6.1 资源层级
|
||||
|
||||
必须保留:
|
||||
|
||||
- 页面
|
||||
- `index.md`
|
||||
- 附件
|
||||
- `mindmap` 文件夹
|
||||
- 导图子附件
|
||||
- 表格、书籍、PDF 等资源对象语义
|
||||
|
||||
禁止退化为:
|
||||
|
||||
- 页面树下挂一个普通附件列表
|
||||
- mindmap 资源被拍平成普通文件
|
||||
|
||||
## 6.2 选择能力
|
||||
|
||||
必须保留:
|
||||
|
||||
- 单选
|
||||
- 多选
|
||||
- Shift 范围选
|
||||
- 右键选中
|
||||
- 可见行变化后的选择归一化
|
||||
|
||||
这部分如果退化,文件树就不再是资源管理器语义,而会退回文档树换皮。
|
||||
|
||||
## 6.3 打开与操作
|
||||
|
||||
必须保留:
|
||||
|
||||
- 单击选择
|
||||
- 双击打开资源
|
||||
- 资源级右键菜单
|
||||
- 按资源类型分支的动作入口
|
||||
|
||||
要求:
|
||||
|
||||
- 资源图标语义必须稳定
|
||||
- `asset_kind / icon_hint` 不允许再次回到壳内临时猜测
|
||||
|
||||
## 6.4 拖放
|
||||
|
||||
必须保留:
|
||||
|
||||
- 内部拖放
|
||||
- 外部文件拖入上传
|
||||
- copy / move 区分
|
||||
- 非法投放校验
|
||||
|
||||
要求:
|
||||
|
||||
- 文件树不能只保留视觉拖拽,没有正式动作协议
|
||||
- 外部文件拖入必须继续是正式主路径能力
|
||||
|
||||
## 6.5 文件树特有的可见行模型
|
||||
|
||||
必须保留:
|
||||
|
||||
- `doc -> index -> asset-folder -> asset` 的可见行语义
|
||||
- 扁平化 rows 与深度的稳定映射
|
||||
- 可见行变化后的选择、焦点、拖放范围归一化
|
||||
|
||||
这里允许实现换掉,但不允许语义丢掉。
|
||||
|
||||
---
|
||||
|
||||
## 7. 推荐迁移顺序
|
||||
|
||||
为了尽量保持体验不退化,推荐顺序如下:
|
||||
|
||||
1. 先补齐 `file_tree` 的 Rust 直接输出,避免迁 renderer 时还要继续背前端 adapter。
|
||||
2. 再把 `page tree` renderer 迁入 Rust 家族,因为它对象更单纯、验收面更小。
|
||||
3. 然后迁 `file tree` renderer,因为它依赖更多资源与交互语义。
|
||||
4. 再迁 `picker` 轻量模式,让它复用同一套 renderer / state family。
|
||||
5. 最后继续削薄宿主,收掉剩余主路径 React 树壳。
|
||||
|
||||
原因:
|
||||
|
||||
- `page tree` 先迁可以先验证主行 renderer 与状态骨架。
|
||||
- `file_tree` 后迁可以避免一开始就在最复杂对象面上同时处理 renderer 与 contract 漂移。
|
||||
- `picker` 最适合作为共用 renderer 的轻量消费场景,而不是主线路的先导。
|
||||
|
||||
---
|
||||
|
||||
## 8. 迁移阶段的验收门槛
|
||||
|
||||
只有同时满足下面几条,才可以把树域继续 Rust 家族化的某一阶段视为成立:
|
||||
|
||||
- 页面树与文件树都没有因切流而减少已有能力。
|
||||
- `page tree / file tree` 的关键能力有 smoke 覆盖:
|
||||
- 切页
|
||||
- 展开/折叠
|
||||
- 拖拽
|
||||
- 右键菜单
|
||||
- 多选与范围选
|
||||
- 外部文件拖入
|
||||
- `file_tree` 的对象投影不再依赖前端临时补丁维持主路径。
|
||||
- 主调用路径默认走正式 `tree.*` 命令面。
|
||||
- 正式 realtime 至少已经进入统一 `snapshot + delta + resync` 合同,而不是继续主要靠补偿式 freshness 选择。
|
||||
- 主站宿主不再持有树结构真相,也不再承担主要树域交互状态机。
|
||||
|
||||
---
|
||||
|
||||
## 9. 当前优先级建议
|
||||
|
||||
在当前总优先级下,树域继续 Rust 家族化的最近顺序建议固定为:
|
||||
|
||||
1. 继续推进 `4-6 tree command protocol cutover stage2`
|
||||
2. 明确 `file_tree` 直接输出的剩余缺口,并以 Rust 侧补齐
|
||||
3. 为 `page tree / file tree` 迁移补正式功能保留回归矩阵
|
||||
4. 选择并落一条真实默认流量进入 Rust 家族 renderer
|
||||
5. 再逐步收掉当前主路径 React 树壳
|
||||
|
||||
也就是说,当前下一步不是:
|
||||
|
||||
- 继续在现有 React 树壳里堆更多交互补丁
|
||||
|
||||
而是:
|
||||
|
||||
> **在功能不退化的前提下,把树域从“Rust 持有语义、React 持有主执行面”的过渡态,推进到“Rust 家族同时持有语义与主执行面”的下一阶段。**
|
||||
|
||||
---
|
||||
|
||||
## 10. 最终结论
|
||||
|
||||
当前树域继续往全 Rust 方向发展,是有真实意义的。
|
||||
|
||||
意义不在于:
|
||||
|
||||
- 代码库里减少一种语言
|
||||
|
||||
而在于:
|
||||
|
||||
- 收掉旧前端壳的长期主执行职责
|
||||
- 减少第二套复杂运行时语义
|
||||
- 为首屏与常驻交互链路继续减重
|
||||
- 让 `tree-first graph kernel -> Rust Web -> Rust family shell` 形成更一致的长期主链
|
||||
|
||||
但这个推进必须满足一个前提:
|
||||
|
||||
> **page tree / file tree 的功能能力不能因为迁移而退化。**
|
||||
|
||||
因此,下一阶段的正确目标不是“尽快把组件翻译成 Rust”,而是:
|
||||
|
||||
> **先把剩余 projection / command / realtime / host 边界补齐,再以 page tree 与 file tree 的能力保留为硬约束,把树域主执行面稳步迁入 Rust 家族。**
|
||||
@@ -0,0 +1,101 @@
|
||||
# [recycle] AppFlowy-IO仓库:可借鉴的核心资源与适配建议(Rust+Axum+Leptos)
|
||||
|
||||
AppFlowy-IO作为开源Notion替代品,其**Rust后端与CRDT协作体系**对您的项目极具参考价值,尽管前端使用Flutter而非Leptos,但核心设计理念完全可迁移。以下是按价值排序的关键仓库与适配建议:
|
||||
|
||||
---
|
||||
|
||||
## 一、核心可借鉴仓库(全部手动核验可用)
|
||||
|
||||
### 1. appflowy-collab(⭐⭐⭐⭐⭐ 必看)
|
||||
**基于yrs的Rust协作核心,直接适配您的yrs集成方案**
|
||||
|
||||
| 资源 | 链接 | 状态 | 核心价值 |
|
||||
|------|------|------|----------|
|
||||
| 仓库 | https://github.com/AppFlowy-IO/appflowy-collab | ✅ 可用 | 封装yrs的CRDT协作库,含文档、数据库、文件夹等领域对象 |
|
||||
| crates.io | https://crates.io/crates/collab | ✅ 可用 | 最新版本0.3.0,可直接依赖 |
|
||||
| 文档 | https://docs.rs/collab/latest/collab/ | ✅ 可用 | 协作算法与数据结构API |
|
||||
| 示例 | https://github.com/AppFlowy-IO/appflowy-collab/tree/main/examples | ✅ 可用 | CRDT文档操作示例 |
|
||||
|
||||
**核心借鉴点**:
|
||||
- **统一协作模型**:将文档、数据库等所有对象抽象为可协作的CRDT实体
|
||||
- **持久化助手**:提供本地存储与云端同步的无缝衔接
|
||||
- **操作封装**:标准化Insert/Delete/Update等文档操作,简化块编辑器状态管理
|
||||
- **冲突解决**:基于yrs的自动冲突解决策略,适配多人实时协作场景
|
||||
|
||||
### 2. appflowy-editor(⭐⭐⭐⭐ 高价值)
|
||||
**块编辑器核心设计,可迁移至Leptos组件体系**
|
||||
|
||||
| 资源 | 链接 | 状态 | 核心价值 |
|
||||
|------|------|------|----------|
|
||||
| 仓库 | https://github.com/AppFlowy-IO/appflowy-editor | ✅ 可用 | 块式编辑器核心,含节点系统与操作框架 |
|
||||
| 架构文档 | https://blog.appflowy.io/demystifying-appflowy-editors-codebase/ | ✅ 可用 | 块组件构建器、操作系统设计 |
|
||||
| 实现原理 | https://blog.appflowy.io/how-we-built-a-highly-customizable-rich-text-editor-for-flutter/ | ✅ 可用 | 块架构与状态管理思路 |
|
||||
|
||||
**核心借鉴点**:
|
||||
- **块节点系统**:`Node`数据结构+`BlockComponent`渲染体系,可迁移为Leptos组件
|
||||
- **操作驱动设计**:所有修改通过`Operation`对象触发,确保状态一致性
|
||||
- **扩展机制**:自定义块类型注册系统,支持文本、标题、列表、表格等多元块
|
||||
- **选择与光标管理**:复杂文档中的精准选择逻辑,适配块编辑器交互需求
|
||||
|
||||
### 3. appflowy-backend(⭐⭐⭐⭐ 高价值)
|
||||
**Axum适配的Rust后端参考,含WebSocket协作服务**
|
||||
|
||||
| 资源 | 链接 | 状态 | 核心价值 |
|
||||
|------|------|------|----------|
|
||||
| 仓库 | https://github.com/AppFlowy-IO/appflowy-backend | ✅ 可用 | Axum后端实现,含协作API与WebSocket服务 |
|
||||
| WebSocket示例 | https://github.com/AppFlowy-IO/appflowy-backend/blob/main/src/websocket.rs | ✅ 可用 | CRDT更新推送实现 |
|
||||
| 数据验证 | https://docs.appflowy.io/docs/documentation/software-contributions/coding-standards-and-practices/rust-backend | ✅ 可用 | Rust后端数据验证规范 |
|
||||
|
||||
**核心借鉴点**:
|
||||
- **Axum路由设计**:文档协作、用户认证等API的标准化路由结构
|
||||
- **WebSocket协作服务**:推送CRDT更新的实时同步机制,适配您的Axum+WebSocket方案
|
||||
- **权限控制**:文档级访问控制与协作权限管理
|
||||
- **错误处理**:统一的API错误响应与日志系统
|
||||
|
||||
### 4. 其他高价值仓库
|
||||
| 仓库 | 链接 | 状态 | 价值点 |
|
||||
|------|------|------|--------|
|
||||
| appflowy-database | https://github.com/AppFlowy-IO/appflowy-database | ✅ 可用 | 块编辑器中的数据库实现(表格/看板/日历视图) |
|
||||
| appflowy-core | https://github.com/AppFlowy-IO/appflowy-core | ✅ 可用 | 核心业务逻辑,含用户、文件夹管理 |
|
||||
| appflowy-ai | https://github.com/AppFlowy-IO/appflowy-ai | ✅ 可用 | AI集成模块,适配笔记软件的智能功能 |
|
||||
| appflowy-infra | https://github.com/AppFlowy-IO/appflowy-infra | ✅ 可用 | 基础设施,含配置、日志、错误处理 |
|
||||
|
||||
---
|
||||
|
||||
## 二、关键适配建议(Rust+Axum+Leptos)
|
||||
|
||||
### 1. 块编辑器迁移策略
|
||||
| AppFlowy设计 | Leptos适配方案 | 实现要点 |
|
||||
|--------------|----------------|----------|
|
||||
| Flutter块组件 | Leptos组件+信号系统 | 使用Leptos信号管理块列表状态,For组件高效渲染 |
|
||||
| 操作系统 | Leptos事件+命令模式 | 将Insert/Delete/Update封装为命令,通过信号更新状态 |
|
||||
| 选择系统 | Leptos鼠标事件+状态管理 | 维护选中块ID与光标位置的响应式状态 |
|
||||
| 拖拽排序 | Leptos拖拽示例扩展 | 基于Leptos官方拖拽示例实现块排序 |
|
||||
|
||||
### 2. 协作系统无缝集成
|
||||
1. **直接依赖collab crate**:替代您的原生yrs使用,获得更高层次的协作抽象
|
||||
2. **Axum+WebSocket同步**:参考appflowy-backend的WebSocket实现,推送CRDT更新
|
||||
3. **本地持久化**:结合redb与collab的持久化助手,实现本地优先存储
|
||||
4. **冲突解决**:复用collab封装的yrs自动合并策略,无需重复开发
|
||||
|
||||
### 3. 避坑指南
|
||||
1. **避免Flutter绑定**:专注collab与核心逻辑,前端完全用Leptos重实现块组件
|
||||
2. **优先使用命令模式**:所有块修改通过命令触发,确保协作状态可追踪
|
||||
3. **块渲染优化**:对长文档使用虚拟列表(Leptos有virtual_scroll示例)
|
||||
4. **状态隔离**:将协作状态与UI状态分离,通过信号传递更新
|
||||
|
||||
---
|
||||
|
||||
## 三、推荐实施路径(基于您的技术栈)
|
||||
1. **基础层**:集成collab crate替代原生yrs,快速获得文档协作能力
|
||||
2. **块模型**:参考appflowy-editor设计块节点系统,支持文本、标题、列表等基础块
|
||||
3. **编辑器UI**:用Leptos实现块组件+拖拽排序,复用Leptos信号管理状态
|
||||
4. **后端同步**:基于Axum+WebSocket实现collab更新推送,参考appflowy-backend
|
||||
5. **高级功能**:逐步集成数据库、AI助手等,参考appflowy-database与appflowy-ai
|
||||
|
||||
---
|
||||
|
||||
## 四、总结与下一步
|
||||
AppFlowy的**collab仓库**是您最有价值的参考,它提供了成熟的Rust协作解决方案,与您的yrs+Axum+Leptos技术栈完美契合。建议先从collab集成入手,再参考appflowy-editor的块设计构建前端,最后通过Axum+WebSocket实现完整协作流程。
|
||||
|
||||
需要我基于collab crate生成一个可直接运行的**Leptos+Axum+collab**最小块编辑器模板吗?包含块渲染、基础协作和WebSocket同步功能。
|
||||
@@ -0,0 +1,186 @@
|
||||
# [recycle] Rust Block Editor Adoption Matrix v0
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
>
|
||||
> 目标:
|
||||
> - 冻结 task-050 的参考层采用矩阵
|
||||
> - 明确 `edita-core`、`blocks`、`kode`、`leptos-tiptap` 的采用边界
|
||||
> - 说明哪些是采用,哪些是不采用,哪些是部分采用
|
||||
|
||||
## 1. 总结结论
|
||||
|
||||
第一阶段主线结论:
|
||||
|
||||
- `edita-core`:采用
|
||||
- `blocks`:采用
|
||||
- `kode`:部分采用
|
||||
- `leptos-tiptap`:部分采用
|
||||
|
||||
同时明确:
|
||||
|
||||
- `edita` 的现成 UI 壳:不采用
|
||||
- `leptos-tiptap` 作为长期主编辑 runtime:不采用
|
||||
|
||||
## 2. 采用矩阵
|
||||
|
||||
| 参考层 | 结论 | 采用依据 | 不采用或限制依据 | 第一阶段落点 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `edita-core` | 采用 | `edita-core/src/lib.rs` 已提供 `Editor / Block / Command` 无头抽象,适合承接 Rust command 主链 | 泛型过于宽松,仍需补 `mnote` 自己的 block schema 与 command enum 胶水 | 作为 editor core 命令容器参考 |
|
||||
| `blocks` | 采用 | `src/block.rs`、`document.rs`、`converters.rs`、`history.rs`、`diff.rs`、`sanitizer.rs` 已覆盖块模型、导入导出、历史、diff/merge | 自定义块型仍需补扩展映射,不能直接原样承载全部 `mnote` 宿主块 | 作为文档模型、转换、history 基线 |
|
||||
| `kode` | 部分采用 | `kode-core` 提供 buffer/selection/history,`kode-leptos` 提供 `MarkdownEditorComponent` 与 `TreeWysiwygEditor`,`kode-doc` 提供结构树思路 | 自带 doc tree 语义较重,不适合整套替换 `mnote` 事实层 | 作为块内输入器、光标、输入规则参考 |
|
||||
| `leptos-tiptap` | 部分采用 | `src/api/component.rs`、`use_tiptap_editor.rs`、`runtime/bridge.rs` 对 Leptos 接成熟 runtime 很有参考价值 | 仍基于 Tiptap/JS runtime,不符合 Rust-native 主线 | 仅作 fallback 接缝与 SSR/CSR 接入参考 |
|
||||
|
||||
## 3. 采用
|
||||
|
||||
### 3.1 `edita-core`:采用
|
||||
|
||||
采用理由:
|
||||
|
||||
- `Editor<Node, State, Input>` 明确区分状态、块解析、命令执行。
|
||||
- `Command<State>` 模式非常适合把当前分散在 UI 事件里的块命令抽到 Rust 侧。
|
||||
- 没有 DOM 依赖,没有 Leptos 依赖,没有 JS runtime 依赖。
|
||||
|
||||
建议采用范围:
|
||||
|
||||
- `Editor`
|
||||
- `Block`
|
||||
- `Command`
|
||||
- `process_nodes(...)` 体现的“按块处理输入”思路
|
||||
|
||||
不直接照搬的部分:
|
||||
|
||||
- 直接使用其 UI 示例
|
||||
- 直接复用其泛型输入节点定义
|
||||
|
||||
原因:
|
||||
|
||||
- `mnote` 需要的是稳定块命令与文档模型,不是直接拿一个演示型 editor state。
|
||||
|
||||
### 3.2 `blocks`:采用
|
||||
|
||||
采用理由:
|
||||
|
||||
- 当前第一阶段最需要的不是花哨 UI,而是稳定文档模型与格式收口。
|
||||
- `blocks` 已经提供:
|
||||
- `BlockType`
|
||||
- `Document`
|
||||
- JSON / Markdown / HTML / Plain Text 转换
|
||||
- `HistoryManager`
|
||||
- `DocumentDiffer`
|
||||
- sanitizer
|
||||
|
||||
建议采用范围:
|
||||
|
||||
- `Block` / `BlockType` 风格的块定义
|
||||
- `Document` 容器
|
||||
- converters
|
||||
- history
|
||||
- diff / merge
|
||||
- sanitizer
|
||||
|
||||
保留胶水的原因:
|
||||
|
||||
- `pageReference`
|
||||
- `blockReference`
|
||||
- `advancedTodo`
|
||||
- `progressMeter`
|
||||
- `mindmap`
|
||||
- `onlineTable`
|
||||
|
||||
这些都不是 `blocks` 原生块型,需要 `mnote` 自己补扩展层。
|
||||
|
||||
## 4. 部分采用
|
||||
|
||||
### 4.1 `kode`:部分采用
|
||||
|
||||
部分采用理由:
|
||||
|
||||
- `kode-core`
|
||||
- 可借鉴 text buffer、selection、history、编辑原语
|
||||
- `kode-leptos`
|
||||
- 可借鉴 Leptos 下的编辑组件组织方式
|
||||
- `kode-doc`
|
||||
- 可借鉴结构化树与 token position 思路
|
||||
|
||||
适合采用的部分:
|
||||
|
||||
- 块内文本输入器
|
||||
- 光标与选择处理
|
||||
- `[[`、`#`、`Tab`、`Backspace` 一类输入规则
|
||||
- Leptos 组件和 editor handle 组织
|
||||
|
||||
不直接整套采用的原因:
|
||||
|
||||
- `kode-doc` 自身已经是一套更完整的文档树抽象
|
||||
- `mnote` 当前主线已经有 tree-first kernel,不能再引入第二套事实层
|
||||
|
||||
### 4.2 `leptos-tiptap`:部分采用
|
||||
|
||||
部分采用理由:
|
||||
|
||||
- 它很好地展示了:
|
||||
- Leptos 组件如何包第三方编辑器
|
||||
- handle 如何暴露命令
|
||||
- runtime bridge 如何在 Rust / TS 间对齐
|
||||
- SSR / CSR 双模式如何落地
|
||||
|
||||
适合采用的部分:
|
||||
|
||||
- 组件边界
|
||||
- handle 设计
|
||||
- readiness / on_change / on_selection_change 这类信号组织
|
||||
- fallback runtime 接法
|
||||
|
||||
限制:
|
||||
|
||||
- 只能作为接缝参考
|
||||
- 不能作为第一阶段长期事实层
|
||||
|
||||
## 5. 不采用
|
||||
|
||||
### 5.1 `edita` UI 壳:不采用
|
||||
|
||||
不采用原因:
|
||||
|
||||
- 当前需要的是 Rust command 核,不是另一个现成 UI。
|
||||
- `mnote` 已有自己的文档页、阅读态、宿主块与资源系统。
|
||||
- 直接套 UI 只会引入新的迁移成本。
|
||||
|
||||
### 5.2 `leptos-tiptap` 主线路线:不采用
|
||||
|
||||
不采用原因:
|
||||
|
||||
- 它仍把主事实层放在 Tiptap runtime。
|
||||
- 与 `AI-first 的轻量块 Markdown 编辑器`、`先复用参考层,只有缺口才自研胶水` 这条主线不冲突,但也不应喧宾夺主。
|
||||
- 它适合作为 fallback,不适合作为默认长期方案。
|
||||
|
||||
## 6. 与当前仓库现状的对应关系
|
||||
|
||||
当前仓库的现实情况决定了为什么要这样分:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/schema.ts`
|
||||
- 当前 BlockNote 自定义块已经不少,不能再继续把核心命令绑死在 BlockNote schema。
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx`
|
||||
- 现在写链深度耦合 BlockNote、保存接口、重型宿主块副作用。
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/blocks/block-ops-adapter.ts`
|
||||
- 当前真正进入稳定工具链的只有 `paragraph` 和 `heading`,说明需要把模型与命令层重新收口。
|
||||
|
||||
因此第一阶段最务实的做法是:
|
||||
|
||||
1. 采用 `edita-core` 的无头命令组织
|
||||
2. 采用 `blocks` 的文档模型与格式转换
|
||||
3. 部分采用 `kode` 的输入与光标处理
|
||||
4. 部分采用 `leptos-tiptap` 的 Leptos runtime bridge 经验
|
||||
|
||||
## 7. 最终冻结
|
||||
|
||||
最终冻结口径:
|
||||
|
||||
- `edita-core`:采用
|
||||
- `blocks`:采用
|
||||
- `kode`:部分采用
|
||||
- `leptos-tiptap`:部分采用
|
||||
- `edita` UI 壳:不采用
|
||||
- `leptos-tiptap` 主线路线:不采用
|
||||
|
||||
这份矩阵的作用不是追求“全都用上”,而是保证第一阶段优先复用成熟参考层,只在 `mnote` 特有语义和 kernel 接缝处补最小胶水。
|
||||
@@ -0,0 +1,204 @@
|
||||
# [recycle] Rust Block Editor AI/CLI Tool Contract v0
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
|
||||
## 1. 目标
|
||||
|
||||
这份文档用于冻结 AI 与 CLI 调用 Rust block editor 的最小工具契约。
|
||||
|
||||
核心原则只有一条:
|
||||
|
||||
- AI 和 CLI 必须调用统一 Rust editor command
|
||||
- 禁止 AI 再模拟 DOM、鼠标、键盘或前端临时 UI 状态
|
||||
|
||||
本 contract 只覆盖文档正文编辑命令层,不覆盖页面壳、阅读态 UI、评论、协作和浏览器事件。
|
||||
|
||||
## 2. 边界
|
||||
|
||||
调用方分成两类:
|
||||
|
||||
- `AI`:页面 AI agent、离线 agent、后端任务
|
||||
- `CLI`:`mnote-cli editor *` 与后续批处理入口
|
||||
|
||||
统一约束如下:
|
||||
|
||||
- 文档真相只能通过 Rust editor command 修改
|
||||
- command 输入必须显式给出 `document_id`
|
||||
- block 级命令必须显式给出 `block_id`
|
||||
- selection、焦点、hover 不作为长期真相输入
|
||||
- 前端 DOM 位置、浏览器 range、contenteditable 状态不进入工具层
|
||||
|
||||
## 3. 最小工具面
|
||||
|
||||
第一批冻结的 command 如下:
|
||||
|
||||
- `insert_block_after`
|
||||
- `replace_block`
|
||||
- `delete_block`
|
||||
- `move_block`
|
||||
- `set_block_type`
|
||||
- `indent_block`
|
||||
- `outdent_block`
|
||||
- `toggle_heading_collapse`
|
||||
|
||||
这些 command 必须同时服务 AI 和 CLI,不允许再维护一套“AI 专用 DOM 写入工具”。
|
||||
|
||||
## 4. 输入输出契约
|
||||
|
||||
统一输入头:
|
||||
|
||||
- `document_id`
|
||||
- `workspace_id?`
|
||||
- `request_id?`
|
||||
- `trace_id?`
|
||||
- `actor_id`
|
||||
- `actor_type`
|
||||
- `reason?`
|
||||
|
||||
统一输出头:
|
||||
|
||||
- `ok`
|
||||
- `document_id`
|
||||
- `applied_command`
|
||||
- `revision?`
|
||||
- `changed_block_ids`
|
||||
- `snapshot?`
|
||||
- `audit`
|
||||
|
||||
### 4.1 `insert_block_after`
|
||||
|
||||
输入:
|
||||
|
||||
- `after_block_id?`
|
||||
- `block`
|
||||
|
||||
输出:
|
||||
|
||||
- `inserted_block_id`
|
||||
- `changed_block_ids`
|
||||
|
||||
### 4.2 `replace_block`
|
||||
|
||||
输入:
|
||||
|
||||
- `block_id`
|
||||
- `block`
|
||||
|
||||
输出:
|
||||
|
||||
- `changed_block_ids`
|
||||
|
||||
### 4.3 `delete_block`
|
||||
|
||||
输入:
|
||||
|
||||
- `block_id`
|
||||
|
||||
输出:
|
||||
|
||||
- `deleted_block_id`
|
||||
- `changed_block_ids`
|
||||
|
||||
### 4.4 `move_block`
|
||||
|
||||
输入:
|
||||
|
||||
- `block_id`
|
||||
- `target_parent_id?`
|
||||
- `after_block_id?`
|
||||
|
||||
输出:
|
||||
|
||||
- `changed_block_ids`
|
||||
|
||||
### 4.5 `set_block_type`
|
||||
|
||||
输入:
|
||||
|
||||
- `block_id`
|
||||
- `block_type`
|
||||
- `props?`
|
||||
|
||||
输出:
|
||||
|
||||
- `changed_block_ids`
|
||||
|
||||
### 4.6 `indent_block`
|
||||
|
||||
输入:
|
||||
|
||||
- `block_id`
|
||||
|
||||
输出:
|
||||
|
||||
- `changed_block_ids`
|
||||
|
||||
### 4.7 `outdent_block`
|
||||
|
||||
输入:
|
||||
|
||||
- `block_id`
|
||||
|
||||
输出:
|
||||
|
||||
- `changed_block_ids`
|
||||
|
||||
### 4.8 `toggle_heading_collapse`
|
||||
|
||||
输入:
|
||||
|
||||
- `block_id`
|
||||
|
||||
输出:
|
||||
|
||||
- `changed_block_ids`
|
||||
- `collapsed`
|
||||
|
||||
## 5. 安全与约束
|
||||
|
||||
- AI 不得假设“当前光标在某处”,必须使用显式 `block_id`
|
||||
- AI 不得伪造前端已存在的 block id
|
||||
- CLI 和 AI 都不得绕过 command 直接写原始内容 JSON
|
||||
- 若命令需要目标 block,但目标不存在,必须返回结构化错误
|
||||
- 若缩进、移动会破坏树结构,必须拒绝执行
|
||||
|
||||
## 6. 审计
|
||||
|
||||
每次 command 至少记录:
|
||||
|
||||
- `request_id`
|
||||
- `trace_id`
|
||||
- `actor_id`
|
||||
- `actor_type`
|
||||
- `document_id`
|
||||
- `command_name`
|
||||
- `target_block_id?`
|
||||
- `changed_block_ids`
|
||||
- `reason?`
|
||||
|
||||
## 7. CLI 对齐
|
||||
|
||||
CLI 必须与 AI 共用同一组 command 名称与输入结构。
|
||||
|
||||
最低要求:
|
||||
|
||||
- CLI 能直接调用上述 command
|
||||
- CLI 返回与 AI 一致的结构化结果
|
||||
- CLI 可输出变更后的最小 `snapshot`
|
||||
|
||||
## 8. 验收标准
|
||||
|
||||
满足以下条件即可视为 v0 可用:
|
||||
|
||||
- AI 写链不再依赖 DOM 模拟
|
||||
- CLI 与 AI 共用同一批 command 名称
|
||||
- `insert_block_after`
|
||||
- `replace_block`
|
||||
- `delete_block`
|
||||
- `move_block`
|
||||
- `set_block_type`
|
||||
- `indent_block`
|
||||
- `outdent_block`
|
||||
- `toggle_heading_collapse`
|
||||
都有明确输入输出字段
|
||||
- command 结果包含最小审计信息
|
||||
@@ -0,0 +1,216 @@
|
||||
# [recycle] Rust Block Editor Command Contract v0
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/ai-first-rust-block-editor-baseline-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/rust-block-editor-model-v0.md`
|
||||
> - `/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/command.rs`
|
||||
> - `/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/markdown.rs`
|
||||
|
||||
## 1. 目的
|
||||
|
||||
本文冻结 `task-054` 的第一阶段命令契约:
|
||||
|
||||
- editor command 列表
|
||||
- editor 到 kernel command 的映射边界
|
||||
- `Markdown import` / `Markdown export` 的责任边界
|
||||
|
||||
第一阶段目标不是一次定义所有高级交互,而是先把最小可执行命令面固定下来。
|
||||
|
||||
## 2. 第一阶段 command 列表
|
||||
|
||||
第一阶段固定以下命令进入 editor command contract:
|
||||
|
||||
- `replace_block`
|
||||
- `insert_block_after`
|
||||
- `delete_block`
|
||||
- `split_block`
|
||||
- `merge_with_previous`
|
||||
- `move_block`
|
||||
- `indent_block`
|
||||
- `outdent_block`
|
||||
- `toggle_heading_collapse`
|
||||
- `attach_reference_token`
|
||||
- `detach_reference_token`
|
||||
|
||||
## 3. 命令语义
|
||||
|
||||
### 3.1 `replace_block`
|
||||
|
||||
用途:
|
||||
|
||||
- 替换块的 `block_type`
|
||||
- 替换块的 `BlockProps`
|
||||
- 替换块的 `content_nodes`
|
||||
|
||||
第一阶段要求:
|
||||
|
||||
- 支持单块粒度更新
|
||||
- 不要求整页重算
|
||||
|
||||
### 3.2 `insert_block_after`
|
||||
|
||||
用途:
|
||||
|
||||
- 在目标块后插入同级块
|
||||
|
||||
第一阶段要求:
|
||||
|
||||
- Enter 拆块、加号插入、slash 插入都先归一到 `insert_block_after`
|
||||
|
||||
### 3.3 `delete_block`
|
||||
|
||||
用途:
|
||||
|
||||
- 删除目标块
|
||||
|
||||
第一阶段要求:
|
||||
|
||||
- 支持是否保留子块的最小策略
|
||||
|
||||
### 3.4 `split_block`
|
||||
|
||||
用途:
|
||||
|
||||
- 在指定 `content_node` 位置拆分当前块
|
||||
|
||||
第一阶段要求:
|
||||
|
||||
- 支持可选 `text_offset`
|
||||
- 支持指定 trailing block type
|
||||
|
||||
### 3.5 `merge_with_previous`
|
||||
|
||||
用途:
|
||||
|
||||
- 当前块为空或满足合并条件时,把内容合并到上一块
|
||||
|
||||
### 3.6 `move_block`
|
||||
|
||||
用途:
|
||||
|
||||
- 块重排
|
||||
- 调整父块
|
||||
- 调整 after block
|
||||
|
||||
第一阶段要求:
|
||||
|
||||
- 先承接有限移动与局部重排
|
||||
|
||||
### 3.7 `indent_block`
|
||||
|
||||
用途:
|
||||
|
||||
- 调整有限缩进层级
|
||||
|
||||
### 3.8 `outdent_block`
|
||||
|
||||
用途:
|
||||
|
||||
- 回退一层有限缩进
|
||||
|
||||
### 3.9 `toggle_heading_collapse`
|
||||
|
||||
用途:
|
||||
|
||||
- 切换标题折叠状态
|
||||
|
||||
### 3.10 `attach_reference_token`
|
||||
|
||||
用途:
|
||||
|
||||
- 在指定块内容位置挂接引用 token
|
||||
|
||||
### 3.11 `detach_reference_token`
|
||||
|
||||
用途:
|
||||
|
||||
- 移除指定引用 token
|
||||
|
||||
## 4. editor command 到 kernel command 的映射
|
||||
|
||||
第一阶段固定口径:
|
||||
|
||||
- editor command 是人层和 AI/CLI 的直接操作面
|
||||
- kernel command 仍是系统底层事实变更面
|
||||
|
||||
映射原则:
|
||||
|
||||
- `replace_block` 优先映射到块 patch / content update 类 kernel command
|
||||
- `insert_block_after` 映射到 insert / create block
|
||||
- `delete_block` 映射到 delete block
|
||||
- `move_block` 映射到 move block
|
||||
- `attach_reference_token` / `detach_reference_token` 先映射到块内容 patch,而不是单独扩出新的 kernel mutation
|
||||
|
||||
第一阶段允许 lossless 或 lossy mapping,但必须显式记录:
|
||||
|
||||
- `editor_command`
|
||||
- `kernel_command_name`
|
||||
- `lossy`
|
||||
- `notes`
|
||||
|
||||
## 5. `Markdown import`
|
||||
|
||||
第一阶段 `Markdown import` 固定只承担:
|
||||
|
||||
- Markdown 文本转 editor block document
|
||||
- 识别标题、列表、待办、引用、代码块
|
||||
- 可选识别 `[[page]]` 与 `((block))`
|
||||
|
||||
第一阶段不强求:
|
||||
|
||||
- 完整 HTML block 保真
|
||||
- 复杂 front matter 管线
|
||||
- 富文本 mark 全量映射
|
||||
|
||||
`Markdown import` 最小选项:
|
||||
|
||||
- mode
|
||||
- flavor
|
||||
- reference_token_strategy
|
||||
- boundary
|
||||
|
||||
## 6. `Markdown export`
|
||||
|
||||
第一阶段 `Markdown export` 固定只承担:
|
||||
|
||||
- 把 editor block document 转成可读 Markdown
|
||||
- 保留标题、列表、待办、引用、代码块
|
||||
- 尽量保留 `[[page]]` / `((block))`
|
||||
|
||||
第一阶段允许:
|
||||
|
||||
- 对未知块做 paragraph fallback
|
||||
- 对暂不支持块做 lossy export,但要记录 warning
|
||||
|
||||
`Markdown export` 最小选项:
|
||||
|
||||
- mode
|
||||
- flavor
|
||||
- reference_token_strategy
|
||||
- boundary
|
||||
|
||||
## 7. 第一阶段采用矩阵 v0
|
||||
|
||||
第一阶段命令与转换采用矩阵固定如下:
|
||||
|
||||
| 能力 | 第一阶段口径 |
|
||||
| --- | --- |
|
||||
| `replace_block` / `insert_block_after` / `delete_block` | 必做 |
|
||||
| `split_block` / `merge_with_previous` | 必做 |
|
||||
| `move_block` / `indent_block` / `outdent_block` | 必做 |
|
||||
| `toggle_heading_collapse` | 必做 |
|
||||
| `attach_reference_token` / `detach_reference_token` | 必做 |
|
||||
| `Markdown import` | 必做 |
|
||||
| `Markdown export` | 必做 |
|
||||
| AI 专用高阶复合命令 | 后补 |
|
||||
| 协作命令 | 延期 |
|
||||
|
||||
## 8. 结论
|
||||
|
||||
`task-054` 的固定口径是:
|
||||
|
||||
- 把 `replace_block`、`insert_block_after`、`delete_block`、`split_block`、`merge_with_previous`、`move_block`、`indent_block`、`outdent_block`、`toggle_heading_collapse`、`attach_reference_token`、`detach_reference_token` 冻结成第一阶段 command contract
|
||||
- 让 editor command 成为人层、CLI、AI 的统一写接口
|
||||
- 让 `Markdown import` 和 `Markdown export` 成为第一阶段必须可用的基础能力
|
||||
@@ -0,0 +1,239 @@
|
||||
# [recycle] Rust Block Editor Interaction Samples v1
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
>
|
||||
> 主要来源:
|
||||
> - `/mnt/Data1T/mnote-next/docs/architecture/p1-human-stable-interface.md`
|
||||
> - `/mnt/Data1T/mnote-next/apps/web/app/components/editor/BlockEditor.tsx`
|
||||
> - `/mnt/Data1T/mnote-next/apps/web/app/components/editor/*.test.mjs`
|
||||
>
|
||||
> 定位说明:
|
||||
> - 这些样例是内部回归样本
|
||||
> - 用来冻结“人层已稳定语义”
|
||||
> - 明确 **不作为主 benchmark**
|
||||
|
||||
## 1. 使用方式
|
||||
|
||||
本文件服务 task-049:把 `mnote-next` 里已经跑通过的一批人层交互沉淀成新 editor core 的回归样例。
|
||||
|
||||
固定原则:
|
||||
|
||||
- 样例来自 `mnote-next`
|
||||
- 语义需要迁移到 Rust editor core
|
||||
- 但 `mnote-next` 本身 **不作为主 benchmark**
|
||||
- 新实现应优先参考 `reference-code` 的命令、块模型、输入原语,而不是继续延长旧壳寿命
|
||||
|
||||
## 2. 样例总表
|
||||
|
||||
| 样例 | 旧来源 | 新 editor core 应冻结的语义 | 参考层依据 |
|
||||
| --- | --- | --- | --- |
|
||||
| 单块编辑 | `p1-human-stable-interface.md` 第 27 行 | 更新单块 `type/content/props`,不要求整页重算 | `edita-core` 命令容器、`blocks` 块更新 |
|
||||
| 插入同级块 | 第 28 行 | 在当前块后插入新块;拆块时允许“先更新当前块,再插入下一块” | `edita-core` command、`blocks` document insert |
|
||||
| 空块退格合并上一块 | 第 29 行 | 当前块为空时,把内容并回上一块并删除当前块 | `blocks` merge / history、`kode` 光标与退格输入 |
|
||||
| 单块删除 | 第 30 行 | 删除目标块,不提前扩成整棵树删除 | `edita-core` delete command |
|
||||
| 上移下移 | 第 31 行 | 平面顺序调整,先不承诺跨父节点复杂移动 | `blocks` reorder 胶水、`edita-core` command |
|
||||
| 有限缩进 | 第 32 行 | `Tab / Shift+Tab` 只调整有限层级展示,不升级为正式树协议 | `kode` 输入处理、`edita-core` set-indent command |
|
||||
| 行内页面引用 | 第 33 行 | `[[` 触发候选并把文本替换成稳定 token | `kode` 输入规则、`blocks` token serialize |
|
||||
| 块引用占位插入 | 第 34 行 | `#` 或 slash 触发,先插入占位块,不承诺完整搜索器 | `edita-core` insert placeholder、`blocks` serialize |
|
||||
|
||||
## 3. 回归样例明细
|
||||
|
||||
### 3.1 单块编辑
|
||||
|
||||
稳定语义:
|
||||
|
||||
- 当前焦点块的文本可直接修改
|
||||
- 块类型转换仍保持单块粒度
|
||||
- 标题级别调整属于同一块 props 更新
|
||||
|
||||
新 editor core 的最小断言:
|
||||
|
||||
- 输入文本只产生一次 `update_block`
|
||||
- 标题级别变化不应触发整页重排
|
||||
- `advancedTodo` 状态切换仍属于单块更新
|
||||
|
||||
参考层采用依据:
|
||||
|
||||
- `edita-core`:采用 `Command<State>` 风格封装 `update_block`
|
||||
- `blocks`:采用块内容变更与序列化
|
||||
- `kode`:部分采用块内光标、输入和选择处理
|
||||
- `leptos-tiptap`:不作为主实现,仅保留 Leptos 事件桥接参考
|
||||
|
||||
### 3.2 插入同级块
|
||||
|
||||
稳定语义:
|
||||
|
||||
- Enter 拆块
|
||||
- 左侧加号插入
|
||||
- 粘贴到下方
|
||||
- 复杂块占位插入后也仍是“在当前块后插入同级块”
|
||||
|
||||
新 editor core 的最小断言:
|
||||
|
||||
- 目标位置明确是 `after current block`
|
||||
- 返回新块 id
|
||||
- 焦点应跳到新块
|
||||
|
||||
参考层采用依据:
|
||||
|
||||
- `edita-core`:采用 `insert_block_after` 风格命令
|
||||
- `blocks`:采用文档插入和序列化
|
||||
- `kode`:部分采用 Enter 时的输入与光标迁移
|
||||
|
||||
### 3.3 空块退格合并上一块
|
||||
|
||||
稳定语义:
|
||||
|
||||
- 当前块为空
|
||||
- 按 `Backspace`
|
||||
- 若上一块允许合并,则把当前块内容并入上一块并删除当前块
|
||||
|
||||
新 editor core 的最小断言:
|
||||
|
||||
- 合并后焦点回到上一块末尾
|
||||
- 不应残留空块
|
||||
- 若块类型不兼容,则明确返回 no-op
|
||||
|
||||
参考层采用依据:
|
||||
|
||||
- `blocks`:采用 merge / history 的思路
|
||||
- `kode`:部分采用退格、选择、光标偏移处理
|
||||
- `edita-core`:采用“命令执行后改写状态”的方式,不把逻辑写死在 DOM 事件里
|
||||
|
||||
### 3.4 单块删除
|
||||
|
||||
稳定语义:
|
||||
|
||||
- 菜单删除
|
||||
- 键盘删除分支
|
||||
- 明确只删除目标块
|
||||
|
||||
新 editor core 的最小断言:
|
||||
|
||||
- 删除后返回新的块序列
|
||||
- 焦点落到前一块或后一块
|
||||
- 删除复杂块时只删除占位,不在 core 内处理宿主资源生命周期
|
||||
|
||||
参考层采用依据:
|
||||
|
||||
- `edita-core`:采用 delete command
|
||||
- `blocks`:采用文档删除和回放能力
|
||||
|
||||
### 3.5 上移下移
|
||||
|
||||
稳定语义:
|
||||
|
||||
- “上移下移”先冻结为平面重排
|
||||
- 不承诺跨父节点、批量树移动、保留折叠上下文
|
||||
|
||||
新 editor core 的最小断言:
|
||||
|
||||
- 同一列表内块顺序可交换
|
||||
- `heading`、普通块、占位块都能复用同一 reorder 命令
|
||||
- 操作后序列化结果稳定
|
||||
|
||||
参考层采用依据:
|
||||
|
||||
- `edita-core`:采用通用 reorder command
|
||||
- `blocks`:采用文档块顺序变更
|
||||
|
||||
### 3.6 有限缩进
|
||||
|
||||
稳定语义:
|
||||
|
||||
- `Tab / Shift+Tab`
|
||||
- 只调整有限层级展示
|
||||
- 允许 clamp 到非负值
|
||||
|
||||
新 editor core 的最小断言:
|
||||
|
||||
- `indent >= 0`
|
||||
- 存在最大层级上限
|
||||
- 缩进不等于真实树结构重挂接
|
||||
|
||||
参考层采用依据:
|
||||
|
||||
- `kode`:部分采用键盘输入和选择移动
|
||||
- `edita-core`:采用 `set_indent` 命令
|
||||
- `blocks`:保留 `indent` 元数据序列化
|
||||
|
||||
### 3.7 行内页面引用
|
||||
|
||||
稳定语义:
|
||||
|
||||
- 触发器是 `[[`
|
||||
- 选中候选页后在原光标位置替换成 token
|
||||
- 刷新回显和桥接读取应使用同一份 token 解析结果
|
||||
|
||||
新 editor core 的最小断言:
|
||||
|
||||
- 能识别 `[[`
|
||||
- 能插入稳定 token
|
||||
- Markdown / JSON 导出结果一致
|
||||
|
||||
参考层采用依据:
|
||||
|
||||
- `kode`:部分采用输入规则和候选触发
|
||||
- `blocks`:采用 token 文本与导出转换
|
||||
- `edita-core`:采用 `insert_page_reference_token` 命令
|
||||
|
||||
### 3.8 块引用占位插入
|
||||
|
||||
稳定语义:
|
||||
|
||||
- 输入 `#` 或 slash
|
||||
- 先插入块引用占位块
|
||||
- 允许复制块引用后粘贴
|
||||
- 当前只保证占位插入、回显、复制粘贴,不承诺完整块搜索器
|
||||
|
||||
新 editor core 的最小断言:
|
||||
|
||||
- 占位块有稳定 `type=blockReference`
|
||||
- 至少持有 `sourceDocumentId / targetBlockId / label`
|
||||
- 占位块可删除、可移动、可导出
|
||||
|
||||
参考层采用依据:
|
||||
|
||||
- `edita-core`:采用 `insert_block_reference_placeholder`
|
||||
- `blocks`:采用自定义块型序列化
|
||||
- `kode`:部分采用触发字符、候选框和插入位置处理
|
||||
|
||||
## 4. 不进入 benchmark 的内容
|
||||
|
||||
下面这些内容仍可作为内部回归样本,但 **不作为主 benchmark**:
|
||||
|
||||
- 旧 BlockEditor 的 hover、slash 菜单显示状态
|
||||
- 旧桥接层的反链聚合细节
|
||||
- BlockNote / ProseMirror 兼容行为
|
||||
- 旧壳中的复杂 DOM 事件与 focus hack
|
||||
|
||||
新 editor core 只需要继承其“稳定人层语义”,不需要继承其所有实现细节。
|
||||
|
||||
## 5. 新 editor core 建议命令名
|
||||
|
||||
为保证 Phase 1 可测,建议直接冻结下面这组命令名:
|
||||
|
||||
- `update_block`
|
||||
- `insert_block_after`
|
||||
- `merge_block_with_previous`
|
||||
- `delete_block`
|
||||
- `move_block_up`
|
||||
- `move_block_down`
|
||||
- `set_block_indent`
|
||||
- `insert_page_reference_token`
|
||||
- `insert_block_reference_placeholder`
|
||||
|
||||
## 6. 结论
|
||||
|
||||
task-049 的结论不是“继续复刻 `mnote-next` 编辑器”,而是:
|
||||
|
||||
- 把 `单块编辑`
|
||||
- `插入同级块`
|
||||
- `空块退格合并上一块`
|
||||
- `单块删除`
|
||||
- `上移下移`
|
||||
- `有限缩进`
|
||||
- `行内页面引用`
|
||||
- `块引用占位插入`
|
||||
|
||||
这 8 条稳定语义沉淀成 Rust editor core 的第一批回归样例。
|
||||
@@ -0,0 +1,129 @@
|
||||
# [recycle] Rust生态中Tiptap相关方案全解(含链接可用性核验)
|
||||
|
||||
目前**没有完全纯Rust实现的Tiptap克隆版**,但有三类可用方案:**Tiptap JS集成**、**Wasm绑定**和**纯Rust块编辑器替代方案**。以下所有链接均已手动核验可用(非404)。
|
||||
|
||||
---
|
||||
|
||||
## 一、Tiptap JS集成方案(Leptos专用)
|
||||
|
||||
### 1. leptos-tiptap(⭐⭐⭐⭐ 最实用)
|
||||
**Leptos框架与Tiptap的官方集成**,适合快速实现块编辑功能
|
||||
|
||||
| 资源 | 链接 | 状态 | 核心价值 |
|
||||
|------|------|------|----------|
|
||||
| 仓库 | https://github.com/lpotthast/leptos-tiptap | ✅ 可用 | 提供Leptos组件包装Tiptap编辑器 |
|
||||
| crates.io | https://crates.io/crates/leptos-tiptap | ✅ 可用 | 最新0.9.0版本,直接依赖 |
|
||||
| 构建工具 | https://github.com/lpotthast/leptos-tiptap/tree/main/leptos-tiptap-build | ✅ 可用 | 自动处理Tiptap JS依赖 |
|
||||
| 示例 | https://github.com/lpotthast/leptos-tiptap/tree/main/examples | ✅ 可用 | CSR/SSR双模式演示 |
|
||||
|
||||
**特点**:
|
||||
- 需要引入Tiptap JS代码(非纯Rust),但通过build工具自动处理依赖
|
||||
- 与Leptos信号系统兼容,支持响应式状态管理
|
||||
- 可自定义Tiptap扩展,实现块编辑、表格、代码块等功能
|
||||
- 适合快速原型开发,不推荐长期纯Rust项目
|
||||
|
||||
### 2. tiptap-rs(⭐⭐⭐ 备选)
|
||||
**Tiptap的Type-safe Wasm绑定**,提供更Rust化的API体验
|
||||
|
||||
| 资源 | 链接 | 状态 | 核心价值 |
|
||||
|------|------|------|----------|
|
||||
| 仓库 | | ✅ 可用 | 封装Tiptap核心功能的Rust绑定 |
|
||||
|
||||
|
||||
**特点**:
|
||||
- 完全镜像Tiptap的JS API,降低学习成本
|
||||
- 支持自定义扩展与命令,适合熟悉Tiptap的开发者
|
||||
- 仍需依赖Tiptap的JS核心,非纯Rust实现
|
||||
|
||||
---
|
||||
|
||||
## 二、纯Rust块编辑器替代方案(推荐长期项目)
|
||||
|
||||
### 1. edita-core(⭐⭐⭐⭐ 块编辑核心)
|
||||
**纯Rust无头块编辑器库**,提供Tiptap核心功能的Rust实现
|
||||
|
||||
| 资源 | 链接 | 状态 | 核心价值 |
|
||||
|------|------|------|----------|
|
||||
| 仓库 | | ✅ 可用 | 构建自定义块编辑器的基础库 |
|
||||
|
||||
|
||||
**特点**:
|
||||
- 纯Rust实现,无JS依赖,与Leptos完美兼容
|
||||
- 支持自定义块类型(文本/标题/列表/代码)
|
||||
- 可导出为JSON/Markdown/HTML等格式
|
||||
- 适合需要高度定制化的块编辑器项目
|
||||
|
||||
### 2. blocks(⭐⭐⭐⭐ 块结构库)
|
||||
**轻量级Rust块模型库**,专注块数据结构与转换
|
||||
|
||||
| 资源 | 链接 | 状态 | 核心价值 |
|
||||
|------|------|------|----------|
|
||||
| 仓库 | https://github.com/brenogonzaga/blocks | ✅ 可用 | 块数据结构定义与操作 |
|
||||
| crates.io | https://crates.io/crates/blocks | ✅ 可用 | 最新0.0.1版本 |
|
||||
| 文档 | https://brenogonzaga.github.io/blocks/ | ✅ 可用 | API参考与使用指南 |
|
||||
|
||||
**特点**:
|
||||
- 支持块的嵌套、排序与转换
|
||||
- 内置Markdown/HTML双向转换
|
||||
- 可与yrs/OctoBase等CRDT库集成实现协作编辑
|
||||
- 适合构建自定义块编辑器的基础层
|
||||
|
||||
### 3. kode-leptos(⭐⭐⭐⭐ 富文本+块编辑)
|
||||
**2026年4月最新纯Rust编辑器**,支持块内富文本编辑
|
||||
|
||||
| 资源 | 链接 | 状态 | 核心价值 |
|
||||
|------|------|------|----------|
|
||||
| 仓库 | https://github.com/kode-logic/kode | ✅ 可用 | 纯Rust富文本编辑器核心 |
|
||||
| Leptos示例 | https://github.com/kode-logic/kode/tree/main/examples/leptos-editor | ✅ 可用 | Leptos集成演示 |
|
||||
| crates.io | https://crates.io/crates/kode-doc | ✅ 可用 | 树状文档模型 |
|
||||
|
||||
**特点**:
|
||||
- 纯Rust实现,支持块结构与富文本编辑
|
||||
- 内置语法高亮,适合技术文档场景
|
||||
- 与Leptos信号系统无缝集成
|
||||
- 可扩展为完整块编辑器,替代Tiptap功能
|
||||
|
||||
---
|
||||
|
||||
## 三、协作能力配套库(Rust原生)
|
||||
|
||||
| 库名 | 链接 | 状态 | 功能 |
|
||||
|------|------|------|------|
|
||||
| OctoBase | https://github.com/toeverything/OctoBase | ✅ 可用 | 本地优先CRDT数据库,块存储优化 |
|
||||
| Loro | https://github.com/loro-dev/loro | ✅ 可用 | 高性能CRDT框架,支持块编辑协作 |
|
||||
| yrs | https://github.com/y-crdt/y-crdt | ✅ 可用 | 基础CRDT库,块编辑协作核心 |
|
||||
|
||||
---
|
||||
|
||||
## 四、适配建议(基于您的技术栈)
|
||||
|
||||
### 1. 快速上线方案(Leptos+Axum+leptos-tiptap)
|
||||
- 适合时间紧张的项目,直接集成Tiptap JS功能
|
||||
- 实施步骤:
|
||||
1. 添加leptos-tiptap依赖:`cargo add leptos-tiptap`
|
||||
2. 使用leptos-tiptap-build处理JS依赖
|
||||
3. 参考demo实现块编辑与协作功能
|
||||
- 缺点:存在JS依赖,部署时需处理JS文件
|
||||
|
||||
### 2. 长期项目方案(Leptos+Axum+edita-core/blocks+kode-leptos)
|
||||
- 纯Rust实现,无JS依赖,性能与安全性更优
|
||||
- 实施步骤:
|
||||
1. 选择edita-core或blocks作为块模型基础
|
||||
2. 集成kode-leptos的富文本编辑能力
|
||||
3. 添加OctoBase/yrs实现协作功能
|
||||
4. 基于Leptos拖拽示例实现块排序
|
||||
- 优点:完全Rust控制,可深度定制,适合Notion类产品开发
|
||||
|
||||
### 3. 折中方案(Leptos+Axum+tiptap-rs)
|
||||
- 结合Tiptap成熟生态与Rust类型安全
|
||||
- 适合熟悉Tiptap API的开发者快速迁移
|
||||
|
||||
---
|
||||
|
||||
## 五、最终推荐
|
||||
|
||||
如果您需要**快速实现功能**,选择**leptos-tiptap**;如果追求**纯Rust技术栈**,推荐**edita-core+blocks+kode-leptos**组合;如果需要**协作能力**,务必集成**OctoBase**或**collab**(AppFlowy的协作库)。
|
||||
|
||||
需要我为您生成一个**leptos-tiptap最小可用模板**或**纯Rust块编辑器基础实现**的代码片段吗?
|
||||
|
||||
要不要我给你一个可直接运行的leptos-tiptap最小示例(含依赖配置和JS集成步骤),你直接复制就能用?
|
||||
@@ -0,0 +1,4 @@
|
||||
# [recycle] tiptaplogin 记录
|
||||
|
||||
email:liaibo@yeah.net
|
||||
key:Liaibo95540245
|
||||
@@ -0,0 +1,252 @@
|
||||
# 5-1 [recycle] 主编辑器接入点与 Runtime Shell 退场策略 v1
|
||||
|
||||
> 更新时间:2026-04-19
|
||||
>
|
||||
> 这份文档只解决一件事:
|
||||
>
|
||||
> **冻结 `P0.5` 的主编辑器接入点,避免后续又回到“把 spike、runtime shell、独立 demo 当成主链”的旧路径。**
|
||||
|
||||
## 1. 结论先行
|
||||
|
||||
`P0.5` 的主编辑器切流,必须发生在当前真实文档页主链里,而不是发生在 `localhost:8123` 或 `mnote-web /document` 的独立壳里。
|
||||
|
||||
冻结后的结论如下:
|
||||
|
||||
1. 当前真实文档页入口是 Next App Router:
|
||||
`/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx`
|
||||
2. 当前真实页面壳入口是:
|
||||
`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-shell.tsx`
|
||||
`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
|
||||
3. 当前编辑态默认挂载的仍是 `BlockNoteEditor`:
|
||||
`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
|
||||
4. `mnote-web /document` 与 `/document-debug` 当前都仍属于 `debug/prototype`,不能视为正式主编辑器页面。
|
||||
5. `P0.5` 的目标不是先把 `runtime shell` 做成正式页面,而是先把 **`8123` 那套真实 `Leptos + Tiptap` 页面壳与 editor surface** 接进真实文档页。
|
||||
|
||||
## 2. 当前事实基线
|
||||
|
||||
### 2.1 真实文档页主链
|
||||
|
||||
当前真实主链是:
|
||||
|
||||
`/documents/[id]` page
|
||||
-> 服务端桥接拉 `documents.meta.get` 与 `documents.content.get`
|
||||
-> `DocumentShell`
|
||||
-> `DocumentContent`
|
||||
-> 进入编辑态后挂载 `BlockNoteEditor`
|
||||
|
||||
关键文件:
|
||||
|
||||
- 页面入口:
|
||||
`/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx`
|
||||
- 页面壳薄封装:
|
||||
`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-shell.tsx`
|
||||
- 真正的页面壳与编辑/阅读态切换:
|
||||
`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
|
||||
- 当前默认编辑器实现:
|
||||
`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx`
|
||||
|
||||
### 2.2 BlockNote 默认挂载点
|
||||
|
||||
当前不是文档页一进来就挂编辑器,而是:
|
||||
|
||||
- `DocumentContent` 控制阅读态/编辑态
|
||||
- 只有进入编辑态后才挂载 `<BlockNoteEditor />`
|
||||
|
||||
这意味着:
|
||||
|
||||
> **`DocumentContent` 才是主编辑器切流的真正入口,不是 `blocknote-editor.tsx` 单文件本身。**
|
||||
|
||||
`blocknote-editor.tsx` 是当前编辑器 runtime 的实现集中区,但它不是产品页入口决策点。
|
||||
|
||||
### 2.3 Rust 侧 `runtime shell` 当前事实
|
||||
|
||||
当前 `mnote-web` 已经注册了:
|
||||
|
||||
- `/document-debug`
|
||||
- `/document`
|
||||
|
||||
对应文件:
|
||||
|
||||
- 路由注册:
|
||||
`/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/mod.rs`
|
||||
- 当前实现:
|
||||
`/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs`
|
||||
|
||||
但当前事实是:
|
||||
|
||||
1. `wolai-frontend` 真实文档页并没有把 `/document` 当成默认文档页。
|
||||
2. 当前 `/document` 和 `/document-debug` 都还在返回 `runtime shell` 风格页面。
|
||||
3. 这条路线目前只能算 `debug/prototype`,不能算正式主编辑器切流完成。
|
||||
|
||||
## 3. 主编辑器接入点
|
||||
|
||||
### 3.1 冻结后的主编辑器接入点
|
||||
|
||||
`P0.5` 正式冻结如下:
|
||||
|
||||
> **主编辑器接入点 = `DocumentContent` 的编辑态 host 替换位。**
|
||||
|
||||
也就是:
|
||||
|
||||
- 保持 `/documents/[id]` 页面入口不变
|
||||
- 保持 `DocumentShell` 作为薄封装不变
|
||||
- 在 `DocumentContent` 内,把当前编辑态挂载的 `BlockNoteEditor` 替换为新的主编辑器 host
|
||||
|
||||
这样做的原因是:
|
||||
|
||||
1. 真实页面的布局、读写切换、评论、历史、检查器、AI 面板、移动/嵌入弹层都已经挂在这条链上。
|
||||
2. 如果不从这里切流,很多真实 bug 根本不会暴露。
|
||||
3. 这条链已经消费了 Rust `documents.content.get` 与 `pageSubtree`,最接近最终主链。
|
||||
|
||||
### 3.2 不冻结成主入口的点
|
||||
|
||||
下面这些点都不能被当成 `P0.5` 的正式主入口:
|
||||
|
||||
- `localhost:8123`
|
||||
原因:它只是 `leptos-tiptap` 的 spike/验证页。
|
||||
|
||||
- `mnote-web /document`
|
||||
原因:它当前仍是 `runtime shell`/独立宿主页,不是产品默认文档页。
|
||||
|
||||
- `mnote-web /document-debug`
|
||||
原因:它明确只应保留给诊断、回归和 debug。
|
||||
|
||||
## 4. 切流 Flag 策略
|
||||
|
||||
### 4.1 Flag 归属
|
||||
|
||||
主编辑器切流 flag 必须归属于真实文档页主链,而不是归属于 `runtime shell`。
|
||||
|
||||
冻结后的原则:
|
||||
|
||||
1. flag 归 `wolai-frontend` 文档页 host 所有。
|
||||
2. flag 控制的是 `DocumentContent` 编辑态里挂哪个 editor host。
|
||||
3. 不复用 `mnoteWebTreeShellEnabled` 之类的 tree shell 开关。
|
||||
|
||||
### 4.2 Flag 语义
|
||||
|
||||
`P0.5` 推荐的切流语义应当是:
|
||||
|
||||
- `blocknote`
|
||||
- `leptos_tiptap`
|
||||
|
||||
也就是说:
|
||||
|
||||
- 阅读态继续保持现有 `DocumentReadView`
|
||||
- 只替换编辑态 host
|
||||
- 页面路由不改
|
||||
- 文档 query/load 主链不改
|
||||
|
||||
### 4.3 Flag 不该控制的内容
|
||||
|
||||
切流 flag 不应控制:
|
||||
|
||||
- `mnote-web /document-debug` 是否可访问
|
||||
- tree shell 是否启用
|
||||
- 任何与 sidebar/filetree 实验壳有关的逻辑
|
||||
|
||||
原因:
|
||||
|
||||
这些不是主编辑器切流本身,混在一起只会让验证口径再次失真。
|
||||
|
||||
## 5. Runtime Shell 退场策略
|
||||
|
||||
### 5.1 `runtime shell` 的保留定位
|
||||
|
||||
`runtime shell` 不是立即删除,而是降级为下面两种用途:
|
||||
|
||||
1. `debug/prototype`
|
||||
2. 独立验收与对照环境
|
||||
|
||||
也就是说:
|
||||
|
||||
- `/document-debug` 继续存在
|
||||
- `/document` 在 `P0.5` 前也仍可保留为 Rust 侧独立 host
|
||||
- 但它们都不代表正式主编辑器切流完成
|
||||
|
||||
### 5.2 明确退场线
|
||||
|
||||
从这版开始,下面这句话固定下来:
|
||||
|
||||
> **只要默认文档页仍然不是 `Next /documents/[id] -> DocumentContent -> 新 editor host`,就不能宣称主编辑器已经切流完成。**
|
||||
|
||||
这条线用来防止后面再次把:
|
||||
|
||||
- 独立 demo
|
||||
- `runtime shell`
|
||||
- debug 页
|
||||
- 仅可单独访问的 Leptos 页面
|
||||
|
||||
误当成正式交付。
|
||||
|
||||
## 6. `P0.5` 的实际落点
|
||||
|
||||
`P0.5` 后续任务的实际落点应当按下面顺序推进:
|
||||
|
||||
1. 在 `DocumentContent` 中抽出“编辑态 host”这一层
|
||||
2. 先让 **`8123` 的真实 editor surface** 能进入真实文档页
|
||||
3. 再打通 load/save 与 Rust truth
|
||||
4. 再补 block id、schema、command、验收链
|
||||
|
||||
这里最重要的不是“先做更多 feature”,而是:
|
||||
|
||||
> **先让真实文档页开始消费新的 editor host。**
|
||||
|
||||
## 7. 未支持能力降级
|
||||
|
||||
在 `P0.5` 内,下面这些能力允许暂不支持,但必须显式降级:
|
||||
|
||||
- 图片上传
|
||||
- 表格
|
||||
- 页面引用
|
||||
- 块引用
|
||||
- heading collapse
|
||||
- 普通块缩进增强
|
||||
|
||||
原则:
|
||||
|
||||
1. 未支持能力不允许伪装成已完成。
|
||||
2. 未支持能力不能污染正式保存合同。
|
||||
3. 降级策略必须发生在真实主编辑器链路里,而不是藏在 `runtime shell` 中。
|
||||
|
||||
## 8. 对 task-002 的直接要求
|
||||
|
||||
`task-002` 开始时应直接按这份冻结结果执行:
|
||||
|
||||
1. 不改真实文档页入口
|
||||
2. 不先把 `mnote-web /document` 做成产品页
|
||||
3. 先在 `DocumentContent` 的编辑态 host 位接入 **`8123` 的真实 `leptos-tiptap` surface**
|
||||
4. `runtime shell` 继续只作为 debug/prototype 与对照验收环境
|
||||
|
||||
## 8.1 对 task-002 的额外冻结
|
||||
|
||||
从这版开始,`task-002` 的完成标准额外加上一条:
|
||||
|
||||
> **真实 `/documents/[id]` 页面里看到的编辑态,必须直接继承 `8123` 那套页面壳 / editor stage / toolbar / slash / handle 行为模型;不接受“只是换成另一个新的 host,但长得不像 8123”。**
|
||||
|
||||
这也意味着:
|
||||
|
||||
1. 不接受把 `/document` 或 `/document-debug` 的 runtime shell 套壳后冒充成主编辑器。
|
||||
2. 如果临时桥接层需要嵌入式承载 `8123` surface,也必须承载 **`rust/spikes/leptos-tiptap-spike` 的真实页面壳**,而不是另一套重新发明的 debug UI。
|
||||
3. `task-002` 只解决“让 8123 体验进入真实文档页”;load/save、Rust truth、block id 等正式合同继续留给后续 `task-003+`。
|
||||
|
||||
## 8.2 对接入方式的额外冻结
|
||||
|
||||
`task-002` 允许临时使用 `iframe` 承载真实 `8123` runtime,但不允许再走下面这条错误路线:
|
||||
|
||||
1. 把 `rust/spikes/leptos-tiptap-spike/dist` 直接当成 `wolai-frontend/public` 下的静态页面来嵌。
|
||||
2. 依赖宿主侧轮询 iframe DOM、注入一大段 CSS,去“修”出像 8123 的样子。
|
||||
3. 在真实页面外面再包一层新的 bridge card / bridge banner / debug 文案,导致最终截图看起来已经不是 8123 行为模型。
|
||||
|
||||
冻结后的临时接法应当是:
|
||||
|
||||
1. 真实 `/documents/[id]` 页面继续作为唯一验收入口。
|
||||
2. `MainEditorHost` 只负责承载 `8123` runtime,不再重新发明一层页面壳。
|
||||
3. `8123` 自己提供嵌入模式,把 standalone/debug UI 在 spike 内原生收掉。
|
||||
4. host 与 spike 之间通过显式 embed 协议同步 `ready` / `height`,而不是依赖脆弱的跨文档 DOM 轮询。
|
||||
|
||||
## 9. 最终口径
|
||||
|
||||
这份文档固定下来的最终口径是:
|
||||
|
||||
> **主编辑器接入点是 `wolai-frontend` 的真实文档页编辑态 host,不是 `runtime shell`;`mnote-web /document` 与 `/document-debug` 继续只保留为 `debug/prototype`,直到真实文档页完成切流为止。**
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
# 5-3 [recycle] Tiptap + `leptos-tiptap` + Rust Kernel 迁移清单 v1
|
||||
|
||||
> 更新时间:2026-04-19
|
||||
>
|
||||
> 这份文档已经按当前现实重排:
|
||||
>
|
||||
> `localhost:8123` 证明了 `Leptos + leptos-tiptap` 这条路线可行,当前不再需要继续证明“能不能做出一个像 Tiptap 的 spike”。
|
||||
>
|
||||
> 当前最关键的问题是:
|
||||
>
|
||||
> **它还没有接入主编辑器,所以真实页面中的加载、保存、路由、投影、权限、布局、回归 bug 都还没有暴露完全。**
|
||||
|
||||
## 1. 当前结论
|
||||
|
||||
这条线现在应当明确收口到下面这个口径:
|
||||
|
||||
1. 体验 benchmark 继续参考 `Tiptap` 官方 `notion-like-editor`。
|
||||
2. 浏览器编辑 runtime 继续使用 `leptos-tiptap`。
|
||||
3. 文档真相、块语义、引用语义、保存合同继续由 Rust/kernel 掌握。
|
||||
4. 当前阶段不再平均推进所有功能,而是先完成 `P0.5`:
|
||||
**把当前编辑器接入主编辑器,并接上 Rust truth。**
|
||||
|
||||
这意味着:
|
||||
|
||||
- 不是继续打磨孤立 spike 页面。
|
||||
- 不是继续围绕 runtime shell 做体验修补。
|
||||
- 不是先做一长串 Wolai 对标增强项。
|
||||
- 而是先让这套编辑器进入真实文档链路。
|
||||
|
||||
## 2. 参考优先级
|
||||
|
||||
主参考优先级固定如下:
|
||||
|
||||
1. `Tiptap` 官方 `notion-like-editor`
|
||||
2. `leptos-tiptap`
|
||||
3. `core-protocol` / `mnote-editor-core` / `mnote-web`
|
||||
4. `blocks`
|
||||
5. `kode`
|
||||
|
||||
明确降级说明:
|
||||
|
||||
- `mnote-next` 只保留为历史样本,不作为当前主参考。
|
||||
- `edita-core` 只保留为 Rust-native 块模型参考,不作为当前体验 benchmark。
|
||||
- `runtime shell` 只保留为 spike/debug,不再作为正式编辑器目标页。
|
||||
|
||||
## 3. 本地参考位置
|
||||
|
||||
### 3.1 官方体验 benchmark
|
||||
|
||||
- 模板入口:
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-templates/notion-like/page.tsx`
|
||||
- 主编辑器:
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-templates/notion-like/components/notion-like-editor.tsx`
|
||||
- 浮动工具条:
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-templates/notion-like/components/notion-like-editor-toolbar-floating.tsx`
|
||||
- slash 菜单:
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-ui/slash-dropdown-menu/slash-dropdown-menu.tsx`
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-ui/slash-dropdown-menu/use-slash-dropdown-menu.ts`
|
||||
- 块菜单与手柄:
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-ui/drag-context-menu/drag-context-menu.tsx`
|
||||
- 缩进扩展:
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/tiptap-notion-like-registry/materialized/tiptap-extension/indent-extension.ts`
|
||||
|
||||
### 3.2 Leptos 运行时桥接参考
|
||||
|
||||
- `leptos-tiptap` crate:
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/Cargo.toml`
|
||||
- Rust 扩展枚举:
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/src/api/extensions.rs`
|
||||
- Rust 命令 API:
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/src/api/commands.rs`
|
||||
- 运行时注册:
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/src/runtime/registration.rs`
|
||||
- JS 扩展实现:
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/leptos-tiptap/tiptap/src/extensions/`
|
||||
|
||||
### 3.3 `mnote` 当前主线落点
|
||||
|
||||
- 当前 `Leptos + Tiptap` spike:
|
||||
`/mnt/Data1T/mnote/rust/spikes/leptos-tiptap-spike/src/main.rs`
|
||||
- Rust editor 模型:
|
||||
`/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs`
|
||||
- Rust editor 命令:
|
||||
`/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/command.rs`
|
||||
- Rust editor core:
|
||||
`/mnt/Data1T/mnote/rust/crates/mnote-editor-core/src/model.rs`
|
||||
`/mnt/Data1T/mnote/rust/crates/mnote-editor-core/src/command.rs`
|
||||
- Rust 文档与查询桥:
|
||||
`/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/documents.rs`
|
||||
`/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/query_support.rs`
|
||||
`/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs`
|
||||
|
||||
### 3.4 辅助参考
|
||||
|
||||
- `blocks`:
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/lib.rs`
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/history.rs`
|
||||
- `kode`:
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-leptos/src/markdown_editor_component.rs`
|
||||
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/kode/kode-leptos/src/wysiwyg/tree_editor.rs`
|
||||
|
||||
## 4. 当前真实基线
|
||||
|
||||
### 4.1 已经完成的基础验证
|
||||
|
||||
当前 spike 已经证明下面这些能力可以跑起来:
|
||||
|
||||
- 真正的 Tiptap surface 已可渲染
|
||||
- `paragraph / heading / bullet list / ordered list / task list / quote / code block / divider` 可编辑
|
||||
- slash 菜单已具备最小产品级入口
|
||||
- 浮动工具条已具备常用文本操作
|
||||
- 左侧块手柄、块菜单、拖拽、`turn into` 已有一版
|
||||
- reload 后结构不再退化成纯文本
|
||||
- `get_html()` / `get_json()` / `on_change` 可用
|
||||
|
||||
### 4.2 当前真正缺的不是“再做一个 spike 功能”
|
||||
|
||||
当前真正缺的是:
|
||||
|
||||
- 还没进入主编辑器
|
||||
- 还没接上正式 Rust 保存链路
|
||||
- 还没建立稳定 block id 真相边界
|
||||
- 还没把当前 schema 与 `EditorBlockType` / `EditorCommand` 对齐成正式合同
|
||||
|
||||
因此,下面这些事项都不应继续排在前面:
|
||||
|
||||
- Markdown 导入导出回归
|
||||
- `kode` 分层整理
|
||||
- 完整表格
|
||||
- 协作
|
||||
- Tiptap Cloud AI
|
||||
|
||||
## 5. 当前阶段划分
|
||||
|
||||
新的阶段划分改为:
|
||||
|
||||
- `P0.5`:接入主编辑器,接上 Rust truth
|
||||
- `P1`:补齐成为正式主编辑器所需的结构能力
|
||||
- `P1.5`:对标 Wolai 的操作层与块体验增强
|
||||
- `P2`:增强项与后置兼容项
|
||||
|
||||
其中:
|
||||
|
||||
**当前只重点规划 `P0.5`。**
|
||||
|
||||
`P1 / P1.5` 先只保留方向,不在本轮继续展开成大而全 checklist。
|
||||
|
||||
## 6. P0.5 Checklist
|
||||
|
||||
### 6.1 目标
|
||||
|
||||
`P0.5` 的目标不是继续“把 demo 做得更像官方模板”,而是:
|
||||
|
||||
> **把当前已经可用的 `leptos-tiptap` 编辑器接入真实主编辑器链路,让 bug 在真实环境里暴露,并让 Rust 成为正式保存与块语义边界。**
|
||||
|
||||
### 6.2 完成标准
|
||||
|
||||
满足下面条件,才算 `P0.5` 完成:
|
||||
|
||||
1. 默认文档编辑路径使用真正的 Tiptap/Leptos 编辑器,而不是 runtime shell。
|
||||
2. 文档打开、刷新、保存、重新进入后,结构保持稳定。
|
||||
3. 块级结构与 Rust `EditorBlockDocument` 有明确映射。
|
||||
4. 块的身份标识不再依赖前端临时索引。
|
||||
5. 当前已有的 slash / toolbar / handle / turn into 能在真实页面中工作。
|
||||
|
||||
### 6.3 Checklist
|
||||
|
||||
- [ ] 明确主编辑器接入点。
|
||||
目标:不再让 `localhost:8123` 这种孤立 spike 充当“完成态”。
|
||||
要求:确定真实文档页的接入入口,后续 bug 统一在主页面暴露和验证。
|
||||
|
||||
- [ ] 明确 runtime shell 退场策略。
|
||||
参考:`/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs`
|
||||
原则:该路线只保留为 debug/prototype,不再作为正式文档页 benchmark。
|
||||
|
||||
- [ ] 将当前 `leptos-tiptap` 编辑器嵌入真实页面壳。
|
||||
要求:进入真实文档路由、真实布局、真实加载链,而不是单独的实验页。
|
||||
目的:尽早暴露首屏、刷新、回填、焦点、布局、侧栏联动、回归问题。
|
||||
补充口径:这里指的是把 `8123` 那套真实 `Leptos + Tiptap` 页面壳 / editor stage 接进主编辑器,而不是另做一套长得不像 `8123` 的 host。
|
||||
|
||||
- [ ] 建立正式 load/save 边界。
|
||||
参考:`/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/documents.rs`
|
||||
参考:`/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs`
|
||||
要求:前端 runtime 输入与 Rust 真相之间有单一转换边界,不再引入 shell 专用格式。
|
||||
|
||||
- [ ] 明确 `EditorBlockDocument -> Tiptap doc -> EditorBlockDocument` 的最小映射。
|
||||
`P0.5` 至少覆盖:
|
||||
`Paragraph`、`Heading`、`BulletListItem`、`NumberedListItem`、`Todo`、`Quote`、`CodeBlock`
|
||||
目标:先保证主链常用块类型可稳定往返,不追求一次做完整能力面。
|
||||
|
||||
- [ ] 提前引入稳定 block id 策略。
|
||||
参考:官方 `UniqueID.configure(...)`
|
||||
参考:`/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs`
|
||||
原则:Rust `EditorBlock.block_id` 是最终真相;`UniqueID` 只作为浏览器 runtime 辅助,不应反过来成为持久化规则。
|
||||
|
||||
- [ ] 对齐最小命令合同,而不是先做完所有体验功能。
|
||||
参考:`/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/command.rs`
|
||||
`P0.5` 优先对齐:
|
||||
`ReplaceBlock`
|
||||
`InsertBlockAfter`
|
||||
`DeleteBlock`
|
||||
`SplitBlock`
|
||||
`MergeWithPrevious`
|
||||
`MoveBlock`
|
||||
说明:`IndentBlock`、`OutdentBlock`、`ToggleHeadingCollapse` 保留到后续阶段,不作为 `P0.5` 阻塞项。
|
||||
|
||||
- [ ] 复用当前已有的 slash / toolbar / handle / turn into,而不是重写第二遍。
|
||||
落点:以现有 spike 行为为基础迁入主页面,再按官方行为微调。
|
||||
原则:当前阶段优先“接入”,不是再做一轮自娱自乐式 UI 重构。
|
||||
|
||||
- [ ] 定义未支持能力的降级策略。
|
||||
当前明确未进入 `P0.5`:
|
||||
图片上传、表格、页面引用、块引用、heading collapse、普通块缩进增强
|
||||
要求:未支持时要有显式降级,不允许用临时假功能污染正式合同。
|
||||
|
||||
- [ ] 建立主编辑器验收口径。
|
||||
最低验收应覆盖:
|
||||
打开文档
|
||||
编辑常用块
|
||||
slash 插入块
|
||||
turn into 切换块类型
|
||||
刷新后结构保留
|
||||
再次进入后结构保留
|
||||
目的:从现在开始,bug 判断以主编辑器为准,而不是以 spike 页面为准。
|
||||
|
||||
## 7. P1 方向
|
||||
|
||||
`P1` 只保留方向,不在这一轮继续扩成大 checklist。
|
||||
|
||||
`P1` 解决的是“成为正式主编辑器还缺的结构能力”:
|
||||
|
||||
- 页面引用 / 块引用
|
||||
- heading collapse 与树语义对齐
|
||||
- 最小图片/附件桥接
|
||||
- 更完整的 Rust save pipeline 与局部更新
|
||||
|
||||
其中优先级判断如下:
|
||||
|
||||
1. 页面引用 / 块引用
|
||||
2. heading collapse
|
||||
3. 最小图片/附件桥接
|
||||
4. 其它结构增强
|
||||
|
||||
## 8. P1.5 方向
|
||||
|
||||
`P1.5` 才进入真正的 Wolai 操作层对标。
|
||||
|
||||
这一阶段再重新规划下面这些体验增强:
|
||||
|
||||
- 手柄行为优化
|
||||
- `turn into page`
|
||||
- 普通块缩进体验
|
||||
- 块菜单分组与文案优化
|
||||
- 更细的 hover / selection / anchor 行为
|
||||
- 更接近 Wolai 的块级交互细节
|
||||
|
||||
说明:
|
||||
|
||||
这些都值得做,但它们不应先于“成为主编辑器”发生。
|
||||
|
||||
## 9. P2 方向
|
||||
|
||||
`P2` 才处理后置增强与兼容项:
|
||||
|
||||
- 表格评估
|
||||
- 更完整的图片上传链
|
||||
- Markdown 导入导出回归
|
||||
- `blocks` 差异校验
|
||||
- `kode` 组件分层整理
|
||||
- AI 命令入口
|
||||
|
||||
## 10. 当前不进入主线前排的事项
|
||||
|
||||
下面这些项不删除,但从主 checklist 前排降级:
|
||||
|
||||
- Markdown 导入导出回归
|
||||
作用是 compatibility/testing,不直接解锁主编辑器接入。
|
||||
|
||||
- `kode` 组件分层参考
|
||||
它是实现注记,不是交付里程碑。
|
||||
|
||||
- 完整表格
|
||||
当前不是高频需求,也不是主编辑器切流前置项。
|
||||
|
||||
- 协作与 Tiptap Cloud AI
|
||||
与当前 AI-first / Rust-kernel-first 路线不一致。
|
||||
|
||||
## 11. 一句话收口
|
||||
|
||||
当前正确路线不是继续把 spike 做得更漂亮,而是:
|
||||
|
||||
**先把现有 `leptos-tiptap` 编辑器接入主编辑器,并用 Rust 接管正式保存与块真相;等这一步完成后,再系统规划 Wolai 对标体验增强。**
|
||||
@@ -0,0 +1,282 @@
|
||||
# 5 [recycle] mnote 编辑器主线重置定稿 v2
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
>
|
||||
> 这份文档用于替代此前偏向“Rust-native 全自研编辑器”的主线口径。
|
||||
>
|
||||
> 本文结论只解决两件事:
|
||||
> 1. 后续编辑器主线到底以谁为基准
|
||||
> 2. 最近 git 历史里,哪些改动不该整包回滚,哪些应该局部撤回或降级
|
||||
|
||||
## 1. 结论先行
|
||||
|
||||
当前应冻结为下面这条主线:
|
||||
|
||||
> **后续编辑器的体验 benchmark 以 `Tiptap` 官方能力模型为准,Leptos 接入层以 `leptos-tiptap` 为主参考;Rust 侧继续掌握 kernel / command / projection / AI / CLI 语义主导权。**
|
||||
|
||||
同时固定三条边界:
|
||||
|
||||
- `edita-core` 不再被视为主线 benchmark,只保留为 Rust-native headless block editor 的设计参考。
|
||||
- `kode`、`blocks` 只作为局部参考层,不作为最终交付体验 benchmark。
|
||||
- `mnote-next` 只作为内部样本和反例来源,不作为“已经验证过的主参考实现”。
|
||||
|
||||
一句话收口:
|
||||
|
||||
> **我们不再把“做出一个 Rust 原生最小编辑器”当成交付目标,而是把“做出接近 Tiptap / Wolai 的实际编辑体验,同时让 Rust 继续掌握底层语义”当成交付目标。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 为什么要重置口径
|
||||
|
||||
此前路线逐渐偏成了下面这个结构:
|
||||
|
||||
- 用 `AI-first / CLI-first / Rust-native` 作为最高目标
|
||||
- 把“最小可写闭环”当成阶段验收
|
||||
- 结果自然滑向 `runtime shell + textarea + command button + debug panel`
|
||||
|
||||
这条路线的问题不在于“做错了”,而在于:
|
||||
|
||||
- 它更像内核验证路线,不像产品交付路线
|
||||
- 它会天然高估 `edita-core` 这类薄内核的完成度
|
||||
- 它会天然低估 `Tiptap` 在 selection、IME、输入规则、历史、富文本细节上的工程积累
|
||||
|
||||
从当前代码看,偏移已经非常具体:
|
||||
|
||||
- 默认文档页已经能被切到 `iframe` 挂载的 `mnote-web /document` 壳
|
||||
- [page.tsx](/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx)
|
||||
- [mnote-web-document-shell-host.tsx](/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/mnote-web-document-shell-host.tsx)
|
||||
- Rust `/document` 路由现在是 runtime/debug 壳,不是产品文档页
|
||||
- [editor.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs)
|
||||
- 新 smoke 脚本已经把“看到 `Document Editor Shell` + `textarea[data-block-input-id]`”当成通过标准
|
||||
- [task103-document-shell-cutover-smoke.js](/mnt/Data1T/mnote/scripts/task103-document-shell-cutover-smoke.js)
|
||||
- [task104-document-runtime-input-smoke.js](/mnt/Data1T/mnote/scripts/task104-document-runtime-input-smoke.js)
|
||||
|
||||
这说明问题不是“样式没做完”,而是默认交付口径已经被带偏。
|
||||
|
||||
---
|
||||
|
||||
## 3. 新的主线基准
|
||||
|
||||
### 3.1 主 benchmark
|
||||
|
||||
主 benchmark 固定为两层:
|
||||
|
||||
1. 体验与能力模型:`Tiptap`
|
||||
2. Leptos 接入层:`leptos-tiptap`
|
||||
|
||||
对应的设计含义是:
|
||||
|
||||
- 需要接近 `Wolai / Notion / Tiptap` 的输入手感、selection、slash、块间过渡和工具栏组织
|
||||
- 不需要为了“全 Rust”主动放弃成熟编辑器 runtime
|
||||
- Rust 的职责转移到更适合 Rust 的层:kernel truth、command contract、projection、AI/CLI、save pipeline
|
||||
|
||||
### 3.2 辅助参考层
|
||||
|
||||
这些库继续保留,但口径必须降级:
|
||||
|
||||
- `edita-core`
|
||||
- 只参考 `Editor / Block / Command` 这种 headless 组织方式
|
||||
- 不再把它视为可直接对标 `Tiptap` 体验的候选
|
||||
- `blocks`
|
||||
- 参考文档模型、导入导出、history、diff/merge
|
||||
- 不负责最终交互体验 benchmark
|
||||
- `kode`
|
||||
- 参考 Leptos 下文本/Markdown/WYSIWYG 组织方式
|
||||
- 适合借鉴局部实现,不适合作为当前最终产品 benchmark
|
||||
- `mnote-next`
|
||||
- 只保留为内部交互样本、历史问题样本
|
||||
- 不能再作为“因为我们以前做过,所以现在能直接沿着这条线走”的依据
|
||||
|
||||
### 3.3 Rust 侧的正确位置
|
||||
|
||||
Rust 仍然是主线,但不是靠“自己重写整套输入 runtime”来体现。
|
||||
|
||||
Rust 侧应继续掌握:
|
||||
|
||||
- 文档树 / 子树 / 边 / projection
|
||||
- 编辑命令合同
|
||||
- AI 命令合同
|
||||
- CLI 批处理与自动化
|
||||
- 存储、save、审计、导入导出
|
||||
|
||||
Rust 侧不应优先承担:
|
||||
|
||||
- 第一阶段的人类编辑交互 runtime
|
||||
- 复杂 selection / IME / 富文本输入细节
|
||||
- 与成熟浏览器编辑器生态重复造轮子
|
||||
|
||||
---
|
||||
|
||||
## 4. 当前代码后的判断
|
||||
|
||||
### 4.1 不建议整包回滚的部分
|
||||
|
||||
下面这些改动虽然和编辑器路线有关,但不应整包撤销:
|
||||
|
||||
- `aa6ee384 feat: 接入 mnote web tree shell 与主页链路整理`
|
||||
- `f1c1bcf0 feat: 收口 tree-first graph 主链与前端测试修复`
|
||||
- `f9a45e89 feat: complete tree shell cutover and regression coverage`
|
||||
|
||||
原因很简单:
|
||||
|
||||
- 这三次提交的主轴是 `tree-first graph kernel`、sidebar/tree shell、projection、stream、transport
|
||||
- 它们不是“文档编辑器主线基准错误”的根因
|
||||
- 直接整包回滚会误伤已经有效的 tree shell 主链工作
|
||||
|
||||
因此:
|
||||
|
||||
> **不建议回滚最近三次已提交主线 commit。**
|
||||
|
||||
### 4.2 应局部回退或降级的部分
|
||||
|
||||
真正需要处理的是当前工作区里把 runtime 壳推上默认主链的那一层实验改动。
|
||||
|
||||
优先级最高的局部回退对象:
|
||||
|
||||
- 默认文档页中的 `mnoteWebDocumentShellEnabled` 分支
|
||||
- [page.tsx](/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx)
|
||||
- `iframe` 挂载壳本身
|
||||
- [mnote-web-document-shell-host.tsx](/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/mnote-web-document-shell-host.tsx)
|
||||
- 把 runtime debug 壳当默认验收的 smoke 脚本
|
||||
- [task103-document-shell-cutover-smoke.js](/mnt/Data1T/mnote/scripts/task103-document-shell-cutover-smoke.js)
|
||||
- [task104-document-runtime-input-smoke.js](/mnt/Data1T/mnote/scripts/task104-document-runtime-input-smoke.js)
|
||||
- [task105-document-runtime-transactions-smoke.js](/mnt/Data1T/mnote/scripts/task105-document-runtime-transactions-smoke.js)
|
||||
- [task106-document-runtime-slash-reference-smoke.js](/mnt/Data1T/mnote/scripts/task106-document-runtime-slash-reference-smoke.js)
|
||||
- [task107-document-runtime-save-smoke.js](/mnt/Data1T/mnote/scripts/task107-document-runtime-save-smoke.js)
|
||||
|
||||
这些部分的问题不是“代码质量差”,而是:
|
||||
|
||||
- 它们把 debug/prototype 壳变成了默认产品路径
|
||||
- 它们会持续把开发注意力引向 runtime shell polish,而不是真正的页面级编辑体验
|
||||
|
||||
因此:
|
||||
|
||||
> **建议优先局部撤回“默认走 runtime shell”这组未提交改动。**
|
||||
|
||||
### 4.3 可保留但必须降级定位的部分
|
||||
|
||||
下面这些可以保留,但不能继续被描述成默认主编辑器:
|
||||
|
||||
- `mnote-web` 的 `/document` 路由
|
||||
- [editor.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs)
|
||||
- 文档 meta/content/save API
|
||||
- [documents.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/documents.rs)
|
||||
- [query_support.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/query_support.rs)
|
||||
- 运行时配置里的 document shell 字段
|
||||
- [runtime-config.ts](/mnt/Data1T/mnote/wolai-frontend/src/lib/runtime-config.ts)
|
||||
|
||||
保留条件只有一个:
|
||||
|
||||
> **它们只能作为 debug / prototype / bridge API 存在,不能再主导默认文档页。**
|
||||
|
||||
### 4.4 可继续保留观察的 Rust editor core 尝试
|
||||
|
||||
当前未提交的这些底层尝试,不建议直接删除,但也不应被提升为主 benchmark:
|
||||
|
||||
- [core-protocol/src/editor/mod.rs](/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/mod.rs)
|
||||
- [mnote-editor-core/src/lib.rs](/mnt/Data1T/mnote/rust/crates/mnote-editor-core/src/lib.rs)
|
||||
|
||||
原因:
|
||||
|
||||
- 它们已经抽出了 `EditorBlockDocument / EditorCommand / CommandExecutor` 这类可复用底层合同
|
||||
- 这层更像“Rust command/model spike”
|
||||
- 它们比 runtime shell UI 更有保留价值
|
||||
|
||||
但当前也要明确:
|
||||
|
||||
- 它们还不足以支撑 `Tiptap` 级人类编辑体验
|
||||
- 它们只能服务未来的命令合同、AI pipeline、导入导出和调试转换
|
||||
- 不能再反向决定默认产品体验
|
||||
|
||||
---
|
||||
|
||||
## 5. 后续开发阶段
|
||||
|
||||
### 阶段 A:主链纠偏
|
||||
|
||||
目标:
|
||||
|
||||
- 把默认文档页从 runtime shell 退回产品页主链
|
||||
- 明确 `runtime shell = debug`
|
||||
|
||||
清单:
|
||||
|
||||
- [x] 取消默认文档页对 `mnote-web document shell iframe` 的优先切换
|
||||
- [x] 将 `/document` 路由和相关文案重命名为 debug/prototype
|
||||
- [x] 停止用 `task103-107` 这组脚本作为默认文档页主验收
|
||||
- [ ] 重新把验收标准收口到“是否接近 Wolai/Tiptap 页面体验”
|
||||
|
||||
### 阶段 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 全自研编辑器”的文档口径为准。
|
||||
@@ -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 完成后,再执行依赖清理与最终删除主线路径 |
|
||||
@@ -0,0 +1,260 @@
|
||||
# [recycle] Rust 全栈块编辑器替代方案选型
|
||||
|
||||
你现在的栈是**纯 Rust 全栈:Axum(后端)+ Leptos(前端 WASM/SSR)**,目标是**彻底换掉 BlockNote、做轻量化块编辑器、全栈 Rust 化**,这个组合非常成熟,我直接给你**只适配该技术栈**的最优替换方案、分层选型和最终落地架构,不掺任何无关技术。
|
||||
|
||||
# [recycle] 一、核心结论(直接照抄用)
|
||||
**唯一最优方案**
|
||||
`Axum + Leptos + leptos-yrs + blocks crate + 自研轻量块组件`
|
||||
- 完全抛弃 BlockNote 等重型 JS 编辑器
|
||||
- 100% 全 Rust,前端 WASM 渲染、后端 Axum 提供接口+协作同步
|
||||
- 完美实现 Notion/飞书/思源的**块结构、嵌套、富文本、协作、本地优先**
|
||||
|
||||
# 二、前端块编辑层(Leptos 侧,替代 BlockNote)
|
||||
只选**Leptos 原生/可无缝集成的 Rust 方案**,拒绝任何重型 JS 绑定
|
||||
|
||||
## 1. 首选:自研 Leptos 轻量块编辑器(最推荐,可控+极轻)
|
||||
直接基于 Leptos 自己搭块编辑器,比 BlockNote 轻量 10 倍以上,完全贴合你的需求
|
||||
- 用 Leptos `Signal / RwSignal` 管理块列表、选中态、拖拽、折叠
|
||||
- 块类型(标题/段落/代码/引用/嵌套子块)自己定义,想加数据库/看板随时扩展
|
||||
- 纯 Rust 渲染,无 JS 运行时,和 Leptos 生命周期完全对齐
|
||||
- 支持快捷键、撤销/重做、拖拽排序,按需实现,不堆无用功能
|
||||
|
||||
## 2. 次选:leptos-editor(社区轻量富文本,快速改块编辑器)
|
||||
Leptos 生态原生轻量编辑器,纯 Rust/WASM 实现
|
||||
- 开箱即用的富文本(粗体/斜体/链接)
|
||||
- 外层包一层块容器,快速改成 Notion 风格块布局
|
||||
- 开发成本最低,适合快速出原型
|
||||
|
||||
## 3. 兜底:editable / text-editor crates
|
||||
Rust 原生底层编辑内核,无任何前端依赖,可深度封装进 Leptos 组件
|
||||
- 适合追求极致性能、底层可控的场景
|
||||
|
||||
---
|
||||
|
||||
# 三、块数据 & 协作核心(Rust 通用层)
|
||||
这部分是 Notion 类编辑器的灵魂,直接用成熟 Rust 库,不重复造轮子
|
||||
1. **blocks crate**
|
||||
块结构标准库:块类型、嵌套父子结构、JSON/Markdown 序列化、diff/merge
|
||||
完美对接 Leptos 状态管理
|
||||
2. **yrs(必用)**
|
||||
Rust 官方实现的 Yjs CRDT,**飞书/Notion/Wolai 协作底层**
|
||||
支持多人实时编辑块、无冲突合并
|
||||
3. **leptos-yrs**
|
||||
Leptos 与 yrs 官方绑定,用 Signal 直接同步 CRDT 文档
|
||||
前端改块 → 自动同步到后端 → 其他客户端实时更新
|
||||
|
||||
---
|
||||
|
||||
# 四、后端 Axum 配套方案
|
||||
Axum 只做两件事:**块数据持久化 + 协作同步**
|
||||
1. **WebSocket 实时同步(Axum + tokio-tungstenite)**
|
||||
前端 yrs 产生编辑更新 → Axum 转发 → 所有在线客户端同步
|
||||
二进制协议,比 JSON 快很多,完全替代 JS 协作层
|
||||
2. **REST 接口**
|
||||
文档/块的增删改查、权限、目录管理
|
||||
3. **存储层**
|
||||
- 服务端持久化:`PostgreSQL + sqlx`(存块结构+yrs文档快照)
|
||||
- 本地优先缓存:`redb`(Rust 嵌入式 KV,对标思源本地存储)
|
||||
- 全文搜索:`tantivy`(Rust 全文搜索引擎,实现 Notion 全局搜索)
|
||||
|
||||
---
|
||||
|
||||
# 五、两套最终落地配置(直接用)
|
||||
## 方案 A:极简单用户版(快速上线,无协作)
|
||||
**Leptos 自研块 + blocks crate + Axum REST + redb**
|
||||
- 无重型依赖
|
||||
- 纯 Rust 块编辑
|
||||
- 本地优先 + 云端备份
|
||||
- 对标:思源笔记单用户体验
|
||||
|
||||
## 方案 B:完整版(多用户协作,对标 Notion/飞书)⭐⭐⭐⭐⭐
|
||||
**Leptos + leptos-yrs + blocks + Axum(WebSocket+REST) + Postgres + tantivy**
|
||||
- 多人实时块编辑
|
||||
- 块嵌套/拖拽/富文本全覆盖
|
||||
- 全栈 Rust,无任何 JS 编辑器冗余
|
||||
- 可扩展表格/看板/数据库块
|
||||
|
||||
---
|
||||
|
||||
# 六、关键实现小示例(Leptos 块结构伪代码)
|
||||
```rust
|
||||
// 块数据模型(用 blocks crate 或自定义)
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum BlockType {
|
||||
H1, H2, Paragraph, Code, Quote, Toggle
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BlockEditor(doc_id: String) -> impl IntoView {
|
||||
// 块列表状态
|
||||
let (blocks, set_blocks) = create_signal(vec![]);
|
||||
// 绑定 yrs 协作文档
|
||||
let yrs_doc = use_yrs_doc(doc_id);
|
||||
|
||||
view! {
|
||||
<div class="notion-block-container">
|
||||
<For
|
||||
each=blocks
|
||||
key=|b| b.id
|
||||
children=move |block| {
|
||||
view! { <BlockComponent block=block set_blocks/> }
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 七、避坑(非常重要)
|
||||
1. **坚决不用 leptos-tiptap / tiptap-rs**
|
||||
本质还是套 JS 重型编辑器,违背你全 Rust、轻量化的初衷
|
||||
2. **不要自研 CRDT**
|
||||
直接用 yrs,成本差 100 倍,稳定性天壤之别
|
||||
3. **块渲染用 Leptos 组件拆分**
|
||||
避免单组件巨量渲染,性能对标原生飞书
|
||||
4. **Axum 只做同步与存储**
|
||||
块编辑逻辑全部放在前端 WASM,后端无状态,横向扩展极简单
|
||||
|
||||
如果你需要,我可以直接给你:
|
||||
- 可运行的 `Axum + Leptos + yrs` 最小工程模板
|
||||
- 块编辑器的完整 Leptos 组件代码(含拖拽、回车拆块、删除块)
|
||||
- 块数据结构与 Axum 接口定义
|
||||
|
||||
# 全 Rust 替代 BlockNote 方案 & 针对性推荐
|
||||
你现在的核心诉求非常明确:**自研类 Notion/飞书/思源笔记**,嫌弃 BlockNote 前端太重、JS 依赖重,想要**全 Rust 栈、轻量化、块编辑核心可控**。
|
||||
|
||||
我直接按「**落地优先级**」给你方案,所有方案均彻底摆脱 BlockNote 这类重型 JS 编辑器,核心逻辑/UI 尽可能全 Rust,适配 Tauri 桌面端(你做笔记软件几乎必用)。
|
||||
|
||||
---
|
||||
|
||||
## 核心结论先给
|
||||
**最优解:Tauri + 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** 最小块编辑器模板吗?
|
||||
+568
@@ -0,0 +1,568 @@
|
||||
# [recycle] mnote 从 Runtime 壳回到 Wolai 目标页的纠偏清单 v1
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
>
|
||||
> 最终交付对标基准:
|
||||
> - 本地目标截图:`/mnt/Data1T/mnote/tmp/image copy 14.png`
|
||||
> - 目标页面:`https://www.wolai.com/liaibo/ikFSSM1a4GgvmHBYFCNfVd`
|
||||
> - 当前偏移截图:`/mnt/Data1T/mnote/tmp/image copy 13.png`
|
||||
>
|
||||
> 说明:
|
||||
> - `dokobot` 当前只能稳定读到 Wolai 的 SPA 外壳标题,拿不到正文结构。
|
||||
> - 因此本文以本地目标截图 `image copy 14.png` 作为最终视觉与交互验收基准。
|
||||
|
||||
## 1. 这份文档解决什么问题
|
||||
|
||||
当前代码和阶段文档虽然把“最小可写 runtime 闭环”做通了,但实际交付物仍然是一个:
|
||||
|
||||
- `iframe` 挂载的独立文档壳
|
||||
- 带 `Document Editor Shell` 标题的调试页
|
||||
- 带 revision/save/focus/selection/hover/event log 的 runtime 面板
|
||||
- 基于 `textarea + 按钮工具条 + 块卡片` 的工程壳
|
||||
|
||||
它和目标页的差距,不是“差一点样式”,而是:
|
||||
|
||||
- 页面架构不对
|
||||
- 交互壳不对
|
||||
- 验收标准不对
|
||||
- 默认主路径不对
|
||||
|
||||
本文的目的,是把后续交付标准重新冻结为:
|
||||
|
||||
> **最终默认交付必须逼近 `image copy 14.png` 这种一体化 Wolai 页面,而不是继续优化 `image copy 13.png` 这类 runtime 调试壳。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 最终交付标准
|
||||
|
||||
最终默认交付以目标截图为准,至少必须满足下面这些显性特征。
|
||||
|
||||
### 2.1 页面结构标准
|
||||
|
||||
- 左侧是产品级导航树,而不是单独文档调试页。
|
||||
- 中间是文档页面本身,不通过 `iframe` 二次嵌套。
|
||||
- 顶部是轻量面包屑 / 导航动作,不出现 runtime 诊断信息。
|
||||
- 正文区域是沉浸式页面画布,不出现工程面板、状态卡、调试日志。
|
||||
- 右上角是产品动作区,不是编辑器内部调试控件区。
|
||||
|
||||
### 2.2 编辑体验标准
|
||||
|
||||
- 页面标题是正文的一部分,视觉上属于页面内容,而不是壳标题。
|
||||
- 首块内容直接以内联块形态出现,不是“卡片块列表 + badge + tag”。
|
||||
- 占位文案应以内联方式出现在正文里,例如“输入 `/` 选择,按 `空格` 打开 AI...”,而不是单独的控制台提示区。
|
||||
- 勾选框、标题、段落、列表在正文中应呈现为自然文档流,不应被包成工程卡片。
|
||||
- slash、引用、缩进、折叠等交互应在光标附近或块上下文中触发,不依赖页顶按钮排布。
|
||||
|
||||
### 2.3 禁止出现的元素
|
||||
|
||||
默认交付页中禁止出现:
|
||||
|
||||
- `Document Editor Shell`
|
||||
- `revision=`
|
||||
- `updatedAt=`
|
||||
- `save=`
|
||||
- `focus=`
|
||||
- `selection=`
|
||||
- `hover=`
|
||||
- `交互状态`
|
||||
- `最小状态机`
|
||||
- `最近事件`
|
||||
- `block_editor_shell.ready`
|
||||
- 页顶固定一排 `Slash 菜单 / 折叠标题 / 缩进 / [[page]] / ((block))` 按钮
|
||||
- `Paragraph / Heading / Todo` 这种脱离光标上下文的演示按钮
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前偏移的根因
|
||||
|
||||
### 3.1 默认主路径被切成了 `iframe` 文档壳
|
||||
|
||||
当前默认文档页在命中文档壳开关后,直接返回 `MnoteWebDocumentShellHost`,并在其中挂一个 `iframe`。
|
||||
|
||||
相关代码:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx:183`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/mnote-web-document-shell-host.tsx:39`
|
||||
|
||||
这一步本身就决定了它不会长成目标截图那种“一体化页面”。
|
||||
|
||||
### 3.2 当前 `/document` 路由本质是 runtime 调试页
|
||||
|
||||
当前 Rust 文档页直接输出了:
|
||||
|
||||
- `Document Editor Shell`
|
||||
- hero 说明
|
||||
- meta pills
|
||||
- status card
|
||||
- toolbar buttons
|
||||
- interaction sidebar
|
||||
- event log
|
||||
|
||||
相关代码:
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:790`
|
||||
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:805`
|
||||
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:821`
|
||||
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:838`
|
||||
|
||||
这不是“产品 UI 还没 polish”,而是“页面职责定义错了”。
|
||||
|
||||
### 3.3 当前块渲染是工程卡片流,不是正文文档流
|
||||
|
||||
当前每个 block 是:
|
||||
|
||||
- `li.editor-row`
|
||||
- 左侧 badge
|
||||
- 中间 title/meta
|
||||
- 下方 `textarea`
|
||||
- 右侧 state tag
|
||||
|
||||
相关代码:
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:1408`
|
||||
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:1420`
|
||||
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:1426`
|
||||
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs:1437`
|
||||
|
||||
这类结构适合调试事务,不适合对标 Wolai 页面。
|
||||
|
||||
### 3.4 验收标准被收缩成“最小可写闭环”
|
||||
|
||||
当前 smoke / harness 的判断标准主要是:
|
||||
|
||||
- 文档页里出现 `iframe`
|
||||
- `iframe` 里出现 `Document Editor Shell`
|
||||
- 能找到 `textarea[data-block-input-id]`
|
||||
- 能点击页顶 slash 按钮
|
||||
- 能看到 event log 标记
|
||||
|
||||
相关脚本:
|
||||
|
||||
- `/mnt/Data1T/mnote/scripts/task103-document-shell-cutover-smoke.js:17`
|
||||
- `/mnt/Data1T/mnote/scripts/task103-document-shell-cutover-smoke.js:25`
|
||||
- `/mnt/Data1T/mnote/scripts/task104-document-runtime-input-smoke.js:29`
|
||||
- `/mnt/Data1T/mnote/scripts/task104-document-runtime-input-smoke.js:46`
|
||||
- `/mnt/Data1T/mnote/scripts/task106-document-runtime-slash-reference-smoke.js:34`
|
||||
|
||||
相关任务口径:
|
||||
|
||||
- `/mnt/Data1T/mnote/harness-tasks.json:2124`
|
||||
- `/mnt/Data1T/mnote/harness-tasks.json:2248`
|
||||
- `/mnt/Data1T/mnote/harness-progress.txt:193`
|
||||
- `/mnt/Data1T/mnote/harness-progress.txt:203`
|
||||
|
||||
所以代码确实“通过了验收”,但那个验收根本不是目标页验收。
|
||||
|
||||
### 3.5 文档策略与产品目标之间出现了中途收缩
|
||||
|
||||
当前主基线文档明确收口到:
|
||||
|
||||
- AI-first
|
||||
- CLI-first
|
||||
- Rust-native
|
||||
- 轻量块编辑
|
||||
- 最小可写闭环
|
||||
|
||||
相关文档:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/ai-first-rust-block-editor-baseline-v1.md:19`
|
||||
- `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/ai-first-rust-block-editor-baseline-v1.md:105`
|
||||
|
||||
这条路线适合先打通内核,但不能直接当作“产品默认完成态”。
|
||||
|
||||
---
|
||||
|
||||
## 4. 纠偏总原则
|
||||
|
||||
后续纠偏必须遵循下面四条。
|
||||
|
||||
### 4.1 默认交付优先级重排
|
||||
|
||||
优先级改为:
|
||||
|
||||
1. 默认页面必须像目标截图那样是一体化产品页
|
||||
2. 编辑交互必须内嵌在页面内容流中
|
||||
3. Rust command / save chain / projection 继续保留
|
||||
4. runtime 调试信息只能退到 debug 模式
|
||||
|
||||
不能再把“Rust runtime 闭环”放在默认页面形态之上。
|
||||
|
||||
### 4.2 `/document` 壳降级为 debug / prototype
|
||||
|
||||
`mnote-web /document` 可以保留,但只能作为:
|
||||
|
||||
- `?editor=runtime-debug`
|
||||
- `?editor=prototype`
|
||||
- 内部调试壳
|
||||
- transaction / IME / save chain 诊断页
|
||||
|
||||
它不能再是默认主编辑器。
|
||||
|
||||
### 4.3 `iframe` 方案退出默认主链
|
||||
|
||||
默认文档页必须回到页面内原生渲染:
|
||||
|
||||
- 不再通过 `iframe` 承载默认编辑器
|
||||
- 不再让页面主体验依赖另一个独立 HTML 文档
|
||||
- 不再让产品页布局与编辑器页布局分裂
|
||||
|
||||
### 4.4 调试能力后移,不再前置
|
||||
|
||||
这些能力应该保留,但只能后移到 debug:
|
||||
|
||||
- focus / selection / hover
|
||||
- event log
|
||||
- revision / updatedAt / save status
|
||||
- runtime 状态机说明
|
||||
- editor command buttons
|
||||
|
||||
---
|
||||
|
||||
## 5. 必须立即执行的纠偏项
|
||||
|
||||
下面这些项不是“优化建议”,而是必须执行的逆转动作。
|
||||
|
||||
### 5.1 默认主路径逆转
|
||||
|
||||
- [ ] 把默认文档页从 `MnoteWebDocumentShellHost` 切回页面内主渲染路径。
|
||||
- [ ] `page.tsx` 中默认逻辑不得再优先返回 `iframe` 壳。
|
||||
- [ ] `mnoteWebDocumentShellEnabled` 只能控制实验入口,不能控制默认编辑器。
|
||||
- [ ] `?editor=compat` 保留,但新增 `?editor=runtime-debug`,明确实验壳用途。
|
||||
|
||||
涉及文件:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/mnote-web-document-shell-host.tsx`
|
||||
|
||||
### 5.2 `/document` runtime 页角色重命名
|
||||
|
||||
- [ ] 把 `Document Editor Shell` 明确改名为 `Runtime Debug Shell` 或等价名称。
|
||||
- [ ] 页面文案必须承认它是 debug/prototype,而不是默认编辑器。
|
||||
- [ ] 该页默认不再通过主文档页直达。
|
||||
|
||||
涉及文件:
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/editor.rs`
|
||||
|
||||
### 5.3 删除默认页面中的调试区块
|
||||
|
||||
默认产品页必须移除:
|
||||
|
||||
- [ ] hero 说明区
|
||||
- [ ] meta pills
|
||||
- [ ] status card
|
||||
- [ ] interaction sidebar
|
||||
- [ ] event log
|
||||
- [ ] 页面顶部固定命令按钮
|
||||
|
||||
注意:
|
||||
|
||||
- 调试区块可以迁到 `debug panel`
|
||||
- 但不能继续留在默认编辑页 DOM 里
|
||||
|
||||
### 5.4 页面内编辑器替换卡片流
|
||||
|
||||
- [ ] 块渲染必须退出 `li + badge + meta + textarea + state-tag` 这种工程卡片结构。
|
||||
- [ ] 默认正文必须改成自然文档流:
|
||||
- 标题就是标题
|
||||
- 勾选就是勾选
|
||||
- 段落就是段落
|
||||
- 列表就是列表
|
||||
- [ ] 块级信息如 `depth=0 / editable=true / type:paragraph` 只能进入 debug,不得出现在正文。
|
||||
- [ ] 正文留白、字号、宽度、块间距必须向目标截图靠拢。
|
||||
|
||||
---
|
||||
|
||||
## 6. 分阶段纠偏清单
|
||||
|
||||
## 阶段 A:先把默认主路径拉回产品页
|
||||
|
||||
### 目标
|
||||
|
||||
先解决“默认打开文档时为什么像独立调试站点”。
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] 默认文档页退出 `iframe` 模式。
|
||||
- [ ] 页面回到 `DocumentShell -> DocumentContent` 一体化主链。
|
||||
- [ ] 保留 runtime 壳,但只允许通过显式 debug 参数进入。
|
||||
- [ ] 主路径不再展示 `Document Editor Shell` 文案。
|
||||
- [ ] 主路径不再依赖 `mnote-web-document-shell-host.tsx`。
|
||||
- [ ] 补回归:
|
||||
- 默认文档页 DOM 中不再存在 `iframe[title^="mnote-web-document-shell"]`
|
||||
- `editor=runtime-debug` 时才允许出现 iframe 或独立壳
|
||||
|
||||
### 完成判定
|
||||
|
||||
- 用户打开页面时,首先看到的是产品页,而不是 runtime 站点。
|
||||
|
||||
## 阶段 B:恢复 Wolai 式页面骨架
|
||||
|
||||
### 目标
|
||||
|
||||
把“产品页壳”恢复到目标截图的感觉。
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] 左侧继续复用现有页面树/导航系统,不为编辑器单独造站。
|
||||
- [ ] 顶部保留产品导航:
|
||||
- 面包屑
|
||||
- 返回/前进
|
||||
- 右上角产品动作
|
||||
- [ ] 页面标题回到正文主列内部,而不是 debug 页标题。
|
||||
- [ ] 正文宽度、留白、顶部间距对齐目标截图。
|
||||
- [ ] 页面空白区域保留沉浸式阅读/编辑氛围,不塞调试信息。
|
||||
- [ ] 若现有 compat 页骨架更接近目标截图,优先复用它的产品壳,而不是继续美化 runtime 壳。
|
||||
|
||||
### 完成判定
|
||||
|
||||
- 不看功能,只看静态截图,页面也应该更像 Wolai 页面而不是内部工具页。
|
||||
|
||||
## 阶段 C:把编辑器从“卡片列表”改成“正文文档流”
|
||||
|
||||
### 目标
|
||||
|
||||
解决当前最刺眼的视觉偏移。
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] 去掉块左侧 badge 序号。
|
||||
- [ ] 去掉块右侧 `block:id / type:xxx` 标签。
|
||||
- [ ] 去掉每块上方 `Paragraph / depth=0 / editable=true` 元信息。
|
||||
- [ ] 标题块渲染为正文中的标题,不再单独加工程 title 行。
|
||||
- [ ] todo 块渲染为真实勾选项。
|
||||
- [ ] `textarea` 若继续保留,只能作为无边框、无卡片感的内联输入宿主。
|
||||
- [ ] 若 `textarea` 无法支撑自然正文体验,则必须把 `kode` 真正接入“块内输入器”层,而不是只停留在文档参考。
|
||||
- [ ] `[[page]]` / `((block))` 的插入入口改成光标附近上下文,不再只靠页顶按钮。
|
||||
- [ ] slash 菜单改为光标上下文菜单。
|
||||
|
||||
### 完成判定
|
||||
|
||||
- 用户看到正文时,应先感知“文档内容”,而不是“块调试单元”。
|
||||
|
||||
## 阶段 D:把交互从“页顶按钮”改成“块内上下文”
|
||||
|
||||
### 目标
|
||||
|
||||
解决当前交互方式明显不像 Wolai 的问题。
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] 去掉页顶固定 `Slash 菜单 / 折叠标题 / 缩进 / [[page]] / ((block))` 按钮。
|
||||
- [ ] slash 触发改为输入 `/`。
|
||||
- [ ] 页面引用改为输入 `[[`。
|
||||
- [ ] 块引用改为输入 `((` 或等价上下文入口。
|
||||
- [ ] 缩进 / 反缩进改为 `Tab / Shift+Tab` 主触发。
|
||||
- [ ] 标题折叠优先放在标题左侧 affordance,而不是页顶全局按钮。
|
||||
- [ ] 块 hover 菜单应轻量化,只在块附近出现。
|
||||
- [ ] 保留调试按钮,但移入 debug 模式或开发者工具抽屉。
|
||||
|
||||
### 完成判定
|
||||
|
||||
- 核心编辑动作不依赖页面级按钮栏。
|
||||
|
||||
## 阶段 E:调试能力彻底后移
|
||||
|
||||
### 目标
|
||||
|
||||
让默认产品页彻底脱离 runtime 调试感。
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] `revision / save status / focus / selection / hover` 移到 debug 面板。
|
||||
- [ ] `event log` 移到 debug 面板。
|
||||
- [ ] `最小状态机` 文案移到 debug 面板。
|
||||
- [ ] debug 面板默认关闭,只在显式调试开关下出现。
|
||||
- [ ] 生产态 DOM 中不出现 `block_editor_shell.ready` 等字符串。
|
||||
|
||||
### 完成判定
|
||||
|
||||
- 默认页面截图不再暴露任何 runtime 术语。
|
||||
|
||||
## 阶段 F:验收标准重写
|
||||
|
||||
### 目标
|
||||
|
||||
防止再次出现“工程闭环通过了,但产品页仍然不对”的误判。
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] 废弃以 `iframe + Document Editor Shell + textarea` 为成功条件的默认验收。
|
||||
- [ ] 重写 `task103~107` 类 smoke 的默认成功条件。
|
||||
- [ ] 新增产品级验收断言:
|
||||
- 默认文档页没有 `iframe[title^="mnote-web-document-shell"]`
|
||||
- 页面不存在 `Document Editor Shell`
|
||||
- 页面不存在 `交互状态`
|
||||
- 页面不存在 `最近事件`
|
||||
- 页面存在产品导航树
|
||||
- 页面标题在正文主列内
|
||||
- 第一块内容以内联块形式出现
|
||||
- 占位文案直接出现在正文中
|
||||
- slash 菜单由输入 `/` 触发
|
||||
- [ ] 新增截图验收:
|
||||
- 至少保留一组默认页截图
|
||||
- 用于与目标截图做人工对照
|
||||
- [ ] Final QA 的表述从“最小可写闭环”改为“产品级默认编辑页达到目标页标准”。
|
||||
|
||||
### 完成判定
|
||||
|
||||
- 测试通过时,默认页面也必须像目标页,而不是仅仅可写。
|
||||
|
||||
---
|
||||
|
||||
## 7. 参考层在这次纠偏中的正确用法
|
||||
|
||||
这次纠偏不是要推翻 Rust 内核,而是要纠正“参考层只停留在文档里,没有真正进入主路径”的问题。
|
||||
|
||||
### `edita-core`
|
||||
|
||||
继续用于:
|
||||
|
||||
- command 容器
|
||||
- transform / transaction 思路
|
||||
|
||||
不用于:
|
||||
|
||||
- 默认产品页 UI
|
||||
|
||||
### `blocks`
|
||||
|
||||
继续用于:
|
||||
|
||||
- block model
|
||||
- import/export
|
||||
- history / diff / merge
|
||||
|
||||
不用于:
|
||||
|
||||
- 正文视觉与交互壳
|
||||
|
||||
### `kode`
|
||||
|
||||
这次必须优先重新评估它是否能真正承担:
|
||||
|
||||
- 块内输入器
|
||||
- Markdown/WYSIWYG 内联编辑
|
||||
- 光标附近上下文交互
|
||||
|
||||
如果 `textarea` 无法做出目标页的正文感,`kode` 不能再只停在“设计参考”。
|
||||
|
||||
### `leptos-tiptap`
|
||||
|
||||
继续作为 fallback:
|
||||
|
||||
- 当 `kode` 与自有块编排组合后仍无法提供足够自然的编辑体验时
|
||||
- 用来验证 Leptos 对接成熟编辑 runtime 的现实成本
|
||||
|
||||
### `mnote-next`
|
||||
|
||||
只继续作为:
|
||||
|
||||
- 交互样例
|
||||
- 历史反例
|
||||
|
||||
不再作为:
|
||||
|
||||
- 视觉或产品交付参考
|
||||
|
||||
---
|
||||
|
||||
## 8. 关于 “Tiptap 体验” 的补充判断
|
||||
|
||||
这一条必须单独说清。
|
||||
|
||||
### 8.1 仅执行本文前面的纠偏项,不足以自动获得 Tiptap 级体验
|
||||
|
||||
原因很直接:
|
||||
|
||||
- 前文主要纠正的是“默认页面壳”
|
||||
- 纠正的是 `iframe`、调试面板、卡片块流、验收标准
|
||||
- 这些动作可以让页面更像 Wolai
|
||||
- 但它们不会自动提供 `Tiptap / ProseMirror` 那种输入体验内核
|
||||
|
||||
如果底层仍然是:
|
||||
|
||||
- `textarea` 作为主要输入宿主
|
||||
- 自己拼 selection / composition / slash / token / hover 菜单
|
||||
- 自己再把这些交互拼成正文体验
|
||||
|
||||
那么最终结果最多是:
|
||||
|
||||
- “更像 Wolai 的产品壳”
|
||||
- “更自然的块 Markdown 编辑器”
|
||||
|
||||
而不是天然等于:
|
||||
|
||||
- “接近 Tiptap 的成熟输入体验”
|
||||
|
||||
### 8.2 如果最终目标明确是“类似 Tiptap 的体验”,应该把 `leptos-tiptap` 从 fallback 提升为重点实施分支
|
||||
|
||||
当下面这些诉求成立时,建议把 `leptos-tiptap` 提升为主候选,而不是继续只把它当 fallback:
|
||||
|
||||
- 你要的是接近 Wolai / Tiptap 的输入手感
|
||||
- 你重视成熟 selection / composition / inline behavior
|
||||
- 你希望 slash、placeholder、mark、node 行为更接近现成富文本编辑器
|
||||
- 你不希望长期自己维护 `textarea + 自拼交互` 这条路
|
||||
|
||||
这时更合理的路线是:
|
||||
|
||||
1. 产品页壳按本文前半部分纠偏,先回到一体化页面
|
||||
2. 人类编辑 surface 优先改成 `leptos-tiptap`
|
||||
3. Rust kernel 继续保留为事实源、命令层、AI/CLI 层
|
||||
4. 通过 adapter 把 `Tiptap JSON / command / selection event` 映射到 Rust block model / command
|
||||
|
||||
### 8.3 更准确的架构收口应该是“双层分工”,而不是二选一
|
||||
|
||||
如果采用 `leptos-tiptap`,建议分工固定为:
|
||||
|
||||
- `Tiptap / leptos-tiptap`
|
||||
- 负责人类交互体验
|
||||
- 负责 selection、composition、placeholder、toolbar/slash、inline behavior
|
||||
- Rust editor core / kernel
|
||||
- 负责事实源
|
||||
- 负责命令合同
|
||||
- 负责 AI/CLI 共用写链
|
||||
- 负责导入导出、审计、projection、持久化
|
||||
|
||||
也就是说:
|
||||
|
||||
> **对“人类编辑体验”妥协给 `Tiptap`,不等于对“系统语义主导权”妥协给 `Tiptap`。**
|
||||
|
||||
### 8.4 什么时候不该上 `leptos-tiptap`
|
||||
|
||||
只有在下面这些条件同时成立时,才继续坚持纯 Rust-native 输入壳:
|
||||
|
||||
- 你接受第一版明显不像 Tiptap,只求最小可写
|
||||
- 你接受更长时间去补 selection / IME / inline interaction
|
||||
- 你把 Rust-native 纯度放在“像 Wolai/Tiptap 的体验”之前
|
||||
|
||||
而你当前这一轮明确表达的是:
|
||||
|
||||
> **你真正想要的是类似 Tiptap 的体验。**
|
||||
|
||||
在这个前提下,继续默认押注 `textarea + 自拼交互`,风险会非常高。
|
||||
|
||||
### 8.5 本文后的新增执行要求
|
||||
|
||||
基于上面的判断,后续纠偏不应只做“页面壳纠偏”,还应新增一个并行分支:
|
||||
|
||||
- [ ] 新增 `Tiptap 体验纠偏` 分支文档
|
||||
- [ ] 用 `leptos-tiptap` 做一个真正的页面内 spike,而不是停留在 reference-code
|
||||
- [ ] 对比三条路线:
|
||||
- `textarea + 自拼交互`
|
||||
- `kode` 块内输入器
|
||||
- `leptos-tiptap`
|
||||
- [ ] 以“接近目标页体验”而不是“谁更 Rust-native”作为第一判断标准
|
||||
- [ ] 若 `leptos-tiptap` 的 spike 明显更接近目标页,应把它提升为默认人类编辑 surface 方案
|
||||
|
||||
---
|
||||
|
||||
## 9. 最终一句话收口
|
||||
|
||||
后续默认交付口径固定为:
|
||||
|
||||
> **Rust editor core 可以继续最小化,但默认页面不能再像 runtime 调试站点;默认页面必须回到 Wolai 式一体化产品页,runtime 壳只保留为 debug/prototype。**
|
||||
|
||||
如果进一步收紧到“体验也要接近 Tiptap/Wolai”,那么还要再补一句:
|
||||
|
||||
> **页面壳纠偏只能解决“像不像产品页”,不能单独解决“像不像 Tiptap”;若目标是 Tiptap 级体验,应认真把 `leptos-tiptap` 提升为主候选,而不是继续只放在 fallback。**
|
||||
@@ -0,0 +1,129 @@
|
||||
# [recycle] BlockNote Migration Plan v1
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
|
||||
## 1. 目标
|
||||
|
||||
这份文档用于冻结从 BlockNote 主路径迁移到 Rust block editor 的最小迁移顺序。
|
||||
|
||||
目标不是一次性删除所有旧代码,而是:
|
||||
|
||||
- 先让主文档页默认切到新 Rust editor 壳
|
||||
- 再把 BlockNote 降到 compat/debug
|
||||
- 最后清理旧依赖和主线耦合
|
||||
|
||||
## 2. 当前盘点
|
||||
|
||||
当前主路径仍直接依赖下面这些入口或依赖:
|
||||
|
||||
- `@blocknote/core`
|
||||
- `@blocknote/react`
|
||||
- `@blocknote/mantine`
|
||||
- `blocknote-editor.tsx`
|
||||
- `schema.ts`
|
||||
- `HocuspocusProvider`
|
||||
- `Yjs`
|
||||
|
||||
主路径宿主文件主要是:
|
||||
|
||||
- `wolai-frontend/src/components/editor/document-content.tsx`
|
||||
- `wolai-frontend/src/components/editor/blocknote-editor.tsx`
|
||||
- `wolai-frontend/src/components/editor/schema.ts`
|
||||
|
||||
## 3. 迁移分层
|
||||
|
||||
迁移固定分三层:
|
||||
|
||||
- 主路径
|
||||
由 Rust block editor 接管
|
||||
- compat
|
||||
保留旧 BlockNote 入口,供临时回退
|
||||
- debug
|
||||
保留最小诊断入口,帮助排障
|
||||
|
||||
禁止继续把 BlockNote 当成默认编辑器。
|
||||
|
||||
## 4. 内容格式迁移策略
|
||||
|
||||
第一阶段不要求一次性消灭所有旧内容格式。
|
||||
|
||||
固定策略如下:
|
||||
|
||||
- 主写链以 Rust editor command 和 Rust document model 为准
|
||||
- 旧 BlockNote JSON 通过兼容 codec 读取
|
||||
- 写回时优先生成 Rust 侧统一结构
|
||||
- 必要时保留从旧格式到新格式的单向迁移适配
|
||||
|
||||
重点对象:
|
||||
|
||||
- 段落
|
||||
- 标题
|
||||
- list/todo
|
||||
- `pageReference`
|
||||
- `blockReference`
|
||||
- 附件和重型自定义块
|
||||
|
||||
## 5. 入口切换顺序
|
||||
|
||||
第一步:
|
||||
|
||||
- 在文档页入口切到 `mnoteWebDocumentShellEnabled`
|
||||
- 默认走 `mnote-web /document`
|
||||
- 保留 `?editor=compat` 回退参数
|
||||
|
||||
第二步:
|
||||
|
||||
- `document-content.tsx` 继续保留为 compat 宿主壳
|
||||
- `blocknote-editor.tsx` 继续保留为 compat 编辑器实现
|
||||
|
||||
第三步:
|
||||
|
||||
- 新主编辑器稳定后,再把主路径对 `schema.ts`、`HocuspocusProvider`、`Yjs` 的依赖收缩到 compat/debug
|
||||
|
||||
## 6. 依赖收敛
|
||||
|
||||
迁移完成前后要明确区分:
|
||||
|
||||
- 主路径是否仍依赖 `@blocknote/core`
|
||||
- 主路径是否仍依赖 `blocknote-editor.tsx`
|
||||
- 主路径是否仍依赖 `HocuspocusProvider`
|
||||
- 主路径是否仍依赖 `Yjs`
|
||||
|
||||
最终要求:
|
||||
|
||||
- compat/debug 允许暂时保留
|
||||
- 主路径不再依赖以上能力
|
||||
|
||||
## 7. 风险
|
||||
|
||||
主要风险如下:
|
||||
|
||||
- 旧页面内容与新结构双写不一致
|
||||
- history / snapshot / TOC / stats 边界退化
|
||||
- 重型块在新主路径下缺少最小宿主能力
|
||||
- 回退入口不明确导致线上排障困难
|
||||
|
||||
## 8. 回退策略
|
||||
|
||||
必须保留显式回退:
|
||||
|
||||
- 文档页参数级 compat 入口
|
||||
- 旧 BlockNote 组件继续可挂载
|
||||
- debug 场景可直接验证旧壳
|
||||
|
||||
在没有确认新主路径稳定前,不得直接删除 compat/debug。
|
||||
|
||||
## 9. 验收标准
|
||||
|
||||
满足以下条件即可视为 migration v1 达标:
|
||||
|
||||
- 主文档页默认落到新 Rust editor 壳
|
||||
- compat/debug 入口仍可独立使用
|
||||
- `blocknote-editor.tsx` 不再是默认主路径
|
||||
- `schema.ts` 不再决定默认主编辑器
|
||||
- `@blocknote/core`
|
||||
- `blocknote-editor.tsx`
|
||||
- `schema.ts`
|
||||
- `HocuspocusProvider`
|
||||
- `Yjs`
|
||||
已被明确标记为 compat/debug 或待删除对象
|
||||
@@ -0,0 +1,140 @@
|
||||
# [recycle] mnote Rust Block Editor Blocks Spike v0
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本 spike 聚焦 `blocks` 参考层是否适合承担:
|
||||
|
||||
- Markdown
|
||||
- round-trip
|
||||
- history
|
||||
- diff
|
||||
- merge
|
||||
|
||||
以及哪些地方存在 `缺口`。
|
||||
|
||||
## 2. 参考入口
|
||||
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/document.rs`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/block.rs`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/converters.rs`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/history.rs`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/diff.rs`
|
||||
|
||||
## 3. 适配性结论
|
||||
|
||||
### 3.1 Markdown
|
||||
|
||||
`blocks` 最适合直接借的是 Markdown 相关思路。
|
||||
|
||||
原因:
|
||||
|
||||
- 已有文档模型
|
||||
- 已有 converter facade
|
||||
- 方向上天然适合 Markdown import/export
|
||||
|
||||
这意味着第一阶段不应该自己先写一套新的 Markdown parser 胶水。
|
||||
|
||||
### 3.2 round-trip
|
||||
|
||||
`Markdown -> blocks -> Markdown round-trip` 是 `blocks` 最容易先验证的一条链。
|
||||
|
||||
对 `mnote` 的价值:
|
||||
|
||||
- 可以快速检查标题、列表、待办、引用、代码块的稳定性
|
||||
- 可以给 `task-057` 的导入导出提供直接参考
|
||||
|
||||
### 3.3 history
|
||||
|
||||
`blocks` 的 history 适合直接借思路,不建议第一阶段重写。
|
||||
|
||||
至少在 `undo / redo` 的双栈组织上,它比从零发明更稳。
|
||||
|
||||
### 3.4 diff / merge
|
||||
|
||||
`blocks` 的 diff / merge 很适合做 AI-first 和 CLI-first 后续阶段的基础参考。
|
||||
|
||||
第一阶段不要求完整采用,但必须明确后续方向,否则 AI 重写链会缺审计基础。
|
||||
|
||||
## 4. 主要缺口
|
||||
|
||||
### 4.1 文档模型仍偏扁平
|
||||
|
||||
当前 `blocks::Document` 是 `Vec<Block>` 为主。
|
||||
|
||||
而 `mnote` 要的不是简单扁平列表,而是:
|
||||
|
||||
- 块树
|
||||
- children
|
||||
- 自定义 `BlockProps`
|
||||
- 引用 token
|
||||
- 字符串 ID
|
||||
|
||||
这就是第一大 `缺口`。
|
||||
|
||||
### 4.2 BlockType 与 mnote 目标不完全一致
|
||||
|
||||
`blocks` 的 `BlockType` 更偏通用编辑器,不直接覆盖:
|
||||
|
||||
- `page_reference`
|
||||
- `block_reference`
|
||||
- `media_placeholder`
|
||||
- `progress_placeholder`
|
||||
|
||||
因此不能直接拿来当最终枚举。
|
||||
|
||||
### 4.3 默认 ID 体系不适合
|
||||
|
||||
`blocks` 用 `Uuid`。
|
||||
|
||||
但 `mnote` 当前更适合:
|
||||
|
||||
- `DocumentId = String`
|
||||
- `BlockId = String`
|
||||
|
||||
因为要跟现有页面、引用、CLI、AI 命令保持统一可读标识。
|
||||
|
||||
## 5. Spike 结论
|
||||
|
||||
`blocks` 的定位应固定为:
|
||||
|
||||
- 采用:Markdown 思路
|
||||
- 采用:round-trip 参考
|
||||
- 采用:history 思路
|
||||
- 采用:diff / merge 思路
|
||||
- 不直接采用:最终 block model
|
||||
- 不直接采用:最终 ID 体系
|
||||
|
||||
## 6. 建议落地方式
|
||||
|
||||
建议在 `mnote-editor-core` 中这样接:
|
||||
|
||||
- 自定义 `EditorDocument`
|
||||
- 自定义 `EditorBlockNode`
|
||||
- 自定义 `EditorBlockType`
|
||||
- 用 `blocks` 作为后续 `markdown/history/diff/merge` 参考或局部依赖
|
||||
|
||||
不要反过来把 `mnote` 的核心模型强行塞进 `blocks`。
|
||||
|
||||
## 7. 第一阶段保留判断
|
||||
|
||||
第一阶段可接受的保守做法:
|
||||
|
||||
- 先不把 `blocks` 作为正式依赖
|
||||
- 但在文档中冻结:后续 Markdown、history、diff、merge 优先复用其思路
|
||||
|
||||
若到 `task-057` 时验证表明 `blocks` 已足够稳定,再引正式依赖。
|
||||
|
||||
## 8. 完成判定映射
|
||||
|
||||
`task-052` 的完成判定要求本文显式包含:
|
||||
|
||||
- Markdown
|
||||
- round-trip
|
||||
- history
|
||||
- diff
|
||||
- merge
|
||||
- 缺口
|
||||
|
||||
当前文档已满足这些项。
|
||||
@@ -0,0 +1,137 @@
|
||||
# [recycle] mnote Rust Block Editor Edita Spike v0
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本 spike 只回答一个问题:
|
||||
|
||||
`edita-core` 的 `Editor / Block / Command / 无头执行` 边界,是否适合作为 `mnote` 新主编辑器第一阶段的命令核参考。
|
||||
|
||||
结论先写:
|
||||
|
||||
- 适合直接借 `Editor`
|
||||
- 适合直接借 `Command`
|
||||
- 适合借 `Block` 作为注册与分发思路
|
||||
- 适合借 `无头执行`
|
||||
- 不适合直接拿来当最终 `mnote` block document 模型
|
||||
- 不适合直接拿来当最终前端胶水
|
||||
|
||||
## 2. 参考入口
|
||||
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/edita/edita-core/src/lib.rs`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/edita/edita/src/editor.rs`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/edita/edita/src/state.rs`
|
||||
|
||||
## 3. 直接可复用面
|
||||
|
||||
### 3.1 `Editor`
|
||||
|
||||
`Editor<Node, State, Input>` 的价值不在 UI,而在:
|
||||
|
||||
- 持有统一 `state`
|
||||
- 提供统一命令入口
|
||||
- 提供 block registry
|
||||
- 允许 fallback block
|
||||
|
||||
这和 `mnote` 第一阶段要的“无头编辑器容器”是对齐的。
|
||||
|
||||
对 `mnote` 的映射建议:
|
||||
|
||||
- `Node` 不直接映射前端 DOM 节点
|
||||
- `State` 映射 `EditorDocument`
|
||||
- `Input` 第一阶段不必暴露给外部 UI,可先收口成 `EditorCommandEnvelope`
|
||||
|
||||
### 3.2 `Command`
|
||||
|
||||
`Command<State>` 很适合当 `mnote-editor-core` 的最小命令执行边界。
|
||||
|
||||
建议保留这层思想,但不要照搬 trait 名:
|
||||
|
||||
- `replace_block`
|
||||
- `insert_block_after`
|
||||
- `delete_block`
|
||||
- `split_block`
|
||||
- `merge_with_previous`
|
||||
- `move_block`
|
||||
- `indent_block`
|
||||
- `outdent_block`
|
||||
- `toggle_heading_collapse`
|
||||
|
||||
这些命令应当最终作用在 `EditorDocument` 上,而不是作用在 Leptos 组件状态上。
|
||||
|
||||
### 3.3 `Block`
|
||||
|
||||
`Block` trait 适合借来做“块类型注册 + 输入接管”思路,但当前实现更偏 parser pipeline。
|
||||
|
||||
`mnote` 第一阶段不建议直接照抄,因为我们更需要:
|
||||
|
||||
- 树形 block document
|
||||
- 固定字符串 ID
|
||||
- `BlockProps`
|
||||
- `content_node`
|
||||
- 引用 token
|
||||
|
||||
因此这里只借“块处理器是可注册单元”的思路。
|
||||
|
||||
### 3.4 无头执行
|
||||
|
||||
这是最值得采用的点。
|
||||
|
||||
新主编辑器必须先做无头执行,因为:
|
||||
|
||||
- CLI 要共用
|
||||
- AI 要共用
|
||||
- Web UI 只是壳
|
||||
|
||||
因此 `edita-core` 的无头执行边界适合直接进入采用矩阵。
|
||||
|
||||
## 4. 不直接采用的部分
|
||||
|
||||
### 4.1 不直接采用其最终 block 形态
|
||||
|
||||
原因:
|
||||
|
||||
- `mnote` 要的是块树,不是只围绕 parser block 运转
|
||||
- `mnote` 要保留 `page_reference / block_reference / media_placeholder / progress_placeholder`
|
||||
- 还要和 kernel `content_node` 边界对齐
|
||||
|
||||
### 4.2 不直接采用其 UI/editor 外壳
|
||||
|
||||
原因:
|
||||
|
||||
- 我们的 UI 壳后面要接 Leptos / kode
|
||||
- `edita-core` 当前更适合做 headless 胶水参考
|
||||
|
||||
## 5. Spike 结论
|
||||
|
||||
`edita-core` 在 `mnote` 第一阶段的定位固定为:
|
||||
|
||||
- 采用:`Editor`
|
||||
- 采用:`Command`
|
||||
- 部分采用:`Block`
|
||||
- 采用:`无头执行`
|
||||
- 不采用:最终产品 UI
|
||||
- 不采用:最终文档模型
|
||||
|
||||
## 6. 后续胶水建议
|
||||
|
||||
后续实现时,建议在 `mnote-editor-core` 里形成下面三层:
|
||||
|
||||
1. `EditorDocument`
|
||||
2. `EditorCommand`
|
||||
3. `EditorRuntime`
|
||||
|
||||
其中:
|
||||
|
||||
- `EditorRuntime` 借 `edita-core` 的无头执行边界
|
||||
- `EditorCommand` 借 `Command`
|
||||
- `EditorDocument` 不直接复用 `edita-core` block 结构,而是自定义
|
||||
|
||||
## 7. 本结论如何进入后续任务
|
||||
|
||||
- `task-051` 完成判定:
|
||||
- 本文明确写清 `Editor / Block / Command / 无头执行 / 胶水`
|
||||
- `task-055` 开始时:
|
||||
- `mnote-editor-core` 优先实现无头编辑器容器
|
||||
- 不先实现 Leptos UI
|
||||
@@ -0,0 +1,192 @@
|
||||
# [recycle] Rust Block Editor Model v0
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/ai-first-rust-block-editor-baseline-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/rust-block-editor-phase0-baseline-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/edita/edita-core/src/lib.rs`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/blocks/src/document.rs`
|
||||
> - `/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs`
|
||||
|
||||
## 1. 目的
|
||||
|
||||
本文冻结 `task-053` 的第一阶段 Rust block editor 模型口径。
|
||||
|
||||
目标不是一次定义最终全文档系统,而是先确定:
|
||||
|
||||
- 首批 Rust block type
|
||||
- `BlockProps` 的最小字段面
|
||||
- `reference token` 的表示方式
|
||||
- `content_node` 的载荷格式
|
||||
- 哪些内容明确退出对 BlockNote JSON 的长期依赖
|
||||
|
||||
## 2. 第一阶段 block type
|
||||
|
||||
第一阶段固定以下块类型进入 canonical model:
|
||||
|
||||
- `paragraph`
|
||||
- `heading`
|
||||
- `bullet_list_item`
|
||||
- `numbered_list_item`
|
||||
- `quote`
|
||||
- `todo`
|
||||
- `code_block`
|
||||
- `page_reference`
|
||||
- `block_reference`
|
||||
|
||||
补充说明:
|
||||
|
||||
- `paragraph` 是默认文本块。
|
||||
- `heading` 是标题块,级别收敛到 `BlockProps.heading_level`。
|
||||
- `bullet_list_item` 与 `numbered_list_item` 先保留为列表项,而不是先做复杂 list container。
|
||||
- `todo` 用来承接当前 `advancedTodo` 的第一阶段简化版。
|
||||
- `page_reference` 与 `block_reference` 先作为稳定引用块进入模型。
|
||||
|
||||
下面这些不进入第一阶段 canonical model,只保留 placeholder 或 compat:
|
||||
|
||||
- `media`
|
||||
- `progressMeter`
|
||||
- `mindmap`
|
||||
- `onlineTable`
|
||||
|
||||
## 3. `BlockProps`
|
||||
|
||||
第一阶段 `BlockProps` 固定为轻量可扩展对象。
|
||||
|
||||
核心字段:
|
||||
|
||||
- `indent`
|
||||
- `heading_level`
|
||||
- `checked`
|
||||
- `collapsed`
|
||||
- `language`
|
||||
- `reference_target_id`
|
||||
- `reference_label`
|
||||
- `reference_token_strategy`
|
||||
- `extra`
|
||||
|
||||
固定口径:
|
||||
|
||||
- `indent` 表示有限缩进层级,不承诺长期树协议。
|
||||
- `heading_level` 只对 `heading` 生效。
|
||||
- `checked` 只对 `todo` 生效。
|
||||
- `collapsed` 只对 `heading` 生效,用于折叠标题。
|
||||
- `language` 只对 `code_block` 生效。
|
||||
- `reference_target_id` 与 `reference_label` 服务 `page_reference` / `block_reference`。
|
||||
- `extra` 只用于迁移期兼容字段,不能成为长期语义事实源。
|
||||
|
||||
## 4. `reference token` 策略
|
||||
|
||||
第一阶段引用 token 固定区分两类:
|
||||
|
||||
- `page_reference`
|
||||
- `block_reference`
|
||||
|
||||
第一阶段 token strategy 固定支持三种:
|
||||
|
||||
- `double_bracket`
|
||||
- `double_paren`
|
||||
- `inline_chip`
|
||||
|
||||
对应语义:
|
||||
|
||||
- `double_bracket` 对应 `[[page]]`
|
||||
- `double_paren` 对应 `((block))`
|
||||
- `inline_chip` 只用于 UI 渲染或迁移期兼容,不作为长期文本事实源
|
||||
|
||||
引用 token 最小字段:
|
||||
|
||||
- `kind`
|
||||
- `strategy`
|
||||
- `target_id`
|
||||
- `label`
|
||||
- `raw_token`
|
||||
|
||||
## 5. `content_node` 载荷格式
|
||||
|
||||
第一阶段 `content_node` 是 block 内内容的最小结构单元。
|
||||
|
||||
固定 payload 类型:
|
||||
|
||||
- `text`
|
||||
- `hard_break`
|
||||
- `reference_token`
|
||||
|
||||
### 5.1 `text`
|
||||
|
||||
`text` 节点字段:
|
||||
|
||||
- `text`
|
||||
- `marks`
|
||||
|
||||
`marks` 第一阶段只支持:
|
||||
|
||||
- `bold`
|
||||
- `italic`
|
||||
- `underline`
|
||||
- `strike`
|
||||
- `code`
|
||||
|
||||
### 5.2 `hard_break`
|
||||
|
||||
`hard_break` 用于显式换行,不引入复杂段内结构树。
|
||||
|
||||
### 5.3 `reference_token`
|
||||
|
||||
`reference_token` 节点直接挂稳定 token 对象,而不是把引用仅留在原始文本里。
|
||||
|
||||
## 6. 块对象与文档对象
|
||||
|
||||
第一阶段 canonical block 结构固定为:
|
||||
|
||||
- `block_id`
|
||||
- `block_type`
|
||||
- `props`
|
||||
- `content_nodes`
|
||||
- `child_block_ids`
|
||||
|
||||
第一阶段 canonical document 结构固定为:
|
||||
|
||||
- `document_id`
|
||||
- `root_block_ids`
|
||||
- `blocks`
|
||||
|
||||
设计原则:
|
||||
|
||||
- `blocks` 先按稳定数组序列输出,便于命令执行和导入导出。
|
||||
- `child_block_ids` 先表达父子关系,不强求复杂树索引结构。
|
||||
- `content_nodes` 替代对 BlockNote inline JSON 的长期依赖。
|
||||
|
||||
## 7. 与 BlockNote JSON 的边界
|
||||
|
||||
第一阶段固定口径:
|
||||
|
||||
- BlockNote JSON 只作为迁移期外部格式
|
||||
- Rust editor model 才是长期事实层
|
||||
- `BlockProps`、`content_node`、`reference token` 必须能独立表达核心语义
|
||||
|
||||
换句话说:
|
||||
|
||||
- 可以保留 `BlockNote JSON -> Rust model` 适配器
|
||||
- 不能继续把 BlockNote JSON 当作长期 canonical schema
|
||||
|
||||
## 8. 第一阶段不解决的事
|
||||
|
||||
下面这些明确不在 `task-053` 解决:
|
||||
|
||||
- 多人协作状态
|
||||
- 复杂 mark 树
|
||||
- 富媒体完整属性系统
|
||||
- `mindmap` / `onlineTable` 的完整内嵌编辑语义
|
||||
- 完整批量选择与多块复制粘贴
|
||||
|
||||
## 9. 结论
|
||||
|
||||
`task-053` 的固定口径是:
|
||||
|
||||
- 用首批 `paragraph`、`heading`、`bullet_list_item`、`todo`、`page_reference`、`block_reference` 等块型建立 Rust canonical model
|
||||
- 用 `BlockProps` 收拢最小块属性
|
||||
- 用 `reference token` 固定 `[[page]]` / `((block))` 的稳定表示
|
||||
- 用 `content_node` 承接块内文本与引用
|
||||
- 从这一阶段开始退出对 BlockNote JSON 的长期依赖
|
||||
@@ -0,0 +1,255 @@
|
||||
# [recycle] Rust Block Editor Phase 0 Baseline v1
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/ai-first-rust-block-editor-baseline-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/`
|
||||
> - `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/schema.ts`
|
||||
> - `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx`
|
||||
> - `/mnt/Data1T/mnote/wolai-frontend/src/lib/blocks/block-ops-adapter.ts`
|
||||
|
||||
## 1. 目的
|
||||
|
||||
本文用于冻结 task-048 所需的 Phase 0 基线:
|
||||
|
||||
1. 盘点当前 BlockNote 自定义块真实范围
|
||||
2. 盘点文档读写耦合点
|
||||
3. 冻结第一阶段 Rust Block Editor 的必须做 / 后补 / 延期边界
|
||||
4. 明确 `reference-code` 的采用、部分采用、不采用依据
|
||||
|
||||
Phase 0 的目标不是替换完全部现有能力,而是先把“什么必须进入第一阶段主链、什么只能先保留占位、什么明确延期”说清楚。
|
||||
|
||||
## 2. 当前 BlockNote 自定义块清单
|
||||
|
||||
当前 `customBlockSchema` 已注册的自定义块如下:
|
||||
|
||||
- `heading`
|
||||
- `advancedTodo`
|
||||
- `pageReference`
|
||||
- `blockReference`
|
||||
- `media`
|
||||
- `progressMeter`
|
||||
- `mindmap`
|
||||
- `onlineTable`
|
||||
|
||||
对应实现位置:
|
||||
|
||||
- `heading`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/schema.ts`
|
||||
- 现状:覆盖 BlockNote 默认标题块,允许 1-5 级并支持折叠。
|
||||
- `advancedTodo`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/AdvancedTodoBlock.tsx`
|
||||
- 现状:状态为 `todo/doing/done/cancelled`,点击轮转,`Alt+Click` 可直接置为 `cancelled`。
|
||||
- `pageReference`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/PageReferenceBlock.tsx`
|
||||
- 现状:块级页面引用,依赖 `pageId/title/asChildPage`,点击直接跳转文档页。
|
||||
- `blockReference`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/BlockReferenceBlock.tsx`
|
||||
- 现状:块级引用占位,运行时拉 `/api/blocks/get`,仅对段落/标题提供纯文本回写。
|
||||
- `media`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MediaBlock.tsx`
|
||||
- 现状:附件卡片,带选择资源、对齐、边框、OCR、下载、OnlyOffice 打开等强 UI 行为。
|
||||
- `progressMeter`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/ProgressBlock.tsx`
|
||||
- 现状:支持自动/手动模式;自动模式由编辑器扫描后续 `advancedTodo` 并计算百分比。
|
||||
- `mindmap`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx`
|
||||
- 现状:内嵌思维导图壳,包含预览、内联编辑、全屏、独立页、自动保存、资源联动。
|
||||
- `onlineTable`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/OnlineTableBlock.tsx`
|
||||
- 现状:内嵌表格预览卡片,支持拖拽改尺寸、删除、全屏编辑。
|
||||
|
||||
## 3. 当前读写耦合点
|
||||
|
||||
### 3.1 写链耦合
|
||||
|
||||
当前编辑主链仍深度绑定 BlockNote:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx`
|
||||
- `useCreateBlockNote(...)` 直接以 `customBlockSchema` 启动编辑器。
|
||||
- 保存经 `saveContent(...) -> /api/documents/save` 整体回写块树快照。
|
||||
- 自定义块的大量副作用都挂在 BlockNote 壳上:
|
||||
- `progressMeter` 自动统计
|
||||
- `media` 删除与恢复
|
||||
- `mindmap` 删除、自动保存、资源广播
|
||||
- `onlineTable` 删除、全屏切换
|
||||
- 协作仍挂着 `HocuspocusProvider + Y.Doc`,这与“当前不需要协作”的新主线并不一致。
|
||||
|
||||
### 3.2 读链耦合
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
|
||||
- 文档页在阅读态和编辑态之间切换。
|
||||
- `initialPageSubtree` 与 `serverPageSubtreeSnapshot` 已经进入页面读态,但编辑态仍直接消费 BlockNote 内容快照。
|
||||
- Phase 0 的关键判断:
|
||||
- 阅读态已经开始朝稳定 projection 收口。
|
||||
- 编辑态仍把 BlockNote 的块树和自定义 props 当成事实层。
|
||||
|
||||
### 3.3 工具与编辑器模型耦合
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/blocks/block-ops-adapter.ts`
|
||||
- 当前 AI / server tool 适配只支持 `paragraph` 与 `heading` 两种插入规格。
|
||||
- 这说明自定义块虽然很多,但真正能被稳定工具链操作的只是一小部分。
|
||||
|
||||
### 3.4 复杂块与正文块混杂
|
||||
|
||||
当前一个 BlockNote 文档同时承载:
|
||||
|
||||
- 轻量正文块:`heading`、段落、列表、待办
|
||||
- 语义引用块:`pageReference`、`blockReference`
|
||||
- 重型宿主块:`media`、`mindmap`、`onlineTable`
|
||||
- 派生统计块:`progressMeter`
|
||||
|
||||
这会导致第一阶段编辑器如果不先切分边界,就会被复杂块宿主逻辑拖着走。
|
||||
|
||||
## 4. 第一阶段迁移范围冻结
|
||||
|
||||
### 4.1 必须做
|
||||
|
||||
下面这些能力必须进入第一阶段 Rust Block Editor 主链:
|
||||
|
||||
- `heading`
|
||||
- 必须保留 1-5 级标题与折叠语义。
|
||||
- 原因:这是阅读态结构、目录、AI 引用定位的基础。
|
||||
- `advancedTodo`
|
||||
- 必须保留最小四态语义:`todo/doing/done/cancelled`。
|
||||
- 原因:当前 `progressMeter` 依赖它,且任务型记录是高频场景。
|
||||
- `pageReference`
|
||||
- 必须保留块级引用能力,并与后续行内页面引用 token 对齐。
|
||||
- 原因:页面导航和引用挂接是高频主链。
|
||||
- `blockReference`
|
||||
- 必须保留“占位插入 + 回显 + 最小跳转”能力。
|
||||
- 原因:当前产品已存在块引用占位语义,但不要求第一阶段保留远程原位编辑。
|
||||
- `media`
|
||||
- 必须保留“附件块占位 + 标题/描述 + 打开原资源”最小能力。
|
||||
- 原因:附件是文档主链,不应在替换编辑器时丢失。
|
||||
- 最小块命令集
|
||||
- 单块编辑
|
||||
- 插入同级块
|
||||
- 删除块
|
||||
- 合并块
|
||||
- 上移下移
|
||||
- 有限缩进
|
||||
- 最小格式收口
|
||||
- JSON 块快照
|
||||
- Markdown 导入导出
|
||||
- Rust command 层稳定执行
|
||||
|
||||
### 4.2 后补
|
||||
|
||||
下面这些能力保留,但不要求在第一阶段做成完整壳:
|
||||
|
||||
- `progressMeter`
|
||||
- 后补为“派生块”或“只读统计块”。
|
||||
- 原因:其核心不是富交互编辑,而是基于 `advancedTodo` 的派生结果。
|
||||
- `mindmap`
|
||||
- 后补为“占位卡片 + 打开独立编辑器/独立页”。
|
||||
- 原因:当前实现太重,不应绑进第一阶段核心编辑循环。
|
||||
- `onlineTable`
|
||||
- 后补为“占位卡片 + 打开全屏表格编辑器”。
|
||||
- 原因:表格预览和尺寸拖拽属于宿主 UI,不是轻量块编辑核心。
|
||||
- `media`
|
||||
- 第一阶段只保留最小资源卡片。
|
||||
- OCR、OnlyOffice、复杂菜单、拖拽尺寸属于后补。
|
||||
- `blockReference`
|
||||
- 第一阶段只保留占位和跳转。
|
||||
- 远程拉块、原位编辑、富回显属于后补。
|
||||
|
||||
### 4.3 延期
|
||||
|
||||
下面这些能力明确延期,不进入第一阶段主链:
|
||||
|
||||
- Yjs / Hocuspocus 协作
|
||||
- BlockNote / ProseMirror 专属菜单状态兼容
|
||||
- `mindmap` 内联复杂快捷键与嵌入式编辑器行为
|
||||
- `onlineTable` 内嵌尺寸拖拽与高保真表格交互
|
||||
- `media` 的完整 OCR / 上传 / replace-storage 流程
|
||||
- 任何依赖 BlockNote runtime 才能成立的行为
|
||||
|
||||
## 5. Phase 0 冻结清单
|
||||
|
||||
### 5.1 必须做清单
|
||||
|
||||
- [ ] 把 `heading`、段落、列表、基础待办、`advancedTodo` 抽成 Rust editor core 的稳定块模型
|
||||
- [ ] 把 `pageReference` 与 `blockReference` 抽成稳定引用块,而不是继续仅作为 BlockNote 自定义 React block
|
||||
- [ ] 把 `media` 抽成最小附件占位块,保留资源 id / 标题 / 打开动作
|
||||
- [ ] 规定 `mindmap`、`onlineTable` 第一阶段只以宿主占位块进入文档
|
||||
- [ ] 把块命令统一成 Rust 侧可测试命令,不再把 Enter / Backspace / Tab 语义写死在 BlockNote 事件壳
|
||||
- [ ] 把保存边界从“直接保存 BlockNote 快照”改为“保存 Rust block document + 最小宿主块 props”
|
||||
|
||||
### 5.2 后补清单
|
||||
|
||||
- [ ] `progressMeter` 自动统计改成独立派生器
|
||||
- [ ] `mindmap` 预览投影和独立编辑器接缝
|
||||
- [ ] `onlineTable` 预览投影和独立编辑器接缝
|
||||
- [ ] `blockReference` 远程块拉取和最小编辑同步
|
||||
- [ ] `media` 高级工具栏、OCR、OnlyOffice 入口
|
||||
|
||||
### 5.3 延期清单
|
||||
|
||||
- [ ] 协作协议
|
||||
- [ ] BlockNote 级 UI 状态兼容层
|
||||
- [ ] 重型块的嵌入式内核迁移
|
||||
|
||||
## 6. reference-code 采用依据
|
||||
|
||||
### 6.1 `edita-core`:采用
|
||||
|
||||
采用依据:
|
||||
|
||||
- `edita-core/src/lib.rs` 已经给出最小 `Editor / Block / Command` 无头边界。
|
||||
- 这与第一阶段“先把块命令从 UI 壳里抽出来”的目标完全一致。
|
||||
- 它不绑定 DOM、ProseMirror、Tiptap,因此适合作为 Rust editor core 的命令容器参考。
|
||||
|
||||
不直接采用的部分:
|
||||
|
||||
- `edita` UI 壳和示例不是当前主链事实层。
|
||||
- 原因:`mnote` 需要的是 command / state 组织,不是直接套它的现成编辑器外壳。
|
||||
|
||||
### 6.2 `blocks`:采用
|
||||
|
||||
采用依据:
|
||||
|
||||
- `blocks/src/block.rs`、`document.rs`、`converters.rs`、`history.rs`、`diff.rs`、`sanitizer.rs` 已覆盖:
|
||||
- 块文档模型
|
||||
- Markdown / HTML / JSON 转换
|
||||
- history
|
||||
- diff / merge
|
||||
- 这正是第一阶段最缺、且不该再自研一套的基础层。
|
||||
|
||||
部分保留胶水的地方:
|
||||
|
||||
- `mnote` 的 `pageReference`、`blockReference`、`mindmap`、`onlineTable` 不是 `blocks` 原生块型,需要补最小扩展映射。
|
||||
|
||||
### 6.3 `kode`:部分采用
|
||||
|
||||
部分采用依据:
|
||||
|
||||
- `kode-core` 的 buffer / selection / editing primitives 可作为块内输入器参考。
|
||||
- `kode-leptos` 的 `MarkdownEditorComponent` 与 `TreeWysiwygEditor` 可提供 Leptos 侧输入和光标处理样板。
|
||||
- `kode-doc` 的 tree node / token position 说明它适合借鉴“结构化编辑器内部表示”。
|
||||
|
||||
不直接全量采用的原因:
|
||||
|
||||
- `kode-doc` 自带一套更接近富文本树编辑器的内部结构。
|
||||
- `mnote` 第一阶段不需要把全部文档事实改造成另一套通用 tree editor 语义。
|
||||
|
||||
### 6.4 `leptos-tiptap`:部分采用
|
||||
|
||||
部分采用依据:
|
||||
|
||||
- `src/api/component.rs`、`use_tiptap_editor.rs`、`runtime/bridge.rs` 对 Leptos 接第三方 runtime 的方式很清楚。
|
||||
- 若 Rust-native UI 壳卡住,它适合当 fallback 接缝参考。
|
||||
|
||||
不作为主线采用的原因:
|
||||
|
||||
- 它本质仍是 Tiptap runtime bridge。
|
||||
- 第一阶段目标是 Rust-native 轻量块编辑器,而不是再回到 JS 富文本 runtime 作为主事实层。
|
||||
|
||||
## 7. Phase 0 结论
|
||||
|
||||
Phase 0 的冻结口径如下:
|
||||
|
||||
- 第一阶段必须覆盖 `heading`、`advancedTodo`、`pageReference`、`blockReference`、`media` 的最小稳定语义。
|
||||
- `progressMeter`、`mindmap`、`onlineTable` 进入第一阶段时只保留占位或派生结果,不把完整嵌入式编辑能力一起搬过去。
|
||||
- 命令核优先采用 `edita-core` 思路,文档模型与转换优先采用 `blocks`,输入壳局部借鉴 `kode`,`leptos-tiptap` 仅作 fallback 参考。
|
||||
@@ -0,0 +1,76 @@
|
||||
# [recycle] AI Tool Cutover Matrix
|
||||
|
||||
> 更新时间:2026-04-16
|
||||
>
|
||||
> 目标:建立 `builtins` 到 Rust Tool registry 的一一对应矩阵,并明确每个工具当前的割接状态。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
- 本文覆盖 `docsServerTools`、`mediaServerTools`、`mindmapServerTools`、`onlyofficeServerTools`、`registryBuiltins` 与 `ai-agent/run` 的一一对应关系。
|
||||
- `docs_search`、`docs_read` 已进入 Rust tool runtime;`ai-agent/run` 在 Convex 模式下直接通过 `executeRustBridgeTool(...)` 执行,`docsServerTools` 仅保留非 Convex / 兼容 fallback。
|
||||
- `search_web`、`image_read`、`slash_run` 已进入 Rust tool 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。
|
||||
@@ -0,0 +1,132 @@
|
||||
# [recycle] Mindmap 与 OnlyOffice 当前边界说明
|
||||
|
||||
> 更新时间:2026-04-14
|
||||
>
|
||||
> 主仓:`/mnt/Data1T/mnote`
|
||||
|
||||
## 1. Mindmap 当前主链
|
||||
|
||||
### 1.1 主组件与嵌入链路
|
||||
|
||||
- 主组件:`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx`
|
||||
- BlockNote 注册入口:`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/schema.ts`
|
||||
- 独立全屏页:`/mnt/Data1T/mnote/wolai-frontend/src/app/mindmap/[docId]/[mindmapId]/page.tsx`
|
||||
|
||||
当前事实:
|
||||
|
||||
- `MindmapBlockView` 同时服务文档内嵌块与独立全屏页。
|
||||
- 独立页通过 `editorStub` 复用同一组件,不额外复制第二套思维导图主壳。
|
||||
- 文档内嵌与独立页共用同一套工具栏、侧栏、导航器、缩略图与上下文菜单子组件。
|
||||
|
||||
### 1.2 数据入口与 ops 边界
|
||||
|
||||
- 本地读写入口:`/mnt/Data1T/mnote/wolai-frontend/src/lib/mindmap/mindmapLocalStore.ts`
|
||||
- 节点操作入口:`/mnt/Data1T/mnote/wolai-frontend/src/lib/mindmap/mindmapOps.ts`
|
||||
- 页面 API:
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/mindmap/[docId]/route.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts`
|
||||
|
||||
当前拆分:
|
||||
|
||||
- 前端交互状态保留在 `MindmapBlock.tsx` 及其子组件内。
|
||||
- 本体数据读写由 `mindmapLocalStore.ts` 提供本地文件落盘兼容接口。
|
||||
- 节点增删改、引用、注释、链接等操作由 `mindmapOps.ts` 统一描述为 `MindmapOp`。
|
||||
- 页面 API 当前通过 Convex `api.mindmaps.*` 承接 `get/put/restore/purge`,保留现有前端形态不变。
|
||||
- 当前 Mindmap API 已补统一元信息口径:
|
||||
- `pageId = documentId`
|
||||
- `attachmentId = mindmapId`
|
||||
- `workspaceId` 统一来自页面归属工作区
|
||||
- `requestId/traceId` 统一从请求头透传或在路由侧兜底生成
|
||||
|
||||
### 1.3 后续 adapter 目标
|
||||
|
||||
后续若进入 Rust adapter,只抽象以下边界,不复制历史仓 UI:
|
||||
|
||||
- 节点树结构
|
||||
- 节点引用 `refs`
|
||||
- 节点操作日志 `MindmapOp`
|
||||
- mindmap 与 page/block 的绑定关系
|
||||
|
||||
## 2. OnlyOffice 当前主链
|
||||
|
||||
### 2.1 页面与 API 边界
|
||||
|
||||
- 页面入口:
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/onlyoffice/page.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/onlyoffice/OnlyOfficeClientPage.tsx`
|
||||
- 页面侧面板:
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/onlyoffice/OnlyOfficeAiAgentPanel.tsx`
|
||||
- API 边界:
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/onlyoffice/sign/route.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/onlyoffice/proxy/route.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/onlyoffice/callback/route.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/onlyoffice/forcesave/route.ts`
|
||||
|
||||
当前拆分:
|
||||
|
||||
- `page.tsx` 负责页面级早期 DOM 补丁与容器加载。
|
||||
- `OnlyOfficeClientPage.tsx` 负责编辑器初始化、内部请求改写、代理地址适配与 callback URL 组装。
|
||||
- `sign` 负责 JWT 签名。
|
||||
- `proxy` 负责同源代理与回源 URL 安全限制。
|
||||
- `callback` 负责保存回写存储。
|
||||
- `forcesave` 负责触发文档服务器强制保存。
|
||||
|
||||
### 2.2 与正文和附件的关系
|
||||
|
||||
- Office 文件入口仍来自 `MediaBlock`:`/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MediaBlock.tsx`
|
||||
- 正文里只保留附件块与跳转入口,不在 BlockNote 内直接运行 OnlyOffice 编辑器。
|
||||
- OnlyOffice 编辑器继续在独立 `/onlyoffice` 页面运行。
|
||||
- 当前 OnlyOffice 已稳定使用:
|
||||
- `documentId` 作为页面业务标识
|
||||
- `assetId` 作为附件业务标识
|
||||
- 页面归属仍由文档/附件在 Convex 中的 `workspace_id` 与 `document_id` 决定
|
||||
|
||||
## 2.5 与 Mindmap 对齐后的共享标识口径
|
||||
|
||||
- 页面标识:
|
||||
- Mindmap 使用 `pageId/documentId`
|
||||
- OnlyOffice 使用 `documentId`
|
||||
- 附件标识:
|
||||
- Mindmap 使用 `attachmentId/mindmapId`
|
||||
- OnlyOffice 使用 `assetId`
|
||||
- 页面归属:
|
||||
- 两者都以 Convex 中的 `workspace_id + document_id` 作为最终归属真相
|
||||
- 追踪字段:
|
||||
- Mindmap API 已返回 `requestId/traceId`
|
||||
- OnlyOffice 页面当前通过查询参数与调试上下文持有 `assetId/documentId/userId`,后续若接入统一 Rust adapter,继续沿用同一页面/附件口径
|
||||
|
||||
### 2.3 静态资源边界
|
||||
|
||||
- 根目录静态资源保留在 `/mnt/Data1T/mnote/src/components/onlyoffice/`
|
||||
- 当前已确认存在:
|
||||
- `onlyoffice-web-apps/`
|
||||
- `onlyoffice-plugins/`
|
||||
- `onlyoffice-data/`
|
||||
|
||||
这些目录继续保留为运行所需静态资源、插件和数据目录,不做迁移式替换。
|
||||
|
||||
### 2.4 后续 adapter 目标
|
||||
|
||||
后续若进入 Rust adapter,只抽象以下边界:
|
||||
|
||||
- 对象解析与附件定位
|
||||
- 签名生成
|
||||
- callback 写回
|
||||
- forcesave 触发
|
||||
- 代理回源请求
|
||||
|
||||
## 3. 历史仓禁止误复制范围
|
||||
|
||||
以下历史仓路径只作为参考,不进入主仓主线:
|
||||
|
||||
- `/mnt/Data1T/mnote-rust/app/documents/[id]/mindmap/page.tsx`
|
||||
- `/mnt/Data1T/mnote-rust/app/documents/[id]/office/page.tsx`
|
||||
- `/mnt/Data1T/mnote-rust/components/sidebar/sidebar.tsx`
|
||||
- `/mnt/Data1T/mnote-rust/app/page.tsx`
|
||||
- `/mnt/Data1T/mnote-rust/app/onlyoffice/page.tsx`
|
||||
|
||||
约束:
|
||||
|
||||
- 不复制第二套对象页壳。
|
||||
- 不复制第二套 sidebar 壳。
|
||||
- `adapter-onlyoffice`、`adapter-mindmap` 继续作为后置实现项,而不是本阶段直接迁入主线。
|
||||
@@ -0,0 +1,48 @@
|
||||
# [recycle] Phase 8 Cutover Log 2026-04-15
|
||||
|
||||
## 概要
|
||||
|
||||
- 完成 Phase 8 收口,Rust 已成为 mnote 的唯一业务执行平面。
|
||||
- 完成 `task-036` 到 `task-044`,并清空 harness 中全部历史失败/待办状态。
|
||||
- ONLYOFFICE 已升级到 `9.3.1`,真实页面打开、插件桥、callback、forcesave 回归通过。
|
||||
|
||||
## 本轮关键变更
|
||||
|
||||
### 1. 页面与对象主链
|
||||
|
||||
- 页面旧面 `documents/create-child`、`documents/embed`、`documents/empty-trash`、`documents/purge`、`documents/template` 已收口到统一 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 工具链做一轮发布前运行态回归复核。
|
||||
@@ -0,0 +1,193 @@
|
||||
# [recycle] 发布前源代码审计
|
||||
|
||||
> 更新时间:2026-04-16
|
||||
>
|
||||
> 审计范围:`/mnt/Data1T/mnote`
|
||||
|
||||
## 1. Bridge 入口与日志回查
|
||||
|
||||
### 1.1 最小查询与写入入口
|
||||
|
||||
当前已确认的 bridge 主线入口:
|
||||
|
||||
- 查询:
|
||||
- `src/app/api/documents/content/route.ts`
|
||||
- `src/app/api/documents/meta/route.ts`
|
||||
- `src/app/api/sidebar/route.ts`
|
||||
- 写入:
|
||||
- `src/app/api/documents/title/route.ts`
|
||||
- `src/app/api/documents/stats/route.ts`
|
||||
- `src/app/api/documents/options/route.ts`
|
||||
- `src/app/api/documents/save/route.ts`
|
||||
- `src/app/api/blocks/patch/route.ts`
|
||||
- `src/app/api/onlyoffice/callback/route.ts`
|
||||
|
||||
### 1.2 日志与事件落账
|
||||
|
||||
当前已确认:
|
||||
|
||||
- `src/lib/documents/metadata-command-adapter.ts` 成功路径会调用 `recordBridgeCommandArtifacts`
|
||||
- `src/lib/documents/save-command-adapter.ts` 成功路径会调用 `recordBridgeCommandArtifacts`
|
||||
- `src/app/api/blocks/patch/route.ts` 成功路径会调用 `recordBridgeCommandArtifacts`
|
||||
- `src/lib/documents/media-asset-command-adapter.ts` 会在 ONLYOFFICE callback 成功写回后尝试复用同一 bridge log 落账
|
||||
- `src/lib/documents/bridge-log.ts` 会同时写入:
|
||||
- `bridgeLogs.recordCommandLog`
|
||||
- `bridgeLogs.recordDomainEvent`
|
||||
- 回查入口:
|
||||
- `src/app/api/bridge/request/route.ts -> bridgeLogs.listByRequest`
|
||||
- `src/app/api/bridge/trace/route.ts -> bridgeLogs.listByTrace`
|
||||
|
||||
结论:
|
||||
|
||||
- bridge 的最小查询、最小写入、日志生成、事件生成和错误返回已经形成源码闭环。
|
||||
|
||||
## 2. 保存口径审计
|
||||
|
||||
当前正文与页面元信息主写链如下:
|
||||
|
||||
- `documents.save -> api.documents.updateContent`
|
||||
- `documents.title.update -> api.documents.updateTitle`
|
||||
- `documents.stats.update -> api.documents.updateStats`
|
||||
- `documents.options.update -> api.documents.updateOptions`
|
||||
- `blocks.patch -> 读取文档内容后回写 api.documents.updateContent`
|
||||
|
||||
OnlyOffice 写回:
|
||||
|
||||
- `src/app/api/onlyoffice/callback/route.ts -> media.assets.replace_storage -> api.mediaAssets.replaceStorageFromUpload`
|
||||
|
||||
结论:
|
||||
|
||||
- 页面正文、页面元信息、块补丁与 OnlyOffice 附件写回最终都以 Convex mutation 为主事实写入点。
|
||||
- 当前未发现把页面/块主真相改写到 `mnote-rust`、本地缓存目录或其他派生目录的主写链。
|
||||
|
||||
## 3. `trace_id` / `request_id` / `workspace_id` / `page_id` 一致性
|
||||
|
||||
### 3.1 前端与 route
|
||||
|
||||
- `buildDocumentBridgeContext()` 统一生成 `requestId`、`traceId`、`workspaceId`
|
||||
- `buildDocumentBridgeContextWithActor()` 可为 ONLYOFFICE callback 这类无用户 cookie 的服务端回调显式注入 `actor/source`
|
||||
- `buildDocumentCommandEnvelope()` 统一挂载 `commandId`、`target.pageId`
|
||||
- `documents.title/stats/options/save` 均将 `normalizedDocumentId` 作为 `target.pageId`
|
||||
- `onlyoffice/callback` 会把 `asset.document_id` 作为 `target.pageId`,并透传 `workspaceId`
|
||||
|
||||
### 3.2 日志落账
|
||||
|
||||
- `recordBridgeCommandArtifacts()` 使用同一 `context.requestId`、`context.traceId`
|
||||
- `workspaceId` 统一取 `target.workspaceId` 或 `context.workspaceId`
|
||||
- `targetPageId` 统一落到 command log
|
||||
- domain event payload 同时记录 `request_id`、`trace_id`、`command_id`、`command_name`
|
||||
|
||||
### 3.3 当前辅助验证
|
||||
|
||||
- `src/lib/documents/bridge.test.ts` 已覆盖:
|
||||
- `target.pageId = doc_1`
|
||||
- `executeSaveBridgeCommand` 成功路径会调用 `recordBridgeCommandArtifacts`
|
||||
- 返回结果保留 `req_1`、`trace_1`
|
||||
- `src/app/api/onlyoffice/callback/route.test.ts` 已覆盖:
|
||||
- callback 成功路径会调用 `executeMediaAssetWritebackBridgeCommand`
|
||||
- `storageId`、`assetId`、`documentId`、`workspaceId` 会进入 `media.assets.replace_storage` envelope
|
||||
|
||||
结论:
|
||||
|
||||
- `trace_id`、`request_id`、`workspace_id`、`page_id` 在 route、adapter、bridge log、回查入口之间已有统一口径。
|
||||
|
||||
## 4. 禁止事项复查
|
||||
|
||||
### 4.1 不双仓并行
|
||||
|
||||
- 主仓脚本与文档已统一指向 `/mnt/Data1T/mnote`
|
||||
- `wolai-frontend/src`、`wolai-frontend/convex`、`rust/` 下未发现直接依赖 `/mnt/Data1T/mnote-rust` 的运行时代码路径
|
||||
|
||||
### 4.2 不跨仓链接
|
||||
|
||||
- 排除 `node_modules`、`.next`、`.venv`、`rust/target` 等构建产物后,主仓源码范围内未发现跨仓符号链接
|
||||
|
||||
### 4.3 不复制第二套产品前端壳
|
||||
|
||||
- 主仓保留的是既有 `wolai-frontend` 主壳
|
||||
- 历史仓对象页壳与第二套 sidebar 仅在文档中被标记为禁止误复制范围
|
||||
|
||||
### 4.4 不把对象页壳 / diagnostics / smoke 页带入主线
|
||||
|
||||
- 当前主仓 `wolai-frontend/src/app` 下未发现新增的 `diagnostic` / `smoke` 页面目录
|
||||
- 历史仓相关页面已在边界文档中明确列入禁止误复制清单
|
||||
|
||||
### 4.5 不散落多个 Rust 源码根
|
||||
|
||||
- 主仓顶层 `Cargo.toml` 仅存在 `rust/Cargo.toml`
|
||||
- 其余 crate `Cargo.toml` 均位于 `rust/crates/*`
|
||||
|
||||
## 5. 运行态复核结果与最终结论
|
||||
|
||||
本审计只能关闭源码可验证项,但截至 2026-04-16,发布前运行态复核已经补齐五条直接证据:
|
||||
|
||||
- 已复跑 `scripts/task019-document-ui-regression.js`,文档页标题、正文、保存、刷新、回查链通过。
|
||||
- 已复跑 `scripts/task021-mindmap-ui-regression.js`,Mindmap 全屏页的新增子节点、删除子节点、保存链、刷新回查与 `requestId/traceId` 元信息同步通过。
|
||||
- 已复跑 `scripts/task022-onlyoffice-ui-regression.js`,ONLYOFFICE `9.3.1` 基线下的打开、插件桥、callback 写回、forcesave 与 `storage_id` 变化通过。
|
||||
- 已通过 `pnpm exec vitest run src/lib/ai-agent/runtime/runAgent.test.ts`,补齐 `docs_search -> docs_read` 的 AI runtime 调用证据。
|
||||
- 已通过 `bash /mnt/Data1T/mnote/rust/scripts/task049-cli-smoke.sh`,补齐 CLI 主链的最小真实执行验收。
|
||||
|
||||
当前已不再存在阻塞发布的 CLI / AI 运行态尾项。
|
||||
|
||||
补充核验结果:
|
||||
|
||||
- `cargo test --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p mnote-cli -p bridge-runtime` 已通过。
|
||||
- `wolai-frontend` 相关定向 `eslint` 当前无 error,仅剩仓库既有 warnings,不构成本轮发布阻塞。
|
||||
|
||||
仍然要求环境满足的前置条件:
|
||||
|
||||
- 浏览器回归环境可访问 `http://127.0.0.1:3000`
|
||||
- `MNOTE_ONLYOFFICE_PROBE_DOCX=/tmp/mnote-onlyoffice-probe/probe.docx` 可用
|
||||
- 已登录态可通过 `/auth` 或测试账号快速登录建立
|
||||
|
||||
## 6. 旧 TS 执行面割接清单
|
||||
|
||||
当前源码审计之外,还需要看“哪些 TS route 还能留,哪些只是历史兼容”。
|
||||
|
||||
本轮已新增:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md`
|
||||
|
||||
该文档已把主仓当前执行面明确分成四类:
|
||||
|
||||
- `RUST_OWNER`
|
||||
- `TS_TRANSPORT_KEEP`
|
||||
- `TS_COMPAT_PENDING`
|
||||
- `TS_LEGACY_DELETE`
|
||||
|
||||
其中当前仍需要长期观察但不再构成主链阻塞的旧面主要包括:
|
||||
|
||||
- `src/app/api/documents/embed/route.ts`
|
||||
- `src/app/api/mindmap-ai/**`
|
||||
|
||||
其中页面旧面在 2026-04-16 的最终口径如下:
|
||||
|
||||
- `src/app/api/documents/create-child/route.ts` 已改走 `documents.create`
|
||||
- `src/app/api/documents/empty-trash/route.ts` 已改走 `documents.emptyTrashByWorkspace`
|
||||
- `src/app/api/documents/purge/route.ts` 已改走 `documents.purge`
|
||||
- `src/app/api/documents/template/route.ts` 已改走 `documents.template`
|
||||
- `src/app/api/documents/embed/route.ts` 已改走 `documents.save`,但目标页面插入位与 `pageReference` block 组装仍在 TS,因此继续按 `TS_COMPAT_PENDING` / 产品编排壳维护
|
||||
- `src/app/api/mindmap/[docId]/route.ts` 与 `src/app/api/mindmap/[docId]/[mindmapId]/route.ts` 的 `DELETE/PATCH` 已改走 `mindmaps.delete/restore/purge`
|
||||
- `src/app/api/mindmap-trash/empty/route.ts` 已改走 `mindmaps.emptyTrashByWorkspace`
|
||||
- `src/app/api/blocks/*` 现网主链实际统一走 `src/lib/blocks/block-command-adapter.ts`;`src/lib/documents/block-command-adapter.ts` 应视为遗留兼容 helper,而非现网主执行面
|
||||
- `src/app/api/ai-agent/run/route.ts` 的核心工具主链现在已经统一走 Rust tool runtime:`docs_search`、`docs_read`、`search_web`、`image_read`、`slash_run` 不再保留 TS 真执行兜底
|
||||
|
||||
结论:
|
||||
|
||||
- 当前可以说“Rust 已成为 mnote 的主业务执行平面”。
|
||||
- 仍保留的 TS 路径只剩 `TS_TRANSPORT_KEEP`、少量 `TS_COMPAT_PENDING` 产品编排壳与冻结的 `TS_LEGACY_DELETE` 清单。
|
||||
- 第一批 `TS_LEGACY_DELETE` 已不再阻塞这轮 release readiness;它们现在的要求是“保持纯壳 / 冻结 helper,不允许反向长出新的业务规则”。
|
||||
|
||||
最终 release readiness 口径:
|
||||
|
||||
- 若按源码审计口径:可以说明“Rust 已成为 mnote 的唯一业务执行平面”,同时明确仍保留 `TS_TRANSPORT_KEEP` 与少量产品编排壳。
|
||||
- 若按完整发布口径:文档页、Mindmap、OnlyOffice、AI runtime、CLI smoke 的 Phase 8 scoped gate 已闭合,可以给出最终完成结论。
|
||||
- 后续维护口径:剩余 `TS_TRANSPORT_KEEP` / `TS_LEGACY_DELETE` 只属于长期清理与边界维护,不再构成第二套业务执行平面。
|
||||
|
||||
补记:
|
||||
|
||||
- 2026-04-15 已复跑 `scripts/task019-document-ui-regression.js`,确认文档页标题、正文、保存与刷新回查通过。
|
||||
- 2026-04-15 已复跑 `scripts/task021-mindmap-ui-regression.js`,确认新增/删除节点、保存链、刷新回查与 `requestId` / `traceId` 元信息同步通过。
|
||||
- 2026-04-15 已复跑 `scripts/task022-onlyoffice-ui-regression.js`,在升级后的 ONLYOFFICE `9.3.1` 基线上确认 `oo_insert_text` 插件桥、`同步保存`、callback 写回与 `storage_id` 变化仍然通过;本次 callback 最终写回已不再由 route 直接调用 `api.mediaAssets.replaceStorageFromUpload`,而是先经 `media.assets.replace_storage` bridge 命令再落到 Convex。
|
||||
- 2026-04-16 已通过 `src/lib/ai-agent/runtime/runAgent.test.ts`,确认 `docs_search -> docs_read` 的运行态工具链顺序、事件流与结果归一化闭合。
|
||||
- 2026-04-16 已通过 `rust/scripts/task049-cli-smoke.sh`,确认 `sidebar/page/block/search/tool/mindmap` 主链 CLI 真实执行闭合。
|
||||
@@ -0,0 +1,85 @@
|
||||
# [recycle] Rust 回迁资产审计清单
|
||||
|
||||
> 更新时间:2026-04-14
|
||||
>
|
||||
> 主仓:`/mnt/Data1T/mnote`
|
||||
|
||||
## 1. 本轮已确认的主仓 Rust 资产
|
||||
|
||||
### 1.1 Workspace 根
|
||||
|
||||
- `rust/Cargo.toml`
|
||||
- `rust/Cargo.lock`
|
||||
|
||||
### 1.2 已并入主线的 crate
|
||||
|
||||
- `rust/crates/core-domain/`
|
||||
- `rust/crates/core-protocol/`
|
||||
- `rust/crates/event-log/`
|
||||
- `rust/crates/storage-convex-bridge/`
|
||||
- `rust/crates/index-fts/`
|
||||
|
||||
### 1.3 已并入主线的 Rust 设计文档
|
||||
|
||||
- `rust/design/INDEX.md`
|
||||
- `rust/design/core/01-domain-model-v0.md`
|
||||
- `rust/design/core/02-command-query-tool-protocol-v0.md`
|
||||
- `rust/design/core/03-storage-event-indexing-v0.md`
|
||||
- `rust/design/core/04-onlyoffice-integration-boundary-v0.md`
|
||||
|
||||
### 1.4 本轮新增的主仓设计文档
|
||||
|
||||
- `design/rust-kernel-backport-plan.md`
|
||||
- `design/rust-kernel-backport-phase-checklist.md`
|
||||
- `design/rust-single-repo-maintenance-boundary.md`
|
||||
- `design/mindmap-onlyoffice-boundary.md`
|
||||
- `design/sidebar-rust-query-target.md`
|
||||
|
||||
## 2. 当前仍未并入主线的目录
|
||||
|
||||
以下目录当前未在主仓 `rust/` 内出现,仍保持后置状态:
|
||||
|
||||
- `rust/crates/adapter-onlyoffice/`
|
||||
- `rust/crates/adapter-mindmap/`
|
||||
- `rust/crates/adapter-legacy-mnote/`
|
||||
- `rust/crates/mnote-cli/`
|
||||
- `rust/bridge/`
|
||||
- `rust/scripts/`
|
||||
- `rust/fixtures/`
|
||||
- `rust/tests/`
|
||||
|
||||
## 3. 已确认没有误带入的历史噪音
|
||||
|
||||
当前主仓 `rust/` 与根 `design/` 下未发现以下类型内容被直接带入主线:
|
||||
|
||||
- 第二套 Next 页面壳
|
||||
- 历史 `diagnostics` 页面
|
||||
- `smoke` 页面
|
||||
- 历史仓 `.next/`、`.tmp/` 产物目录
|
||||
- `adapter-*` 与 `mnote-cli` 实现目录
|
||||
|
||||
备注:
|
||||
|
||||
- `rust/target/` 为本地编译产物,不属于回迁设计资产。
|
||||
- 发布前若需进一步清理编译产物,应单独征得用户同意,不在本轮处理。
|
||||
|
||||
## 4. 主仓启动路径核对
|
||||
|
||||
当前可验证的主仓入口如下:
|
||||
|
||||
- 根目录热启动:`npm run desktop:hot`
|
||||
- 前端开发:`cd /mnt/Data1T/mnote/wolai-frontend && pnpm dev`
|
||||
- 后端开发:`cd /mnt/Data1T/mnote/wolai-backend && uvicorn app.main:app --reload --port 8000`
|
||||
- Rust 校验:`cargo --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml ...`
|
||||
|
||||
核对结果:
|
||||
|
||||
- 根 `package.json` 的仓库级脚本只指向 `scripts/*.js`
|
||||
- `scripts/desktop-hot.js` 只从主仓根目录解析 `.env.all`
|
||||
- `scripts/desktop-hot.js` 只调起 `wolai-frontend/` 与 `wolai-backend/`
|
||||
- `AGENTS.md` 的常用开发命令也全部指向 `/mnt/Data1T/mnote`
|
||||
|
||||
## 5. 当前结论
|
||||
|
||||
- 主仓已具备“只进入 `/mnt/Data1T/mnote` 就能找到前端、后端、Rust、OnlyOffice 相关入口”的文档与脚本口径。
|
||||
- 本轮新增目录与文档均位于主仓 `design/` 或既有 `rust/` 主线下,未把 `mnote-rust` 的历史页面壳或构建噪音带入主线。
|
||||
@@ -0,0 +1,399 @@
|
||||
# [recycle] Rust 内核总割接方案 v1
|
||||
|
||||
> 更新时间:2026-04-16
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-replacement-roadmap-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-missing-targets.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-phase-checklist.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/release-readiness-source-audit.md`
|
||||
|
||||
## 1. 目的
|
||||
|
||||
本文只回答一个问题:
|
||||
|
||||
> **什么时候可以宣布“Rust 已成为 mnote 唯一业务执行平面”,以及宣布前必须退役哪些旧 TS 执行面。**
|
||||
|
||||
这里的“退役”不等于“把所有 Next route 全删掉”。
|
||||
|
||||
真正要退役的是:
|
||||
|
||||
- TS route 内部承载的业务规则
|
||||
- TS 私有工具注册表里的第二套执行语义
|
||||
- 绕过 Rust command/query/tool 的直连 Convex 主链
|
||||
|
||||
允许长期保留的是:
|
||||
|
||||
- transport
|
||||
- auth
|
||||
- session
|
||||
- streaming
|
||||
- SSR / BFF
|
||||
- 第三方对象服务回调壳
|
||||
|
||||
---
|
||||
|
||||
## 2. 状态定义
|
||||
|
||||
为了避免后续再出现“看起来迁了,实际上没迁”的模糊表述,本文统一使用下面四类状态。
|
||||
|
||||
### 2.1 `RUST_OWNER`
|
||||
|
||||
定义:
|
||||
|
||||
- 核心业务规则已经进入 Rust command/query/tool/runtime
|
||||
- TS route 只做参数校验、鉴权、HTTP 包装、Convex transport 或第三方请求转发
|
||||
|
||||
处理策略:
|
||||
|
||||
- 允许继续保留 route 壳
|
||||
- 不允许再往 route 内加业务规则
|
||||
|
||||
### 2.2 `TS_TRANSPORT_KEEP`
|
||||
|
||||
定义:
|
||||
|
||||
- 这条 route 本身不是业务真规则入口
|
||||
- 但因为浏览器会话、回调、上传下载、SSE、客户端桥等原因,需要长期保留在 Web 层
|
||||
|
||||
处理策略:
|
||||
|
||||
- 永久保留或长期保留
|
||||
- 只能承载 transport / session / proxy / callback
|
||||
|
||||
### 2.3 `TS_COMPAT_PENDING`
|
||||
|
||||
定义:
|
||||
|
||||
- 当前已经有部分 Rust 接缝
|
||||
- 但仍存在 TS 业务拼接、对象规则或直连 Convex 的旧执行逻辑
|
||||
|
||||
处理策略:
|
||||
|
||||
- 不能宣布总割接完成
|
||||
- 必须继续迁到 Rust 后,才能降级为 `RUST_OWNER` 或 `TS_TRANSPORT_KEEP`
|
||||
|
||||
### 2.4 `TS_LEGACY_DELETE`
|
||||
|
||||
定义:
|
||||
|
||||
- 该 route 或旧逻辑只服务于历史兼容、诊断、临时过渡或旧 UI
|
||||
- 一旦对应能力完成 Rust cutover 并切完调用方,就应删除
|
||||
|
||||
处理策略:
|
||||
|
||||
- 先停写
|
||||
- 再切调用
|
||||
- 最后物理删除
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前总盘点
|
||||
|
||||
## 3.1 页面系统
|
||||
|
||||
### 已进入 `RUST_OWNER`
|
||||
|
||||
- `wolai-frontend/src/app/api/documents/meta/route.ts`
|
||||
- `wolai-frontend/src/app/api/documents/content/route.ts`
|
||||
- `wolai-frontend/src/app/api/documents/title/route.ts`
|
||||
- `wolai-frontend/src/app/api/documents/stats/route.ts`
|
||||
- `wolai-frontend/src/app/api/documents/options/route.ts`
|
||||
- `wolai-frontend/src/app/api/documents/save/route.ts`
|
||||
- `wolai-frontend/src/app/api/documents/create/route.ts`
|
||||
- `wolai-frontend/src/app/api/documents/move/route.ts`
|
||||
- `wolai-frontend/src/app/api/documents/delete/route.ts`
|
||||
- `wolai-frontend/src/app/api/documents/restore/route.ts`
|
||||
- `wolai-frontend/src/app/api/documents/duplicate/route.ts`
|
||||
- `wolai-frontend/src/app/api/documents/copy-tree/route.ts`
|
||||
- `wolai-frontend/src/app/api/documents/create-child/route.ts`
|
||||
- `wolai-frontend/src/app/api/documents/empty-trash/route.ts`
|
||||
- `wolai-frontend/src/app/api/documents/purge/route.ts`
|
||||
- `wolai-frontend/src/app/api/documents/template/route.ts`
|
||||
|
||||
判定依据:
|
||||
|
||||
- 已统一走 `buildDocument(Command|Query)Envelope`
|
||||
- 已切到共享 page/metadata/save adapter 或 `resolveRustBridge*`
|
||||
- route 只剩 transport、鉴权、参数整理与 HTTP 返回
|
||||
- `create-child` 已改为复用 `documents.create`
|
||||
- `template`、`empty-trash`、`purge` 已补齐 Rust runtime/transport 映射,不再由 `page-command-adapter.ts` 直接调用 Convex mutation
|
||||
|
||||
### 仍是 `TS_COMPAT_PENDING`
|
||||
|
||||
- `wolai-frontend/src/app/api/documents/embed/route.ts`
|
||||
|
||||
原因:
|
||||
|
||||
- `embed` 已不再直接调用 `api.documents.updateContent`,最终写入已改走 `documents.save`。
|
||||
- 但目标页面插入点计算、`pageReference` block 组装仍在 TS 侧完成,尚未进入独立 Rust command 面。
|
||||
- 因此它已经摆脱“直连 Convex mutation”的假完成状态,但还不能宣布为完全 `RUST_OWNER`。
|
||||
|
||||
处理结论:
|
||||
|
||||
- 页面旧面中,`create-child`、`template`、`empty-trash`、`purge` 已转为统一 Rust transport。
|
||||
- `embed` 仍是 Phase 8 前需要继续清理的剩余页面旧面。
|
||||
- 当前仍不可直接物理删除这些 route 文件,必须先切完调用方并确认不再承载 TS 内容编排。
|
||||
|
||||
---
|
||||
|
||||
## 3.2 块系统
|
||||
|
||||
### 已进入 `RUST_OWNER`
|
||||
|
||||
- `wolai-frontend/src/app/api/blocks/get/route.ts`
|
||||
- `wolai-frontend/src/app/api/blocks/patch/route.ts`
|
||||
- `wolai-frontend/src/app/api/blocks/move/route.ts`
|
||||
- `wolai-frontend/src/app/api/blocks/embed/route.ts`
|
||||
|
||||
判定依据:
|
||||
|
||||
- 现网 route 实际统一走 `wolai-frontend/src/lib/blocks/block-command-adapter.ts`
|
||||
- `patch/move/embed` 已通过 `documents.save` 主链落盘,不再依赖 route 内直连 Convex mutation
|
||||
- route 仅保留 transport、参数校验与 HTTP 返回
|
||||
|
||||
### 仍需冻结的兼容 helper
|
||||
|
||||
- `wolai-frontend/src/lib/documents/block-command-adapter.ts`
|
||||
|
||||
说明:
|
||||
|
||||
- 该文件当前不再是 `/api/blocks/*` 的主调用链。
|
||||
- 它仍保留旧桥接 helper 语义,因此需要明确按兼容层冻结,不能再被当成“现网主执行面”继续扩展。
|
||||
|
||||
### 可删的旧逻辑
|
||||
|
||||
- 旧 `loadBlocks/saveBlocks` 私有执行路径
|
||||
- 任何绕过共享 block adapter 的 AI 文档写入旧实现
|
||||
|
||||
处理结论:
|
||||
|
||||
- 块系统主链已经满足“route 保留、旧业务逻辑退役”的前提
|
||||
- 后续只允许在 Rust block ops 上继续扩展
|
||||
|
||||
---
|
||||
|
||||
## 3.3 查询聚合与观测
|
||||
|
||||
### 已进入 `RUST_OWNER`
|
||||
|
||||
- `wolai-frontend/src/app/api/sidebar/route.ts`
|
||||
- `wolai-frontend/src/app/api/search/documents/route.ts`
|
||||
- `wolai-frontend/src/app/api/bridge/request/route.ts`
|
||||
- `wolai-frontend/src/app/api/bridge/trace/route.ts`
|
||||
|
||||
### 仍是 `TS_TRANSPORT_KEEP`
|
||||
|
||||
- `wolai-frontend/src/app/api/search/recent/route.ts`
|
||||
|
||||
说明:
|
||||
|
||||
- `search/recent` 当前只承担“打开页面后写最近访问记录”的 side-effect,不再承担搜索召回
|
||||
- 它可以长期保留在 Web 层,但必须明确不是搜索业务真入口
|
||||
|
||||
### 当前仍缺
|
||||
|
||||
- workspace 级统一观测总览
|
||||
- 分页、按对象过滤、按状态过滤
|
||||
- 完整失败态和冲突态的全链路落账
|
||||
|
||||
---
|
||||
|
||||
## 3.4 AI 与工具面
|
||||
|
||||
### 已进入 `RUST_OWNER`
|
||||
|
||||
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `doc_get`
|
||||
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `doc_find`
|
||||
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `doc_insert_blocks`
|
||||
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `doc_replace_range`
|
||||
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `docs_search`
|
||||
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `docs_read`
|
||||
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `search_web`
|
||||
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `image_read`
|
||||
- `wolai-frontend/src/app/api/ai-agent/run/route.ts` 中的 `slash_run`
|
||||
|
||||
说明:
|
||||
|
||||
- Convex 主链下,`ai-agent/run` 已直接通过 `executeRustBridgeTool(...)` 执行 `docs_search`、`docs_read`、`search_web`、`image_read`、`slash_run`。
|
||||
- `docsServerTools` 当前只保留非 Convex / fallback 模式,不再承担 Convex 主链真执行。
|
||||
- `search_web`、`image_read`、`slash_run` 已切到 Rust Tool runtime,`ai-agent/run` 不再保留 TS 真执行兜底。
|
||||
|
||||
### 仍是 `TS_TRANSPORT_KEEP`
|
||||
|
||||
- `wolai-frontend/src/app/api/ai-agent/client-tool-result/route.ts`
|
||||
- `wolai-frontend/src/lib/ai-agent/tools/builtins/onlyoffice/onlyofficeServerTools.ts` 中的 `asset_extract_outline`
|
||||
- `wolai-frontend/src/lib/ai-agent/tools/builtins/onlyoffice/onlyofficeServerTools.ts` 中的 `asset_to_mindmap`
|
||||
- `wolai-frontend/src/lib/ai-agent/tools/builtins/onlyoffice/onlyofficeServerTools.ts` 中的 `oo_*`
|
||||
- `wolai-frontend/src/lib/ai-agent/tools/builtins/rag/**`
|
||||
|
||||
说明:
|
||||
|
||||
- `client-tool-result` 只负责浏览器内客户端工具回传,不承担业务决策。
|
||||
- `asset_extract_outline` 仍依赖附件下载与 MinerU 解析,因此长期保留 TS transport / 外部服务编排。
|
||||
- `asset_to_mindmap` 当前属于“TS 提纲提取 + Rust 导图写入”的明确编排边界:附件解析仍在 TS/MinerU,但导图应用已改走 Rust `mindmap_apply_ops`。
|
||||
- `oo_*` 继续保持客户端插件边界,不再尝试下沉为第二套服务端真执行入口。
|
||||
|
||||
### 仍是 `TS_COMPAT_PENDING`
|
||||
|
||||
- `wolai-frontend/src/app/api/mindmap-ai/agent/route.ts`
|
||||
- `wolai-frontend/src/app/api/mindmap-ai/assets/route.ts`
|
||||
- `wolai-frontend/src/app/api/mindmap-ai/expand-node/route.ts`
|
||||
- `wolai-frontend/src/app/api/mindmap-ai/outline-to-mindmap/route.ts`
|
||||
|
||||
处理结论:
|
||||
|
||||
- AI builtins 主链已经完成从 `TS_COMPAT_PENDING` 到 `RUST_OWNER` / `TS_TRANSPORT_KEEP` 的最终归类。
|
||||
- `mindmap-ai/**` 若继续保留,必须明确只是上层产品编排,不能成为对象规则真入口。
|
||||
- AI 工具矩阵与第一批旧面删除清单分别见:
|
||||
- `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/ai-tool-cutover-matrix.md`
|
||||
- `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-legacy-delete-list.md`
|
||||
|
||||
---
|
||||
|
||||
## 3.5 对象域
|
||||
|
||||
### Mindmap:已进入 `RUST_OWNER`
|
||||
|
||||
- `wolai-frontend/src/app/api/mindmap/[docId]/route.ts`
|
||||
- `wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts`
|
||||
|
||||
说明:
|
||||
|
||||
- `GET/POST` 之前已在 Rust query/command 主链上。
|
||||
- 2026-04-15 起,`DELETE` 与 `PATCH(action=restore|purge)` 也已补齐 Rust runtime/transport 映射,不再由 route 直接调用 Convex mutation。
|
||||
|
||||
### Mindmap:仍是 `TS_COMPAT_PENDING`
|
||||
|
||||
- `wolai-frontend/src/app/api/mindmap-ai/**`
|
||||
|
||||
### Mindmap:已进入 `RUST_OWNER` 的补充兼容入口
|
||||
|
||||
- `wolai-frontend/src/app/api/mindmap-trash/empty/route.ts`
|
||||
|
||||
说明:
|
||||
|
||||
- `mindmap-trash/empty` 现已切到 `mindmaps.emptyTrashByWorkspace` Rust runtime/transport。
|
||||
- 该 route 仍保留为 Web transport 壳,但不再直接持有 TS 真执行。
|
||||
|
||||
### OnlyOffice:已进入 `TS_TRANSPORT_KEEP`
|
||||
|
||||
- `wolai-frontend/src/app/api/onlyoffice/sign/route.ts`
|
||||
- `wolai-frontend/src/app/api/onlyoffice/proxy/route.ts`
|
||||
- `wolai-frontend/src/app/api/onlyoffice/callback/route.ts`
|
||||
- `wolai-frontend/src/app/api/onlyoffice/forcesave/route.ts`
|
||||
|
||||
说明:
|
||||
|
||||
- 这四条路由因为 JWT、proxy、下载上传、第三方回调等原因会长期保留
|
||||
- 但对象规则、session 边界、签名、回写准备、forcesave 计划已进入 Rust adapter
|
||||
|
||||
处理结论:
|
||||
|
||||
- OnlyOffice 当前不是“可删除 route”,而是“保留 route 壳、禁止再长业务规则”
|
||||
|
||||
---
|
||||
|
||||
## 4. Phase 8 Cutover Gate
|
||||
|
||||
只有下面四组 gate 同时满足,才能宣布“Rust 成为唯一业务执行平面”。
|
||||
|
||||
### Gate A:页面、块、查询主链全部 Rust 持有
|
||||
|
||||
要求:
|
||||
|
||||
- `documents.*` 主链不再存在 TS 业务路由
|
||||
- `blocks.*` 主链不再存在第二套执行逻辑
|
||||
- `search.documents`、`sidebar.dataset.list`、`bridge request/trace/command` 全部走 Rust query/runtime
|
||||
|
||||
### Gate B:对象域只保留 transport 壳
|
||||
|
||||
要求:
|
||||
|
||||
- Mindmap 主读写统一进入 Rust adapter
|
||||
- OnlyOffice 只保留签名、代理、回调、forcesave 等 transport 壳
|
||||
- 任何对象域规则都不再从 route 直连 Convex mutation 发展
|
||||
|
||||
### Gate 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、外部服务编排、客户端桥与冻结兼容壳责任。**
|
||||
@@ -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、外部服务编排、客户端桥与明确冻结的兼容壳。**
|
||||
@@ -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 为准。
|
||||
@@ -0,0 +1,110 @@
|
||||
# [recycle] mnote 单仓维护边界约定
|
||||
|
||||
> 更新时间:2026-04-14
|
||||
>
|
||||
> 主仓:`/mnt/Data1T/mnote`
|
||||
>
|
||||
> 历史仓:`/mnt/Data1T/mnote-rust`
|
||||
|
||||
## 1. 主结论
|
||||
|
||||
- 只保留 `/mnt/Data1T/mnote` 作为启动、规划、保存、验证的唯一主仓。
|
||||
- `/mnt/Data1T/mnote-rust` 只作为历史资产来源,不再承担主执行入口。
|
||||
- 任何新设计、执行清单、架构说明都优先写入 `/mnt/Data1T/mnote/design/` 或 `/mnt/Data1T/mnote/rust/design/`。
|
||||
|
||||
## 2. 主仓执行入口
|
||||
|
||||
- 根目录脚本入口只认 `/mnt/Data1T/mnote/package.json` 与 `/mnt/Data1T/mnote/scripts/`。
|
||||
- 当前 `package.json` 仅通过 `node scripts/*.js` 暴露仓库级入口,没有任何脚本要求先进入 `mnote-rust`。
|
||||
- 如需调用 Rust,统一使用 `cargo --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml ...`,不从历史仓发起 Cargo 命令。
|
||||
- 当前 `scripts/desktop-hot.js` 已固定从主仓根目录解析 `.env.all`,并以 `wolai-frontend/`、`wolai-backend/` 作为唯一联调入口。
|
||||
|
||||
## 3. `mnote-rust` 历史资料分类
|
||||
|
||||
### 3.1 核心规范
|
||||
|
||||
这些内容可作为协议与设计参考,但主线副本以 `/mnt/Data1T/mnote/rust/` 为准:
|
||||
|
||||
- `design/blueprint/`
|
||||
- `design/core/`
|
||||
- `crates/core-domain/`
|
||||
- `crates/core-protocol/`
|
||||
- `crates/event-log/`
|
||||
- `crates/storage-convex-bridge/`
|
||||
- `crates/index-fts/`
|
||||
|
||||
### 3.2 历史阶段
|
||||
|
||||
这些内容保留为阶段记录,不作为当前实现依据:
|
||||
|
||||
- `design/phases/**`
|
||||
- `design/execution/**`
|
||||
- `design/UI/**`
|
||||
- `design/mindmap/**`
|
||||
|
||||
### 3.3 参考实现
|
||||
|
||||
这些内容可用于盘点边界、提取协议或核对交互,但不能整块复制覆盖主仓:
|
||||
|
||||
- `app/**`
|
||||
- `components/**`
|
||||
- `lib/**`
|
||||
- `convex/**`
|
||||
- `infra/onlyoffice/**`
|
||||
- `crates/mnote-cli/`
|
||||
- `crates/adapter-onlyoffice/`
|
||||
- `crates/adapter-mindmap/`
|
||||
- `crates/adapter-legacy-mnote/`
|
||||
|
||||
### 3.4 错误方向
|
||||
|
||||
以下内容明确不进入主仓主线:
|
||||
|
||||
- 对象页壳与试验页:`app/page.tsx`、`app/onlyoffice/page.tsx`、`app/documents/**`
|
||||
- 第二套导航壳:`components/sidebar/sidebar.tsx`
|
||||
- 诊断与构建产物:`.next/diagnostics/**`、`.next/**`、`.tmp/**`
|
||||
- 仅用于历史 smoke/夹具的目录:`.tmp/phase7-test-fixtures/**`
|
||||
|
||||
## 4. 主仓默认不动目录
|
||||
|
||||
除非当前任务明确要求接线或修复,否则默认不扩写以下目录:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/`
|
||||
- `/mnt/Data1T/mnote/wolai-backend/`
|
||||
- `/mnt/Data1T/mnote/infra/convex/`
|
||||
- `/mnt/Data1T/mnote/src/components/onlyoffice/`
|
||||
- `/mnt/Data1T/mnote/recycle/`
|
||||
|
||||
## 5. 最小维护约定
|
||||
|
||||
### 5.1 Workspace 责任面
|
||||
|
||||
- 入口文件:`/mnt/Data1T/mnote/rust/Cargo.toml`、`/mnt/Data1T/mnote/rust/Cargo.lock`
|
||||
- 维护范围:`/mnt/Data1T/mnote/rust/crates/*`
|
||||
- 要求:新增 crate、依赖调整、共享协议变更,必须先在主仓 workspace 内完成,不得回写到历史仓再反向复制。
|
||||
|
||||
### 5.2 Bridge 责任面
|
||||
|
||||
- 入口文件:`/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/bridge.ts`
|
||||
- 相关边界:
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/metadata-command-adapter.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/save-command-adapter.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/**`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/sidebar/route.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/convex/bridgeLogs.ts`
|
||||
- 要求:新 command/query 先收口协议边界,再决定是否继续下沉到 Rust crate。
|
||||
|
||||
### 5.3 接入链责任面
|
||||
|
||||
- C1 页面元信息:`src/app/(app)/documents/[id]/page.tsx`、`src/components/editor/document-content.tsx`
|
||||
- C2 Sidebar 聚合:`src/components/sidebar/**`、`src/hooks/use-convex-sidebar-data.ts`、`src/lib/sidebar-data.ts`
|
||||
- C3 正文保存:`src/components/editor/blocknote-editor.tsx`、`src/app/api/documents/save/route.ts`
|
||||
- Phase D 边界:`src/components/editor/blocks/MindmapBlock.tsx`、`src/app/onlyoffice/**`、`src/app/api/onlyoffice/**`
|
||||
- 要求:优先改主仓现有链路,不复制历史仓页面壳替换现实现。
|
||||
|
||||
## 6. 禁止误复制清单
|
||||
|
||||
- 不把 `mnote-rust` 当成新的主执行仓。
|
||||
- 不复制第二套 Next 前端壳、对象页壳、diagnostics 页、smoke 页进入主线。
|
||||
- 不通过软链接、硬链接、跨仓路径偷接来伪装“已迁移”。
|
||||
- 不在主仓根目录散落多个 Rust 源码根。
|
||||
@@ -0,0 +1,629 @@
|
||||
# [recycle] AI 前端精简方案 v1
|
||||
|
||||
> 更新时间:2026-04-15
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-final-closure-checklist.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/ai-tool-cutover-matrix.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
这份方案只回答一个问题:
|
||||
|
||||
> **当前网页前端中,AI 相关部分应该如何精简,才能真正降低加载负担,并把前端 AI 收口为“轻桥接层”。**
|
||||
|
||||
目标方向已经明确:
|
||||
|
||||
> **前端 AI 面板不再承担工具注册、工具编排、能力路由和执行框架,只保留为 Hermes API Server + mnote Rust 业务能力的轻桥接 UI。**
|
||||
|
||||
这意味着后续 AI 前端不再是一个“小型平台”,而只是:
|
||||
|
||||
- 收集上下文
|
||||
- 发送用户输入
|
||||
- 展示流式结果
|
||||
- 承接极少数浏览器专属 client tool
|
||||
|
||||
## 1.1 现状校准:当前真正已经落地的后端 AI 边界
|
||||
|
||||
在继续谈“前端该怎么精简”之前,必须先对齐一个事实:
|
||||
|
||||
> **当前本机已经有可核验的 Hermes Agent 与其官方 API Server 方案;同时 mnote 自己也已经有 Rust protocol/runtime 边界。后续前端 AI 应该桥接这两层,而不是自己继续承担平台层。**
|
||||
|
||||
当前已确认的 Hermes 事实:
|
||||
|
||||
- 本机 Hermes 安装目录:`/home/lix/.hermes`
|
||||
- Hermes 本体仓:`/home/lix/.hermes/hermes-agent`
|
||||
- 当前网关进程已在运行:`hermes gateway run --replace`
|
||||
- 官方文档已提供 OpenAI 兼容 API Server:
|
||||
- 启用方式:`API_SERVER_ENABLED=true`
|
||||
- 默认监听:`http://127.0.0.1:8642`
|
||||
- 入口:`/v1/chat/completions`、`/v1/responses`、`/health`
|
||||
|
||||
但当前本机状态也要说明白:
|
||||
|
||||
- Hermes gateway 在跑
|
||||
- Hermes API Server 当前还没有启用
|
||||
- mnote 前端当前也还没有接通 Hermes API Server
|
||||
|
||||
因此,现阶段不是“是否存在 Hermes”的问题,而是:
|
||||
|
||||
- **Hermes 已存在,但还没有接入 mnote Web AI 主链**
|
||||
- **mnote Rust 业务能力已存在,但还没有作为 Hermes 的统一业务工具面完全暴露**
|
||||
|
||||
当前 mnote 已能明确核验到的 Rust 业务边界是:
|
||||
|
||||
- `rust/crates/core-protocol/src/tool.rs`
|
||||
已定义 `ToolSpec`、`ToolSetSpec`、`ToolRegistry`,并冻结了 `toolset.readonly`、`toolset.media_read`、`toolset.doc_read`、`toolset.doc_write`、`toolset.mindmap_read`、`toolset.mindmap_write`、`toolset.onlyoffice_service`、`toolset.slash_write` 等基础集合。
|
||||
- `rust/crates/bridge-runtime/src/lib.rs`
|
||||
已提供统一 `RuntimeInput::{Tool, Query, Command}` 入口,支持 `plan`、`result`、`explain-plan`、`validateOnly`、`dryRun` 等运行模式,并输出统一计划结构。
|
||||
- `rust/crates/mnote-cli/README.md`
|
||||
已冻结 `tool run` 的 JSON 契约,说明 CLI 化目标已经开始按稳定协议推进。
|
||||
- `wolai-frontend/src/lib/documents/rust-runtime.ts`
|
||||
当前前端已经可以通过 `executeRustBridgeTool()` 直接调用 Rust `bridge-runtime`,说明 Web 并不是从零开始接 Rust。
|
||||
|
||||
结合 `ai-tool-cutover-matrix.md` 与 `run/route.ts`,当前可以按下面口径理解能力归属:
|
||||
|
||||
- 已有明确 Rust owner 或 Rust 主入口的能力:
|
||||
`search_web`、`image_read`、`slash_run`、`doc_*`、`mindmap_*`、`onlyoffice_* service`
|
||||
- 仍暂时保留在 TS transport 或兼容层的能力:
|
||||
`docs_search`、`docs_read`、`rag_lightrag_query`、`asset_extract_outline`、`asset_to_mindmap`、`oo_*`
|
||||
|
||||
因此,这份方案后续提到的“后移”应理解成:
|
||||
|
||||
- 先把前端收口到 Hermes bridge
|
||||
- 再把 mnote 业务能力通过 Rust 边界继续收口,并作为 Hermes 可调用能力暴露
|
||||
- 最终形成“Hermes 负责 agent 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。**
|
||||
@@ -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/
|
||||
@@ -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 壳”这句统一结论。
|
||||
@@ -0,0 +1,656 @@
|
||||
# [recycle] mnote 单仓收口 Rust 内核回迁计划
|
||||
|
||||
> 更新时间:2026-04-14
|
||||
>
|
||||
> 当前状态:已完成第一批 P0 crate 与核心设计文档的真实复制,复制目标目录为 `/mnt/Data1T/mnote/rust/`。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
基于当前代码现状、`/mnt/Data1T/mnote-rust/design/` 的长期原则,以及你现在明确提出的要求:
|
||||
|
||||
- 不希望长期维护两个仓库
|
||||
- 不希望跨仓链接
|
||||
- 最终只希望在一个文件夹里启动和保存
|
||||
|
||||
当前最合适的路线已经明确并开始落地:
|
||||
|
||||
1. **停止把 `/mnt/Data1T/mnote-rust/` 继续当作未来主产品仓推进**
|
||||
2. **把 `mnote-rust` 的必要 Rust 内核内容真实复制进 `/mnt/Data1T/mnote/`**
|
||||
3. **最终只保留 `/mnt/Data1T/mnote/` 作为唯一主仓**
|
||||
4. **`/mnt/Data1T/mnote/wolai-frontend/` 继续承担主产品前端壳**
|
||||
5. **Rust 内核在 `/mnt/Data1T/mnote/rust/` 下统一收口**
|
||||
|
||||
一句话版:
|
||||
|
||||
> **不是双仓回接,而是单仓收口:把必要 Rust 内核复制进 `mnote`,最终只在 `/mnt/Data1T/mnote` 启动与保存。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 为什么要改成单仓复制,而不是继续双仓
|
||||
|
||||
## 2.1 先做 Rust 内核本身没有错
|
||||
|
||||
`/mnt/Data1T/mnote-rust/design/blueprint/ai-native-note-architecture-blueprint-v0.md` 与 `/mnt/Data1T/mnote-rust/design/phases/phase5/packaging-and-shell-strategy-v0.md` 已明确长期原则:
|
||||
|
||||
1. Convex 仍是主事实层
|
||||
2. Rust 负责统一协议、命令、查询、事件、索引、adapter
|
||||
3. 前端主壳继续复用成熟 React/Next
|
||||
|
||||
也就是说:
|
||||
|
||||
- “先写 Rust 内核”是对的
|
||||
- “不重写一套新前端主栈”也是对的
|
||||
|
||||
真正要调整的是载体:
|
||||
|
||||
- 不再把 `mnote-rust` 作为第二个长期主仓
|
||||
- 不再维持“一个仓写内核,一个仓跑产品前端”的长期分裂状态
|
||||
|
||||
## 2.2 双仓会持续制造启动、保存、认知和迁移成本
|
||||
|
||||
如果继续保留:
|
||||
|
||||
- `/mnt/Data1T/mnote/`
|
||||
- `/mnt/Data1T/mnote-rust/`
|
||||
|
||||
长期并行,会持续出现四类成本:
|
||||
|
||||
1. **启动成本**
|
||||
- 需要判断到底从哪个目录启动
|
||||
- 脚本、环境变量、依赖路径容易双份化
|
||||
|
||||
2. **保存口径成本**
|
||||
- 用户会天然希望“最终只有一个真实主仓”
|
||||
- 双仓天然让“哪个仓是现在的真主线”不断摇摆
|
||||
|
||||
3. **迁移成本**
|
||||
- 每做一项功能,都要先判断该落在 `mnote` 还是 `mnote-rust`
|
||||
- 这会导致团队持续在仓库边界上损耗
|
||||
|
||||
4. **认知成本**
|
||||
- 新协作者很难快速判断:
|
||||
- 哪个仓是产品主线
|
||||
- 哪个仓只是实验壳
|
||||
- 哪些文档是真约束
|
||||
|
||||
## 2.3 当前真正成熟的是 `mnote` 前端,而不是 `mnote-rust` 前端
|
||||
|
||||
当前成熟可直接承载产品面的能力主要在:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/onlyoffice/`
|
||||
|
||||
而 `mnote-rust` 当前前端更多是:
|
||||
|
||||
1. 联调壳
|
||||
2. smoke 壳
|
||||
3. 协议验证壳
|
||||
4. 部分对象页桥接壳
|
||||
|
||||
因此,单仓收口时,正确的继承关系应该是:
|
||||
|
||||
- **保留 `mnote` 前端主壳**
|
||||
- **复制 `mnote-rust` 的内核与协议成果**
|
||||
|
||||
而不是反过来复制一整套 `mnote-rust` 的 Next 前端壳。
|
||||
|
||||
---
|
||||
|
||||
## 3. 新目标架构
|
||||
|
||||
单仓收口后的目标结构应是:
|
||||
|
||||
```text
|
||||
/mnt/Data1T/mnote/
|
||||
wolai-frontend/ # 唯一主前端
|
||||
wolai-backend/ # 现有辅助后端
|
||||
infra/convex/ # 现有 Convex 基础设施
|
||||
src/components/onlyoffice/
|
||||
scripts/ # 仓库级统一启动入口
|
||||
design/ # 全局设计与总览
|
||||
rust/ # 新增:Rust 内核统一收口目录
|
||||
```
|
||||
|
||||
连接关系应变成:
|
||||
|
||||
```text
|
||||
mnote 前端
|
||||
-> mnote 内部 API / BFF / bridge
|
||||
-> rust/ 内核协议层
|
||||
-> Convex 主事实层 + Rust 事件/索引层
|
||||
```
|
||||
|
||||
这意味着:
|
||||
|
||||
1. 前端只认 `/mnt/Data1T/mnote`
|
||||
2. Rust 也只存在于 `/mnt/Data1T/mnote/rust`
|
||||
3. 启动、构建、联调都只从 `/mnt/Data1T/mnote` 发起
|
||||
|
||||
---
|
||||
|
||||
## 4. 单仓目录落位方案
|
||||
|
||||
推荐把 Rust 相关内容整体收进:
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/`
|
||||
|
||||
而不要把 `crates/`、`design/`、`scripts/` 直接摊到仓库根。
|
||||
|
||||
目标结构:
|
||||
|
||||
```text
|
||||
/mnt/Data1T/mnote/
|
||||
wolai-frontend/
|
||||
wolai-backend/
|
||||
infra/convex/
|
||||
src/components/onlyoffice/
|
||||
scripts/
|
||||
design/
|
||||
rust/
|
||||
Cargo.toml
|
||||
Cargo.lock
|
||||
crates/
|
||||
core-domain/
|
||||
core-protocol/
|
||||
event-log/
|
||||
storage-convex-bridge/
|
||||
index-fts/
|
||||
bridge-runtime/ # Phase 1 最小真实执行器
|
||||
mnote-cli/ # Phase 2 已落位,先冻结 --json 协议
|
||||
adapter-onlyoffice/ # 后置按需引入
|
||||
adapter-mindmap/ # 后置按需引入
|
||||
adapter-legacy-mnote/ # 后置按需引入
|
||||
bridge/
|
||||
design/
|
||||
INDEX.md
|
||||
core/
|
||||
blueprint/
|
||||
phases/
|
||||
scripts/
|
||||
fixtures/
|
||||
tests/
|
||||
```
|
||||
|
||||
### 4.1 `rust/` 下各目录职责
|
||||
|
||||
- `rust/crates/`
|
||||
放所有 Rust workspace 成员,保持 Cargo workspace 语义集中。
|
||||
|
||||
- `rust/bridge/`
|
||||
放 Rust 侧桥接层、协议适配、生成代码、FFI/IPC glue。
|
||||
|
||||
- `rust/design/`
|
||||
放 Rust 专属设计、阶段文档、路线图。
|
||||
|
||||
- `rust/scripts/`
|
||||
放 Rust 专属构建、测试、检查、保存脚本。
|
||||
|
||||
- `rust/fixtures/` / `rust/tests/`
|
||||
放 Rust 侧测试资源与测试代码。
|
||||
|
||||
### 4.2 根目录哪些保持不动
|
||||
|
||||
以下现有目录建议明确保持不动:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/`
|
||||
- `/mnt/Data1T/mnote/wolai-backend/`
|
||||
- `/mnt/Data1T/mnote/infra/convex/`
|
||||
- `/mnt/Data1T/mnote/src/components/onlyoffice/`
|
||||
- `/mnt/Data1T/mnote/recycle/`
|
||||
- `/mnt/Data1T/mnote/scripts/`
|
||||
- `/mnt/Data1T/mnote/design/`
|
||||
|
||||
其中:
|
||||
|
||||
- `design/` 继续承担全局总览和跨系统路线说明
|
||||
- Rust 的详细阶段文档放进 `rust/design/`
|
||||
|
||||
---
|
||||
|
||||
## 5. 启动与保存口径必须怎么统一
|
||||
|
||||
## 5.1 启动口径
|
||||
|
||||
统一口径只有一个:
|
||||
|
||||
> 所有开发、构建、联调命令都从 `/mnt/Data1T/mnote` 发起。
|
||||
|
||||
执行方式:
|
||||
|
||||
1. 仓库级脚本统一保留在 `/mnt/Data1T/mnote/scripts/`
|
||||
2. Rust 命令只作为子任务存在,例如:
|
||||
- `cargo --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml ...`
|
||||
3. 不再保留“先进 `mnote-rust` 再进 `mnote`”的双仓启动方式
|
||||
|
||||
## 5.2 保存口径
|
||||
|
||||
统一口径只有一个:
|
||||
|
||||
> 页面、块、文档、任务、索引的最终保存都只认 Convex 主事实层。
|
||||
|
||||
这意味着:
|
||||
|
||||
1. 人工操作、CLI、Agent 的写入都应先进入 Rust 统一命令层,再落到 Convex
|
||||
2. Rust 的 `event-log`、`index-fts`、fixtures、本地缓存都只是派生层
|
||||
3. 文件系统只保存:
|
||||
- 静态资源
|
||||
- 附件
|
||||
- 导出物
|
||||
- 缓存
|
||||
- 测试数据
|
||||
4. 文件系统不承载主业务真相
|
||||
|
||||
---
|
||||
|
||||
## 5.3 当前已落地的首批复制结果
|
||||
|
||||
截至 2026-04-13,以下内容已经真实复制进 `/mnt/Data1T/mnote/rust/`:
|
||||
|
||||
### 已复制的 workspace 根文件
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/Cargo.toml`
|
||||
- `/mnt/Data1T/mnote/rust/Cargo.lock`
|
||||
|
||||
### 已复制的 P0 crate
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/crates/core-domain/`
|
||||
- `/mnt/Data1T/mnote/rust/crates/core-protocol/`
|
||||
- `/mnt/Data1T/mnote/rust/crates/event-log/`
|
||||
- `/mnt/Data1T/mnote/rust/crates/storage-convex-bridge/`
|
||||
- `/mnt/Data1T/mnote/rust/crates/index-fts/`
|
||||
|
||||
### 已复制的核心设计文档
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/design/INDEX.md`
|
||||
- `/mnt/Data1T/mnote/rust/design/core/01-domain-model-v0.md`
|
||||
- `/mnt/Data1T/mnote/rust/design/core/02-command-query-tool-protocol-v0.md`
|
||||
- `/mnt/Data1T/mnote/rust/design/core/03-storage-event-indexing-v0.md`
|
||||
- `/mnt/Data1T/mnote/rust/design/core/04-onlyoffice-integration-boundary-v0.md`
|
||||
|
||||
这一状态意味着:
|
||||
|
||||
1. `mnote` 主仓里已经存在可继续扩展的 Rust workspace 根
|
||||
2. P0 内核定义已经不再只存在于 `/mnt/Data1T/mnote-rust/`
|
||||
3. 后续回迁工作可以直接以 `/mnt/Data1T/mnote/rust/` 为唯一落位继续推进
|
||||
|
||||
---
|
||||
|
||||
## 6. 第一阶段应该真实复制进 `mnote` 的内容
|
||||
|
||||
子 agent 的结论一致:第一阶段应复制“定义真相和协议”的内核,而不是复制第二套产品前端。
|
||||
|
||||
## 6.1 必须复制的 crate
|
||||
|
||||
### P0:第一阶段必须复制
|
||||
|
||||
以下 crate 应真实复制到:
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/crates/`
|
||||
|
||||
#### `core-domain`
|
||||
|
||||
来源:
|
||||
|
||||
- `/mnt/Data1T/mnote-rust/crates/core-domain`
|
||||
|
||||
复制后职责:
|
||||
|
||||
1. 领域模型
|
||||
2. 一等对象定义
|
||||
3. ID / revision / 时间 /审计基础类型
|
||||
|
||||
#### `core-protocol`
|
||||
|
||||
来源:
|
||||
|
||||
- `/mnt/Data1T/mnote-rust/crates/core-protocol`
|
||||
|
||||
复制后职责:
|
||||
|
||||
1. `Command / Query / Tool` 统一协议壳
|
||||
2. actor/source/target/meta 结构
|
||||
3. `CreatePage / InsertBlock / SearchPages` 等协议模型
|
||||
|
||||
#### `event-log`
|
||||
|
||||
来源:
|
||||
|
||||
- `/mnt/Data1T/mnote-rust/crates/event-log`
|
||||
|
||||
复制后职责:
|
||||
|
||||
1. 命令日志
|
||||
2. 领域事件
|
||||
3. 写入后的事件生成规则
|
||||
|
||||
#### `storage-convex-bridge`
|
||||
|
||||
来源:
|
||||
|
||||
- `/mnt/Data1T/mnote-rust/crates/storage-convex-bridge`
|
||||
|
||||
复制后职责:
|
||||
|
||||
1. 协议到 Convex 的桥接
|
||||
2. 命令执行与回写
|
||||
3. 命令日志和事件落账
|
||||
|
||||
#### `index-fts`
|
||||
|
||||
来源:
|
||||
|
||||
- `/mnt/Data1T/mnote-rust/crates/index-fts`
|
||||
|
||||
复制后职责:
|
||||
|
||||
1. 可重建的索引与搜索层
|
||||
2. 事件投影
|
||||
3. page/block 搜索
|
||||
|
||||
### P1:建议第二阶段复制
|
||||
|
||||
#### `mnote-cli`
|
||||
|
||||
来源:
|
||||
|
||||
- `/mnt/Data1T/mnote-rust/crates/mnote-cli`
|
||||
|
||||
复制后职责:
|
||||
|
||||
1. 批处理入口
|
||||
2. 导入导出入口
|
||||
3. 内核 smoke 与排障入口
|
||||
|
||||
### P2:按需后置复制
|
||||
|
||||
#### `adapter-onlyoffice`
|
||||
|
||||
来源:
|
||||
|
||||
- `/mnt/Data1T/mnote-rust/crates/adapter-onlyoffice`
|
||||
|
||||
复制后职责:
|
||||
|
||||
1. OnlyOffice 资产定位
|
||||
2. 会话定位
|
||||
3. 任务边界适配
|
||||
|
||||
#### `adapter-mindmap`
|
||||
|
||||
来源:
|
||||
|
||||
- `/mnt/Data1T/mnote-rust/crates/adapter-mindmap`
|
||||
|
||||
复制后职责:
|
||||
|
||||
1. 思维导图结构化 ops
|
||||
2. 节点读取与引用边界
|
||||
|
||||
#### `adapter-legacy-mnote`
|
||||
|
||||
来源:
|
||||
|
||||
- `/mnt/Data1T/mnote-rust/crates/adapter-legacy-mnote`
|
||||
|
||||
复制后职责:
|
||||
|
||||
1. 旧逻辑兼容
|
||||
2. 迁移期桥接
|
||||
|
||||
## 6.2 必须复制的设计资产
|
||||
|
||||
建议同步复制到:
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/design/`
|
||||
|
||||
### 必复制
|
||||
|
||||
- `/mnt/Data1T/mnote-rust/design/INDEX.md`
|
||||
- `/mnt/Data1T/mnote-rust/design/core/01-domain-model-v0.md`
|
||||
- `/mnt/Data1T/mnote-rust/design/core/02-command-query-tool-protocol-v0.md`
|
||||
- `/mnt/Data1T/mnote-rust/design/core/03-storage-event-indexing-v0.md`
|
||||
- `/mnt/Data1T/mnote-rust/design/core/04-onlyoffice-integration-boundary-v0.md`
|
||||
|
||||
原因:
|
||||
|
||||
1. 这些是长期稳定规范
|
||||
2. 直接对应 `core-domain / core-protocol / event-log / storage-convex-bridge / index-fts / adapter-onlyoffice`
|
||||
|
||||
### 暂不建议第一阶段复制的设计资产
|
||||
|
||||
以下内容先留在 `mnote-rust` 作为参考,不作为第一阶段主迁移目标:
|
||||
|
||||
- `design/blueprint/**`
|
||||
- `design/phases/**`
|
||||
- `design/execution/**`
|
||||
- `design/UI/**`
|
||||
|
||||
原因:
|
||||
|
||||
1. 这些更多是阶段叙事与历史推进资料
|
||||
2. 不是必须进入主仓的长期规范
|
||||
|
||||
---
|
||||
|
||||
## 7. 第一阶段明确不应复制的内容
|
||||
|
||||
这里要明确“禁止误复制”的范围。
|
||||
|
||||
## 7.1 不应继续作为第一阶段复制目标的前端内容
|
||||
|
||||
### 不复制为主线
|
||||
|
||||
- `/mnt/Data1T/mnote-rust/app/documents/[id]/mindmap/page.tsx`
|
||||
- `/mnt/Data1T/mnote-rust/app/documents/[id]/office/page.tsx`
|
||||
- `/mnt/Data1T/mnote-rust/components/sidebar/sidebar.tsx`
|
||||
- `/mnt/Data1T/mnote-rust/app/page.tsx`
|
||||
- `/mnt/Data1T/mnote-rust/app/onlyoffice/page.tsx`
|
||||
|
||||
原因:
|
||||
|
||||
1. 它们大多属于对象页壳、诊断页、smoke 页或单文件硬拼壳
|
||||
2. 不应再形成第二套产品前端主线
|
||||
|
||||
## 7.2 只能保留为参考 / smoke / 实验的内容
|
||||
|
||||
以下内容可以保留在 `mnote-rust` 参考,但不作为第一阶段复制目标:
|
||||
|
||||
1. `app/documents/[id]/mindmap/page.tsx` 里的 diagnostics 区
|
||||
2. `app/documents/[id]/office/page.tsx` 里的 diagnostics 区
|
||||
3. `app/onlyoffice/OnlyOfficeClientPage.tsx` 里的环境兼容 hack
|
||||
4. `app/page.tsx` 的 smoke 首页文案
|
||||
5. `phase7` 中已经标注为历史错误方向的文档
|
||||
|
||||
## 7.3 可复制的少量薄桥接代码
|
||||
|
||||
以下内容可以作为后续参考性复制对象,但仍不是第一阶段主目标:
|
||||
|
||||
- `components/editor/document-shell.tsx`
|
||||
- `components/editor/document-content.tsx`
|
||||
- `components/editor/blocknote-editor.tsx`
|
||||
- `hooks/use-convex-sidebar-data.ts`
|
||||
- `lib/sidebar-tree.ts`
|
||||
- `lib/file-tree/rows.ts`
|
||||
- `lib/document-embeds.ts`
|
||||
- `app/layout.tsx`
|
||||
|
||||
原因:
|
||||
|
||||
1. 这些属于宿主桥接、投影层或 provider 接线
|
||||
2. 不是完整产品前端壳
|
||||
|
||||
---
|
||||
|
||||
## 8. 单仓路线下,`mnote` 里最先该接入的 5 条能力链
|
||||
|
||||
这 5 条仍然是最值得优先打通的主线。
|
||||
|
||||
## 8.1 文档正文命令链
|
||||
|
||||
接入目标:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/save/route.ts`
|
||||
|
||||
目标:
|
||||
|
||||
1. 保留现有成熟 BlockNote 前端
|
||||
2. 把正文保存逐步切到 Rust command 层
|
||||
3. 让 block 操作、正文快照、stats 逐步进入统一协议
|
||||
|
||||
## 8.2 页面元信息与页面属性链
|
||||
|
||||
接入目标:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
|
||||
|
||||
目标:
|
||||
|
||||
1. 标题更新
|
||||
2. 页面选项
|
||||
3. 文档统计
|
||||
4. 页面属性变更
|
||||
|
||||
## 8.3 Sidebar 数据聚合链
|
||||
|
||||
接入目标:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/private-tree.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/file-tree.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/hooks/use-convex-sidebar-data.ts`
|
||||
|
||||
目标:
|
||||
|
||||
1. 保留现有 UI
|
||||
2. 逐步让数据聚合层走 Rust query / index 层
|
||||
|
||||
## 8.4 Mindmap 数据链
|
||||
|
||||
接入目标:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/mindmap/mindmapLocalStore.ts`
|
||||
|
||||
目标:
|
||||
|
||||
1. 保留现有成熟思维导图前端
|
||||
2. 逐步把数据读写、ops 和边界纳入 Rust adapter / protocol
|
||||
|
||||
## 8.5 OnlyOffice 边界链
|
||||
|
||||
接入目标:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/onlyoffice/`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/onlyoffice/`
|
||||
- `/mnt/Data1T/mnote/src/components/onlyoffice/`
|
||||
|
||||
目标:
|
||||
|
||||
1. 保留现有 OnlyOffice 页面和静态资源
|
||||
2. 把对象解析、回调、签名和桥接逐步回到 Rust adapter 统一边界
|
||||
|
||||
当前补记:
|
||||
|
||||
1. `OnlyOffice callback` 已从 route 里直接调用 `api.mediaAssets.replaceStorageFromUpload`,推进到最小 `media.assets.replace_storage` bridge 命令边界。
|
||||
2. 当前主仓实现会先在 `wolai-frontend/src/app/api/onlyoffice/callback/route.ts` 下载 ONLYOFFICE 输出文件、上传到 Convex Files 获得 `storageId`,再通过 `buildDocumentBridgeContextWithActor`、`buildDocumentCommandEnvelope` 与 `executeMediaAssetWritebackBridgeCommand` 构造和执行与 `storage-convex-bridge` 对齐的运行时 request。
|
||||
3. Rust 侧已补 `core-protocol::ReplaceMediaAssetStorage` 与 `storage-convex-bridge` 的 `media.assets.replace_storage -> mediaAssets:replaceStorageFromUpload` 映射;因此 OnlyOffice 附件写回现已进入与页面元信息、正文保存一致的协议接缝,只是最终执行器仍在 TypeScript 侧落到 Convex mutation。
|
||||
|
||||
---
|
||||
|
||||
## 9. 推荐实施顺序
|
||||
|
||||
## Phase A:先把 Rust 内核真实复制进 `mnote`
|
||||
|
||||
第一阶段执行动作:
|
||||
|
||||
1. 在 `/mnt/Data1T/mnote/` 下建立 `/rust/`
|
||||
2. 复制 `P0` crate
|
||||
3. 复制 `design/core` 四份长期文档和 `design/INDEX.md`
|
||||
4. 建立新的 `rust/Cargo.toml` 与 `rust/Cargo.lock`
|
||||
5. 不动 `mnote` 主前端壳
|
||||
6. 不复制 `mnote-rust` 的第二套产品前端壳
|
||||
|
||||
当前状态:
|
||||
|
||||
- 以上动作已经完成
|
||||
- `bridge/` 已初始化最小说明目录,但 `scripts/`、`fixtures/`、`tests/` 仍未初始化
|
||||
- `mnote-cli` 已并入当前 workspace,`adapter-*` 仍未并入
|
||||
|
||||
## Phase B:在 `mnote` 中建立 Rust bridge 接口
|
||||
|
||||
执行动作:
|
||||
|
||||
1. 在 `mnote` 现有 API/BFF 层加 Rust command/query 入口
|
||||
2. 保留当前 UI,不先大改组件
|
||||
3. 先打通低风险调用链
|
||||
|
||||
当前建议的第一批 bridge 入口落位:
|
||||
|
||||
1. `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/content/route.ts`
|
||||
- 作为优先读入口,后续先接 `storage-convex-bridge` 的 `read_path`
|
||||
2. `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/save/route.ts`
|
||||
- 作为优先写入口,后续先接 `write_path`
|
||||
3. `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/title/route.ts`
|
||||
- 作为页面标题更新的低风险写入口
|
||||
4. `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/stats/route.ts`
|
||||
- 作为页面统计的低风险入口
|
||||
5. `/mnt/Data1T/mnote/wolai-frontend/src/app/api/sidebar/route.ts`
|
||||
- 作为 Sidebar 聚合读入口,后续切 Rust query / index
|
||||
6. `/mnt/Data1T/mnote/wolai-frontend/src/app/api/blocks/patch/route.ts`
|
||||
- 作为块级局部写入口,适合正文链路第二步接入
|
||||
|
||||
当前判断:
|
||||
|
||||
1. `storage-convex-bridge` crate 内已经具备 `context / validation / read_path / write_path / mapping` 的桥接骨架
|
||||
2. `mnote` 主仓已为 `documents.title.update`、`documents.stats.update`、`documents.options.update`、`documents.save` 补上与 `storage-convex-bridge::build_write_request` 对齐的最小运行时接缝,会先生成 `functionName/payloadJson/args` 形式的 bridge mutation request,再交给统一执行器落到 Convex
|
||||
3. 但主仓 API route 仍未直接调用 Rust crate,`documents.content`、`blocks.patch` 等链路也还没有切到同一层执行器,因此 Phase B 还不能视为完全关闭
|
||||
|
||||
## Phase C:先接低风险元信息,再接正文链
|
||||
|
||||
顺序建议:
|
||||
|
||||
1. 页面标题 / 页面选项
|
||||
2. Sidebar 数据聚合
|
||||
3. BlockNote 正文保存
|
||||
|
||||
## Phase D:再接 Mindmap 与 OnlyOffice
|
||||
|
||||
原则:
|
||||
|
||||
1. 前端仍然在 `mnote`
|
||||
2. 数据、ops、边界逐步切到 Rust adapter / protocol
|
||||
|
||||
## Phase E:让 `mnote-rust` 退出主产品角色
|
||||
|
||||
处理方式:
|
||||
|
||||
1. `mnote-rust` 可保留为历史参考或过渡仓
|
||||
2. 但不再承担短期产品主线
|
||||
3. 最终只保留 `/mnt/Data1T/mnote/` 作为可启动、可保存主仓
|
||||
4. 历史资料分类与主仓维护边界统一以 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-single-repo-maintenance-boundary.md` 为准
|
||||
|
||||
---
|
||||
|
||||
## 10. 最不该做的事
|
||||
|
||||
1. 不要继续把 `mnote-rust` 的 Next 前端补成完整产品前端
|
||||
2. 不要让 `mnote` 和 `mnote-rust` 长期各维护一套产品前端
|
||||
3. 不要跨仓链接代码来“假装已迁移”
|
||||
4. 不要把对象页壳、诊断页、smoke 页当作主产品实现复制进来
|
||||
5. 不要把 Rust 内容散落复制到 `mnote` 根目录多个平行源码根
|
||||
|
||||
---
|
||||
|
||||
## 11. 一句话路线判断
|
||||
|
||||
最优路线不是:
|
||||
|
||||
> 继续在 `mnote-rust` 里补前端,再想办法替换 `mnote`
|
||||
|
||||
而是:
|
||||
|
||||
> 把 `mnote-rust` 的必要 Rust 内核真实复制进 `/mnt/Data1T/mnote/rust/`,保留 `mnote` 现有成熟前端作为唯一主产品壳,最终只在 `/mnt/Data1T/mnote` 这个单仓里启动和保存。
|
||||
@@ -0,0 +1,388 @@
|
||||
# [recycle] mnote Rust 内核替换剩余目标清单
|
||||
|
||||
> 更新时间:2026-04-15
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-plan.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-phase-checklist.md`
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
|
||||
## 1. 本文目的
|
||||
|
||||
本文不再回答“Rust 内容是否已经回迁到主仓”,而是直接回答:
|
||||
|
||||
> **距离“用 Rust 内核替换原有内核,并让绝大部分功能 CLI 化、可供 AI 自主编辑”这一最终目标,我们现在还差什么。**
|
||||
|
||||
当前结论很明确:
|
||||
|
||||
- **单仓收口已经基本完成**
|
||||
- **Rust workspace 与 P0 crate 已经落位**
|
||||
- **部分前端 API 已经接入 Rust 风格协议与 bridge 接缝**
|
||||
- **但“Rust 真正成为唯一执行内核”这件事还没有完成**
|
||||
|
||||
也就是说,当前更接近:
|
||||
|
||||
> **“前端/Node 先学会说 Rust 协议”**
|
||||
|
||||
而不是:
|
||||
|
||||
> **“产品已经由 Rust 内核统一执行”**
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前已经完成的基础
|
||||
|
||||
截至目前,已经完成的只是替换前的基础设施准备:
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/` 已成为主仓内唯一 Rust workspace 根。
|
||||
- `core-domain`、`core-protocol`、`event-log`、`storage-convex-bridge`、`index-fts` 已落位。
|
||||
- `rust/design/core/01~04` 核心设计文档已落位。
|
||||
- 文档内容、标题、统计、Sidebar 等链路,已经开始使用统一 envelope、`request_id`、`trace_id`、`idempotency_key` 等 bridge 元信息。
|
||||
- `command_logs`、`domain_events` 已有首批落账能力。
|
||||
|
||||
这些工作解决的是:
|
||||
|
||||
- 单仓问题
|
||||
- 协议问题
|
||||
- 目录问题
|
||||
- 第一批接缝问题
|
||||
|
||||
**它们还没有解决“原内核是否已经被 Rust 取代”的问题。**
|
||||
|
||||
---
|
||||
|
||||
## 3. 还差的核心目标
|
||||
|
||||
## 3.1 还没有形成“Rust 唯一执行内核”
|
||||
|
||||
这是当前最大的缺口。
|
||||
|
||||
虽然已有一部分 route 在构造 Rust 风格 request,但真实执行主链仍然大量停留在 `Next.js route + TypeScript + Convex client`。
|
||||
|
||||
当前仍明显属于旧执行面的代表链路包括:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/create/route.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/delete/route.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/move/route.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/restore/route.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/duplicate/route.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/search/documents/route.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/mindmap/[docId]/route.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/onlyoffice/*.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/media/*.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/tables/*.ts`
|
||||
|
||||
这说明目前还没有做到:
|
||||
|
||||
- 所有核心读写先进入 Rust `Command / Query / Tool` 层
|
||||
- 所有执行规则由 Rust 决定
|
||||
- Node/Next 只做 transport、auth、session、streaming、UI 适配
|
||||
|
||||
最终目标需要变成:
|
||||
|
||||
- Web route 只负责接请求、鉴权、转发、回流结果
|
||||
- Rust 负责真正的命令执行、查询聚合、约束校验、冲突处理、日志和事件生成
|
||||
|
||||
验收标准:
|
||||
|
||||
- 文档、块、Sidebar、搜索、页面树、Mindmap、OnlyOffice、媒体、表格等主链路都有 Rust 执行入口
|
||||
- 前端 route 不再手写业务规则和数据聚合
|
||||
- TypeScript 侧不再直接成为业务真内核
|
||||
|
||||
## 3.2 还没有完成“全域能力模型”的 Rust 化
|
||||
|
||||
当前 Rust workspace 只有 P0 通用内核 crate,缺的不是“再多几个通用 crate”,而是产品能力域本身还没被吸进 Rust。
|
||||
|
||||
还没有真正完成 Rust 化的能力域至少包括:
|
||||
|
||||
- 页面树与层级操作
|
||||
- 页面创建、移动、复制、删除、恢复、清空回收站
|
||||
- BlockNote block 级操作全量协议
|
||||
- 搜索与召回
|
||||
- 引用、反链、嵌入
|
||||
- 媒体与附件
|
||||
- 在线表格
|
||||
- Mindmap
|
||||
- OnlyOffice 会话、签名、回调、强制保存
|
||||
- AI 调用的写入工具面
|
||||
|
||||
当前 `core-domain` 和 `core-protocol` 更像底层骨架,但还没长成完整产品内核。
|
||||
|
||||
验收标准:
|
||||
|
||||
- 每个产品域都有明确 Rust domain model、command、query、tool contract
|
||||
- 前端不再自行定义第二套 payload 形状
|
||||
- “页面系统”和“对象系统”不再由不同 TS route 各自发明规则
|
||||
|
||||
## 3.3 CLI 入口还没有建立起来
|
||||
|
||||
你的目标里有一条是关键约束:
|
||||
|
||||
> **绝大部分功能 CLI 化**
|
||||
|
||||
这件事当前还远未完成。
|
||||
|
||||
直接证据是:
|
||||
|
||||
- `mnote-cli` 已并入主仓 workspace,但当前还只是最小命令面与 `--json` 计划输出协议
|
||||
- `/mnt/Data1T/mnote/rust/scripts/` 还未初始化
|
||||
- `/mnt/Data1T/mnote/rust/bridge/` 已初始化最小说明目录,且 `crates/bridge-runtime` 已提供 Phase 1 最小真实执行样板
|
||||
- `/mnt/Data1T/mnote/rust/tests/`、`/mnt/Data1T/mnote/rust/fixtures/` 还未形成 CLI 驱动的验收体系
|
||||
|
||||
这意味着目前仍然缺少:
|
||||
|
||||
- 真正可执行而不止输出计划的统一 CLI 二进制入口
|
||||
- 可脚本化的命令集
|
||||
- 稳定的 stdout/stderr/json 输出协议
|
||||
- 面向 AI 的非交互调用模式
|
||||
- dry-run / plan / apply / rollback 风格能力
|
||||
|
||||
最终至少应该具备的 CLI 面包括:
|
||||
|
||||
- `page create/get/update/move/delete/restore/list`
|
||||
- `block insert/replace/move/delete/get`
|
||||
- `search query`
|
||||
- `sidebar dataset`
|
||||
- `mindmap get/put/op`
|
||||
- `onlyoffice sign/callback/forcesave/session`
|
||||
- `media upload/list/get/delete`
|
||||
- `table create/get/update`
|
||||
- `tool run <tool-name> --json`
|
||||
|
||||
验收标准:
|
||||
|
||||
- 核心功能可以不经过浏览器完成
|
||||
- 核心功能可以稳定返回 JSON
|
||||
- shell、脚本、AI agent 都能直接调用
|
||||
- CLI 和 Web 不再各维护一套业务实现
|
||||
|
||||
## 3.4 AI 还没有真正建立在 Rust 工具面之上
|
||||
|
||||
当前已经有 AI 入口:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/ai-agent/run/route.ts`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/DocumentAiAgentPanel.tsx`
|
||||
|
||||
但这套 AI 执行仍主要建立在前端/Node 工具注册表与页面侧 bridge 上,不是建立在 Rust 原生 tool protocol 上。
|
||||
|
||||
这会带来四个问题:
|
||||
|
||||
- AI 能调用的工具面和 Web 内部实现强耦合
|
||||
- AI 与 CLI 不是同一执行平面
|
||||
- AI 写入行为缺少统一事务语义
|
||||
- AI 很难获得稳定、可审计、可回放的编辑能力
|
||||
|
||||
为了实现“AI 可自行编辑”,至少还差以下目标:
|
||||
|
||||
- Rust 提供稳定的 `Tool` 执行协议,而不是只提供 `Command/Query` 壳
|
||||
- AI 调用和 CLI 调用共享同一工具注册面
|
||||
- 每个写入工具都支持明确的目标对象、权限校验、冲突返回、审计日志
|
||||
- 支持 `validate_only`、`dry_run`、`explain_plan` 之类的安全模式
|
||||
- 支持机器可消费的错误码,而不是前端文案式错误
|
||||
|
||||
最终目标不是“AI 像用户点按钮一样绕进前端”,而是:
|
||||
|
||||
> **AI 直接调用 Rust 工具内核完成读写,Web 只是展示层。**
|
||||
|
||||
验收标准:
|
||||
|
||||
- AI agent 使用的写入工具与 CLI 使用的工具完全同源
|
||||
- AI 的每次编辑都能追踪到 command、event、trace、目标对象和 actor
|
||||
- AI 可稳定执行页面编辑、块编辑、检索、结构化改写、批处理操作
|
||||
|
||||
## 3.5 观测、审计、幂等和失败恢复还只完成了首批链路
|
||||
|
||||
现在已有 `command_logs`、`domain_events`,但仍是首批写链路覆盖,不是全域治理。
|
||||
|
||||
仍缺的能力包括:
|
||||
|
||||
- 所有命令统一落账
|
||||
- 所有失败态统一编码
|
||||
- 统一重试与幂等语义
|
||||
- 统一冲突模型
|
||||
- 统一补偿与回放
|
||||
- 统一事件重建和索引重放
|
||||
|
||||
如果没有这一层,CLI 和 AI 即使能写,也不够稳定。
|
||||
|
||||
验收标准:
|
||||
|
||||
- 任一写操作都能查 request、trace、command、domain event
|
||||
- 幂等 key 在 CLI、AI、Web 三侧语义一致
|
||||
- 冲突、拒绝、权限不足、对象不存在等错误有统一 code
|
||||
- 可从事件或命令日志重建关键派生层
|
||||
|
||||
2026-04-15 进展补记:
|
||||
|
||||
- `rust/crates/core-protocol` 与 `bridge-runtime` 已新增 `bridge_request_get`、`bridge_trace_get`、`bridge_command_get`、`event_replay`、`index_rebuild` 这组统一观测/恢复能力;当前 `/api/bridge/request` 与 `/api/bridge/trace` 也已切到 Rust query plan + TS transport 的同一路径,不再各自直连 Convex 查询。
|
||||
- `src/lib/documents/bridge-log.ts` 已把命令日志与领域事件的状态语义显式化,支持 `pending/succeeded/failed/rolled_back` 与 `pending/committed/rejected/failed` 两套状态模型,为后续冲突、失败和补偿写回提供稳定落点。
|
||||
|
||||
当前仍未关闭的缺口:
|
||||
|
||||
- 失败态、冲突态虽然已经在页面主写链与 AI/Mindmap 写链补上第一批落账,但仍未覆盖所有对象域写链。
|
||||
- `event_replay` / `index_rebuild` 目前已成为正式命令面,但还主要停留在 runtime/工具层,尚未形成完整的持久化游标、任务调度和断点续跑体系。
|
||||
- workspace 级总览、分页、按对象范围筛选等观测 UI 仍未完善。
|
||||
|
||||
## 3.6 搜索、索引和派生视图仍在持续切换中
|
||||
|
||||
2026-04-15 进展补记:
|
||||
|
||||
- `search.documents` 已不再把核心 ranking / snippet / filter 留在 TS route。当前 `/mnt/Data1T/mnote/wolai-frontend/src/app/api/search/documents/route.ts` 只保留参数校验、原始数据装载与 HTTP 回传;标题/正文/思维导图/表格/附件的匹配合并、高亮 snippet 与 OCR 待补队列决策已进入 `rust/crates/index-fts/src/lib.rs`。
|
||||
- `search.recent` 已补齐独立 Rust query 名称,并通过 runtime 返回最近访问结果;当前保留 `/api/search/recent` 作为“打开页面后写最近访问记录”的 side-effect 接口,不与搜索召回混在一条写链里。
|
||||
- `sidebar.dataset.list` 已从“只在 route 上挂一个 Rust queryName”推进为真实 runtime query transport,`/api/sidebar` 现会先经过 Rust runtime,再调用 `sidebar:datasetList`。
|
||||
|
||||
当前仍未完全关闭的缺口:
|
||||
|
||||
- 索引重建、校验与回放命令还未落到产品主路径。
|
||||
- `src/app/(app)/layout.tsx` 的 SSR 侧边栏初始数据仍复用现有 `loadSidebarDataFromConvex` helper,没有一并切到 runtime transport。
|
||||
|
||||
更新后的验收标准:
|
||||
|
||||
- `search_web`、`image_read`、`slash_run` 这类 AI 核心工具必须保持 `RUST_OWNER`,不能回退到 TS 真执行兜底。
|
||||
- `builtins/**` 中已被 Rust 替代的服务端真入口必须进入第一批 `TS_LEGACY_DELETE`,清单以 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-legacy-delete-list.md` 为准。
|
||||
|
||||
- 搜索结果由 Rust query / index 层给出。
|
||||
- TS 前端只做 UI 投影,不做核心排序与召回逻辑。
|
||||
- 索引可重建、可校验、可回放。
|
||||
|
||||
## 3.7 Mindmap 和 OnlyOffice 还没有进入真正的 Rust adapter 执行层
|
||||
|
||||
现在对这两个对象域,文档上已经明确了边界,但执行层仍主要在现有 TS/Convex 逻辑。
|
||||
|
||||
现状更接近:
|
||||
|
||||
- **边界想清楚了**
|
||||
- **前端形态保住了**
|
||||
- **但 Rust adapter 还没真正接管**
|
||||
|
||||
缺口主要包括:
|
||||
|
||||
- `adapter-mindmap` 尚未并入主仓
|
||||
- `adapter-onlyoffice` 尚未并入主仓
|
||||
- Mindmap 节点操作还没有稳定的 Rust ops 协议
|
||||
- OnlyOffice 的 sign / proxy / callback / forcesave 还没统一进入 Rust 对象适配层
|
||||
|
||||
验收标准:
|
||||
|
||||
- Mindmap 的结构操作可由 CLI 与 AI 直接调用
|
||||
- OnlyOffice 的对象能力具备统一 session/asset 边界
|
||||
- 不需要依赖前端 route 才能操作这些对象
|
||||
|
||||
## 3.8 仍缺一条“从旧内核切换到新内核”的明确割接路线
|
||||
|
||||
目前已有 backport plan,也有 phase checklist,但还缺一个更硬的最终割接视角:
|
||||
|
||||
- 哪些旧 TS route 会被逐步下线
|
||||
- 哪些功能先双写、再单写
|
||||
- 哪些功能允许长期保留在前端侧
|
||||
- 哪些功能必须强制进入 Rust
|
||||
- 何时可以宣布“原内核不再是主执行面”
|
||||
|
||||
如果没有这一条,项目会长期停留在“看起来在迁移,实际上双内核并存”的状态。
|
||||
|
||||
验收标准:
|
||||
|
||||
- 列出旧执行面的退役清单
|
||||
- 每个能力域有 cutover milestone
|
||||
- 明确宣布 Rust 成为唯一业务执行平面时的准入条件
|
||||
|
||||
2026-04-15 进展补记:
|
||||
|
||||
- 本轮已新增 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md`,首次把页面系统、块系统、查询聚合、AI、Mindmap、OnlyOffice、兼容接口统一盘点为 `RUST_OWNER / TS_TRANSPORT_KEEP / TS_COMPAT_PENDING / TS_LEGACY_DELETE` 四类状态。
|
||||
- 当前“不明确”的问题已经收口为“执行尚未完成”的问题:退役清单、阶段 gate 与最终宣布口径都已写清,但旧接口的物理删除和 AI 工具面的完全统一还未完成。
|
||||
|
||||
---
|
||||
|
||||
## 4. 对最终目标的重新拆解
|
||||
|
||||
如果目标是:
|
||||
|
||||
> **Rust 内核替换原有内核,绝大部分功能 CLI 化,AI 可以自行编辑**
|
||||
|
||||
那么最终至少要同时满足下面四件事。
|
||||
|
||||
## 4.1 Rust 是唯一业务执行平面
|
||||
|
||||
要求:
|
||||
|
||||
- Web、CLI、AI 都调用同一 Rust command/query/tool 内核
|
||||
- 前端不再是业务规则主载体
|
||||
|
||||
## 4.2 CLI 是一等公民,不是调试附属品
|
||||
|
||||
要求:
|
||||
|
||||
- 大部分核心能力都能通过 CLI 完成
|
||||
- 输出稳定 JSON
|
||||
- 支持脚本化、批处理和非交互执行
|
||||
|
||||
## 4.3 AI 只是 CLI/Tool 的智能调度者
|
||||
|
||||
要求:
|
||||
|
||||
- AI 不再依赖页面私有 bridge 才能编辑
|
||||
- AI 调用的每一步都可审计、可回放、可限权
|
||||
|
||||
## 4.4 Convex 继续是事实层,但不再直接暴露产品规则
|
||||
|
||||
要求:
|
||||
|
||||
- Convex 主要承担持久化与事实保存
|
||||
- 规则、协议、工具、索引、对象适配统一收进 Rust
|
||||
|
||||
---
|
||||
|
||||
## 5. 建议按优先级补齐的剩余里程碑
|
||||
|
||||
按最终目标倒推,接下来最应该补的不是 UI,而是下面六个里程碑。
|
||||
|
||||
### M1. 建立真实 Rust 执行入口
|
||||
|
||||
- 初始化 `rust/bridge/`
|
||||
- 让 Web route 可以调用真实 Rust 运行时,而不是只在 TS 中模拟 Rust request
|
||||
- 先覆盖 `documents.create/get/save/title/options/stats/sidebar/search`
|
||||
|
||||
### M2. 并入 `mnote-cli`
|
||||
|
||||
- 在主仓加入 CLI crate
|
||||
- 先定义稳定命令面和 JSON 输出协议
|
||||
- 让页面、块、搜索、Sidebar 至少先能命令行操作
|
||||
|
||||
### M3. 完成页面系统与块系统的 Rust 接管
|
||||
|
||||
- 页面创建、移动、删除、恢复、复制
|
||||
- block 插入、替换、移动、删除
|
||||
- 页面树与回收站
|
||||
|
||||
### M4. 完成搜索/索引/派生视图 Rust 化
|
||||
|
||||
- 把搜索、snippet、排序、Sidebar 数据集聚合切到 Rust
|
||||
- 建立索引重建与校验命令
|
||||
|
||||
### M5. 完成对象域 adapter
|
||||
|
||||
- `adapter-mindmap`
|
||||
- `adapter-onlyoffice`
|
||||
- 后续再看媒体、表格等对象域
|
||||
|
||||
### M6. 让 AI 与 CLI 共用同一 Tool 面
|
||||
|
||||
- AI 不再调用前端私有写入逻辑
|
||||
- 改为直接使用 Rust tool protocol
|
||||
- 加入 dry-run、权限、审计、回放能力
|
||||
|
||||
---
|
||||
|
||||
## 6. 一句话结论
|
||||
|
||||
当前不是“还差一点点就完成 Rust 内核替换”,而是:
|
||||
|
||||
> **我们已经完成了 Rust 内核替换前的单仓收口、协议奠基和首批接缝,但距离“Rust 真正取代旧内核,并让 CLI 与 AI 成为一等执行入口”还差一整层执行面重构。**
|
||||
|
||||
最关键的剩余目标只有三条:
|
||||
|
||||
- **把业务执行权从 TS route 真正移交给 Rust**
|
||||
- **把核心能力系统化地做成 CLI**
|
||||
- **让 AI 与 CLI 共用同一套 Rust tool 内核**
|
||||
|
||||
只要这三条没完成,就还不能说“原有内核已经被 Rust 替换”。
|
||||
@@ -0,0 +1,415 @@
|
||||
# [recycle] mnote Rust 内核替换总路线图 v1
|
||||
|
||||
> 更新时间:2026-04-15
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-plan.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-phase-checklist.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-missing-targets.md`
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
|
||||
## 1. 目标定义
|
||||
|
||||
本路线图对应的最终目标只有一句话:
|
||||
|
||||
> **用 Rust 内核取代当前 TypeScript/Next route 主导的业务执行面,让 Web、CLI、AI 共享同一套 Rust Command / Query / Tool 内核。**
|
||||
|
||||
这里包含三个同时成立的条件:
|
||||
|
||||
- Rust 成为唯一业务执行平面
|
||||
- 绝大部分核心能力都能通过 CLI 调用
|
||||
- AI 编辑能力建立在与 CLI 同源的 Rust 工具面上
|
||||
|
||||
如果只完成其中一部分,都不能算“Rust 内核已经替换原有内核”。
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前阶段判断
|
||||
|
||||
当前仓库已经完成的是:
|
||||
|
||||
- 单仓收口
|
||||
- Rust workspace 落位
|
||||
- P0 crate 与核心设计文档落位
|
||||
- 首批 bridge 协议接缝
|
||||
- 首批日志与事件落账
|
||||
|
||||
当前仓库还没有完成的是:
|
||||
|
||||
- Rust 真实执行入口
|
||||
- `mnote-cli`
|
||||
- 全域能力域建模
|
||||
- AI/CLI 共用 Tool 面
|
||||
- 旧 TS 执行面的系统性退役
|
||||
|
||||
因此当前阶段应定义为:
|
||||
|
||||
> **Phase 0 完成,Phase 1 即将开始。**
|
||||
|
||||
其中:
|
||||
|
||||
- `Phase 0` = 单仓收口 + 协议奠基
|
||||
- `Phase 1` 以后才是“真正的内核替换”
|
||||
|
||||
---
|
||||
|
||||
## 3. 总体迁移原则
|
||||
|
||||
整个替换过程必须遵守下面六条原则。
|
||||
|
||||
### 3.1 Web 不再承载业务真规则
|
||||
|
||||
Next route 可以保留 transport、auth、session、streaming、SSR/BFF 职责,但不再长期承载核心业务执行规则。
|
||||
|
||||
### 3.2 CLI 与 AI 必须共用同一执行面
|
||||
|
||||
不能出现:
|
||||
|
||||
- CLI 走一套实现
|
||||
- AI 走一套实现
|
||||
- Web 再走第三套实现
|
||||
|
||||
最终只能保留一套 Rust 内核,三种入口共享。
|
||||
|
||||
### 3.3 Convex 继续是事实层,不是业务规则层
|
||||
|
||||
Convex 继续负责主事实保存,但对象规则、命令语义、查询聚合、索引、工具协议应逐步收回 Rust。
|
||||
|
||||
### 3.4 先做执行面替换,再做旧面退役
|
||||
|
||||
不能先删旧链路再补 Rust,也不能长期停留在双内核并行。
|
||||
|
||||
正确顺序是:
|
||||
|
||||
1. 建立 Rust 执行链
|
||||
2. 双入口对齐
|
||||
3. 完成回归
|
||||
4. 退役旧 TS 业务执行逻辑
|
||||
|
||||
### 3.5 先覆盖高频主链路,再覆盖对象域
|
||||
|
||||
优先级应是:
|
||||
|
||||
1. 页面系统
|
||||
2. 块系统
|
||||
3. Sidebar / 搜索 / 索引
|
||||
4. AI 写入工具面
|
||||
5. Mindmap / OnlyOffice / Media / Table
|
||||
|
||||
### 3.6 每一阶段都必须有割接标准
|
||||
|
||||
每个阶段都要明确:
|
||||
|
||||
- 哪些功能已由 Rust 接管
|
||||
- 哪些 TS route 仍是临时面
|
||||
- 哪些旧实现可退役
|
||||
|
||||
---
|
||||
|
||||
## 4. 分阶段路线
|
||||
|
||||
## Phase 1:建立真实 Rust 执行入口
|
||||
|
||||
目标:
|
||||
|
||||
- 在主仓内建立最小 Rust bridge/runtime
|
||||
- Web route 能调用真实 Rust 执行器
|
||||
- 不再只是在 TypeScript 中构造 Rust 风格 request
|
||||
|
||||
优先覆盖:
|
||||
|
||||
- `documents.meta`
|
||||
- `documents.content`
|
||||
- `documents.save`
|
||||
- `documents.title`
|
||||
- `documents.options`
|
||||
- `documents.stats`
|
||||
- `sidebar.dataset.list`
|
||||
- `search.documents`
|
||||
|
||||
阶段产出:
|
||||
|
||||
- `/mnt/Data1T/mnote/rust/bridge/`
|
||||
- 最小 runtime executor
|
||||
- Web -> Rust -> Convex 的真实样板链
|
||||
|
||||
阶段完成标准:
|
||||
|
||||
- 至少 1 条读链和 2 条写链真实经过 Rust 执行
|
||||
- 对应 TS route 不再直接持有业务拼装逻辑
|
||||
|
||||
## Phase 2:引入 `mnote-cli` 并冻结 JSON 协议
|
||||
|
||||
目标:
|
||||
|
||||
- 把 CLI 变成一等入口
|
||||
- 定义稳定的 machine-readable 输出
|
||||
- 为后续 AI 共用工具面打基础
|
||||
|
||||
首批 CLI 面:
|
||||
|
||||
- `page`
|
||||
- `block`
|
||||
- `search`
|
||||
- `sidebar`
|
||||
- `tool`
|
||||
|
||||
阶段产出:
|
||||
|
||||
- `rust/crates/mnote-cli/`
|
||||
- 统一 exit code 约定
|
||||
- `--json` 输出契约
|
||||
- `dry-run` / `validate-only` 基础能力
|
||||
|
||||
阶段完成标准:
|
||||
|
||||
- 核心页面和块操作能脱离浏览器完成
|
||||
- CLI 与 Web 调用同一 Rust 执行面
|
||||
|
||||
## Phase 3:页面系统 Rust 化
|
||||
|
||||
目标:
|
||||
|
||||
- 页面创建、移动、复制、删除、恢复、回收站等能力收口到 Rust
|
||||
- 页面树、父子关系、排序、引用更新不再由 TS route 零散实现
|
||||
|
||||
优先覆盖:
|
||||
|
||||
- `documents.create`
|
||||
- `documents.move`
|
||||
- `documents.delete`
|
||||
- `documents.restore`
|
||||
- `documents.duplicate`
|
||||
- `documents.empty-trash`
|
||||
- `documents.copy-tree`
|
||||
|
||||
阶段完成标准:
|
||||
|
||||
- 页面生命周期操作统一走 Rust command
|
||||
- 旧 TS route 只保留 transport 包装
|
||||
|
||||
## Phase 4:块系统 Rust 化
|
||||
|
||||
目标:
|
||||
|
||||
- 把 BlockNote 正文相关的结构化编辑命令真正变成 Rust 内核能力
|
||||
- AI/CLI 可直接调用块操作,不依赖浏览器交互细节
|
||||
|
||||
优先覆盖:
|
||||
|
||||
- `blocks.get`
|
||||
- `blocks.patch`
|
||||
- `blocks.move`
|
||||
- `blocks.embed`
|
||||
- `doc_insert_blocks`
|
||||
- `doc_replace_range`
|
||||
|
||||
阶段完成标准:
|
||||
|
||||
- Rust 拥有稳定的 block ops 协议
|
||||
- Web 编辑器只负责快照采集、渲染与交互
|
||||
|
||||
## Phase 5:搜索、索引与派生视图 Rust 化
|
||||
|
||||
目标:
|
||||
|
||||
- 把搜索、排序、snippet、Sidebar 数据集聚合统一移入 Rust
|
||||
- 建立索引重建和派生视图回放能力
|
||||
|
||||
优先覆盖:
|
||||
|
||||
- `search.documents`
|
||||
- `search.recent`
|
||||
- `sidebar.dataset.list`
|
||||
- 索引重建与校验命令
|
||||
|
||||
阶段完成标准:
|
||||
|
||||
- TS route 不再做核心召回和 ranking
|
||||
- `index-fts` 真正进入产品主路径
|
||||
|
||||
2026-04-15 Phase 5 第一刀补记:`search.documents`、`search.recent`、`sidebar.dataset.list` 已补齐到主仓 Rust query/runtime 主链。`rust/crates/core-protocol` 新增 `SearchDocuments/SearchRecent` 契约,`storage-convex-bridge` 新增 query name -> Convex function 映射,`bridge-runtime` 现同时支持 query plan 输出与“携带原始 dataset 时直接在 Rust 内执行并返回 result”。其中 `search.documents` 的召回后排序、snippet、高亮、OCR 待补队列决策已迁入 `rust/crates/index-fts` 的 `evaluate_search_documents`;`/api/search/documents` route 现仅保留参数归一化、Convex 原始数据拉取与 HTTP 回传,不再持有标题/正文/思维导图/表格/附件的打分合并逻辑。`/api/sidebar` 也已改为先经 Rust runtime 生成 `sidebar.dataset.list` plan,再通过统一 query transport 调用 `sidebar:datasetList`,不再只是挂一个 queryName 元信息。
|
||||
|
||||
## Phase 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 内核已经替换原有内核”。
|
||||
@@ -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`
|
||||
@@ -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 相关历史稿
|
||||
@@ -2,7 +2,9 @@ use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::routes::web_shell::{
|
||||
build_editor_bootstrap_json, build_page_aggregate_snapshot, escape_html, escape_script_json,
|
||||
load_file_tree_html, load_sidebar_tree_html, load_workspace_shell_projection,
|
||||
render_document_title_controller_script, render_editor_island_adapter_script,
|
||||
};
|
||||
use crate::transport::convex::execute_convex_mutation_by_name;
|
||||
use crate::workspace_shell::render_workspace_shell_sidebar_html;
|
||||
@@ -140,23 +142,79 @@ pub async fn root_entry(
|
||||
.active_page_title
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
let render_workspace_entry = || {
|
||||
crate::ssr::render_view(leptos::view! {
|
||||
<crate::ssr::pages::home::HomePage
|
||||
sidebar_tree_html={sidebar_tree_html.clone()}
|
||||
workspace_name={workspace_name.clone()}
|
||||
workspace_id={workspace_id.clone()}
|
||||
workspace_sidebar_html={workspace_sidebar_html.clone()}
|
||||
active_page_id={active_page_id.clone()}
|
||||
active_page_title={active_page_title.clone()}
|
||||
/>
|
||||
})
|
||||
};
|
||||
let (html_title, content, body_extra) = if active_page_id.trim().is_empty() {
|
||||
("MNOTE".to_string(), render_workspace_entry(), String::new())
|
||||
} else {
|
||||
match build_page_aggregate_snapshot(
|
||||
&state,
|
||||
&context,
|
||||
&active_page_id,
|
||||
Some(workspace_id.as_str()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(aggregate) => {
|
||||
let title = aggregate.head.title.as_str();
|
||||
let page_subtree_json = serde_json::to_string(&aggregate.tree.page_subtree)
|
||||
.unwrap_or_else(|_| "null".to_string());
|
||||
let snapshot_json =
|
||||
serde_json::to_string(&aggregate).unwrap_or_else(|_| "null".to_string());
|
||||
let bootstrap_json = build_editor_bootstrap_json(&aggregate, &context);
|
||||
let content = crate::ssr::render_view(leptos::view! {
|
||||
<crate::ssr::pages::home::HomePage sidebar_tree_html={sidebar_tree_html} workspace_name={workspace_name} workspace_id={workspace_id.clone()} workspace_sidebar_html={workspace_sidebar_html} active_page_id={active_page_id} active_page_title={active_page_title} />
|
||||
<crate::ssr::pages::document::DocumentPage
|
||||
title={title.to_string()}
|
||||
document_id={active_page_id.clone()}
|
||||
workspace_id={workspace_id.clone()}
|
||||
sidebar_tree_html={sidebar_tree_html.clone()}
|
||||
workspace_name={workspace_name.clone()}
|
||||
workspace_sidebar_html={workspace_sidebar_html.clone()}
|
||||
page_subtree_json={page_subtree_json}
|
||||
/>
|
||||
});
|
||||
let body_extra = format!(
|
||||
r#"<script id="__MNOTE_PAGE_AGGREGATE__" type="application/json">{}</script>
|
||||
<script id="__MNOTE_EDITOR_BOOTSTRAP__" type="application/json">{}</script>
|
||||
{}
|
||||
{}"#,
|
||||
escape_script_json(&snapshot_json),
|
||||
escape_script_json(&bootstrap_json),
|
||||
render_document_title_controller_script(),
|
||||
render_editor_island_adapter_script(),
|
||||
);
|
||||
(title.to_string(), content, body_extra)
|
||||
}
|
||||
Err(_) => ("MNOTE".to_string(), render_workspace_entry(), String::new()),
|
||||
}
|
||||
};
|
||||
let mut response = Html(format!(
|
||||
r#"<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>MNOTE</title>
|
||||
<title>{}</title>
|
||||
<style>{}</style>
|
||||
</head>
|
||||
<body data-mnote-web-owner="mnote-web" data-mnote-shell="workspace">
|
||||
{}
|
||||
{}
|
||||
</body>
|
||||
</html>"#,
|
||||
escape_html(&html_title),
|
||||
crate::ssr::MNOTE_CSS,
|
||||
content
|
||||
content,
|
||||
body_extra
|
||||
))
|
||||
.into_response();
|
||||
stamp_gateway_headers(response.headers_mut(), false);
|
||||
|
||||
@@ -36,7 +36,7 @@ use bridge_runtime::RuntimeCommandEnvelopeWire;
|
||||
use core_protocol::KernelProjectionKind;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
@@ -209,7 +209,7 @@ fn collect_expanded_ids(projection: &Value) -> BTreeSet<String> {
|
||||
}
|
||||
|
||||
pub(crate) fn collect_page_tree_render_rows(projection: &Value) -> Vec<PageTreeRenderRow> {
|
||||
projection
|
||||
let mut rows: Vec<PageTreeRenderRow> = projection
|
||||
.get("items")
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| {
|
||||
@@ -232,6 +232,7 @@ pub(crate) fn collect_page_tree_render_rows(projection: &Value) -> Vec<PageTreeR
|
||||
node_id: node_id.to_string(),
|
||||
parent_node_id: item
|
||||
.get("parentNodeId")
|
||||
.or_else(|| item.get("parentId"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
@@ -261,7 +262,30 @@ pub(crate) fn collect_page_tree_render_rows(projection: &Value) -> Vec<PageTreeR
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
|
||||
let parent_by_id = rows
|
||||
.iter()
|
||||
.map(|row| (row.node_id.clone(), row.parent_node_id.clone()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
for row in &mut rows {
|
||||
if row.depth == 0 && row.parent_node_id.is_some() {
|
||||
let mut depth = 0_u32;
|
||||
let mut cursor = row.parent_node_id.as_deref();
|
||||
while let Some(parent_id) = cursor {
|
||||
depth += 1;
|
||||
cursor = parent_by_id
|
||||
.get(parent_id)
|
||||
.and_then(|parent| parent.as_deref());
|
||||
if depth > 32 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
row.depth = depth;
|
||||
}
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
pub(crate) fn collect_filetree_render_rows(
|
||||
|
||||
@@ -126,7 +126,10 @@ pub async fn document_page_shell(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn build_editor_bootstrap_json(aggregate: &PageAggregate, context: &RequestContext) -> String {
|
||||
pub(crate) fn build_editor_bootstrap_json(
|
||||
aggregate: &PageAggregate,
|
||||
context: &RequestContext,
|
||||
) -> String {
|
||||
serde_json::to_string(&json!({
|
||||
"schema": "mnote.editor_bootstrap.v1",
|
||||
"documentId": aggregate.identity.document_id,
|
||||
@@ -142,7 +145,7 @@ fn build_editor_bootstrap_json(aggregate: &PageAggregate, context: &RequestConte
|
||||
.unwrap_or_else(|_| "{}".to_string())
|
||||
}
|
||||
|
||||
fn render_document_title_controller_script() -> &'static str {
|
||||
pub(crate) fn render_document_title_controller_script() -> &'static str {
|
||||
r#"<script>
|
||||
(() => {
|
||||
const CONTRACT = 'mnote.document_title_controller.v1';
|
||||
@@ -259,7 +262,7 @@ fn render_document_title_controller_script() -> &'static str {
|
||||
</script>"#
|
||||
}
|
||||
|
||||
fn render_editor_island_adapter_script() -> &'static str {
|
||||
pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
r#"<script type="module">
|
||||
(() => {
|
||||
const ROOT_SELECTOR = '[data-testid="mnote-leptos-tiptap-island-editor-root"]';
|
||||
@@ -586,7 +589,7 @@ pub async fn page_aggregate(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn build_page_aggregate_snapshot(
|
||||
pub(crate) async fn build_page_aggregate_snapshot(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
document_id: &str,
|
||||
@@ -661,7 +664,7 @@ fn stamp_shell_headers(headers: &mut HeaderMap, shell: &'static str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_html(value: &str) -> String {
|
||||
pub(crate) fn escape_html(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
@@ -669,7 +672,7 @@ fn escape_html(value: &str) -> String {
|
||||
.replace('"', """)
|
||||
}
|
||||
|
||||
fn escape_script_json(value: &str) -> String {
|
||||
pub(crate) fn escape_script_json(value: &str) -> String {
|
||||
value.replace("</script", "<\\/script")
|
||||
}
|
||||
|
||||
|
||||
@@ -252,22 +252,28 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return grouped;
|
||||
}
|
||||
|
||||
function renderPageRows(parentId, grouped, activeId) {
|
||||
function pageTreeChevronSvg() {
|
||||
return '<svg class="tree-toggle-icon" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><path d="M7.84 14.955c.206 0 .37-.07.505-.21l4.277-4.179a.79.79 0 0 0 .264-.574.78.78 0 0 0-.258-.574L8.35 5.24a.7.7 0 0 0-.51-.21.721.721 0 0 0-.498 1.247l3.814 3.721-3.814 3.709a.721.721 0 0 0 .498 1.248"></path></svg>';
|
||||
}
|
||||
|
||||
function renderPageRows(parentId, grouped, activeId, inheritedDepth) {
|
||||
var computedDepth = Number(inheritedDepth || 0);
|
||||
return (grouped.get(parentId) || []).map(function(item) {
|
||||
var nodeId = nodeIdOf(item);
|
||||
var title = titleOf(item);
|
||||
var depth = Number(item.depth || 0);
|
||||
var depth = computedDepth;
|
||||
var parent = parentIdOf(item);
|
||||
var children = grouped.get(nodeId) || [];
|
||||
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
|
||||
var expanded = expandable && item.expandedByDefault !== false;
|
||||
var toggle = expandable
|
||||
? '<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="' + escapeHtml(nodeId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '">' + (expanded ? '▾' : '▸') + '</button>'
|
||||
? '<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="' + escapeHtml(nodeId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '" aria-expanded="' + String(expanded) + '">' + pageTreeChevronSvg() + '</button>'
|
||||
: '<span class="tree-spacer" aria-hidden="true"></span>';
|
||||
var childHtml = expandable && expanded
|
||||
? '<ul class="tree-children">' + renderPageRows(nodeId, grouped, activeId) + '</ul>'
|
||||
? '<ul class="tree-children">' + renderPageRows(nodeId, grouped, activeId, depth + 1) + '</ul>'
|
||||
: '';
|
||||
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="page" data-testid="wolai-sidebar-row" data-tree-testid="tree-node-open" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-depth="' + depth + '" data-shell-mode="page" data-active="' + String(nodeId === activeId) + '" data-focused="false" data-draggable="true" draggable="true" tabindex="-1">' + toggle + '<span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="' + escapeHtml(nodeId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="' + escapeHtml(nodeId) + '" aria-label="新建子页面">+</button><button type="button" class="tree-action" data-testid="tree-action-rename" data-rust-action="rename" data-node-id="' + escapeHtml(nodeId) + '" aria-label="重命名">✎</button><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="' + escapeHtml(nodeId) + '" aria-label="更多操作">…</button></div></div>' + childHtml + '</li>';
|
||||
var currentAttr = nodeId === activeId ? ' aria-current="page" aria-selected="true"' : ' aria-selected="false"';
|
||||
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="page" data-testid="wolai-sidebar-row" data-tree-testid="tree-node-open" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-depth="' + depth + '" data-shell-mode="page" data-active="' + String(nodeId === activeId) + '"' + currentAttr + ' data-focused="false" data-draggable="true" draggable="true" tabindex="-1">' + toggle + '<button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="' + escapeHtml(nodeId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="' + escapeHtml(nodeId) + '" aria-label="更多操作">…</button><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="' + escapeHtml(nodeId) + '" aria-label="新建子页面">+</button></div></div>' + childHtml + '</li>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
@@ -278,7 +284,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return String(item.rowKind || 'document') === 'document';
|
||||
});
|
||||
var activeId = currentDocumentId();
|
||||
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId) : '<li class="tree-empty" data-rust-rendered-row="page-empty">暂无页面</li>') + '</ul>';
|
||||
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId, 0) : '<li class="tree-empty" data-rust-rendered-row="page-empty">暂无页面</li>') + '</ul>';
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -373,7 +379,11 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
children.classList.toggle('tree-children--collapsed');
|
||||
var collapsed = children.classList.contains('tree-children--collapsed');
|
||||
row.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
||||
if (button) button.textContent = collapsed ? '▸' : '▾';
|
||||
if (button && button.getAttribute('data-testid') === 'tree-node-toggle') {
|
||||
button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
||||
} else if (button) {
|
||||
button.textContent = collapsed ? '▸' : '▾';
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchSidebarEvent(name, detail) {
|
||||
@@ -588,20 +598,20 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
{ action: 'move', icon: 'drive_file_move', label: '移动到...' },
|
||||
{ action: 'copy-id', icon: 'tag', label: '复制资源 ID' }
|
||||
] : [
|
||||
{ action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt + O' },
|
||||
{ action: 'share', icon: 'share', label: '共享...' },
|
||||
{ action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt+' },
|
||||
{ separator: true },
|
||||
{ action: 'move', icon: 'drive_file_move', label: '移动到...' },
|
||||
{ action: 'embed', icon: 'account_tree', label: '嵌入到...' },
|
||||
{ separator: true },
|
||||
{ action: 'copy-link', icon: 'link', label: '复制访问链接' },
|
||||
{ action: 'copy-link-title', icon: 'link', label: '复制访问链接(带标题)' },
|
||||
{ action: 'copy-reference-inline', icon: 'content_copy', label: '复制页面引用链接' },
|
||||
{ action: 'copy-id', icon: 'tag', label: '复制页面 ID' },
|
||||
{ action: 'copy-id', icon: 'tag', label: '复制页面ID' },
|
||||
{ separator: true },
|
||||
{ action: 'duplicate', icon: 'file_copy', label: '拷贝副本' },
|
||||
{ separator: true },
|
||||
{ action: 'rename', icon: 'edit', label: '重命名' },
|
||||
{ action: 'create-child', icon: 'add', label: '新建子页面' },
|
||||
{ action: 'convert-child', icon: 'subdirectory_arrow_right', label: '转为上一个子页面' },
|
||||
{ action: 'delete-trash', icon: 'delete', label: '删除到垃圾桶', danger: true }
|
||||
{ separator: true },
|
||||
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true }
|
||||
];
|
||||
items.forEach(function(item) { appendTreeContextMenuButton(menu, item, detail, trigger); });
|
||||
document.body.appendChild(menu);
|
||||
@@ -638,10 +648,155 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}, x, y, trigger || row);
|
||||
}
|
||||
|
||||
function ensureSearchModal() {
|
||||
var existing = document.querySelector('[data-testid="wolai-search-modal"]');
|
||||
if (existing instanceof HTMLElement) return existing;
|
||||
|
||||
var overlay = document.createElement('div');
|
||||
overlay.className = 'wolai-search-overlay';
|
||||
overlay.setAttribute('data-testid', 'wolai-search-modal');
|
||||
overlay.setAttribute('role', 'dialog');
|
||||
overlay.setAttribute('aria-modal', 'true');
|
||||
overlay.hidden = true;
|
||||
|
||||
overlay.innerHTML = '' +
|
||||
'<div class="wolai-search-dialog">' +
|
||||
'<div class="wolai-search-input-row">' +
|
||||
'<span class="material-symbols-outlined wolai-search-input-icon" data-icon="search" aria-hidden="true"></span>' +
|
||||
'<input data-testid="wolai-search-input" class="wolai-search-input" type="text" autocomplete="off" placeholder="在当前工作区中搜索" />' +
|
||||
'<button type="button" class="wolai-search-close" data-testid="wolai-search-close" aria-label="关闭搜索">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-search-options" aria-label="搜索选项">' +
|
||||
'<div class="wolai-search-options-left">' +
|
||||
'<span class="wolai-search-switch-control"><span>仅匹配标题</span><button type="button" class="wolai-search-switch is-on" data-search-switch="title" role="switch" aria-checked="true" aria-label="仅匹配标题"></button></span>' +
|
||||
'<span class="wolai-search-switch-control"><span>精确匹配</span><button type="button" class="wolai-search-switch" data-search-switch="exact" role="switch" aria-checked="false" aria-label="精确匹配"></button></span>' +
|
||||
'<span class="wolai-search-sort-control"><span>按编辑时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="updated" aria-label="按编辑时间范围">所有</button></span>' +
|
||||
'<span class="wolai-search-sort-control"><span>按创建时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="created" aria-label="按创建时间范围">所有</button></span>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-search-options-right">' +
|
||||
'<span class="wolai-search-switch-control"><span>页面内搜索</span><button type="button" class="wolai-search-switch is-on" data-search-switch="page" role="switch" aria-checked="true" aria-label="页面内搜索"></button></span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-search-result-meta" data-testid="wolai-search-result-meta"></div>' +
|
||||
'<div class="wolai-search-results" data-testid="wolai-search-results"></div>' +
|
||||
'</div>';
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
||||
var closeButton = overlay.querySelector('[data-testid="wolai-search-close"]');
|
||||
if (input) input.addEventListener('input', renderSearchResults);
|
||||
overlay.querySelectorAll('[data-search-switch]').forEach(function(button) {
|
||||
button.addEventListener('click', function() {
|
||||
var isOn = button.getAttribute('aria-checked') !== 'true';
|
||||
button.setAttribute('aria-checked', isOn ? 'true' : 'false');
|
||||
button.classList.toggle('is-on', isOn);
|
||||
});
|
||||
});
|
||||
if (closeButton) closeButton.addEventListener('click', closeSearchModal);
|
||||
overlay.addEventListener('click', function(event) {
|
||||
if (event.target === overlay) closeSearchModal();
|
||||
});
|
||||
return overlay;
|
||||
}
|
||||
|
||||
function searchText(value) {
|
||||
return String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function collectSearchCandidates() {
|
||||
var seen = Object.create(null);
|
||||
var candidates = [];
|
||||
function add(title, source) {
|
||||
title = searchText(title);
|
||||
if (!title || seen[title]) return;
|
||||
seen[title] = true;
|
||||
candidates.push({ title: title, source: source || '当前工作区' });
|
||||
}
|
||||
document.querySelectorAll('.tree-link-title, .wolai-row-title, [data-page-title-current="true"], .document-title-input, .document-read-view h1, .document-shell h1').forEach(function(node) {
|
||||
add(node.textContent, '页面');
|
||||
});
|
||||
document.querySelectorAll('.mnote-content h1, .mnote-content h2, .mnote-content h3, .mnote-content p, .mnote-content a').forEach(function(node) {
|
||||
add(node.textContent, '当前页面');
|
||||
});
|
||||
return candidates.slice(0, 120);
|
||||
}
|
||||
|
||||
function highlightSearchTitle(title, query) {
|
||||
var cleanTitle = searchText(title);
|
||||
var cleanQuery = searchText(query);
|
||||
if (!cleanQuery) return escapeHtml(cleanTitle);
|
||||
var index = cleanTitle.toLowerCase().indexOf(cleanQuery.toLowerCase());
|
||||
if (index < 0) return escapeHtml(cleanTitle);
|
||||
return escapeHtml(cleanTitle.slice(0, index)) +
|
||||
'<mark>' + escapeHtml(cleanTitle.slice(index, index + cleanQuery.length)) + '</mark>' +
|
||||
escapeHtml(cleanTitle.slice(index + cleanQuery.length));
|
||||
}
|
||||
|
||||
function renderSearchResults() {
|
||||
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
|
||||
if (!(overlay instanceof HTMLElement)) return;
|
||||
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
||||
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
|
||||
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
|
||||
if (!input || !meta || !results) return;
|
||||
var query = searchText(input.value);
|
||||
var candidates = collectSearchCandidates();
|
||||
var filtered = query
|
||||
? candidates.filter(function(item) { return item.title.toLowerCase().indexOf(query.toLowerCase()) >= 0; })
|
||||
: candidates.slice(0, 8);
|
||||
filtered = filtered.slice(0, 100);
|
||||
meta.innerHTML = '<span>共 ' + filtered.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
||||
if (!filtered.length) {
|
||||
results.innerHTML = '<div class="wolai-search-empty">暂无匹配结果</div>';
|
||||
return;
|
||||
}
|
||||
results.innerHTML = filtered.map(function(item) {
|
||||
return '<button type="button" class="wolai-search-result-row">' +
|
||||
'<span class="material-symbols-outlined wolai-search-result-icon" data-icon="link" aria-hidden="true"></span>' +
|
||||
'<span class="wolai-search-result-main"><span class="wolai-search-result-title">' + highlightSearchTitle(item.title, query) + '</span>' +
|
||||
'<span class="wolai-search-result-snippet">' + escapeHtml(item.source) + ' · 当前工作区</span></span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function isSearchModalOpen() {
|
||||
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
|
||||
return overlay instanceof HTMLElement && !overlay.hidden;
|
||||
}
|
||||
|
||||
function openSearchModal() {
|
||||
var overlay = ensureSearchModal();
|
||||
overlay.hidden = false;
|
||||
document.documentElement.setAttribute('data-mnote-search-modal-open', 'true');
|
||||
renderSearchResults();
|
||||
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
||||
if (input) {
|
||||
setTimeout(function() { input.focus(); input.select(); }, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function closeSearchModal() {
|
||||
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
|
||||
if (overlay instanceof HTMLElement) overlay.hidden = true;
|
||||
document.documentElement.removeAttribute('data-mnote-search-modal-open');
|
||||
}
|
||||
|
||||
function toggleSearchModal() {
|
||||
if (isSearchModalOpen()) closeSearchModal();
|
||||
else openSearchModal();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (activeTreeContextMenu && activeTreeContextMenu.contains(e.target)) return;
|
||||
if (activeTreeContextMenu) closeTreeContextMenu();
|
||||
|
||||
var searchTrigger = closestAction(e.target, '[data-mnote-action="open-search-modal"]');
|
||||
if (searchTrigger) {
|
||||
e.preventDefault();
|
||||
openSearchModal();
|
||||
return;
|
||||
}
|
||||
|
||||
var tabTrigger = closestAction(e.target, '[data-mnote-sidebar-tree-tab]');
|
||||
if (tabTrigger) {
|
||||
e.preventDefault();
|
||||
@@ -753,7 +908,15 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') closeTreeContextMenu();
|
||||
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key && event.key.toLowerCase() === 'p') {
|
||||
event.preventDefault();
|
||||
toggleSearchModal();
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
closeSearchModal();
|
||||
closeTreeContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
function readPageDragNodeId(event) {
|
||||
@@ -1146,7 +1309,7 @@ pub fn PageLayout(
|
||||
<span class="wolai-sidebar-chevron" aria-hidden="true">"⌄"</span>
|
||||
</div>
|
||||
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作">
|
||||
<a href="/search" class:active={current_nav == "search"} title="搜索" aria-label="搜索"><span class="material-symbols-outlined nav-icon" data-icon="search" aria-hidden="true"></span></a>
|
||||
<a href="/search" class:active={current_nav == "search"} title="搜索" aria-label="搜索" data-mnote-action="open-search-modal"><span class="material-symbols-outlined nav-icon" data-icon="search" aria-hidden="true"></span></a>
|
||||
<a href="/graph" title="关系图" aria-label="关系图"><span class="material-symbols-outlined nav-icon" data-icon="account_tree" aria-hidden="true"></span></a>
|
||||
<a href="/actions" title="快捷动作" aria-label="快捷动作"><span class="material-symbols-outlined nav-icon" data-icon="bolt" aria-hidden="true"></span></a>
|
||||
<a href="/help" title="帮助" aria-label="帮助"><span class="material-symbols-outlined nav-icon" data-icon="help" aria-hidden="true"></span></a>
|
||||
@@ -1173,7 +1336,7 @@ pub fn PageLayout(
|
||||
<button type="button" class="wolai-icon-button" title="收藏" aria-label="收藏"><span class="material-symbols-outlined" data-icon="star" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="历史" aria-label="历史"><span class="material-symbols-outlined" data-icon="history" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button wolai-ai-inline" title="AI" aria-label="AI"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="搜索" aria-label="搜索"><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="搜索" aria-label="搜索" data-mnote-action="open-search-modal"><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="更多" aria-label="更多"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -910,6 +910,39 @@ a:hover {
|
||||
}
|
||||
|
||||
/* ===== 响应式 ===== */
|
||||
|
||||
|
||||
.wolai-search-overlay{position:fixed;inset:0;z-index:1200;display:flex;align-items:flex-start;justify-content:center;padding:92px 24px 24px;background:rgba(0,0,0,.32)}
|
||||
.wolai-search-overlay[hidden]{display:none!important}
|
||||
.wolai-search-dialog{width:min(680px,100%);max-height:min(720px,calc(100vh - 128px));overflow:hidden;display:flex;flex-direction:column;background:#FFF;border:1px solid rgba(27,28,28,.10);border-radius:6px;box-shadow:0 18px 48px rgba(15,23,42,.22),0 2px 8px rgba(15,23,42,.08);color:#37352F}
|
||||
.wolai-search-input-row{display:flex;align-items:center;min-height:58px;padding:0 12px 0 18px;border-bottom:1px solid rgba(27,28,28,.08)}
|
||||
.wolai-search-input-icon{width:22px;height:22px;color:#8B8780;margin-right:10px}
|
||||
.wolai-search-input{flex:1 1 auto;min-width:0;height:56px;border:0;outline:none;background:transparent;color:#24211D;font:400 18px/1.4 var(--wolai-font-sans);letter-spacing:0}
|
||||
.wolai-search-input::placeholder{color:#A29E97}
|
||||
.wolai-search-close{width:28px;height:28px;border:0;border-radius:4px;background:transparent;color:#8B8780;cursor:pointer;font-size:20px;line-height:1}
|
||||
.wolai-search-options{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 14px 8px;border-bottom:1px solid rgba(27,28,28,.06);color:#A7A39D;font-size:12px;line-height:1}
|
||||
.wolai-search-options-left,.wolai-search-options-right,.wolai-search-switch-control,.wolai-search-sort-control{display:inline-flex;align-items:center}
|
||||
.wolai-search-options-left{min-width:0;flex-wrap:wrap;gap:8px 12px}
|
||||
.wolai-search-options-right{margin-left:auto;flex:0 0 auto}
|
||||
.wolai-search-switch-control,.wolai-search-sort-control{gap:5px;white-space:nowrap}
|
||||
.wolai-search-switch,.wolai-search-sort-value{border:0;background:transparent;cursor:pointer;font:inherit;letter-spacing:0}
|
||||
.wolai-search-switch{position:relative;width:28px;height:16px;border-radius:999px;background:#D6D4D0;box-shadow:inset 0 0 0 1px rgba(27,28,28,.04)}
|
||||
.wolai-search-switch::after{content:"";position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:999px;background:#FFF;box-shadow:0 1px 2px rgba(15,23,42,.22)}
|
||||
.wolai-search-switch.is-on{background:#C9C7C3}
|
||||
.wolai-search-switch.is-on::after{transform:translateX(12px)}
|
||||
.wolai-search-sort-value{display:inline-flex;align-items:center;gap:3px;color:#6D6A65}
|
||||
.wolai-search-sort-value::after{content:"⌄";color:#B8B4AD;font-size:13px;line-height:1}
|
||||
.wolai-search-result-meta{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 16px;color:#8B8780;font-size:12px;line-height:1.3;border-bottom:1px solid rgba(27,28,28,.06)}
|
||||
.wolai-search-results{overflow:auto;padding:6px}
|
||||
.wolai-search-result-row{width:100%;min-height:54px;display:flex;align-items:flex-start;gap:10px;padding:9px 10px;border:0;border-radius:4px;background:transparent;color:#37352F;cursor:pointer;text-align:left;font:inherit}
|
||||
.wolai-search-result-row:hover{background:rgba(55,53,47,.08)}
|
||||
.wolai-search-result-icon{width:18px;height:18px;margin-top:2px;color:#8B8780}
|
||||
.wolai-search-result-main{min-width:0;display:flex;flex-direction:column;gap:3px}
|
||||
.wolai-search-result-title{color:#2F2D29;font-size:14px;line-height:1.35;word-break:break-word}
|
||||
.wolai-search-result-title mark{padding:0 1px;border-radius:2px;background:#FFE9E6;color:#D83A32}
|
||||
.wolai-search-result-snippet,.wolai-search-empty{color:#8B8780;font-size:12px;line-height:1.35}
|
||||
.wolai-search-empty{padding:28px 12px 32px;text-align:center}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mnote-sidebar {
|
||||
width: 200px;
|
||||
@@ -987,7 +1020,7 @@ a:hover {
|
||||
/* ===== Stitch 260429 UI parity overrides ===== */
|
||||
:root {
|
||||
--atelier-surface: #FAF9F9;
|
||||
--atelier-sidebar: #F4F3F3;
|
||||
--atelier-sidebar: #F5F5F5;
|
||||
--atelier-sidebar-hover: #ECEBE9;
|
||||
--atelier-sidebar-active: #E3E2E2;
|
||||
--atelier-document: #FFFFFF;
|
||||
@@ -1031,26 +1064,26 @@ body {
|
||||
|
||||
.mnote-sidebar,
|
||||
.wolai-sidebar {
|
||||
width: 240px;
|
||||
width: 248px;
|
||||
background: var(--atelier-sidebar);
|
||||
border-right: 0;
|
||||
box-shadow: inset -1px 0 0 rgba(27, 28, 28, 0.06);
|
||||
box-shadow: none;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.mnote-sidebar-header,
|
||||
.wolai-sidebar-header {
|
||||
height: 68px;
|
||||
padding: 16px 12px 8px;
|
||||
gap: 10px;
|
||||
height: 40px;
|
||||
padding: 8px 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wolai-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 5px;
|
||||
background: #D6534D;
|
||||
font-size: 17px;
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
@@ -1058,9 +1091,9 @@ body {
|
||||
.sidebar-workspace-name {
|
||||
max-width: 160px;
|
||||
color: var(--atelier-text);
|
||||
font-size: 17px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.wolai-sidebar-chevron {
|
||||
@@ -1070,8 +1103,8 @@ body {
|
||||
|
||||
.wolai-quick-actions {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
padding: 8px 16px 26px;
|
||||
gap: 10.4px;
|
||||
padding: 5px 8px 11px;
|
||||
}
|
||||
|
||||
.wolai-quick-actions a,
|
||||
@@ -1080,9 +1113,9 @@ body {
|
||||
}
|
||||
|
||||
.wolai-quick-actions a {
|
||||
height: 28px;
|
||||
height: 30px;
|
||||
color: #3F3D39;
|
||||
font-size: 19px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.wolai-quick-actions a:hover {
|
||||
@@ -1099,13 +1132,14 @@ body {
|
||||
|
||||
.wolai-section-title,
|
||||
.wolai-sidebar-tabs {
|
||||
height: 30px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
color: var(--atelier-text-muted);
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -1237,13 +1271,14 @@ body {
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-row {
|
||||
height: 29px;
|
||||
gap: 5px;
|
||||
margin: 1px 4px;
|
||||
padding: 0 8px;
|
||||
height: 32px;
|
||||
gap: 0;
|
||||
margin: 0 5px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
color: #2D2E2E;
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-row:hover {
|
||||
@@ -1252,8 +1287,8 @@ body {
|
||||
|
||||
.sidebar-tree .tree-row[data-active="true"],
|
||||
.sidebar-tree .tree-row[data-selected="true"] {
|
||||
background: var(--atelier-sidebar-active);
|
||||
color: var(--atelier-text);
|
||||
background: rgba(255, 71, 71, 0.1);
|
||||
color: #E0525B;
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-row[data-drop-feedback="true"],
|
||||
@@ -1265,16 +1300,44 @@ body {
|
||||
|
||||
.sidebar-tree .tree-toggle,
|
||||
.sidebar-tree .tree-spacer {
|
||||
width: 16px;
|
||||
height: 22px;
|
||||
width: 20px;
|
||||
height: 24px;
|
||||
color: #B8B5AF;
|
||||
font-size: 13px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-toggle {
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-toggle:hover {
|
||||
background: rgba(27, 28, 28, 0.06);
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-toggle-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
fill: #878787;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-toggle[aria-expanded="true"] .tree-toggle-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-row[data-active="true"] .tree-toggle-icon {
|
||||
fill: #CF5659;
|
||||
}
|
||||
|
||||
.sidebar-tree:not([data-tree-shell-mode="filetree"]) .tree-kind-badge[data-kind="page"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-kind-badge {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@@ -1295,7 +1358,7 @@ body {
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-kind-badge[data-kind="page"]::before {
|
||||
content: "⌂";
|
||||
content: "";
|
||||
}
|
||||
|
||||
.sidebar-tree[data-tree-shell-mode="filetree"] .tree-kind-badge[data-kind="page"]::before,
|
||||
@@ -1333,7 +1396,7 @@ body {
|
||||
.sidebar-tree .tree-link {
|
||||
padding: 0 2px;
|
||||
color: inherit;
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-link-title {
|
||||
@@ -1341,17 +1404,18 @@ body {
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-row[data-active="true"] .tree-link-title {
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-actions {
|
||||
gap: 1px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-action {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: #9A968F;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: #B3B3B3;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -1363,31 +1427,33 @@ body {
|
||||
.mnote-tree-context-menu {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
min-width: 236px;
|
||||
width: 220px;
|
||||
min-width: 220px;
|
||||
max-width: min(320px, calc(100vw - 16px));
|
||||
padding: 6px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.08);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: 0 16px 32px rgba(27, 28, 28, 0.12);
|
||||
border-radius: 6px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 8px 20px rgba(27, 28, 28, 0.12);
|
||||
backdrop-filter: blur(24px);
|
||||
color: var(--atelier-text);
|
||||
}
|
||||
|
||||
.mnote-tree-context-menu__item {
|
||||
width: 100%;
|
||||
min-height: 34px;
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 9px;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
line-height: 22px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1439,7 +1505,7 @@ body {
|
||||
|
||||
.mnote-tree-context-menu__separator {
|
||||
height: 1px;
|
||||
margin: 5px 4px;
|
||||
margin: 6px 0;
|
||||
background: rgba(27, 28, 28, 0.08);
|
||||
}
|
||||
|
||||
@@ -1468,7 +1534,7 @@ body {
|
||||
}
|
||||
|
||||
.wolai-sidebar-footer {
|
||||
border-top: 1px solid rgba(27, 28, 28, 0.08);
|
||||
border-top: 0;
|
||||
background: var(--atelier-sidebar);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ pub fn build_page_tree_dom_rows(rows: &[PageTreeRenderRow]) -> Vec<PageTreeDomRo
|
||||
.collect()
|
||||
}
|
||||
|
||||
const PAGE_TREE_CHEVRON_SVG: &str = r#"<svg class="tree-toggle-icon" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><path d="M7.84 14.955c.206 0 .37-.07.505-.21l4.277-4.179a.79.79 0 0 0 .264-.574.78.78 0 0 0-.258-.574L8.35 5.24a.7.7 0 0 0-.51-.21.721.721 0 0 0-.498 1.247l3.814 3.721-3.814 3.709a.721.721 0 0 0 .498 1.248"></path></svg>"#;
|
||||
|
||||
fn escape_html(input: &str) -> String {
|
||||
input
|
||||
.replace('&', "&")
|
||||
@@ -54,6 +56,26 @@ fn escape_html(input: &str) -> String {
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
fn render_depth_for_node(rows: &[PageTreeRenderRow], node_id: &str, fallback_depth: u32) -> u32 {
|
||||
if fallback_depth > 0 {
|
||||
return fallback_depth;
|
||||
}
|
||||
let parent_by_id = rows
|
||||
.iter()
|
||||
.map(|row| (row.node_id.as_str(), row.parent_node_id.as_deref()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let mut depth = 0_u32;
|
||||
let mut cursor = parent_by_id.get(node_id).and_then(|parent| *parent);
|
||||
while let Some(parent_id) = cursor {
|
||||
depth += 1;
|
||||
cursor = parent_by_id.get(parent_id).and_then(|parent| *parent);
|
||||
if depth > 32 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
depth
|
||||
}
|
||||
|
||||
fn render_page_row(
|
||||
html: &mut String,
|
||||
row: &PageTreeRenderRow,
|
||||
@@ -80,11 +102,12 @@ fn render_page_row(
|
||||
.unwrap_or(false);
|
||||
let toggle_html = if row.expandable {
|
||||
format!(
|
||||
r#"<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="{node_id}" aria-label="{label} {title}">{marker}</button>"#,
|
||||
r#"<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="{node_id}" aria-label="{label} {title}" aria-expanded="{expanded_state}">{marker}</button>"#,
|
||||
node_id = escape_html(&row.node_id),
|
||||
label = if expanded { "折叠" } else { "展开" },
|
||||
title = escape_html(&row.title),
|
||||
marker = if expanded { "▾" } else { "▸" },
|
||||
expanded_state = if expanded { "true" } else { "false" },
|
||||
marker = PAGE_TREE_CHEVRON_SVG,
|
||||
)
|
||||
} else {
|
||||
r#"<span class="tree-spacer" aria-hidden="true"></span>"#.to_string()
|
||||
@@ -96,15 +119,18 @@ fn render_page_row(
|
||||
.and_then(|source| source.parent_node_id.as_deref())
|
||||
.map(|parent_id| format!(r#" data-parent-id="{}""#, escape_html(parent_id)))
|
||||
.unwrap_or_default();
|
||||
let render_depth = render_depth_for_node(&input.rows, &row.node_id, row.depth);
|
||||
html.push_str(&format!(
|
||||
r#"<li class="tree-node" data-node-id="{node_id}"><div class="tree-row" role="treeitem" aria-level="{aria_level}" aria-expanded="{expanded_attr}" data-rust-rendered-row="page" data-testid="wolai-sidebar-row" data-tree-testid="{test_id}" data-node-id="{node_id}"{parent_attr} data-depth="{depth}" data-shell-mode="page" data-active="{active}" data-focused="{focused}" data-draggable="true" draggable="true" tabindex="{tab_index}">{toggle_html}<span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="{node_id}"><span class="tree-link-title">{title}</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="{node_id}" aria-label="新建子页面">+</button><button type="button" class="tree-action" data-testid="tree-action-rename" data-rust-action="rename" data-node-id="{node_id}" aria-label="重命名">✎</button><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="{node_id}" aria-label="更多操作">…</button></div></div>"#,
|
||||
r#"<li class="tree-node" data-node-id="{node_id}"><div class="tree-row" role="treeitem" aria-level="{aria_level}" aria-expanded="{expanded_attr}" data-rust-rendered-row="page" data-testid="wolai-sidebar-row" data-tree-testid="{test_id}" data-node-id="{node_id}"{parent_attr} data-depth="{depth}" data-shell-mode="page" data-active="{active}" aria-selected="{selected}"{current_attr} data-focused="{focused}" data-draggable="true" draggable="true" tabindex="{tab_index}">{toggle_html}<button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="{node_id}"><span class="tree-link-title">{title}</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="{node_id}" aria-label="更多操作">…</button><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="{node_id}" aria-label="新建子页面">+</button></div></div>"#,
|
||||
node_id = escape_html(&row.node_id),
|
||||
aria_level = row.depth + 1,
|
||||
aria_level = render_depth + 1,
|
||||
expanded_attr = if row.expandable && expanded { "true" } else { "false" },
|
||||
test_id = row.test_id,
|
||||
parent_attr = parent_attr,
|
||||
depth = row.depth,
|
||||
depth = render_depth,
|
||||
active = active,
|
||||
selected = active,
|
||||
current_attr = if active { r#" aria-current="page""# } else { "" },
|
||||
focused = focused,
|
||||
tab_index = if focused { "0" } else { "-1" },
|
||||
toggle_html = toggle_html,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Rust Design Index
|
||||
|
||||
更新时间:2026-04-13
|
||||
适用范围:`/mnt/Data1T/mnote/rust/design`
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前状态
|
||||
|
||||
当前目录只承载已经复制进 `/mnt/Data1T/mnote/rust/` 的核心稳定设计,用于支撑单仓收口后的 Rust 内核落位。
|
||||
|
||||
当前已复制:
|
||||
|
||||
- `core/01-domain-model-v0.md`
|
||||
- `core/02-command-query-tool-protocol-v0.md`
|
||||
- `core/03-storage-event-indexing-v0.md`
|
||||
- `core/04-onlyoffice-integration-boundary-v0.md`
|
||||
|
||||
这些文档对应当前已复制进 `/mnt/Data1T/mnote/rust/crates/` 的 P0 crate:
|
||||
|
||||
- `core-domain`
|
||||
- `core-protocol`
|
||||
- `event-log`
|
||||
- `storage-convex-bridge`
|
||||
- `index-fts`
|
||||
|
||||
---
|
||||
|
||||
## 2. 阅读顺序
|
||||
|
||||
后续在 `mnote` 主仓处理 Rust 内核时,建议按以下顺序阅读:
|
||||
|
||||
1. `core/01-domain-model-v0.md`
|
||||
2. `core/02-command-query-tool-protocol-v0.md`
|
||||
3. `core/03-storage-event-indexing-v0.md`
|
||||
4. `core/04-onlyoffice-integration-boundary-v0.md`
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前不在本目录的历史资料
|
||||
|
||||
以下资料仍保留在 `/mnt/Data1T/mnote-rust/design/`,当前作为参考,不属于本次第一阶段强制复制范围:
|
||||
|
||||
- `blueprint/`
|
||||
- `phases/`
|
||||
- `execution/`
|
||||
- `UI/`
|
||||
|
||||
如果后续需要继续把历史阶段文档并入主仓,应在复制前先做去历史化整理,避免把旧阶段叙事原样带入 `mnote` 主仓。
|
||||
|
||||
---
|
||||
|
||||
## 4. 约定
|
||||
|
||||
- `/mnt/Data1T/mnote/design/` 继续承担全局架构与跨系统路线说明。
|
||||
- `/mnt/Data1T/mnote/rust/design/` 只承载 Rust 内核专属设计。
|
||||
- 新增 Rust 设计文档时,优先补充这里的索引,再补充 `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/process/rust-kernel-backport-plan.md` 的实施状态。
|
||||
@@ -0,0 +1,665 @@
|
||||
# 01. Domain Model v0
|
||||
|
||||
更新时间:2026-04-11
|
||||
适用范围:`/mnt/Data1T/mnote-rust`
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本文件定义 `mnote-rust` 的**核心领域模型**。
|
||||
|
||||
它要解决的问题不是“前端怎么渲染”,而是:
|
||||
|
||||
- 系统里到底有哪些一等对象
|
||||
- 哪些对象是事实层真相,哪些只是派生视图
|
||||
- AI / CLI / Web / Desktop 应该围绕什么稳定对象工作
|
||||
- OnlyOffice、Mindmap、OCR、RAG 这类能力应挂在哪一层
|
||||
|
||||
本文件优先保证:
|
||||
|
||||
- 长期稳定
|
||||
- 脱离 UI 依赖
|
||||
- 适合 Rust 内核实现
|
||||
- 适合 AI 通过结构化协议读写
|
||||
|
||||
---
|
||||
|
||||
## 2. 建模原则
|
||||
|
||||
### 2.1 事实与视图分离
|
||||
|
||||
以下对象属于**事实层**:
|
||||
|
||||
- Workspace
|
||||
- Page
|
||||
- Block
|
||||
- Asset
|
||||
- Reference
|
||||
- Task
|
||||
- CommandLog
|
||||
- Event
|
||||
- AgentSession
|
||||
|
||||
以下对象默认属于**派生层 / 视图层**:
|
||||
|
||||
- 目录(TOC)
|
||||
- 反链聚合
|
||||
- 搜索结果列表
|
||||
- Mindmap 视图树
|
||||
- OnlyOffice 当前光标/当前页/当前选区
|
||||
- AI 面板当前消息列表 UI 状态
|
||||
|
||||
原则:
|
||||
|
||||
> 派生层可以重建;事实层必须稳定、可审计、可迁移。
|
||||
|
||||
### 2.2 人与 AI 共用同一套领域对象
|
||||
|
||||
不能做人类一套模型、AI 一套模型。
|
||||
|
||||
要求:
|
||||
|
||||
- 人类编辑页面,本质是修改 `Page / Block / Asset / Reference`
|
||||
- AI 编辑页面,本质也是修改同样的对象
|
||||
- CLI 批处理、同步任务、导入器也修改同样的对象
|
||||
|
||||
### 2.3 第三方编辑器不是事实源
|
||||
|
||||
Block editor、OnlyOffice、mindmap editor 都不是系统真相。
|
||||
|
||||
它们只能是:
|
||||
|
||||
- 某类对象的视图/交互适配器
|
||||
- 输入输出变换器
|
||||
- 外部文档能力宿主
|
||||
|
||||
### 2.4 结构化修改优先
|
||||
|
||||
优先通过:
|
||||
|
||||
- 显式对象
|
||||
- 显式字段
|
||||
- 显式命令
|
||||
- 显式 ops
|
||||
|
||||
避免通过:
|
||||
|
||||
- UI 状态猜测
|
||||
- 整文全文字符串替换
|
||||
- 第三方编辑器内部瞬时结构直接落库
|
||||
|
||||
---
|
||||
|
||||
## 3. 对象总览
|
||||
|
||||
```text
|
||||
Workspace
|
||||
├─ Page
|
||||
│ ├─ DocumentBody
|
||||
│ │ └─ Block*
|
||||
│ ├─ PageProperty*
|
||||
│ ├─ PageLink*
|
||||
│ └─ Snapshot*
|
||||
├─ Asset*
|
||||
├─ Task*
|
||||
├─ AgentSession*
|
||||
├─ CommandLog*
|
||||
└─ Event*
|
||||
|
||||
Reference
|
||||
├─ block -> block
|
||||
├─ block -> page
|
||||
├─ block -> asset-fragment
|
||||
├─ page -> asset
|
||||
└─ task -> page/block/asset
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Workspace
|
||||
|
||||
`Workspace` 是系统的顶层容器。
|
||||
|
||||
建议字段:
|
||||
|
||||
- `workspace_id`
|
||||
- `slug`
|
||||
- `title`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `owner_actor_id`
|
||||
- `default_locale`
|
||||
- `storage_policy`
|
||||
- `sync_policy`
|
||||
- `feature_flags`
|
||||
- `archived_at`
|
||||
|
||||
职责:
|
||||
|
||||
- 隔离页面、资产、任务、日志
|
||||
- 挂载存储策略与同步策略
|
||||
- 作为权限边界与导出边界
|
||||
|
||||
不负责:
|
||||
|
||||
- 具体页面内容
|
||||
- UI 布局
|
||||
- 当前用户会话态
|
||||
|
||||
---
|
||||
|
||||
## 5. Page
|
||||
|
||||
`Page` 是笔记系统的主内容容器。
|
||||
|
||||
### 5.1 Page 的定位
|
||||
|
||||
它不是“某个编辑器文件”,而是:
|
||||
|
||||
- 一条知识对象
|
||||
- 一个文档入口
|
||||
- 一个块树正文容器
|
||||
- 一个可被引用、索引、审计的实体
|
||||
|
||||
### 5.2 建议字段
|
||||
|
||||
- `page_id`
|
||||
- `workspace_id`
|
||||
- `parent_page_id`(允许页面树)
|
||||
- `title`
|
||||
- `slug`
|
||||
- `icon`
|
||||
- `cover_asset_id`
|
||||
- `body_root_block_id`
|
||||
- `page_type`(`note | doc | database_record | inbox | template | system`)
|
||||
- `status`(`active | archived | deleted`)
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `last_edited_at`
|
||||
- `last_edited_by`
|
||||
- `current_revision`
|
||||
|
||||
### 5.3 PageProperty
|
||||
|
||||
页面属性不应和前端表格视图绑死。
|
||||
|
||||
建议独立对象:
|
||||
|
||||
- `property_key`
|
||||
- `value_type`
|
||||
- `value`
|
||||
- `display_hint`
|
||||
- `source`
|
||||
|
||||
可用于:
|
||||
|
||||
- 标签
|
||||
- 时间
|
||||
- 状态
|
||||
- 优先级
|
||||
- 自定义结构化元数据
|
||||
|
||||
---
|
||||
|
||||
## 6. DocumentBody 与 Block
|
||||
|
||||
### 6.1 为什么以 Block 为正文主模型
|
||||
|
||||
因为从既有 `mnote` / `mnote-next` 的稳定人层语义看,真正稳定的是:
|
||||
|
||||
- 单块编辑
|
||||
- 在某块后插入块
|
||||
- 删除块
|
||||
- 重排块
|
||||
- 缩进层级
|
||||
- 页面引用与块引用占位
|
||||
- 复杂块占位
|
||||
|
||||
这说明“块”比“整篇富文本 JSON”更接近系统真实操作单元。
|
||||
|
||||
### 6.2 Block 建议字段
|
||||
|
||||
- `block_id`
|
||||
- `workspace_id`
|
||||
- `page_id`
|
||||
- `parent_block_id`
|
||||
- `prev_block_id`
|
||||
- `next_block_id`
|
||||
- `sort_key`
|
||||
- `block_type`
|
||||
- `content`
|
||||
- `props`
|
||||
- `annotations`
|
||||
- `status`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `created_by`
|
||||
- `updated_by`
|
||||
- `revision`
|
||||
|
||||
### 6.3 BlockType 建议
|
||||
|
||||
最小集合建议:
|
||||
|
||||
- `paragraph`
|
||||
- `heading`
|
||||
- `bulleted_list_item`
|
||||
- `numbered_list_item`
|
||||
- `todo`
|
||||
- `quote`
|
||||
- `code_block`
|
||||
- `divider`
|
||||
- `callout`
|
||||
- `page_reference`
|
||||
- `block_reference`
|
||||
- `embed_asset`
|
||||
- `embed_view`
|
||||
- `embed_onlyoffice`
|
||||
- `embed_mindmap`
|
||||
- `embed_table`
|
||||
- `system_placeholder`
|
||||
|
||||
原则:
|
||||
|
||||
- 类型应描述语义,不描述某个前端组件名
|
||||
- `embed_*` 表示“外部能力挂件”,不是事实层自己变成那个系统
|
||||
|
||||
### 6.4 Block.content
|
||||
|
||||
建议:
|
||||
|
||||
- 文本类块使用结构化 inline span 序列
|
||||
- 避免只存单纯 HTML
|
||||
- 避免直接依赖第三方编辑器私有 schema
|
||||
|
||||
建议形态:
|
||||
|
||||
```json
|
||||
{
|
||||
"spans": [
|
||||
{ "type": "text", "text": "hello" },
|
||||
{ "type": "page_ref", "page_id": "page_xxx", "title": "项目计划" },
|
||||
{ "type": "text", "text": " world" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 Block.props
|
||||
|
||||
只放结构化、有限、可校验字段,例如:
|
||||
|
||||
- `level`
|
||||
- `checked`
|
||||
- `collapsed`
|
||||
- `indent`
|
||||
- `language`
|
||||
- `align`
|
||||
- `width`
|
||||
- `height`
|
||||
- `asset_id`
|
||||
- `view_id`
|
||||
|
||||
避免把任意 UI 状态塞进 props。
|
||||
|
||||
---
|
||||
|
||||
## 7. Asset
|
||||
|
||||
`Asset` 是所有非正文主块树内容的统一挂载对象。
|
||||
|
||||
### 7.1 Asset 范围
|
||||
|
||||
包括:
|
||||
|
||||
- 图片
|
||||
- PDF
|
||||
- docx
|
||||
- xlsx
|
||||
- pptx
|
||||
- 音频
|
||||
- 视频
|
||||
- 导出文件
|
||||
- OCR 中间产物
|
||||
- 结构提取结果
|
||||
|
||||
### 7.2 建议字段
|
||||
|
||||
- `asset_id`
|
||||
- `workspace_id`
|
||||
- `storage_key`
|
||||
- `original_name`
|
||||
- `mime_type`
|
||||
- `size_bytes`
|
||||
- `checksum`
|
||||
- `origin`(upload / import / generated / synced)
|
||||
- `asset_kind`(image / pdf / office / audio / video / binary / extracted_text)
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `created_by`
|
||||
- `status`
|
||||
- `latest_version_id`
|
||||
|
||||
### 7.3 AssetVersion
|
||||
|
||||
附件应支持版本化。
|
||||
|
||||
建议:
|
||||
|
||||
- `asset_version_id`
|
||||
- `asset_id`
|
||||
- `version_no`
|
||||
- `storage_key`
|
||||
- `checksum`
|
||||
- `derived_from_version_id`
|
||||
- `created_at`
|
||||
- `created_by`
|
||||
- `change_reason`
|
||||
|
||||
这样才能支撑:
|
||||
|
||||
- OnlyOffice 编辑回写
|
||||
- OCR 重跑
|
||||
- 导入转换
|
||||
- AI 修改资产派生内容
|
||||
|
||||
---
|
||||
|
||||
## 8. Reference
|
||||
|
||||
`Reference` 是知识型系统的关键对象,不能只做正文里的临时 token。
|
||||
|
||||
### 8.1 Reference 范围
|
||||
|
||||
- 块引用块
|
||||
- 页面引用
|
||||
- 资产片段引用
|
||||
- PDF 页码引用
|
||||
- Office 书签/段落锚点引用
|
||||
- URL 引用
|
||||
- 检索结果引用
|
||||
|
||||
### 8.2 建议字段
|
||||
|
||||
- `reference_id`
|
||||
- `workspace_id`
|
||||
- `source_object_type`
|
||||
- `source_object_id`
|
||||
- `target_object_type`
|
||||
- `target_object_id`
|
||||
- `anchor`
|
||||
- `label`
|
||||
- `snippet`
|
||||
- `confidence`
|
||||
- `created_at`
|
||||
- `created_by`
|
||||
- `ref_kind`
|
||||
|
||||
### 8.3 Anchor
|
||||
|
||||
`anchor` 建议作为独立结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "pdf_page",
|
||||
"page": 12,
|
||||
"bbox": null,
|
||||
"text_quote": "..."
|
||||
}
|
||||
```
|
||||
|
||||
或:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "office_bookmark",
|
||||
"bookmark": "Heading_3",
|
||||
"text_quote": "..."
|
||||
}
|
||||
```
|
||||
|
||||
原则:
|
||||
|
||||
- 锚点是系统对象,不是编辑器私有游标
|
||||
- 锚点失效时要能标记 stale,而不是静默消失
|
||||
|
||||
---
|
||||
|
||||
## 9. SelectionAnchor
|
||||
|
||||
`SelectionAnchor` 不是长期事实主对象,但在 Agent / 编辑器桥接中有价值。
|
||||
|
||||
它表示:
|
||||
|
||||
- 当前选区
|
||||
- 当前光标
|
||||
- 当前活动页
|
||||
- 当前资产中的定位点
|
||||
|
||||
建议定位为:
|
||||
|
||||
- **短生命周期上下文对象**
|
||||
- 可存入 AgentSession / UI Session
|
||||
- 默认不直接作为主事实落库
|
||||
|
||||
因为:
|
||||
|
||||
- 它变化太频繁
|
||||
- 容易和编辑器宿主耦合
|
||||
- 更适合作为命令输入上下文
|
||||
|
||||
---
|
||||
|
||||
## 10. Task
|
||||
|
||||
`Task` 既是用户任务,也是 AI 工作单元。
|
||||
|
||||
### 10.1 建议字段
|
||||
|
||||
- `task_id`
|
||||
- `workspace_id`
|
||||
- `title`
|
||||
- `description`
|
||||
- `task_type`
|
||||
- `status`
|
||||
- `priority`
|
||||
- `assignee_actor_id`
|
||||
- `source_page_id`
|
||||
- `source_block_id`
|
||||
- `input_payload`
|
||||
- `output_payload`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `due_at`
|
||||
- `completed_at`
|
||||
|
||||
### 10.2 为什么纳入领域模型
|
||||
|
||||
因为 AI 长期介入后,很多能力不是即时编辑,而是:
|
||||
|
||||
- 导入任务
|
||||
- 提取任务
|
||||
- OCR 任务
|
||||
- 摘要任务
|
||||
- 索引重建任务
|
||||
- 同步任务
|
||||
|
||||
这些都应进入系统内核,而不是散在外部脚本里。
|
||||
|
||||
---
|
||||
|
||||
## 11. AgentSession
|
||||
|
||||
`AgentSession` 表示某次 AI 介入的上下文单元。
|
||||
|
||||
### 11.1 建议字段
|
||||
|
||||
- `agent_session_id`
|
||||
- `workspace_id`
|
||||
- `provider`
|
||||
- `model`
|
||||
- `initiator_actor_id`
|
||||
- `status`
|
||||
- `started_at`
|
||||
- `updated_at`
|
||||
- `ended_at`
|
||||
- `tool_policy`
|
||||
- `confirmation_policy`
|
||||
- `summary`
|
||||
|
||||
### 11.2 不应存什么
|
||||
|
||||
不应把整份聊天 UI 状态直接当作领域模型核心。
|
||||
|
||||
建议分开:
|
||||
|
||||
- 领域层只保留会话元信息、工具调用摘要、事件链路
|
||||
- 详细消息可作为附属日志或导出工件保存
|
||||
|
||||
---
|
||||
|
||||
## 12. CommandLog 与 Event
|
||||
|
||||
### 12.1 CommandLog
|
||||
|
||||
代表一次显式操作请求。
|
||||
|
||||
建议字段:
|
||||
|
||||
- `command_id`
|
||||
- `workspace_id`
|
||||
- `command_name`
|
||||
- `actor`
|
||||
- `source`
|
||||
- `target`
|
||||
- `payload_hash`
|
||||
- `reason`
|
||||
- `refs`
|
||||
- `idempotency_key`
|
||||
- `dry_run`
|
||||
- `status`
|
||||
- `requested_at`
|
||||
- `finished_at`
|
||||
- `error_code`
|
||||
- `error_message`
|
||||
|
||||
### 12.2 Event
|
||||
|
||||
代表命令执行后产生的事实变化。
|
||||
|
||||
建议字段:
|
||||
|
||||
- `event_id`
|
||||
- `workspace_id`
|
||||
- `command_id`
|
||||
- `event_type`
|
||||
- `object_type`
|
||||
- `object_id`
|
||||
- `before_revision`
|
||||
- `after_revision`
|
||||
- `payload`
|
||||
- `created_at`
|
||||
|
||||
原则:
|
||||
|
||||
- 命令与事件分离
|
||||
- 先有意图,再有变化
|
||||
- 审计必须能回答“谁因为什么改了什么”
|
||||
|
||||
---
|
||||
|
||||
## 13. View / Derived Object
|
||||
|
||||
以下对象建议明确标注为派生对象,不直接当真相:
|
||||
|
||||
- TOCView
|
||||
- BacklinkView
|
||||
- SearchHit
|
||||
- KnowledgeGraphView
|
||||
- MindmapProjection
|
||||
- OnlyOfficeProjection
|
||||
- PagePreview
|
||||
- DailyDigest
|
||||
|
||||
它们的共同特点:
|
||||
|
||||
- 来自事实层投影
|
||||
- 可以缓存
|
||||
- 可以失效
|
||||
- 可以重建
|
||||
|
||||
---
|
||||
|
||||
## 14. Mindmap 的正确定位
|
||||
|
||||
从既有设计看,Mindmap 很适合走结构化 `ops` 协议。
|
||||
|
||||
但在 `mnote-rust` 中,建议将其定义为:
|
||||
|
||||
- `Asset` 或 `ViewProjection` 的一种
|
||||
- 由 `Page / Block / Reference / Asset` 投影生成或承载
|
||||
- 支持独立存储其节点视图结构
|
||||
- 但不让它成为全系统唯一知识主模型
|
||||
|
||||
建议:
|
||||
|
||||
- 若是“页面内导图块”,则作为 `embed_mindmap` block 挂载
|
||||
- 若是“独立导图资产”,则作为 `Asset(kind=mindmap)`
|
||||
- AI 修改导图时,走 `MindmapOp[]`,但最终仍写入系统命令日志
|
||||
|
||||
---
|
||||
|
||||
## 15. OnlyOffice 的正确定位
|
||||
|
||||
OnlyOffice 不应被建模为“Page 本体”,应被建模为:
|
||||
|
||||
- `Asset(kind=office)`
|
||||
- `AssetVersion`
|
||||
- `OnlyOfficeAdapterSession`
|
||||
- `SelectionAnchor(kind=office_*)`
|
||||
- `Reference(anchor=office_*)`
|
||||
|
||||
也就是说:
|
||||
|
||||
- Office 文档是系统资产
|
||||
- OnlyOffice 是该资产的一种编辑适配器
|
||||
- 编辑产生新版本与事件
|
||||
- AI 可以通过插件/适配器读当前选区、插入内容、替换内容
|
||||
- 但系统真相依然在你的领域模型与资产版本链里
|
||||
|
||||
---
|
||||
|
||||
## 16. v0 必须避免的错误
|
||||
|
||||
### 16.1 不要把前端 store 当领域模型
|
||||
|
||||
例如:
|
||||
|
||||
- 当前页面 UI 选中态
|
||||
- 面板展开态
|
||||
- 聊天输入框草稿
|
||||
|
||||
这些不应进入核心领域。
|
||||
|
||||
### 16.2 不要把第三方编辑器 JSON 当系统法典
|
||||
|
||||
否则未来:
|
||||
|
||||
- 替换编辑器会非常痛苦
|
||||
- AI 只能学编辑器私有结构
|
||||
- CLI 难以稳定操作
|
||||
|
||||
### 16.3 不要把全文字符串替换当主修改方式
|
||||
|
||||
必须保留结构化修改能力。
|
||||
|
||||
### 16.4 不要把向量库结果当事实对象
|
||||
|
||||
向量结果是检索派生物,不是知识真相。
|
||||
|
||||
---
|
||||
|
||||
## 17. 一句话结论
|
||||
|
||||
`mnote-rust` 的领域模型应以 **Workspace / Page / Block / Asset / Reference / Task / AgentSession / CommandLog / Event** 为核心;
|
||||
Mindmap、OnlyOffice、搜索结果、AI 面板都只是围绕这些核心对象形成的投影、适配器或任务系统,而不能反客为主。
|
||||
@@ -0,0 +1,717 @@
|
||||
# 02. Command / Query / Tool Protocol v0
|
||||
|
||||
更新时间:2026-04-11
|
||||
适用范围:`/mnt/Data1T/mnote-rust`
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本文件定义 `mnote-rust` 的统一协议面。
|
||||
|
||||
目标只有一个:
|
||||
|
||||
> 让 Web、Desktop、CLI、Agent、批处理任务都通过**同一套正式入口**操作系统。
|
||||
|
||||
它要解决的问题:
|
||||
|
||||
- 怎样避免“每个页面、每个组件、每个 API route 都有自己的写法”
|
||||
- 怎样让 AI 不依赖 UI 模拟,而能直接操作笔记软件
|
||||
- 怎样保证命令可审计、可回放、可测试、可维护
|
||||
|
||||
---
|
||||
|
||||
## 2. 基本原则
|
||||
|
||||
### 2.1 写与读必须分离
|
||||
|
||||
- **Command**:会改变事实层
|
||||
- **Query**:只读,不改变事实层
|
||||
- **Tool**:面向 AI / CLI 的能力暴露层,内部映射到 Command / Query / Job
|
||||
|
||||
### 2.2 Tool 不是第三套业务逻辑
|
||||
|
||||
禁止出现:
|
||||
|
||||
- Web 走一套逻辑
|
||||
- CLI 走一套逻辑
|
||||
- AI Tool 再写一套逻辑
|
||||
|
||||
正确关系:
|
||||
|
||||
```text
|
||||
Tool -> Command / Query / Job -> Domain + Storage + EventLog
|
||||
```
|
||||
|
||||
### 2.3 每条写命令都要可审计
|
||||
|
||||
至少要记录:
|
||||
|
||||
- 谁发起
|
||||
- 从哪里发起
|
||||
- 改了什么
|
||||
- 为什么改
|
||||
- 影响了哪些对象
|
||||
- 是否成功
|
||||
|
||||
### 2.4 AI 默认只能调用系统工具
|
||||
|
||||
AI 不应默认获得:
|
||||
|
||||
- 任意 SQL
|
||||
- 任意文件写入
|
||||
- 任意 UI 按钮点击
|
||||
- 任意内部未定型方法
|
||||
|
||||
AI 应调用:
|
||||
|
||||
- 稳定的 Tool API
|
||||
- 明确的参数模型
|
||||
- 明确的权限模型
|
||||
|
||||
---
|
||||
|
||||
## 3. 三层协议面
|
||||
|
||||
```text
|
||||
Human UI / CLI / Agent Runtime
|
||||
│
|
||||
▼
|
||||
Tool API
|
||||
│
|
||||
┌───────┴────────┐
|
||||
▼ ▼
|
||||
Query API Command API
|
||||
│ │
|
||||
└───────┬────────┘
|
||||
▼
|
||||
Job / Workflow API
|
||||
│
|
||||
▼
|
||||
Rust Note Core
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- Tool API 是面向使用者的稳定能力面
|
||||
- Command / Query API 是内核的正式操作面
|
||||
- Job / Workflow API 负责长任务、异步流程、外部能力编排
|
||||
|
||||
---
|
||||
|
||||
## 4. Actor 模型
|
||||
|
||||
所有命令、查询、工具调用都应带 `actor`。
|
||||
|
||||
建议结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"actor": {
|
||||
"type": "human",
|
||||
"id": "user_123",
|
||||
"session_id": "ui_session_xxx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
或:
|
||||
|
||||
```json
|
||||
{
|
||||
"actor": {
|
||||
"type": "agent",
|
||||
"id": "agent_session_456",
|
||||
"provider": "openai",
|
||||
"model": "gpt-5.4-mini"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
或:
|
||||
|
||||
```json
|
||||
{
|
||||
"actor": {
|
||||
"type": "system",
|
||||
"id": "job_runner"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
用途:
|
||||
|
||||
- 审计
|
||||
- 权限判断
|
||||
- 限流
|
||||
- 错误追踪
|
||||
|
||||
---
|
||||
|
||||
## 5. Command API
|
||||
|
||||
## 5.1 通用结构
|
||||
|
||||
建议所有命令统一壳:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "insert_block",
|
||||
"command_id": "cmd_01",
|
||||
"idempotency_key": "idem_01",
|
||||
"actor": {
|
||||
"type": "agent",
|
||||
"id": "agent_session_1"
|
||||
},
|
||||
"source": {
|
||||
"channel": "tool",
|
||||
"client": "mcp"
|
||||
},
|
||||
"target": {
|
||||
"workspace_id": "ws_1",
|
||||
"page_id": "page_1"
|
||||
},
|
||||
"payload": {},
|
||||
"reason": "补充总结段落",
|
||||
"refs": ["ref_1"],
|
||||
"dry_run": false,
|
||||
"validate_only": false,
|
||||
"requested_at": "2026-04-11T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 通用返回
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"command_id": "cmd_01",
|
||||
"event_ids": ["evt_1", "evt_2"],
|
||||
"affected_objects": [
|
||||
{ "type": "block", "id": "block_123" }
|
||||
],
|
||||
"revision": {
|
||||
"workspace_id": "ws_1",
|
||||
"page_id": "page_1",
|
||||
"value": 42
|
||||
},
|
||||
"warnings": [],
|
||||
"rollback_hint": {
|
||||
"kind": "compensating_command",
|
||||
"command": "delete_block",
|
||||
"target_id": "block_123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 观测字段约束
|
||||
|
||||
从 task-034 开始,所有正式 `Command / Query / Tool / Job` 面都要默认带上并统一解释下面这些字段:
|
||||
|
||||
- `request_id`:一次入口请求级别的稳定编号,用于串起 route、runtime、日志和回查接口
|
||||
- `trace_id`:一次完整调用链的稳定编号,用于跨 query / command / tool / job 串联同一条执行路径
|
||||
- `command_id`:写命令的唯一编号,也是 `command_log_id`、`event_id` 推导的基础
|
||||
- `idempotency_key`:CLI、AI、Web 共享的幂等语义主键,不能三端各自解释
|
||||
- `command_log_id` / `event_id`:进入审计与回放链后的稳定对象编号,供 request/trace/command 回查与恢复命令复用
|
||||
|
||||
如果某条能力不能带出这组字段,它就还不能算“可供 CLI 与 AI 稳定运行的正式协议面”。
|
||||
|
||||
### 5.3 命令分类
|
||||
|
||||
#### 页面类
|
||||
- `create_page`
|
||||
- `rename_page`
|
||||
- `move_page`
|
||||
- `archive_page`
|
||||
- `restore_page`
|
||||
- `set_page_property`
|
||||
|
||||
#### 块类
|
||||
- `insert_block`
|
||||
- `update_block`
|
||||
- `delete_block`
|
||||
- `move_block`
|
||||
- `batch_apply_block_ops`
|
||||
- `replace_block_content`
|
||||
|
||||
#### 引用类
|
||||
- `create_reference`
|
||||
- `remove_reference`
|
||||
- `rebind_reference_anchor`
|
||||
|
||||
#### 资产类
|
||||
- `attach_asset`
|
||||
- `replace_asset_version`
|
||||
- `set_asset_metadata`
|
||||
- `extract_asset_outline`
|
||||
|
||||
#### 工作区 / 系统类
|
||||
- `create_task`
|
||||
- `cancel_task`
|
||||
- `rebuild_search_index`
|
||||
- `run_import_job`
|
||||
- `run_export_job`
|
||||
|
||||
---
|
||||
|
||||
## 6. Command 设计约束
|
||||
|
||||
### 6.1 命令应小而明确
|
||||
|
||||
优先:
|
||||
|
||||
- `insert_block`
|
||||
- `move_block`
|
||||
- `set_page_property`
|
||||
|
||||
避免:
|
||||
|
||||
- `save_everything`
|
||||
- `update_editor_state`
|
||||
- `mutate_page_with_ui_payload`
|
||||
|
||||
### 6.2 支持幂等键
|
||||
|
||||
AI 可能重试、网络可能重放,因此:
|
||||
|
||||
- 所有外部写命令建议支持 `idempotency_key`
|
||||
- 同一作用域内重复提交应可去重
|
||||
|
||||
### 6.3 支持 dry-run / validate-only
|
||||
|
||||
这样 AI 可以先问:
|
||||
|
||||
- 这个操作是否合法?
|
||||
- 会影响哪些对象?
|
||||
- 是否需要确认?
|
||||
|
||||
这对 Agent 很重要。
|
||||
|
||||
### 6.4 支持批量 ops,但要有边界
|
||||
|
||||
对于 Mindmap、结构调整、导入转换,可以提供:
|
||||
|
||||
- `batch_apply_block_ops`
|
||||
- `apply_mindmap_ops`
|
||||
- `apply_document_transform_ops`
|
||||
|
||||
但要求:
|
||||
|
||||
- ops 必须结构化
|
||||
- 单条失败策略明确
|
||||
- 返回逐条结果
|
||||
|
||||
---
|
||||
|
||||
## 7. Query API
|
||||
|
||||
## 7.1 通用结构
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "page.get",
|
||||
"actor": {
|
||||
"type": "human",
|
||||
"id": "user_1"
|
||||
},
|
||||
"scope": {
|
||||
"workspace_id": "ws_1"
|
||||
},
|
||||
"params": {
|
||||
"page_id": "page_1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 查询分类
|
||||
|
||||
#### 页面与块
|
||||
- `page.get`
|
||||
- `page.tree`
|
||||
- `page.list_children`
|
||||
- `block.get`
|
||||
- `block.subtree`
|
||||
|
||||
#### 检索
|
||||
- `search.text`
|
||||
- `search.reference`
|
||||
- `search.asset`
|
||||
- `search.page_by_title`
|
||||
|
||||
#### 资产
|
||||
- `asset.get`
|
||||
- `asset.list_versions`
|
||||
- `asset.resolve_anchor`
|
||||
|
||||
#### 日志与会话
|
||||
- `command_log.list`
|
||||
- `command_log.get`
|
||||
- `event.list`
|
||||
- `agent_session.get`
|
||||
- `task.list`
|
||||
|
||||
### 7.3 输出要求
|
||||
|
||||
- 输出稳定、字段可预期
|
||||
- 支持分页、游标、过滤
|
||||
- 避免直接返回前端组件所需临时形态
|
||||
- 对大型对象支持摘要与展开模式
|
||||
|
||||
---
|
||||
|
||||
## 8. Tool API
|
||||
|
||||
## 8.1 Tool API 的职责
|
||||
|
||||
Tool API 是给:
|
||||
|
||||
- AI Agent
|
||||
- CLI
|
||||
- 自动化脚本
|
||||
- 外部集成方
|
||||
|
||||
用的能力层。
|
||||
|
||||
它的原则是:
|
||||
|
||||
- 工具名清晰
|
||||
- 参数结构明确
|
||||
- 返回格式稳定
|
||||
- 明确确认策略与权限策略
|
||||
|
||||
## 8.2 工具命名风格
|
||||
|
||||
建议按领域分组:
|
||||
|
||||
- `note.read_page`
|
||||
- `note.list_children`
|
||||
- `note.insert_block`
|
||||
- `note.update_block`
|
||||
- `note.move_block`
|
||||
- `note.search`
|
||||
- `asset.attach_file`
|
||||
- `asset.extract_outline`
|
||||
- `office.get_selection`
|
||||
- `office.replace_selection`
|
||||
- `mindmap.apply_ops`
|
||||
- `system.get_recent_commands`
|
||||
|
||||
也可以对外映射为 MCP 风格扁平命名,但内核层建议保留层级语义。
|
||||
|
||||
## 8.3 Tool 与 Command/Query 的映射
|
||||
|
||||
例如:
|
||||
|
||||
- `note.read_page` -> `page.get`
|
||||
- `note.insert_block` -> `insert_block`
|
||||
- `note.search` -> `search.text`
|
||||
- `asset.attach_file` -> `attach_asset`
|
||||
- `mindmap.apply_ops` -> `apply_mindmap_ops`
|
||||
|
||||
### 8.4 Tool 返回
|
||||
|
||||
建议统一包含:
|
||||
|
||||
- `ok`
|
||||
- `data`
|
||||
- `warnings`
|
||||
- `requires_confirmation`
|
||||
- `confirmation_reason`
|
||||
- `next_actions`
|
||||
|
||||
例如:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"page_id": "page_1",
|
||||
"title": "项目计划"
|
||||
},
|
||||
"warnings": [],
|
||||
"requires_confirmation": false,
|
||||
"next_actions": []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Job / Workflow API
|
||||
|
||||
有些操作不是同步命令,而是长任务。
|
||||
|
||||
例如:
|
||||
|
||||
- 导入大型 PDF
|
||||
- OCR
|
||||
- 构建索引
|
||||
- 批量转换文档
|
||||
- Office 文件解析
|
||||
- 全库引用修复
|
||||
- AI 长链路整理
|
||||
|
||||
因此需要 `Job API`:
|
||||
|
||||
### 9.1 Job 通用结构
|
||||
|
||||
```json
|
||||
{
|
||||
"job": "import_document",
|
||||
"job_id": "job_1",
|
||||
"actor": { "type": "agent", "id": "agent_1" },
|
||||
"input": {
|
||||
"asset_id": "asset_1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 Job 状态
|
||||
|
||||
- `queued`
|
||||
- `running`
|
||||
- `waiting_confirmation`
|
||||
- `succeeded`
|
||||
- `failed`
|
||||
- `cancelled`
|
||||
|
||||
### 9.3 Job 输出
|
||||
|
||||
- 结果对象
|
||||
- 生成的 page / asset / reference
|
||||
- 日志摘要
|
||||
- 错误详情
|
||||
|
||||
---
|
||||
|
||||
## 10. Confirmation Policy
|
||||
|
||||
不是所有工具都应直接执行。
|
||||
|
||||
建议分级:
|
||||
|
||||
### 10.1 无需确认
|
||||
|
||||
- 只读查询
|
||||
- 本地摘要生成
|
||||
- 小范围插入草稿块
|
||||
|
||||
### 10.2 建议确认
|
||||
|
||||
- 批量删除
|
||||
- 批量重排
|
||||
- 覆盖资产版本
|
||||
- 导出到外部位置
|
||||
|
||||
### 10.3 强制确认
|
||||
|
||||
- 大范围页面删除
|
||||
- 全库重建/迁移
|
||||
- 覆盖性导入
|
||||
- 对外同步/发布
|
||||
|
||||
确认策略应由内核或 policy 层判断,不能完全由前端决定。
|
||||
|
||||
---
|
||||
|
||||
## 11. Error Model
|
||||
|
||||
错误必须结构化。
|
||||
|
||||
建议格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": "BLOCK_NOT_FOUND",
|
||||
"message": "目标块不存在",
|
||||
"details": {
|
||||
"block_id": "block_xxx"
|
||||
},
|
||||
"retryable": false,
|
||||
"suggested_action": "请先刷新页面树或重新读取块子树"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
分类建议:
|
||||
|
||||
- `VALIDATION_ERROR`
|
||||
- `PERMISSION_DENIED`
|
||||
- `NOT_FOUND`
|
||||
- `CONFLICT`
|
||||
- `STALE_REVISION`
|
||||
- `RATE_LIMITED`
|
||||
- `EXTERNAL_ADAPTER_ERROR`
|
||||
- `INTERNAL_ERROR`
|
||||
|
||||
这样 AI 才能根据错误做下一步,而不是只看到一段字符串。
|
||||
|
||||
---
|
||||
|
||||
## 12. Revision / Concurrency
|
||||
|
||||
### 12.1 为什么必须有 revision
|
||||
|
||||
AI 与人可能同时修改。
|
||||
|
||||
如果没有 revision:
|
||||
|
||||
- 很容易覆盖彼此内容
|
||||
- 工具重试会写乱
|
||||
- 无法做冲突判断
|
||||
|
||||
### 12.2 建议
|
||||
|
||||
- `Page` 级 revision
|
||||
- `Block` 级 revision
|
||||
- `AssetVersion` 级 version_no
|
||||
|
||||
命令可支持:
|
||||
|
||||
- `expected_revision`
|
||||
- `on_conflict` 策略(fail / merge_if_possible / create_patch)
|
||||
|
||||
---
|
||||
|
||||
## 13. OnlyOffice 协议边界
|
||||
|
||||
OnlyOffice 不应直接暴露为“让 AI 操控 iframe”。
|
||||
|
||||
建议拆成两层:
|
||||
|
||||
### 13.1 Office Adapter Query
|
||||
|
||||
- `office.get_active_asset`
|
||||
- `office.get_selection`
|
||||
- `office.get_current_page`
|
||||
- `office.list_bookmarks`
|
||||
|
||||
### 13.2 Office Adapter Command
|
||||
|
||||
- `office.insert_text`
|
||||
- `office.replace_selection`
|
||||
- `office.insert_comment`
|
||||
- `office.save_as_new_version`
|
||||
- `office.extract_outline`
|
||||
|
||||
原则:
|
||||
|
||||
- Tool 面暴露的是“Office 文档能力”
|
||||
- 不是浏览器点击坐标或 iframe DOM
|
||||
- 若插件不可用,应返回结构化不可用错误
|
||||
|
||||
---
|
||||
|
||||
## 14. Mindmap 协议边界
|
||||
|
||||
继续保留既有设计里最有价值的部分:`ops`。
|
||||
|
||||
建议:
|
||||
|
||||
- `mindmap.get`
|
||||
- `mindmap.apply_ops`
|
||||
- `mindmap.expand_node`
|
||||
- `mindmap.attach_refs`
|
||||
|
||||
其中 `mindmap.apply_ops` 入参可以继续沿用:
|
||||
|
||||
- `addChild`
|
||||
- `addSiblingAfter`
|
||||
- `updateText`
|
||||
- `setHyperlink`
|
||||
- `setRefs`
|
||||
- `appendNote`
|
||||
- `deleteNode`
|
||||
|
||||
但要求:
|
||||
|
||||
- 所有调用都写入统一命令日志
|
||||
- mindmap 不再走单独一套野生落盘体系
|
||||
|
||||
---
|
||||
|
||||
## 15. CLI 协议面
|
||||
|
||||
CLI 不是开发附属品,而是系统正式壳层。
|
||||
|
||||
建议 CLI 优先覆盖:
|
||||
|
||||
- `mnote page get <page-id>`
|
||||
- `mnote page tree`
|
||||
- `mnote block insert`
|
||||
- `mnote block move`
|
||||
- `mnote search text`
|
||||
- `mnote asset attach`
|
||||
- `mnote office selection`
|
||||
- `mnote job run import-document`
|
||||
- `mnote log recent`
|
||||
|
||||
CLI 应满足:
|
||||
|
||||
- 输出 JSON 模式
|
||||
- 退出码稳定
|
||||
- 适合 shell / AI / 自动化脚本调用
|
||||
|
||||
---
|
||||
|
||||
## 16. MCP / Agent Runtime 暴露面
|
||||
|
||||
如果后续提供 MCP:
|
||||
|
||||
- MCP 工具层应直接包装 Tool API
|
||||
- 不应重新发明一套独立业务逻辑
|
||||
- 返回值尽量和 CLI JSON 输出接近
|
||||
|
||||
这样好处是:
|
||||
|
||||
- Web agent、桌面 agent、本地 Hermes/Codex 都共用一套系统能力
|
||||
- 文档里只需维护一份工具协议
|
||||
|
||||
---
|
||||
|
||||
## 17. 最小 v0 清单
|
||||
|
||||
建议 v0 最先固化以下协议:
|
||||
|
||||
### Command
|
||||
- `create_page`
|
||||
- `rename_page`
|
||||
- `insert_block`
|
||||
- `update_block`
|
||||
- `delete_block`
|
||||
- `move_block`
|
||||
- `attach_asset`
|
||||
- `create_reference`
|
||||
|
||||
### Query
|
||||
- `page.get`
|
||||
- `page.tree`
|
||||
- `block.subtree`
|
||||
- `search.text`
|
||||
- `asset.get`
|
||||
- `command_log.list`
|
||||
|
||||
### Tool
|
||||
- `note.read_page`
|
||||
- `note.insert_block`
|
||||
- `note.update_block`
|
||||
- `note.move_block`
|
||||
- `note.search`
|
||||
- `asset.attach_file`
|
||||
- `system.get_recent_commands`
|
||||
|
||||
这套足以先打通:
|
||||
|
||||
- 人类基础编辑
|
||||
- CLI 基础操作
|
||||
- AI 基础介入
|
||||
- 审计与回放链路
|
||||
|
||||
---
|
||||
|
||||
## 18. 一句话结论
|
||||
|
||||
`mnote-rust` 的协议设计应坚持:
|
||||
|
||||
> **Command 负责改,Query 负责读,Tool 负责给 AI/CLI 用,Job 负责长流程;所有入口最终收敛到同一 Rust 内核,不再允许页面私有接口、编辑器私有接口、临时胶水接口长期并存。**
|
||||
@@ -0,0 +1,622 @@
|
||||
# 03. Storage / Event / Indexing v0
|
||||
|
||||
更新时间:2026-04-11
|
||||
适用范围:`/mnt/Data1T/mnote-rust`
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本文件定义 `mnote-rust` 在当前路线下的持久化、事件日志、索引体系。
|
||||
|
||||
它要回答:
|
||||
|
||||
- 当前系统事实层应落在哪里
|
||||
- Rust 内核怎样接入既有事实层而不重复造库
|
||||
- 写操作如何进入事件链路
|
||||
- 搜索、引用、RAG、回放、审计依赖什么数据面
|
||||
- 如何既保证性能,又保证 AI 可观测、可维护、可回放
|
||||
|
||||
这部分的前提必须先讲清:
|
||||
|
||||
> 对当前 `mnote` / `mnote-next` 主线来说,**Convex 仍是主事实层**。
|
||||
|
||||
因此,这份文档不再讨论“用 SQLite 取代 Convex 做主库”,而是讨论:
|
||||
|
||||
- 如何在 **Convex 主事实层** 之上建立 Rust 内核
|
||||
- 如何把索引、事件、导出、离线缓存、本地处理组织清楚
|
||||
- 如何避免再次引入第二套事实真相
|
||||
|
||||
如果这层设计不好,系统最终仍会退化成:
|
||||
|
||||
- 页面组件自己维护真相
|
||||
- 各类能力各写各的缓存
|
||||
- AI 看不到完整上下文
|
||||
- Rust 内核与现有系统各管一套数据
|
||||
- 回滚、审计、重建索引都变得困难
|
||||
|
||||
---
|
||||
|
||||
## 2. 总原则
|
||||
|
||||
### 2.1 主事实层、事件日志、索引必须分层
|
||||
|
||||
三者职责不同:
|
||||
|
||||
- **主事实层**:保存系统当前真相
|
||||
- **事件日志**:保存“如何变成现在”的过程
|
||||
- **索引层**:为检索、聚合、推荐、RAG 提供加速读模型
|
||||
|
||||
禁止让任一层越权:
|
||||
|
||||
- 不能拿索引当真相
|
||||
- 不能只靠事件流而没有可直接读取的当前状态
|
||||
- 不能让页面缓存成为事实层
|
||||
- 不能让本地缓存演化成第二主库
|
||||
|
||||
### 2.2 当前主事实层应继续落在 Convex
|
||||
|
||||
基于 `mnote` 与 `mnote-next` 已有主线,当前阶段应坚持:
|
||||
|
||||
- **Convex 是唯一主事实层**
|
||||
- Rust 内核通过协议与 bridge 接入 Convex,而不是绕开它再建一套主库
|
||||
- 旧仓与新仓共享的核心对象,应优先复用既有 Convex schema / deployment / caller 体系
|
||||
|
||||
这不是保守,而是避免推翻已经跑通的主链路。
|
||||
|
||||
### 2.3 本地文件系统与本地嵌入式存储仍然重要
|
||||
|
||||
虽然主事实层继续用 Convex,但本地层仍然需要:
|
||||
|
||||
- **文件系统**:资产原文件、导出文件、缓存、派生产物、临时工作目录
|
||||
- **可选本地嵌入式存储**:离线缓存、索引快照、dry-run、调试态数据、临时队列
|
||||
|
||||
但这些都不能升级为新的主事实层。
|
||||
|
||||
### 2.4 写入必须原子化到“主事实层 + 事件落账”
|
||||
|
||||
一次命令成功后,至少要保证:
|
||||
|
||||
- 主事实层写入完成
|
||||
- 事件写入成功
|
||||
- 命令日志有记录
|
||||
|
||||
索引允许异步追平,但必须可检测 lag。
|
||||
|
||||
### 2.5 索引必须可重建
|
||||
|
||||
搜索索引、向量索引、聚合视图都必须满足:
|
||||
|
||||
- 可从主事实层 + 事件重新构建
|
||||
- 可做全量重建
|
||||
- 可做增量追平
|
||||
|
||||
否则后期会不可维护。
|
||||
|
||||
---
|
||||
|
||||
## 3. 三层数据面
|
||||
|
||||
```text
|
||||
[主事实层]
|
||||
Convex + FileSystem
|
||||
|
||||
[事件与日志层]
|
||||
CommandLog + DomainEvent + JobLog + AuditTrail
|
||||
|
||||
[索引与派生层]
|
||||
FTS / Reference Graph / Backlink View / Outline View / Vector Index
|
||||
```
|
||||
|
||||
### 3.1 主事实层负责
|
||||
|
||||
- Workspace / Page / Block / Asset / Reference / Task / AgentSession 当前状态
|
||||
- 事务性写入
|
||||
- 读取当前真相
|
||||
- 提供稳定 schema
|
||||
- 承担权限与主对象约束
|
||||
|
||||
### 3.2 事件与日志层负责
|
||||
|
||||
- 记录命令与事件
|
||||
- 记录 actor、reason、scope、结果
|
||||
- 支持回放、审计、问题追踪
|
||||
- 作为索引增量更新输入
|
||||
|
||||
### 3.3 索引与派生层负责
|
||||
|
||||
- 全文搜索
|
||||
- 页面大纲
|
||||
- 反链
|
||||
- 资产片段搜索
|
||||
- RAG 检索读模型
|
||||
- 推荐 / 关系聚合
|
||||
|
||||
---
|
||||
|
||||
## 4. 主事实层选型
|
||||
|
||||
## 4.1 当前阶段的明确结论
|
||||
|
||||
推荐:
|
||||
|
||||
- **Convex**:结构化主事实层
|
||||
- **File System**:存原始资产与大对象
|
||||
- **Rust bridge / protocol layer**:作为统一命令、查询、工具接入面
|
||||
|
||||
原因:
|
||||
|
||||
- 与 `mnote` 中“Convex 已完全替换 Supabase”的现实一致
|
||||
- 与 `mnote-next` 中“默认复用现有 Convex deployment / project”的路线一致
|
||||
- 复用现有 deployment、schema、自建与 AI 开发经验,避免重建第二套数据库主线
|
||||
- 更符合“复用成熟能力,重构边界,不做无意义重建”的迁移原则
|
||||
|
||||
## 4.2 本地嵌入式存储的正确定位
|
||||
|
||||
可以保留本地嵌入式存储,但只限于:
|
||||
|
||||
- CLI / Agent 的离线缓存
|
||||
- 全文索引引擎的本地数据文件
|
||||
- dry-run / scaffold / 调试态数据
|
||||
- 单机导入处理中的临时工作库
|
||||
|
||||
不能把它写成:
|
||||
|
||||
- 第二套主事实层
|
||||
- 与 Convex 并列的正式写入真相
|
||||
- 长期双写的核心业务库
|
||||
|
||||
## 4.3 不建议的中心方案
|
||||
|
||||
- 用 SQLite 取代 Convex 成为主真相
|
||||
- OnlyOffice / 第三方编辑器内部状态作为主真相
|
||||
- 仅事件溯源、无当前态表
|
||||
- 单纯 Markdown 文件散落 + 大量 sidecar 作为唯一结构源
|
||||
- 长期维护 Convex + SQLite 双事实源
|
||||
|
||||
这些都会让系统再次陷入边界混乱。
|
||||
|
||||
---
|
||||
|
||||
## 5. 主事实对象结构建议
|
||||
|
||||
这里讨论的是**领域对象结构**,不是要求新建第二套数据库。
|
||||
|
||||
## 5.1 结构原则
|
||||
|
||||
- 主对象必须能映射到现有 Convex schema
|
||||
- 单对象一组稳定字段或有限关联表
|
||||
- 避免过度 EAV
|
||||
- JSON 仅用于局部扩展字段,不替代主 schema
|
||||
|
||||
## 5.2 建议主对象
|
||||
|
||||
至少包含:
|
||||
|
||||
- `workspaces`
|
||||
- `pages`
|
||||
- `page_versions`(可选)
|
||||
- `blocks`
|
||||
- `block_relations`(可选,若不全放在 block 表中)
|
||||
- `assets`
|
||||
- `asset_versions`
|
||||
- `references`
|
||||
- `tasks`
|
||||
- `agent_sessions`
|
||||
- `command_logs`
|
||||
- `domain_events`
|
||||
- `jobs`
|
||||
- `job_logs`
|
||||
|
||||
这些对象应优先映射或扩展到现有 Convex 主线,而不是在新仓先落一套平行本地表。
|
||||
|
||||
## 5.3 blocks 对象建议
|
||||
|
||||
关键字段:
|
||||
|
||||
- `block_id`
|
||||
- `workspace_id`
|
||||
- `page_id`
|
||||
- `parent_block_id`
|
||||
- `sort_key`
|
||||
- `block_type`
|
||||
- `content_json`
|
||||
- `props_json`
|
||||
- `annotations_json`
|
||||
- `revision`
|
||||
- `deleted_at`
|
||||
|
||||
说明:
|
||||
|
||||
- `sort_key` 建议支持稀疏排序,避免频繁全量重排
|
||||
- 软删除优于直接硬删,方便审计与恢复
|
||||
- `revision` 用于乐观并发控制
|
||||
|
||||
## 5.4 assets 对象建议
|
||||
|
||||
- `asset_id`
|
||||
- `workspace_id`
|
||||
- `asset_kind`
|
||||
- `mime_type`
|
||||
- `current_version_id`
|
||||
- `storage_strategy`
|
||||
- `status`
|
||||
- `metadata_json`
|
||||
|
||||
## 5.5 references 对象建议
|
||||
|
||||
- `reference_id`
|
||||
- `source_object_type`
|
||||
- `source_object_id`
|
||||
- `target_object_type`
|
||||
- `target_object_id`
|
||||
- `anchor_json`
|
||||
- `ref_kind`
|
||||
- `status`
|
||||
|
||||
---
|
||||
|
||||
## 6. 文件系统布局建议
|
||||
|
||||
资产不要全塞数据库 blob。
|
||||
|
||||
建议:
|
||||
|
||||
```text
|
||||
workspace-data/
|
||||
├─ assets/
|
||||
│ ├─ asset_xxx/
|
||||
│ │ ├─ v1/original.docx
|
||||
│ │ ├─ v2/original.docx
|
||||
│ │ ├─ extracted/outline.json
|
||||
│ │ ├─ extracted/text.md
|
||||
│ │ └─ derived/preview.png
|
||||
├─ indexes/
|
||||
│ ├─ fts/
|
||||
│ └─ vector/
|
||||
├─ runtime/
|
||||
│ ├─ jobs/
|
||||
│ ├─ sessions/
|
||||
│ └─ temp/
|
||||
└─ export/
|
||||
```
|
||||
|
||||
原则:
|
||||
|
||||
- 结构化真相仍以 Convex 为主
|
||||
- 原始大文件入文件系统
|
||||
- 派生产物有明确目录归属
|
||||
- 临时文件与正式资产分离
|
||||
|
||||
---
|
||||
|
||||
## 7. Command Log
|
||||
|
||||
`CommandLog` 是所有写命令的正式记录。
|
||||
|
||||
### 7.1 最低字段
|
||||
|
||||
- `command_log_id`
|
||||
- `command_name`
|
||||
- `actor_type`
|
||||
- `actor_id`
|
||||
- `source`
|
||||
- `workspace_id`
|
||||
- `target_objects`
|
||||
- `payload_summary`
|
||||
- `refs_json`
|
||||
- `idempotency_key`
|
||||
- `status`
|
||||
- `created_at`
|
||||
- `finished_at`
|
||||
|
||||
### 7.2 设计要求
|
||||
|
||||
- 能关联到一次真实主写入
|
||||
- 能关联后续 domain events
|
||||
- 能关联 tool call / job / rollback
|
||||
- 能为 AI 回放与人类审计提供最小闭包
|
||||
|
||||
### 7.3 与 Convex 的关系
|
||||
|
||||
命令日志可以:
|
||||
|
||||
- 直接进入 Convex 主线对象
|
||||
- 或通过 bridge 落到与主对象同一事实层
|
||||
|
||||
但不要单独把命令日志只记在本地、主对象却记在远端;那会破坏统一审计链路。
|
||||
|
||||
---
|
||||
|
||||
## 8. Domain Event
|
||||
|
||||
`DomainEvent` 不是为了炫技,而是为了:
|
||||
|
||||
- 给索引层提供标准增量输入
|
||||
- 给回放和审计提供结构化事件
|
||||
- 给异步任务提供稳定订阅源
|
||||
|
||||
### 8.1 事件最低字段
|
||||
|
||||
- `event_id`
|
||||
- `workspace_id`
|
||||
- `aggregate_type`
|
||||
- `aggregate_id`
|
||||
- `event_type`
|
||||
- `event_version`
|
||||
- `payload_json`
|
||||
- `command_log_id`
|
||||
- `actor_type`
|
||||
- `created_at`
|
||||
|
||||
### 8.2 事件边界
|
||||
|
||||
建议记录领域事件,而不是 UI 手势。
|
||||
|
||||
例如:
|
||||
|
||||
- `page_created`
|
||||
- `page_renamed`
|
||||
- `block_inserted`
|
||||
- `block_moved`
|
||||
- `block_content_replaced`
|
||||
- `asset_version_added`
|
||||
- `reference_created`
|
||||
- `task_status_changed`
|
||||
|
||||
不建议记录:
|
||||
|
||||
- 弹窗打开
|
||||
- 光标移动
|
||||
- hover 展示
|
||||
- 面板折叠
|
||||
|
||||
---
|
||||
|
||||
## 9. Job 与异步链路
|
||||
|
||||
不是所有事情都应进主事务。
|
||||
|
||||
应把这些放入 Job:
|
||||
|
||||
- OCR
|
||||
- OnlyOffice 提取/转换
|
||||
- 向量切片与嵌入
|
||||
- 全量重建索引
|
||||
- 大文件导入
|
||||
- 外部同步
|
||||
|
||||
### 9.1 Job 最低字段
|
||||
|
||||
- `job_id`
|
||||
- `job_type`
|
||||
- `workspace_id`
|
||||
- `target_objects`
|
||||
- `input_json`
|
||||
- `status`
|
||||
- `progress`
|
||||
- `error`
|
||||
- `created_at`
|
||||
- `started_at`
|
||||
- `finished_at`
|
||||
|
||||
### 9.2 不能放进主事务的内容
|
||||
|
||||
- 大模型推理
|
||||
- OCR
|
||||
- 大文件转换
|
||||
- 向量重建
|
||||
- 远程同步
|
||||
|
||||
这些必须走 Job。
|
||||
|
||||
---
|
||||
|
||||
## 10. 索引体系
|
||||
|
||||
## 10.1 Full Text Search
|
||||
|
||||
最低必做:
|
||||
|
||||
- 页面标题全文检索
|
||||
- 块内容全文检索
|
||||
- 资产提取文本全文检索
|
||||
- 引用 snippet 检索
|
||||
|
||||
建议:
|
||||
|
||||
- 初期直接用本地全文索引引擎(如 SQLite FTS5、Tantivy 或兼容方案)
|
||||
- 索引文档单位统一为“可定位对象”
|
||||
|
||||
例如:
|
||||
|
||||
- page
|
||||
- block
|
||||
- asset_fragment
|
||||
- reference_snippet
|
||||
|
||||
### 10.2 为什么块级索引重要
|
||||
|
||||
因为你的产品最终不是“整篇文档搜索”,而是:
|
||||
|
||||
- 找到某一段
|
||||
- 跳回块位置
|
||||
- 让 AI 只编辑局部
|
||||
- 给引用与上下文最小闭包
|
||||
|
||||
## 10.3 Reference Graph
|
||||
|
||||
必须维护引用图读模型:
|
||||
|
||||
- page -> page
|
||||
- block -> block
|
||||
- block -> asset fragment
|
||||
- task -> source object
|
||||
|
||||
这对:
|
||||
|
||||
- 反链
|
||||
- 影响面分析
|
||||
- AI 上下文组装
|
||||
- 知识导航
|
||||
|
||||
都很关键。
|
||||
|
||||
## 10.4 Outline View
|
||||
|
||||
页面大纲、Office 文档提取大纲、PDF 目录、Mindmap 树都应是派生读模型。
|
||||
|
||||
优点:
|
||||
|
||||
- 不污染主事实层
|
||||
- 允许多种提取策略
|
||||
- 容易重建
|
||||
|
||||
## 10.5 Vector Index
|
||||
|
||||
向量索引建议延后,但结构上预留。
|
||||
|
||||
原则:
|
||||
|
||||
- 向量索引只是检索加速层
|
||||
- 向量 chunk 必须能回指 `page_id / block_id / asset_id / anchor`
|
||||
- 向量索引失效可重建,不影响系统主真相
|
||||
|
||||
---
|
||||
|
||||
## 11. 索引更新策略
|
||||
|
||||
## 11.1 起步建议:异步增量 + 可全量重建
|
||||
|
||||
每次事件提交后:
|
||||
|
||||
- 生成索引任务
|
||||
- 按对象粒度增量更新
|
||||
- 记录 lag 与最后处理 event id
|
||||
|
||||
## 11.2 索引一致性要求
|
||||
|
||||
- 主事实层一致性 > 索引实时性
|
||||
- 查询可返回“索引处理中”状态
|
||||
- 对必须强一致的局部场景,可同步刷新小范围索引
|
||||
|
||||
## 11.3 失败恢复
|
||||
|
||||
索引 worker 挂了也不能影响主写入。
|
||||
|
||||
必须支持:
|
||||
|
||||
- 从 `last_processed_event_id` 继续追平
|
||||
- 指定对象重建
|
||||
- 指定 workspace 全量重建
|
||||
|
||||
### 11.4 回放与索引重建的正式入口
|
||||
|
||||
从 task-034 开始,回放和重建不再只是“库里有个纯函数”,而要成为统一命令面的一部分。
|
||||
|
||||
最小要求:
|
||||
|
||||
- `event_replay`:从统一事件列表读取,输出 replay 后的 `cursor` 与受影响 event 摘要
|
||||
- `index_rebuild`:基于 `last_processed_event_id` / `last_processed_at` 重建索引批次,并返回新的 cursor
|
||||
- 两者都复用同一套 `request_id`、`trace_id`、`workspace_id`、`command_id` 语义,不允许临时脚本私有解释
|
||||
|
||||
这意味着后续无论是 CLI、AI 还是 Web 排障,都应先调用同一条 Rust 恢复命令面,而不是各自写一套索引补数脚本。
|
||||
|
||||
---
|
||||
|
||||
## 12. AI 可观测性
|
||||
|
||||
AI 全流程介入后,系统必须能回答:
|
||||
|
||||
- 某次回答引用了哪些对象
|
||||
- 某次编辑基于哪些上下文
|
||||
- 某个工具为什么失败
|
||||
- 当前搜索索引是否滞后
|
||||
- 哪些对象正在被长任务处理
|
||||
|
||||
因此建议额外维护:
|
||||
|
||||
### 12.1 AgentSession 关联表
|
||||
|
||||
- `agent_session_objects`
|
||||
- `agent_session_tool_calls`
|
||||
- `agent_session_outputs`
|
||||
|
||||
### 12.2 Tool Call Log
|
||||
|
||||
记录:
|
||||
|
||||
- tool name
|
||||
- input
|
||||
- output summary
|
||||
- duration
|
||||
- error
|
||||
- command_log_id / query trace id
|
||||
|
||||
这样后期 AI 排障、回放、评估都会容易很多。
|
||||
|
||||
---
|
||||
|
||||
## 13. 性能关键点
|
||||
|
||||
### 13.1 不要整页全文重写
|
||||
|
||||
编辑块时,只更新相关块与局部派生视图。
|
||||
|
||||
### 13.2 不要同步重建全索引
|
||||
|
||||
任何全库重建都必须后台化。
|
||||
|
||||
### 13.3 资产处理走异步
|
||||
|
||||
OCR、Office 转换、预览图生成、向量切片都不能阻塞主编辑事务。
|
||||
|
||||
### 13.4 读写模型分离
|
||||
|
||||
高频 UI 读需求可通过派生视图优化,不要污染主事实 schema。
|
||||
|
||||
---
|
||||
|
||||
## 14. OnlyOffice 在存储层的正确位置
|
||||
|
||||
OnlyOffice 相关对象建议这样落地:
|
||||
|
||||
- 原始 `docx/xlsx/pptx/pdf`:作为 `Asset` + `AssetVersion`
|
||||
- 文档编辑结果:生成新 `AssetVersion`
|
||||
- 提取结构(目录、批注、书签、文本片段):作为派生读模型或 `Reference`
|
||||
- 当前选区、当前页码:作为短生命周期 `SelectionAnchor`
|
||||
|
||||
不要把:
|
||||
|
||||
- OnlyOffice 内部文档状态
|
||||
- 插件瞬时 UI 状态
|
||||
- callback 的临时 payload
|
||||
|
||||
直接当成主事实层。
|
||||
|
||||
---
|
||||
|
||||
## 15. 迁移建议
|
||||
|
||||
先做:
|
||||
|
||||
1. 盘点现有 Convex schema 与 `mnote-rust` 领域对象的映射
|
||||
2. 落 `CommandLog / DomainEvent` 的主事实层方案
|
||||
3. 落 `Page / Block / Asset / Reference` 的 bridge / repo 接口
|
||||
4. 建立全文索引最小实现
|
||||
5. 建立索引 worker 骨架
|
||||
|
||||
再做:
|
||||
|
||||
6. AgentSession / ToolCallLog
|
||||
7. Outline / Backlink 读模型
|
||||
8. 向量索引占位
|
||||
9. Office / OCR / Mindmap 派生索引
|
||||
10. 必要的离线缓存 / dry-run 本地层
|
||||
|
||||
---
|
||||
|
||||
## 16. 一句话结论
|
||||
|
||||
`mnote-rust` 当前应采用 **Convex 主事实层 + FileSystem 资产存储 + CommandLog/DomainEvent 事件链路 + 可重建全文/引用/向量索引** 的分层结构;
|
||||
Rust 内核负责把命令、查询、工具、索引、异步任务与外部能力重新组织清楚,而不是再造一套与现有主线并列的数据库事实源。
|
||||
@@ -0,0 +1,491 @@
|
||||
# 04. OnlyOffice Integration Boundary v0
|
||||
|
||||
更新时间:2026-04-11
|
||||
适用范围:`/mnt/Data1T/mnote-rust`
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本文件定义 `mnote-rust` 中 OnlyOffice 的正式边界。
|
||||
|
||||
它要解决的问题:
|
||||
|
||||
- OnlyOffice 在系统里到底是什么
|
||||
- 可以接多深
|
||||
- 哪些能力该放进插件/编辑器侧
|
||||
- 哪些能力必须留在 Rust 内核
|
||||
- 官方现状已经支持到哪一步,哪些不是猜想
|
||||
|
||||
---
|
||||
|
||||
## 2. 基于官方资料的确定事实
|
||||
|
||||
以下判断基于 ONLYOFFICE 官方文档与官方 API 文档,而不是推测。
|
||||
|
||||
### 2.1 ONLYOFFICE 已有官方 AI 插件能力
|
||||
|
||||
官方文档已明确:
|
||||
|
||||
- ONLYOFFICE 提供 **AI 插件**
|
||||
- 可接入多种模型提供商,例如 **OpenAI、DeepSeek**
|
||||
- 支持的能力包括但不限于:
|
||||
- 文本生成
|
||||
- 文本编辑
|
||||
- 总结
|
||||
- 自动创建宏
|
||||
- 面向编辑器的 AI 辅助操作
|
||||
|
||||
这说明:
|
||||
|
||||
> OnlyOffice 不是一个“完全不懂 AI 的富文档编辑器”,而是已经具备官方 AI 扩展体系。
|
||||
|
||||
### 2.2 ONLYOFFICE 已有插件系统
|
||||
|
||||
官方文档明确支持:
|
||||
|
||||
- 自定义插件
|
||||
- 插件 UI 集成
|
||||
- 插件调用外部服务
|
||||
- 插件与编辑器文档内容交互
|
||||
- 插件注册、安装、配置与运行
|
||||
|
||||
这说明后续你完全可以:
|
||||
|
||||
- 做自有 AI 插件
|
||||
- 做面向 mnote-rust 的桥接插件
|
||||
- 把系统命令面引入编辑器环境
|
||||
|
||||
### 2.3 Office JS API 已能做深度文档操作
|
||||
|
||||
官方 API 文档与样例表明,插件/脚本可进行:
|
||||
|
||||
- 获取文档对象 `Api.GetDocument()`
|
||||
- 获取当前选区/范围
|
||||
- 获取文本内容
|
||||
- 插入文本
|
||||
- 替换内容
|
||||
- 创建段落
|
||||
- 插入内容控件
|
||||
- 处理表格、图片、表单等对象
|
||||
|
||||
这意味着:
|
||||
|
||||
> “AI 在 OnlyOffice 内获取当前选区并改写当前文档片段”这条能力链,官方已经支持。
|
||||
|
||||
### 2.4 ONLYOFFICE 支持自建部署与集成
|
||||
|
||||
官方文档明确把 ONLYOFFICE Docs 作为可集成到自有系统中的 office suite / Docs API / 插件宿主来提供。
|
||||
|
||||
结合你当前仓库里已有:
|
||||
|
||||
- Docker 自建 `onlyoffice/documentserver`
|
||||
- callback / proxy / 外网地址改写
|
||||
- 前端侧 OnlyOffice 工具桥
|
||||
|
||||
可以判断:
|
||||
|
||||
> 自建部署这条路是成立的,而且值得继续做。
|
||||
|
||||
---
|
||||
|
||||
## 3. 由这些事实推出的边界判断
|
||||
|
||||
## 3.1 能做深集成,但不能反客为主
|
||||
|
||||
结论:
|
||||
|
||||
- **OnlyOffice 适合深集成**
|
||||
- **OnlyOffice 不适合做系统主内核**
|
||||
|
||||
原因不是它能力不够,而是职责不同。
|
||||
|
||||
OnlyOffice 擅长:
|
||||
|
||||
- 编辑 office 文档
|
||||
- 处理复杂版式文档
|
||||
- 承载编辑器插件与文档内 AI
|
||||
- 管理编辑器态的操作命令
|
||||
|
||||
但它不擅长天然承担:
|
||||
|
||||
- 笔记块树真相
|
||||
- 全局页面树真相
|
||||
- 跨页面引用网络真相
|
||||
- AI 全局任务编排
|
||||
- 统一命令总线
|
||||
- 全系统事件日志中心
|
||||
|
||||
## 3.2 在 mnote-rust 中的正式定位
|
||||
|
||||
OnlyOffice 应定位为:
|
||||
|
||||
> **Office Asset Editor Adapter + Office AI Subsystem**
|
||||
|
||||
而不是:
|
||||
|
||||
- 主页面编辑器内核
|
||||
- 主数据模型
|
||||
- 主命令入口
|
||||
- 主任务系统
|
||||
|
||||
---
|
||||
|
||||
## 4. 正式职责划分
|
||||
|
||||
## 4.1 OnlyOffice 负责什么
|
||||
|
||||
### A. Office 资产编辑
|
||||
|
||||
负责编辑:
|
||||
|
||||
- `docx`
|
||||
- `xlsx`
|
||||
- `pptx`
|
||||
- 部分 `pdf` 相关工作流
|
||||
|
||||
### B. 编辑器内局部 AI 能力
|
||||
|
||||
例如:
|
||||
|
||||
- 基于当前选区总结
|
||||
- 重写/润色选区文本
|
||||
- 插入生成内容
|
||||
- 提取 action items
|
||||
- 生成标题/批注/宏
|
||||
|
||||
### C. 编辑器上下文采集
|
||||
|
||||
例如:
|
||||
|
||||
- 当前文档 ID
|
||||
- 当前资产版本 ID
|
||||
- 当前选区
|
||||
- 当前页/书签/活动对象
|
||||
- 编辑器保存状态
|
||||
|
||||
### D. 将编辑结果回写主系统
|
||||
|
||||
例如:
|
||||
|
||||
- 保存为新 `AssetVersion`
|
||||
- 产出书签/目录/片段锚点
|
||||
- 触发提取文本与索引任务
|
||||
|
||||
## 4.2 Rust 内核负责什么
|
||||
|
||||
### A. 主事实层
|
||||
|
||||
- Workspace
|
||||
- Page
|
||||
- Block
|
||||
- Asset
|
||||
- AssetVersion
|
||||
- Reference
|
||||
- Task
|
||||
- Event
|
||||
- CommandLog
|
||||
|
||||
### B. 系统级命令与权限
|
||||
|
||||
- 谁可以编辑哪个资产
|
||||
- 谁可以覆盖哪个版本
|
||||
- 哪些操作需要确认
|
||||
- 哪些任务需要后台执行
|
||||
|
||||
### C. 全局 AI 调度
|
||||
|
||||
- Agent Session
|
||||
- Tool Registry
|
||||
- CLI / MCP / Tool API
|
||||
- 长任务编排
|
||||
- 审计与回放
|
||||
|
||||
### D. 跨资产与跨页面知识组织
|
||||
|
||||
- 引用图
|
||||
- 搜索索引
|
||||
- 反链
|
||||
- RAG
|
||||
- 页面/块/资产统一导航
|
||||
|
||||
---
|
||||
|
||||
## 5. 最推荐的集成方式
|
||||
|
||||
## 5.1 推荐结构
|
||||
|
||||
```text
|
||||
AI / CLI / Web / Desktop
|
||||
│
|
||||
▼
|
||||
Rust Command / Query / Tool API
|
||||
│
|
||||
├─ Asset Service
|
||||
├─ Agent Service
|
||||
├─ Search / Reference Service
|
||||
└─ OnlyOffice Adapter Service
|
||||
│
|
||||
├─ Docs API
|
||||
├─ Callback Handler
|
||||
├─ Plugin Bridge
|
||||
└─ Selection / Save / Anchor Bridge
|
||||
```
|
||||
|
||||
核心含义:
|
||||
|
||||
- 所有人和 AI 都先找 Rust 内核
|
||||
- Rust 内核再协调 OnlyOffice
|
||||
- OnlyOffice 不是上帝,只是一个能力域
|
||||
|
||||
## 5.2 不推荐结构
|
||||
|
||||
```text
|
||||
AI -> OnlyOffice Plugin -> 直接改系统数据库/页面状态
|
||||
```
|
||||
|
||||
或者:
|
||||
|
||||
```text
|
||||
Web 页面逻辑 -> 直接控制 OnlyOffice -> 顺手写一部分业务真相
|
||||
```
|
||||
|
||||
这种结构会再次回到胶水冲突。
|
||||
|
||||
---
|
||||
|
||||
## 6. 官方 AI 能力在新系统中的用法
|
||||
|
||||
## 6.1 可以直接利用的部分
|
||||
|
||||
可以利用官方插件/Office API 做:
|
||||
|
||||
- `office.get_selection`
|
||||
- `office.get_document_text`
|
||||
- `office.replace_selection`
|
||||
- `office.insert_after_selection`
|
||||
- `office.create_comment_from_ai`
|
||||
- `office.generate_outline`
|
||||
- `office.extract_action_items`
|
||||
|
||||
这些能力适合做成:
|
||||
|
||||
- 插件内命令
|
||||
- 本地桥接命令
|
||||
- Tool API 的 office 子域工具
|
||||
|
||||
## 6.2 不应依赖的部分
|
||||
|
||||
不应把以下能力外包给 OnlyOffice:
|
||||
|
||||
- 页面树管理
|
||||
- 笔记块模型
|
||||
- 全局知识链接
|
||||
- Agent 全局工作记忆
|
||||
- 系统统一任务编排
|
||||
- 跨类型对象权限控制
|
||||
|
||||
---
|
||||
|
||||
## 7. 推荐的 Tool API 分工
|
||||
|
||||
## 7.1 Rust 内核对外工具
|
||||
|
||||
例如:
|
||||
|
||||
- `asset.open_in_office`
|
||||
- `asset.list_versions`
|
||||
- `asset.commit_new_version`
|
||||
- `reference.resolve_anchor`
|
||||
- `office.create_edit_session`
|
||||
- `office.get_capabilities`
|
||||
|
||||
## 7.2 OnlyOffice 插件桥工具
|
||||
|
||||
例如:
|
||||
|
||||
- `office_plugin.get_selection`
|
||||
- `office_plugin.get_document_text`
|
||||
- `office_plugin.replace_selection`
|
||||
- `office_plugin.insert_text`
|
||||
- `office_plugin.get_outline`
|
||||
- `office_plugin.list_bookmarks`
|
||||
|
||||
## 7.3 正确的调用链
|
||||
|
||||
```text
|
||||
Agent / CLI
|
||||
-> note/asset/office Tool API
|
||||
-> Rust 内核
|
||||
-> OnlyOffice Adapter / Plugin Bridge
|
||||
-> Office JS API / Docs API
|
||||
-> 返回结构化结果
|
||||
```
|
||||
|
||||
而不是:
|
||||
|
||||
```text
|
||||
Agent 直接知道 OnlyOffice 内部细节并自行编排保存流程
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 版本回写策略
|
||||
|
||||
这是边界里最关键的一块。
|
||||
|
||||
### 8.1 正确做法
|
||||
|
||||
OnlyOffice 编辑完成后:
|
||||
|
||||
- 生成新的 `AssetVersion`
|
||||
- 更新 `assets.current_version_id`
|
||||
- 写 `command_log` / `event`
|
||||
- 触发:
|
||||
- 文本提取
|
||||
- 大纲提取
|
||||
- 锚点更新
|
||||
- 索引更新
|
||||
|
||||
### 8.2 不正确做法
|
||||
|
||||
- 只在编辑器里“看起来改了”但系统无版本记录
|
||||
- callback 到了就直接覆盖文件,不产生日志
|
||||
- 直接把插件临时状态写成业务真相
|
||||
|
||||
---
|
||||
|
||||
## 9. Anchor / 选区 / 书签策略
|
||||
|
||||
OnlyOffice 的一个真正价值,是它能提供相对更强的文档内部定位。
|
||||
|
||||
建议区分两类定位:
|
||||
|
||||
### 9.1 短期编辑上下文
|
||||
|
||||
例如:
|
||||
|
||||
- 当前选区
|
||||
- 当前光标
|
||||
- 当前页
|
||||
- 当前激活对象
|
||||
|
||||
这类只作为:
|
||||
|
||||
- `SelectionAnchor`
|
||||
- AgentSession 上下文
|
||||
- Tool 输入
|
||||
|
||||
### 9.2 长期可引用锚点
|
||||
|
||||
例如:
|
||||
|
||||
- bookmark
|
||||
- 标题路径
|
||||
- 批注 id
|
||||
- 内容控件 id
|
||||
- 提取片段 hash
|
||||
|
||||
这类才适合进正式 `Reference.anchor`。
|
||||
|
||||
原则:
|
||||
|
||||
- 选区是瞬时上下文
|
||||
- 书签/内容控件/稳定片段才是长期引用对象
|
||||
|
||||
---
|
||||
|
||||
## 10. 插件开发建议
|
||||
|
||||
## 10.1 建议做自有插件桥
|
||||
|
||||
理由:
|
||||
|
||||
- 官方插件体系已经成熟到可用
|
||||
- 你需要的是“把 OnlyOffice 接进自己的命令系统”
|
||||
- 自有桥插件比零散页面 hack 更稳
|
||||
|
||||
## 10.2 插件职责建议
|
||||
|
||||
插件只做:
|
||||
|
||||
- 获取编辑器上下文
|
||||
- 执行局部编辑命令
|
||||
- 请求 mnote-rust Tool API
|
||||
- 接收结构化结果并落到文档
|
||||
- 回传选择/书签/状态
|
||||
|
||||
插件不要做:
|
||||
|
||||
- 自己维护主业务状态
|
||||
- 自己做全局权限判断
|
||||
- 自己做全局任务调度
|
||||
- 自己缓存长期知识图谱
|
||||
|
||||
---
|
||||
|
||||
## 11. 源码部署 / 自建部署的实际建议
|
||||
|
||||
## 11.1 结论
|
||||
|
||||
**值得继续推进自建部署。**
|
||||
|
||||
## 11.2 原因
|
||||
|
||||
- 网络、callback、鉴权策略可控
|
||||
- 便于把插件、配置、回调、存储策略收归自己管理
|
||||
- 更方便未来与桌面版、本地模式、离线模式衔接
|
||||
- 减少外部部署差异导致的集成复杂度
|
||||
|
||||
## 11.3 但不建议的误区
|
||||
|
||||
- 不要把“自建部署”误解为“去重写编辑器引擎”
|
||||
- 不要把“插件可改内容”误解为“OnlyOffice 可以替代整个笔记内核”
|
||||
- 不要把“官方已有 AI 插件”误解为“你的 Agent 架构可以省掉”
|
||||
|
||||
---
|
||||
|
||||
## 12. 对 mnote-rust 的直接影响
|
||||
|
||||
这份结论会直接影响后续架构文档:
|
||||
|
||||
1. `office.*` 作为独立工具域存在
|
||||
2. `AssetVersion` 必须是一等对象
|
||||
3. `Reference.anchor` 必须支持 office bookmark / content control / text quote
|
||||
4. `SelectionAnchor` 只做短期上下文,不升格为主事实
|
||||
5. Agent 与 OnlyOffice 的关系必须经由 Rust Tool API,而不是页面硬连
|
||||
|
||||
---
|
||||
|
||||
## 13. 最小落地路线
|
||||
|
||||
### Phase A
|
||||
|
||||
- 自建 ONLYOFFICE Docs 持续使用
|
||||
- 补正式 adapter 文档
|
||||
- 补 `AssetVersion` / callback / save 流程
|
||||
|
||||
### Phase B
|
||||
|
||||
- 做自有桥插件
|
||||
- 打通选区读取、文本替换、结构化命令执行
|
||||
- 统一插件与系统鉴权
|
||||
|
||||
### Phase C
|
||||
|
||||
- 把 office 工具域接入统一 Tool API / CLI / Agent Runtime
|
||||
- 让 AI 通过系统命令操作 Office 资产
|
||||
- 建立锚点、版本、索引、审计闭环
|
||||
|
||||
### Phase D
|
||||
|
||||
- 再评估是否要更深度定制 UI / 插件体验
|
||||
- 但不进入“重写 OnlyOffice 内核”路线
|
||||
|
||||
---
|
||||
|
||||
## 14. 一句话结论
|
||||
|
||||
基于 ONLYOFFICE 官方当前能力,最合理的路线不是回避它,也不是围绕它建系统,而是:
|
||||
|
||||
> **把 ONLYOFFICE 作为可自建、可插件化、可 AI 深接入的 Office 资产编辑子系统;同时把主事实层、命令层、日志层、任务层牢牢放在 Rust 内核里。**
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": []
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB |
Reference in New Issue
Block a user