# 5-28 Leptos Tiptap 编辑器 Runtime 模块拆分 v1

> 创建时间：2026-05-24
> 状态：`done`
>
> 触发背景：
> - `bugs/0524.md` 第 4 条删除本地 Markdown 附件链接后 `Ctrl+Z/Ctrl+Y` 不可用，根因是编辑器删除路径绕过 Tiptap/ProseMirror history。
> - 修复过程中 CodeGraph 能索引 Rust 符号，但 `rust/spikes/leptos-tiptap-spike/src/lib.rs` 单文件约一万行，职责过多，主控和 worker 都需要反复字面搜索。
> - `design/03-rust-web/done/3-19-rust-web-browser-runtime-module-extraction-v1.md` 解决 Rust Web shell/browser host runtime 外置化；本设计补齐编辑器主线拆分。

## 1. 结论

`leptos-tiptap-spike/src/lib.rs` 的问题不是 raw string，而是编辑器 runtime 仍以单个巨型 Rust 文件承载过多职责：Tiptap 初始化、命令执行、附件上传、附件链接增强、history、块菜单、DOM overlay、mindmap node view、page options、AI surface 都混在一起。CodeGraph 对这种文件只能给出符号位置，不能给出清晰模块边界。

后续不应继续在该文件中追加新的编辑器行为。应分阶段把编辑器命令、附件链接/上传、history 安全命令、DOM bridge/overlay、mindmap node view 拆入独立 Rust 模块；保持 `leptos-tiptap` island 仍是当前文档页主编辑器 runtime，但让每个行为域能被单独搜索、测试、派发给 worker。

## 2. 当前代码基线

- `rust/spikes/leptos-tiptap-spike/src/lib.rs`
  - 约一万行。
  - 同时包含 CSS、Tiptap extension 列表、slash menu、toolbar、block menu、table toolbar、attachment upload、attachment link handling、local folder path、history/undo、DOM selection、mindmap node view、runtime bridge。
  - 最近第 4 条 bug 的关键函数和逻辑散落在同一文件中，例如附件链接识别、keydown 处理、`apply_html_update`、`sync_persisted_editor_command`、`delete_top_level_block_html`。

- `reference-code/leptos-tiptap/`
  - 提供底层 Tiptap Rust/JS bridge。
  - 已有 `TiptapEditorHandle`、`TiptapExtension`、`TiptapRange`、`delete_selection`、`delete_range`、`undo`、`redo` 等命令。
  - MNote 不应把这些底层 bridge 再复制一遍，而应在 spike runtime 上方建立 MNote 编辑器行为模块。

## 3. 与 3-19 的边界

`03-rust-web` 负责：

- Rust Web SSR shell。
- document pane host adapter。
- resource tab / secondary pane / URL / conflict panel 生命周期。
- filetree / resource open / local upload 的 shell 侧适配。

`05-editor-mainline` 负责：

- Tiptap 编辑器内部命令。
- 编辑器内附件链接和上传块行为。
- undo/redo/history 安全边界。
- 编辑器 DOM selection / overlay / toolbar / block menu。
- Tiptap node view，例如 mindmap block。

交界规则：

- Shell 可以告诉 editor “打开某个文档/资源/保存状态/挂载 pane”。
- Editor 可以告诉 shell “内容变更/附件打开 intent/资源上传完成/冲突状态”。
- Shell 不应直接改 Tiptap document 内部结构。
- Editor 不应决定 workspace tree/resource 归属真相。

## 4. 目标

- 把 `src/lib.rs` 从“巨型编辑器 runtime”收敛为组件入口和模块装配层。
- 让附件、history、命令、DOM bridge 等高频 bug 区域成为独立 Rust 模块。
- 修 bug 时能把 worker 限定到单一模块和单一 smoke。
- 统一编辑器命令合同：能用 Tiptap command/transaction 的行为，不再通过 `set_content`/HTML 整文替换实现用户编辑动作。
- 建立 history 安全基线：删除、插入、重排、格式转换、附件操作都要明确是否进入 undo stack。

## 5. 非目标

- 不替换 Tiptap 或 `leptos-tiptap` bridge。
- 不把文档事实源迁回前端。
- 不一次性重写编辑器 UI。
- 不在本设计中实现 Phase C 流式 apply / review session。
- 不把 Page AI panel 或 Hermes/Reasonix 工具链混入本次编辑器 runtime 拆分。

