feat: cut over rust web main shell

This commit is contained in:
lix-2026
2026-04-29 12:24:44 +08:00
parent 7965c6c107
commit 048fe28a4d
97 changed files with 9396 additions and 1263 deletions
@@ -1,6 +1,6 @@
# 3-1 [process] Rust Web 长期架构实施清单 v2
> 更新时间:2026-04-16
> 更新时间:2026-04-28
>
> 基于以下实际状态重写:
> - 当前未提交代码
@@ -56,11 +56,11 @@
- [ ] `Phase 5` AI runtime 仍然很重,只是懒挂载了
- [ ] `Phase 6` Mindmap 仍然是重前端交互壳,不是独立对象页壳
- [ ] `Phase 7` 文档页外围面板仍集中在 `DocumentContent`
- [ ] `Phase 8` 旧 Next/React 主路径完全没有完成切换
- [x] `Phase 8` 主入口已切到 `mnote-web`Next App Router 降为 legacy compat / island bundle source;彻底删除旧链仍留给后续清理
### 2.3 结论
> **当前真实状态不是“Phase 0 ~ 8 已全部完成”,而是“Phase 0 基本完成,Phase 1/2/4/5/6/7 处于不同程度的部分落地,Phase 3 Phase 8 仍远未完成”。**
> **当前真实状态不是“Phase 0 ~ 8 已全部完成”,而是“Phase 0 基本完成,Phase 1/2/4/5/6/7 处于不同程度的部分落地,Phase 3 仍需继续收口;Phase 8 已完成主入口 owner cutover,但旧 Next/React 链仍作为 legacy compat / island source 保留”。**
---
@@ -106,7 +106,7 @@
| Phase 5 | AI 面板 bridge island 化 | `PARTIAL` | host/runtime 拆分已做,且已开始把 `node` / `subtree` / `outline` / `evidence` 送入 Hermes,但 runtime 仍重,协议也未统一到真正“最小壳” |
| Phase 6 | Mindmap 独立对象化 | `PARTIAL` | 独立页脱离 editor stub 主入口,并补出 `standalone` / `documentBridge` 边界,但仍是客户端重壳 |
| Phase 7 | BlockNote 孤岛化 | `PARTIAL` | 阅读态已直接消费 `pageSubtree`,编辑器按需挂载,但外围 drawer/panel 仍集中在同一内容组件 |
| Phase 8 | 旧前端壳下线 | `NOT_STARTED` | 当前主入口仍是 Next/React,不能宣称主路径切换完成 |
| Phase 8 | 旧前端壳下线 | `PARTIAL` 接近 `DONE` | 3000 gateway / 文档 shell / tree realtime / Search / AI bridge / Mindmap object shell 已由 `mnote-web` 持有;Next App Router 降为 legacy compat / island bundle source,旧链彻底删除仍待后续 |
---
@@ -377,27 +377,26 @@
## 13. Phase 8:旧前端壳下线与兼容清理
**当前状态:`NOT_STARTED`**
**当前状态:`PARTIAL`,主入口 owner cutover 已完成**
### 13.1 直接证据
- [x] 当前主应用入口仍是 Next App Router
- [layout.tsx](/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/layout.tsx)
- [x] 当前主文档页仍是 Next 页面
- [page.tsx](/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx)
- [x] 当前主搜索、主 Sidebar、主 Mindmap 页都仍在旧前端壳内
- [x] `3000` 默认 owner 已是 `mnote-web` Rust gateway。
- [x] `/auth``/documents/:id``/api/tree/events``/search``/api/hermes/bridge``/mindmap/:docId/:mindmapId` 已具备 Rust Web owner / shell / transport contract。
- [x] `SKIP_NEXT_LEGACY=1` 可证明核心 shell 不依赖 Next legacy upstream。
- [x] Next App Router 已降为 legacy compat / React island bundle source,不再是主 Web 入口。
### 13.2 因此不能成立的说法
### 13.2 不能成立的说法
- [ ]Rust Web 主路径切换已完成
- [ ]双栈收缩已完成
- [ ]旧 Next/React 页面壳已清理
- [ ]旧 Next/React 代码已经删除
- [ ]所有重交互 runtime 都已迁出 React island
- [ ]OnlyOffice、复杂编辑器与历史 debug 链都已完全退役
### 13.3 v2 后续任务
- [ ] 先定义哪条真实流量先切到 `mnote-web`
- [ ] 先建立一条真实主路径,而不是只有 compat bridge
- [ ] 等真正有主路径后,再谈 Phase 8 清理
- [ ] 继续清理 legacy compat route 与旧 Next 页面。
- [ ] 为仍保留的 React islands 建立更细的删除 / 替换清单。
- [ ] 保持 `3104` 只在显式 legacy/debug 场景可见,不恢复为公开入口。
---
@@ -466,7 +465,7 @@
- `Phase 5`:只完成轻量拆分
- `Phase 6`:只完成独立页去耦第一步
- `Phase 7`:只完成阅读/编辑分离第一步
- `Phase 8`尚未开始
- `Phase 8`主入口 owner cutover 已完成,旧链清理继续推进
因此,从当前实际代码出发,接下来最需要的不是继续宣布完成,而是:
@@ -0,0 +1,258 @@
# 3-3 [process] Rust Web Tree Realtime Event Stream 方案 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`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
## 1. 目标
这份文档用于固定 Stage C-1 的正式实时链路口径:
- 保留 Convex 作为 realtime substrate
- Rust 成为 tree-first graph 的 semantic owner
- Rust Web 负责正式页面 transport 与实时事件流
- 前端只消费 projection 与 delta,不再消费实验壳真相
目标效果是接近网页版 wolai / notion 的体验:
- 快速进入页面
- 树结构实时同步
- 局部变化快速响应
- 浏览器端不持有第二套树真相
## 2. 职责划分
### 2.1 Convex substrate
Convex 继续承担:
- 持久化
- mutation / query 底座
- 实时订阅底座
- 文件 / 对象存储协作
Convex 在这里是:
> storage / realtime substrate
而不是页面树语义 owner。
### 2.2 Rust semantic owner
Rust kernel 负责:
- `page_tree / sidebar_tree / file_tree / subtree` 语义
- node / edge / projection 规则
- command / query 的语义收口
- domain event 的统一格式
- trace / audit / version 口径
Rust 在这里是:
> Rust semantic owner
### 2.3 Rust Web transport
Rust Web 负责:
- SSR 页面壳
- SSE / WS 主链
- workspace / subtree 订阅入口
- 把 Convex 订阅与 Rust domain event 连接起来
- 向前端输出稳定的 projection snapshot + delta stream
### 2.4 前端 projection consumer
前端只负责:
- 首屏渲染
- islands 交互
- optimistic UI
- 应用 delta 到本地 projection cache
前端不再负责:
- 重新定义树结构
- 重新计算页面树语义
- 维护实验壳 iframe 作为正式实时主链
## 3. 为什么当前 tree shell 不是正式实时链路
当前 `mnote-web tree shell` 仍然只是实验壳,原因有三点:
1. 它依赖 iframe / HTML shell / postMessage 交互。
2. 它的 command path 和状态边界更接近实验 viewer,而不是正式页面 transport。
3. 它没有成为首页、sidebar、文档页共享的正式实时订阅主链。
因此当前 tree shell 可以继续保留为:
- 实验验证壳
- 独立 tree viewer
- picker / filetree 的可选增强壳
但不能继续当成正式 realtime 主链。
## 4. 正式 realtime tree event stream 分层
正式链路建议固定为四层:
### 4.1 Convex 持久化/订阅底座
这里负责:
- 命令落账
- 持久化页面树状态
- 输出 mutation 后可订阅的数据变化
### 4.2 Rust kernel 语义 owner
这里负责:
- 把命令结果解释为 domain event
- 把底层 mutation 变化转成树域可消费的语义事件
- 维护 projection rebuild 与 delta 生成规则
### 4.3 Rust Web SSE/WS transport
这里负责:
- 暴露正式 `/api/tree/events` 或等价 stream 入口
- 管理 workspace / subtree 订阅
- 发送 snapshot、delta、cursor、ack、resync 信号
推荐策略:
- 首屏与重连优先使用 snapshot
- 连续变化优先推送 delta
- 大规模漂移或 cursor 失配时回退到 resync snapshot
### 4.4 前端 projection consumer
这里负责:
- 接收初始 projection snapshot
- 接收增量事件
- 合并到 sidebar / filetree / page subtree cache
- 必要时触发局部重渲染
## 5. 建议事件形状
正式事件建议至少覆盖三类 scope:
- workspace
- subtree
- tree delta
### 5.1 Workspace stream
用于:
- 当前工作区根树更新
- 垃圾桶、模板、共享页等全局区域变化
建议形状:
```json
{
"stream": "workspace",
"workspaceId": "ws_123",
"cursor": "evt_1001",
"kind": "snapshot|delta|resync",
"projection": "sidebar_tree",
"data": {}
}
```
### 5.2 Subtree stream
用于:
- 当前文档子树
- file tree 局部展开区域
- picker 只关注的局部节点集合
建议形状:
```json
{
"stream": "subtree",
"workspaceId": "ws_123",
"rootNodeId": "page_1",
"cursor": "evt_1002",
"kind": "snapshot|delta|resync",
"projection": "page_tree",
"data": {}
}
```
### 5.3 Tree delta payload
建议包含:
- `eventId`
- `commandId`
- `traceId`
- `aggregateType`
- `aggregateId`
- `op`
- `node`
- `parentNodeId`
- `position`
- `removedNodeIds`
- `changedProjectionKeys`
参考形状:
```json
{
"eventId": "evt_1003",
"commandId": "cmd_2001",
"traceId": "trace_3001",
"aggregateType": "page",
"aggregateId": "page_9",
"op": "create|rename|move|delete|restore|purge|embed",
"node": {
"id": "page_9",
"title": "新页面"
},
"parentNodeId": "page_root",
"position": 3,
"removedNodeIds": [],
"changedProjectionKeys": ["sidebar_tree", "file_tree"]
}
```
## 6. 与首页和首屏的关系
首页与 `/api/sidebar` 的 P0 原则必须保持不变:
- 首页不能再依赖 3104 实验壳
- `3000` 主入口必须可稳定进入 `/auth` 或文档页
- 实时链路失败时,首屏仍要有稳定 snapshot fallback
也就是说:
- 首屏可用性优先于实时增强
- realtime stream 是正式增强层,不是新的首屏阻塞点
## 7. 实施顺序
推荐顺序如下:
1. 先固定首页、sidebar、picker 不再依赖 tree shell 实验壳。
2. 再固定 Rust kernel 的 tree command / projection owner 身份。
3. 然后补正式 Rust Web SSE/WS tree stream。
4. 最后把 sidebar、page subtree、filetree 逐步切到统一 stream。
## 8. 最终固定口径
长期口径固定为:
- Convex substrate 不拆
- Rust semantic owner 收口语义
- Rust Web 提供正式实时 transport
- 前端只消费 projection snapshot 与 tree delta
- iframe/postMessage tree shell 不是正式 realtime 主链
@@ -0,0 +1,452 @@
# 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`
@@ -0,0 +1,222 @@
# 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/`
@@ -0,0 +1,591 @@
# 3 [process] Rust Web 长期架构方案 v1
> 更新时间:2026-04-16
>
> 相关文档:
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-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/process/ai-frontend-simplification-plan-v1.md`
## 1. 文档目的
本文回答的是长期架构问题,而不是短期优化问题:
> **当 mnote 要继续沿着 Rust 主导路线演进时,Web 层到底应不应该一起 Rust 化;如果要 Rust 化,应该如何分层,而不是只说一句“改成 Rust”。**
本文重点不是:
- 再做几处懒加载
- 再精简几个前端面板
- 再给现有 Next 页面补一些缓存
本文重点是:
- Rust 内核和 Web 承载层如何分工
- 前端哪些部分应该退出当前重 React 壳
- `axum``Leptos` 这类 Rust Web 方案是否值得采用
- `BlockNote` 应如何被隔离到最后处理
---
## 2. 先给结论
结论分三句:
### 2.1 不能只说“用 Rust”
如果只说“以后 Web 也改成 Rust”,但不明确:
- 谁负责 HTTP / SSE / WebSocket / 路由
- 谁负责 SSR 页面壳
- 谁负责 islands / hydration 模型
- 谁负责保留必须存在的浏览器重交互模块
那么最后大概率只是:
- Rust 内核继续存在
- 旧前端继续承担页面运行时主负担
这不叫根治。
### 2.2 长期推荐方案不是“只上 `axum`”,而是分层方案
长期推荐采用:
- **业务执行面:现有 Rust workspace 继续作为唯一业务真执行面**
- **Web 承载层:`axum`**
- **页面渲染模型:`Leptos Islands`**
- **AI 运行时:Hermes**
- **最后保留的重前端孤岛:`BlockNote`**
一句话概括:
> **mnote 的长期正确方向不是“Next 外壳上继续打补丁”,也不是“只把 API 改成 Rust”,而是“Rust 内核 + Rust Web 壳 + server-first 页面 + 少量交互孤岛”。**
### 2.3 `BlockNote` 不是第一刀,而是最后一刀
当前真正难替代的主要是:
- `BlockNote`
而下面这些能力,从长期看都可以先退出当前大前端壳:
- Sidebar / 页面树 / 文件树
- 搜索面板
- Mindmap
- AI 面板
- 评论 / 回链 / 历史 / 页面选项
- 大部分页面级壳与详情面板
所以长期路线应当是:
> **先把能 Rust 化、能 server-first 化、能 island 化的东西全部迁走,最后再处理 `BlockNote`。**
---
## 3. 为什么当前问题不能靠“只做 Rust 内核”解决
结合当前仓库状态,可以确认一个关键事实:
- `/mnt/Data1T/mnote/rust/` 已经是业务协议、桥接、CLI、Convex bridge 的 Rust workspace
- 但它现在还不是 Web 页面承载层
- 它没有直接接管文档页、页面树、搜索壳、阅读页 SSR、浏览器交互分层
这意味着当前 Rust 已经影响了:
- 业务命令面
- 查询面
- AI tools / bridge 的真执行面
- CLI 化能力
但它**还没有从根上决定“网页怎么加载”**。
所以之前用户体感到的页面切换卡顿,本质仍然主要来自:
- 页面运行模型
- 客户端挂载链
- 重编辑器与周边面板的初始化方式
而不是“后端是不是 Rust”这一件事本身。
换句话说:
> **Rust 内核已经在替换业务执行平面,但网页访问性能要根治,还必须补上 Rust Web 层与页面运行模型重构。**
---
## 4. 为什么长期要引入 Rust Web 层,而不是停在现状
如果继续维持现在的分工:
- Rust 只负责业务执行
- Next/React 继续负责几乎全部页面壳和交互壳
那么会长期存在三个问题。
### 4.1 页面访问链仍然受制于重前端应用模型
即使业务调用已经走 Rust,页面还是会在浏览器里经历:
- 布局挂载
- 客户端组件挂载
- 动态导入
- 面板初始化
- 编辑器初始化
这类成本不会因为底层业务改成 Rust 自动消失。
### 4.2 Web 层仍然会保留第二套复杂运行语义
如果页面系统继续主要靠当前大前端维护,那么:
- 页面壳逻辑
- 面板装配逻辑
- 浏览器状态拼装逻辑
- 部分对象读取与表现层规则
仍会长期留在现有前端系统里。
这会让“Rust 已成为唯一主线”变成一句不完整的话。
### 4.3 很难把页面访问做成真正的 server-first
用户真正想要的是:
- 页面先出来
- 阅读先可用
- 交互只在局部发生
如果继续沿用大范围客户端 hydration 的页面模型,这个目标很难彻底实现。
---
## 5. `axum` 在这个体系里的正确位置
根据 `axum` 官方文档,它非常适合承担:
- Router
- Handler
- Middleware
- JSON / Form / Query 提取
- WebSocket
- SSE
- 基于 `tokio` 的服务承载
这使它非常适合 mnote 长期担任:
- 主 API 层
- 文档查询与聚合层
- 树结构装配层
- 搜索服务层
- AI bridge / Hermes bridge 层
- 页面 SSR 外壳承载层
- 流式更新、通知、事件推送层
但也必须明确一件事:
> **`axum` 是优秀的 Rust Web 服务框架,但它不是页面交互模型本身。**
也就是说,`axum` 可以很好地承接:
- HTTP
- API
- 页面响应
- 流式接口
但它并不会直接回答:
- 哪些页面内容不该 hydrate
- 哪些交互应该变成 island
- `BlockNote` 之外还有多少东西需要进浏览器
所以:
> **长期方案不能只有 `axum`,还必须有“页面如何 server-first 输出、如何只给少量模块 hydration”的明确答案。**
---
## 6. 为什么推荐 `Leptos Islands`
根据 Leptos 官方 Islands 文档,它最重要的特征是:
- 默认可以让大量内容保持服务端输出
- 只有显式声明为 island 的部分进入客户端 hydration
- 父级服务端组件可以保留纯服务端逻辑
- 页面可以由少量交互岛包住,而不是整页一起变成大前端应用
这和 mnote 现在想解决的问题高度匹配。
因为 mnote 现在最需要的不是“再换一个前端框架”,而是:
- 把页面阅读恢复成轻页面
- 把交互缩到真正必要的模块
- 把大块浏览器运行时代码压缩成少量孤岛
Leptos Islands 很适合承接下面这类长期目标:
- 页面树 / 文件树作为轻交互 island
- 搜索输入与结果面板作为轻交互 island
- AI 面板作为独立 island
- 文档工具栏作为轻交互 island
- `BlockNote` 作为最后的重交互 island
- 其余阅读内容保持服务端输出
所以它的价值不是“因为它是 Rust 前端”,而是:
> **它天然支持 server-first + 少量 islands 的页面运行模型,这正是 mnote 当前最缺的东西。**
---
## 7. 为什么不建议只说“就 Rust”,也不建议直接押注别的 Rust 前端路线
### 7.1 不建议只说“就 Rust”
因为这句话缺少架构含义。
如果没有 Web 承载层和页面模型的明确选型,最后通常只会得到:
- Rust 业务内核
- 旧网页壳继续不变
这解决不了根因。
### 7.2 不建议把 `axum` 单独当成完整答案
`axum` 很强,但它解决的是:
- 服务承载
- HTTP / 流式能力
- 路由与 middleware
它不直接解决:
- islands
- 页面 hydration 边界
- 大前端退场策略
所以单独采用 `axum`,只够完成“Rust BFF / Rust API 化”,不够完成“页面运行模型重构”。
### 7.3 不把 Dioxus / Yew 作为当前主推荐
不是说它们不能用,而是它们不是当前最贴合目标的第一选择。
原因很简单:
- mnote 当前最核心的问题不是“缺一个 Rust UI 框架”
- 而是“如何让绝大多数页面不再变成整页重 hydration 应用”
从这个目标出发,`Leptos Islands` 比较直接地支持:
- server-first 页面
- 局部 hydration
- 少量交互孤岛
因此当前更适合作为第一推荐。
---
## 8. mnote 长期推荐分层
## 8.1 总体结构
长期推荐结构如下:
1. **Rust Core**
- `core-domain`
- `core-protocol`
- `bridge-runtime`
- `storage-convex-bridge`
- `index-fts`
2. **Rust Web**
- `axum` 作为主 HTTP / SSE / WS / API / SSR 承载
3. **Rust View Shell**
- `Leptos Islands` 作为页面壳、阅读态、轻交互 islands
4. **Browser Islands**
- 搜索框
- Sidebar 树
- AI bridge panel
- Mindmap 交互页
- `BlockNote` 编辑岛
5. **外挂或外部编辑器**
- OnlyOffice
- 后续可替换的在线表格 / 飞书文档插件
### 8.2 运行原则
长期运行原则应改成:
- 阅读优先
- 服务端先输出
- 浏览器只接必要交互
- 重交互能力单独孤岛化
- 业务规则全部留在 Rust 内核
### 8.3 实施治理口径
为了避免长期路线再次滑回“Rust 内核 + 重前端页面壳”的临时态,实施时必须额外固定四个治理口径:
1. **阶段边界固定**
- `Phase 0` 先冻结旧壳扩张、产出基线和 islands 候选
- `Phase 1` 先把 Rust Web 承载层立起来
- `Phase 2` ~ `Phase 6` 迁阅读页、Sidebar、搜索、AI、Mindmap
- `Phase 7` 最后处理 `BlockNote`
- `Phase 8` 再清理旧壳
2. **依赖顺序固定**
- 没有 `Phase 1` 的 Rust Web 入口,就不算真正进入长期主线
- 没有阅读态/编辑态分离,就不允许继续扩大编辑器默认挂载链
- 没有横向能力(trace、缓存、权限、回归脚本),就不允许每个模块各自发明运行语义
3. **横向能力固定**
- 统一 `request_id` / `trace_id`
- 统一缓存与预取口径
- 统一页面壳、对象、AI bridge 的权限边界
- 统一回归脚本和性能采样口径
4. **最终验收固定**
- 文档打开先看到阅读态,而不是编辑器 loading
- Sidebar/搜索/AI/Mindmap 已退出当前重前端主壳
- Rust Web 已能承接主 API、页面壳和主流式链路
- `BlockNote` 已降级为最后的重交互孤岛
对应的执行细节、逐阶段清单和可验证项,当前应先以 `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md` 确认优先级;`/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md` 保留为 Rust Web 分阶段全景参考。
### 8.4 当前已落地的最小里程碑(2026-04-16)
到目前为止,长期路线里已经有几条可以被源码直接证明的最小里程碑:
- Rust Web 承载层已经有 `mnote-web` crate 骨架,`router / middleware / request context / SSE / WS / Hermes bridge` 的主接缝已立住。
- 文档页已经从“默认先进编辑器”改成“先阅读、后编辑”:服务端先读 `meta + content`,阅读态单独渲染,`BlockNote` 只在进入编辑态后挂载。
- Sidebar 已形成“服务端首包 + 客户端局部 island”的最小边界,导航数据聚合契约不再散落在布局层。
- SearchPalette 与页面级 AI 面板都已经收成轻 host + 按需 runtime island,重量运行态不再默认跟随主布局常驻。
- Global AI 继续保留为实验入口,但不再回到 app layout 主链,避免长期路线再次滑回“全局大面板常驻”模式。
- Mindmap 已经具备独立页能力,文档内嵌形态也已降为轻预览/轻交互入口。
---
## 9. 各模块长期归位建议
### 9.1 文档阅读页
目标:
- 先直接返回可阅读页面
- 不等待编辑器初始化
- 不让评论、历史、AI、回链等阻塞正文出现
归位:
- 页面壳迁到 `axum + Leptos`
- 文档阅读内容默认服务端输出
- 编辑入口点击后才挂载编辑 island
### 9.2 Sidebar / 页面树 / 文件树
目标:
- 从当前大布局中的重量级客户端组件,变成轻交互 island
归位:
- 数据查询与聚合走 Rust
- 页面壳服务端输出
- 展开、拖拽、快捷过滤、局部刷新才在 island 内运行
### 9.3 搜索系统
目标:
- 搜索框与搜索结果只做局部交互
- 不再把整个页面切换都拖进搜索运行时
归位:
- 索引、召回、聚合全部 Rust 化
- 搜索输入与结果列表作为 island
- 搜索页本身保持 server-first
### 9.4 AI 面板
目标:
- 继续桥接 Hermes
- 不再承担前端本地 orchestration
- 不再成为常驻大壳的一部分
归位:
- 面板只保留最小 UI 与上下文桥接
- 工具执行全部走 Hermes + Rust tools
- 面板按页面需要懒挂载成单独 island
### 9.5 Mindmap
目标:
- 从 BlockNote 自定义块逻辑中进一步解耦
- 变成独立对象和独立页面能力
归位:
- 对象读写继续走 Rust
- 独立页面优先改成 Rust Web 壳 + 独立交互岛
- 文档内嵌版本退化为预览卡片或轻交互嵌入
### 9.6 OnlyOffice
目标:
- 维持外挂页面定位
归位:
- 继续独立页面或外部容器打开
- 只保留必要的签名、代理、回调
- 不参与主文档访问性能判断
### 9.7 `BlockNote`
目标:
- 最终变成单独的重交互编辑岛
归位:
- 阅读页不默认依赖它
- 编辑态按需挂载
- 它之外的能力尽量不再绑在同一前端运行时里
---
## 10. 为什么这是“根治路线”,不是“临时路线”
因为它改的不是几个组件,而是四个底层前提:
### 10.1 改的是页面运行模型
从:
- 先进入前端应用
变成:
- 先进入页面
### 10.2 改的是业务归属
从:
- Web 层和业务层长期混写
变成:
- 业务规则只留在 Rust
### 10.3 改的是浏览器职责
从:
- 浏览器承担整页大运行时
变成:
- 浏览器只承担必要的交互岛
### 10.4 改的是迁移顺序
从:
- 一上来就想替掉最难的 `BlockNote`
变成:
- 先清走外围大块能力
- 最后再处理 `BlockNote`
---
## 11. 推荐迁移顺序
长期上推荐按下面顺序推进,而不是乱序推进。
### Phase A:先把 Rust Web 层立起来
目标:
- 建立 `axum` 主承载层
- 建立 Rust 侧统一 Web 入口
- 接住 API、SSE、WS、文档 SSR 壳
此阶段不追求一次性替换全部页面,只追求:
- 先把“Rust 也能承接 Web 层”这件事落地
### Phase B:先迁轻页面与高频结构页
优先对象:
- Sidebar
- 页面树 / 文件树
- 搜索页
- 文档阅读页壳
- AI 面板壳
这些东西的特点是:
- 高频访问
- 强烈影响页面切换体感
- 又不像 `BlockNote` 那么难替换
### Phase C:把 Mindmap 独立化
目标:
- 让 Mindmap 从当前 BlockNote 共生结构里进一步脱离
- 独立页面优先 Rust 化
- 文档内嵌形态缩成轻版本
### Phase D:最后处理 `BlockNote`
只有当前三阶段基本稳定后,才建议处理:
- `BlockNote` 编辑器本体
- 文档编辑态与阅读态的最终分离
- 自定义 block 的最终重构边界
---
## 12. 最终建议
如果只问一句“是不是要参考 Rust 的 Web 框架,比如 `axum`,还是只要 Rust 就行”,最终答案是:
> **要参考,而且必须明确采用 Rust Web 分层;不能只说“就 Rust”。**
更具体的推荐是:
- **不是**“只保留现有 Rust 内核,网页继续主要靠当前重前端”
- **不是**“只引入 `axum` 做 API,然后页面模型不变”
- **而是**“Rust 内核 + `axum` Web 承载 + `Leptos Islands` 页面壳 + `BlockNote` 最后孤岛化”
这是当前最符合 mnote 长期目标的路线,因为它同时满足:
- 真正减少页面访问时的前端运行负担
- 继续强化 Rust 作为唯一业务执行平面
- 支持绝大部分功能逐步 CLI 化、服务化、AI 可编辑化
- 不要求一开始就碰最难替换的 `BlockNote`
---
## 13. 官方依据
本方案涉及的 Rust Web 判断,主要基于以下官方资料方向:
- `axum` 官方文档与 README
- 重点能力:Router、Handler、Middleware、JSON、WebSocket、SSE、`tokio` 服务承载
- Leptos 官方文档的 Islands 章节
- 重点能力:只有显式 island 进入客户端 hydration,其余内容可保持服务端输出
因此本文的判断不是“因为 Rust 很快所以推荐 Rust”,而是:
> **因为 mnote 需要的是 server-first 页面模型、少量交互孤岛、统一业务执行面,而 `axum + Leptos Islands + Rust Core` 正好能形成这套长期结构。**