# 5-12 [done] 主编辑区 Object Tab 与 Resource Tree 对齐执行清单 v1

> 更新时间：2026-05-13
>
> 2026-05-15 口径更新：
> - 本文件中的 `index.md` tab / row 表述是完成当时用于隔离页面正文与资源对象的历史命名。
> - Convex File Tree 默认可见页面正文行已由 `/mnt/Data1T/mnote/design/04-tree-domain/done/4-35-convex-filetree-title-md-source-alignment-v1.md` 覆盖为 `doc:<documentId>` + `{title}.md`；后续 UI、smoke 与 review 不应再把可见 `index.md` 子行作为目标模型。
>
> 上游依据：
> - `/mnt/Data1T/mnote/design/10-review/05-tree.md`
> - `/mnt/Data1T/mnote/bugs/05-editor-mainline/done/5-10-mindmap-filetree-index-single-truth-split-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-24-resource-tree-filetree-pagetree-source-contract-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/06-mindmap/reference/6-mindmap-kernel-phase6-projection-editor-v1.md`

## 1. 目标

本清单用于把主编辑区从“只等于页面正文编辑器”推进为 workspace object tab host：

- `index.md` tab 只编辑 Page Aggregate body。
- mindmap tab 只编辑 mindmap projection / command。
- OnlyOffice tab 只编辑对应附件对象。
- 代码附件 / 普通附件 tab 只编辑或预览对应资源对象。
- 所有 tab 使用独立 `ObjectIdentity`、草稿身份和保存命令，不互相伪装。

## 2. 当前优先级

P0 是关闭当前 mindmap 文件树打开污染 `index.md` 的缺陷。

P1 是建立主编辑区 object tab 的最小协议，让后续 mindmap、OnlyOffice、附件都能进入同一套打开/关闭/保存/恢复模型。

P2 是把 Page Aggregate 的 block-asset relation 与 Resource Tree projection 串起来。

执行前置：

- 必须先按 `design/04-tree-domain/done/4-24-resource-tree-filetree-pagetree-source-contract-checklist-v1.md` 固定 Resource Tree / File Tree / Page Tree 的真源合同。
- 本文件不定义新的资源归属真相，只消费 `4-24` 输出的 `ObjectIdentity`、`resourceMeta` 和 `tree.asset.open` intent。
- P0 可以作为止血先落地，但 P0 中所有草稿 key、保存命令、打开路径都必须与 `4-24` 的 object identity 兼容。

当前关键代码锚点：

- `rust/crates/mnote-web/src/ssr/pages/layout.rs`：file tree asset 打开事件、`buildMindmapOpenPath`、`openConvexAssetFromFileTree`、`tree.asset.open` listener。
- `rust/crates/mnote-web/src/routes/mindmap_shell.rs`：standalone mindmap shell 与临时 bootstrap。
- `rust/crates/mnote-web/src/ssr/pages/mindmap.rs`：mindmap object editor 页面标记与 command form。
- `rust/crates/mnote-web/src/routes/mindmap_api.rs`：mindmap command / put 保存链。
- `rust/crates/mnote-web/src/routes/documents.rs`：`page.body.save` 页面正文保存链。
- `rust/spikes/leptos-tiptap-spike/src/lib.rs`：文档页 island 草稿、bootstrap、保存 runtime。
- `scripts/task169-mindmap-realtime-smoke.js`：当前 mindmap 长中文、保存、实时刷新 smoke，应升级为本 bug 的主验收脚本。

## 3. P0：mindmap object editor 止血清单

### 3.1 打开路径

- [x] 文件树 mindmap asset 普通点击必须打开明确的 mindmap object editor。
  - 当前允许继续使用 `/mindmap/{documentId}/{mindmapId}`。
  - 该路由必须声明自己是 object editor / asset editor。
  - 不允许被 `index.md` 吞掉。
  - 不允许把临时 mindmap-only bootstrap 视为真实页面正文。
  - 推荐实现口径：
    - 保留 `/mindmap/{documentId}/{mindmapId}` 作为独立 object editor 路由。
    - 在 HTML 根节点或主容器加可测标记，例如 `data-mnote-object-editor="mindmap"`、`data-mnote-object-identity="resource:mindmap:{documentId}:{mindmapId}"`。
    - file tree 点击 mindmap 后，若继续整页导航，必须让页面显式进入 mindmap object editor，而不是复用文档页正文 island 的页面草稿 key。
    - 后续 object tab host 接入后，再把整页导航替换为主编辑区 tab 打开。