## 6. 目标模块布局

建议新增目录：

```text
rust/spikes/leptos-tiptap-spike/src/
  lib.rs
  editor_runtime/
    mod.rs
    extensions.rs
    command_sync.rs
    history_safe_commands.rs
    attachment_links.rs
    attachment_upload.rs
    local_markdown_paths.rs
    dom_selection.rs
    overlays.rs
    block_menu.rs
    table_toolbar.rs
    mindmap_node_view.rs
    bridge_events.rs
    persistence.rs
```

第一阶段可以只拆高频问题模块，不强求一次建完所有文件。

## 7. 分阶段执行清单

归档说明（2026-05-25）：本节保留原始分阶段路线，但不再作为当前 `done/` 状态的未完成 checklist。已经满足归档条件的范围以第 13 节 Done Gate 为准；仍未实施或被明确划到 `03-rust-web` / 后续编辑器切片的条目改为普通状态项，后续需要另起设计或 bug/checklist 承接，不能在本 done 文档中继续以未勾选 checklist 表达。

### P0：热点函数归属表和基线保护

- 已记录：当前 `src/lib.rs` 中最近 bug 高频函数的归属模块：
  - `selected_local_attachment_link_in_editor_state` -> `attachment_links.rs`
  - `selected_local_attachment_link_text` -> `attachment_links.rs` / `dom_selection.rs`
  - `sync_persisted_editor_command` -> `command_sync.rs`
  - `apply_html_update` / `apply_document_update` -> `command_sync.rs`，并标记 history 风险
  - `delete_top_level_block_html` / `reorder_top_level_block_html` -> 待替换为 `history_safe_commands.rs`
    - 2026-05-24：`delete_top_level_block_html` 已删除，块菜单删除改走 `delete_top_level_block_with_history(...)` -> Tiptap `delete_range` / `insert_content_at`。
  - upload slash action / local asset path -> `attachment_upload.rs` / `local_markdown_paths.rs`
  - toolbar/block menu DOM anchor -> `overlays.rs` / `block_menu.rs`
- 后续：建立 history 风险清单；所有用户编辑动作中仍调用 `set_content` 或 HTML 整文替换的路径必须登记。
- 已保留：当前已通过的 `scripts/task488-local-attachment-link-delete-undo-smoke.js` 作为 RED/GREEN 基线。

### P1：抽出附件链接和 history-safe delete

- [x] 新增 `editor_runtime/attachment_links.rs`。
- [x] 迁出本地附件链接识别：
  - DOM selection fallback。
  - Tiptap editor state link attributes 判断。
  - `/api/local-folder/files/open` 链接识别。
- [x] 新增 `editor_runtime/history_safe_commands.rs`。
- [x] 将“选中附件链接文本后 Delete/Backspace”收口为命名函数，例如 `delete_selected_attachment_link_with_history(...)`。
- [x] 验证 `task488` 仍通过。

### P2：抽出 command sync / persistence

- [x] 新增 `editor_runtime/command_sync.rs`。
- [x] 新增 `editor_runtime/persistence.rs`。
- [x] 迁出：
  - `read_editor_snapshot`
  - `sync_editor_outputs`
  - `sync_persisted_editor_command`
  - `apply_document_update`（当前保持零调用者现状）
- 后续：迁出 change/status event dispatch 的编辑器侧包装。
  - 2026-05-24 复核：`apply_html_update` 仍依赖 `ChangePayload` / `ChangeMetaPayload`、`dispatch_change_event`、`dispatch_runtime_state`、`HoveredBlockState` 和 toolbar overlay state，暂留 `lib.rs`，等待 P4 `bridge_events.rs` / `overlays.rs` 后再迁。
- [x] 明确 `set_content` 只允许用于：
  - 初始加载
  - host 替换文档
  - 外部同步/恢复
  - 非用户编辑的兼容兜底
- [x] 用户编辑动作必须优先走 Tiptap command/transaction。
  - 2026-05-24：块菜单删除已从 `apply_html_update` / `set_content(TiptapContent::html(...))` 迁到 `history_safe_commands.rs` 的 `delete_top_level_block_with_history(...)`，并由 `scripts/task489-block-menu-delete-undo-smoke.js` 覆盖 Ctrl+Z/Ctrl+Y。

### P3：抽出附件上传与本地 Markdown 路径

