# 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_`。 - 页面正文 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. [ ] 若清理历史幽灵副本,必须经用户确认,并保留清理前后证据。