fix: relink rust web tree editor runtime

This commit is contained in:
lix-2026
2026-04-29 14:36:24 +08:00
parent 6c3d20ca55
commit 33ddda9dfd
28 changed files with 1839 additions and 303 deletions
@@ -1,202 +0,0 @@
# 3-10 [process] Rust Web 3000 Tree / Editor Runtime Integration Checklist v1
> 更新时间:2026-04-29
>
> 对应计划:
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-9-rust-web-3000-tree-editor-runtime-integration-plan-v1.md`
>
> 参考输入:
> - 当前 3000 截图:`/mnt/Data1T/mnote/tmp/image copy 32.png`
> - 当前 shell parity checklist`/mnt/Data1T/mnote/design/03-rust-web/process/3-8-rust-web-3000-wolai-ui-parity-checklist-v1.md`
> - Page Aggregate 对齐主线:`/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
## 1. 当前结论
当前已经完成的是:
- [x] `3000` 默认由 `mnote-web` 监听。
- [x] `3000` 根页已经有 Wolai 风格 workspace shell。
- [x] 根页包含左栏、顶栏、右下浮动按钮和 `data-mnote-shell="workspace"`
- [x] `/documents/:id` 已有 Rust Web document shell、Page Aggregate marker 和 `data-editor-host="leptos_tiptap_island"` marker。
当前没有完成的是:
- [ ] 左栏 `我的页面` 显示真实 file/page tree rows,而不是空 section。
- [ ] 根页正文不再使用设计稿静态链接列表作为伪主内容。
- [ ] active page selection 能从 projection / recent / URL 一致落到 sidebar 和 document shell。
- [ ] `/documents/:id` 浏览器运行态能挂载 `data-testid="mnote-leptos-tiptap-island-editor-root"`
- [ ] `.ProseMirror[contenteditable="true"]` 可见、可输入、可保存、reload 后可读回。
- [ ] smoke 能区分“只有外壳 UI”与“真实树和编辑器已接入”。
一句话判断:
> **需要继续规划并执行下一阶段;3-8 只应视为 shell/UI parity,不能视为文件树、页面树和主编辑区 runtime parity。**
## 2. 不可误判为完成的情况
- [ ] 只看到 `data-testid="wolai-sidebar"`,不算页面树接入。
- [ ] 只看到 `我的页面` 文案,不算真实树接入。
- [ ] 只看到正文静态链接列表,不算 Page Aggregate 主内容接入。
- [ ] 只看到 `data-editor-host="leptos_tiptap_island"`,不算 editor island 已 hydrate。
- [ ] 只看到 `#mnote-editor-island` 空占位,不算主编辑区完成。
- [ ] 只在 fixture gateway 下通过,不算当前真实 `3000` 主链完成。
## 3. Phase E-0:冻结真实树和编辑器失败用例
目标:让当前截图暴露的问题可以自动失败。
- [ ] 新增 `scripts/task120-rust-web-tree-integration-smoke.js`
- [ ] `task120` 通过 `POST /api/tree/commands action=create` 创建临时根页面。
- [ ] `task120` 创建临时子页面并记录 `workspaceId` / `documentId`
- [ ] `task120` 断言 `GET /``/documents/:id` 的左栏包含 `data-testid="wolai-sidebar-row"`
- [ ] `task120` 断言临时页面 row 的 `data-node-id` 等于真实 document id。
- [ ] `task120` 断言 active row 有 `data-active="true"` 或等价稳定标记。
- [ ] `task120` 点击 row 后能打开 Rust Web `/documents/:id`
- [ ] `task120``purge` 清理临时页面。
- [ ] 新增 `scripts/task121-rust-web-editor-island-hydration-smoke.js`
- [ ] `task121` 断言 `data-testid="mnote-leptos-tiptap-island-editor-root"` 可见。
- [ ] `task121` 断言 `.editor-surface .ProseMirror[contenteditable="true"]` 可见。
- [ ] `task121` 输入唯一文本并触发保存。
- [ ] `task121` reload 后仍能读回唯一文本。
- [ ] `task121` 拉取 `/api/page-aggregate/:id` 后能读回保存内容或版本变化。
- [ ] `task119` 增加负向断言,不能只靠 `.mnote-home-links` 静态列表通过。
验证命令:
```bash
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/task121-rust-web-editor-island-hydration-smoke.js
```
## 4. Phase E-1workspace sidebar 接入真实 projection rows
目标:左栏 `我的页面` 不再是空 section,而是 projection 驱动的真实页面树。
- [ ] `WorkspaceShellProjection.myPageItems` 来源于 `sidebar_tree` / `sidebar.dataset.list` / kernel projection。
- [ ] `WorkspaceShellProjection.starredItems` 来源于真实 projection,不硬编码。
- [ ] `render_workspace_shell_sidebar_html` 输出 `data-testid="wolai-sidebar-row"`
- [ ] 每个 row 输出 `data-node-id`
- [ ] 子页面 row 输出 `data-parent-id` 或等价层级标记。
- [ ] active row 输出 `data-active="true"`
- [ ] 空 workspace 输出明确 empty state,不回到设计稿静态链接列表。
- [ ] Cargo 测试覆盖 root/child/active/empty 四类输入。
验证命令:
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web workspace_shell
```
## 5. Phase E-2:根路径接入 active page / empty workspace
目标:`GET /` 选择真实 active page 或显示真实空 workspace,不再把设计稿静态首页当主编辑区。
- [ ] `gateway.rs::root_entry` 选择顺序固定为 `pageId` > recent cookie > projection active > first page > empty workspace。
- [ ] 有 active page 时,根路径渲染或跳转到真实 document shell。
- [ ] 无 active page 时,根路径显示空 workspace state。
- [ ] 根页不再输出 `.mnote-home-links` 静态设计稿列表作为主内容。
- [ ] recent page cookie 与 document shell 打开行为一致。
- [ ] Rust 测试覆盖 root selection 顺序。
- [ ] `task118` / `task119` 仍通过。
验证命令:
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web root_entry
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
```
## 6. Phase E-3document shell 接上真实 Page Aggregate 与 page tree
目标:文档页标题、页面树、正文 bootstrap 来自同一份 Page Aggregate / projection。
- [ ] `/documents/:id` 标题来自 Page Aggregate `head.title`
- [ ] breadcrumb / topbar active title 与 Page Aggregate 标题一致。
- [ ] 左栏 active row 与当前 document id 一致。
- [ ] 页面内 page tree / subtree 使用 Page Aggregate `tree.pageSubtree` 或 kernel `page_tree` projection。
- [ ] HTML 内嵌 `__MNOTE_PAGE_AGGREGATE__`
- [ ] HTML 内嵌 `__MNOTE_EDITOR_BOOTSTRAP__`
- [ ] bootstrap 包含 `documentId``workspaceId``saveEndpoint``editorHostKind``assetMode`
- [ ] 不从 URL 或静态 fallback 重新推导第二份页面真相。
验证命令:
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web document_shell page_aggregate
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task115-rust-web-document-shell-smoke.js
```
## 7. Phase E-4:挂载 `leptos-tiptap` island 并证明可编辑
目标:主编辑区不是 marker,而是浏览器内真实可编辑 runtime。
- [ ] Rust document shell 输出稳定 mount root。
- [ ] 前端 island adapter 能读取 `__MNOTE_PAGE_AGGREGATE__`
- [ ] 前端 island adapter 能读取 `__MNOTE_EDITOR_BOOTSTRAP__`
- [ ] `leptos_tiptap_island` 成为 active host。
- [ ] 页面出现 `data-testid="mnote-leptos-tiptap-island-editor-root"`
- [ ] 页面出现 `.editor-surface .ProseMirror[contenteditable="true"]`
- [ ] `data-editor-host-active="leptos_tiptap_island"` 或等价 observability 可见。
- [ ] BlockNote 没有成为默认主编辑器。
- [ ] 输入文本能保存。
- [ ] reload 后文本仍在。
验证命令:
```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
```
## 8. Phase E-5:树事件与编辑保存联动回归
目标:树和编辑器不是一次性 SSR,而是接入当前 Rust Web transport / realtime 主链。
- [ ] 新建页面后 `/api/tree/events` 或 projection reload 可看到新增节点。
- [ ] 重命名页面后 sidebar row title 更新。
- [ ] 删除 / purge 页面后 sidebar row 消失。
- [ ] 编辑保存走 `/api/documents/save` 或约定 Rust Web save endpoint。
- [ ] 保存后 `/api/page-aggregate/:id` 能看到正文变化或 revision 变化。
- [ ] `task114``task115``task118``task119``task120``task121` 在当前 3000 全部通过。
验证命令:
```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
```
## 9. Harness 映射建议
本 checklist 建立阶段不直接修改 `harness-tasks.json`。若进入执行,建议追加:
| 新任务 | 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 |
## 10. 完成判定
- [ ] `task120` 在当前 `3000` 通过。
- [ ] `task121` 在当前 `3000` 通过。
- [ ] 左栏页面树来自真实 projection。
- [ ] 根页不再用设计稿静态列表伪装主内容。
- [ ] 文档页有真实 `leptos-tiptap` 可编辑 surface。
- [ ] 保存后 Page Aggregate / reload 可读回。
- [ ] `3-8` 可移动到 `done` 后,本 checklist 才能进入完成评估。
@@ -1,452 +0,0 @@
# 3-7 [process] Rust Web 3000 Wolai UI Parity Implementation Plan v1
> **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:**`mnote-web` 持有的 `3000` 公共入口恢复到可用的 Wolai 工作区产品壳,并继续向 `/mnt/Data1T/mnote/design/design/html/image.png` 的目标 UI 收敛。
**Architecture:** 保持 `3000` 的 owner 是 `mnote-web`,不把 Next App Router 恢复为主入口。Rust Web 输出 workspace/document server-first shell、Page Aggregate snapshot、Sidebar projection 与 topbar/floating action 的稳定 HTML 合同;React/Next 仅保留交互 island、legacy 对照链和显式 debug 边界。
**Tech Stack:** Rust workspace、`mnote-web``axum`、Leptos SSR、Page Aggregate、Convex substrate、Rust tree projection、Playwright/Node smoke、Cargo tests、Vitest。
---
## 1. 当前事实基线
本计划基于 2026-04-29 本机检查结果建立:
- `127.0.0.1:3000` 当前由 `target/debug/mnote-web` 监听,`GET /` 返回 `x-mnote-web-owner: mnote-web`
- `127.0.0.1:3100` 当前由 `node scripts/dev-server.js -p 3100` 监听,根路径返回 `307 /auth`,仍可作为 legacy Next 对照链。
- 当前 `3000` 截图 `/mnt/Data1T/mnote/tmp/image copy 30.png` 是极简 MNOTE 首页,只有“首页 / 搜索 / 文档”和欢迎文案。
- 当前 legacy/Next 对照截图 `/mnt/Data1T/mnote/tmp/image copy 31.png` 已有 workspace/document 左栏、页面树和文档内容,但仍不是目标 UI。
- 目标 UI 参考是 `/mnt/Data1T/mnote/design/design/html/image.png``/mnt/Data1T/mnote/design/design/html/个人.html`,它包含完整 Wolai 桌面壳:账号区、顶栏、快捷图标、星标置顶、我的页面、页面树、底部入口、右下悬浮按钮和居中页面内容。
- `harness-tasks.json` 目前只有 `task-001``task-026`,全部是 `completed`,没有待办任务。
- `task-019``task-026` 已证明主 Web owner cutover,但没有证明产品 UI parity。
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task114-rust-web-gateway-entry-smoke.js` 当前失败,原因是脚本仍断言根页包含 `Rust Web gateway` 文案,而当前根页已经是极简 `MNOTE` 首页。
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task115-rust-web-document-shell-smoke.js` 当前失败,原因是 `/documents/doc_1?workspaceId=ws_demo` 经过 Convex 查询返回 `未登录`HTTP 状态为 `502`
- `rust/crates/mnote-web/src/routes/gateway.rs::root_entry` 当前直接渲染 `ssr/pages/home.rs::HomePage`,不会把 `/` 代理到 3100 的 workspace/document 壳。
- `rust/crates/mnote-web/src/ssr/pages/layout.rs::PageLayout` 当前只实现极简侧栏,不具备目标 UI 的 workspace shell 合同。
结论:当前问题不是“3000 owner 还没切到 Rust Web”,而是“owner 已切到 Rust Web 后,产品工作区 UI、真实会话、真实数据壳和视觉/交互验收没有接上”。
## 2. 边界原则
- `3000` 保持唯一公开入口,默认 owner 继续是 `mnote-web`
- 不恢复 `3104` 作为公开入口。
- 不把 `3100` 恢复成主入口;`3100` 只作为 legacy/reference/debug upstream。
- 不在前端重新创造树真相;Sidebar / 页面树继续消费 Rust kernel projection 或 Page Aggregate 中的稳定 projection。
- 不把“截图样式补丁”当成完成;必须同时修复当前 3000 的真实会话、真实路由和 smoke 验收。
- 不修改或回滚当前未提交改动;执行本计划时按任务最小写集落地。
## 3. 文件结构
### Rust Web SSR shell
- `rust/crates/mnote-web/src/routes/gateway.rs`
-`/` 从极简欢迎页改成 workspace entry,负责选择 active workspace / active page / fallback route。
- `rust/crates/mnote-web/src/routes/web_shell.rs`
- 继续承接 document shell 与 Page Aggregate;新增 workspace shell 所需的共享加载器。
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
- 从极简 `PageLayout` 演进为 `WolaiWorkspaceLayout` 或等价组件,输出目标 UI 的稳定 DOM 合同。
- `rust/crates/mnote-web/src/ssr/pages/home.rs`
- 从营销/欢迎页改为 workspace home/page shell,不再作为默认产品首屏。
- `rust/crates/mnote-web/src/ssr/pages/document.rs`
- 对齐目标页面内容区:breadcrumb、顶部操作区、页面 icon/title、子页面列表、编辑 island mount。
- `rust/crates/mnote-web/src/ssr/styles.rs`
- 收口目标 UI 样式变量、sidebar/topbar/content/floating action 样式。
### Rust Web 数据合同
- `rust/crates/mnote-web/src/workspace_shell.rs`
- 新建 workspace shell projection 聚合器,组合 workspace summary、sidebar dataset、active page、starred/pinned 区域和 bottom entries。
- `rust/crates/mnote-web/src/routes/session.rs`
- 修复 3000 当前真实请求的 session/auth handoff,避免文档页在登录态/开发态下返回 `未登录` 502。
- `rust/crates/mnote-web/src/transport/convex.rs`
- 确认 Rust Web 到 Convex 的 cookie/header/token 透传合同;必要时补只读 helper,不绕开用户鉴权。
### 前端 legacy/island
- `wolai-frontend/src/lib/rust-web-main-execution-boundary.ts`
- 补充 UI parity guard,明确 Next 只能是 legacy/reference/island source。
- `wolai-frontend/src/components/sidebar/**`
- 只在需要复用现有 projection/DOM 合同时修改,不重新把 Next Sidebar 设为主产品壳。
- `wolai-frontend/src/components/editor/**`
- 只保留编辑器 island runtime 和必要 mount contract。
### Smoke / 验收
- `scripts/task114-rust-web-gateway-entry-smoke.js`
- 从“根页包含调试文案”升级为“根页是 Rust-owned Wolai workspace shell”。
- `scripts/task115-rust-web-document-shell-smoke.js`
- 从 fixture-only 文档壳验证升级为“当前 3000 可打开真实/开发态文档壳且不返回未登录 502”。
- `scripts/task118-rust-web-wolai-ui-parity-smoke.js`
- 新建目标 UI parity smoke,覆盖 sidebar、topbar、content、floating actions、legacy 禁用组合。
- `scripts/task119-rust-web-wolai-visual-regression-smoke.js`
- 新建可选 Playwright 截图 smoke,对比 3000、3100 legacy 和 `design/design/html/image.png` 的关键布局指标。
## 4. 实施任务
### Task U-0:冻结 UI parity 失败用例
**Files:**
- Create: `scripts/task118-rust-web-wolai-ui-parity-smoke.js`
- Modify: `scripts/task114-rust-web-gateway-entry-smoke.js`
- Modify: `scripts/task115-rust-web-document-shell-smoke.js`
- [ ] **Step 1: 写 3000 UI parity smoke**
创建 `scripts/task118-rust-web-wolai-ui-parity-smoke.js`,固定当前必须失败的产品壳断言:
```js
#!/usr/bin/env node
"use strict";
const assert = require("node:assert");
const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
const REQUEST_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_TIMEOUT_MS || 20_000);
async function fetchWithTimeout(path) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(new Error(`请求超时: ${path}`)), REQUEST_TIMEOUT_MS);
try {
return await fetch(`${BASE_URL}${path}`, {
redirect: "manual",
cache: "no-store",
signal: controller.signal,
});
} finally {
clearTimeout(timer);
}
}
async function readHtml(path) {
const response = await fetchWithTimeout(path);
const html = await response.text();
assert.equal(response.status, 200, `${path} 状态码应为 200`);
assert.equal(response.headers.get("x-mnote-web-owner"), "mnote-web", `${path} 必须由 mnote-web 拥有`);
assert.match(response.headers.get("content-type") || "", /text\/html/, `${path} 必须返回 HTML`);
return html;
}
async function main() {
const rootHtml = await readHtml("/");
assert.match(rootHtml, /data-mnote-shell="workspace"/, "根页必须是 workspace shell,而不是欢迎页");
assert.match(rootHtml, /liaibo的个人空间|个人空间|开发用户 的空间/, "侧栏必须显示工作区身份");
assert.match(rootHtml, /星标置顶/, "侧栏必须包含星标置顶区域");
assert.match(rootHtml, /我的页面/, "侧栏必须包含我的页面区域");
assert.match(rootHtml, /垃圾箱/, "侧栏底部必须包含垃圾箱入口");
assert.match(rootHtml, /模板中心/, "侧栏底部必须包含模板中心入口");
assert.match(rootHtml, /data-testid="wolai-topbar"/, "主内容区必须包含 Wolai 顶栏");
assert.match(rootHtml, /data-testid="wolai-floating-ai"/, "页面必须包含右下 AI 悬浮入口");
assert.doesNotMatch(rootHtml, /欢迎使用 MNOTE 知识管理平台/, "根页不能停留在极简欢迎页");
assert.doesNotMatch(rootHtml, /<a href="\/documents">文档<\/a>/, "根页不能停留在三项导航壳");
console.log(JSON.stringify({ ok: true, baseUrl: BASE_URL, shell: "workspace", owner: "mnote-web" }, null, 2));
}
main().catch((error) => {
console.error(error instanceof Error ? error.stack || error.message : String(error));
process.exit(1);
});
```
- [ ] **Step 2: 运行 smoke,确认当前失败**
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
```
Expected before implementation: FAIL,第一处失败应指向 `data-mnote-shell="workspace"` 缺失或极简欢迎页仍存在。
- [ ] **Step 3: 更新 task114 的根页断言**
`scripts/task114-rust-web-gateway-entry-smoke.js` 中对 `/Rust Web gateway/` 的断言改成:
```js
assert.match(rootText, /data-mnote-shell="workspace"/);
assert.doesNotMatch(rootText, /Rust Web gateway|欢迎使用 MNOTE 知识管理平台/);
```
- [ ] **Step 4: 更新 task115 的失败信息**
`scripts/task115-rust-web-document-shell-smoke.js` 中保留 `x-mnote-web-owner` / `x-mnote-web-shell` 断言,并在 502 时输出 body,确保“未登录”不被隐藏成普通 shell 失败。
Run:
```bash
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task115-rust-web-document-shell-smoke.js
```
Expected before implementation: FAIL,错误信息包含 `未登录``convex_upstream_error`
### Task U-1:修复 3000 当前真实 session / document shell 基线
**Files:**
- Modify: `rust/crates/mnote-web/src/routes/session.rs`
- Modify: `rust/crates/mnote-web/src/transport/convex.rs`
- Modify: `rust/crates/mnote-web/src/routes/web_shell.rs`
- Test: `rust/crates/mnote-web/src/routes/web_shell.rs`
- Test: `scripts/task115-rust-web-document-shell-smoke.js`
- [ ] **Step 1: 写 Rust 测试覆盖 session handoff**
`web_shell.rs` 的测试模块中增加测试,构造带 `mnote_web_convex_token` cookie 的请求,断言 Rust Web 会把 token 传给 Convex transport plan,不会落入未登录错误路径。
Run:
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web document_shell session
```
Expected before implementation: FAIL,失败点是缺少 cookie/header 透传或测试 helper 不存在。
- [ ] **Step 2: 修复 Rust Web 到 Convex 的认证透传**
保持 `DEV_USER_ID` 只作为开发辅助,不绕过真实用户鉴权。实际请求优先级固定为:浏览器 cookie / session handoff token > `mnote_web_convex_token` > 开发态显式 DEV_USER fallback。
- [ ] **Step 3: 验证当前 3000 文档页不再 502**
Run:
```bash
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task115-rust-web-document-shell-smoke.js
```
Expected after implementation: PASS,输出 JSON 中 `owner``mnote-web``shell``document`
### Task U-2:建立 workspace shell projection 聚合器
**Files:**
- Create: `rust/crates/mnote-web/src/workspace_shell.rs`
- Modify: `rust/crates/mnote-web/src/lib.rs`
- Modify: `rust/crates/mnote-web/src/routes/gateway.rs`
- Modify: `rust/crates/mnote-web/src/routes/web_shell.rs`
- Test: `rust/crates/mnote-web/src/workspace_shell.rs`
- [ ] **Step 1: 定义 workspace shell 最小数据结构**
新增结构:
```rust
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceShellProjection {
pub schema: String,
pub workspace_id: String,
pub workspace_name: String,
pub active_page_id: Option<String>,
pub active_page_title: Option<String>,
pub starred_items: Vec<WorkspaceShellItem>,
pub my_page_items: Vec<WorkspaceShellItem>,
pub bottom_entries: Vec<WorkspaceShellEntry>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceShellItem {
pub id: String,
pub title: String,
pub icon: Option<String>,
pub href: String,
pub depth: u32,
pub active: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceShellEntry {
pub id: String,
pub label: String,
pub href: String,
pub icon: String,
}
```
- [ ] **Step 2: 写聚合器测试**
测试输入使用 `sidebar.dataset.list` 形状,断言输出包含:`星标置顶` 数据、`我的页面` 数据、`垃圾箱``模板中心`,且 active page 能从请求参数或 projection 中确定。
Run:
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web workspace_shell
```
Expected before implementation: FAIL,模块或类型不存在。
- [ ] **Step 3: 接入 gateway root**
`GET /` 不再直接输出 `HomePage` 欢迎页,而是加载 `WorkspaceShellProjection` 后输出 workspace shell。无 active page 时,选择第一个可见 my page;没有页面时输出空 workspace shell,不输出营销欢迎页。
### Task U-3:把 `PageLayout` 替换为 Wolai workspace shell 合同
**Files:**
- Modify: `rust/crates/mnote-web/src/ssr/pages/layout.rs`
- Modify: `rust/crates/mnote-web/src/ssr/pages/home.rs`
- Modify: `rust/crates/mnote-web/src/ssr/pages/document.rs`
- Modify: `rust/crates/mnote-web/src/ssr/styles.rs`
- Test: `rust/crates/mnote-web/src/ssr/pages/layout.rs`
- [ ] **Step 1: 写 SSR DOM 合同测试**
测试 `WolaiWorkspaceLayout` 渲染结果必须包含:
```text
data-mnote-shell="workspace"
data-testid="wolai-sidebar"
data-testid="wolai-topbar"
星标置顶
我的页面
垃圾箱
模板中心
data-testid="wolai-floating-ai"
data-testid="wolai-floating-help"
```
Run:
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web wolai_workspace_layout
```
Expected before implementation: FAIL,当前只有 `mnote-shell` / `mnote-sidebar-nav`
- [ ] **Step 2: 实现 workspace shell DOM**
把极简三项导航替换为目标 UI 合同:顶部账号区、快捷操作区、星标置顶、我的页面、底部入口、主内容 topbar 和右下浮动入口。
- [ ] **Step 3: 样式对齐目标 UI 的关键布局指标**
关键指标:左栏宽度约 `280px``360px` 区间;侧栏背景为浅灰;选中行浅红背景;主内容首屏留白大;页面中心内容宽度约 `720px`;顶栏高度约 `44px`;右下两个圆形浮动按钮不遮挡正文。
### Task U-4:恢复 3000 根页到 workspace/document 产品首屏
**Files:**
- Modify: `rust/crates/mnote-web/src/routes/gateway.rs`
- Modify: `rust/crates/mnote-web/src/ssr/pages/home.rs`
- Modify: `rust/crates/mnote-web/src/routes/web_shell.rs`
- Test: `scripts/task118-rust-web-wolai-ui-parity-smoke.js`
- [ ] **Step 1: 根路径选择 active page**
选择顺序固定为:URL `pageId` 参数 > session 最近页面 > projection active page > 第一个 `my_page_items` > 空 workspace。
- [ ] **Step 2: 根路径输出 workspace shell**
`GET /` 的 body 必须包含 `data-mnote-shell="workspace"`,不再包含 `欢迎使用 MNOTE 知识管理平台`
- [ ] **Step 3: 验证 3000 smoke**
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
```
Expected after implementation: PASS。
### Task U-5:文档页内容区对齐 legacy 3100 与目标 UI
**Files:**
- Modify: `rust/crates/mnote-web/src/ssr/pages/document.rs`
- Modify: `rust/crates/mnote-web/src/routes/web_shell.rs`
- Modify: `rust/crates/mnote-web/src/ssr/styles.rs`
- Test: `scripts/task115-rust-web-document-shell-smoke.js`
- [ ] **Step 1: 文档 shell 输出 topbar / breadcrumb / action slots**
文档页必须输出 `data-testid="wolai-topbar"`、breadcrumb 当前页标题、收藏/更多/AI action slot。
- [ ] **Step 2: 文档 shell 继续嵌入 Page Aggregate snapshot**
保留现有:`id="__MNOTE_PAGE_AGGREGATE__"``data-page-aggregate-snapshot="mnote.page_aggregate.v1"``data-editor-host="leptos_tiptap_island"`
- [ ] **Step 3: 文档内容区不退回 BlockNote-first**
默认编辑 host 仍是 `leptos_tiptap_island`BlockNote 只作为 fallback/debug。
### Task U-6:补视觉回归 smoke
**Files:**
- Create: `scripts/task119-rust-web-wolai-visual-regression-smoke.js`
- Modify: `package.json` 或既有 smoke 调度脚本,仅在仓库已有集中 smoke 入口时接入。
- [ ] **Step 1: 写 Playwright 截图与布局指标检查**
检查项固定为:左栏存在且宽度稳定、topbar 存在、主内容不被侧栏遮挡、右下浮动按钮不遮挡正文、根页没有极简欢迎页。
- [ ] **Step 2: 运行可视 smoke**
Run:
```bash
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task119-rust-web-wolai-visual-regression-smoke.js
```
Expected after implementation: PASS,并输出截图路径到 `/mnt/Data1T/mnote/test-results/`
### Task U-7:文档和 harness 收口
**Files:**
- Modify: `design/03-rust-web/process/3-8-rust-web-3000-wolai-ui-parity-checklist-v1.md`
- Modify: `harness-tasks.json` only when explicitly scheduling execution.
- Modify: `harness-progress.txt` only when execution starts.
- [ ] **Step 1: 更新 checklist 状态**
每完成一个 U 阶段,只更新对应 checklist 项,不把 owner cutover 结果重复写成 UI parity 完成。
- [ ] **Step 2: 如需进入 harness,追加 task-027 起的新任务**
建议映射:
```text
task-027 -> U-0 UI parity smoke
task-028 -> U-1 session/document shell baseline
task-029 -> U-2 workspace shell projection
task-030 -> U-3 Wolai workspace layout
task-031 -> U-4 root workspace entry
task-032 -> U-5 document UI parity
task-033 -> U-6 visual regression smoke
```
只有在明确开始执行时修改 `harness-tasks.json`,避免把分析计划误写成已排队任务。
## 5. 完成判定
全部完成时必须同时成立:
- `GET http://127.0.0.1:3000/` 返回 `x-mnote-web-owner: mnote-web`
- 根页 body 标记为 `data-mnote-shell="workspace"`
- 根页不再出现 `欢迎使用 MNOTE 知识管理平台` 这类极简欢迎页文案。
- 根页包含目标 UI 的结构区:账号区、快捷入口、星标置顶、我的页面、底部入口、topbar、右下浮动按钮。
- `/documents/:id` 不因当前登录态或开发态缺失而返回 `未登录` 502。
- 文档页仍消费 Page Aggregate snapshot,并保留 `leptos_tiptap_island` 默认编辑 host。
- `MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT=0` 或等价 skip legacy 场景下,核心 workspace/document shell 仍可打开。
- `3100` 仅作为 legacy/reference/debug,不是主入口 owner。
- 新增 smoke 能在当前 3000 上证明产品壳,而不只是证明 owner header。
## 6. 自检
- Spec coverage:覆盖当前 3000 截图、3100 对照截图、目标 UI 参考、git/harness 现状、session 502、root shell 极简化、owner cutover 与 UI parity 的口径拆分。
- Placeholder scan:本计划不使用 `TBD``TODO``implement later` 作为执行步骤。
- Type consistency:统一使用 `WorkspaceShellProjection``WolaiWorkspaceLayout``data-mnote-shell="workspace"``mnote-web``next-app-router legacy compat`
@@ -1,222 +0,0 @@
# 3-8 [process] Rust Web 3000 Wolai UI Parity Checklist v1
> 更新时间:2026-04-29
>
> 对应计划:
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-7-rust-web-3000-wolai-ui-parity-plan-v1.md`
>
> 参考输入:
> - 当前 3000 截图:`/mnt/Data1T/mnote/tmp/image copy 30.png`
> - 当前 3100 legacy/Next 对照截图:`/mnt/Data1T/mnote/tmp/image copy 31.png`
> - 目标 UI`/mnt/Data1T/mnote/design/design/html/image.png`
> - 目标 HTML`/mnt/Data1T/mnote/design/design/html/个人.html`
## 1. 当前结论
当前已经完成的是:
- [x] `3000` 默认由 `mnote-web` 监听。
- [x] `GET /` 返回 `x-mnote-web-owner: mnote-web`
- [x] `3100` 已降为 legacy/Next 对照链,不是公开主入口 owner。
- [x] `harness-tasks.json``task-001``task-026` 全部为 `completed`
当前没有完成的是:
- [x] `3000` 首屏恢复到 3100 的 workspace/document 产品壳。
- [x] `3000` 首屏达到目标 Wolai UI 的结构与视觉效果。
- [x] `3000` 文档页在当前真实运行态下稳定打开,不返回 `未登录` 502。
- [x] smoke 验收从“owner header 通过”升级为“产品壳 UI parity 通过”。
一句话判断:
> **Rust Web owner cutover 已完成;本轮已把 3000 恢复为 Rust Web 持有的 Wolai workspace/document 产品壳,并用 task114/task115/task118/task119 固化验收。**
## 2. 不可误判为完成的情况
- [x] 只看到 `x-mnote-web-owner: mnote-web`,不算 UI parity 完成。
- [x] 只看到极简 `MNOTE` 欢迎页,不算工作区 shell 完成。
- [x] 只在 `MNOTE_WEB_ALLOW_DEV_FIXTURES=1` 下通过,不算当前真实 3000 完成。
- [x] 只把 3000 proxy 回 3100,不算 Rust Web 产品壳完成。
- [x] 只恢复 3100 legacy UI,不算 Rust Web 3000 UI 完成。
- [x] 只补 CSS 像截图,但 session/document shell 仍 502,不算完成。
## 3. Phase U-0:冻结 UI parity smoke
目标:先让当前差距可自动失败,避免继续靠人工看截图判断。
- [x] 新增 `scripts/task118-rust-web-wolai-ui-parity-smoke.js`
- [x] smoke 断言 `GET /``data-mnote-shell="workspace"`
- [x] smoke 断言根页包含账号/工作区身份。
- [x] smoke 断言根页包含 `星标置顶`
- [x] smoke 断言根页包含 `我的页面`
- [x] smoke 断言根页包含 `垃圾箱`
- [x] smoke 断言根页包含 `模板中心`
- [x] smoke 断言根页包含 `data-testid="wolai-topbar"`
- [x] smoke 断言根页包含 `data-testid="wolai-floating-ai"`
- [x] smoke 负向断言根页不再包含 `欢迎使用 MNOTE 知识管理平台`
- [x] `task114-rust-web-gateway-entry-smoke.js` 不再检查 `Rust Web gateway` 调试文案。
- [x] `task115-rust-web-document-shell-smoke.js` 在 502 时输出 body,能看见 `未登录` 根因。
验证命令:
```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/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
```
## 4. Phase U-1:修复真实 session / document shell 基线
目标:当前 3000 文档页不能继续因为 Rust Web 到 Convex 的 session/handoff 问题返回 `未登录` 502。
- [x] 确认当前 3000 进程环境和浏览器请求中的 auth/session 来源。
- [x] 固定 Rust Web session handoff 优先级:浏览器 cookie / session handoff token > `mnote_web_convex_token` > 显式开发态 fallback。
- [x] `transport/convex.rs` 能把必要 cookie/header/token 传到 Convex。
- [x] `/documents/:id` 在当前登录态或开发态下不返回 `convex_upstream_error: 未登录`
- [x] 文档页仍返回 `x-mnote-web-owner: mnote-web`
- [x] 文档页仍返回 `x-mnote-web-shell: document`
- [x] 文档页仍嵌入 `__MNOTE_PAGE_AGGREGATE__`
- [x] 文档页仍标记默认 `data-editor-host="leptos_tiptap_island"`
验证命令:
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web document_shell session
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task115-rust-web-document-shell-smoke.js
```
## 5. Phase U-2:建立 workspace shell projection
目标:Rust Web 根页不再手写欢迎页,而是消费稳定 workspace/sidebar/page projection。
- [x] 新增 `WorkspaceShellProjection` 或等价 Rust 类型。
- [x] projection 包含 `workspaceId`
- [x] projection 包含 `workspaceName`
- [x] projection 包含 `activePageId` / `activePageTitle`
- [x] projection 包含 `starredItems`
- [x] projection 包含 `myPageItems`
- [x] projection 包含 `bottomEntries`,至少覆盖 `垃圾箱``模板中心`
- [x] projection 从 `sidebar.dataset.list` 或 Rust kernel sidebar projection 构建,不在 UI 层另造树真相。
- [x] 空数据时输出空 workspace shell,不回到极简营销欢迎页。
验证命令:
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web workspace_shell
```
## 6. Phase U-3:替换极简 PageLayout 为 Wolai workspace shell
目标:`mnote-web` SSR 输出与目标 UI 同级的产品壳结构。
- [x] SSR layout 根节点标记 `data-mnote-shell="workspace"`
- [x] 左侧栏标记 `data-testid="wolai-sidebar"`
- [x] 左侧栏顶部包含头像/首字母块与工作区名称。
- [x] 左侧栏快捷操作区包含搜索、图谱/关系、快捷动作、收纳/文件、更多等 slot。
- [x] 左侧栏包含 `星标置顶` section。
- [x] 左侧栏包含 `我的页面` section。
- [x] 左侧栏 page row 支持 active/selected 视觉状态。
- [x] 左侧栏底部包含 `垃圾箱``模板中心`
- [x] 主区顶部包含 `data-testid="wolai-topbar"`
- [x] 主区右上包含收藏、演示/评论/关系/成员/历史/更多等 action slot。
- [x] 右下包含 `data-testid="wolai-floating-ai"`
- [x] 右下包含 `data-testid="wolai-floating-help"`
- [x] CSS 左栏宽度、浅灰背景、选中行浅红背景、主内容居中宽度与目标图接近。
验证命令:
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web wolai_workspace_layout
```
## 7. Phase U-4:恢复 3000 根路径产品首屏
目标:`GET /` 是真实 workspace/document 入口,而不是极简欢迎页。
- [x] `gateway.rs::root_entry` 不再直接渲染 `HomePage` 欢迎页。
- [x] 根路径选择 active page 的顺序固定:URL `pageId` > session 最近页面 > projection active page > 第一个我的页面 > 空 workspace。
- [x] 根路径返回 HTML 包含 `data-mnote-shell="workspace"`
- [x] 根路径返回 HTML 不包含 `欢迎使用 MNOTE 知识管理平台`
- [x] 根路径返回 HTML 不包含旧三项导航壳 `首页 / 搜索 / 文档` 作为主 UI。
- [x] `MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT=0` 时根路径仍可打开核心 shell。
验证命令:
```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
```
## 8. Phase U-5:文档页内容区对齐
目标:`/documents/:id` 恢复 workspace/document 产品壳,同时继续保持 Page Aggregate 与 `leptos-tiptap` 主链。
- [x] 文档页包含 workspace sidebar。
- [x] 文档页包含 topbar / breadcrumb。
- [x] 文档页包含收藏/更多/AI action slot。
- [x] 文档页标题来自 Page Aggregate。
- [x] 文档页正文 mount 保持 `leptos_tiptap_island`
- [x] 文档页不重新把 BlockNote 设为默认主编辑器。
- [x] 文档页在真实 3000 下通过 smoke,不返回 502。
验证命令:
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web document_shell page_aggregate
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task115-rust-web-document-shell-smoke.js
```
## 9. Phase U-6:补视觉回归 smoke
目标:用浏览器自动化把“看起来像目标 UI”变成可回归指标。
- [x] 新增 `scripts/task119-rust-web-wolai-visual-regression-smoke.js`
- [x] 检查左栏存在且宽度稳定。
- [x] 检查 topbar 存在且不遮挡正文。
- [x] 检查主内容区未被侧栏覆盖。
- [x] 检查右下浮动按钮存在且不遮挡正文。
- [x] 检查根页没有极简欢迎页。
- [x] 输出截图到 `/mnt/Data1T/mnote/test-results/`
验证命令:
```bash
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task119-rust-web-wolai-visual-regression-smoke.js
```
## 10. Harness 映射建议
当前 `harness-tasks.json``task-001``task-026` 已全部 completed。若进入执行,建议追加新任务,不修改旧任务状态:
| 新任务 | Phase | 标题 | 优先级 |
| --- | --- | --- | --- |
| `task-027` | U-0 | 冻结 Rust Web 3000 Wolai UI parity smoke | P0 |
| `task-028` | U-1 | 修复 Rust Web session/document shell 当前 502 基线 | P0 |
| `task-029` | U-2 | 建立 workspace shell projection 聚合器 | P0 |
| `task-030` | U-3 | 替换极简 PageLayout 为 Wolai workspace shell | P0 |
| `task-031` | U-4 | 恢复 3000 根路径 workspace/document 产品首屏 | P0 |
| `task-032` | U-5 | 对齐文档页 topbar/content/Page Aggregate shell | P1 |
| `task-033` | U-6 | 补 Wolai UI visual regression smoke | P1 |
说明:本 checklist 建立阶段不直接修改 `harness-tasks.json`,避免把分析结果误登记为已排队执行任务。
## 11. 完成判定
- [x] `GET http://127.0.0.1:3000/` 返回 `x-mnote-web-owner: mnote-web`
- [x] 根页 body 标记 `data-mnote-shell="workspace"`
- [x] 根页不再出现极简欢迎页文案。
- [x] 根页包含目标 UI 的核心结构区。
- [x] `/documents/:id` 不返回 `未登录` 502。
- [x] 文档页继续消费 Page Aggregate snapshot。
- [x] 文档页默认编辑 host 仍是 `leptos_tiptap_island`
- [x] `3100` 只作为 legacy/reference/debug。
- [x] `task114``task115``task118` 和可选 `task119` 在当前 3000 上通过。
- [ ] 本 checklist 才能移动到 `design/03-rust-web/done/`
@@ -1,296 +0,0 @@
# 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`