- [x] 新增 `editor_runtime/attachment_upload.rs`。
- 不纳入本设计当前归档范围：新增 `editor_runtime/local_markdown_paths.rs`。
  - 2026-05-24 复核：`local_markdown_paths.rs` 不应落在 `leptos-tiptap` 内部；本地 Markdown 路径、`rootUri`、`/api/local-folder/files/open` URL 和上传落盘归 `03-rust-web` 的 shell host runtime / `local-upload-runtime.js`。
- [x] 迁出 slash 上传附件、图片的编辑器 intent。
  - `dispatch_editor_upload_request(kind, accept)` 已迁入 `attachment_upload.rs`。
  - `SlashActionKind::Image` / `UploadAttachment` 仍留在 slash menu 基础结构中，只调用 editor intent。
- 不纳入本设计当前归档范围：迁出 Office/PDF/code 文件的编辑器内插入逻辑。
  - 当前实际插入链路在 `layout.rs` / `local-upload-runtime.js`，不是 editor runtime；后续由 `03-rust-web` 继续收口。
- [x] 明确 editor 只负责发起上传 intent；资源保存位置和归属仍由 shell/Rust API 合同决定。
- 后续覆盖：
  - 上传第一个/第二个附件后链接仍可点击。
  - 刷新后链接不退化为普通外链。
  - secondary pane 上传与 primary pane 隔离。

### P4：抽出 DOM bridge / overlay / block menu

- 后续：新增 `editor_runtime/dom_selection.rs`、`overlays.rs`、`block_menu.rs`。
- 后续：迁出 selection、toolbar anchor、slash menu anchor、block menu anchor、image/table toolbar anchor。
- 后续：对所有 keydown/click 捕获建立“谁先处理、谁 stop propagation”的规则，避免 Tiptap 原生快捷键与外层 window listener 互相吞事件。
- 后续覆盖：
  - `Ctrl+Z/Ctrl+Y`
  - slash menu
  - block menu
  - table toolbar
  - attachment context menu

### P5：抽出 mindmap node view

- 后续：新增 `editor_runtime/mindmap_node_view.rs`。
- 后续：迁出 mindmap block 的 node view、runtime mount、projection refresh、simplemindmap event bridge。
- 后续：保持 mindmap 只是编辑器块视图，不成为正文事实源。

### P6：收尾

- 后续：`src/lib.rs` 只保留：
  - module imports
  - top-level component装配
  - 少量常量和 view glue
- 后续：单个模块超过约 800 行时继续拆分。
- 已完成：运行 CodeGraph sync/index，确认新增 Rust 模块被索引。
- 后续：更新 worker skill/task template；编辑器 bug 默认先查 `editor_runtime/*`，再回到 `lib.rs`。

## 8. History 安全规则

用户直接触发的编辑动作：

- 必须优先使用 Tiptap command/transaction。
- 如果临时必须使用 `set_content`，必须在 bug/design 中标明为什么不能走 command，并补一个 undo/redo 不支持或受限的说明。
- 新增编辑动作的 smoke 或单测应明确检查 undo/redo，除非该动作按产品定义不可撤销。

非用户编辑动作：

- 初始加载、host 替换文档、外部文件 watcher 重载、冲突恢复可以使用 `set_content`，但必须避免污染用户 undo stack。
- 外部同步应与 working copy 冲突模型结合，不能悄悄覆盖当前用户编辑。

## 9. Worker 协同规则

拆分后给 ClaudeCode / Reasonix 的任务应限定为：

```text
允许读取/修改：
- rust/spikes/leptos-tiptap-spike/src/editor_runtime/attachment_links.rs
- rust/spikes/leptos-tiptap-spike/src/editor_runtime/history_safe_commands.rs
- rust/spikes/leptos-tiptap-spike/src/editor_runtime/command_sync.rs
- rust/spikes/leptos-tiptap-spike/src/editor_runtime/persistence.rs
- scripts/task488-local-attachment-link-delete-undo-smoke.js

禁止：
- 修改 shell resource open 逻辑
- 修改 Rust tree command 语义
- 整文重写 src/lib.rs
```

采纳条件：

- worker handoff 明确命中哪个模块。
- diff 不跨越无关模块。
- 主控能运行对应 smoke。
- UI/浏览器行为必须有截图或 result.json 可审计证据。

## 10. 执行策略

本设计与 `3-19` 同步采用 Codex 主控、Reasonix 短跑 worker：

