Files
mnote/design/old/03-rust-web/done/3-7-rust-web-3000-wolai-ui-parity-plan-v1.md
T
Agent Board b798f628ee chore: land tree view-state, vault, Pi module split, and repo hygiene
Persist PageTree expand state via control-plane view-state and align
chevron/DOM with restored expansion; keep Sidex-style shallow page-tree
scan and drop the unused recursive scanner that only added cargo noise.

Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi
into a module package, and retire Hermes/ACP/OpenHub recycle + root
harness evidence from the index while gitignoring recycle and local
diag dumps.

Archive superseded design/bugs docs under old/, point architecture at
ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor
regressions so the working tree can stay clean.
2026-07-21 05:13:05 +08:00

453 lines
20 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.
# [recycle] 3-7 [done] 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 (`- [x]`) 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`
- [x] **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);
});
```
- [x] **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"` 缺失或极简欢迎页仍存在。
- [x] **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 知识管理平台/);
```
- [x] **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`
- [x] **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 不存在。
- [x] **Step 2: 修复 Rust Web 到 Convex 的认证透传**
保持 `DEV_USER_ID` 只作为开发辅助,不绕过真实用户鉴权。实际请求优先级固定为:浏览器 cookie / session handoff token > `mnote_web_convex_token` > 开发态显式 DEV_USER fallback。
- [x] **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`
- [x] **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,
}
```
- [x] **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,模块或类型不存在。
- [x] **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`
- [x] **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`
- [x] **Step 2: 实现 workspace shell DOM**
把极简三项导航替换为目标 UI 合同:顶部账号区、快捷操作区、星标置顶、我的页面、底部入口、主内容 topbar 和右下浮动入口。
- [x] **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`
- [x] **Step 1: 根路径选择 active page**
选择顺序固定为:URL `pageId` 参数 > session 最近页面 > projection active page > 第一个 `my_page_items` > 空 workspace。
- [x] **Step 2: 根路径输出 workspace shell**
`GET /` 的 body 必须包含 `data-mnote-shell="workspace"`,不再包含 `欢迎使用 MNOTE 知识管理平台`
- [x] **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`
- [x] **Step 1: 文档 shell 输出 topbar / breadcrumb / action slots**
文档页必须输出 `data-testid="wolai-topbar"`、breadcrumb 当前页标题、收藏/更多/AI action slot。
- [x] **Step 2: 文档 shell 继续嵌入 Page Aggregate snapshot**
保留现有:`id="__MNOTE_PAGE_AGGREGATE__"``data-page-aggregate-snapshot="mnote.page_aggregate.v1"``data-editor-host="leptos_tiptap_island"`
- [x] **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 入口时接入。
- [x] **Step 1: 写 Playwright 截图与布局指标检查**
检查项固定为:左栏存在且宽度稳定、topbar 存在、主内容不被侧栏遮挡、右下浮动按钮不遮挡正文、根页没有极简欢迎页。
- [x] **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/done/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.
- [x] **Step 1: 更新 checklist 状态**
每完成一个 U 阶段,只更新对应 checklist 项,不把 owner cutover 结果重复写成 UI parity 完成。
- [x] **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`