- [x] `index.md` 点击必须只打开真实文档页。
  - 目标为 `/documents/{documentId}?treeView=filetree`。
  - 读取来源为 Rust `/api/page-aggregate/{documentId}`。
  - 不读取 mindmap standalone 草稿。
  - 推荐检查点：
    - `layout.rs` 中 file tree row click 对 `index/document/markdown` 的分支。
    - `leptos-tiptap` island 初始化时是否优先使用服务端 Page Aggregate bootstrap。

### 3.2 草稿与保存身份

- [x] standalone mindmap 的草稿 key 必须使用 object identity。
  - 推荐格式：`__mindmap_object__:{documentId}:{mindmapId}`。
  - 禁止使用真实页面的 `workspaceId:documentId` 草稿 key。
  - 推荐检查点：`rust/spikes/leptos-tiptap-spike/src/lib.rs` 内 localStorage 草稿读写 key 生成逻辑。
  - 负向要求：打开 `/mindmap/{documentId}/{mindmapId}` 后，localStorage 中不得新增或覆盖文档正文草稿 key。

- [x] 文档页有服务端 bootstrap content 时，不得被 localStorage 旧草稿覆盖。
  - 如果存在历史坏草稿，文档页应优先使用 Page Aggregate body。
  - 打开文档页后应把坏草稿覆盖或隔离，不让其继续污染后续打开。
  - 推荐策略：
    - 当 bootstrap contract 是 `mnote.page_aggregate.v1` 时，优先采用 bootstrap content。
    - 对与 mindmap object key 匹配的草稿直接隔离，不参与文档页正文恢复。
    - 对历史坏草稿只允许在同一 `ObjectIdentity` 下恢复，不允许跨 `page:index` 与 `resource:mindmap` 恢复。

- [x] mindmap 保存只走 mindmap command。
  - 保存命令：`mindmap.command.apply` 或当前正式 mindmap put/apply 链。
  - 禁止触发 `page.body.save`。
  - 推荐检查点：
    - `rust/crates/mnote-web/src/routes/mindmap_api.rs`
    - `rust/crates/mnote-web/src/transport/convex.rs`
    - `wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts`
  - 负向测试：mindmap object editor 保存期间拦截或记录网络请求，不能出现 `/api/documents/save` 或 canonical `page.body.save`。

### 3.3 真实网页 smoke

- [x] 更新 `scripts/task169-mindmap-realtime-smoke.js`。
  - 新建测试页面。
  - 插入或生成 mindmap block。
  - 从文件树点击本轮 `documentId + mindmapId` 对应 mindmap asset。
  - 断言进入 mindmap object editor，而不是 `index.md`。
  - 输入长中文并保存。
  - 切到其他页面。
  - 再点击原页面 `index.md`。
  - 断言 `index.md` 仍显示 Page Aggregate body。
  - 再打开 mindmap asset。
  - 断言长中文仍存在。
  - 测试数据前缀：`TEST-T169-MINDMAP-OBJECT-<timestamp>`。
  - 必须记录并断言：
    - `documentId`
    - `mindmapId`
    - mindmap object editor URL 或 tab identity
    - `index.md` 返回 URL
    - 长中文内容保存后的重开结果

- [x] 负向断言。
  - 点击 mindmap asset 后不得调用 `page.body.save`。
  - 打开 `index.md` 后不得加载 `__mindmap_object__` 草稿。
  - mindmap live refresh 不得在长中文编辑过程中重挂载或覆盖输入。
  - 修复前失败时 bug 留在 `bugs/05-editor-mainline/process/5-10-mindmap-filetree-index-single-truth-split-v1.md`；本轮通过后已移动到 done。

### 3.4 P0 建议实施顺序

- [x] Step 1：先补 smoke 红灯。
  - 修改：`scripts/task169-mindmap-realtime-smoke.js`。
  - 目标：当前实现应能暴露 `index.md` 与 mindmap object editor 身份混淆或草稿污染风险。
  - 运行：`node scripts/task169-mindmap-realtime-smoke.js`。
  - 预期：修复前至少一个负向断言失败，或者脚本明确输出当前路径仍有污染风险。

- [x] Step 2：标记 mindmap object editor 身份。
  - 修改：`rust/crates/mnote-web/src/routes/mindmap_shell.rs`、`rust/crates/mnote-web/src/ssr/pages/mindmap.rs`。
  - 目标：`/mindmap/{documentId}/{mindmapId}` 输出明确 object editor contract，不能被识别为 Page Aggregate body。
  - 验收：页面 DOM 能被 smoke 定位到 `data-mnote-object-editor="mindmap"` 或等价稳定标记。