- Codex 负责编辑器主线边界、history 规则、最终 diff 复核和 smoke 验收。
- Reasonix 只处理短任务，不在一个 run 里同时做架构判断、代码迁移和浏览器验收。
- 同时最多 2 个 Reasonix worker；每批完成后必须先读取 `process-handoff.md/json` 和 Hindsight `reasonix` recall，再进入下一批。
- Batch 0 只读，不修改文件，用来生成 `src/lib.rs` 热点函数归属表和 `set_content` / HTML 整文替换风险表。
- 实施批次必须优先放在 `editor_runtime/*`，避免继续向 `src/lib.rs` 追加大块逻辑。

当前建议批次：

- Batch 0 / R2：只读审计 `rust/spikes/leptos-tiptap-spike/src/lib.rs` 中 history、`set_content`、附件链接删除、上传插入相关路径，输出函数归属表和风险等级。
- Batch 1：先抽出 `attachment_links.rs` 和最小 `history_safe_commands.rs`，保持 `task488` 通过；随后优先处理块菜单 delete 仍走 HTML 整文替换的 history 风险。
- Batch 2：抽出 `command_sync.rs` / `persistence.rs`，登记仍允许 `set_content` 的 host 替换和外部同步路径。
- Batch 3：抽出 `attachment_upload.rs` / `local_markdown_paths.rs`，覆盖第一个/第二个附件上传和点击打开 smoke。
- Batch 4：抽出 DOM selection / overlay / block menu，建立 keydown/click 捕获顺序规则。

Batch 0 验收标准：

- R2 必须列出搜索过的函数、文件位置、当前职责、建议目标模块、history 风险和 smoke 建议。
- R2 不允许修改源码、脚本、文档或运行 git 写操作。
- Codex 只采纳能被本地源码定位和现有 bug 证据支持的结论。
- 若 R2 输出泛化建议但无法对应具体函数，视为不可采纳，只记录为 worker 质量问题。

### 10.1 Batch 0 执行记录（2026-05-24）

Reasonix R2 run：

- run id: `reasonix-2026-05-24T12-38-22-683Z-3826b3b6`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T12-38-22-683Z-3826b3b6/process-handoff.md`
- 类型：只读审计，无工作区改动。
- Codex 复核：已抽样用 `rg` 核对关键符号位置；Hindsight `reasonix` bank 可 recall 到该 handoff。

可采纳结论：

- 最近已修复的附件链接 `Delete/Backspace` 路径现在使用 `editor.delete_selection()`，属于可迁入 `history_safe_commands.rs` 的正向模板。
- `selected_local_attachment_link_text` 和 `selected_local_attachment_link_in_editor_state` 可优先迁入 `attachment_links.rs`，迁移本身应保持行为不变并继续跑 `task488`。
- 仍需登记的高风险用户编辑路径：
  - 块菜单 delete：`delete_top_level_block_html` -> `apply_html_update` -> `editor.set_content(TiptapContent::html(...))`，用户删除动作绕过 history，后续应替换为 Tiptap command/transaction。
  - 块类型转换或 slash action 中经过 `apply_document_update` 的路径：`editor.set_content(TiptapContent::json(...))` 可能绕过 history，需要逐调用点确认。
  - `run_block_turn_into_page_action` / `run_block_turn_into_page`：整文 JSON 替换，是否可撤销属于产品决策，不在第一刀默认修改。
- 可接受的 `set_content` 路径：
  - `apply_host_document_payload`
  - 外部 block delta listener
  - 编辑器初始化恢复本地草稿
  这些是非用户直接编辑动作，允许保留，但必须避免污染用户 undo stack 和覆盖当前用户编辑。

本轮发现的 worker 协同问题：

- Reasonix 对大文件只读审计有效，但输出中的“行号和风险分级”只能作为候选事实；主控必须用本地 `rg` / 源码抽样复核。
- Reasonix runner 产出 `completed` 结果后底层 `reasonix acp` 进程未自动退出；主控必须在每批结束时检查并清理本轮启动进程。

### 10.2 Batch 1 执行记录（2026-05-24）

Reasonix run：

- run id: `reasonix-2026-05-24T14-00-12-979Z-1125fb29`
- worker worktree: `/mnt/Data1T/mnote-wt-5-28-attachment-history-b1`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T14-00-12-979Z-1125fb29/process-handoff.md`
- 类型：窄范围机械迁移，允许修改 `src/lib.rs` 和 `src/editor_runtime/{mod.rs,attachment_links.rs,history_safe_commands.rs}`。

