Files
mnote/design/05-editor-mainline/process/5-28-leptos-tiptap-editor-runtime-module-extraction-v1.md
T
lix-2026 0a03572601 refactor: externalize local upload helpers
Add local-upload-runtime.js as a browser runtime module for upload and attachment helper functions, with sidebar fallback wrappers and a fixed runtime asset route.

While validating task479, fix the editor attachment delete path so DOM-selected local attachment links are mapped back to a Tiptap text selection before delete_selection(), preserving undo history and adjacent links.
2026-05-24 22:42:53 +08:00

18 KiB
Raw Blame History

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

创建时间:2026-05-24 状态:process

触发背景:

  • 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/process/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_updatesync_persisted_editor_commanddelete_top_level_block_html
  • reference-code/leptos-tiptap/

    • 提供底层 Tiptap Rust/JS bridge。
    • 已有 TiptapEditorHandleTiptapExtensionTiptapRangedelete_selectiondelete_rangeundoredo 等命令。
    • 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. 目标模块布局

建议新增目录:

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. 分阶段执行清单

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
    • 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

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

P2:抽出 command sync / persistence

  • 新增 editor_runtime/command_sync.rs
  • 迁出:
    • read_editor_snapshot
    • sync_editor_outputs
    • sync_persisted_editor_command
    • change/status event dispatch 的编辑器侧包装
  • 明确 set_content 只允许用于:
    • 初始加载
    • host 替换文档
    • 外部同步/恢复
    • 非用户编辑的兼容兜底
  • 用户编辑动作必须优先走 Tiptap command/transaction。

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

  • 新增 editor_runtime/attachment_upload.rs
  • 新增 editor_runtime/local_markdown_paths.rs
  • 迁出 slash 上传附件、图片、Office/PDF/code 文件的编辑器内插入逻辑。
  • 明确 editor 只负责插入附件链接/块,资源保存位置和归属仍由 shell/Rust API 合同决定。
  • 覆盖:
    • 上传第一个/第二个附件后链接仍可点击。
    • 刷新后链接不退化为普通外链。
    • secondary pane 上传与 primary pane 隔离。

P4:抽出 DOM bridge / overlay / block menu

  • 新增 editor_runtime/dom_selection.rsoverlays.rsblock_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 的任务应限定为:

允许读取/修改:
- rust/spikes/leptos-tiptap-spike/src/editor_runtime/attachment_links.rs
- rust/spikes/leptos-tiptap-spike/src/editor_runtime/history_safe_commands.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_textselected_local_attachment_link_in_editor_state 可优先迁入 attachment_links.rs,迁移本身应保持行为不变并继续跑 task488
  • 仍需登记的高风险用户编辑路径:
    • 块菜单 deletedelete_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.rssrc/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.toml35 个既有 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
    • 步骤:uploaded -> selected -> deleted -> undo -> redo
    • 证据:tmp/task488-local-attachment-link-delete-undo-smoke/result.jsontmp/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_historyselected_local_attachment_link_textis_local_attachment_link_selected 均可通过 CodeGraph 搜索定位到 editor_runtime/*

11. 测试与验收

Rust 检查

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

node scripts/task488-local-attachment-link-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/ 前必须满足:

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

14. 后续问题

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