- [x] Step 3：隔离 mindmap 草稿 key。
  - 修改：`rust/spikes/leptos-tiptap-spike/src/lib.rs`。
  - 目标：mindmap standalone 使用 `__mindmap_object__:{documentId}:{mindmapId}`；文档页使用 `page:index:{documentId}` 或现有页面草稿 key，但不得互读。
  - 验收：smoke 检查 localStorage 中 mindmap key 与 page key 分离。

- [x] Step 4：确认保存命令隔离。
  - 修改：优先不改实现，先用测试确认；如失败，再收口 `mindmap_api.rs` / `documents.rs` / transport adapter。
  - 目标：mindmap 保存只走 `mindmap.command.apply`，`index.md` 保存只走 `page.body.save`。
  - 验收：网络请求和 command result 中的 canonical command 分别正确。

- [x] Step 5：跑完整 smoke 并补 bug 证据。
  - 运行：`node scripts/task169-mindmap-realtime-smoke.js`。
  - 预期：长中文编辑、保存、切页、回 `index.md`、再开 mindmap 全链路通过。

## 4. P1：Object Tab Host 最小合同

### 4.1 Tab Identity

- [x] 定义主编辑区 tab identity。
  - `page:index:{documentId}`
  - `resource:mindmap:{documentId}:{mindmapId}`
  - `resource:onlyoffice:{documentId}:{assetId}`
  - `resource:attachment:{documentId}:{assetId}`
  - `resource:code:{documentId}:{assetId}`

- [x] 每个 tab 必须声明：
  - `objectKind`
  - `documentId`
  - `blockId`
  - `assetId`
  - `title`
  - `dirtyState`
  - `saveCommand`
  - `closeBehavior`

### 4.2 Tab 打开规则

- [x] `index.md` row 打开 `page:index:{documentId}`。
- [x] mindmap row 打开 `resource:mindmap:{documentId}:{mindmapId}`。
- [x] OnlyOffice row 打开 `resource:onlyoffice:{documentId}:{assetId}`。
- [x] 代码附件 row 打开 `resource:code:{documentId}:{assetId}`。
- [x] 普通附件 row 打开 `resource:attachment:{documentId}:{assetId}`。
  - 输入来源只能是 `4-24` 的 file tree row `resourceMeta.objectIdentity` 或等价字段。
  - 如果字段缺失，允许临时由 `resourceKind + documentId + assetId` 推导，但必须在代码注释或测试名中标明是兼容推导，不得作为新真源。

### 4.3 Tab 保存规则

- [x] Page tab 保存只调用 `page.body.save`。
- [x] Mindmap tab 保存只调用 `mindmap.command.apply`。
- [x] OnlyOffice tab 保存只通过 OnlyOffice callback / forcesave。
- [x] Code tab 保存只写对应 asset。
- [x] Attachment preview tab 默认无正文保存行为。
  - 负向规则：任一 resource tab 保存不得调用 `page.body.save`，除非该操作明确修改的是 Page Aggregate body 中的引用 block。

### 4.4 P1 建议实施顺序

- [x] Step 1：定义 tab identity 类型。
  - 首选落点：`rust/crates/core-protocol/src/kernel.rs` 或 `rust/crates/core-protocol/src/editor/model.rs`。
  - 前端适配落点：`wolai-frontend/src/components/sidebar/tree-shell-dom-model.ts`、`wolai-frontend/src/components/sidebar/sidebar-navigation.ts`。

- [x] Step 2：让 file tree open intent 携带 tab identity。
  - 修改：`rust/crates/mnote-web/src/routes/tree.rs`、`rust/crates/mnote-web/src/ssr/pages/layout.rs`。
  - 验收：`tree.asset.open` detail 中包含 `objectIdentity` 或可无歧义生成 object identity 的字段。

- [x] Step 3：主编辑区 host 消费 tab identity。
  - 初期允许只有 `page:index` 与 `resource:mindmap` 两类 tab。
  - 不要求一次完成 OnlyOffice / code / attachment 的 UI，但必须保留类型位。

- [x] Step 4：补 tab identity 单测。
  - 建议测试文件：`wolai-frontend/src/components/sidebar/sidebar-navigation.test.ts`、`wolai-frontend/src/components/sidebar/tree-shell-host.test.tsx`。
  - 断言：`index.md` 与 mindmap asset 打开生成不同 tab identity。