采纳内容：

- 新增 `editor_runtime/mod.rs`。
- 新增 `editor_runtime/attachment_links.rs`，迁出本地附件链接的 DOM selection fallback 与 Tiptap editor state link attributes 判断。
- 新增 `editor_runtime/history_safe_commands.rs`，把选中本地附件链接后的 `Delete/Backspace` 删除收口到 `delete_selected_attachment_link_with_history(...)`。
- `src/lib.rs` 只保留模块装配和 keydown handler 调用，不再直接承载本地附件链接识别函数。

Codex 主控复核修正：

- worker 初版把底层错误格式化为 `删除附件引用失败：{e}`，调用处也会加同一前缀；主控已改为 `error.to_string()`，避免用户反馈重复。
- worker 初版自报 `cargo check` 通过依赖临时 `reference-code` symlink；主控在 worktree 和主工作区分别重新验证。
- Reasonix runner 写出 completed 后底层 `reasonix acp` 残留，主控已清理本轮进程。
- 后续 `task479` 暴露 DOM `range.selectNode(link)` + `Backspace` 场景下 Tiptap selection 可能未同步，导致相邻附件删除检查失败；主控已在 `history_safe_commands` 删除前把 DOM 选中的本地附件链接映射回 Tiptap text selection，再调用 `delete_selection()`。

已通过验证：

- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
- `cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`（35 个既有 warning）
- `cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release`
- `wasm-bindgen rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island`
- `node scripts/task488-local-attachment-link-delete-undo-smoke.js`
  - 结果：`ok=true`
  - 证据：`tmp/task488-local-attachment-link-delete-undo-smoke/result.json`
- `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`
  - 结果：`ok=true`
  - 证据：命令 stdout JSON，覆盖 `editor-upload-to-page-resource-dir`、`filetree-folder-drop-to-target`、`broken-link-after-real-file-delete`、`direct-attach-open-without-active-md`、`editor-delete-one-attachment-preserves-adjacent` 等检查。
- `node scripts/task488-local-attachment-link-delete-undo-smoke.js`
  - 结果：`ok=true`
  - 步骤：`uploaded -> selected -> deleted -> undo -> redo`
  - 证据：`tmp/task488-local-attachment-link-delete-undo-smoke/result.json`、`tmp/task488-local-attachment-link-delete-undo-smoke/after-redo.png`
- `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`
  - 结果：`ok=true`
  - 覆盖：单附件 `Backspace` 删除保留相邻附件、真实附件文件不被级联删除、刷新后被删引用不回来。
- `codegraph index . --force && codegraph status .`
  - 结果：索引 318 个文件，状态 up to date。
  - 复核：`delete_selected_attachment_link_with_history`、`selected_local_attachment_link_text`、`is_local_attachment_link_selected` 均可通过 CodeGraph 搜索定位到 `editor_runtime/*`。

### 10.3 Batch 2 执行记录（2026-05-24）

Reasonix read-only audit：

- run id: `reasonix-2026-05-24T15-01-33-138Z-dc32246a`
- worker worktree: `/mnt/Data1T/mnote-wt-5-28-command-sync-b2`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T15-01-33-138Z-dc32246a/process-handoff.md`
- 类型：只读审计，主控已读取 Hindsight recall、`process-handoff.md`、`process-handoff.json`。

采纳内容：

- 新增 `editor_runtime/command_sync.rs`，迁出 `read_editor_snapshot`、`sync_editor_outputs`、`sync_persisted_editor_command`。
- 新增 `editor_runtime/persistence.rs`，迁出 `PersistedSpikeDocument`、`PersistedDocumentIdentity`、identity normalization、storage key、localStorage load/persist。
- `apply_document_update` 迁入 `command_sync.rs`，保持迁移前零调用者现状。
- `apply_html_update` 暂留 `lib.rs`：它仍强依赖 host change/status event、`HoveredBlockState` 和 toolbar overlay state；后续等 `bridge_events.rs` / `overlays.rs` 拆出后再迁。

Codex 主控复核：

- `persist_document_state` 的 payload、storage key、`localStorage` 写入错误文案保持不变。
- `sync_persisted_editor_command` 现在委托 `persist_current_editor_snapshot(...)`，dirty count、自身快照读取、HTML/JSON output 更新和反馈文案顺序保持不变。
- `normalize_identity_value` 改为 `pub(crate)`，用于 mindmap draft identity；`persisted_document_storage_key` 只在 `#[cfg(test)]` 下导入。

