20260513 mindmap优化01

This commit is contained in:
lix-2026
2026-05-13 22:43:16 +08:00
parent 17c003976b
commit b4a452a8b7
89 changed files with 11557 additions and 707 deletions
@@ -0,0 +1,117 @@
# 4-25 [process][bug] 树命令新建/删除后强制刷新导致卡顿 v1
> 更新时间:2026-05-13
>
> 分类归属:
> - `04-tree-domain/process`
> - 关联缺陷:`bugs/05-editor-mainline/process/5-11-mindmap-ghost-assets-and-tree-command-latency-v1.md`
>
> 用户反馈:
> - “删除页面和新建页面都很慢,不知道为什么这么卡。”
## 1. 问题定义
当前页面树/文件树的新建页面、删除页面等命令,在服务端命令成功后仍会触发整页刷新或完整树刷新。用户感知是:新建和删除并不是局部更新,而是卡一下、等整套页面壳或树壳重新加载。
这属于 `tree command -> projection update -> UI apply` 链路问题,应归到 `04-tree-domain`,而不是只作为编辑器 UI 问题处理。
## 2. 真实现象
用户反馈:
1. 新建页面慢。
2. 删除页面慢。
3. 卡顿与同轮 mindmap 文件树重复增长一起出现,怀疑树刷新链路被频繁触发。
期望结果:
1. `tree.node.create` 成功后,树 UI 局部插入新节点并进入重命名/导航状态。
2. `tree.node.archive` 成功后,树 UI 局部移除节点或移动到回收站 projection。
3. 只有 delta 无法应用、projection 缺失、SSE 断线或数据不一致时,才进入 resync/reload fallback。
4. 普通新建/删除不应默认整页 reload。
## 3. 证据
### 3.1 Rust tree shell 有硬刷新路径
`rust/crates/mnote-web/src/routes/tree.rs:2976``:2986`
- `scheduleRefresh()` 在 80ms 后执行 `window.location.reload()`
- 如果带 `renameRowId`,则通过 `window.location.assign(url.toString())` 重新加载。
调用点:
- 文件树删除:`rust/crates/mnote-web/src/routes/tree.rs:2791``:2824`
- 新建页面:`rust/crates/mnote-web/src/routes/tree.rs:4309``:4340`
- 重命名:`rust/crates/mnote-web/src/routes/tree.rs:4373``:4406`
- 移动:`rust/crates/mnote-web/src/routes/tree.rs:4448``:4502`
这条路径会让 tree shell 命令体验明显慢于纯 delta / reducer 更新。
### 3.2 主页面已有 tree live controller,但命令后仍刷新
主页面已经监听 tree live
- `rust/crates/mnote-web/src/ssr/pages/layout.rs:3881``:3917` 处理 `tree:snapshot` / `tree:delta` / `tree:resync`
- `rust/crates/mnote-web/src/ssr/pages/layout.rs:3939``:4050` 启动 `/api/tree/events` EventSource
也就是说,命令完成后理论上可以由 delta/resync 更新 projection;当前硬 reload 与 live projection 机制重复。
### 3.3 命令本身已有 delta hint
Bridge runtime 对树命令生成 stream delta hint
- `tree.node.create``rust/crates/bridge-runtime/src/lib.rs:9545``:9592`
- `tree.node.archive``rust/crates/bridge-runtime/src/lib.rs:9655``:9702`
当前前端没有把这些结果作为默认局部更新来源,而是在 tree shell 中继续 reload。
### 3.4 React Sidebar 路径也存在重刷链
子代理只读调查发现 React Sidebar 路径也存在重刷:
- 新建页面:`wolai-frontend/src/components/sidebar/sidebar.tsx:1799``:1847` 附近在 command 后 `await refreshTree()` 再跳转。
- 删除页面:`wolai-frontend/src/components/sidebar/sidebar.tsx:2148` 附近删除后等待 command、`refreshTree()`、广播文档变化/跳转。
- Next API 的 `api/tree/commands` create/delete 还会执行 workspace/default scaffold、bridge mutation、artifact 记录等多段流程。
这说明卡顿不只来自单个 reload,而是命令后刷新链路偏重。
## 4. 当前判断
当前慢的核心不是“Convex 一定慢”,而是树命令执行后缺少轻量本地 apply:
1. 命令 result / delta hint 已经具备局部更新信息。
2. UI 仍经常走 `refreshTree()``window.location.reload()` 或 full snapshot。
3. `resync_required` 类事件会触发完整 workspace snapshot,结构性变化越多,越容易造成卡顿。
## 5. 建议验证
修复前先补一条计时 smoke
1. 登录真实测试账号。
2. 新建 `TEST-TREE-LATENCY-<timestamp>` 页面。
3. 记录 `POST /api/tree/commands` 响应耗时。
4. 记录下一次 `tree:delta` / `tree:resync` 到达耗时。
5. 断言过程中是否发生 `window.location.reload()` 或顶层 navigation。
6. 删除该测试页面,重复同样计时。
7. 输出新建/删除从点击到 DOM 稳定的总耗时。
## 6. 建议修复方向
1. `tree.node.create` 成功后直接用 command result 插入本地节点,并只在后台等待 live delta 校准。
2. `tree.node.archive` 成功后直接从当前 projection 移除节点,并只在后台等待 live delta 校准。
3. `scheduleRefresh()` 改成显式 fallback,只有 reducer 无法应用时才调用。
4. React Sidebar 的 `refreshTree()` 应去重、节流,并避免与 SSE/full snapshot 同时触发。
5. smoke 增加无 reload 断言和耗时阈值。
## 7. 流转条件
当前状态:`process`
只有满足以下条件后才能移动到 `bugs/04-tree-domain/done/`
1. [ ] 新建页面普通路径不再默认整页 reload。
2. [ ] 删除页面普通路径不再默认整页 reload。
3. [ ] command result / tree delta 能局部更新页面树与文件树 projection。
4. [ ] fallback reload 只在明确异常条件下触发,并有可观测标记。
5. [ ] smoke 记录新建/删除耗时,并通过无 reload 断言。
@@ -0,0 +1,154 @@
# 5-10 [done][bug] 文件树思维导图打开污染 index.md 单一真源 v1
> 更新时间:2026-05-13
>
> 分类归属:
> - `05-editor-mainline/done`
> - 涉及边界:`04-tree-domain/file_tree asset intent`、`03-rust-web/mindmap standalone shell`、`06-mindmap/projection editor`
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/10-review/README.md`
> - `/mnt/Data1T/mnote/design/10-review/02-frontend-editor-tree-review.md`
> - `/mnt/Data1T/mnote/design/10-review/04-secondary-domains-and-design-governance-review.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
> - `/mnt/Data1T/mnote/design/06-mindmap/process/6-mindmap-kernel-phase6-projection-editor-v1.md`
## 1. 问题定义
当前文件树中的思维导图文件点击打开后,会进入 standalone `/mindmap/{documentId}/{mindmapId}` 壳。这个壳会构造一份只包含单个 mindmap block 的临时 editor bootstrap,而不是读取该页面真实的 `index.md` / Page Aggregate body。
结果是:用户从文件树打开思维导图、编辑、切换页面后,再点击同一页面的 `index.md`,可能看到只剩思维导图默认内容的页面体验。这里的核心问题不是单次保存失败,而是 `index.md` 页面正文与文件树里的 mindmap asset 没有明确收口到同一份页面真源。
## 2. 归类理由
本问题归到 `05-editor-mainline`,而不是只归到 `06-mindmap`,理由如下:
- 用户可见故障发生在文档页壳、文件树点击打开、主编辑区内容切换体验上。
- 受影响对象是 `index.md` 页面正文与主编辑区当前内容,而不只是 mindmap runtime 内部编辑能力。
- `design/10-review` 已明确当前 Page Aggregate 仍是“主链已切、单一真源收口未完”的状态,本问题正是页面正文、asset view 与 standalone shell 边界未闭合的具体缺陷。
- `04-tree-domain` 需要定义 filetree asset row 的 open intent,但真正承载用户体验和页面真源一致性的 owner 仍是编辑主线。
## 3. 真实现象
用户复现结论:
1. 打开文件树中的思维导图文件,编辑后可以正常保存。
2. 切换页面后,文件树中的思维导图仍能保存。
3. 切换页面后点击文件树中的 `index.md`,主编辑区只显示思维导图默认内容,而不是该页面真实正文。
用户判断的根因方向:
1. 之前对文件树中的思维导图文件点击操作会对主页面产生干扰,应取消该干扰。
2. 思维导图文件的点击打开应该在主编辑区弹出新页面或新 tab,类似 VS Code,而不是污染 `index.md`
3. 当前文件树 `index.md` 与其中的思维导图没有只持有一份真源,需要收口成单一来源。
## 4. 证据
设计审查证据:
- `design/10-review/README.md` 明确当前最需要收口的是 `Page Aggregate` 单一真源边界、tree realtime/live cache、side effect 一致性,以及 legacy/compat/fallback 退场边界。
- `design/10-review/02-frontend-editor-tree-review.md` 的 F-01 指出 Page Aggregate 读链已 Rust-first,但客户端仍有本地 aggregate reducer 持有标题、正文、设置、子树快照等临时真相,页面域单一真源尚未闭环。
- `design/10-review/04-secondary-domains-and-design-governance-review.md` 的 Mindmap 段落指出 standalone mindmap shell 已存在,但 compat 数据层仍承载导图数据,不应被描述为长期 canonical truth。
代码锚点:
- `rust/crates/mnote-web/src/ssr/pages/layout.rs:1100` 定义 `buildMindmapOpenPath(documentId, assetId)`,直接生成 `/mindmap/{documentId}/{assetId}`
- `rust/crates/mnote-web/src/ssr/pages/layout.rs:1128``:1134``openConvexAssetFromFileTree` 在识别到 mindmap asset 后直接 `window.location.assign(buildMindmapOpenPath(documentId, assetId))`
- `rust/crates/mnote-web/src/ssr/pages/layout.rs:1180``:1182``tree.asset.open` 统一交给 `openConvexAssetFromFileTree`
- `rust/crates/mnote-web/src/ssr/pages/layout.rs:3551``:3555` 文件树点击 `index/document/markdown` 时导航到文档页,但非文档 row 且有 `assetId` 时 dispatch `tree.asset.open`mindmap asset 因而进入 standalone mindmap 路由。
- `rust/crates/mnote-web/src/routes/mindmap_shell.rs:75``:104` standalone mindmap shell 构造临时 `editor_bootstrap.content`,内容只有一个 `mnoteBlockType: "mindmap"` 的 paragraph block。
- `rust/crates/mnote-web/src/routes/mindmap_shell.rs:105``:123` standalone contract 标记为 `mnote.mindmap_shell.v1`projection source 仍是 `compat-blob`
- `wolai-frontend/src/components/sidebar/sidebar-navigation.ts:21``:33` 的 React 侧 `buildSidebarMindmapOpenTarget``main` 模式下返回 `/documents/{documentId}`,只有 `sidebar` 模式才打开 `/mindmap/{documentId}/{mindmapId}`;这说明至少已有一条路径表达了“主编辑区不应直接进入 standalone mindmap shell”的倾向。
## 5. 当前判断
当前更像是三条路径混在一起:
1. `index.md` 应代表页面正文,并由 Page Aggregate body / page command 持有。
2. 文件树中的 mindmap asset 应代表页面内某个 mindmap block 关联的资源视图或编辑入口。
3. standalone `/mindmap/{doc}/{mindmap}` 是过渡兼容壳,应作为独立编辑页或调试/弹出入口,不应替代主编辑区的 `index.md` 真源。
因此,即使 mindmap asset 自己可以保存,也不能说明 `index.md` 与 mindmap block 已经同源。当前故障的关键是:打开 asset 的动作把用户带进了会伪造临时正文的 shell,导致页面主编辑体验看起来像 `index.md` 被替换成默认 mindmap。
## 6. 建议修复方向
### 阶段一:立即止血
1. 取消文件树 mindmap asset 点击直接 `window.location.assign('/mindmap/{doc}/{asset}')` 的主路径行为。
2. 点击 `index.md` 必须始终打开真实 `/documents/{documentId}?treeView=filetree`,并读取真实 Page Aggregate body。
3. mindmap asset 点击只能发出明确的 asset-open intent,不应改写当前页面正文、不应伪造 `index.md` 内容、不应让主编辑区误认为当前正文就是 standalone bootstrap。
4. 保留 `/mindmap/{doc}/{asset}` 作为显式独立页面入口时,应只用于新窗口、新 tab、调试入口或明确的 asset editor,不作为普通文件树主点击默认行为。
### 阶段二:主编辑区 asset tab
建议把文件树里的 mindmap asset 打开为主编辑区内的 asset tab / preview,而不是替换 `index.md`
- 标签形态类似 `index.md | mindmap.json`
- `index.md` tab 只显示真实页面正文。
- `mindmap.json` 或 mindmap asset tab 使用 `{ documentId, mindmapId }` 加载 mindmap projection。
- asset tab 保存只写 mindmap command / projection 对应资源,不创建第二份页面正文。
- 关闭 asset tab 后回到 `index.md`,正文内容应保持不变。
### 阶段三:Page Aggregate 与 block-asset 单一真源
长期应把 mindmap asset 与页面正文 block 关系收口到 Page Aggregate / kernel projection
1. 页面正文中的 mindmap block attrs 持有稳定 `mindmapId`
2. 文件树 mindmap asset row 来自同一份 page aggregate/resource projection,而不是另起一套对象真相。
3. mindmap asset 的删除、恢复、移动、重命名应同步维护 page body block 与 resource projection 的关系。
4. standalone shell 如果继续存在,也必须声明自己是 asset editor,不再构造会被误认为 `index.md` 的页面正文 bootstrap。
## 7. 建议 smoke 验证
修复完成后至少补一条文件树 mindmap 回归 smoke
1. 登录真实测试账号。
2. 新建测试页面,插入或生成一个 mindmap block,记录 `documentId``mindmapId`
3. 从文件树点击该页面下的 mindmap asset。
4. 断言主编辑区没有导航到会污染 `index.md` 的 standalone bootstrap;如果打开 asset tab,则 tab 标识与 `index.md` 分离。
5. 在 mindmap 中输入一段较长中文内容并保存。
6. 切换到其它页面,再从文件树点击原页面 `index.md`
7. 断言 `index.md` 仍显示真实页面正文,且页面内 mindmap block 仍引用同一个 `mindmapId`
8. 再打开 mindmap asset,断言刚才输入的长中文内容仍存在。
需要保留一条负向断言:
- 普通文件树主点击 mindmap asset 不应触发 `window.location.assign('/mindmap/{doc}/{asset}')` 并替换当前文档页正文。
## 8. 修复证据
本问题已按 `4-24``5-12` 收口:
- `rust/crates/core-protocol/src/kernel.rs` 增加 `KernelObjectIdentity` / `KernelBlockAssetRelation`
- `rust/crates/bridge-runtime/src/lib.rs` 的 file tree projection 为 `index.md`、mindmap、OnlyOffice、代码附件、普通附件输出不同 `resourceMeta.objectIdentity`
- `rust/crates/mnote-web/src/tree_shell/filetree_renderer.rs``rust/crates/mnote-web/src/routes/tree.rs``rust/crates/mnote-web/src/ssr/pages/layout.rs``objectIdentity` 下发到文件树 DOM 与 `tree.asset.open` intent。
- `/mindmap/{documentId}/{mindmapId}``rust/crates/mnote-web/src/ssr/pages/mindmap.rs` 明确标记为 `data-mnote-object-editor="mindmap"``data-mnote-object-identity="resource:mindmap:{documentId}:{mindmapId}"`
- `rust/crates/mnote-web/src/routes/mindmap_shell.rs` 的 standalone bootstrap 使用 `__mindmap_object__:{documentId}:{mindmapId}`,不复用真实页面正文草稿身份。
验证命令:
```bash
cd /mnt/Data1T/mnote/rust && cargo test -p core-protocol --test resource_tree_contract -- --nocapture
cd /mnt/Data1T/mnote/rust && cargo test -p bridge-runtime file_tree_projection -- --nocapture
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web file_tree_projection -- --nocapture
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web mindmap -- --nocapture
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web documents_save_route_executes_page_body_save_command -- --nocapture
cd /mnt/Data1T/mnote/rust/spikes/leptos-tiptap-spike && cargo test standalone_mindmap_object_uses_isolated_draft_identity -- --nocapture
node scripts/task112-tree-rust-family-regression-smoke.js
node scripts/task169-mindmap-realtime-smoke.js
```
`task169` 已覆盖:文件树 mindmap asset 打开、长中文编辑、保存、切页、从文件树回 `index.md`、再次打开 mindmap,且未触发 `page.body.save` 污染链。
## 9. 流转条件
当前状态:`done`
只有在以下条件都满足后,本文才能移动到 `bugs/05-editor-mainline/done/`
1. [x] 文件树 mindmap asset 普通点击不再污染 `index.md` 主编辑区。
2. [x] `index.md` 与页面内 mindmap block 的关系有明确单一真源,至少不再出现切页后只剩默认导图的现象。
3. [x] `/mindmap/{doc}/{asset}` 的产品定位被明确为独立页、新 tab、debug 或 asset editor,不再被文件树主路径误用。
4. [x] smoke 覆盖长中文 mindmap 编辑、保存、切页、回到 `index.md`、再次打开 asset 的完整链路。
5. [x] 浏览器复测确认 mindmap 编辑可用性优先,不因实时刷新或路由切换导致闪烁、跑位或内容错乱。
@@ -0,0 +1,193 @@
# 5-11 [process][bug] Mindmap 幽灵附件增长与树命令卡顿 v1
> 更新时间:2026-05-13
>
> 分类归属:
> - `05-editor-mainline/process`
> - 涉及边界:`04-tree-domain/tree command + file_tree projection`、`06-mindmap/runtime save + resource relation`
>
> 用户证据:
> - `/mnt/Data1T/mnote/tmp/image copy 94.png`
> - `/mnt/Data1T/mnote/tmp/image copy 93.png`
## 1. 问题定义
用户在页面中只是修改了一下 mindmap,文件树/资源树下却陆续出现多个 `mindmap-mindmap...` 附件行。截图显示同一页面 `新页面3` 下有 `index.md`,并且 mindmap 附件从 2 条增长到 4 条。
同一轮反馈还指出:删除页面和新建页面都很慢,表现为树操作后明显卡顿。
这不是单纯的图标显示问题。当前症状同时暴露两条链路风险:
1. mindmap 编辑/初始化/保存链路会多次向资产层广播同一个资源存在,且缺少“页面正文 block 与 mindmap asset 关系唯一”的硬约束。
2. tree shell 的页面新建、删除、重命名、移动仍在命令成功后强制整页刷新,和当前 live projection/SSE 机制重复,导致用户感知卡顿。
## 2. 真实现象
已观察到的用户现象:
1. 新页面下初始只有 `index.md` 和少量 mindmap 附件。
2. 用户只是编辑 mindmap,不是主动新建 mindmap。
3. 等一会儿或再次修改后,同一页面下又出现新的 `mindmap-mindmap...` 行。
4. 页面新建和删除动作响应慢,像是页面/树整体重新加载。
期望结果:
1. 一个页面内的一个 mindmap block 只对应一个稳定 `mindmapId` 和一个 file tree asset row。
2. mindmap 保存只能更新既有资源,不应创建新的 mindmap 资产行。
3. file tree projection 应按 `{documentId, blockId, assetId}` 或明确 object identity 去重。
4. 页面新建/删除成功后应优先消费 command result / tree delta 更新局部投影,不应默认整页 reload。
## 3. 初步调查证据
### 3.1 mindmap 资产广播存在多入口
`wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx` 中同一个 mindmap 资源至少有三处会触发资产刷新广播:
- 保存成功后广播 `emitAssetsChanged(docId, { id: mindmapId, asset_type: "mindmap", ... })``MindmapBlock.tsx:1499``:1534`
- 初始同步 `createOnly: true` 成功后广播:`MindmapBlock.tsx:1591``:1621`
- mindmap 实例就绪后立即广播:`MindmapBlock.tsx:1636``:1647`
另一个 legacy/compat block wrapper 也会在 mount 时执行 `createOnly` 并广播资产:`MindmapBlock.tsx:3573``:3612`
这些广播本身用 `id = mindmapId`,理论上同 ID 会被 sidebar 本地 state 去重;但只要保存/转换链路让同一视觉 mindmap 换了新的 `mindmapId`,就会生成新的资产行。
### 3.2 mindmapId 仍可能由时间戳生成
当前 `leptos-tiptap` 插入 mindmap 时使用:
- `rust/spikes/leptos-tiptap-spike/src/lib.rs:5681``:5689`
这里 `next_mindmap_id()` 生成 `mindmap_{Date.now()}`,并写入 paragraph attrs 的 `mindmapId`。如果后续转换、保存、重新挂载中丢失原 attrs,fallback 会用新的 block identity / 新插入节点创建新的 `mindmapId`,资产层就会认为这是另一个 mindmap。
相关转换锚点:
- `rust/crates/mnote-web/src/routes/web_shell.rs:818``:838`legacy block 转 TipTap 时写入 `mindmapId`
- `rust/crates/mnote-web/src/routes/web_shell.rs:983``:1009`TipTap 节点转 editor block 时若 attrs 缺失则用 blockId fallback
- `wolai-frontend/src/lib/documents/tiptap-content-converter.ts:188``:213``mindmapReferenceProps` 会在缺少 `mindmapId` 时 fallback 到 blockId
- `wolai-frontend/src/lib/documents/tiptap-content-converter.ts:410``:418`editor block 转 TipTap 时把 mindmap props 写回 paragraph attrs
当前缺少一条回归断言:连续编辑同一个 mindmap 后,保存前后 `mindmapId` 必须保持不变,且 file tree 下同一页面 mindmap asset 数量不增长。
### 3.3 Convex mindmaps 表允许同页多 mindmap,但缺少 block 关系唯一约束
`wolai-frontend/convex/schema.ts:164``:183` 定义 mindmaps 表,并以 `(document_id, mindmap_id)` 做查询索引。注意这里是普通索引,不是唯一约束。
`wolai-frontend/convex/mindmaps.ts``put` 对同一 `(document_id, mindmap_id)` 是幂等 patch/insert,但它并不知道页面正文中的哪个 block 才是唯一来源。也就是说:
- 同一个 `mindmapId` 重复保存不会多插入。
- 如果历史或并发路径已经写出同一 `(document_id, mindmap_id)` 的多行,`put` 当前用 `.first()` 只会 patch 第一行,剩余重复行仍会被后续 list/projection 展示。
- 但如果前端生成了新的 `mindmapId`,后端会按合法新 mindmap 插入。
- file tree 会把同一 document 下所有 active mindmaps 映射为资产行。
对应映射:
- `wolai-frontend/convex/mindmaps.ts:206``:245``put` 查询 `by_doc_mindmap``.first()`,不存在则 insert
- `wolai-frontend/convex/sidebar.ts:79``:100``mindmap_id` 被映射为 `id``mindmap-{mindmap_id}.json`
- `wolai-frontend/convex/sidebar.ts:193``:220`:所有 active mindmaps 都进入 `mindmap_assets`
- `rust/crates/bridge-runtime/src/lib.rs:7569``:7647`file tree projection 从 `mindmap_assets` 构建资源行,并只按 asset id 去重
因此,当前有两个需要实测区分的分支:
1. 同一视觉对象被保存成多个不同 `mindmapId`,每个都被合法展示为一个资源。
2. 数据表中已经存在同一 `(document_id, mindmap_id)` 多行,`.first()` 更新掩盖重复行,sidebar list 把重复行全部暴露出来。
两者都会在截图中表现为同一页面下多个 `mindmap-mindmap...` 行。
### 3.4 Rust / Next API 都会把 mindmap 保存转成 tree resync
保存路径还会触发树刷新:
- Next API `POST /api/mindmap/[docId]/[mindmapId]` 构造 `mindmaps.put``mindmap.command.apply``wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts:229``:254`
- Rust API 普通保存也包装为 `mindmaps.put` 并携带 `createOnly``rust/crates/mnote-web/src/routes/mindmap_api.rs:177``:200`
- Bridge runtime 给 `mindmaps.put` 生成 `resync_required` 树事件:`rust/crates/bridge-runtime/src/lib.rs:9273``:9326`
这意味着 mindmap 每次保存都会推动资源树重新读 projection;如果底层 mindmaps 数据已经重复,保存/刷新会把重复行显性化。
## 4. 页面新建/删除慢的证据
tree shell 在多处命令成功后调用 `scheduleRefresh()`,而 `scheduleRefresh()` 的实现是 80ms 后整页 reload 或带 `renameRowId` 重新 assign
- `rust/crates/mnote-web/src/routes/tree.rs:2976``:2986`
调用点包括:
- 文件树删除:`rust/crates/mnote-web/src/routes/tree.rs:2791``:2824`
- 新建页面:`rust/crates/mnote-web/src/routes/tree.rs:4309``:4340`
- 重命名:`rust/crates/mnote-web/src/routes/tree.rs:4373``:4406`
- 移动:`rust/crates/mnote-web/src/routes/tree.rs:4448``:4502`
与此同时,主页面已经有 tree live controller 接收 `snapshot` / `delta` / `resync`
- `rust/crates/mnote-web/src/ssr/pages/layout.rs:3881``:3917`
- `rust/crates/mnote-web/src/ssr/pages/layout.rs:3939``:4050`
这会造成两种低效叠加:
1. 命令结果本身已经返回 `tree.node.created` / `tree.node.archived` 等 delta hint。
2. 前端仍然整页刷新,重新加载 sidebar、file tree、workspace shell、编辑器 runtime。
这解释了“新建页面和删除页面都很慢”的用户感知。
## 5. 当前判断
当前根因尚需实测最终确认,但静态代码已经能支持以下判断:
1. mindmap ghost asset 增长的高概率根因是 `mindmapId` 稳定性没有被端到端锁死。只要编辑/保存/重挂载过程中 attrs 丢失或重新创建 block,就会插入新 `mindmap_{timestamp}`,后端会合法保存,file tree 会合法展示。
2. 另一个可疑根因是 `mindmaps` 表没有唯一约束,`put` 只 patch `.first()`,无法清除或阻止同键重复行。
3. 资产广播入口过多会放大问题。它们让新 mindmapId 或重复数据几乎立即进入 sidebar 本地 state 和 Convex projection,用户就会看到“自己又新增了一个”。
4. file tree projection 只按 `asset.id` 去重,无法识别“同一 document + 同一正文 block 的多个 mindmapId 其实是幽灵副本”,也无法处理同 id 多行投影的上游异常。
5. 页面新建/删除慢不是 Convex 单点问题,当前 tree shell 命令成功后仍强制 reload,是明确的性能和体验缺陷。该慢操作应单独归入 `04-tree-domain` 跟踪,本文只保留关联证据。
## 6. 建议复现与验证
建议补一条失败优先 smoke,先不要直接修代码:
1. 使用真实测试账号登录 `http://localhost:3000/auth`
2. 新建测试页面,记录 `documentId`
3. 插入一个 mindmap,记录首个 `mindmapId`
4. 连续修改 mindmap 中心主题或新增节点 3 次,每次等待保存完成。
5. 切到文件树,统计该页面下 `asset_type=mindmap` 的行数和 `data-mnote-object-identity`
6. 刷新页面后再次统计。
7. 断言同一页面下 mindmap asset 数量仍为 1,且 `mindmapId` 未变化。
8. 同一脚本计时新建页面、删除页面从点击到 DOM 稳定的耗时,并记录是否触发 `window.location.reload()` / navigation。
建议同时检查 Convex 数据:
- `mindmaps` 中同一 `document_id` 下是否出现多个 `mindmap_id`
- 新增出来的 `mindmap_id` 是否都形如 `mindmap_<timestamp>`
- 页面正文 TipTap JSON 中是否只有一个 mindmap paragraph,且 attrs.mindmapId 是否随保存变化。
## 7. 建议修复方向
### 阶段一:防止继续增长
1. mindmap block 创建时生成一次稳定 `blockId``mindmapId`,后续保存、转换、重挂载必须保留。
2. `documents.save` / TipTap converter 对 `mnoteBlockType=mindmap` 增加 contract:缺少 `mindmapId` 时不得静默新建时间戳 ID,应先从 block identity / object identity 恢复,恢复不了则报可观测错误。
3. `mindmaps.put` 或上层 command 增加可选 `blockId` / `blockAssetRelation`,对同一 `{documentId, blockId}` 已有关联 mindmap 时拒绝插入第二个 mindmapId。
4. asset 广播入口收口:保存成功、initial createOnly、实例就绪不应都伪造 asset;前端应优先消费 kernel/file tree projection 返回的 object identity。
### 阶段二:清理幽灵副本
1. 提供只读诊断脚本:列出同一页面下多个 mindmapId、对应 updated_at、正文引用的 mindmapId。
2. 只有在用户确认后,才可清理未被页面正文引用的 mindmap 行。
3. 清理必须进入 bug 修复 checklist,不能在本缺陷说明阶段直接删除数据。
### 阶段三:树命令性能收口
1. `tree.node.create` 成功后用 command result / delta 局部插入节点,而不是 reload。
2. `tree.node.archive` 成功后用 `remove_document` delta 局部移除节点。
3. 只有 projection 丢失、SSE 断线或 reducer 无法应用时才走 resync/reload fallback。
4. smoke 增加“无整页 reload”断言和耗时阈值。
## 8. 流转条件
当前状态:`process`
只有满足以下条件后才能移动到 `bugs/05-editor-mainline/done/`
1. [ ] 已有 smoke 能稳定复现当前 mindmap 资产增长问题,或能证明 Convex 数据中存在同页多 mindmap 幽灵副本。
2. [ ] 同一 mindmap 连续编辑保存后,`mindmapId` 和 file tree object identity 保持稳定。
3. [ ] 同一页面下未主动新建多个 mindmap 时,file tree 只出现一个 mindmap asset row。
4. [ ] 新建页面和删除页面不再默认整页 reload,或至少有明确 fallback 条件。
5. [ ] 浏览器实测记录新建/删除耗时,并明显低于当前整页 reload 体验。
6. [ ] 若清理历史幽灵副本,必须经用户确认,并保留清理前后证据。
@@ -0,0 +1,114 @@
# 5-9-C12 [process][bug] 文档页顶栏左上角切换侧栏按钮无效 v1
> 更新时间:2026-05-12
>
> 分类归属:
> - `05-editor-mainline/process`
> - 对应体验主线:`5-9 Wolai-aline continuous checklist`
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-7-wolai-page-tree-main-editor-experience-restoration-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
## 1. 问题定义
当前 `3000` 文档页顶部左上角的 `切换侧栏` 按钮可以被点击,但点击后左侧 sidebar 没有收起,再次点击也没有重新展开的状态变化。
这不是页面树节点的展开/折叠问题,而是文档页壳层的 `sidebar shell toggle` 缺失。
## 2. 归类理由
本问题归到 `05-editor-mainline`,而不是 `04-tree-domain`,理由如下:
- 问题入口位于文档页顶栏,不在 `page_tree/file_tree` 行内交互层。
- 问题影响的是页面壳、顶栏和侧栏显隐体验,属于 `Wolai 页面树与主编辑器体验复刻` 主线。
- `04-tree-domain` 更关注树 projection、row model、selection/focus、keyboard、DnD、tree command 等合同;本文问题不在这些运行时合同里。
## 3. 真实现象
复现路径:
1. 打开 `http://127.0.0.1:3000/auth`
2. 点击 `测试账号快速登录`
3. 进入文档页后,点击顶栏左上角 `切换侧栏`
4. 观察左侧 sidebar 是否收起
5. 再点击一次,观察是否重新展开
实际结果:
- 第 1 次点击后,左侧 sidebar 仍然完整可见
- 第 2 次点击后,左侧 sidebar 仍然完整可见
- 页面 URL 不变
- 浏览器 console 未出现报错
- 关键 network 未出现与 toggle 对应的新请求
期望结果:
- 第 1 次点击应收起左侧 sidebar
- 第 2 次点击应恢复展开
- 行为应与 Wolai / 常规文档页壳的 sidebar toggle 一致
## 4. 证据
截图证据:
- 切换前:`/mnt/Data1T/mnote/tmp/hermes-tester/manual-before-toggle.png`
- 点击一次后:`/mnt/Data1T/mnote/tmp/hermes-tester/manual-after-toggle.png`
- 点击两次后:`/mnt/Data1T/mnote/tmp/hermes-tester/manual-after-second-toggle.png`
Hermes tester 首轮取证:
- `/mnt/Data1T/mnote/tmp/hermes-tester/first-run/screenshots/01-auth-entry.png`
- `/mnt/Data1T/mnote/tmp/hermes-tester/first-run/screenshots/02-after-quick-login.png`
窄范围侧栏取证:
- `/mnt/Data1T/mnote/tmp/hermes-tester/sidebar-run/01-auth.png`
- `/mnt/Data1T/mnote/tmp/hermes-tester/sidebar-run/02-after-login-before-toggle.png`
代码锚点:
- 顶栏按钮渲染位于 `/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:3938`
- 当前仅能找到按钮渲染与样式,未看到对应的显隐切换绑定:
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:3938`
- `/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/styles.rs:1940`
## 5. 当前判断
当前更像是:
- 顶栏按钮已经进入 SSR 页面壳
-`sidebar open/collapsed` 状态未接线
- 或按钮没有绑定到实际的 sidebar toggle handler
也就是说,这是一条“控件已渲染,但未驱动真实状态”的缺陷,而不是视觉误差。
## 6. 建议完成方式
完成本缺陷至少需要满足下面四条:
1. 顶栏 `切换侧栏` 按钮绑定真实的 sidebar toggle 状态
2. 第 1 次点击后 sidebar 收起
3. 第 2 次点击后 sidebar 再展开
4. 增补可回归 smoke,覆盖:
- 登录后点击 toggle
- 收起态存在明确 DOM/样式状态
- 再次点击后恢复展开
建议验收同时包含:
- 代码验证:相关 Rust SSR / 前端状态接线
- 浏览器 smoke:真实点击与状态断言
- 截图复核:收起前 / 收起后 / 再展开后三张图
## 7. 流转条件
当前状态:`process`
只有在以下条件都满足后,本文才能移动到 `bugs/05-editor-mainline/done/`
1. 真实代码已修复
2. smoke 已覆盖且通过
3. 浏览器复测确认收起/展开都生效
4. 证据路径已补齐到修复后的截图或日志
+34
View File
@@ -0,0 +1,34 @@
# bugs 缺陷索引
> 更新时间:2026-05-12
>
> 状态口径以当前仓库真实代码与真实验证结果为准:
> - `[done]`:问题已修复,且已通过代码、smoke、浏览器或截图证据验证
> - `[process]`:问题已确认存在,仍在修复、验证或等待收口
## 分类方式
- `bugs/` 默认镜像 `design/` 的主线分类方式。
- 当前主线缺陷按对应大类放置,例如:
- `01-tree-first-graph-kernel/`
- `02-convex-rust-long-term-architecture/`
- `03-rust-web/`
- `04-tree-domain/`
- `05-editor-mainline/`
- `06-mindmap/`
- `07-ai/`
- 每个大类继续按 `process/``done/` 分层。
## 归类原则
- 缺陷归类以真正 owner 和长期主线为准,不以表面症状命名。
- `sidebar``topbar``breadcrumb`、文档页壳、Wolai 体验对齐、页面树与主编辑器整体体验问题,优先归到 `05-editor-mainline/`
- `projection``row model``selection/focus model``keyboard``DnD``tree command``page_tree/file_tree/sidebar_tree` 协议问题,优先归到 `04-tree-domain/`
- `transport`、route、SSR page shell owner、compat/bridge 边界问题,按 owner 判断是否落到 `03-rust-web/``05-editor-mainline/`
## 流转规则
- 新确认的缺陷先进入对应大类的 `process/`
- 缺陷在真实代码中修复并完成对应验证后,必须移动到对应大类的 `done/`
- 不允许在 `process/``done/` 同时保留同一条缺陷。
- 若后续发现旧缺陷记录归类错误,应直接迁移到正确大类,而不是在错误大类继续追加。