Files
mnote/design/03-rust-web/process/3-9-rust-web-3000-tree-editor-runtime-integration-plan-v1.md
T

297 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 3-9 [process] Rust Web 3000 Tree / Editor Runtime Integration Plan v1
> 更新时间:2026-04-29
>
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:**`3000` 不只拥有 Wolai 视觉外壳,而是真正接入 Rust kernel/sidebar projection、页面树、Page Aggregate 与 `leptos-tiptap` 主编辑 island。
**Architecture:** `mnote-web` 继续持有 `3000` gateway 和 server-first shell;树、页面结构、排序、选中态来自 Rust kernel projection / Page Aggregate,不在 UI 层重新造一份静态真相。编辑区由 Rust shell 输出稳定 bootstrap contract,前端只作为 `leptos-tiptap` island runtime 与必要 bundle source,不能把 Next App Router 恢复为主壳,也不能把 BlockNote 写回默认主编辑器。
**Tech Stack:** Rust workspace、`mnote-web`、Axum、Leptos SSR、Page Aggregate、Rust tree projection、Convex substrate、`leptos-tiptap` / ProseMirror island、Node/Playwright smoke、Cargo tests、Vitest。
---
## 1. 当前事实基线
本计划基于 2026-04-29 对 `3000`、截图 `/mnt/Data1T/mnote/tmp/image copy 32.png` 和当前 DOM 的检查:
- `3000` 当前已经由 `mnote-web` 持有,根页返回 `x-mnote-web-owner: mnote-web`
- 根页已经有 Wolai 风格外壳:左栏、顶栏、右下浮动按钮、`data-mnote-shell="workspace"`
- 左栏 `我的页面` section 当前没有真实页面树行;页面列表出现在正文中,是 `/mnt/Data1T/mnote/design/design/html` 参考 UI 的静态内容形态。
- 根页正文仍是 `mnote-home-links` 静态链接列表,不能证明 active page、Page Aggregate 或页面树选中态已经接入。
- `/documents/:id` 当前只证明 SSR shell 包含 `data-editor-host="leptos_tiptap_island"``#mnote-editor-island` 占位;还没有证明浏览器里出现 `data-testid="mnote-leptos-tiptap-island-editor-root"``.ProseMirror[contenteditable="true"]` 或保存回路。
- `scripts/task115-rust-web-document-shell-smoke.js` 目前只检查 HTML marker 和 Page Aggregate JSON,不检查 editor island hydration。
- `scripts/task119-rust-web-wolai-visual-regression-smoke.js` 目前只检查视觉布局,不检查真实树数据或编辑器运行态。
结论:`3-7/3-8` 完成的是 **Rust-owned Wolai shell parity**;下一阶段必须单独收口 **tree/editor runtime parity**
## 2. 边界原则
- `3000` 继续是唯一公开入口;`3100` 只作为 legacy/reference/debug。
- 不把 Next App Router 恢复为主路由;如临时使用 Next 产物,只能作为显式 island bundle source。
- 不在 Rust SSR 中硬编码“个人 / 正版软件备份 / 杂记”等设计稿页面列表作为真实数据。
- Sidebar / 页面树 / active selection 必须来自 `sidebar_tree``page_tree` 或 Page Aggregate 的稳定 projection。
- 文档正文必须由 Page Aggregate 和 `leptos-tiptap` island 共同驱动;仅有空 `<section id="mnote-editor-island">` 不算完成。
- 保存、重命名、新建、删除、页面切换的 smoke 要使用当前 `3000`,不能只依赖 fixture-only 通过。
## 3. 文件结构
### Rust Web shell 与 projection
- `rust/crates/mnote-web/src/workspace_shell.rs`
- 收紧 `WorkspaceShellProjection`:真实 `myPageItems``starredItems`、active row、空状态和 fallback 原因都可观测。
- `rust/crates/mnote-web/src/routes/gateway.rs`
- 根路径选择 active page`pageId` 查询参数 > recent cookie > projection active > 第一个真实页面 > 空 workspace。
- 根路径不再展示静态设计稿页面列表作为伪内容。
- `rust/crates/mnote-web/src/routes/web_shell.rs`
- 统一加载 workspace sidebar、page tree、Page Aggregate 和 editor bootstrap contract。
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
- 侧栏只渲染 projection rows;补 `data-testid="wolai-sidebar-row"``data-node-id``data-active` 等稳定验收点。
- `rust/crates/mnote-web/src/ssr/pages/home.rs`
- workspace home 只在没有 active page 时渲染空状态或真实页面入口,不再承担主编辑区。
- `rust/crates/mnote-web/src/ssr/pages/document.rs`
- 文档页输出 editor bootstrap、Page Aggregate snapshot、真实编辑器 mount root 和 fallback/error observability。
### Editor island runtime
- `wolai-frontend/src/components/editor/editor-host.tsx`
- 继续保持 `leptos_tiptap_island` 为默认 host。
- `wolai-frontend/src/components/editor/leptos-tiptap-island-editor-host.tsx`
- 作为 Rust shell 的 island runtime 入口复用现有初始化、保存与观测属性。
- `wolai-frontend/src/components/editor/document-content.tsx`
- 如需复用,只抽出最小 bootstrap/hydration adapter,不把完整 Next 文档页壳带回主路径。
- `wolai-frontend/src/lib/documents/page-aggregate-loader.ts` / `page-aggregate-builder.ts`
- 确认 Rust shell 内嵌 Page Aggregate 与前端类型一致。
### Smoke / 验收
- `scripts/task120-rust-web-tree-integration-smoke.js`
- 新增真实树接入 smoke:在 `3000` 创建临时页面和子页面,断言左栏 / 页面树出现真实 rows、active selection、点击打开、清理成功。
- `scripts/task121-rust-web-editor-island-hydration-smoke.js`
- 新增编辑器接入 smoke:打开当前 `3000` 文档页,断言 `leptos-tiptap` root、`.ProseMirror[contenteditable="true"]`、Page Aggregate bootstrap、输入保存和 reload 后内容一致。
- `scripts/task119-rust-web-wolai-visual-regression-smoke.js`
- 保留视觉布局检查,增加负向断言:不能只靠 `.mnote-home-links` 静态设计列表通过。
## 4. 实施任务
### Task E-0:冻结真实树和编辑器失败用例
**Files:**
- Create: `scripts/task120-rust-web-tree-integration-smoke.js`
- Create: `scripts/task121-rust-web-editor-island-hydration-smoke.js`
- Modify: `scripts/task119-rust-web-wolai-visual-regression-smoke.js`
- [ ] **Step 1: 写树接入 smoke**
`task120` 应执行以下链路:
```text
POST /api/tree/commands action=create title=task120-root
POST /api/tree/commands action=create parentId=<root> title=task120-child
GET /
断言 [data-testid="wolai-sidebar-row"][data-node-id=<root>] 存在
断言 [data-testid="wolai-sidebar-row"][data-node-id=<child>] 存在或可展开后存在
点击 root row,断言 URL 进入 /documents/<root>
断言 active row 的 data-active="true"
POST /api/tree/commands action=purge 清理 child/root
```
Expected before implementation: FAIL,当前 `我的页面` 没有真实 row。
- [ ] **Step 2: 写编辑器 hydration smoke**
`task121` 应执行以下链路:
```text
创建临时文档
打开 /documents/<id>?workspaceId=<workspace>
等待 [data-testid="mnote-leptos-tiptap-island-editor-root"]
等待 .editor-surface .ProseMirror[contenteditable="true"]
输入唯一文本
触发保存或等待自动保存
重新加载同一页面
断言 ProseMirror 或 Page Aggregate body 含唯一文本
清理临时文档
```
Expected before implementation: FAIL,当前只有 `#mnote-editor-island` 占位。
- [ ] **Step 3: 收紧视觉 smoke 的负向断言**
`task119` 中增加:根页不应只出现 `.mnote-home-links` 静态设计列表作为“主编辑区”;若有 active page,必须能进入 document shell 或展示真实 Page Aggregate 标题。
### Task E-1:让 workspace sidebar 渲染真实 projection rows
**Files:**
- Modify: `rust/crates/mnote-web/src/workspace_shell.rs`
- Modify: `rust/crates/mnote-web/src/ssr/pages/layout.rs`
- Test: `rust/crates/mnote-web/src/workspace_shell.rs`
- [ ] **Step 1: 补 projection 到 DOM 的单元测试**
测试输入包含 root/child 两个文档,期望输出:
```html
<a data-testid="wolai-sidebar-row" data-node-id="doc_root" data-active="true" href="/documents/doc_root?workspaceId=ws_demo">Root</a>
<a data-testid="wolai-sidebar-row" data-node-id="doc_child" data-parent-id="doc_root" href="/documents/doc_child?workspaceId=ws_demo">Child</a>
```
- [ ] **Step 2: 实现真实 rows 渲染**
`render_workspace_shell_sidebar_html` 只能从 `WorkspaceShellProjection.my_page_items` / `starred_items` / `sidebar_tree_html` 渲染,不再把设计稿列表注入正文或左栏。
- [ ] **Step 3: 验证**
Run:
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web workspace_shell
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task120-rust-web-tree-integration-smoke.js
```
### Task E-2:把根路径从静态首页切到 active page / empty workspace
**Files:**
- Modify: `rust/crates/mnote-web/src/routes/gateway.rs`
- Modify: `rust/crates/mnote-web/src/ssr/pages/home.rs`
- Test: `rust/crates/mnote-web/src/routes/gateway.rs`
- [ ] **Step 1: 固定 root selection 测试**
覆盖 `pageId`、recent cookie、projection active、第一个真实页面、空 workspace 五种顺序。
- [ ] **Step 2: 改根路径行为**
有 active page 时,根路径可以 server-side render 当前 page shell,也可以 302/303 到 `/documents/:id`;二者只能选一种并固化 smoke。无 active page 时,显示明确空状态,不显示静态设计稿页面列表。
- [ ] **Step 3: 验证根页不再伪装为编辑区**
Run:
```bash
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task118-rust-web-wolai-ui-parity-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task119-rust-web-wolai-visual-regression-smoke.js
```
### Task E-3:把 document shell 接上真实 Page Aggregate 与 page tree
**Files:**
- Modify: `rust/crates/mnote-web/src/routes/web_shell.rs`
- Modify: `rust/crates/mnote-web/src/ssr/pages/document.rs`
- Test: `rust/crates/mnote-web/src/routes/web_shell.rs`
- [ ] **Step 1: 增加 document shell contract 测试**
断言 HTML 同时包含:
```html
<script id="__MNOTE_PAGE_AGGREGATE__" type="application/json">...</script>
<script id="__MNOTE_EDITOR_BOOTSTRAP__" type="application/json">...</script>
<div data-testid="mnote-leptos-tiptap-island-editor-root" data-editor-host-kind="leptos_tiptap_island"></div>
```
- [ ] **Step 2: 输出 editor bootstrap contract**
bootstrap 至少包含:`schema``documentId``workspaceId``pageAggregateScriptId``saveEndpoint``editorHostKind``assetMode``requestId``traceId`
- [ ] **Step 3: 页面树与标题对齐 Page Aggregate**
文档页标题、breadcrumb、active sidebar row、page subtree 都必须来自同一份 Page Aggregate / projection,不从 URL 或静态 fallback 重复推断第二份真相。
### Task E-4:挂载 `leptos-tiptap` island 并证明可编辑
**Files:**
- Modify: `rust/crates/mnote-web/src/ssr/pages/document.rs`
- Modify: `wolai-frontend/src/components/editor/leptos-tiptap-island-editor-host.tsx`
- Modify: `wolai-frontend/src/components/editor/editor-host.tsx`
- Test: `wolai-frontend/src/components/editor/*.test.tsx`
- Test: `scripts/task121-rust-web-editor-island-hydration-smoke.js`
- [ ] **Step 1: 确定 island asset 加载方式**
短期允许从 Next legacy 产物加载 island bundle,但必须在 bootstrap 中显式标记 `assetMode: "legacy-next-island-bundle"`;中长期迁入 Rust Web 静态资产。
- [ ] **Step 2: 实现 hydration adapter**
adapter 从 `__MNOTE_PAGE_AGGREGATE__``__MNOTE_EDITOR_BOOTSTRAP__` 读取初始数据,挂载 `leptos_tiptap_island`,并输出现有 smoke 需要的观测属性:
```html
<div data-testid="mnote-leptos-tiptap-island-editor-root" data-editor-host-kind="leptos_tiptap_island">
<div class="editor-surface"><div class="ProseMirror" contenteditable="true"></div></div>
</div>
```
- [ ] **Step 3: 验证保存回路**
Run:
```bash
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task121-rust-web-editor-island-hydration-smoke.js
```
### Task E-5:树事件和编辑保存联动回归
**Files:**
- Modify: `rust/crates/mnote-web/src/routes/sse.rs`
- Modify: `rust/crates/mnote-web/src/routes/tree.rs`
- Modify: `rust/crates/mnote-web/src/routes/documents.rs`
- Modify: `scripts/task120-rust-web-tree-integration-smoke.js`
- Modify: `scripts/task121-rust-web-editor-island-hydration-smoke.js`
- [ ] **Step 1: 新建 / 重命名 / 删除后 tree stream 可观测**
`task120` 订阅 `/api/tree/events` 或二次拉取 projection,证明页面树变化不是只改 DOM。
- [ ] **Step 2: 保存后 Page Aggregate 可观测**
`task121` 保存后拉取 `/api/page-aggregate/:id`,证明正文内容进入 `body.editorDocument` 或约定字段。
- [ ] **Step 3: 运行回归组合**
Run:
```bash
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task114-rust-web-gateway-entry-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task115-rust-web-document-shell-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task118-rust-web-wolai-ui-parity-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task119-rust-web-wolai-visual-regression-smoke.js
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/task121-rust-web-editor-island-hydration-smoke.js
```
## 5. 完成判定
- `3000` 根页不再用静态设计稿页面列表伪装真实页面树。
- 左栏 `我的页面` 能显示当前 Convex/Rust projection 中真实页面,并能反映 active selection。
- 创建临时页面后,当前 3000 左栏 / page tree 能看到该页面;清理后消失。
- `/documents/:id` 在浏览器里能看到 `leptos-tiptap` island 根节点和可编辑 ProseMirror surface。
- 输入文本保存后,reload 和 `/api/page-aggregate/:id` 都能读回。
- `task120``task121` 通过,并与 `task114/115/118/119` 一起形成 3000 主链验收。
## 6. Harness 映射建议
`task-027``task-033` 已被 `3-8` 建议预留给 shell UI parity。若进入本计划执行,建议从 `task-034` 继续追加,不修改旧 completed 任务:
| 新任务 | Phase | 标题 | 优先级 |
| --- | --- | --- | --- |
| `task-034` | E-0 | 冻结真实树和编辑器 hydration 失败用例 | P0 |
| `task-035` | E-1 | 让 workspace sidebar 渲染真实 projection rows | P0 |
| `task-036` | E-2 | 把根路径从静态首页切到 active page / empty workspace | P0 |
| `task-037` | E-3 | 把 document shell 接上真实 Page Aggregate 与 page tree | P0 |
| `task-038` | E-4 | 挂载 leptos-tiptap island 并证明可编辑 | P0 |
| `task-039` | E-5 | 树事件和编辑保存联动回归 | P1 |
本计划建立阶段不直接修改 `harness-tasks.json`