已通过验证：

- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
- `cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`（35 个既有 warning）
- `cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release`（35 个既有 warning）
- `wasm-bindgen rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island`

受阻验证：

- `cargo test --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml persisted_document_storage_key_isolated_per_document_identity`
- `cargo test --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml standalone_mindmap_object_uses_isolated_draft_identity`
- 阻塞原因：lib test 编译阶段命中既有 `slash_menu_css_uses_viewport_overlay_layer` 中 `STYLE` 未定义；HEAD 中该测试已引用 `STYLE`，而实际常量名为 `SPIKE_STYLE`。该问题不属于本批拆分，已记录为后续单独处理。

### 10.4 Batch 3 第一刀执行记录（2026-05-24）

Reasonix read-only audit：

- run id: `reasonix-2026-05-24T15-17-16-430Z-18054487`
- worker worktree: `/mnt/Data1T/mnote-wt-5-28-upload-boundary-b3`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T15-17-16-430Z-18054487/process-handoff.md`
- 类型：只读审计，主控已读取 Hindsight recall、`process-handoff.md`、`process-handoff.json`。

采纳结论：

- 当前上传链路应保持 `editor intent -> CustomEvent -> shell pickup`。
- `attachment_upload.rs` 只承接 `dispatch_editor_upload_request(kind, accept)` 这一类编辑器 intent，不迁入真实上传、文件选择器、API POST、资源归属或插入链接主流程。
- `local_markdown_paths.rs` 暂不在 `leptos-tiptap` 内创建；本地 Markdown 路径、`rootUri`、`/api/local-folder/files/open` URL、`markdownRelativePath` 与落盘语义归 `03-rust-web` shell host runtime / `local-upload-runtime.js`。

采纳内容：

- 新增 `editor_runtime/attachment_upload.rs`，迁出 `dispatch_editor_upload_request(kind, accept)`。
- `src/lib.rs` 的 `run_slash_action` 继续保留 `SlashActionKind::Image` / `UploadAttachment` 的 slash menu 决策，只调用 `attachment_upload` 发出上传 intent。
- `src/lib.rs` 不再直接构造 `mnote:editor-upload-request`。

已通过验证：

- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
- `cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`（35 个既有 warning）
- `cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release`（35 个既有 warning）
- `wasm-bindgen rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island`
- `node scripts/task488-local-attachment-link-delete-undo-smoke.js`
  - 结果：`ok=true`
  - 证据：`tmp/task488-local-attachment-link-delete-undo-smoke/result.json`
- `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`
  - 结果：`ok=true`
  - 证据：命令 stdout JSON，覆盖 `editor-upload-to-page-resource-dir`、`filetree-folder-drop-to-target`、`broken-link-after-real-file-delete`、`direct-attach-open-without-active-md`、`editor-delete-one-attachment-preserves-adjacent` 等检查。

后续归属：

- `resolveEditorUploadContext`、`editorRootFromUploadOptions`、`openEditorUploadFilePicker` 等 shell-side DOM/context 探针适合进入 `03-rust-web` 的 `local-upload-runtime.js` 后续批次。
- `uploadFileToMediaAsset`、`uploadFilesWithResolvedTarget`、`insertUploadedAssetIntoEditor` 仍是高耦合主流程，暂不迁入 editor runtime。

### 10.5 Batch 4 块菜单删除 history 收口（2026-05-24）

Codex 主控直接完成：

- 新增 `scripts/task489-block-menu-delete-undo-smoke.js`，先在旧实现上确认 RED：块菜单删除 `Beta block` 后，`Ctrl+Z` 未恢复目标块，失败证据写入 `tmp/task489-block-menu-delete-undo-smoke/result.json`。
- `editor_runtime/history_safe_commands.rs` 新增 `delete_top_level_block_with_history(editor, index)`：
  - 多块文档：计算顶层块完整 ProseMirror range 后调用 Tiptap `delete_range(...)`。
  - 单块文档：用 `insert_content_at(range, paragraph, update_selection=true)` 替换为空段落。
  - 删除完成后仅调用 `sync_persisted_editor_command(...)` 同步快照和草稿，不再走 `apply_html_update`。
- 删除 `src/lib.rs` 中旧的 `delete_top_level_block_html(...)`，避免块菜单删除继续保留 HTML 整文替换入口。

Reasonix read-only audit：

- run id: `reasonix-2026-05-24T15-45-44-865Z-bb0ede13`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T15-45-44-865Z-bb0ede13/process-handoff.md`
- 类型：只读审计，主控已读取 Hindsight recall、`process-handoff.md`、`process-handoff.json`。
- 采纳结论：可采纳；块菜单删除已从 `apply_html_update(...)` / `set_content(...)` 路径迁到 Tiptap `delete_range(...)` / `insert_content_at(...)`。Reasonix 提醒 `sync_persisted_editor_command(...)` 与 Tiptap `on_change` 后续可能存在 dirty count / localStorage 冗余，作为后续优化风险登记，不阻塞本批。

