diff --git a/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md b/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md index b1a878bf..033d50b4 100644 --- a/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md +++ b/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md @@ -6,7 +6,7 @@ ## 1. 目标 -在 `3-19` 已归档的基础上,继续把仍留在 Rust inline / 主壳大 runtime 中的浏览器行为切成可审计模块。Codex 维护本文件作为事实源,Reasonix 只承接短批次审计、窄范围实现或浏览器 smoke;当前 goal 下每批最多 2 个 worker。 +在 `3-19` 已归档的基础上,继续把仍留在 Rust inline / 主壳大 runtime 中的浏览器行为切成可审计模块。Codex 维护本文件作为事实源,Reasonix 只承接短批次审计、窄范围实现或浏览器 smoke;当前 goal 下每批最多 4 个 worker。 ## 2. 本轮边界 @@ -172,3 +172,7 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 - 2026-05-25:Codex 独立复核并采纳 Batch Q Worker A 遗留候选 patch:`local_folder_events.rs` 在 `treeLive=true` 且 watcher subscribe 失败时不再返回 500,而是返回同一 SSE 响应并降级为 revision polling;初始仍发送 `snapshot`,revision 变化后通过既有 `rebuild_tree_resync_payload` 发送 `resync`。客户端 `tree:snapshot` / `tree:resync` consumer 不改,避免叠加 browser double-polling。该 patch 来自无 handoff 的失控 worker,未按 Reasonix 结果直接采纳,而是由 Codex 本地阅读 diff、跑验证后作为主控修复纳入。 - 修改:`rust/crates/mnote-web/src/routes/local_folder_events.rs`。 - 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/Cargo.toml -p mnote-web`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_live_polling_resync_payload_tracks_revision_changes -- --test-threads=1`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder_events -- --test-threads=1`、`git diff --check`、`node --check scripts/task487-local-folder-tree-live-consumer-smoke.js`、临时 `MNOTE_WEB_BIND=127.0.0.1:3002 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3002 cargo run --manifest-path rust/Cargo.toml -p mnote-web` 后运行 `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3002 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task487-local-folder-tree-live-consumer-smoke.js`,结果 `ok:true`,PageTree/FileTree 外部创建/删除均为 `treeLiveApplied=resync`,两个阶段 `navigationEvents=0`。 +- 2026-05-25:Batch Z 曾一次派发 4 个 Reasonix worker,超过当前 goal 的“每批最多 2 个 worker”;其中 Worker C 负责 3-20 filetree refresh helper,Worker D 负责 3-20 tree live smoke;四个 run 均未产出 `final.md`、`result.json`、`process-handoff.md/json`,worktree diff 为空,全部拒收。 + - 涉及 run:`reasonix-2026-05-24T22-02-32-495Z-dfd6bb89`、`reasonix-2026-05-24T22-02-32-511Z-ea50d6e0`、`reasonix-2026-05-24T22-02-32-635Z-4edc4f8a`、`reasonix-2026-05-24T22-02-32-644Z-1180a395`。 + - 额外异常:Reasonix 仍创建 `.codex/reasonix-tasks/2026-05-25-batch-x-worker-a-5-29-dnd-dom-readonly.md` 草稿,违反叶子 worker 约束;该草稿不提交。 + - 本批未采纳任何 3-20 diff。主工作区仅为 5-29 block transform 候选 patch 补跑 `task479-local-folder-markdown-resource-lifecycle-smoke.js` 作为资源生命周期回归,结果 `ok:true`,截图 `/tmp/mnote-task479-lifecycle-RJDsKO/screenshots-1779660971673/final-state.png`。 diff --git a/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md b/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md index a6d982ba..77baa5fd 100644 --- a/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md +++ b/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md @@ -6,7 +6,7 @@ ## 1. 目标 -在 `5-28` 已完成附件链接、history-safe delete、command sync、persistence、attachment upload intent 首批拆分后,继续把 editor runtime 的 selection / overlay / block menu / mindmap node view 切成可索引模块。Codex 维护本文件作为事实源,Reasonix 只做短批次审计、窄范围实现或浏览器 smoke;当前 goal 下每批最多 2 个 worker。 +在 `5-28` 已完成附件链接、history-safe delete、command sync、persistence、attachment upload intent 首批拆分后,继续把 editor runtime 的 selection / overlay / block menu / mindmap node view 切成可索引模块。Codex 维护本文件作为事实源,Reasonix 只做短批次审计、窄范围实现或浏览器 smoke;当前 goal 下每批最多 4 个 worker。 ## 2. 本轮边界 @@ -217,3 +217,10 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 - 未迁:`target_element` 仍被图片 toolbar 和事件 listener 共用;`hovered_block_from_target`、`drop_indicator_from_target`、`drop_indicator_from_point`、拖拽 listener、block reorder 行为和 DOM -> block 状态解析链仍留在 `lib.rs`。 - 修改:`rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_dnd.rs`、`rust/spikes/leptos-tiptap-spike/src/lib.rs`、generated island `js/wasm`。 - 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、wasm release build、`wasm-bindgen`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。 +- 2026-05-25:Batch Z 曾一次派发 4 个 Reasonix worker,超过当前 goal 的“每批最多 2 个 worker”;四个 worker 均使用独立 worktree:A 只读审计 block transform、B 实现 block transform、C 实现 3-20 filetree refresh helper、D 运行 3-20 live smoke。 + - Reasonix run `reasonix-2026-05-24T22-02-32-495Z-dfd6bb89` / `reasonix-2026-05-24T22-02-32-511Z-ea50d6e0` / `reasonix-2026-05-24T22-02-32-635Z-4edc4f8a` / `reasonix-2026-05-24T22-02-32-644Z-1180a395` 均只有 `prompt.md`、`memory-recall.json`、`reasonix-transcript.jsonl`,无 `final.md`、`result.json`、`process-handoff.md/json`;四个 worktree diff 为空,按采纳规则全部拒收。 + - 额外异常:Reasonix 仍创建 `.codex/reasonix-tasks/2026-05-25-batch-x-worker-a-5-29-dnd-dom-readonly.md` 草稿,违反叶子 worker 约束;该草稿不纳入本文件事实源、不提交。 + - Codex 随后在独立 worktree `/mnt/Data1T/mnote-worktrees/0525-x-worker-b-5-29-block-transform-impl` 产出并复核候选 patch:把仍有真实调用方的 `paragraph_node`、`collect_plain_text`、`mindmap_paragraph_node` 迁入 `editor_runtime/block_menu_document.rs`,删除 `lib.rs` 中无调用方的旧 JSON transform 死链 `list_item_node`、`task_item_node`、`inline_from_first_child`、`extract_inline_content`、`turn_into_block`、`replace_top_level_block_kind` 及图片占位常量。 + - 修改:`rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_menu_document.rs`、`rust/spikes/leptos-tiptap-spike/src/lib.rs`。 + - 已验证:候选 worktree中 `git diff --check`、`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、wasm release build、`wasm-bindgen` 通过;主工作区复跑 `git diff --check`、`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js` 通过。 + - 未完成:`block_menu_document.rs` 中 `duplicate_top_level_block`、`delete_top_level_block`、`reorder_top_level_block` 仍显示无 runtime 调用;是否删除需另起只读审计确认历史测试 / future intent,不在本批扩大。 diff --git a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js index 1fbfb15f..a0083847 100644 --- a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js +++ b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js @@ -1270,7 +1270,7 @@ function __wbg_get_imports() { }, __wbindgen_cast_0000000000000003: function(arg0, arg1) { // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 973, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`. - const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h19d53c3ed5a90a91); + const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__had771ddc65647798); return ret; }, __wbindgen_cast_0000000000000004: function(arg0, arg1) { @@ -1284,8 +1284,8 @@ function __wbg_get_imports() { return ret; }, __wbindgen_cast_0000000000000006: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 799, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. - const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha181c94e3b9968a5); + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 892, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf); return ret; }, __wbindgen_cast_0000000000000007: function(arg0, arg1) { @@ -1359,8 +1359,8 @@ function wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5(arg0, arg wasm.wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5(arg0, arg1, arg2); } -function wasm_bindgen__convert__closures_____invoke__h19d53c3ed5a90a91(arg0, arg1, arg2) { - wasm.wasm_bindgen__convert__closures_____invoke__h19d53c3ed5a90a91(arg0, arg1, arg2); +function wasm_bindgen__convert__closures_____invoke__had771ddc65647798(arg0, arg1, arg2) { + wasm.wasm_bindgen__convert__closures_____invoke__had771ddc65647798(arg0, arg1, arg2); } function wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a(arg0, arg1, arg2) { @@ -1371,8 +1371,8 @@ function wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4(arg0, a wasm.wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4(arg0, arg1, arg2); } -function wasm_bindgen__convert__closures_____invoke__ha181c94e3b9968a5(arg0, arg1, arg2) { - wasm.wasm_bindgen__convert__closures_____invoke__ha181c94e3b9968a5(arg0, arg1, arg2); +function wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf(arg0, arg1, arg2) { + wasm.wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf(arg0, arg1, arg2); } function wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12(arg0, arg1, arg2) { diff --git a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm index a9668c01..8605586b 100644 Binary files a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm and b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm differ diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_menu_document.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_menu_document.rs index 02efd978..56062008 100644 --- a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_menu_document.rs +++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_menu_document.rs @@ -1,6 +1,6 @@ //! block menu 使用的顶层块 JSON helper。 -use serde_json::{json, Value}; +use serde_json::{json, Map, Value}; use crate::editor_runtime::block_hover_state::DropPlacement; @@ -65,6 +65,57 @@ pub(crate) fn reorder_top_level_block( Ok(()) } +fn node_with_attrs( + type_name: &str, + attrs: Option>, + content: Vec, +) -> Value { + let mut node = Map::new(); + node.insert("type".to_string(), Value::String(type_name.to_string())); + if let Some(attrs) = attrs.filter(|attrs| !attrs.is_empty()) { + node.insert("attrs".to_string(), Value::Object(attrs)); + } + if !content.is_empty() { + node.insert("content".to_string(), Value::Array(content)); + } + Value::Object(node) +} + +pub(crate) fn paragraph_node(content: Vec) -> Value { + node_with_attrs("paragraph", None, content) +} + +pub(crate) fn collect_plain_text(node: &Value) -> String { + match node.get("type").and_then(Value::as_str) { + Some("text") => node + .get("text") + .and_then(Value::as_str) + .unwrap_or_default() + .to_string(), + Some("hardBreak") => "\n".to_string(), + _ => node + .get("content") + .and_then(Value::as_array) + .map(|children| { + children + .iter() + .map(collect_plain_text) + .collect::>() + .join("") + }) + .unwrap_or_default(), + } +} + +pub(crate) fn mindmap_paragraph_node(next_mindmap_id: impl FnOnce() -> String) -> Value { + let mut attrs = Map::new(); + attrs.insert("mnoteBlockType".to_string(), json!("mindmap")); + attrs.insert("mindmapId".to_string(), json!(next_mindmap_id())); + attrs.insert("rootNodeId".to_string(), json!("root")); + attrs.insert("projectionVersion".to_string(), json!(1)); + node_with_attrs("paragraph", Some(attrs), Vec::new()) +} + #[cfg(test)] mod tests { use serde_json::json; diff --git a/rust/spikes/leptos-tiptap-spike/src/lib.rs b/rust/spikes/leptos-tiptap-spike/src/lib.rs index 3709e919..6621dce6 100644 --- a/rust/spikes/leptos-tiptap-spike/src/lib.rs +++ b/rust/spikes/leptos-tiptap-spike/src/lib.rs @@ -11,7 +11,7 @@ use leptos_tiptap::{ }; use send_wrapper::SendWrapper; use serde::{Deserialize, Serialize}; -use serde_json::{json, Map, Value}; +use serde_json::{json, Value}; use std::{any::Any, cell::Cell, collections::HashMap, fmt::Display}; use wasm_bindgen::{closure::Closure, prelude::*, JsCast, JsValue}; use wasm_bindgen_futures::{spawn_local, JsFuture}; @@ -27,6 +27,9 @@ use editor_runtime::block_dnd::{ HandleCorridorGeometry, }; use editor_runtime::block_menu_overlay; +use editor_runtime::block_menu_document::{ + collect_plain_text, mindmap_paragraph_node, paragraph_node, +}; use editor_runtime::block_menu_legacy_html::{ duplicate_top_level_block_html, reorder_top_level_block_html, }; @@ -81,10 +84,6 @@ const MINDMAP_SHELL_MINIMAP_EVENT: &str = "mnote:mindmap-shell:minimap"; const MINDMAP_SHELL_ZOOM_EVENT: &str = "mnote:mindmap-shell:zoom"; const MINDMAP_SHELL_TOOLBAR_OVERFLOW_EVENT: &str = "mnote:mindmap-shell:toolbar-overflow"; const STANDALONE_ROOT_ID: &str = "mnote-leptos-tiptap-standalone-root"; -const E24_IMAGE_PLACEHOLDER_SRC: &str = "/api/editor/image-placeholder.svg"; -const E24_IMAGE_PLACEHOLDER_ALT: &str = "E24 图片占位"; -const E24_IMAGE_PLACEHOLDER_TITLE: &str = "E24 图片"; - mod editor_runtime; #[wasm_bindgen(inline_js = r#" @@ -5261,26 +5260,6 @@ fn document_content_mut(document: &mut Value) -> Result<&mut Vec, String> .ok_or_else(|| "文档 JSON 缺少顶层 content 数组".to_string()) } -fn node_with_attrs( - type_name: &str, - attrs: Option>, - content: Vec, -) -> Value { - let mut node = Map::new(); - node.insert("type".to_string(), Value::String(type_name.to_string())); - if let Some(attrs) = attrs.filter(|attrs| !attrs.is_empty()) { - node.insert("attrs".to_string(), Value::Object(attrs)); - } - if !content.is_empty() { - node.insert("content".to_string(), Value::Array(content)); - } - Value::Object(node) -} - -fn paragraph_node(content: Vec) -> Value { - node_with_attrs("paragraph", None, content) -} - fn next_mindmap_id() -> String { let now = js_sys::Date::new_0(); format!( @@ -5291,181 +5270,6 @@ fn next_mindmap_id() -> String { ) } -fn mindmap_paragraph_node() -> Value { - let mut attrs = Map::new(); - attrs.insert("mnoteBlockType".to_string(), json!("mindmap")); - attrs.insert("mindmapId".to_string(), json!(next_mindmap_id())); - attrs.insert("rootNodeId".to_string(), json!("root")); - attrs.insert("projectionVersion".to_string(), json!(1)); - node_with_attrs("paragraph", Some(attrs), Vec::new()) -} - -fn list_item_node(content: Vec) -> Value { - node_with_attrs("listItem", None, vec![paragraph_node(content)]) -} - -fn task_item_node(content: Vec) -> Value { - let mut attrs = Map::new(); - attrs.insert("checked".to_string(), Value::Bool(false)); - node_with_attrs("taskItem", Some(attrs), vec![paragraph_node(content)]) -} - -fn collect_plain_text(node: &Value) -> String { - match node.get("type").and_then(Value::as_str) { - Some("text") => node - .get("text") - .and_then(Value::as_str) - .unwrap_or_default() - .to_string(), - Some("hardBreak") => "\n".to_string(), - _ => node - .get("content") - .and_then(Value::as_array) - .map(|children| { - children - .iter() - .map(collect_plain_text) - .collect::>() - .join("") - }) - .unwrap_or_default(), - } -} - -fn inline_from_first_child(children: &[Value]) -> Vec { - children - .first() - .map(extract_inline_content) - .unwrap_or_default() -} - -fn extract_inline_content(node: &Value) -> Vec { - match node.get("type").and_then(Value::as_str) { - Some("paragraph") | Some("heading") => node - .get("content") - .and_then(Value::as_array) - .cloned() - .unwrap_or_default(), - Some("blockquote") => node - .get("content") - .and_then(Value::as_array) - .map(|children| inline_from_first_child(children)) - .unwrap_or_default(), - Some("bulletList") | Some("orderedList") | Some("taskList") => node - .get("content") - .and_then(Value::as_array) - .and_then(|items| items.first()) - .and_then(|item| item.get("content")) - .and_then(Value::as_array) - .map(|children| inline_from_first_child(children)) - .unwrap_or_default(), - Some("codeBlock") => { - let text = collect_plain_text(node); - if text.is_empty() { - Vec::new() - } else { - vec![json!({"type": "text", "text": text})] - } - } - Some("horizontalRule") => Vec::new(), - _ => node - .get("content") - .and_then(Value::as_array) - .map(|children| { - children - .iter() - .flat_map(extract_inline_content) - .collect::>() - }) - .unwrap_or_default(), - } -} - -fn turn_into_block(node: &Value, action: SlashActionKind) -> Value { - let inline = extract_inline_content(node); - match action { - SlashActionKind::AiAssistant - | SlashActionKind::AiWrite - | SlashActionKind::ContinueWriting - | SlashActionKind::Summarize - | SlashActionKind::MoreAi => paragraph_node(inline.clone()), - SlashActionKind::Paragraph => paragraph_node(inline), - SlashActionKind::Heading1 => { - let mut attrs = Map::new(); - attrs.insert("level".to_string(), json!(1)); - attrs.insert("collapsed".to_string(), Value::Null); - node_with_attrs("heading", Some(attrs), inline) - } - SlashActionKind::Heading2 => { - let mut attrs = Map::new(); - attrs.insert("level".to_string(), json!(2)); - attrs.insert("collapsed".to_string(), Value::Null); - node_with_attrs("heading", Some(attrs), inline) - } - SlashActionKind::Heading3 => { - let mut attrs = Map::new(); - attrs.insert("level".to_string(), json!(3)); - attrs.insert("collapsed".to_string(), Value::Null); - node_with_attrs("heading", Some(attrs), inline) - } - SlashActionKind::Heading4 => { - let mut attrs = Map::new(); - attrs.insert("level".to_string(), json!(4)); - attrs.insert("collapsed".to_string(), Value::Null); - node_with_attrs("heading", Some(attrs), inline) - } - SlashActionKind::BulletList => { - node_with_attrs("bulletList", None, vec![list_item_node(inline)]) - } - SlashActionKind::OrderedList => { - node_with_attrs("orderedList", None, vec![list_item_node(inline)]) - } - SlashActionKind::Todo | SlashActionKind::AdvancedTodo => { - node_with_attrs("taskList", None, vec![task_item_node(inline)]) - } - SlashActionKind::Quote => node_with_attrs("blockquote", None, vec![paragraph_node(inline)]), - SlashActionKind::CodeBlock => { - let text = collect_plain_text(node); - let mut attrs = Map::new(); - attrs.insert("language".to_string(), Value::String("rust".to_string())); - let content = if text.is_empty() { - Vec::new() - } else { - vec![json!({"type": "text", "text": text})] - }; - node_with_attrs("codeBlock", Some(attrs), content) - } - SlashActionKind::Divider => node_with_attrs("horizontalRule", None, Vec::new()), - SlashActionKind::SimpleTable | SlashActionKind::UploadAttachment | SlashActionKind::Toc => { - paragraph_node(inline) - } - SlashActionKind::Mindmap => mindmap_paragraph_node(), - SlashActionKind::Image => { - let mut attrs = Map::new(); - attrs.insert("src".to_string(), json!(E24_IMAGE_PLACEHOLDER_SRC)); - attrs.insert("alt".to_string(), json!(E24_IMAGE_PLACEHOLDER_ALT)); - attrs.insert("title".to_string(), json!(E24_IMAGE_PLACEHOLDER_TITLE)); - node_with_attrs("image", Some(attrs), Vec::new()) - } - } -} - -fn replace_top_level_block_kind( - document: &mut Value, - index: usize, - action: SlashActionKind, -) -> Result<(), String> { - let content = document_content_mut(document)?; - let next = turn_into_block( - content - .get(index) - .ok_or_else(|| format!("找不到第 {index} 个块"))?, - action, - ); - content[index] = next; - Ok(()) -} - fn apply_html_update( editor: TiptapEditorHandle, persisted_identity: &PersistedDocumentIdentity, @@ -6185,7 +5989,10 @@ fn run_slash_action( editor.insert_table(4, 3, false) } SlashActionKind::Mindmap => { - editor.insert_content(TiptapContent::json(mindmap_paragraph_node()), None) + editor.insert_content( + TiptapContent::json(mindmap_paragraph_node(next_mindmap_id)), + None, + ) } SlashActionKind::Image => { dispatch_editor_upload_request("image", "image/*")?; @@ -6272,7 +6079,11 @@ fn run_block_turn_into_action( .map(|_| "已把当前块转成分割线") .map_err(|err| format!("命令执行失败:{err}")), SlashActionKind::Mindmap => editor - .insert_content_at(range, TiptapContent::json(mindmap_paragraph_node()), None) + .insert_content_at( + range, + TiptapContent::json(mindmap_paragraph_node(next_mindmap_id)), + None, + ) .map(|_| "已把当前块转成思维导图") .map_err(|err| format!("命令执行失败:{err}")), _ => {