## 5. P2：Page Aggregate 与 Block-Asset Relation

- [x] 页面正文中的 mindmap block attrs 必须持有稳定 `mindmapId`。
- [x] Page Aggregate 输出 mindmap block 与 resource relation。
- [x] File Tree mindmap row 来源于同一 relation。
- [x] 删除 mindmap block 时，资源关系必须进入 detach / archive 计划。
- [x] 删除 mindmap asset 时，页面正文 block 必须进入明确处理计划：删除 block、保留占位、或标记资源缺失，不能静默断链。
  - 优先落点：`rust/crates/core-protocol/src/page_aggregate.rs`、`rust/crates/bridge-runtime/src/lib.rs`、`wolai-frontend/src/lib/documents/tiptap-content-converter.ts`。
  - 验收：同一个 `mindmapId` 能同时从 Page Aggregate block attrs 与 File Tree resource row 追踪到。

## 6. P3：OnlyOffice / 附件 / 插件对象推广

- [x] OnlyOffice 附件打开复用 Object Tab identity。
- [x] 代码附件编辑复用 Object Tab identity。
- [x] 普通附件预览复用 Object Tab identity。
- [x] 插件型对象只能通过 block-asset relation 接入页面正文，不能把插件 runtime data 当 Page Aggregate body。
  - OnlyOffice 参考落点：`rust/crates/mnote-web/src/routes/onlyoffice.rs`、`scripts/task174-rust-onlyoffice-attachment-open-smoke.js`。
  - 上传 / 普通附件参考落点：`rust/crates/mnote-web/src/routes/media.rs`、`scripts/task175-rust-upload-entry-smoke.js`。

## 7. 验收命令

最低验收顺序：

```bash
node scripts/task169-mindmap-realtime-smoke.js
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
```

扩展验收：

```bash
pnpm --dir wolai-frontend test -- sidebar-navigation.test.ts tree-shell-host.test.tsx
node scripts/task174-rust-onlyoffice-attachment-open-smoke.js
node scripts/task175-rust-upload-entry-smoke.js
```

如本地服务未启动，先执行：

```bash
npm run desktop:hot
```

真实浏览器 smoke 必须使用默认测试账号 `mnote.e2e@example.com` / `MnoteE2E123!`，并优先从 `http://localhost:3000/auth` 的“测试账号快速登录”进入。

## 8. Done Gate

本文移动到 `done/` 前必须满足：

- [x] 当前 mindmap bug 文档 `5-10` 可移动到 `bugs/05-editor-mainline/done/`。
- [x] 真实浏览器 smoke 证明 mindmap 文件树点击、长中文编辑、保存、切页、回 `index.md`、再开 mindmap 全链路正常。
- [x] `index.md`、mindmap、OnlyOffice、附件的 object identity 已在打开路径和保存路径中区分。
- [x] 至少 mindmap 与 `index.md` 已完成草稿隔离。
- [x] 文件树打开行为不再让 asset 伪装成页面正文。

## 9. 完成证据

- Object identity：`rust/crates/core-protocol/src/kernel.rs` 定义 `KernelObjectIdentity`，file tree projection 与 DOM 传递 `page/index/mindmap/only_office/code/attachment` identity。
- Mindmap object editor：`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` 使用 `__mindmap_object__:{doc}:{mindmap}` bootstrap identity；`rust/spikes/leptos-tiptap-spike/src/lib.rs` 的 `standalone_mindmap_object_uses_isolated_draft_identity` 通过。
- 保存隔离：`cargo test -p mnote-web mindmap -- --nocapture` 与 `cargo test -p mnote-web documents_save_route_executes_page_body_save_command -- --nocapture` 通过。
- 真实 smoke：`node scripts/task169-mindmap-realtime-smoke.js` 通过，覆盖文件树 mindmap 打开、长中文编辑、保存、切页、回 `index.md`、再打开 mindmap。

## 10. 完成后回填

实现完成后需要回填以下位置：

- `bugs/05-editor-mainline/done/5-10-mindmap-filetree-index-single-truth-split-v1.md`：已补真实修复证据并移动到 `done/`。
- `design/04-tree-domain/done/4-24-resource-tree-filetree-pagetree-source-contract-checklist-v1.md`：已标记 `ObjectIdentity` 与 file tree row contract 被编辑区消费。
- `design/10-review/05-tree.md`：补最终采用的 object editor / object tab 方案摘要。