已通过验证：

- `node --check scripts/task489-block-menu-delete-undo-smoke.js`
- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
- `cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`（35 个既有 warning）
- `cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release`（35 个既有 warning）
- `wasm-bindgen rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island`
- `node scripts/task489-block-menu-delete-undo-smoke.js`
  - 结果：`ok=true`
  - 证据：`tmp/task489-block-menu-delete-undo-smoke/result.json`、`tmp/task489-block-menu-delete-undo-smoke/after-redo.png`
- `node scripts/task488-local-attachment-link-delete-undo-smoke.js`
  - 结果：`ok=true`
  - 证据：`tmp/task488-local-attachment-link-delete-undo-smoke/result.json`
- `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`
  - 结果：`ok=true`
  - 证据：命令 stdout JSON，覆盖上传目录、文件树 drop、broken link、无 active document 附件 tab、标题来源、附件键盘/手柄删除。
- smoke 启动方式：
  - 使用稳定入口 `MNOTE_WEB_BIND=0.0.0.0:3000 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3000 cargo run --manifest-path rust/Cargo.toml -p mnote-web --bin mnote-web`。
  - `npm run dev:hot` / `cargo watch` 在本轮验收中发生过重编译重启，导致 `task479` 中段 `ERR_CONNECTION_REFUSED`；该结果已废弃，不作为功能失败证据。
- `codegraph search delete_top_level_block_with_history top_level_block_full_range prosemirror_node_size`
  - 结果：`delete_top_level_block_with_history`、`top_level_block_full_range`、`prosemirror_node_size` 可定位到 `rust/spikes/leptos-tiptap-spike/src/editor_runtime/history_safe_commands.rs`。

剩余风险：

- 块手柄拖拽重排和复制副本仍使用 HTML 整文替换，继续登记为后续 history-safe command 切片；本设计 done gate 只要求至少一个已登记用户编辑路径完成迁移。

### 10.6 后续 P4 只读审计补充（2026-05-25）

Reasonix read-only audit：

- run id: `reasonix-2026-05-24T16-25-22-430Z-3a9a1a82`
- worktree: `/mnt/Data1T/mnote-worktrees/5-28-dom-bridge-audit`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T16-25-22-430Z-3a9a1a82/process-handoff.md`
- 类型：只读审计，主控已读取 Hindsight recall 与 `process-handoff.md`。

可采纳结论：

- 后续 P4 第一刀应优先新增 `editor_runtime/dom_selection.rs`，而不是直接抽 `block_menu.rs`。
- 第一刀候选函数：
  - `active_editor_text_selection`
  - `selection_summary`
  - `has_active_text_selection`
  - `selection_has_rich_marks`
  - `selection_payload` / selection event payload 构造
  - `send_selection_state` / `send_selection_state_to_target`
- `block_menu.rs` 暂不先做：当前 block menu / overlay 相关函数仍强依赖大量 `ReadSignal` / `WriteSignal`，在合并 overlay state 或先迁出 `HoveredBlockState` 前，直接抽模块只会把长参数耦合搬到新文件。
- `overlays.rs` 可作为 `dom_selection.rs` 之后的第二刀；`apply_html_update` 仍等 `bridge_events.rs` / overlay state 收口后再迁。

建议验证：

- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
- `cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`
- `cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release`
- `wasm-bindgen rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island`
- `node scripts/task488-local-attachment-link-delete-undo-smoke.js`
- `node scripts/task489-block-menu-delete-undo-smoke.js`

### 10.7 Batch 5 DOM selection 第一刀（2026-05-25）

Codex 主控基于上一批 Reasonix P4 只读审计直接完成：

- 新增 `editor_runtime/dom_selection.rs`。
- 迁出纯 DOM / selection helper：
  - `node_is_within_root`
  - `active_editor_text_selection`
  - `has_active_text_selection`
  - `selection_summary`
  - `selection_has_rich_marks`
- `src/lib.rs` 保留 `SelectionPayload`、`selection_payload`、`send_selection_state`、`send_selection_state_to_target`、`selection_event_payload` 和事件 dispatch；这些仍依赖 bridge event、`HoveredBlockState`、block id 解析和 target dispatch，后续等 `bridge_events.rs` / overlay state 边界更稳后再迁。
- `editor_root_element()` 改为 `pub(crate)`，供 `dom_selection.rs` 读取当前编辑器根节点；shell/resource 语义未进入 editor DOM 模块。

已通过验证：

- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
- `cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`（35 个既有 warning）
- `cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release`（35 个既有 warning）
- `wasm-bindgen rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island`
- `node scripts/task488-local-attachment-link-delete-undo-smoke.js`
  - 结果：`ok=true`
  - 证据：`tmp/task488-local-attachment-link-delete-undo-smoke/result.json`
- `node scripts/task489-block-menu-delete-undo-smoke.js`
  - 结果：`ok=true`
  - 证据：`tmp/task489-block-menu-delete-undo-smoke/result.json`
  - 备注：首次与 `task488` 并行执行时在 `Ctrl+Z` 等待上超时；单独重跑通过，后续 smoke 建议串行跑编辑器键盘类场景，避免浏览器/服务端时序争用误报。

后续建议：

- 下一刀可迁 `floating_toolbar_anchor_from_selection` / `sync_text_selection_overlay` 等 overlay anchor 纯计算部分进入 `overlays.rs`。
- `selection_payload` / `send_selection_state*` 应与 `bridge_events.rs` 一起迁，避免为了单次拆分扩大 `SelectionPayload`、`HoveredBlockState` 和 runtime block id 的可见性。

## 11. 测试与验收

### Rust 检查

```bash
cargo fmt --manifest-path rust/Cargo.toml --all --check
cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml
cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release
wasm-bindgen rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island
```

### Browser smoke

```bash
node scripts/task488-local-attachment-link-delete-undo-smoke.js
node scripts/task489-block-menu-delete-undo-smoke.js
node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
node scripts/task472-side-target-secondary-pane-smoke.js
```

后续拆到 block menu / table toolbar 时，应补对应 smoke，不复用附件 smoke 代替。

## 12. 风险与控制

- 风险：拆模块时改变 Leptos signal 捕获和生命周期。
  - 控制：每次只迁一个行为域；迁移后立即跑 wasm build 和浏览器 smoke。
- 风险：为了通过编译把函数参数扩得过大。
  - 控制：只在边界稳定后引入小型 context struct；不要先抽象大而全 runtime context。
- 风险：把 shell/resource 语义搬进 editor 模块。
  - 控制：editor 只发出 intent，不决定 workspace/resource 真相。
- 风险：继续产生新的巨型模块。
  - 控制：单模块超过约 800 行必须拆出子模块。

## 13. Done Gate

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

- [x] `attachment_links.rs` 和 `history_safe_commands.rs` 已落地。
- [x] `command_sync.rs` 已落地，用户编辑动作与 host 替换动作边界明确。
- [x] 至少一个仍使用 `set_content` 的用户编辑路径已迁到 Tiptap command/transaction。
- [x] `task488` 通过，且至少覆盖一次真实键盘 `Ctrl+Z/Ctrl+Y`。
- [x] 新增 editor_runtime Rust 模块能被 CodeGraph 定位。
- [x] `src/lib.rs` 不再新增附件/history/DOM bridge 大块逻辑。

## 14. 后续问题

- 是否把 `leptos-tiptap-spike` 从 spike 正式改名：不作为本设计前置，等模块拆分后再评估。
- 是否回 upstream `reference-code/leptos-tiptap` 增加命令：只有当底层 bridge 缺少通用 Tiptap command 时才考虑；MNote 特有行为优先留在 `editor_runtime`。
- 是否引入更强类型的 editor command contract：P1-P3 完成后再评估，不作为第一阶段阻塞项。
