diff --git a/.claw/sessions/570ce2f4c13b9ce5/session-1776666102061-0.jsonl b/.claw/sessions/570ce2f4c13b9ce5/session-1776666102061-0.jsonl new file mode 100644 index 00000000..9a7c79bf --- /dev/null +++ b/.claw/sessions/570ce2f4c13b9ce5/session-1776666102061-0.jsonl @@ -0,0 +1,14 @@ +{"created_at_ms":1776666102060,"model":"gpt-5.4","session_id":"session-1776666102061-0","type":"session_meta","updated_at_ms":1776672611351,"version":1,"workspace_root":"/mnt/Data1T/mnote"} +{"count":6,"removed_message_count":82,"summary":"\nConversation summary:\n- Previously compacted context:\n - Previously compacted context:\n - Previously compacted context:\n - Previously compacted context:\n - Previously compacted context:\n - Scope: 7 earlier messages compacted (user=1, assistant=3, tool=3).\n - Tools mentioned: glob_search, read_file.\n - Recent user requests:\n - 请检查/mnt/Data1T/mnote/design,看看当下我们需要最优先推进什么\n - Key files referenced: /design/cankao/filetree.md, /design/cankao/yemianshu.md, /mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md, /mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md, /mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md.\n - Current work: 请检查/mnt/Data1T/mnote/design,看看当下我们需要最优先推进什么\n - Newly compacted context:\n - Scope: 22 earlier messages compacted (user=1, assistant=11, tool=10).\n - Tools mentioned: read_file.\n - Recent user requests:\n - 当前git中已经有一些是在推进了,请检查一下其完成的是否正确。\n - Pending work:\n - 我看了当前 `design/` 里和主线最相关的文档,结论很明确:\n ## 当前最优先推进的,不是树域、不是 Rust Web 总体、也不是 AI\n 而是:\n # **05-editor-mainline / P0.5:把 `leptos-tiptap` 主编辑器真正接入真实文档页,并接上 Rust truth**\n 最…\n - Key files referenced: /mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md, /mnt/Data1T/mnote/design/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md, /mnt/Data1T/mnote/harness-tasks.json, /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.test.ts.\n - Current work: 当前git中已经有一些是在推进了,请检查一下其完成的是否正确。\n - Newly compacted context:\n - Scope: 72 earlier messages compacted (user=1, assistant=36, tool=35).\n - Tools mentioned: TodoWrite, bash, edit_file, glob_search, grep_search, read_file, write_file.\n - Recent user requests:\n - 请按你的想法,继续推进完成,我想要达到的检查点是:3000端口打开后,我检查发现tiptap已经替换blocknote作为主编辑器了,并且可以正常编辑,没有现在的阅读状态,我们所有前端都是直接对标wolai的,wolai直接打开就能编辑,阅读状态只是一种妥协。\n - Pending work:\n - 我检查了当前 git 里这批“正在推进主编辑器 cutover”的改动。结论先说:\n # 结论\n **这批改动方向基本对,但完成度并不算“正确完成”。**\n 更准确地说是:\n - **task-002 这一层接入真实页面 host 的方向是对的**\n - 但 **task-003 ~ task-006 所依赖的“Rust t…\n - Key files referenced: /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs, /mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx, /mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/save/route.ts, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-shell.tsx.\n - Current work: 请按你的想法,继续推进完成,我想要达到的检查点是:3000端口打开后,我检查发现tiptap已经替换blocknote作为主编辑器了,并且可以正常编辑,没有现在的阅读状态,我们所有前端都是直接对标wolai的,wolai直接打开就能编辑,阅读状态只是一种妥协。\n - Newly compacted context:\n - Scope: 28 earlier messages compacted (user=1, assistant=14, tool=13).\n - Tools mentioned: TodoWrite, bash, grep_search, read_file.\n - Recent user requests:\n - 我手动测试发现:/mnt/Data1T/mnote/tmp/image copy 22.png,显示有问题,只显示了上面一小部分,而且所有页面都是使用的一个tiptap页面,内容都一样。离真正可用差很远,我们之前是存储在convex中的,你是否没有挂存储。\n - Pending work:\n - 我已经继续往你要的检查点推进了一步,核心目标是:\n - **3000 打开文档页默认就是可编辑态**\n - **主编辑器入口是 Tiptap host,不再以 BlockNote 为主**\n - **开始把 Tiptap <-> Rust truth 的最小 block/load/save 合同纠正过来**\n - **去掉“…\n - Key files referenced: /mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/content/route.ts, /mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/save/route.ts, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-subtree-response.ts, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/save-contract.ts, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.ts.\n - Current work: 我手动测试发现:/mnt/Data1T/mnote/tmp/image copy 22.png,显示有问题,只显示了上面一小部分,而且所有页面都是使用的一个tiptap页面,内容都一样。离真正可用差很远,我们之前是存储在convex中的,你是否没有挂存储。\n - Newly compacted context:\n - Scope: 2 earlier messages compacted (user=0, assistant=1, tool=1).\n - Tools mentioned: read_file.\n - Key files referenced: /mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host-types.ts.\n- Newly compacted context:\n - Scope: 82 earlier messages compacted (user=2, assistant=41, tool=39).\n - Tools mentioned: TodoWrite, bash, edit_file, glob_search, grep_search, read_file, write_file.\n - Recent user requests:\n - 所以,请你规划任务,真正实现“tiptap 已替换 blocknote 成为可用主编辑器,再让我验收\n - 开始执行,待我可以人工核验时再通知我\n - Pending work:\n - 你这个判断是对的。\n **我前一轮并没有把真实存储链正确挂上。**\n 而且从你描述的两个现象看,问题基本已经定位得很清楚了:\n ---\n # 现在的两个核心问题\n ## 1) “只显示上面一小部分”\n 这大概率不是内容本身,而是 **iframe 嵌入高度同步没做好**。\n 当前 `LeptosTiptapEditor…\n - 下面是我会执行的**正式任务规划**,目标只有一个:\n **让 3000 文档页上,Tiptap 真正替换 BlockNote 成为可用主编辑器,并且你可以直接人工验收。**\n ---\n # 总目标\n 达到这几个硬标准:\n 1. **打开 `/documents/[id]` 默认直接进入可编辑态**\n 2. **页面主编…\n - Key files referenced: /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs, /mnt/Data1T/mnote/rust/spikes/leptos-tiptap-spike/src/main.rs, /mnt/Data1T/mnote/wolai-frontend/.next/dev/types/routes.d.ts, /mnt/Data1T/mnote/wolai-frontend/.next/dev/types/validator.ts, /mnt/Data1T/mnote/wolai-frontend/.next/types/routes.d.ts, /mnt/Data1T/mnote/wolai-frontend/.next/types/validator.ts, /mnt/Data1T/mnote/wolai-frontend/node_modules/.pnpm/@mantine+core@8.3.8_@mantine+hooks@8.3.8_react@19.2.0__@types+react@19.2.5_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@mantine/core/lib/components/TableOfContents/TableOfContents.d.ts.\n - Current work: 我先开始做实现,不再中途打扰你。\n 当前已经确认两个关键事实:\n - 现在确实是 **demo iframe 接入**,不是真实文档级主编辑器闭环\n - 8123 runtime 里**几乎还没有正式 host bridge 协议实现**,所以必须同时改宿主和 runtime\n 我接下来按“先打通真实 bootstrap / reset / height / save,再做验收链”直接往下做。\n- Key timeline:\n - assistant: 你这个判断是对的。\n **我前一轮并没有把真实存储链正确挂上。**\n","type":"compaction"} +{"text":"请检查/mnt/Data1T/mnote/design,看看当下我们需要最优先推进什么","timestamp_ms":1776666141936,"type":"prompt_history"} +{"text":"当前git中已经有一些是在推进了,请检查一下其完成的是否正确。","timestamp_ms":1776667640453,"type":"prompt_history"} +{"text":"请按你的想法,继续推进完成,我想要达到的检查点是:3000端口打开后,我检查发现tiptap已经替换blocknote作为主编辑器了,并且可以正常编辑,没有现在的阅读状态,我们所有前端都是直接对标wolai的,wolai直接打开就能编辑,阅读状态只是一种妥协。","timestamp_ms":1776668500096,"type":"prompt_history"} +{"text":"我手动测试发现:/mnt/Data1T/mnote/tmp/image copy 22.png,显示有问题,只显示了上面一小部分,而且所有页面都是使用的一个tiptap页面,内容都一样。离真正可用差很远,我们之前是存储在convex中的,你是否没有挂存储。","timestamp_ms":1776670439374,"type":"prompt_history"} +{"text":"所以,请你规划任务,真正实现“tiptap 已替换 blocknote 成为可用主编辑器,再让我验收","timestamp_ms":1776670763211,"type":"prompt_history"} +{"text":"开始执行,待我可以人工核验时再通知我","timestamp_ms":1776670991445,"type":"prompt_history"} +{"message":{"blocks":[{"text":"This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.\n\nSummary:\nConversation summary:\n- Previously compacted context:\n - Previously compacted context:\n - Previously compacted context:\n - Previously compacted context:\n - Previously compacted context:\n - Scope: 7 earlier messages compacted (user=1, assistant=3, tool=3).\n - Tools mentioned: glob_search, read_file.\n - Recent user requests:\n - 请检查/mnt/Data1T/mnote/design,看看当下我们需要最优先推进什么\n - Key files referenced: /design/cankao/filetree.md, /design/cankao/yemianshu.md, /mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md, /mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md, /mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md.\n - Current work: 请检查/mnt/Data1T/mnote/design,看看当下我们需要最优先推进什么\n - Newly compacted context:\n - Scope: 22 earlier messages compacted (user=1, assistant=11, tool=10).\n - Tools mentioned: read_file.\n - Recent user requests:\n - 当前git中已经有一些是在推进了,请检查一下其完成的是否正确。\n - Pending work:\n - 我看了当前 `design/` 里和主线最相关的文档,结论很明确:\n ## 当前最优先推进的,不是树域、不是 Rust Web 总体、也不是 AI\n 而是:\n # **05-editor-mainline / P0.5:把 `leptos-tiptap` 主编辑器真正接入真实文档页,并接上 Rust truth**\n 最…\n - Key files referenced: /mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md, /mnt/Data1T/mnote/design/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md, /mnt/Data1T/mnote/harness-tasks.json, /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.test.ts.\n - Current work: 当前git中已经有一些是在推进了,请检查一下其完成的是否正确。\n - Newly compacted context:\n - Scope: 72 earlier messages compacted (user=1, assistant=36, tool=35).\n - Tools mentioned: TodoWrite, bash, edit_file, glob_search, grep_search, read_file, write_file.\n - Recent user requests:\n - 请按你的想法,继续推进完成,我想要达到的检查点是:3000端口打开后,我检查发现tiptap已经替换blocknote作为主编辑器了,并且可以正常编辑,没有现在的阅读状态,我们所有前端都是直接对标wolai的,wolai直接打开就能编辑,阅读状态只是一种妥协。\n - Pending work:\n - 我检查了当前 git 里这批“正在推进主编辑器 cutover”的改动。结论先说:\n # 结论\n **这批改动方向基本对,但完成度并不算“正确完成”。**\n 更准确地说是:\n - **task-002 这一层接入真实页面 host 的方向是对的**\n - 但 **task-003 ~ task-006 所依赖的“Rust t…\n - Key files referenced: /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs, /mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx, /mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/save/route.ts, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-shell.tsx.\n - Current work: 请按你的想法,继续推进完成,我想要达到的检查点是:3000端口打开后,我检查发现tiptap已经替换blocknote作为主编辑器了,并且可以正常编辑,没有现在的阅读状态,我们所有前端都是直接对标wolai的,wolai直接打开就能编辑,阅读状态只是一种妥协。\n - Newly compacted context:\n - Scope: 28 earlier messages compacted (user=1, assistant=14, tool=13).\n - Tools mentioned: TodoWrite, bash, grep_search, read_file.\n - Recent user requests:\n - 我手动测试发现:/mnt/Data1T/mnote/tmp/image copy 22.png,显示有问题,只显示了上面一小部分,而且所有页面都是使用的一个tiptap页面,内容都一样。离真正可用差很远,我们之前是存储在convex中的,你是否没有挂存储。\n - Pending work:\n - 我已经继续往你要的检查点推进了一步,核心目标是:\n - **3000 打开文档页默认就是可编辑态**\n - **主编辑器入口是 Tiptap host,不再以 BlockNote 为主**\n - **开始把 Tiptap <-> Rust truth 的最小 block/load/save 合同纠正过来**\n - **去掉“…\n - Key files referenced: /mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/content/route.ts, /mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/save/route.ts, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-subtree-response.ts, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/save-contract.ts, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.ts.\n - Current work: 我手动测试发现:/mnt/Data1T/mnote/tmp/image copy 22.png,显示有问题,只显示了上面一小部分,而且所有页面都是使用的一个tiptap页面,内容都一样。离真正可用差很远,我们之前是存储在convex中的,你是否没有挂存储。\n - Newly compacted context:\n - Scope: 2 earlier messages compacted (user=0, assistant=1, tool=1).\n - Tools mentioned: read_file.\n - Key files referenced: /mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host-types.ts.\n- Newly compacted context:\n - Scope: 82 earlier messages compacted (user=2, assistant=41, tool=39).\n - Tools mentioned: TodoWrite, bash, edit_file, glob_search, grep_search, read_file, write_file.\n - Recent user requests:\n - 所以,请你规划任务,真正实现“tiptap 已替换 blocknote 成为可用主编辑器,再让我验收\n - 开始执行,待我可以人工核验时再通知我\n - Pending work:\n - 你这个判断是对的。\n **我前一轮并没有把真实存储链正确挂上。**\n 而且从你描述的两个现象看,问题基本已经定位得很清楚了:\n ---\n # 现在的两个核心问题\n ## 1) “只显示上面一小部分”\n 这大概率不是内容本身,而是 **iframe 嵌入高度同步没做好**。\n 当前 `LeptosTiptapEditor…\n - 下面是我会执行的**正式任务规划**,目标只有一个:\n **让 3000 文档页上,Tiptap 真正替换 BlockNote 成为可用主编辑器,并且你可以直接人工验收。**\n ---\n # 总目标\n 达到这几个硬标准:\n 1. **打开 `/documents/[id]` 默认直接进入可编辑态**\n 2. **页面主编…\n - Key files referenced: /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs, /mnt/Data1T/mnote/rust/spikes/leptos-tiptap-spike/src/main.rs, /mnt/Data1T/mnote/wolai-frontend/.next/dev/types/routes.d.ts, /mnt/Data1T/mnote/wolai-frontend/.next/dev/types/validator.ts, /mnt/Data1T/mnote/wolai-frontend/.next/types/routes.d.ts, /mnt/Data1T/mnote/wolai-frontend/.next/types/validator.ts, /mnt/Data1T/mnote/wolai-frontend/node_modules/.pnpm/@mantine+core@8.3.8_@mantine+hooks@8.3.8_react@19.2.0__@types+react@19.2.5_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@mantine/core/lib/components/TableOfContents/TableOfContents.d.ts.\n - Current work: 我先开始做实现,不再中途打扰你。\n 当前已经确认两个关键事实:\n - 现在确实是 **demo iframe 接入**,不是真实文档级主编辑器闭环\n - 8123 runtime 里**几乎还没有正式 host bridge 协议实现**,所以必须同时改宿主和 runtime\n 我接下来按“先打通真实 bootstrap / reset / height / save,再做验收链”直接往下做。\n- Key timeline:\n - assistant: 你这个判断是对的。\n **我前一轮并没有把真实存储链正确挂上。**\n\nRecent messages are preserved verbatim.\nContinue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, and do not preface with continuation text.","type":"text"}],"role":"system"},"type":"message"} +{"message":{"blocks":[{"id":"call_9pakRXq3pLtenYxFSXGzHvey","input":"{\"allowedMounts\":[\"/mnt/Data1T/mnote\"],\"command\":\"python3 - <<'PY'\\nfrom pathlib import Path\\ntext = Path('/mnt/Data1T/mnote/rust/spikes/leptos-tiptap-spike/src/main.rs').read_text()\\nfor needle in ['#[component]\\\\nfn App()', 'TiptapEditor::new(', 'TiptapEditorHandle::new()', 'initial_editor_content', 'restored_from_storage', 'editor.on_update', 'on_transaction', 'set_document_json.set', 'mount_to_body']:\\n idx = text.find(needle)\\n print('\\\\n===', needle, idx)\\n if idx != -1:\\n start=max(0, idx-200)\\n end=min(len(text), idx+1800)\\n print(text[start:end])\\nPY\",\"dangerouslyDisableSandbox\":false,\"description\":\"Inspect app/component initialization and editor hooks in spike runtime\",\"filesystemMode\":\"workspace-only\",\"isolateNetwork\":true,\"namespaceRestrictions\":true,\"run_in_background\":false,\"timeout\":120000}","name":"bash","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":165036,"output_tokens":242}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"stdout\": \"\\n=== #[component]\\nfn App() 59827\\ny_ok_feedback(\\n setter: WriteSignal,\\n result: Result<(), E>,\\n ok_message: &'static str,\\n) where\\n E: Display,\\n{\\n apply_feedback_result(setter, result.map(|_| ok_message));\\n}\\n\\n#[component]\\nfn App() -> impl IntoView {\\n let editor = TiptapEditorHandle::new();\\n let persisted = load_persisted_document();\\n let restored_from_storage = persisted.is_some();\\n let restored_html = persisted\\n .as_ref()\\n .and_then(|document| document.html.clone());\\n let initial_title_value = persisted\\n .as_ref()\\n .map(|document| document.title.clone())\\n .unwrap_or_else(default_title);\\n let initial_editor_content = persisted\\n .as_ref()\\n .map(|document| {\\n document\\n .html\\n .clone()\\n .map(TiptapContent::html)\\n .unwrap_or_else(|| TiptapContent::json(document.content.clone()))\\n })\\n .unwrap_or_else(initial_content);\\n\\n let (title, set_title) = signal(initial_title_value);\\n let (html_output, set_html_output) = signal(String::new());\\n let (document_json, set_document_json) = signal(\\n persisted\\n .as_ref()\\n .map(|document| document.content.clone())\\n .unwrap_or_else(|| json!({\\\"type\\\": \\\"doc\\\", \\\"content\\\": []})),\\n );\\n let (json_output, set_json_output) = signal(String::new());\\n let (selection_state, set_selection_state) = signal(TiptapSelectionState::default());\\n let (dirty_count, set_dirty_count) = signal(0_u32);\\n let (editor_focused, set_editor_focused) = signal(false);\\n let (text_selection_active, set_text_selection_active) = signal(false);\\n let (floating_toolbar_anchor, set_floating_toolbar_anchor) =\\n signal(None::);\\n let (locked_toolbar_anchor, set_locked_toolbar_anchor) = signal(None::);\\n let (slash_open, set_slash_open) = signal(false);\\n let (slash_index, set_slash_index) = signal(0_usize);\\n let (tur\\n\\n=== TiptapEditor::new( -1\\n\\n=== TiptapEditorHandle::new() 59885\\nult: Result<(), E>,\\n ok_message: &'static str,\\n) where\\n E: Display,\\n{\\n apply_feedback_result(setter, result.map(|_| ok_message));\\n}\\n\\n#[component]\\nfn App() -> impl IntoView {\\n let editor = TiptapEditorHandle::new();\\n let persisted = load_persisted_document();\\n let restored_from_storage = persisted.is_some();\\n let restored_html = persisted\\n .as_ref()\\n .and_then(|document| document.html.clone());\\n let initial_title_value = persisted\\n .as_ref()\\n .map(|document| document.title.clone())\\n .unwrap_or_else(default_title);\\n let initial_editor_content = persisted\\n .as_ref()\\n .map(|document| {\\n document\\n .html\\n .clone()\\n .map(TiptapContent::html)\\n .unwrap_or_else(|| TiptapContent::json(document.content.clone()))\\n })\\n .unwrap_or_else(initial_content);\\n\\n let (title, set_title) = signal(initial_title_value);\\n let (html_output, set_html_output) = signal(String::new());\\n let (document_json, set_document_json) = signal(\\n persisted\\n .as_ref()\\n .map(|document| document.content.clone())\\n .unwrap_or_else(|| json!({\\\"type\\\": \\\"doc\\\", \\\"content\\\": []})),\\n );\\n let (json_output, set_json_output) = signal(String::new());\\n let (selection_state, set_selection_state) = signal(TiptapSelectionState::default());\\n let (dirty_count, set_dirty_count) = signal(0_u32);\\n let (editor_focused, set_editor_focused) = signal(false);\\n let (text_selection_active, set_text_selection_active) = signal(false);\\n let (floating_toolbar_anchor, set_floating_toolbar_anchor) =\\n signal(None::);\\n let (locked_toolbar_anchor, set_locked_toolbar_anchor) = signal(None::);\\n let (slash_open, set_slash_open) = signal(false);\\n let (slash_index, set_slash_index) = signal(0_usize);\\n let (turn_into_open, set_turn_into_open) = signal(false);\\n let \\n\\n=== initial_editor_content 60271\\n .and_then(|document| document.html.clone());\\n let initial_title_value = persisted\\n .as_ref()\\n .map(|document| document.title.clone())\\n .unwrap_or_else(default_title);\\n let initial_editor_content = persisted\\n .as_ref()\\n .map(|document| {\\n document\\n .html\\n .clone()\\n .map(TiptapContent::html)\\n .unwrap_or_else(|| TiptapContent::json(document.content.clone()))\\n })\\n .unwrap_or_else(initial_content);\\n\\n let (title, set_title) = signal(initial_title_value);\\n let (html_output, set_html_output) = signal(String::new());\\n let (document_json, set_document_json) = signal(\\n persisted\\n .as_ref()\\n .map(|document| document.content.clone())\\n .unwrap_or_else(|| json!({\\\"type\\\": \\\"doc\\\", \\\"content\\\": []})),\\n );\\n let (json_output, set_json_output) = signal(String::new());\\n let (selection_state, set_selection_state) = signal(TiptapSelectionState::default());\\n let (dirty_count, set_dirty_count) = signal(0_u32);\\n let (editor_focused, set_editor_focused) = signal(false);\\n let (text_selection_active, set_text_selection_active) = signal(false);\\n let (floating_toolbar_anchor, set_floating_toolbar_anchor) =\\n signal(None::);\\n let (locked_toolbar_anchor, set_locked_toolbar_anchor) = signal(None::);\\n let (slash_open, set_slash_open) = signal(false);\\n let (slash_index, set_slash_index) = signal(0_usize);\\n let (turn_into_open, set_turn_into_open) = signal(false);\\n let (color_menu_open, set_color_menu_open) = signal(false);\\n let (more_menu_open, set_more_menu_open) = signal(false);\\n let (hovered_block, set_hovered_block) = signal(None::);\\n let (block_menu_anchor, set_block_menu_anchor) = signal(None::);\\n let (block_menu_open, set_block_menu_open) = signal(false);\\n let (block_turn_into_open, set_b\\n\\n=== restored_from_storage 59967\\nply_feedback_result(setter, result.map(|_| ok_message));\\n}\\n\\n#[component]\\nfn App() -> impl IntoView {\\n let editor = TiptapEditorHandle::new();\\n let persisted = load_persisted_document();\\n let restored_from_storage = persisted.is_some();\\n let restored_html = persisted\\n .as_ref()\\n .and_then(|document| document.html.clone());\\n let initial_title_value = persisted\\n .as_ref()\\n .map(|document| document.title.clone())\\n .unwrap_or_else(default_title);\\n let initial_editor_content = persisted\\n .as_ref()\\n .map(|document| {\\n document\\n .html\\n .clone()\\n .map(TiptapContent::html)\\n .unwrap_or_else(|| TiptapContent::json(document.content.clone()))\\n })\\n .unwrap_or_else(initial_content);\\n\\n let (title, set_title) = signal(initial_title_value);\\n let (html_output, set_html_output) = signal(String::new());\\n let (document_json, set_document_json) = signal(\\n persisted\\n .as_ref()\\n .map(|document| document.content.clone())\\n .unwrap_or_else(|| json!({\\\"type\\\": \\\"doc\\\", \\\"content\\\": []})),\\n );\\n let (json_output, set_json_output) = signal(String::new());\\n let (selection_state, set_selection_state) = signal(TiptapSelectionState::default());\\n let (dirty_count, set_dirty_count) = signal(0_u32);\\n let (editor_focused, set_editor_focused) = signal(false);\\n let (text_selection_active, set_text_selection_active) = signal(false);\\n let (floating_toolbar_anchor, set_floating_toolbar_anchor) =\\n signal(None::);\\n let (locked_toolbar_anchor, set_locked_toolbar_anchor) = signal(None::);\\n let (slash_open, set_slash_open) = signal(false);\\n let (slash_index, set_slash_index) = signal(0_usize);\\n let (turn_into_open, set_turn_into_open) = signal(false);\\n let (color_menu_open, set_color_menu_open) = signal(false);\\n let (more_menu_open, s\\n\\n=== editor.on_update -1\\n\\n=== on_transaction -1\\n\\n=== set_document_json.set 25544\\n set_document_json: WriteSignal,\\n set_json_output: WriteSignal,\\n) -> Value {\\n let (html, json_value, json_text) = read_editor_snapshot(editor);\\n set_html_output.set(html);\\n set_document_json.set(json_value.clone());\\n set_json_output.set(json_text);\\n json_value\\n}\\n\\nfn editor_root_element() -> Option {\\n window()\\n .and_then(|win| win.document())\\n .and_then(|document| document.query_selector(EDITOR_ROOT_SELECTOR).ok().flatten())\\n}\\n\\nfn editor_stage_element() -> Option {\\n window()\\n .and_then(|win| win.document())\\n .and_then(|document| {\\n document\\n .query_selector(EDITOR_STAGE_SELECTOR)\\n .ok()\\n .flatten()\\n })\\n}\\n\\nfn direct_block_from_element(mut element: Element, root: &Element) -> Option {\\n loop {\\n let parent = element.parent_element()?;\\n if parent.is_same_node(Some(root)) {\\n return Some(element);\\n }\\n element = parent;\\n }\\n}\\n\\nfn top_level_block_index(root: &Element, block: &Element) -> Option {\\n let children = root.children();\\n (0..children.length()).find_map(|index| {\\n children\\n .item(index)\\n .filter(|child| child.is_same_node(Some(block)))\\n .map(|_| index as usize)\\n })\\n}\\n\\nfn block_label_for_element(block: &Element) -> String {\\n match block.get_attribute(\\\"data-type\\\").as_deref() {\\n Some(\\\"taskList\\\") => \\\"Todo 列表\\\".to_string(),\\n Some(\\\"taskItem\\\") => \\\"Todo 项\\\".to_string(),\\n _ => match block.tag_name().as_str() {\\n \\\"H1\\\" => \\\"一级标题\\\".to_string(),\\n \\\"H2\\\" => \\\"二级标题\\\".to_string(),\\n \\\"H3\\\" => \\\"三级标题\\\".to_string(),\\n \\\"P\\\" => \\\"段落\\\".to_string(),\\n \\\"UL\\\" => \\\"无序列表\\\".to_string(),\\n \\\"OL\\\" => \\\"有序列表\\\".to_string(),\\n \\\"BLOCKQUOTE\\\" => \\\"引用块\\\".to_string(),\\n \\\"PRE\\\" => \\\"代码块\\\".to_string(),\\n \\\"HR\\\" => \\\"分割线\\\".to_string(),\\n other => format!(\\\"块节点 {ot\\n\\n=== mount_to_body 35\\nuse leptos::ev;\\nuse leptos::mount::mount_to_body;\\nuse leptos::prelude::*;\\nuse leptos_dom::helpers::window_event_listener;\\nuse leptos_tiptap::{\\n TiptapCodeBlockAttributes, TiptapColorAttributes, TiptapContent, TiptapEditor,\\n TiptapEditorHandle, TiptapExtension, TiptapHeadingLevel, TiptapHighlightAttributes,\\n TiptapLinkResource, TiptapMarkName, TiptapRange, TiptapSelectionState, TiptapTextAlign,\\n};\\nuse serde::{Deserialize, Serialize};\\nuse serde_json::{json, Map, Value};\\nuse std::fmt::Display;\\nuse wasm_bindgen::JsCast;\\nuse web_sys::{window, DragEvent, Element, HtmlElement, MouseEvent, Node, Storage, WheelEvent};\\n\\nconst EDITOR_STAGE_SELECTOR: &str = \\\"#editor-stage\\\";\\nconst EDITOR_ROOT_SELECTOR: &str = \\\".editor-surface .ProseMirror\\\";\\nconst HANDLE_SHELL_SELECTOR: &str = \\\".block-handle-shell\\\";\\nconst SPIKE_STORAGE_KEY: &str = \\\"mnote.leptos-tiptap-spike.document\\\";\\nconst HANDLE_MULTILINE_HEIGHT: f64 = 40.0;\\nconst HANDLE_GUTTER_WIDTH: f64 = 92.0;\\n\\n#[derive(Clone, Copy)]\\nstruct ToolbarColorOption {\\n id: &'static str,\\n label: &'static str,\\n value: &'static str,\\n}\\n\\n#[derive(Clone, Copy)]\\nstruct ToolbarAlignOption {\\n id: &'static str,\\n label: &'static str,\\n alignment: TiptapTextAlign,\\n}\\n\\nconst TOOLBAR_TEXT_COLORS: [ToolbarColorOption; 10] = [\\n ToolbarColorOption {\\n id: \\\"default\\\",\\n label: \\\"默认文字\\\",\\n value: \\\"#111827\\\",\\n },\\n ToolbarColorOption {\\n id: \\\"gray\\\",\\n label: \\\"灰色\\\",\\n value: \\\"#6b7280\\\",\\n },\\n ToolbarColorOption {\\n id: \\\"brown\\\",\\n label: \\\"棕色\\\",\\n value: \\\"#8b5e3c\\\",\\n },\\n ToolbarColorOption {\\n id: \\\"orange\\\",\\n label: \\\"橙色\\\",\\n value: \\\"#c96b1f\\\",\\n },\\n ToolbarColorOption {\\n id: \\\"yellow\\\",\\n label: \\\"黄色\\\",\\n value: \\\"#b7791f\\\",\\n },\\n ToolbarColorOption {\\n id: \\\"green\\\",\\n la\\n\",\n \"stderr\": \"\",\n \"rawOutputPath\": null,\n \"interrupted\": false,\n \"isImage\": null,\n \"backgroundTaskId\": null,\n \"backgroundedByUser\": null,\n \"assistantAutoBackgrounded\": null,\n \"dangerouslyDisableSandbox\": false,\n \"returnCodeInterpretation\": null,\n \"noOutputExpected\": false,\n \"structuredContent\": null,\n \"persistedOutputPath\": null,\n \"persistedOutputSize\": null,\n \"sandboxStatus\": {\n \"enabled\": true,\n \"requested\": {\n \"enabled\": true,\n \"namespace_restrictions\": true,\n \"network_isolation\": true,\n \"filesystem_mode\": \"workspace-only\",\n \"allowed_mounts\": [\n \"/mnt/Data1T/mnote\"\n ]\n },\n \"supported\": true,\n \"active\": true,\n \"namespace_supported\": true,\n \"namespace_active\": true,\n \"network_supported\": true,\n \"network_active\": true,\n \"filesystem_mode\": \"workspace-only\",\n \"filesystem_active\": true,\n \"allowed_mounts\": [\n \"/mnt/Data1T/mnote\"\n ],\n \"in_container\": false,\n \"container_markers\": [],\n \"fallback_reason\": null\n }\n}","tool_name":"bash","tool_use_id":"call_9pakRXq3pLtenYxFSXGzHvey","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"text":"已开始实际改造,不再按“demo 嵌入”思路修补。\n\n我已经先动了主宿主层,方向明确是:\n\n- `DocumentContent -> EditorHost` 透传真实 title / document metadata\n- `LeptosTiptapEditorHost` 从 **query-string content 初始化** 改成:\n - iframe 只承载 runtime\n - ready 后发 **bootstrap**\n - 后续文档切换发 **replace-document**\n- 同时补了 **height message** 接口和宿主侧 iframe 动态高度\n\n接下来我会继续把 8123 runtime 补成真正能接收这些 bridge 消息的嵌入式运行页,并把它从 localStorage demo 模式改成文档级 bootstrap/reset 模式。 \n完成到可人工验收前,我不会再打断你。","type":"text"},{"id":"call_kcvjrrsFEEi2gVmIRxwFs5t0","input":"{\"todos\":[{\"activeForm\":\"审计真实 documents.content/documents.save/Convex content contract\",\"content\":\"审计真实 documents.content/documents.save/Convex content contract\",\"status\":\"completed\"},{\"activeForm\":\"把 LeptosTiptapEditorHost 从 query-string 初始化改成 bootstrap postMessage\",\"content\":\"把 LeptosTiptapEditorHost 从 query-string 初始化改成 bootstrap postMessage\",\"status\":\"in_progress\"},{\"activeForm\":\"让 8123 runtime 支持 bootstrap/reset 并避免跨文档复用错误状态\",\"content\":\"让 8123 runtime 支持 bootstrap/reset 并避免跨文档复用错误状态\",\"status\":\"in_progress\"},{\"activeForm\":\"补 iframe 高度同步\",\"content\":\"补 iframe 高度同步\",\"status\":\"in_progress\"},{\"activeForm\":\"建立 stored content 到 tiptap doc 的正式 adapter\",\"content\":\"建立 stored content 到 tiptap doc 的正式 adapter\",\"status\":\"pending\"},{\"activeForm\":\"建立 tiptap doc 到 stored content 的正式 adapter\",\"content\":\"建立 tiptap doc 到 stored content 的正式 adapter\",\"status\":\"pending\"},{\"activeForm\":\"接通真实保存与 persisted meta 回写\",\"content\":\"接通真实保存与 persisted meta 回写\",\"status\":\"pending\"},{\"activeForm\":\"固定 /documents/[id] 默认直接编辑态\",\"content\":\"固定 /documents/[id] 默认直接编辑态\",\"status\":\"pending\"},{\"activeForm\":\"固定 EditorHost 主路径为 tiptap 并把 BlockNote 退到 compat\",\"content\":\"固定 EditorHost 主路径为 tiptap 并把 BlockNote 退到 compat\",\"status\":\"pending\"},{\"activeForm\":\"补验收脚本并完成主编辑器人工验收准备\",\"content\":\"补验收脚本并完成主编辑器人工验收准备\",\"status\":\"pending\"}]}","name":"TodoWrite","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":168941,"output_tokens":575}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"oldTodos\": [\n {\n \"content\": \"审计真实 documents.content/documents.save/Convex content contract\",\n \"activeForm\": \"审计真实 documents.content/documents.save/Convex content contract\",\n \"status\": \"in_progress\"\n },\n {\n \"content\": \"把 LeptosTiptapEditorHost 从 query-string 初始化改成 bootstrap postMessage\",\n \"activeForm\": \"把 LeptosTiptapEditorHost 从 query-string 初始化改成 bootstrap postMessage\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"让 8123 runtime 支持 bootstrap/reset 并避免跨文档复用错误状态\",\n \"activeForm\": \"让 8123 runtime 支持 bootstrap/reset 并避免跨文档复用错误状态\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"补 iframe 高度同步\",\n \"activeForm\": \"补 iframe 高度同步\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"建立 stored content 到 tiptap doc 的正式 adapter\",\n \"activeForm\": \"建立 stored content 到 tiptap doc 的正式 adapter\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"建立 tiptap doc 到 stored content 的正式 adapter\",\n \"activeForm\": \"建立 tiptap doc 到 stored content 的正式 adapter\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"接通真实保存与 persisted meta 回写\",\n \"activeForm\": \"接通真实保存与 persisted meta 回写\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"固定 /documents/[id] 默认直接编辑态\",\n \"activeForm\": \"固定 /documents/[id] 默认直接编辑态\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"固定 EditorHost 主路径为 tiptap 并把 BlockNote 退到 compat\",\n \"activeForm\": \"固定 EditorHost 主路径为 tiptap 并把 BlockNote 退到 compat\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"补验收脚本并完成主编辑器人工验收准备\",\n \"activeForm\": \"补验收脚本并完成主编辑器人工验收准备\",\n \"status\": \"pending\"\n }\n ],\n \"newTodos\": [\n {\n \"content\": \"审计真实 documents.content/documents.save/Convex content contract\",\n \"activeForm\": \"审计真实 documents.content/documents.save/Convex content contract\",\n \"status\": \"completed\"\n },\n {\n \"content\": \"把 LeptosTiptapEditorHost 从 query-string 初始化改成 bootstrap postMessage\",\n \"activeForm\": \"把 LeptosTiptapEditorHost 从 query-string 初始化改成 bootstrap postMessage\",\n \"status\": \"in_progress\"\n },\n {\n \"content\": \"让 8123 runtime 支持 bootstrap/reset 并避免跨文档复用错误状态\",\n \"activeForm\": \"让 8123 runtime 支持 bootstrap/reset 并避免跨文档复用错误状态\",\n \"status\": \"in_progress\"\n },\n {\n \"content\": \"补 iframe 高度同步\",\n \"activeForm\": \"补 iframe 高度同步\",\n \"status\": \"in_progress\"\n },\n {\n \"content\": \"建立 stored content 到 tiptap doc 的正式 adapter\",\n \"activeForm\": \"建立 stored content 到 tiptap doc 的正式 adapter\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"建立 tiptap doc 到 stored content 的正式 adapter\",\n \"activeForm\": \"建立 tiptap doc 到 stored content 的正式 adapter\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"接通真实保存与 persisted meta 回写\",\n \"activeForm\": \"接通真实保存与 persisted meta 回写\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"固定 /documents/[id] 默认直接编辑态\",\n \"activeForm\": \"固定 /documents/[id] 默认直接编辑态\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"固定 EditorHost 主路径为 tiptap 并把 BlockNote 退到 compat\",\n \"activeForm\": \"固定 EditorHost 主路径为 tiptap 并把 BlockNote 退到 compat\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"补验收脚本并完成主编辑器人工验收准备\",\n \"activeForm\": \"补验收脚本并完成主编辑器人工验收准备\",\n \"status\": \"pending\"\n }\n ],\n \"verificationNudgeNeeded\": null\n}","tool_name":"TodoWrite","tool_use_id":"call_kcvjrrsFEEi2gVmIRxwFs5t0","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"text":"收到。等到**可以人工核验**时,我再通知你。","type":"text"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":170539,"output_tokens":20}},"type":"message"} diff --git a/.claw/sessions/570ce2f4c13b9ce5/session-1776666102061-0.rot-1776669768652.jsonl b/.claw/sessions/570ce2f4c13b9ce5/session-1776666102061-0.rot-1776669768652.jsonl new file mode 100644 index 00000000..3e3a691e --- /dev/null +++ b/.claw/sessions/570ce2f4c13b9ce5/session-1776666102061-0.rot-1776669768652.jsonl @@ -0,0 +1,83 @@ +{"created_at_ms":1776666102060,"model":"gpt-5.4","session_id":"session-1776666102061-0","type":"session_meta","updated_at_ms":1776668050214,"version":1,"workspace_root":"/mnt/Data1T/mnote"} +{"count":2,"removed_message_count":22,"summary":"\nConversation summary:\n- Previously compacted context:\n - Scope: 7 earlier messages compacted (user=1, assistant=3, tool=3).\n - Tools mentioned: glob_search, read_file.\n - Recent user requests:\n - 请检查/mnt/Data1T/mnote/design,看看当下我们需要最优先推进什么\n - Key files referenced: /design/cankao/filetree.md, /design/cankao/yemianshu.md, /mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md, /mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md, /mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md.\n - Current work: 请检查/mnt/Data1T/mnote/design,看看当下我们需要最优先推进什么\n- Newly compacted context:\n - Scope: 22 earlier messages compacted (user=1, assistant=11, tool=10).\n - Tools mentioned: read_file.\n - Recent user requests:\n - 当前git中已经有一些是在推进了,请检查一下其完成的是否正确。\n - Pending work:\n - 我看了当前 `design/` 里和主线最相关的文档,结论很明确:\n ## 当前最优先推进的,不是树域、不是 Rust Web 总体、也不是 AI\n 而是:\n # **05-editor-mainline / P0.5:把 `leptos-tiptap` 主编辑器真正接入真实文档页,并接上 Rust truth**\n 最…\n - Key files referenced: /mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md, /mnt/Data1T/mnote/design/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md, /mnt/Data1T/mnote/harness-tasks.json, /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.test.ts.\n - Current work: 当前git中已经有一些是在推进了,请检查一下其完成的是否正确。\n- Key timeline:\n - assistant: tool_use read_file({\"limit\":220,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md\"})\n - tool: tool_result read_file: {\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md\",\n …\n - assistant: tool_use read_file({\"limit\":220,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/design/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md\"})\n - tool: tool_result read_file: {\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/design/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md\"…\n - assistant: 我看了当前 `design/` 里和主线最相关的文档,结论很明确:\n","type":"compaction"} +{"text":"请检查/mnt/Data1T/mnote/design,看看当下我们需要最优先推进什么","timestamp_ms":1776666141936,"type":"prompt_history"} +{"text":"当前git中已经有一些是在推进了,请检查一下其完成的是否正确。","timestamp_ms":1776667640453,"type":"prompt_history"} +{"message":{"blocks":[{"text":"This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.\n\nSummary:\nConversation summary:\n- Previously compacted context:\n - Scope: 7 earlier messages compacted (user=1, assistant=3, tool=3).\n - Tools mentioned: glob_search, read_file.\n - Recent user requests:\n - 请检查/mnt/Data1T/mnote/design,看看当下我们需要最优先推进什么\n - Key files referenced: /design/cankao/filetree.md, /design/cankao/yemianshu.md, /mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md, /mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md, /mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md.\n - Current work: 请检查/mnt/Data1T/mnote/design,看看当下我们需要最优先推进什么\n- Newly compacted context:\n - Scope: 22 earlier messages compacted (user=1, assistant=11, tool=10).\n - Tools mentioned: read_file.\n - Recent user requests:\n - 当前git中已经有一些是在推进了,请检查一下其完成的是否正确。\n - Pending work:\n - 我看了当前 `design/` 里和主线最相关的文档,结论很明确:\n ## 当前最优先推进的,不是树域、不是 Rust Web 总体、也不是 AI\n 而是:\n # **05-editor-mainline / P0.5:把 `leptos-tiptap` 主编辑器真正接入真实文档页,并接上 Rust truth**\n 最…\n - Key files referenced: /mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md, /mnt/Data1T/mnote/design/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md, /mnt/Data1T/mnote/harness-tasks.json, /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.test.ts.\n - Current work: 当前git中已经有一些是在推进了,请检查一下其完成的是否正确。\n- Key timeline:\n - assistant: tool_use read_file({\"limit\":220,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md\"})\n - tool: tool_result read_file: {\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md\",\n …\n - assistant: tool_use read_file({\"limit\":220,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/design/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md\"})\n - tool: tool_result read_file: {\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/design/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md\"…\n - assistant: 我看了当前 `design/` 里和主线最相关的文档,结论很明确:\n\nRecent messages are preserved verbatim.\nContinue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, and do not preface with continuation text.","type":"text"}],"role":"system"},"type":"message"} +{"message":{"blocks":[{"id":"call_ZFiuJJW2Ag4SMtohmQeA20Md","input":"{\"limit\":80,\"offset\":220,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/lib/runtime-config.ts\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":74457,"output_tokens":42}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/lib/runtime-config.ts\",\n \"content\": \" const mnoteWebTreeShellEnabled =\\n parseRuntimeBoolean(cfg.mnoteWebTreeShellEnabled) ?? false;\\n\\n return {\\n ...cfg,\\n isDesktop,\\n mnoteWebBaseUrl,\\n mnoteWebTreeShellEnabled,\\n onlyofficeBaseUrl,\\n onlyofficeStorageHostOverride,\\n onlyofficeProxyOrigin,\\n onlyofficeCallbackOrigin,\\n };\\n};\\n\\nexport function getMnoteRuntimeConfig(): MnoteRuntimeConfig {\\n if (typeof window !== \\\"undefined\\\") {\\n return normalizeRuntimeConfig(window.__MNOTE_RUNTIME_CONFIG__ ?? readFromEnv());\\n }\\n const isDesktop = process.env.MNOTE_DESKTOP === \\\"1\\\";\\n const publicRuntime = readFromPublicJson();\\n const envRuntime = readFromEnv();\\n // 说明:桌面端需要优先使用 public/mnote-env.json 来覆盖 build 时注入的 NEXT_PUBLIC_*。\\n // Web 端开发时则应优先使用环境变量(例如本机 http://127.0.0.1:18000),避免被\\n // public/mnote-env.json 中的远程/自签地址覆盖导致浏览器登录请求失败。\\n const merged: MnoteRuntimeConfig = isDesktop\\n ? {\\n ...envRuntime,\\n ...publicRuntime,\\n isDesktop,\\n }\\n : {\\n ...publicRuntime,\\n ...envRuntime,\\n // 说明:Rust Web 的 tree shell 属于运行期开关,必须允许 public/mnote-env.json\\n // 在网页端覆盖环境变量;否则开发机上的旧 NEXT_PUBLIC_* 会把显式开关吃掉。\\n mnoteWebTreeShellEnabled:\\n publicRuntime.mnoteWebTreeShellEnabled ?? envRuntime.mnoteWebTreeShellEnabled,\\n };\\n return normalizeRuntimeConfig(merged);\\n}\",\n \"numLines\": 41,\n \"startLine\": 221,\n \"totalLines\": 261\n }\n}","tool_name":"read_file","tool_use_id":"call_ZFiuJJW2Ag4SMtohmQeA20Md","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_n6mGgVRvEtafqATSrIwAspHU","input":"{\"limit\":220,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host-types.ts\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":74994,"output_tokens":44}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host-types.ts\",\n \"content\": \"\\\"use client\\\";\\n\\nimport type { DocumentStats, PageOptionsState } from \\\"@/types/page-options\\\";\\nimport type { Json } from \\\"@/types/supabase\\\";\\nimport type { ReferenceTarget } from \\\"@/types/search\\\";\\n\\nexport interface DocumentEditorHostProps {\\n documentId: string;\\n workspaceId: string;\\n initialContent: unknown;\\n initialRevision?: number | null;\\n initialConflictDetectionKey?: string | null;\\n pageOptions: PageOptionsState;\\n readOnly?: boolean;\\n onStatsChange?: (stats: DocumentStats) => void;\\n onSnapshot?: (payload: { blocks: Json; stats: DocumentStats }) => void;\\n onCloseToc?: () => void;\\n onPersistedMetaChange?: (payload: {\\n revision: number | null;\\n conflictDetectionKey: string | null;\\n }) => void;\\n}\\n\\nexport type BlockNoteEditorProps = DocumentEditorHostProps;\\n\\nexport type LeptosTiptapHostBridgeState = {\\n ready: boolean;\\n runtimeName: string;\\n runtimeVersion: string;\\n runtimeUrl: string;\\n documentId: string;\\n workspaceId: string;\\n status: string;\\n lastChangeAt?: string | null;\\n lastSaveRequestAt?: string | null;\\n lastError?: string | null;\\n};\\n\\nexport type LeptosTiptapHostBridgeEventDetail = {\\n documentId: string;\\n workspaceId: string;\\n runtimeUrl: string;\\n status?: string;\\n payload?: unknown;\\n snapshot?: { blocks: Json; stats?: DocumentStats };\\n references?: ReferenceTarget[];\\n revision?: number | null;\\n conflictDetectionKey?: string | null;\\n error?: string | null;\\n at?: string;\\n};\",\n \"numLines\": 51,\n \"startLine\": 1,\n \"totalLines\": 51\n }\n}","tool_name":"read_file","tool_use_id":"call_n6mGgVRvEtafqATSrIwAspHU","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"text":"我检查了当前 git 里这批“正在推进主编辑器 cutover”的改动。结论先说:\n\n# 结论\n**这批改动方向基本对,但完成度并不算“正确完成”。**\n更准确地说是:\n\n- **task-002 这一层接入真实页面 host 的方向是对的**\n- 但 **task-003 ~ task-006 所依赖的“Rust truth / block_id / adapter contract”目前还没有正确落地**\n- 还有一些实现细节,已经出现了**偏离设计目标**或**后续必返工**的问题\n\n所以我不会把当前状态判成“已经正确完成”,更像是:\n\n- **UI/host 接入:部分正确**\n- **数据合同/真相边界:明显未完成**\n- **部分测试名义上存在,但还没真正覆盖对的合同**\n\n---\n\n# 我重点看了哪些地方\n\n主要看了这些新增/改动文件:\n\n- `wolai-frontend/src/components/editor/editor-host.tsx`\n- `wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx`\n- `wolai-frontend/src/components/editor/editor-host-types.ts`\n- `wolai-frontend/src/lib/documents/tiptap-content-converter.ts`\n- `wolai-frontend/src/lib/documents/tiptap-content-converter.test.ts`\n- `rust/crates/core-protocol/src/editor/tiptap.rs`\n- `rust/crates/core-protocol/tests/editor_tiptap_bridge.rs`\n- 以及 `document-content.tsx` / `page.tsx` / `sidebar.tsx` / `runtime-config.ts` 的接入改动\n\n---\n\n# 一、做对了的部分\n\n## 1. 主编辑器接入点选得对\n这点是对的,符合 `5-1-main-editor-cutover-entry-v1.md`:\n\n- 真实入口仍然是 `/documents/[id]`\n- `DocumentContent` 里把原来的 `BlockNoteEditor` 替换为 `EditorHost`\n- 没有把 `mnote-web /document` 误当主页面\n- 也没有把 `8123` spike 单独页误算成主编辑器完成\n\n这一点我认为是**正确推进**。\n\n---\n\n## 2. `autoEnterEdit` / 新建页面后直接进入编辑态,这个也合理\n这里:\n\n- `page.tsx` 增加 `edit=1`\n- `DocumentContent` 支持 `autoEnterEdit`\n- `Sidebar` 新建页面后跳到 `/documents/:id?edit=1&workspaceId=...`\n\n这符合真实页面上的编辑态切换思路,没问题。\n\n---\n\n## 3. `EditorHost` 抽象层本身是合理的\n`EditorHost -> LeptosTiptapEditorHost` 这层抽出来是对的:\n\n- 把 `BlockNoteEditor` 的 props 统一成 host props\n- 为后续 host 替换保留接缝\n- 没做太多抽象,没有明显过度设计\n\n这也是对的。\n\n---\n\n# 二、目前最主要的问题:**真正的 Rust truth contract 还没立住**\n\n这部分是当前改动里最不对的地方。\n\n---\n\n## 问题 1:前端 converter 仍然是“自定义 BlockLike JSON”,不是 Rust `EditorBlockDocument` 合同\n\n你现在的前端转换器:\n\n`wolai-frontend/src/lib/documents/tiptap-content-converter.ts`\n\n处理的是这种东西:\n\n```ts\ntype BlockLike = {\n id?: string;\n type?: string;\n content?: unknown;\n props?: Record;\n children?: unknown;\n};\n```\n\n然后在 Tiptap <-> blocks 之间互转。\n\n这和设计目标明显不一致。\n\n### 为什么不对\n`P0.5-3 ~ P0.5-6` 的目标是:\n\n- Rust `EditorBlock.block_id` 为真相\n- Rust `EditorBlockDocument` 为真相\n- Tiptap 只是 runtime surface / adapter\n\n但你现在前端 converter 的真相仍然是一个**前端散装 JSON block 数组**,不是 Rust protocol 的共享合同。\n\n### 直接后果\n这会导致:\n\n- block id 规则继续漂在前端\n- save/load contract 继续是页面自己猜\n- 后面 Rust/TS 两边会有两份“几乎一样但不完全一样”的 schema\n- task-003/004/005 名义上推进了,实际上没真正落到共享真相\n\n### 结论\n这部分**不算正确完成**。\n\n---\n\n## 问题 2:`block_id` 明确还没正确实现,反而被新代码继续冲淡了\n\n### Rust 侧\n在 `core-protocol/src/editor/tiptap.rs` 里,`from_tiptap_doc` 现在是这样生成 block id:\n\n```rust\nlet block_id = format!(\"block_{}\", index + 1);\n```\n\n也就是说:\n\n- 完全按导入顺序生成\n- 没优先读取稳定 block id\n- 也没把 paragraph / heading / list item / todo 的 block_id 从 attrs 里承接回来\n\n只有 code block 导出时带了:\n\n```rust\nblock_id: Some(block.block_id.clone())\n```\n\n但导入时又没用它。\n\n这和 `task-003` 的目标正好相反。\n\n### 前端侧\n前端测试里更明显:\n\n```ts\nexpect(blocksFromTiptapDoc(tiptapDocFromBlocks(blocks as never))).toEqual([\n { id: \"block-0\", type: \"paragraph\", content: \"你好,世界\" },\n]);\n```\n\n也就是说一 round-trip,原来的 `\"1\"` 已经丢了,变成 `\"block-0\"`。\n\n这说明当前测试实际上在**确认 block id 不稳定**,而不是防止它不稳定。\n\n### 结论\n这一点不是“没做完”而已,而是:\n\n# **当前实现与 task-003 目标相反**\n\n所以这部分我会直接判定为:\n\n- **未正确完成**\n- **而且测试口径本身就是错的**\n\n---\n\n## 问题 3:Rust bridge 只支持极薄的一层文本节点,过早命名为 bridge,会误导完成度\n\n`core-protocol/src/editor/tiptap.rs` 现在支持的内容非常窄:\n\n- `Text`\n- `Paragraph`\n- `Heading`\n- `BulletList` / `OrderedList` / `TaskList`\n- `Blockquote`\n- `CodeBlock`\n\n而且内容层只支持:\n\n```rust\nContentNodePayload::Text\nContentNodePayload::HardBreak -> \"\\n\"\nReferenceToken -> Unsupported\n```\n\n这本身没问题,**如果你把它明确叫做“最小 P0 adapter”**。\n\n但现在它的命名和测试很容易给人一种错觉:像是“EditorBlockDocument 和 Tiptap bridge 已经打通了”。\n\n其实还没有,因为:\n\n- block_id 不稳定\n- marks 没接\n- reference token 没接\n- list 结构只是平铺成一个 block\n- child_block_ids 完全没接\n- attrs/contract 很不完整\n\n### 结论\n不是说不能先做最小版,而是:\n**现在应该明确这是“最小 adapter skeleton”,不能把它算成 task-003~006 的正确完成。**\n\n---\n\n# 三、host 接入也有几个实现问题\n\n## 问题 4:`leptos-tiptap-editor-host.tsx` 仍然通过 iframe + query string 传完整 content,风险很高\n当前这里:\n\n```ts\nurl.searchParams.set(\"content\", JSON.stringify(normalizeRuntimeDoc(tiptapDoc)));\n```\n\n把整个文档内容塞进 iframe URL query。\n\n### 这有什么问题\n1. URL 长度风险\n - 文档一大就炸\n2. 内容暴露在地址栏 / history / 代理日志里\n3. 不是一个正式的 editor host contract\n4. reloadKey 一变就会重新生成很长 URL\n5. 这更像“spike embed”,不是主编辑器长期接口\n\n### 设计上允许短期这么做吗?\n作为 **task-002 的临时桥**,可以勉强接受;\n但如果你要判断“当前完成是否正确”,那我会说:\n\n- **临时验证可接受**\n- **不能算正式正确落地**\n- 必须尽快换成 postMessage 初始化或明确加载通道\n\n---\n\n## 问题 5:`resolveRuntimeUrl()` 把 `mnoteWebBaseUrl` 强行改到 8123 端口,这很脆\n这里:\n\n```ts\nconst url = new URL(cfg.mnoteWebBaseUrl);\nurl.port = String(RUNTIME_PORT);\n```\n\n这实际上是假设:\n\n- 配置里有个 mnote web base\n- 但 editor runtime 总能在同 host 的 8123 端口找到\n\n这是非常 spike 化的规则,不像正式 runtime config。\n\n### 问题\n- 8123 是开发/验证端口,不是稳定产品合同\n- 如果 host/domain 不同,这里就不成立\n- runtime config 已新增 `documentEditorHost`,但没新增真正的 runtime url 配置\n\n### 结论\n这部分可以支撑 smoke,但不算正确长期方案。\n\n---\n\n## 问题 6:save 仍然 POST 回旧 `/api/documents/save`,没有真正体现 Rust truth adapter 的正式边界\n现在 save 路径:\n\n```ts\nfetch(\"/api/documents/save\", ...)\n```\n\nbody 里传的是:\n\n```ts\ncontent: blocks\n```\n\n而 `blocks` 又是前端 converter 产物,不是 Rust `EditorBlockDocument`。\n\n这意味着当前只是:\n\n- 8123 iframe 输出 Tiptap doc\n- 前端把它转成散装 blocks\n- 再走旧 save route\n\n这仍然没有真正形成:\n\n- Tiptap doc -> EditorBlockDocument\n- EditorBlockDocument -> Rust truth persistence\n\n所以从任务视角说:\n\n- task-002:可以说页面接进来了\n- task-003~005:**还没真的对**\n\n---\n\n# 四、测试明显不够,且有错误导向\n\n## 问题 7:前端测试覆盖太弱,而且断言方向不对\n`wolai-frontend/src/lib/documents/tiptap-content-converter.test.ts` 只有两条:\n\n- empty\n- paragraph roundtrip\n\n而且 roundtrip 的断言接受 id 丢失:\n\n```ts\n{ id: \"block-0\", ... }\n```\n\n这跟当前主任务“稳定 block_id”相冲突。\n\n### 正确的测试至少要覆盖\n- id 保持\n- paragraph / heading / bullet / ordered / todo / quote / codeblock\n- reload 后不变\n- unsupported node 明确降级而非 silently corrupt\n- save/load roundtrip 与 Rust fixture 一致\n\n---\n\n## 问题 8:Rust 测试也在接受错误 block_id 行为\nRust 测试里:\n\n```rust\nassert_eq!(imported.blocks[2].block_id, \"block_3\");\n```\n\n这其实是在确认“重新编号”行为。\n\n如果 task-003 的目标是 Rust block_id 为真相,那这里就不应该以 index-based id 为正确结果。\n\n### 所以这条测试的问题不是“缺”\n而是:\n**它把错误行为写成了预期行为。**\n\n---\n\n# 五、还有一个小但明显的配置问题\n\n## 问题 9:`documentEditorHost` 类型加了,但实际上没真正参与 host 选择\n`runtime-config.ts` 里新增了:\n\n```ts\ndocumentEditorHost?: \"leptos_tiptap\";\n```\n\n但 `EditorHost` 里是硬编码:\n\n```tsx\nreturn ;\n```\n\n所以现在这个配置字段只是“写进类型里”,并没有真实参与切流。\n\n这不算大 bug,但说明:\n\n- 这还不是完整切流实现\n- 只是当前阶段固定单 host\n\n如果要诚实判断完成度,这一点应该明确写成“未真正使用 config”。\n\n---\n\n# 我对当前改动的总体判断\n\n## 可以认定为“基本正确推进”的\n1. 在真实 `/documents/[id]` 页面引入新的 editor host\n2. 在 `DocumentContent` 抽 host 层\n3. 新建页面直接进入编辑态\n4. 把 8123 runtime 嵌进真实文档页做主链验证\n\n## 不能认定为“正确完成”的\n1. `block_id` 真相策略\n2. Rust `EditorBlockDocument` 作为正式 load/save truth\n3. Tiptap adapter 合同\n4. save pipeline\n5. 回归测试口径\n\n---\n\n# 如果按 task 来判\n\n## task-002\n**大体可算完成,方向正确。**\n但要注明:\n- 这是“真实页面接入 8123 runtime 的桥接版完成”\n- 不是长期正式 host contract 完成\n\n## task-003\n**没有正确完成。**\n而且当前一些测试和实现还在反方向固化不稳定 id。\n\n## task-004\n**未正确完成。**\n因为目前 load adapter 不是以 Rust `EditorBlockDocument` 为真相,而是前端散装 blocks。\n\n## task-005\n**未正确完成。**\nsave adapter 仍然没真正形成 Rust truth 边界。\n\n## task-006\n**远未完成。**\n块类型 roundtrip 和 reload 稳定性还没有足够严谨的合同与测试。\n\n---\n\n# 最关键的整改建议\n\n我建议下一步不要继续盲目往后推功能,而是先把下面三件事纠正:\n\n## 1. 先纠正 block_id 合同\n目标:\n\n- Rust `EditorBlock.block_id` 为唯一真相\n- Tiptap node attrs 要能承接 block_id\n- import 时优先读已有 block_id\n- 不能再按 index 重编\n- 前后端测试都改成“id 保持稳定”\n\n这是第一优先。\n\n---\n\n## 2. 把前端 converter 改成围绕共享 EditorBlock 合同,而不是散装 BlockLike\n现在的 `BlockLike` 过于临时。\n至少要做到:\n\n- TS 侧定义与 Rust `EditorBlockDocument` 对齐的最小共享结构\n- converter 直接在 Tiptap doc 与该结构之间转换\n- 不再让页面 save pipeline 围绕“旧 blocknote 风格 blocks”运转\n\n---\n\n## 3. 把 iframe 初始化从 query-string content 改为更正式的初始化协议\n短期可以继续用 iframe,但建议:\n\n- iframe src 只传 documentId/workspaceId/embedded/reloadKey\n- 初始内容走 postMessage init\n- 后续 change/save 都走 message 协议\n- 不把大文档内容塞 URL\n\n---\n\n# 最终判断\n\n如果你要我一句话判断这批 git 改动:\n\n# **不是“完成得正确”,而是“接入方向对了,但核心合同还没对齐,尤其 block_id 和 Rust truth 边界还明显不对”。**\n\n如果你愿意,我下一步可以直接继续帮你做两件很具体的事之一:\n\n1. **逐文件列出“哪些代码要改,才能让 task-003/004/005 真正成立”**\n2. **直接给你一份 block_id / adapter contract 的修正方案清单**","type":"text"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":75542,"output_tokens":3408}},"type":"message"} +{"text":"请按你的想法,继续推进完成,我想要达到的检查点是:3000端口打开后,我检查发现tiptap已经替换blocknote作为主编辑器了,并且可以正常编辑,没有现在的阅读状态,我们所有前端都是直接对标wolai的,wolai直接打开就能编辑,阅读状态只是一种妥协。","timestamp_ms":1776668500096,"type":"prompt_history"} +{"message":{"blocks":[{"text":"请按你的想法,继续推进完成,我想要达到的检查点是:3000端口打开后,我检查发现tiptap已经替换blocknote作为主编辑器了,并且可以正常编辑,没有现在的阅读状态,我们所有前端都是直接对标wolai的,wolai直接打开就能编辑,阅读状态只是一种妥协。","type":"text"}],"role":"user"},"type":"message"} +{"message":{"blocks":[{"id":"call_DlxcA45AVeQy3N4dsclBSh3p","input":"{\"todos\":[{\"activeForm\":\"审查当前主编辑器接入与保存链路的相关代码和测试\",\"content\":\"审查当前主编辑器接入与保存链路的相关代码和测试\",\"status\":\"in_progress\"},{\"activeForm\":\"修正默认进入编辑态与主编辑器 host 选择,确保 3000 文档页默认使用 tiptap 而非 blocknote\",\"content\":\"修正默认进入编辑态与主编辑器 host 选择,确保 3000 文档页默认使用 tiptap 而非 blocknote\",\"status\":\"pending\"},{\"activeForm\":\"修正 tiptap 与 Rust truth 的最小 block_id/load/save 合同\",\"content\":\"修正 tiptap 与 Rust truth 的最小 block_id/load/save 合同\",\"status\":\"pending\"},{\"activeForm\":\"补充并运行针对主编辑器切换与编辑链路的测试\",\"content\":\"补充并运行针对主编辑器切换与编辑链路的测试\",\"status\":\"pending\"}]}","name":"TodoWrite","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":59882,"output_tokens":222}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"oldTodos\": [],\n \"newTodos\": [\n {\n \"content\": \"审查当前主编辑器接入与保存链路的相关代码和测试\",\n \"activeForm\": \"审查当前主编辑器接入与保存链路的相关代码和测试\",\n \"status\": \"in_progress\"\n },\n {\n \"content\": \"修正默认进入编辑态与主编辑器 host 选择,确保 3000 文档页默认使用 tiptap 而非 blocknote\",\n \"activeForm\": \"修正默认进入编辑态与主编辑器 host 选择,确保 3000 文档页默认使用 tiptap 而非 blocknote\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"修正 tiptap 与 Rust truth 的最小 block_id/load/save 合同\",\n \"activeForm\": \"修正 tiptap 与 Rust truth 的最小 block_id/load/save 合同\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"补充并运行针对主编辑器切换与编辑链路的测试\",\n \"activeForm\": \"补充并运行针对主编辑器切换与编辑链路的测试\",\n \"status\": \"pending\"\n }\n ],\n \"verificationNudgeNeeded\": null\n}","tool_name":"TodoWrite","tool_use_id":"call_DlxcA45AVeQy3N4dsclBSh3p","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_cfdVcoG8kp0OAfj9Nr4We9Rm","input":"{\"limit\":220,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":60396,"output_tokens":359}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx\",\n \"content\": \"\\\"use client\\\";\\n\\nimport \\\"@blocknote/core/style.css\\\";\\nimport \\\"@blocknote/react/style.css\\\";\\nimport \\\"@blocknote/mantine/style.css\\\";\\n\\nimport { useCallback, useEffect, useMemo, useRef, useState } from \\\"react\\\";\\nimport { BlockNoteView } from \\\"@blocknote/mantine\\\";\\nimport {\\n SideMenuController,\\n useCreateBlockNote,\\n type SideMenuProps,\\n} from \\\"@blocknote/react\\\";\\nimport { HocuspocusProvider } from \\\"@hocuspocus/provider\\\";\\nimport * as Y from \\\"yjs\\\";\\nimport type { Block } from \\\"@blocknote/core\\\";\\nimport { cn } from \\\"@/lib/utils\\\";\\nimport type { Json } from \\\"@/types/supabase\\\";\\nimport type { DocumentStats } from \\\"@/types/page-options\\\";\\nimport type { MediaAsset } from \\\"@/types/media\\\";\\nimport { customBlockSchema, type CustomBlockSchema } from \\\"./schema\\\";\\nimport { CustomSideMenu } from \\\"./menus/CustomSideMenu\\\";\\nimport { CustomSlashMenu } from \\\"./menus/CustomSlashMenu\\\";\\nimport { useDebouncedCallback } from \\\"@/hooks/use-debounced-callback\\\";\\nimport { DocumentToc, type TocEntry } from \\\"@/components/editor/document-toc\\\";\\nimport { useSearchPaletteStore } from \\\"@/store/search-palette\\\";\\nimport { useEditorBridgeStore } from \\\"@/store/editor-bridge\\\";\\nimport type { ReferenceTarget } from \\\"@/types/search\\\";\\nimport FullScreenTableEditor from \\\"@/components/online-table/FullScreenTableEditor\\\";\\nimport { clamp, MIN_BLOCK_LEVEL, MAX_BLOCK_LEVEL } from \\\"@/lib/constants\\\";\\nimport { ASSETS_CHANGED_EVENT, ASSETS_RESTORED_EVENT, emitAssetsChanged } from \\\"@/lib/events\\\";\\nimport { deleteOnlineTable } from \\\"@/lib/online-table\\\";\\nimport { useAppPreferencesStore } from \\\"@/store/app-preferences\\\";\\nimport { useCommentsUiStore } from \\\"@/store/comments-ui\\\";\\nimport { useConvexAuth, useQuery } from \\\"convex/react\\\";\\nimport { api } from \\\"@/lib/convex/api\\\";\\nimport { buildDocumentSavePayload } from \\\"@/lib/documents/save-contract\\\";\\nimport type { EditorReferenceBridge } from \\\"@/store/editor-bridge\\\";\\nimport type { BlockNoteEditorProps } from \\\"@/components/editor/editor-host-types\\\";\\n\\nconst extractInitialBlocks = (content: unknown): Json | undefined => {\\n if (Array.isArray(content) && content.length > 0) {\\n return content as Json;\\n }\\n if (content && typeof content === \\\"object\\\") {\\n const maybeBlocks = (content as Record).blocks;\\n if (Array.isArray(maybeBlocks) && maybeBlocks.length > 0) {\\n return maybeBlocks as Json;\\n }\\n }\\n return undefined;\\n};\\n\\nconst extractInlineText = (block: Block): string => {\\n const inlineNodes = (block.content ?? []) as Array<{ text?: string }>;\\n return inlineNodes.map((node) => (typeof node.text === \\\"string\\\" ? node.text : \\\"\\\")).join(\\\"\\\").trim();\\n};\\n\\nconst buildHeadingToc = (blocks: Block[]): TocEntry[] => {\\n const counters = [0, 0, 0, 0, 0];\\n const entries: TocEntry[] = [];\\n\\n const walk = (targetBlocks: Block[]) => {\\n targetBlocks.forEach((block) => {\\n if (block.type === \\\"heading\\\") {\\n const level = clamp(Number(block.props.level) || 1, MIN_BLOCK_LEVEL, MAX_BLOCK_LEVEL);\\n counters[level - 1] += 1;\\n for (let i = level; i < counters.length; i += 1) {\\n counters[i] = 0;\\n }\\n const numbering = counters.slice(0, level).filter((value) => value > 0).join(\\\".\\\");\\n entries.push({\\n id: block.id,\\n level,\\n numbering,\\n title: extractInlineText(block),\\n });\\n }\\n if (block.children && block.children.length > 0) {\\n walk(block.children as Block[]);\\n }\\n });\\n };\\n\\n walk(blocks);\\n return entries;\\n};\\n\\nconst findBlockById = (\\n blocks: Block[],\\n id: string,\\n): Block | undefined => {\\n for (const block of blocks) {\\n if (block.id === id) return block;\\n if (block.children && block.children.length > 0) {\\n const child = findBlockById(block.children as Block[], id);\\n if (child) return child;\\n }\\n }\\n return undefined;\\n};\\n\\nconst syncProgressMeters = (editorInstance: ReturnType) => {\\n if (!editorInstance) {\\n return;\\n }\\n const blocks = editorInstance.topLevelBlocks as Block[];\\n const progressStats = new Map<\\n string,\\n { done: number; doing: number; total: number }\\n >();\\n let activeProgressId: string | null = null;\\n\\n const traverse = (targetBlocks: Block[]) => {\\n targetBlocks.forEach((block) => {\\n if (block.type === \\\"progressMeter\\\" && block.props.auto) {\\n activeProgressId = block.id;\\n progressStats.set(block.id, { done: 0, doing: 0, total: 0 });\\n } else if (block.type === \\\"progressMeter\\\" && !block.props.auto) {\\n activeProgressId = null;\\n } else if (block.type === \\\"heading\\\") {\\n activeProgressId = null;\\n } else if (block.type === \\\"advancedTodo\\\" && activeProgressId) {\\n const currentStat = progressStats.get(activeProgressId);\\n if (!currentStat) return;\\n if (block.props.status === \\\"cancelled\\\") {\\n return;\\n }\\n currentStat.total += 1;\\n if (block.props.status === \\\"done\\\") {\\n currentStat.done += 1;\\n } else if (block.props.status === \\\"doing\\\") {\\n currentStat.doing += 1;\\n }\\n }\\n\\n if (block.children && block.children.length > 0) {\\n traverse(block.children as Block[]);\\n }\\n });\\n };\\n\\n traverse(blocks);\\n\\n progressStats.forEach((stat, progressId) => {\\n const block = findBlockById(blocks, progressId);\\n if (!block) return;\\n if (block.type !== \\\"progressMeter\\\") return;\\n const weightedDone = stat.done + stat.doing * 0.5;\\n const percent = stat.total === 0 ? 0 : Math.min(100, Math.round((weightedDone / stat.total) * 100));\\n const summary = stat.total === 0 ? \\\"暂无条目\\\" : `${stat.done}/${stat.total} 完成`;\\n\\n if (block.props.percent !== percent || block.props.summary !== summary) {\\n editorInstance.updateBlock(block, {\\n props: {\\n percent,\\n summary,\\n },\\n });\\n }\\n });\\n};\\n\\nexport function BlockNoteEditor({\\n documentId,\\n workspaceId,\\n initialContent,\\n initialRevision = null,\\n initialConflictDetectionKey = null,\\n pageOptions,\\n readOnly = false,\\n onStatsChange,\\n onSnapshot,\\n onCloseToc,\\n onPersistedMetaChange,\\n}: BlockNoteEditorProps) {\\n const [isSaving, setIsSaving] = useState(false);\\n const [saveError, setSaveError] = useState(null);\\n const [tocEntries, setTocEntries] = useState([]);\\n const [fullScreenTableId, setFullScreenTableId] = useState(null);\\n const isFullScreenTableOpen = fullScreenTableId !== null;\\n const revisionRef = useRef(initialRevision);\\n const conflictDetectionKeyRef = useRef(initialConflictDetectionKey);\\n const onStatsChangeRef = useRef(onStatsChange);\\n const onSnapshotRef = useRef(onSnapshot);\\n const onPersistedMetaChangeRef = useRef(onPersistedMetaChange);\\n const stableInitialContentRef = useRef<{ documentId: string; content: Json | undefined } | null>(null);\\n \\n const openReferencePalette = useSearchPaletteStore((state) => state.openReference);\\n const registerEditorBridge = useEditorBridgeStore((state) => state.registerBridge);\\n const spellCheck = useAppPreferencesStore((s) => s.spellCheck);\\n const showStructure = useAppPreferencesStore((s) => s.showStructure);\\n const openCommentsForBlock = useCommentsUiStore((s) => s.openForBlock);\\n const openCommentsForPage = useCommentsUiStore((s) => s.openForPage);\\n const { isAuthenticated } = useConvexAuth();\\n const threads = useQuery(\\n api.comments.listThreadsByDocument,\\n isAuthenticated && documentId ? { documentId, includeResolved: false } : \\\"skip\\\",\\n );\\n\\n const unresolvedCommentCountByBlockId = useMemo(() => {\\n const map: Record = {};\\n if (!Array.isArray(threads)) return map;\\n for (const t of threads as any[]) {\\n const bid = String(t?.blockId ?? \\\"\\\");\\n if (!bid) continue;\\n map[bid] = (map[bid] ?? 0) + 1;\\n }\\n return map;\\n }, [threads]);\\n\\n const normalizedInitialContent = useMemo(\\n () => extractInitialBlocks(initialContent),\\n [initialContent],\\n );\\n\\n if (\\n !stableInitialContentRef.current ||\\n stableInitialContentRef.current.documentId !== documentId\\n ) {\",\n \"numLines\": 220,\n \"startLine\": 1,\n \"totalLines\": 1399\n }\n}","tool_name":"read_file","tool_use_id":"call_cfdVcoG8kp0OAfj9Nr4We9Rm","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_w8u5GLEOeMJGqzjFOtYilR6O","input":"{\"limit\":240,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host.tsx\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":62919,"output_tokens":44}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host.tsx\",\n \"content\": \"\\\"use client\\\";\\n\\nimport dynamic from \\\"next/dynamic\\\";\\nimport type { ComponentType } from \\\"react\\\";\\nimport type { DocumentEditorHostProps } from \\\"@/components/editor/editor-host-types\\\";\\n\\nconst LeptosTiptapEditor = dynamic(\\n () => import(\\\"@/components/editor/leptos-tiptap-editor-host\\\").then((mod) => mod.LeptosTiptapEditorHost),\\n {\\n ssr: false,\\n loading: () => (\\n
编辑器加载中...
\\n ),\\n },\\n) as ComponentType;\\n\\nexport function EditorHost(props: DocumentEditorHostProps) {\\n return ;\\n}\",\n \"numLines\": 19,\n \"startLine\": 1,\n \"totalLines\": 19\n }\n}","tool_name":"read_file","tool_use_id":"call_w8u5GLEOeMJGqzjFOtYilR6O","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_iBVFWn8lqTsI0sf9195ZPJ3O","input":"{\"limit\":320,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":63237,"output_tokens":50}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx\",\n \"content\": \"\\\"use client\\\";\\n\\nimport { useEffect, useMemo, useRef, useState } from \\\"react\\\";\\nimport type {\\n DocumentEditorHostProps,\\n LeptosTiptapHostBridgeState,\\n} from \\\"@/components/editor/editor-host-types\\\";\\nimport { blocksFromTiptapDoc, tiptapDocFromBlocks } from \\\"@/lib/documents/tiptap-content-converter\\\";\\nimport { useEditorBridgeStore } from \\\"@/store/editor-bridge\\\";\\nimport { getMnoteRuntimeConfig } from \\\"@/lib/runtime-config\\\";\\nimport type { EditorReferenceBridge } from \\\"@/store/editor-bridge\\\";\\nimport type { DocumentStats } from \\\"@/types/page-options\\\";\\nimport type { Json } from \\\"@/types/supabase\\\";\\n\\nconst RUNTIME_PORT = 8123;\\nconst RUNTIME_NAME = \\\"8123-leptos-tiptap-runtime\\\";\\nconst RUNTIME_VERSION = \\\"1.0.0\\\";\\nconst EVENT_PREFIX = \\\"mnote:leptos-tiptap-spike\\\";\\nconst READY_EVENT = `${EVENT_PREFIX}:ready`;\\nconst CHANGE_EVENT = `${EVENT_PREFIX}:change`;\\nconst STATE_EVENT = `${EVENT_PREFIX}:state`;\\nconst SAVE_REQUEST_EVENT = `${EVENT_PREFIX}:save-request`;\\nconst PROTOCOL = \\\"mnote.leptos_tiptap.bridge.v1\\\";\\nconst EMPTY_DOC = { type: \\\"doc\\\", content: [{ type: \\\"paragraph\\\" }] } as const;\\n\\ntype HostEnvelope = {\\n protocol?: string;\\n runtime?: string;\\n version?: string;\\n source?: string;\\n event?: string;\\n payload?: T;\\n};\\n\\ntype HostDocumentPayload = {\\n title?: string;\\n content?: unknown;\\n meta?: {\\n dirty_count?: number;\\n editor_focused?: boolean;\\n slash_open?: boolean;\\n toolbar_open?: boolean;\\n revision?: number | null;\\n conflict_detection_key?: string | null;\\n };\\n};\\n\\nfunction resolveRuntimeUrl() {\\n if (typeof window === \\\"undefined\\\") {\\n return `http://localhost:${RUNTIME_PORT}/`;\\n }\\n const cfg = getMnoteRuntimeConfig();\\n if (cfg.mnoteWebBaseUrl) {\\n try {\\n const url = new URL(cfg.mnoteWebBaseUrl);\\n url.port = String(RUNTIME_PORT);\\n url.pathname = \\\"/\\\";\\n url.search = \\\"\\\";\\n url.hash = \\\"\\\";\\n return url.toString();\\n } catch {\\n // ignore\\n }\\n }\\n try {\\n const url = new URL(window.location.href);\\n url.port = String(RUNTIME_PORT);\\n url.pathname = \\\"/\\\";\\n url.search = \\\"\\\";\\n url.hash = \\\"\\\";\\n return url.toString();\\n } catch {\\n return `http://localhost:${RUNTIME_PORT}/`;\\n }\\n}\\n\\nfunction normalizeRuntimeDoc(tiptapDoc: Json) {\\n if (!tiptapDoc || typeof tiptapDoc !== \\\"object\\\") {\\n return EMPTY_DOC;\\n }\\n const doc = tiptapDoc as { type?: unknown; content?: unknown };\\n if (doc.type !== \\\"doc\\\") {\\n return EMPTY_DOC;\\n }\\n if (!Array.isArray(doc.content) || doc.content.length === 0) {\\n return EMPTY_DOC;\\n }\\n return tiptapDoc;\\n}\\n\\nfunction buildIframeSrc(runtimeUrl: string, props: DocumentEditorHostProps, tiptapDoc: Json, reloadKey: number) {\\n const url = new URL(runtimeUrl);\\n url.searchParams.set(\\\"embedded\\\", \\\"1\\\");\\n url.searchParams.set(\\\"host\\\", \\\"leptos_tiptap\\\");\\n url.searchParams.set(\\\"documentId\\\", props.documentId);\\n url.searchParams.set(\\\"workspaceId\\\", props.workspaceId);\\n url.searchParams.set(\\\"reloadKey\\\", String(reloadKey));\\n url.searchParams.set(\\\"content\\\", JSON.stringify(normalizeRuntimeDoc(tiptapDoc)));\\n if (props.initialRevision != null) {\\n url.searchParams.set(\\\"revision\\\", String(props.initialRevision));\\n }\\n if (props.initialConflictDetectionKey) {\\n url.searchParams.set(\\\"conflictDetectionKey\\\", props.initialConflictDetectionKey);\\n }\\n return url.toString();\\n}\\n\\nfunction isBridgeEnvelope(value: unknown): value is HostEnvelope {\\n if (!value || typeof value !== \\\"object\\\") {\\n return false;\\n }\\n const maybe = value as HostEnvelope;\\n return maybe.protocol === PROTOCOL && maybe.runtime === RUNTIME_NAME && maybe.source === EVENT_PREFIX;\\n}\\n\\nfunction flattenText(value: unknown): string {\\n if (typeof value === \\\"string\\\") return value;\\n if (Array.isArray(value)) return value.map(flattenText).join(\\\"\\\");\\n if (value && typeof value === \\\"object\\\") {\\n const obj = value as { text?: unknown; content?: unknown };\\n return `${flattenText(obj.text)}${flattenText(obj.content)}`;\\n }\\n return \\\"\\\";\\n}\\n\\nfunction buildStats(blocks: Json): DocumentStats {\\n const items = Array.isArray(blocks) ? blocks : [];\\n const text = items\\n .map((item) => (item && typeof item === \\\"object\\\" ? flattenText((item as { content?: unknown }).content) : \\\"\\\"))\\n .join(\\\"\\\\n\\\")\\n .trim();\\n const todoItems = items.filter((item) => item && typeof item === \\\"object\\\" && (item as { type?: unknown }).type === \\\"todo\\\");\\n const todoDone = todoItems.filter(\\n (item) => item && typeof item === \\\"object\\\" && Boolean((item as { props?: { checked?: unknown } }).props?.checked),\\n ).length;\\n return {\\n wordCount: text ? text.split(/\\\\s+/).filter(Boolean).length : 0,\\n characterCount: text.length,\\n blockCount: items.length,\\n todoTotal: todoItems.length,\\n todoDone,\\n };\\n}\\n\\nfunction toIsoNow() {\\n return new Date().toISOString();\\n}\\n\\nexport function LeptosTiptapEditorHost(props: DocumentEditorHostProps) {\\n const iframeRef = useRef(null);\\n const runtimeUrl = useMemo(() => resolveRuntimeUrl(), []);\\n const registerBridge = useEditorBridgeStore((state) => state.registerBridge);\\n const onSnapshotRef = useRef(props.onSnapshot);\\n const onStatsChangeRef = useRef(props.onStatsChange);\\n const onPersistedMetaChangeRef = useRef(props.onPersistedMetaChange);\\n const revisionRef = useRef(props.initialRevision ?? null);\\n const conflictDetectionKeyRef = useRef(props.initialConflictDetectionKey ?? null);\\n const [runtimeDoc, setRuntimeDoc] = useState(() => {\\n const initialDoc = tiptapDocFromBlocks(props.initialContent as Json) as Json;\\n return initialDoc;\\n });\\n\\n const [reloadKey, setReloadKey] = useState(0);\\n const [bridgeState, setBridgeState] = useState({\\n ready: false,\\n runtimeName: RUNTIME_NAME,\\n runtimeVersion: RUNTIME_VERSION,\\n runtimeUrl,\\n documentId: props.documentId,\\n workspaceId: props.workspaceId,\\n status: \\\"booting\\\",\\n lastChangeAt: null,\\n lastSaveRequestAt: null,\\n lastError: null,\\n });\\n\\n const iframeSrc = useMemo(\\n () => buildIframeSrc(runtimeUrl, props, runtimeDoc, reloadKey),\\n [props, reloadKey, runtimeDoc, runtimeUrl],\\n );\\n\\n useEffect(() => {\\n onSnapshotRef.current = props.onSnapshot;\\n onStatsChangeRef.current = props.onStatsChange;\\n onPersistedMetaChangeRef.current = props.onPersistedMetaChange;\\n revisionRef.current = props.initialRevision ?? null;\\n conflictDetectionKeyRef.current = props.initialConflictDetectionKey ?? null;\\n }, [props.initialConflictDetectionKey, props.initialRevision, props.onPersistedMetaChange, props.onSnapshot, props.onStatsChange]);\\n\\n useEffect(() => {\\n setRuntimeDoc(tiptapDocFromBlocks(props.initialContent as Json) as Json);\\n setReloadKey((value) => value + 1);\\n setBridgeState((prev) => ({\\n ...prev,\\n ready: false,\\n documentId: props.documentId,\\n workspaceId: props.workspaceId,\\n status: \\\"booting\\\",\\n lastError: null,\\n }));\\n }, [props.documentId, props.initialContent, props.workspaceId]);\\n\\n useEffect(() => {\\n const bridge: EditorReferenceBridge = {\\n insertInlineReference: () => ({ blockId: null }),\\n insertEmbedReference: () => ({ blockId: null }),\\n undo: () => {\\n // 现阶段 iframe runtime 还没有父->子撤销命令,先保持最小 bridge 兼容。\\n },\\n redo: () => {\\n // 现阶段 iframe runtime 还没有父->子重做命令,先保持最小 bridge 兼容。\\n },\\n getCursorBlockId: () => null,\\n replaceWithSnapshot: (blocks: Json) => {\\n setRuntimeDoc(tiptapDocFromBlocks(blocks) as Json);\\n setReloadKey((value) => value + 1);\\n setBridgeState((prev) => ({\\n ...prev,\\n ready: false,\\n status: \\\"reloading\\\",\\n lastError: null,\\n }));\\n },\\n };\\n registerBridge(bridge);\\n return () => registerBridge(null);\\n }, [registerBridge]);\\n\\n useEffect(() => {\\n let disposed = false;\\n const expectedOrigin = (() => {\\n try {\\n return new URL(runtimeUrl).origin;\\n } catch {\\n return \\\"\\\";\\n }\\n })();\\n\\n const publishSnapshot = (payload: HostDocumentPayload) => {\\n const blocks = blocksFromTiptapDoc(payload.content) as Json;\\n const stats = buildStats(blocks);\\n onSnapshotRef.current?.({ blocks, stats });\\n onStatsChangeRef.current?.(stats);\\n return { blocks, stats };\\n };\\n\\n const saveSnapshot = async (payload: HostDocumentPayload) => {\\n const { blocks } = publishSnapshot(payload);\\n const response = await fetch(\\\"/api/documents/save\\\", {\\n method: \\\"POST\\\",\\n headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\\n body: JSON.stringify({\\n documentId: props.documentId,\\n workspaceId: props.workspaceId,\\n revision: payload.meta?.revision ?? revisionRef.current,\\n conflictDetectionKey: payload.meta?.conflict_detection_key ?? conflictDetectionKeyRef.current,\\n snapshotCapturedAt: toIsoNow(),\\n blockCount: Array.isArray(blocks) ? blocks.length : 0,\\n content: blocks,\\n }),\\n });\\n const nextMeta = await response.json().catch(() => null);\\n if (!response.ok) {\\n const errorMessage = typeof nextMeta?.error === \\\"string\\\" ? nextMeta.error : `保存失败(${response.status})`;\\n throw new Error(errorMessage);\\n }\\n const nextRevision = typeof nextMeta?.revision === \\\"number\\\" ? nextMeta.revision : payload.meta?.revision ?? null;\\n const nextConflictDetectionKey =\\n typeof nextMeta?.conflictDetectionKey === \\\"string\\\"\\n ? nextMeta.conflictDetectionKey\\n : payload.meta?.conflict_detection_key ?? null;\\n revisionRef.current = nextRevision;\\n conflictDetectionKeyRef.current = nextConflictDetectionKey;\\n onPersistedMetaChangeRef.current?.({\\n revision: nextRevision,\\n conflictDetectionKey: nextConflictDetectionKey,\\n });\\n };\\n\\n const handleMessage = (event: MessageEvent) => {\\n if (!expectedOrigin || event.origin !== expectedOrigin) {\\n return;\\n }\\n if (event.source !== iframeRef.current?.contentWindow) {\\n return;\\n }\\n if (!isBridgeEnvelope(event.data)) {\\n return;\\n }\\n const eventName = typeof event.data.event === \\\"string\\\" ? event.data.event : \\\"\\\";\\n const payload = (event.data.payload ?? {}) as HostDocumentPayload;\\n if (eventName === READY_EVENT) {\\n setBridgeState((prev) => ({ ...prev, ready: true, status: \\\"ready\\\", lastError: null }));\\n return;\\n }\\n if (eventName === STATE_EVENT) {\\n setBridgeState((prev) => ({ ...prev, ready: true, status: \\\"editing\\\", lastError: null }));\\n return;\\n }\\n if (eventName === CHANGE_EVENT) {\\n publishSnapshot(payload);\\n setBridgeState((prev) => ({ ...prev, ready: true, status: \\\"dirty\\\", lastChangeAt: toIsoNow(), lastError: null }));\\n return;\\n }\\n if (eventName === SAVE_REQUEST_EVENT) {\\n setBridgeState((prev) => ({\\n ...prev,\\n ready: true,\\n status: \\\"saving\\\",\\n lastSaveRequestAt: toIsoNow(),\\n lastError: null,\\n }));\\n void saveSnapshot(payload)\\n .then(() => {\\n if (disposed) return;\\n setBridgeState((prev) => ({ ...prev, ready: true, status: \\\"saved\\\", lastError: null }));\\n })\\n .catch((error) => {\\n if (disposed) return;\\n setBridgeState((prev) => ({\",\n \"numLines\": 320,\n \"startLine\": 1,\n \"totalLines\": 349\n }\n}","tool_name":"read_file","tool_use_id":"call_iBVFWn8lqTsI0sf9195ZPJ3O","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_mYKdUDYPeClTklmCb42rsXxU","input":"{\"limit\":120,\"offset\":320,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":66592,"output_tokens":50}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx\",\n \"content\": \" ...prev,\\n ready: true,\\n status: \\\"error\\\",\\n lastError: error instanceof Error ? error.message : \\\"保存失败\\\",\\n }));\\n });\\n }\\n };\\n\\n window.addEventListener(\\\"message\\\", handleMessage);\\n return () => {\\n disposed = true;\\n window.removeEventListener(\\\"message\\\", handleMessage);\\n };\\n }, [props.documentId, props.workspaceId, runtimeUrl]);\\n\\n return (\\n \\n );\\n}\",\n \"numLines\": 29,\n \"startLine\": 321,\n \"totalLines\": 349\n }\n}","tool_name":"read_file","tool_use_id":"call_mYKdUDYPeClTklmCb42rsXxU","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_D7v4uwi6xsyOAHVHxlYjCECW","input":"{\"limit\":260,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.ts\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":66938,"output_tokens":48}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.ts\",\n \"content\": \"export type TiptapNode = {\\n type: string;\\n attrs?: Record;\\n content?: TiptapNode[];\\n text?: string;\\n marks?: Array<{ type: string; attrs?: Record }>;\\n};\\n\\nexport type TiptapDoc = {\\n type: \\\"doc\\\";\\n content: TiptapNode[];\\n};\\n\\ntype Json = unknown;\\n\\ntype BlockLike = {\\n id?: string;\\n type?: string;\\n content?: unknown;\\n props?: Record;\\n children?: unknown;\\n};\\n\\nconst emptyDoc: TiptapDoc = { type: \\\"doc\\\", content: [] };\\n\\nconst toText = (value: unknown): string => {\\n if (typeof value === \\\"string\\\") return value;\\n if (Array.isArray(value)) return value.map(toText).join(\\\"\\\");\\n if (value && typeof value === \\\"object\\\") {\\n const obj = value as { text?: unknown; content?: unknown; value?: unknown };\\n return `${toText(obj.text)}${toText(obj.value)}${toText(obj.content)}`;\\n }\\n return \\\"\\\";\\n};\\n\\nconst toTiptapInline = (value: unknown): TiptapNode[] => {\\n const text = toText(value);\\n return text ? [{ type: \\\"text\\\", text }] : [];\\n};\\n\\nconst toHeadingLevel = (value: unknown): number => {\\n const level = Number(value);\\n return Number.isInteger(level) && level >= 1 && level <= 6 ? level : 1;\\n};\\n\\nconst blockToNode = (block: BlockLike): TiptapNode | null => {\\n const type = String(block.type ?? \\\"\\\").toLowerCase();\\n const content = block.content ?? block.props?.content ?? block.props?.text ?? block.props?.title;\\n if (type === \\\"paragraph\\\" || type === \\\"text\\\") return { type: \\\"paragraph\\\", content: toTiptapInline(content) };\\n if (type === \\\"heading\\\") return { type: \\\"heading\\\", attrs: { level: toHeadingLevel(block.props?.level) }, content: toTiptapInline(content) };\\n if (type === \\\"bullet_list\\\" || type === \\\"bullet-list\\\") return { type: \\\"bulletList\\\", content: [] };\\n if (type === \\\"ordered_list\\\" || type === \\\"ordered-list\\\") return { type: \\\"orderedList\\\", content: [] };\\n if (type === \\\"todo\\\" || type === \\\"task\\\") return { type: \\\"taskItem\\\", attrs: { checked: Boolean(block.props?.checked) }, content: toTiptapInline(content) };\\n if (type === \\\"quote\\\" || type === \\\"blockquote\\\") return { type: \\\"blockquote\\\", content: [{ type: \\\"paragraph\\\", content: toTiptapInline(content) }] };\\n if (type === \\\"code\\\" || type === \\\"code_block\\\" || type === \\\"code-block\\\") return { type: \\\"codeBlock\\\", content: toTiptapInline(content) };\\n return null;\\n};\\n\\nexport function blocksToTiptapDoc(blocks: Json): TiptapDoc {\\n if (!Array.isArray(blocks) || blocks.length === 0) return { ...emptyDoc };\\n const content = blocks.flatMap((item) => {\\n if (!item || typeof item !== \\\"object\\\") return [];\\n const node = blockToNode(item as BlockLike);\\n return node ? [node] : [];\\n });\\n return { type: \\\"doc\\\", content: content.length > 0 ? content : [{ type: \\\"paragraph\\\" }] };\\n}\\n\\nconst nodeToBlock = (node: TiptapNode, index: number): Record | null => {\\n if (node.type === \\\"paragraph\\\") return { id: `block-${index}`, type: \\\"paragraph\\\", content: toText(node.content) };\\n if (node.type === \\\"heading\\\") return { id: `block-${index}`, type: \\\"heading\\\", props: { level: Number(node.attrs?.level ?? 1) }, content: toText(node.content) };\\n if (node.type === \\\"bulletList\\\") return { id: `block-${index}`, type: \\\"bullet_list\\\" };\\n if (node.type === \\\"orderedList\\\") return { id: `block-${index}`, type: \\\"ordered_list\\\" };\\n if (node.type === \\\"taskItem\\\") return { id: `block-${index}`, type: \\\"todo\\\", props: { checked: Boolean(node.attrs?.checked) }, content: toText(node.content) };\\n if (node.type === \\\"blockquote\\\") return { id: `block-${index}`, type: \\\"quote\\\", content: toText(node.content) };\\n if (node.type === \\\"codeBlock\\\") return { id: `block-${index}`, type: \\\"code_block\\\", content: toText(node.content) };\\n return null;\\n};\\n\\nexport function tiptapDocToBlocks(doc: unknown): Json {\\n if (!doc || typeof doc !== \\\"object\\\") return [];\\n const root = doc as { type?: string; content?: TiptapNode[] };\\n if (root.type !== \\\"doc\\\" || !Array.isArray(root.content) || root.content.length === 0) return [];\\n return root.content.flatMap((node, index) => {\\n if (!node || typeof node !== \\\"object\\\") return [];\\n const block = nodeToBlock(node, index);\\n return block ? [block] : [];\\n }) as Json;\\n}\\n\\nexport const tiptapDocFromBlocks = blocksToTiptapDoc;\\nexport const blocksFromTiptapDoc = tiptapDocToBlocks;\",\n \"numLines\": 92,\n \"startLine\": 1,\n \"totalLines\": 92\n }\n}","tool_name":"read_file","tool_use_id":"call_D7v4uwi6xsyOAHVHxlYjCECW","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_DMxMRA6oZW6jTIwSlcX4pHOc","input":"{\"limit\":260,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":68377,"output_tokens":45}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs\",\n \"content\": \"use crate::editor::model::{\\n BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument, EditorBlockType,\\n};\\nuse serde::{Deserialize, Serialize};\\nuse std::collections::BTreeMap;\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"snake_case\\\")]\\npub enum TiptapBlockType {\\n Paragraph,\\n Heading,\\n BulletList,\\n OrderedList,\\n TaskList,\\n ListItem,\\n TaskItem,\\n Blockquote,\\n CodeBlock,\\n Text,\\n Doc,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapHeadingAttrs {\\n pub level: u8,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapTaskItemAttrs {\\n #[serde(default)]\\n pub checked: bool,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapCodeBlockAttrs {\\n pub language: Option,\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(tag = \\\"type\\\", rename_all = \\\"snake_case\\\")]\\npub enum TiptapNode {\\n Doc { content: Vec },\\n Paragraph { #[serde(default)] attrs: BTreeMap, #[serde(default)] content: Vec },\\n Heading { attrs: TiptapHeadingAttrs, #[serde(default)] content: Vec },\\n BulletList { #[serde(default)] content: Vec },\\n OrderedList { #[serde(default)] content: Vec, #[serde(default)] attrs: BTreeMap },\\n TaskList { #[serde(default)] content: Vec },\\n ListItem { #[serde(default)] content: Vec },\\n TaskItem { attrs: TiptapTaskItemAttrs, #[serde(default)] content: Vec },\\n Blockquote { #[serde(default)] content: Vec },\\n CodeBlock { attrs: TiptapCodeBlockAttrs, #[serde(default)] content: Vec },\\n Text { text: String },\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq)]\\npub enum EditorBlockDocumentTiptapError {\\n UnsupportedNode(&'static str),\\n InvalidDocument(&'static str),\\n}\\n\\npub struct EditorBlockDocumentTiptapBridge;\\n\\nimpl EditorBlockDocumentTiptapBridge {\\n pub fn to_tiptap_doc(document: &EditorBlockDocument) -> Result {\\n let mut content = Vec::new();\\n for block_id in &document.root_block_ids {\\n let block = document.blocks.iter().find(|block| &block.block_id == block_id).ok_or(EditorBlockDocumentTiptapError::InvalidDocument(\\\"根块不存在\\\"))?;\\n content.push(block_to_tiptap_node(block)?);\\n }\\n Ok(TiptapNode::Doc { content })\\n }\\n\\n pub fn from_tiptap_doc(document_id: impl Into, doc: &TiptapNode) -> Result {\\n match doc {\\n TiptapNode::Doc { content } => {\\n let mut blocks = Vec::new();\\n let mut root_block_ids = Vec::new();\\n for (index, node) in content.iter().enumerate() {\\n let block = node_to_block(node, index)?;\\n root_block_ids.push(block.block_id.clone());\\n blocks.push(block);\\n }\\n Ok(EditorBlockDocument { document_id: document_id.into(), root_block_ids, blocks })\\n }\\n _ => Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"顶层必须是 doc\\\")),\\n }\\n }\\n}\\n\\nfn block_to_tiptap_node(block: &EditorBlock) -> Result {\\n let content = text_nodes_to_tiptap(&block.content_nodes)?;\\n match block.block_type {\\n EditorBlockType::Paragraph => Ok(TiptapNode::Paragraph { attrs: BTreeMap::new(), content }),\\n EditorBlockType::Heading => Ok(TiptapNode::Heading { attrs: TiptapHeadingAttrs { level: block.props.heading_level.unwrap_or(1) }, content }),\\n EditorBlockType::BulletListItem => Ok(TiptapNode::BulletList { content: vec![TiptapNode::ListItem { content }] }),\\n EditorBlockType::NumberedListItem => Ok(TiptapNode::OrderedList { attrs: BTreeMap::new(), content: vec![TiptapNode::ListItem { content }] }),\\n EditorBlockType::Todo => Ok(TiptapNode::TaskList { content: vec![TiptapNode::TaskItem { attrs: TiptapTaskItemAttrs { checked: block.props.checked.unwrap_or(false) }, content }] }),\\n EditorBlockType::Quote => Ok(TiptapNode::Blockquote { content }),\\n EditorBlockType::CodeBlock => Ok(TiptapNode::CodeBlock { attrs: TiptapCodeBlockAttrs { language: block.props.language.clone(), block_id: Some(block.block_id.clone()) }, content }),\\n _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\\"当前块型暂不支持导出到 Tiptap\\\")),\\n }\\n}\\n\\nfn node_to_block(node: &TiptapNode, index: usize) -> Result {\\n let block_id = format!(\\\"block_{}\\\", index + 1);\\n match node {\\n TiptapNode::Paragraph { content, .. } => Ok(EditorBlock { block_id, block_type: EditorBlockType::Paragraph, props: BlockProps::default(), content_nodes: text_nodes_from_tiptap(content)?, child_block_ids: vec![] }),\\n TiptapNode::Heading { attrs, content } => Ok(EditorBlock { block_id, block_type: EditorBlockType::Heading, props: BlockProps { heading_level: Some(attrs.level), ..BlockProps::default() }, content_nodes: text_nodes_from_tiptap(content)?, child_block_ids: vec![] }),\\n TiptapNode::BulletList { content } => list_node_to_block(block_id, EditorBlockType::BulletListItem, content),\\n TiptapNode::OrderedList { content, .. } => list_node_to_block(block_id, EditorBlockType::NumberedListItem, content),\\n TiptapNode::TaskList { content } => list_task_node_to_block(block_id, content),\\n TiptapNode::Blockquote { content } => Ok(EditorBlock { block_id, block_type: EditorBlockType::Quote, props: BlockProps::default(), content_nodes: text_nodes_from_tiptap(content)?, child_block_ids: vec![] }),\\n TiptapNode::CodeBlock { attrs, content } => Ok(EditorBlock { block_id, block_type: EditorBlockType::CodeBlock, props: BlockProps { language: attrs.language.clone(), ..BlockProps::default() }, content_nodes: text_nodes_from_tiptap(content)?, child_block_ids: vec![] }),\\n _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\\"当前节点暂不支持导入为 EditorBlockDocument\\\")),\\n }\\n}\\n\\nfn list_node_to_block(block_id: String, block_type: EditorBlockType, content: &[TiptapNode]) -> Result {\\n let Some(first) = content.first() else { return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"列表节点缺少 list_item/task_item\\\")); };\\n let item_content = match first { TiptapNode::ListItem { content } | TiptapNode::TaskItem { content, .. } => content, _ => return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"列表节点首子节点类型不正确\\\")), };\\n Ok(EditorBlock { block_id, block_type, props: BlockProps::default(), content_nodes: text_nodes_from_tiptap(item_content)?, child_block_ids: vec![] })\\n}\\n\\nfn list_task_node_to_block(block_id: String, content: &[TiptapNode]) -> Result {\\n let Some(first) = content.first() else { return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"任务列表节点缺少 task_item\\\")); };\\n let (checked, item_content) = match first { TiptapNode::TaskItem { attrs, content } => (Some(attrs.checked), content), _ => return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"任务列表节点首子节点类型不正确\\\")), };\\n Ok(EditorBlock { block_id, block_type: EditorBlockType::Todo, props: BlockProps { checked, ..BlockProps::default() }, content_nodes: text_nodes_from_tiptap(item_content)?, child_block_ids: vec![] })\\n}\\n\\nfn text_nodes_to_tiptap(nodes: &[ContentNode]) -> Result, EditorBlockDocumentTiptapError> {\\n let mut out = Vec::new();\\n for node in nodes {\\n match &node.payload {\\n ContentNodePayload::Text { text, .. } => out.push(TiptapNode::Text { text: text.clone() }),\\n ContentNodePayload::HardBreak => out.push(TiptapNode::Text { text: \\\"\\\\n\\\".into() }),\\n ContentNodePayload::ReferenceToken { .. } => return Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\\"引用 token 暂不映射到 Tiptap 文本合同\\\")),\\n }\\n }\\n Ok(out)\\n}\\n\\nfn text_nodes_from_tiptap(nodes: &[TiptapNode]) -> Result, EditorBlockDocumentTiptapError> {\\n let mut out = Vec::new();\\n for node in nodes {\\n match node {\\n TiptapNode::Text { text } => out.push(ContentNode { payload: ContentNodePayload::Text { text: text.clone(), marks: vec![] }, attrs: BTreeMap::new() }),\\n _ => return Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\\"当前只支持文本节点内容\\\")),\\n }\\n }\\n Ok(out)\\n}\",\n \"numLines\": 155,\n \"startLine\": 1,\n \"totalLines\": 155\n }\n}","tool_name":"read_file","tool_use_id":"call_DMxMRA6oZW6jTIwSlcX4pHOc","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_uZ2hM13DLKIyY5MDkHqMgwMa","input":"{\"limit\":260,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":70969,"output_tokens":46}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs\",\n \"content\": \"use core_protocol::editor::{\\n BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument,\\n EditorBlockDocumentTiptapBridge, EditorBlockType, TiptapCodeBlockAttrs, TiptapHeadingAttrs,\\n TiptapNode, TiptapTaskItemAttrs,\\n};\\nuse std::collections::BTreeMap;\\n\\n#[test]\\nfn p05_blocks_round_trip_through_tiptap_doc() {\\n let document = EditorBlockDocument {\\n document_id: \\\"doc_1\\\".into(),\\n root_block_ids: vec![\\\"p1\\\".into(), \\\"h1\\\".into(), \\\"b1\\\".into(), \\\"n1\\\".into(), \\\"t1\\\".into(), \\\"q1\\\".into(), \\\"c1\\\".into()],\\n blocks: vec![\\n EditorBlock { block_id: \\\"p1\\\".into(), block_type: EditorBlockType::Paragraph, props: BlockProps::default(), content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"段落\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\\n EditorBlock { block_id: \\\"h1\\\".into(), block_type: EditorBlockType::Heading, props: BlockProps { heading_level: Some(2), ..BlockProps::default() }, content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"标题\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\\n EditorBlock { block_id: \\\"b1\\\".into(), block_type: EditorBlockType::BulletListItem, props: BlockProps::default(), content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"项目\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\\n EditorBlock { block_id: \\\"n1\\\".into(), block_type: EditorBlockType::NumberedListItem, props: BlockProps::default(), content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"序号\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\\n EditorBlock { block_id: \\\"t1\\\".into(), block_type: EditorBlockType::Todo, props: BlockProps { checked: Some(true), ..BlockProps::default() }, content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"待办\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\\n EditorBlock { block_id: \\\"q1\\\".into(), block_type: EditorBlockType::Quote, props: BlockProps::default(), content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"引用\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\\n EditorBlock { block_id: \\\"c1\\\".into(), block_type: EditorBlockType::CodeBlock, props: BlockProps { language: Some(\\\"rust\\\".into()), ..BlockProps::default() }, content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"fn main() {}\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\\n ],\\n };\\n\\n let doc = EditorBlockDocumentTiptapBridge::to_tiptap_doc(&document).expect(\\\"export should work\\\");\\n let round_trip = EditorBlockDocumentTiptapBridge::from_tiptap_doc(\\\"doc_2\\\", &doc).expect(\\\"import should work\\\");\\n\\n assert_eq!(round_trip.blocks.len(), 7);\\n assert_eq!(round_trip.root_block_ids.len(), 7);\\n assert_eq!(round_trip.blocks[0].block_type, EditorBlockType::Paragraph);\\n assert_eq!(round_trip.blocks[1].props.heading_level, Some(2));\\n assert_eq!(round_trip.blocks[4].props.checked, Some(true));\\n assert_eq!(round_trip.blocks[6].props.language.as_deref(), Some(\\\"rust\\\"));\\n}\\n\\n#[test]\\nfn tiptap_list_and_code_block_attrs_preserve_contract_edges() {\\n let doc = TiptapNode::Doc {\\n content: vec![\\n TiptapNode::Heading { attrs: TiptapHeadingAttrs { level: 3 }, content: vec![TiptapNode::Text { text: \\\"H\\\".into() }] },\\n TiptapNode::TaskList { content: vec![TiptapNode::TaskItem { attrs: TiptapTaskItemAttrs { checked: true }, content: vec![TiptapNode::Text { text: \\\"todo\\\".into() }] }] },\\n TiptapNode::CodeBlock { attrs: TiptapCodeBlockAttrs { language: Some(\\\"ts\\\".into()), block_id: Some(\\\"c1\\\".into()) }, content: vec![TiptapNode::Text { text: \\\"let x = 1;\\\".into() }] },\\n ],\\n };\\n let imported = EditorBlockDocumentTiptapBridge::from_tiptap_doc(\\\"doc_3\\\", &doc).unwrap();\\n assert_eq!(imported.blocks[0].props.heading_level, Some(3));\\n assert_eq!(imported.blocks[1].props.checked, Some(true));\\n assert_eq!(imported.blocks[2].props.language.as_deref(), Some(\\\"ts\\\"));\\n assert_eq!(imported.blocks[2].block_id, \\\"block_3\\\");\\n}\",\n \"numLines\": 49,\n \"startLine\": 1,\n \"totalLines\": 49\n }\n}","tool_name":"read_file","tool_use_id":"call_uZ2hM13DLKIyY5MDkHqMgwMa","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_CLL8QcMs22VnG0pziMGqUfDQ","input":"{\"-A\":3,\"-B\":3,\"-C\":0,\"-i\":false,\"-n\":true,\"context\":0,\"glob\":\"**/*.{ts,tsx,rs}\",\"head_limit\":50,\"multiline\":false,\"offset\":0,\"output_mode\":\"content\",\"path\":\"/mnt/Data1T/mnote\",\"pattern\":\"isEditing\",\"type\":\"text\"}","name":"grep_search","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":72408,"output_tokens":94}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"mode\": \"content\",\n \"numFiles\": 0,\n \"filenames\": [],\n \"content\": \"\",\n \"numLines\": 0,\n \"numMatches\": null,\n \"appliedLimit\": null,\n \"appliedOffset\": null\n}","tool_name":"grep_search","tool_use_id":"call_CLL8QcMs22VnG0pziMGqUfDQ","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_PvQur70ewoVZeRBK6wy68RuY","input":"{\"-A\":2,\"-B\":2,\"-C\":0,\"-i\":false,\"-n\":true,\"context\":0,\"glob\":\"wolai-frontend/src/components/editor/document-content.tsx\",\"head_limit\":50,\"multiline\":false,\"offset\":0,\"output_mode\":\"content\",\"path\":\"/mnt/Data1T/mnote\",\"pattern\":\"isEditing|keepEditorMounted|编辑\", \"type\":\"text\"}","name":"grep_search","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":72573,"output_tokens":103}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"mode\": \"content\",\n \"numFiles\": 0,\n \"filenames\": [],\n \"content\": \"\",\n \"numLines\": 0,\n \"numMatches\": null,\n \"appliedLimit\": null,\n \"appliedOffset\": null\n}","tool_name":"grep_search","tool_use_id":"call_PvQur70ewoVZeRBK6wy68RuY","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_LvvTlP4SsoETRWGZJSDZhHPp","input":"{\"limit\":260,\"offset\":880,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":72746,"output_tokens":44}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx\",\n \"content\": \"

当前为阅读态,编辑器仅在进入编辑后挂载。

\\n ) : null}\\n

最近更新:{formattedUpdatedAt}

\\n \\n {canEditDocument && (\\n
\\n {isEditing ? (\\n \\n ) : (\\n \\n )}\\n
\\n )}\\n \\n \\n
\\n {contentLoading ? (\\n showContentLoadingIndicator ? (\\n
\\n 页面内容加载中...\\n
\\n ) : (\\n
\\n )\\n ) : contentError ? (\\n
\\n
{contentError}
\\n {\\n setContentError(null);\\n setContentLoading(true);\\n setContentReloadKey((prev) => prev + 1);\\n }}\\n >\\n 重试\\n \\n
\\n ) : (\\n
\\n {keepEditorMounted && (\\n
\\n {\\n setContentRevision(meta.revision);\\n setConflictDetectionKey(meta.conflictDetectionKey);\\n }}\\n />\\n
\\n )}\\n {!isEditing && (\\n
\\n \\n \\n
\\n )}\\n
\\n )}\\n \\n
\\n
\\n {showInspector && (\\n setHistoryOpen(true)}\\n onOpenComments={() => openCommentsForPage({ workspaceId, documentId })}\\n onUndo={canEditDocument ? handleUndo : undefined}\\n onRedo={canEditDocument ? handleRedo : undefined}\\n onDeletePage={canEditDocument ? handleDeletePage : undefined}\\n onOpenMoveEmbedPicker={canEditDocument ? handleOpenMoveEmbed : undefined}\\n onCopyPageLink={handleCopyPageLink}\\n onCopyPageReference={handleCopyPageReference}\\n onAddToTemplates={canEditDocument ? handleAddToTemplates : undefined}\\n />\\n )}\\n \\n \\n \\n \\n latestBlocksRef.current}\\n getLatestPageSubtree={() => pageSubtree}\\n />\\n \\n );\\n}\",\n \"numLines\": 131,\n \"startLine\": 881,\n \"totalLines\": 1011\n }\n}","tool_name":"read_file","tool_use_id":"call_LvvTlP4SsoETRWGZJSDZhHPp","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_OJ0ep72t0wgXAzlEmZgUwUxI","input":"{\"limit\":180,\"offset\":120,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":74095,"output_tokens":44}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx\",\n \"content\": \"}: DocumentContentProps) {\\n const setCurrentDocument = useCurrentDocumentStore((state) => state.setCurrent);\\n const clearIfMatch = useCurrentDocumentStore((state) => state.clearIfMatch);\\n const canEditDocument = !readOnly;\\n const [options, setOptions] = useState(initialOptions ?? defaultOptions);\\n const [stats, setStats] = useState(initialStats ?? defaultStats);\\n const [history, setHistory] = useState([]);\\n const [historyOpen, setHistoryOpen] = useState(false);\\n const editorBridge = useEditorBridgeStore((state) => state.bridge);\\n const router = useRouter();\\n const showInspector = usePageLayoutStore((state) => state.showInspector);\\n const openCommentsForPage = useCommentsUiStore((s) => s.openForPage);\\n const spellCheck = useAppPreferencesStore((s) => s.spellCheck);\\n const openMoveEmbedPicker = useMoveEmbedPickerStore((s) => s.openPicker);\\n const [pageTitle, setPageTitle] = useState(title ?? \\\"无标题\\\");\\n const [content, setContent] = useState(initialContent);\\n const [serverContentSnapshot, setServerContentSnapshot] = useState(initialContent);\\n const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState(\\n initialPageSubtree,\\n );\\n const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState(title ?? \\\"无标题\\\");\\n const [contentRevision, setContentRevision] = useState(initialContentRevision);\\n const [conflictDetectionKey, setConflictDetectionKey] = useState(initialConflictDetectionKey);\\n const [contentLoading, setContentLoading] = useState(() => initialContent == null);\\n const [contentError, setContentError] = useState(null);\\n const [contentReloadKey, setContentReloadKey] = useState(0);\\n const [showContentLoadingIndicator, setShowContentLoadingIndicator] = useState(false);\\n const shouldStartEditing = (Boolean((openTableId ?? \\\"\\\").trim()) || autoEnterEdit) && !readOnly;\\n const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\\n const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\\n const contentLoadingTimerRef = useRef | null>(null);\\n const editorUnmountTimerRef = useRef | null>(null);\\n const pendingOpenTableRef = useRef(null);\\n const latestBlocksRef = useRef(null);\\n const pageRootRef = useRef(null);\\n const readViewRootRef = useRef(null);\\n const pendingRestoreSnapshotRef = useRef(null);\\n const lastCopyBlockedAtRef = useRef(0);\\n\\n useEffect(() => {\\n setCurrentDocument(documentId, readOnly, disableDownload, disableCopy);\\n return () => {\\n clearIfMatch(documentId);\\n };\\n }, [clearIfMatch, disableCopy, disableDownload, documentId, readOnly, setCurrentDocument]);\\n\\n useEffect(() => {\\n if (!disableCopy) return;\\n\\n const toElement = (node: Node | null): Element | null => {\\n if (!node) return null;\\n if (node.nodeType === Node.TEXT_NODE) {\\n return node.parentElement;\\n }\\n return node instanceof Element ? node : null;\\n };\\n\\n const isEventInsidePage = () => {\\n const root = pageRootRef.current;\\n if (!root) return false;\\n const selection = typeof window !== \\\"undefined\\\" ? window.getSelection() : null;\\n const anchor = selection?.anchorNode ?? null;\\n const focus = selection?.focusNode ?? null;\\n const anchorEl = toElement(anchor);\\n const focusEl = toElement(focus);\\n return Boolean((anchorEl && root.contains(anchorEl)) || (focusEl && root.contains(focusEl)));\\n };\\n\\n const notifyBlocked = () => {\\n const now = Date.now();\\n if (now - lastCopyBlockedAtRef.current < 1200) return;\\n lastCopyBlockedAtRef.current = now;\\n window.alert(\\\"该页面已禁止复制\\\");\\n };\\n\\n const onCopy = (event: ClipboardEvent) => {\\n if (!isEventInsidePage()) return;\\n event.preventDefault();\\n event.stopPropagation();\\n notifyBlocked();\\n };\\n\\n const onCut = (event: ClipboardEvent) => {\\n if (!isEventInsidePage()) return;\\n event.preventDefault();\\n event.stopPropagation();\\n notifyBlocked();\\n };\\n\\n const onKeyDown = (event: KeyboardEvent) => {\\n if (!isEventInsidePage()) return;\\n const key = String(event.key ?? \\\"\\\").toLowerCase();\\n const ctrlOrMeta = event.ctrlKey || event.metaKey;\\n if (!ctrlOrMeta) return;\\n if (key === \\\"c\\\" || key === \\\"x\\\" || key === \\\"insert\\\") {\\n event.preventDefault();\\n event.stopPropagation();\\n notifyBlocked();\\n }\\n };\\n\\n document.addEventListener(\\\"copy\\\", onCopy, true);\\n document.addEventListener(\\\"cut\\\", onCut, true);\\n document.addEventListener(\\\"keydown\\\", onKeyDown, true);\\n return () => {\\n document.removeEventListener(\\\"copy\\\", onCopy, true);\\n document.removeEventListener(\\\"cut\\\", onCut, true);\\n document.removeEventListener(\\\"keydown\\\", onKeyDown, true);\\n };\\n }, [disableCopy]);\\n\\n useEffect(() => {\\n const tableId = (openTableId ?? \\\"\\\").trim();\\n if (!tableId) return;\\n if (!editorBridge?.openTableFullScreen) return;\\n if (pendingOpenTableRef.current === tableId) return;\\n pendingOpenTableRef.current = tableId;\\n\\n editorBridge.openTableFullScreen(tableId);\\n\\n if (typeof window !== \\\"undefined\\\") {\\n try {\\n const url = new URL(window.location.href);\\n url.searchParams.delete(\\\"openTableId\\\");\\n window.history.replaceState({}, \\\"\\\", url.toString());\\n } catch {\\n router.replace(`/documents/${documentId}`);\\n }\\n }\\n }, [documentId, editorBridge, openTableId, router]);\\n\\n useEffect(() => {\\n setPageTitle(title ?? \\\"无标题\\\");\\n }, [title]);\\n\\n useEffect(() => {\\n setServerPageSubtreeTitle(title ?? \\\"无标题\\\");\\n }, [title]);\\n\\n useEffect(() => {\\n setServerPageSubtreeSnapshot(initialPageSubtree);\\n }, [initialPageSubtree]);\\n\\n useEffect(() => {\\n setOptions(initialOptions ?? defaultOptions);\\n }, [initialOptions]);\\n\\n useEffect(() => {\\n setStats(initialStats ?? defaultStats);\\n }, [initialStats]);\\n\\n useEffect(() => {\\n setContentRevision(initialContentRevision);\\n }, [initialContentRevision]);\\n\\n useEffect(() => {\\n setConflictDetectionKey(initialConflictDetectionKey);\\n }, [initialConflictDetectionKey]);\\n\\n useEffect(() => {\\n setServerContentSnapshot(initialContent);\\n }, [initialContent]);\\n\\n useEffect(() => {\\n const nextBlocks = extractPageBlocks(content);\\n latestBlocksRef.current = nextBlocks.length > 0 ? (nextBlocks as Json) : null;\\n }, [content]);\\n\\n useEffect(() => {\\n const shouldForceEdit = Boolean((openTableId ?? \\\"\\\").trim()) && canEditDocument;\\n if (shouldForceEdit) {\\n setIsEditing(true);\\n setKeepEditorMounted(true);\\n }\\n }, [canEditDocument, openTableId]);\\n\\n useEffect(() => {\\n if (isEditing) {\\n if (editorUnmountTimerRef.current) {\\n clearTimeout(editorUnmountTimerRef.current);\",\n \"numLines\": 180,\n \"startLine\": 121,\n \"totalLines\": 1011\n }\n}","tool_name":"read_file","tool_use_id":"call_OJ0ep72t0wgXAzlEmZgUwUxI","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_5p3Gt7qaDsIO7KxjE4NEFuOy","input":"{\"limit\":140,\"offset\":300,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":76149,"output_tokens":44}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx\",\n \"content\": \" editorUnmountTimerRef.current = null;\\n }\\n setKeepEditorMounted(true);\\n return;\\n }\\n if (editorUnmountTimerRef.current) {\\n clearTimeout(editorUnmountTimerRef.current);\\n }\\n editorUnmountTimerRef.current = setTimeout(() => {\\n setKeepEditorMounted(false);\\n editorUnmountTimerRef.current = null;\\n }, EDITOR_UNMOUNT_GRACE_MS);\\n return () => {\\n if (editorUnmountTimerRef.current) {\\n clearTimeout(editorUnmountTimerRef.current);\\n editorUnmountTimerRef.current = null;\\n }\\n };\\n }, [isEditing]);\\n\\n useEffect(() => {\\n let canceled = false;\\n const controller = new AbortController();\\n\\n const load = async () => {\\n setContentError(null);\\n setContentLoading(initialContent == null);\\n setContent(initialContent);\\n setShowContentLoadingIndicator(false);\\n\\n if (initialContent != null) {\\n return;\\n }\\n\\n if (contentLoadingTimerRef.current) {\\n clearTimeout(contentLoadingTimerRef.current);\\n contentLoadingTimerRef.current = null;\\n }\\n contentLoadingTimerRef.current = setTimeout(() => {\\n if (!canceled) {\\n setShowContentLoadingIndicator(true);\\n }\\n }, CONTENT_LOADING_DELAY_MS);\\n\\n try {\\n const response = await fetch(\\n `/api/documents/content?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,\\n {\\n method: \\\"GET\\\",\\n credentials: \\\"include\\\",\\n signal: controller.signal,\\n },\\n );\\n if (!response.ok) {\\n const payload = await response.json().catch(() => ({}));\\n throw new Error(payload?.error ?? \\\"加载页面内容失败\\\");\\n }\\n const payload = (await response.json()) as {\\n content?: unknown;\\n revision?: number | null;\\n conflictDetectionKey?: string | null;\\n pageSubtree?: PageSubtreeProjection | null;\\n };\\n if (canceled) return;\\n setContent(payload.content ?? null);\\n setServerContentSnapshot(payload.content ?? null);\\n setContentRevision(\\n typeof payload.revision === \\\"number\\\" && Number.isInteger(payload.revision)\\n ? payload.revision\\n : 0,\\n );\\n setConflictDetectionKey(\\n typeof payload.conflictDetectionKey === \\\"string\\\" && payload.conflictDetectionKey.trim()\\n ? payload.conflictDetectionKey\\n : `${documentId}:0`,\\n );\\n setServerPageSubtreeSnapshot(payload.pageSubtree ?? null);\\n setServerPageSubtreeTitle(\\n typeof payload.pageSubtree?.rootNode.metadata.title === \\\"string\\\" &&\\n payload.pageSubtree.rootNode.metadata.title.trim()\\n ? payload.pageSubtree.rootNode.metadata.title\\n : title ?? \\\"无标题\\\",\\n );\\n } catch (error) {\\n if (canceled) return;\\n if ((error as { name?: string })?.name === \\\"AbortError\\\") return;\\n setContentError(error instanceof Error ? error.message : \\\"加载页面内容失败\\\");\\n } finally {\\n if (!canceled) {\\n setContentLoading(false);\\n setShowContentLoadingIndicator(false);\\n }\\n if (contentLoadingTimerRef.current) {\\n clearTimeout(contentLoadingTimerRef.current);\\n contentLoadingTimerRef.current = null;\\n }\\n }\\n };\\n\\n void load();\\n\\n return () => {\\n canceled = true;\\n controller.abort();\\n if (contentLoadingTimerRef.current) {\\n clearTimeout(contentLoadingTimerRef.current);\\n contentLoadingTimerRef.current = null;\\n }\\n };\\n }, [documentId, initialContent, contentReloadKey, title, workspaceId]);\\n\\n const persistTitle = useCallback(\\n async (nextTitle: string) => {\\n if (readOnly) return;\\n const payload = nextTitle.trim() || \\\"无标题\\\";\\n try {\\n await renameDocumentCommand({ documentId, workspaceId, title: payload });\\n } catch (error) {\\n console.error(\\\"更新页面标题失败\\\", error);\\n }\\n },\\n [documentId, readOnly, workspaceId],\\n );\\n\\n const debouncedPersistTitle = useDebouncedCallback((value: string) => {\\n void persistTitle(value);\\n }, 600);\\n\\n const handleTitleChange = (event: ChangeEvent) => {\\n if (!canEditDocument) return;\\n const value = event.target.value;\\n setPageTitle(value);\\n debouncedPersistTitle(value);\\n };\\n\\n const handleTitleBlur = () => {\\n if (!canEditDocument) return;\\n void persistTitle(pageTitle);\\n };\\n\",\n \"numLines\": 140,\n \"startLine\": 301,\n \"totalLines\": 1011\n }\n}","tool_name":"read_file","tool_use_id":"call_5p3Gt7qaDsIO7KxjE4NEFuOy","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_cilRtcA76zIgfx5OjE0PMtx5","input":"{\"limit\":220,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/save/route.ts\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":77398,"output_tokens":46}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/save/route.ts\",\n \"content\": \"import { NextResponse } from \\\"next/server\\\";\\nimport { isConvexEnabled } from \\\"@/lib/convex/enabled\\\";\\nimport {\\n assertDocumentId,\\n buildDocumentBridgeContext,\\n buildDocumentCommandEnvelope,\\n documentBridgeErrorResponse,\\n} from \\\"@/lib/documents/bridge\\\";\\nimport { executeSaveBridgeCommand } from \\\"@/lib/documents/save-command-adapter\\\";\\nimport {\\n buildDocumentSavePayload,\\n type DocumentSavePayload,\\n} from \\\"@/lib/documents/save-contract\\\";\\n\\nexport async function POST(request: Request) {\\n if (isConvexEnabled()) {\\n try {\\n const body = await request.json() as Partial & { content: unknown };\\n const normalizedDocumentId = assertDocumentId(body.documentId);\\n const payload = buildDocumentSavePayload({\\n documentId: normalizedDocumentId,\\n workspaceId: body.workspaceId,\\n revision: body.revision,\\n content: body.content as DocumentSavePayload[\\\"content\\\"],\\n conflictDetectionKey: body.conflictDetectionKey,\\n });\\n const normalizedWorkspaceId = payload.workspaceId;\\n const bridgeContext = await buildDocumentBridgeContext({ request, workspaceId: normalizedWorkspaceId });\\n const envelope = buildDocumentCommandEnvelope({\\n name: \\\"documents.save\\\",\\n payload: payload satisfies DocumentSavePayload,\\n context: bridgeContext,\\n target: {\\n workspaceId: normalizedWorkspaceId,\\n pageId: normalizedDocumentId,\\n },\\n });\\n const result = await executeSaveBridgeCommand({\\n context: bridgeContext,\\n envelope,\\n });\\n\\n return NextResponse.json({\\n ok: true,\\n revision: result.revision,\\n conflictDetectionKey: result.conflictDetectionKey,\\n meta: {\\n requestId: result.requestId,\\n traceId: result.traceId,\\n commandId: result.commandId,\\n commandName: result.commandName,\\n },\\n });\\n } catch (error) {\\n return documentBridgeErrorResponse(error);\\n }\\n }\\n\\n return NextResponse.json({ error: \\\"当前仅支持 Convex 模式\\\" }, { status: 501 });\\n}\\n\\nexport const runtime = \\\"nodejs\\\";\",\n \"numLines\": 62,\n \"startLine\": 1,\n \"totalLines\": 62\n }\n}","tool_name":"read_file","tool_use_id":"call_cilRtcA76zIgfx5OjE0PMtx5","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_evTsic3h3JdsRWubRubRBDHk","input":"{\"limit\":260,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/save-contract.ts\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":78056,"output_tokens":44}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/save-contract.ts\",\n \"content\": \"import type { Json } from \\\"@/types/supabase\\\";\\n\\nexport type DocumentSavePayload = {\\n documentId: string;\\n workspaceId: string | null;\\n revision: number | null;\\n content: Json;\\n conflictDetectionKey: string | null;\\n snapshotCapturedAt: string | null;\\n blockCount: number | null;\\n};\\n\\nexport function buildDocumentSavePayload(input: {\\n documentId: string;\\n workspaceId?: string | null;\\n revision?: number | null;\\n content: Json;\\n conflictDetectionKey?: string | null;\\n snapshotCapturedAt?: string | null;\\n blockCount?: number | null;\\n}): DocumentSavePayload {\\n const revision =\\n typeof input.revision === \\\"number\\\" && Number.isInteger(input.revision) && input.revision >= 0\\n ? input.revision\\n : null;\\n const conflictDetectionKey =\\n typeof input.conflictDetectionKey === \\\"string\\\" && input.conflictDetectionKey.trim()\\n ? input.conflictDetectionKey.trim()\\n : null;\\n const snapshotCapturedAt =\\n typeof input.snapshotCapturedAt === \\\"string\\\" && input.snapshotCapturedAt.trim()\\n ? input.snapshotCapturedAt.trim()\\n : null;\\n const blockCount =\\n typeof input.blockCount === \\\"number\\\" && Number.isInteger(input.blockCount) && input.blockCount >= 0\\n ? input.blockCount\\n : null;\\n\\n return {\\n documentId: input.documentId.trim(),\\n workspaceId: input.workspaceId?.trim() || null,\\n revision,\\n content: input.content,\\n conflictDetectionKey,\\n snapshotCapturedAt,\\n blockCount,\\n };\\n}\",\n \"numLines\": 48,\n \"startLine\": 1,\n \"totalLines\": 48\n }\n}","tool_name":"read_file","tool_use_id":"call_evTsic3h3JdsRWubRubRBDHk","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_FTTVLkxWLEFGJOcOlaXDZeYX","input":"{\"-A\":2,\"-B\":2,\"-C\":0,\"-i\":false,\"-n\":true,\"context\":0,\"glob\":\"wolai-frontend/src/**/*.{ts,tsx}\",\"head_limit\":50,\"multiline\":false,\"offset\":0,\"output_mode\":\"content\",\"path\":\"/mnt/Data1T/mnote\",\"pattern\":\"DocumentContent\\\\(|\\n
\\n \\n
\\n \\n );\\n }\\n\\n redirect(\\\"/auth\\\");\\n}\",\n \"numLines\": 53,\n \"startLine\": 151,\n \"totalLines\": 203\n }\n}","tool_name":"read_file","tool_use_id":"call_8P8kMkntqpnilo1oD3gZ7Pdf","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_tTsdnuiaZlxKHO4ZlT08AfX7","input":"{\"-A\":3,\"-B\":3,\"-C\":0,\"-i\":false,\"-n\":true,\"context\":0,\"glob\":\"wolai-frontend/src/**/*.{ts,tsx}\",\"head_limit\":20,\"multiline\":false,\"offset\":0,\"output_mode\":\"content\",\"path\":\"/mnt/Data1T/mnote\",\"pattern\":\"function DocumentShell|const DocumentShell|export function DocumentShell|export default function DocumentShell\",\"type\":\"text\"}","name":"grep_search","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":79382,"output_tokens":112}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"mode\": \"content\",\n \"numFiles\": 0,\n \"filenames\": [],\n \"content\": \"\",\n \"numLines\": 0,\n \"numMatches\": null,\n \"appliedLimit\": null,\n \"appliedOffset\": null\n}","tool_name":"grep_search","tool_use_id":"call_tTsdnuiaZlxKHO4ZlT08AfX7","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_ia3uDpE2uceuBryKO5pOgnP4","input":"{\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components\",\"pattern\":\"**/*document*shell*.tsx\"}","name":"glob_search","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":79565,"output_tokens":41}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"durationMs\": 0,\n \"numFiles\": 1,\n \"filenames\": [\n \"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-shell.tsx\"\n ],\n \"truncated\": false\n}","tool_name":"glob_search","tool_use_id":"call_ia3uDpE2uceuBryKO5pOgnP4","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_lP4uAv3lZxbzNNSxaOiU76pk","input":"{\"limit\":220,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-shell.tsx\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":79671,"output_tokens":44}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-shell.tsx\",\n \"content\": \"\\\"use client\\\";\\n\\nimport type { DocumentContentProps } from \\\"@/components/editor/document-content\\\";\\nimport { DocumentContent } from \\\"@/components/editor/document-content\\\";\\n\\nexport function DocumentShell(props: DocumentContentProps) {\\n return ;\\n}\",\n \"numLines\": 8,\n \"startLine\": 1,\n \"totalLines\": 8\n }\n}","tool_name":"read_file","tool_use_id":"call_lP4uAv3lZxbzNNSxaOiU76pk","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_Atc1irpo4T2T4d1JlOaUvSJP","input":"{\"-A\":2,\"-B\":2,\"-C\":0,\"-i\":false,\"-n\":true,\"context\":0,\"glob\":\"rust/crates/core-protocol/src/editor/model.rs\",\"head_limit\":80,\"multiline\":false,\"offset\":0,\"output_mode\":\"content\",\"path\":\"/mnt/Data1T/mnote\",\"pattern\":\"pub struct EditorBlock|pub struct EditorBlockDocument|pub enum EditorBlockType|pub struct BlockProps\",\"type\":\"text\"}","name":"grep_search","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":79861,"output_tokens":114}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"mode\": \"content\",\n \"numFiles\": 0,\n \"filenames\": [],\n \"content\": \"\",\n \"numLines\": 0,\n \"numMatches\": null,\n \"appliedLimit\": null,\n \"appliedOffset\": null\n}","tool_name":"grep_search","tool_use_id":"call_Atc1irpo4T2T4d1JlOaUvSJP","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_gvFGhnu39pe9MXcqKAByylDK","input":"{\"limit\":260,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":80046,"output_tokens":43}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs\",\n \"content\": \"use serde::{Deserialize, Serialize};\\nuse serde_json::Value;\\nuse std::collections::BTreeMap;\\n\\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\\n#[serde(rename_all = \\\"snake_case\\\")]\\npub enum EditorBlockType {\\n Paragraph,\\n Heading,\\n BulletListItem,\\n NumberedListItem,\\n Quote,\\n Todo,\\n CodeBlock,\\n PageReference,\\n BlockReference,\\n}\\n\\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\\n#[serde(rename_all = \\\"snake_case\\\")]\\npub enum ReferenceTokenKind {\\n PageReference,\\n BlockReference,\\n}\\n\\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\\n#[serde(rename_all = \\\"snake_case\\\")]\\npub enum ReferenceTokenStrategy {\\n DoubleBracket,\\n DoubleParen,\\n InlineChip,\\n}\\n\\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\\n#[serde(rename_all = \\\"snake_case\\\")]\\npub enum TextMark {\\n Bold,\\n Italic,\\n Underline,\\n Strike,\\n Code,\\n}\\n\\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct ReferenceToken {\\n pub kind: ReferenceTokenKind,\\n pub strategy: ReferenceTokenStrategy,\\n pub target_id: String,\\n pub label: Option,\\n pub raw_token: Option,\\n}\\n\\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\\n#[serde(tag = \\\"type\\\", rename_all = \\\"snake_case\\\")]\\npub enum ContentNodePayload {\\n Text {\\n text: String,\\n #[serde(default)]\\n marks: Vec,\\n },\\n HardBreak,\\n ReferenceToken {\\n token: ReferenceToken,\\n },\\n}\\n\\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct ContentNode {\\n pub payload: ContentNodePayload,\\n #[serde(default)]\\n pub attrs: BTreeMap,\\n}\\n\\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct BlockProps {\\n pub indent: Option,\\n pub heading_level: Option,\\n pub checked: Option,\\n pub collapsed: Option,\\n pub language: Option,\\n pub reference_target_id: Option,\\n pub reference_label: Option,\\n pub reference_token_strategy: Option,\\n #[serde(default)]\\n pub extra: BTreeMap,\\n}\\n\\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct EditorBlock {\\n pub block_id: String,\\n pub block_type: EditorBlockType,\\n #[serde(default)]\\n pub props: BlockProps,\\n #[serde(default)]\\n pub content_nodes: Vec,\\n #[serde(default)]\\n pub child_block_ids: Vec,\\n}\\n\\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct EditorBlockDocument {\\n pub document_id: String,\\n #[serde(default)]\\n pub root_block_ids: Vec,\\n #[serde(default)]\\n pub blocks: Vec,\\n}\",\n \"numLines\": 112,\n \"startLine\": 1,\n \"totalLines\": 112\n }\n}","tool_name":"read_file","tool_use_id":"call_gvFGhnu39pe9MXcqKAByylDK","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_KtiWFamKaQOEkblBevoGen20","input":"{\"-A\":2,\"-B\":2,\"-C\":0,\"-i\":false,\"-n\":true,\"context\":0,\"glob\":\"wolai-frontend/src/**/*.{ts,tsx}\",\"head_limit\":50,\"multiline\":false,\"offset\":0,\"output_mode\":\"content\",\"path\":\"/mnt/Data1T/mnote\",\"pattern\":\"buildDocumentSavePayload\\\\(|content:\\\\s*blocks|documents.save\",\"type\":\"text\"}","name":"grep_search","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":81001,"output_tokens":108}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"mode\": \"content\",\n \"numFiles\": 0,\n \"filenames\": [],\n \"content\": \"\",\n \"numLines\": 0,\n \"numMatches\": null,\n \"appliedLimit\": null,\n \"appliedOffset\": null\n}","tool_name":"grep_search","tool_use_id":"call_KtiWFamKaQOEkblBevoGen20","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_FD9KLjmU23otcO00Fe7nXQmt","input":"{\"limit\":260,\"offset\":0,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.test.ts\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":81180,"output_tokens":49}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.test.ts\",\n \"content\": \"import { blocksFromTiptapDoc, tiptapDocFromBlocks } from \\\"@/lib/documents/tiptap-content-converter\\\";\\nimport { describe, expect, it } from \\\"vitest\\\";\\n\\ndescribe(\\\"tiptap-content-converter\\\", () => {\\n it(\\\"keeps empty content empty\\\", () => {\\n expect(tiptapDocFromBlocks([])).toEqual({ type: \\\"doc\\\", content: [] });\\n expect(blocksFromTiptapDoc({ type: \\\"doc\\\", content: [] })).toEqual([]);\\n });\\n\\n it(\\\"round-trips basic paragraph content\\\", () => {\\n const blocks = [{ id: \\\"1\\\", type: \\\"paragraph\\\", content: \\\"你好,世界\\\" }];\\n expect(blocksFromTiptapDoc(tiptapDocFromBlocks(blocks as never))).toEqual([\\n { id: \\\"block-0\\\", type: \\\"paragraph\\\", content: \\\"你好,世界\\\" },\\n ]);\\n });\\n});\",\n \"numLines\": 16,\n \"startLine\": 1,\n \"totalLines\": 16\n }\n}","tool_name":"read_file","tool_use_id":"call_FD9KLjmU23otcO00Fe7nXQmt","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_KHy82V8pHxEkmMiG1NiMvRn6","input":"{\"new_string\":\" const shouldStartEditing = (Boolean((openTableId ?? \\\"\\\").trim()) || autoEnterEdit) && !readOnly;\\n const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\\n const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\\n\",\"old_string\":\" const shouldStartEditing = (Boolean((openTableId ?? \\\"\\\").trim()) || autoEnterEdit) && !readOnly;\\n const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\\n const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\\n\",\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx\",\"replace_all\":false}","name":"edit_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":81541,"output_tokens":191}},"type":"message"} +{"message":{"blocks":[{"is_error":true,"output":"old_string and new_string must differ","tool_name":"edit_file","tool_use_id":"call_KHy82V8pHxEkmMiG1NiMvRn6","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_SSO1jfLdk6z3L2iDwI5SvUJU","input":"{\"new_string\":\" const shouldStartEditing = canEditDocument;\\n const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\\n const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\\n\",\"old_string\":\" const shouldStartEditing = (Boolean((openTableId ?? \\\"\\\").trim()) || autoEnterEdit) && !readOnly;\\n const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\\n const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\\n\",\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx\",\"replace_all\":false}","name":"edit_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":81750,"output_tokens":173}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx\",\n \"oldString\": \" const shouldStartEditing = (Boolean((openTableId ?? \\\"\\\").trim()) || autoEnterEdit) && !readOnly;\\n const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\\n const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\\n\",\n \"newString\": \" const shouldStartEditing = canEditDocument;\\n const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\\n const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\\n\",\n \"originalFile\": \"\\\"use client\\\";\\n\\nimport dynamic from \\\"next/dynamic\\\";\\nimport { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent as ReactKeyboardEvent } from \\\"react\\\";\\nimport type { BooleanPageOptionKey, DocumentStats, PageOptionsState, PageFont, PageLayoutDensity } from \\\"@/types/page-options\\\";\\nimport { useEditorBridgeStore } from \\\"@/store/editor-bridge\\\";\\nimport { usePageLayoutStore } from \\\"@/store/page-layout\\\";\\nimport { useCurrentDocumentStore } from \\\"@/store/current-document\\\";\\nimport type { Json } from \\\"@/types/supabase\\\";\\nimport type { DocumentSnapshot } from \\\"@/types/document\\\";\\nimport { useDebouncedCallback } from \\\"@/hooks/use-debounced-callback\\\";\\nimport { ImagePickerProvider } from \\\"@/components/media/image-picker-context\\\";\\nimport { useRouter } from \\\"next/navigation\\\";\\nimport { DocumentAiAgentPanel } from \\\"./DocumentAiAgentPanel\\\";\\nimport { CONTENT_LOADING_DELAY_MS } from \\\"@/lib/constants\\\";\\nimport { useCommentsUiStore } from \\\"@/store/comments-ui\\\";\\nimport { useAppPreferencesStore } from \\\"@/store/app-preferences\\\";\\nimport { useMoveEmbedPickerStore } from \\\"@/store/move-embed-picker\\\";\\nimport { cn } from \\\"@/lib/utils\\\";\\nimport { Button } from \\\"@/components/ui/button\\\";\\nimport { DocumentToc } from \\\"@/components/editor/document-toc\\\";\\nimport { DocumentReadView } from \\\"@/components/editor/document-read-view\\\";\\nimport { extractPageBlocks, type PageSubtreeProjection } from \\\"@/lib/documents/page-subtree\\\";\\nimport {\\n deleteDocumentCommand,\\n embedDocumentCommand,\\n moveDocumentCommand,\\n renameDocumentCommand,\\n} from \\\"@/lib/documents/tree-command-client\\\";\\nimport { EditorHost } from \\\"@/components/editor/editor-host\\\";\\n\\nconst PageOptionsSidebar = dynamic(\\n () => import(\\\"@/components/editor/page-options-sidebar\\\").then((mod) => mod.PageOptionsSidebar),\\n { ssr: false },\\n);\\n\\nconst PageBacklinksPanel = dynamic(\\n () => import(\\\"@/components/editor/page-backlinks-panel\\\").then((mod) => mod.PageBacklinksPanel),\\n {\\n ssr: false,\\n loading: () => null,\\n },\\n);\\n\\nconst DocumentHistoryDrawer = dynamic(\\n () => import(\\\"@/components/editor/document-history-drawer\\\").then((mod) => mod.DocumentHistoryDrawer),\\n {\\n ssr: false,\\n loading: () => null,\\n },\\n);\\n\\nconst MoveEmbedPickerHost = dynamic(\\n () => import(\\\"@/components/documents/move-embed-picker-host\\\").then((mod) => mod.MoveEmbedPickerHost),\\n {\\n ssr: false,\\n loading: () => null,\\n },\\n);\\n\\nconst DocumentCommentsDrawer = dynamic(\\n () => import(\\\"@/components/editor/document-comments-drawer\\\").then((mod) => mod.DocumentCommentsDrawer),\\n {\\n ssr: false,\\n loading: () => null,\\n },\\n);\\n\\nexport interface DocumentContentProps {\\n documentId: string;\\n workspaceId: string;\\n title: string | null;\\n updatedAt: string | null;\\n initialContent: unknown;\\n initialContentRevision?: number | null;\\n initialConflictDetectionKey?: string | null;\\n initialPageSubtree?: PageSubtreeProjection | null;\\n initialOptions: PageOptionsState;\\n initialStats: DocumentStats | null;\\n openTableId?: string | null;\\n autoEnterEdit?: boolean;\\n readOnly?: boolean;\\n disableDownload?: boolean;\\n disableCopy?: boolean;\\n}\\n\\nconst defaultOptions: PageOptionsState = {\\n wideLayout: false,\\n smallText: false,\\n showHeadingNumbers: true,\\n showToc: false,\\n showStructure: false,\\n protectEditing: false,\\n showWordCount: true,\\n collapseBacklinks: false,\\n pageFont: \\\"default\\\",\\n layoutDensity: \\\"normal\\\",\\n hideChildPages: false,\\n showBlockRefCount: false,\\n embedDefaultBlockId: null,\\n};\\nconst defaultStats: DocumentStats = { wordCount: 0, characterCount: 0, blockCount: 0, todoTotal: 0, todoDone: 0 };\\nconst EDITOR_UNMOUNT_GRACE_MS = 1000;\\n\\nexport function DocumentContent({\\n documentId,\\n workspaceId,\\n title,\\n updatedAt,\\n initialContent,\\n initialContentRevision = null,\\n initialConflictDetectionKey = null,\\n initialPageSubtree = null,\\n initialOptions,\\n initialStats,\\n openTableId,\\n autoEnterEdit = false,\\n readOnly = false,\\n disableDownload = false,\\n disableCopy = false,\\n}: DocumentContentProps) {\\n const setCurrentDocument = useCurrentDocumentStore((state) => state.setCurrent);\\n const clearIfMatch = useCurrentDocumentStore((state) => state.clearIfMatch);\\n const canEditDocument = !readOnly;\\n const [options, setOptions] = useState(initialOptions ?? defaultOptions);\\n const [stats, setStats] = useState(initialStats ?? defaultStats);\\n const [history, setHistory] = useState([]);\\n const [historyOpen, setHistoryOpen] = useState(false);\\n const editorBridge = useEditorBridgeStore((state) => state.bridge);\\n const router = useRouter();\\n const showInspector = usePageLayoutStore((state) => state.showInspector);\\n const openCommentsForPage = useCommentsUiStore((s) => s.openForPage);\\n const spellCheck = useAppPreferencesStore((s) => s.spellCheck);\\n const openMoveEmbedPicker = useMoveEmbedPickerStore((s) => s.openPicker);\\n const [pageTitle, setPageTitle] = useState(title ?? \\\"无标题\\\");\\n const [content, setContent] = useState(initialContent);\\n const [serverContentSnapshot, setServerContentSnapshot] = useState(initialContent);\\n const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState(\\n initialPageSubtree,\\n );\\n const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState(title ?? \\\"无标题\\\");\\n const [contentRevision, setContentRevision] = useState(initialContentRevision);\\n const [conflictDetectionKey, setConflictDetectionKey] = useState(initialConflictDetectionKey);\\n const [contentLoading, setContentLoading] = useState(() => initialContent == null);\\n const [contentError, setContentError] = useState(null);\\n const [contentReloadKey, setContentReloadKey] = useState(0);\\n const [showContentLoadingIndicator, setShowContentLoadingIndicator] = useState(false);\\n const shouldStartEditing = (Boolean((openTableId ?? \\\"\\\").trim()) || autoEnterEdit) && !readOnly;\\n const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\\n const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\\n const contentLoadingTimerRef = useRef | null>(null);\\n const editorUnmountTimerRef = useRef | null>(null);\\n const pendingOpenTableRef = useRef(null);\\n const latestBlocksRef = useRef(null);\\n const pageRootRef = useRef(null);\\n const readViewRootRef = useRef(null);\\n const pendingRestoreSnapshotRef = useRef(null);\\n const lastCopyBlockedAtRef = useRef(0);\\n\\n useEffect(() => {\\n setCurrentDocument(documentId, readOnly, disableDownload, disableCopy);\\n return () => {\\n clearIfMatch(documentId);\\n };\\n }, [clearIfMatch, disableCopy, disableDownload, documentId, readOnly, setCurrentDocument]);\\n\\n useEffect(() => {\\n if (!disableCopy) return;\\n\\n const toElement = (node: Node | null): Element | null => {\\n if (!node) return null;\\n if (node.nodeType === Node.TEXT_NODE) {\\n return node.parentElement;\\n }\\n return node instanceof Element ? node : null;\\n };\\n\\n const isEventInsidePage = () => {\\n const root = pageRootRef.current;\\n if (!root) return false;\\n const selection = typeof window !== \\\"undefined\\\" ? window.getSelection() : null;\\n const anchor = selection?.anchorNode ?? null;\\n const focus = selection?.focusNode ?? null;\\n const anchorEl = toElement(anchor);\\n const focusEl = toElement(focus);\\n return Boolean((anchorEl && root.contains(anchorEl)) || (focusEl && root.contains(focusEl)));\\n };\\n\\n const notifyBlocked = () => {\\n const now = Date.now();\\n if (now - lastCopyBlockedAtRef.current < 1200) return;\\n lastCopyBlockedAtRef.current = now;\\n window.alert(\\\"该页面已禁止复制\\\");\\n };\\n\\n const onCopy = (event: ClipboardEvent) => {\\n if (!isEventInsidePage()) return;\\n event.preventDefault();\\n event.stopPropagation();\\n notifyBlocked();\\n };\\n\\n const onCut = (event: ClipboardEvent) => {\\n if (!isEventInsidePage()) return;\\n event.preventDefault();\\n event.stopPropagation();\\n notifyBlocked();\\n };\\n\\n const onKeyDown = (event: KeyboardEvent) => {\\n if (!isEventInsidePage()) return;\\n const key = String(event.key ?? \\\"\\\").toLowerCase();\\n const ctrlOrMeta = event.ctrlKey || event.metaKey;\\n if (!ctrlOrMeta) return;\\n if (key === \\\"c\\\" || key === \\\"x\\\" || key === \\\"insert\\\") {\\n event.preventDefault();\\n event.stopPropagation();\\n notifyBlocked();\\n }\\n };\\n\\n document.addEventListener(\\\"copy\\\", onCopy, true);\\n document.addEventListener(\\\"cut\\\", onCut, true);\\n document.addEventListener(\\\"keydown\\\", onKeyDown, true);\\n return () => {\\n document.removeEventListener(\\\"copy\\\", onCopy, true);\\n document.removeEventListener(\\\"cut\\\", onCut, true);\\n document.removeEventListener(\\\"keydown\\\", onKeyDown, true);\\n };\\n }, [disableCopy]);\\n\\n useEffect(() => {\\n const tableId = (openTableId ?? \\\"\\\").trim();\\n if (!tableId) return;\\n if (!editorBridge?.openTableFullScreen) return;\\n if (pendingOpenTableRef.current === tableId) return;\\n pendingOpenTableRef.current = tableId;\\n\\n editorBridge.openTableFullScreen(tableId);\\n\\n if (typeof window !== \\\"undefined\\\") {\\n try {\\n const url = new URL(window.location.href);\\n url.searchParams.delete(\\\"openTableId\\\");\\n window.history.replaceState({}, \\\"\\\", url.toString());\\n } catch {\\n router.replace(`/documents/${documentId}`);\\n }\\n }\\n }, [documentId, editorBridge, openTableId, router]);\\n\\n useEffect(() => {\\n setPageTitle(title ?? \\\"无标题\\\");\\n }, [title]);\\n\\n useEffect(() => {\\n setServerPageSubtreeTitle(title ?? \\\"无标题\\\");\\n }, [title]);\\n\\n useEffect(() => {\\n setServerPageSubtreeSnapshot(initialPageSubtree);\\n }, [initialPageSubtree]);\\n\\n useEffect(() => {\\n setOptions(initialOptions ?? defaultOptions);\\n }, [initialOptions]);\\n\\n useEffect(() => {\\n setStats(initialStats ?? defaultStats);\\n }, [initialStats]);\\n\\n useEffect(() => {\\n setContentRevision(initialContentRevision);\\n }, [initialContentRevision]);\\n\\n useEffect(() => {\\n setConflictDetectionKey(initialConflictDetectionKey);\\n }, [initialConflictDetectionKey]);\\n\\n useEffect(() => {\\n setServerContentSnapshot(initialContent);\\n }, [initialContent]);\\n\\n useEffect(() => {\\n const nextBlocks = extractPageBlocks(content);\\n latestBlocksRef.current = nextBlocks.length > 0 ? (nextBlocks as Json) : null;\\n }, [content]);\\n\\n useEffect(() => {\\n const shouldForceEdit = Boolean((openTableId ?? \\\"\\\").trim()) && canEditDocument;\\n if (shouldForceEdit) {\\n setIsEditing(true);\\n setKeepEditorMounted(true);\\n }\\n }, [canEditDocument, openTableId]);\\n\\n useEffect(() => {\\n if (isEditing) {\\n if (editorUnmountTimerRef.current) {\\n clearTimeout(editorUnmountTimerRef.current);\\n editorUnmountTimerRef.current = null;\\n }\\n setKeepEditorMounted(true);\\n return;\\n }\\n if (editorUnmountTimerRef.current) {\\n clearTimeout(editorUnmountTimerRef.current);\\n }\\n editorUnmountTimerRef.current = setTimeout(() => {\\n setKeepEditorMounted(false);\\n editorUnmountTimerRef.current = null;\\n }, EDITOR_UNMOUNT_GRACE_MS);\\n return () => {\\n if (editorUnmountTimerRef.current) {\\n clearTimeout(editorUnmountTimerRef.current);\\n editorUnmountTimerRef.current = null;\\n }\\n };\\n }, [isEditing]);\\n\\n useEffect(() => {\\n let canceled = false;\\n const controller = new AbortController();\\n\\n const load = async () => {\\n setContentError(null);\\n setContentLoading(initialContent == null);\\n setContent(initialContent);\\n setShowContentLoadingIndicator(false);\\n\\n if (initialContent != null) {\\n return;\\n }\\n\\n if (contentLoadingTimerRef.current) {\\n clearTimeout(contentLoadingTimerRef.current);\\n contentLoadingTimerRef.current = null;\\n }\\n contentLoadingTimerRef.current = setTimeout(() => {\\n if (!canceled) {\\n setShowContentLoadingIndicator(true);\\n }\\n }, CONTENT_LOADING_DELAY_MS);\\n\\n try {\\n const response = await fetch(\\n `/api/documents/content?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,\\n {\\n method: \\\"GET\\\",\\n credentials: \\\"include\\\",\\n signal: controller.signal,\\n },\\n );\\n if (!response.ok) {\\n const payload = await response.json().catch(() => ({}));\\n throw new Error(payload?.error ?? \\\"加载页面内容失败\\\");\\n }\\n const payload = (await response.json()) as {\\n content?: unknown;\\n revision?: number | null;\\n conflictDetectionKey?: string | null;\\n pageSubtree?: PageSubtreeProjection | null;\\n };\\n if (canceled) return;\\n setContent(payload.content ?? null);\\n setServerContentSnapshot(payload.content ?? null);\\n setContentRevision(\\n typeof payload.revision === \\\"number\\\" && Number.isInteger(payload.revision)\\n ? payload.revision\\n : 0,\\n );\\n setConflictDetectionKey(\\n typeof payload.conflictDetectionKey === \\\"string\\\" && payload.conflictDetectionKey.trim()\\n ? payload.conflictDetectionKey\\n : `${documentId}:0`,\\n );\\n setServerPageSubtreeSnapshot(payload.pageSubtree ?? null);\\n setServerPageSubtreeTitle(\\n typeof payload.pageSubtree?.rootNode.metadata.title === \\\"string\\\" &&\\n payload.pageSubtree.rootNode.metadata.title.trim()\\n ? payload.pageSubtree.rootNode.metadata.title\\n : title ?? \\\"无标题\\\",\\n );\\n } catch (error) {\\n if (canceled) return;\\n if ((error as { name?: string })?.name === \\\"AbortError\\\") return;\\n setContentError(error instanceof Error ? error.message : \\\"加载页面内容失败\\\");\\n } finally {\\n if (!canceled) {\\n setContentLoading(false);\\n setShowContentLoadingIndicator(false);\\n }\\n if (contentLoadingTimerRef.current) {\\n clearTimeout(contentLoadingTimerRef.current);\\n contentLoadingTimerRef.current = null;\\n }\\n }\\n };\\n\\n void load();\\n\\n return () => {\\n canceled = true;\\n controller.abort();\\n if (contentLoadingTimerRef.current) {\\n clearTimeout(contentLoadingTimerRef.current);\\n contentLoadingTimerRef.current = null;\\n }\\n };\\n }, [documentId, initialContent, contentReloadKey, title, workspaceId]);\\n\\n const persistTitle = useCallback(\\n async (nextTitle: string) => {\\n if (readOnly) return;\\n const payload = nextTitle.trim() || \\\"无标题\\\";\\n try {\\n await renameDocumentCommand({ documentId, workspaceId, title: payload });\\n } catch (error) {\\n console.error(\\\"更新页面标题失败\\\", error);\\n }\\n },\\n [documentId, readOnly, workspaceId],\\n );\\n\\n const debouncedPersistTitle = useDebouncedCallback((value: string) => {\\n void persistTitle(value);\\n }, 600);\\n\\n const handleTitleChange = (event: ChangeEvent) => {\\n if (!canEditDocument) return;\\n const value = event.target.value;\\n setPageTitle(value);\\n debouncedPersistTitle(value);\\n };\\n\\n const handleTitleBlur = () => {\\n if (!canEditDocument) return;\\n void persistTitle(pageTitle);\\n };\\n\\n const handleTitleKeyDown = (event: ReactKeyboardEvent) => {\\n if (event.key === \\\"Enter\\\") {\\n event.preventDefault();\\n event.currentTarget.blur();\\n }\\n };\\n\\n const persistOptions = useCallback(\\n async (patch: Partial) => {\\n if (readOnly) return;\\n try {\\n const response = await fetch(\\\"/api/documents/options\\\", {\\n method: \\\"POST\\\",\\n headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\\n body: JSON.stringify({ documentId, workspaceId, options: patch }),\\n });\\n if (!response.ok) {\\n const payload = await response.json().catch(() => null);\\n console.error(payload?.error ?? \\\"更新页面选项失败\\\");\\n }\\n } catch (error) {\\n console.error(error);\\n }\\n },\\n [documentId, readOnly, workspaceId],\\n );\\n\\n const toggleOption = useCallback(\\n (key: BooleanPageOptionKey) => {\\n if (readOnly) return;\\n setOptions((prev) => {\\n const nextValue = !prev[key];\\n const next = { ...prev, [key]: nextValue };\\n void persistOptions({ [key]: nextValue } as Partial);\\n return next;\\n });\\n },\\n [persistOptions, readOnly],\\n );\\n\\n const setOptionPatch = useCallback(\\n (patch: Partial) => {\\n if (readOnly) return;\\n setOptions((prev) => {\\n const next = { ...prev, ...patch };\\n void persistOptions(patch);\\n return next;\\n });\\n },\\n [persistOptions, readOnly],\\n );\\n\\n const closeToc = useCallback(() => {\\n if (readOnly) return;\\n setOptions((prev) => {\\n if (!prev.showToc) return prev;\\n const next = { ...prev, showToc: false };\\n void persistOptions({ showToc: false });\\n return next;\\n });\\n }, [persistOptions, readOnly]);\\n\\n const handleSetPageFont = useCallback(\\n (font: PageFont) => {\\n setOptionPatch({ pageFont: font });\\n },\\n [setOptionPatch],\\n );\\n\\n const handleSetLayoutDensity = useCallback(\\n (density: PageLayoutDensity) => {\\n setOptionPatch({ layoutDensity: density });\\n },\\n [setOptionPatch],\\n );\\n\\n const handleSetEmbedDefaultToCursor = useCallback(() => {\\n if (!canEditDocument) return;\\n if (!isEditing) {\\n setIsEditing(true);\\n return;\\n }\\n const blockId = editorBridge?.getCursorBlockId?.() ?? null;\\n if (!blockId) {\\n window.alert(\\\"未找到当前光标所在块,请先在编辑器中点击任意块\\\");\\n return;\\n }\\n setOptionPatch({ embedDefaultBlockId: blockId });\\n window.alert(\\\"已设置“嵌入默认位置”\\\");\\n }, [canEditDocument, editorBridge, isEditing, setOptionPatch]);\\n\\n const handleClearEmbedDefault = useCallback(() => {\\n if (readOnly) return;\\n setOptionPatch({ embedDefaultBlockId: null });\\n window.alert(\\\"已清除“嵌入默认位置”\\\");\\n }, [readOnly, setOptionPatch]);\\n\\n useEffect(() => {\\n const onKeyDown = (event: KeyboardEvent) => {\\n const root = pageRootRef.current;\\n if (root) {\\n const target = event.target;\\n if (target && target instanceof Node && !root.contains(target)) {\\n return;\\n }\\n }\\n const ctrlOrMeta = event.ctrlKey || event.metaKey;\\n if (!ctrlOrMeta) return;\\n if (!event.shiftKey) return;\\n const key = String(event.key ?? \\\"\\\").toLowerCase();\\n if (key === \\\"l\\\") {\\n event.preventDefault();\\n toggleOption(\\\"showToc\\\");\\n } else if (key === \\\"c\\\") {\\n event.preventDefault();\\n toggleOption(\\\"protectEditing\\\");\\n }\\n };\\n window.addEventListener(\\\"keydown\\\", onKeyDown);\\n return () => window.removeEventListener(\\\"keydown\\\", onKeyDown);\\n }, [toggleOption]);\\n\\n const buildDocumentUrl = useCallback((id: string): string => {\\n if (typeof window === \\\"undefined\\\" || !window.location) {\\n return `/documents/${id}`;\\n }\\n return `${window.location.origin}/documents/${id}`;\\n }, []);\\n\\n const copyText = useCallback(async (text: string, successMessage: string) => {\\n if (typeof navigator !== \\\"undefined\\\" && navigator.clipboard) {\\n try {\\n await navigator.clipboard.writeText(text);\\n window.alert(successMessage);\\n return;\\n } catch {\\n }\\n }\\n window.prompt(\\\"复制失败,请手动复制内容\\\", text);\\n }, []);\\n\\n const handleCopyPageLink = useCallback(\\n async (includeTitle: boolean) => {\\n const url = buildDocumentUrl(documentId);\\n if (includeTitle) {\\n const text = `${pageTitle || \\\"无标题\\\"}\\\\n${url}`;\\n await copyText(text, \\\"标题 + 链接已复制\\\");\\n return;\\n }\\n await copyText(url, \\\"页面链接已复制\\\");\\n },\\n [buildDocumentUrl, copyText, documentId, pageTitle],\\n );\\n\\n const handleCopyPageReference = useCallback(\\n async (mode: \\\"inline\\\" | \\\"embed\\\") => {\\n const template = mode === \\\"inline\\\" ? `((${documentId}))` : `{{${documentId}}}`;\\n await copyText(template, mode === \\\"inline\\\" ? \\\"行内引用已复制\\\" : \\\"嵌入引用已复制\\\");\\n },\\n [copyText, documentId],\\n );\\n\\n const handleUndo = useCallback(() => {\\n if (!isEditing) {\\n setIsEditing(true);\\n return;\\n }\\n editorBridge?.undo?.();\\n }, [editorBridge, isEditing]);\\n\\n const handleRedo = useCallback(() => {\\n if (!isEditing) {\\n setIsEditing(true);\\n return;\\n }\\n editorBridge?.redo?.();\\n }, [editorBridge, isEditing]);\\n\\n const handleDeletePage = useCallback(async () => {\\n if (readOnly) return;\\n const ok = window.confirm(\\\"确定删除该页面吗?删除后会进入垃圾桶。\\\");\\n if (!ok) return;\\n try {\\n await deleteDocumentCommand({ documentId, workspaceId });\\n } catch (error) {\\n const payload = error instanceof Error ? error.message : null;\\n window.alert(payload ?? \\\"删除失败\\\");\\n return;\\n }\\n router.push(\\\"/\\\");\\n router.refresh();\\n }, [documentId, readOnly, router, workspaceId]);\\n\\n const handleOpenMoveEmbed = useCallback(() => {\\n if (readOnly) return;\\n openMoveEmbedPicker({\\n workspaceId,\\n defaultMode: \\\"move\\\",\\n modes: [\\\"move\\\", \\\"embed\\\"],\\n allowRoot: true,\\n excludeIds: [documentId],\\n onPick: async (mode, targetId) => {\\n if (mode === \\\"move\\\") {\\n try {\\n await moveDocumentCommand({\\n documentId,\\n parentId: targetId ?? null,\\n position: 999999,\\n workspaceId,\\n });\\n } catch (error) {\\n window.alert(error instanceof Error ? error.message : \\\"移动失败\\\");\\n return;\\n }\\n window.alert(\\\"移动成功\\\");\\n router.refresh();\\n return;\\n }\\n\\n if (!targetId) {\\n window.alert(\\\"请选择目标页面\\\");\\n return;\\n }\\n\\n try {\\n await embedDocumentCommand({\\n sourceId: documentId,\\n targetId,\\n });\\n } catch (error) {\\n const message = error instanceof Error ? error.message : \\\"嵌入失败\\\";\\n window.alert(message);\\n return;\\n }\\n\\n window.alert(\\\"已嵌入到目标页面\\\");\\n },\\n });\\n }, [documentId, openMoveEmbedPicker, readOnly, router, workspaceId]);\\n\\n const handleAddToTemplates = useCallback(async () => {\\n if (readOnly) return;\\n const ok = window.confirm(\\\"将该页面添加为模板?\\\");\\n if (!ok) return;\\n const resp = await fetch(\\\"/api/documents/template\\\", {\\n method: \\\"POST\\\",\\n headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\\n body: JSON.stringify({ documentId, isTemplate: true }),\\n });\\n if (!resp.ok) {\\n const payload = await resp.json().catch(() => ({}));\\n window.alert(payload?.error ?? \\\"设置模板失败\\\");\\n return;\\n }\\n window.alert(\\\"已添加为模板\\\");\\n router.refresh();\\n }, [documentId, readOnly, router]);\\n\\n const formattedUpdatedAt = useMemo(() => {\\n if (!updatedAt) return \\\"\\\";\\n const date = new Date(updatedAt);\\n if (Number.isNaN(date.getTime())) return \\\"\\\";\\n return new Intl.DateTimeFormat(\\\"zh-CN\\\", {\\n timeZone: \\\"Asia/Shanghai\\\",\\n year: \\\"numeric\\\",\\n month: \\\"numeric\\\",\\n day: \\\"numeric\\\",\\n hour: \\\"2-digit\\\",\\n minute: \\\"2-digit\\\",\\n second: \\\"2-digit\\\",\\n hour12: false,\\n }).format(date);\\n }, [updatedAt]);\\n\\n const handleExport = useCallback(() => {\\n if (disableDownload) {\\n window.alert(\\\"该页面已禁止下载\\\");\\n return;\\n }\\n const latest = history[0];\\n const exportBlocks = latest?.blocks ?? latestBlocksRef.current;\\n if (!exportBlocks) {\\n window.alert(\\\"暂无可导出的内容\\\");\\n return;\\n }\\n const payload = JSON.stringify(exportBlocks, null, 2);\\n const blob = new Blob([payload], { type: \\\"application/json\\\" });\\n const url = URL.createObjectURL(blob);\\n const anchor = document.createElement(\\\"a\\\");\\n anchor.href = url;\\n anchor.download = `${title ?? \\\"未命名页面\\\"}-${new Date().toISOString()}.json`;\\n anchor.click();\\n URL.revokeObjectURL(url);\\n }, [disableDownload, history, title]);\\n\\n const pageRootClass = cn(\\n \\\"flex h-full overflow-hidden bg-wolai-bg\\\",\\n options.pageFont === \\\"song\\\" && \\\"wolai-page-font-song\\\",\\n options.pageFont === \\\"kai\\\" && \\\"wolai-page-font-kai\\\",\\n options.layoutDensity === \\\"compact\\\" && \\\"wolai-page-density-compact\\\",\\n options.layoutDensity === \\\"spacious\\\" && \\\"wolai-page-density-spacious\\\",\\n options.smallText && \\\"wolai-small-text\\\",\\n options.hideChildPages && \\\"wolai-hide-child-pages\\\",\\n );\\n const pageSubtree = useMemo(() => {\\n const hasServerPageSubtree = Boolean(serverPageSubtreeSnapshot);\\n const titleUnchanged = pageTitle === serverPageSubtreeTitle;\\n const contentUnchanged = content === serverContentSnapshot;\\n\\n if (hasServerPageSubtree && titleUnchanged && contentUnchanged) {\\n return serverPageSubtreeSnapshot;\\n }\\n return null;\\n }, [\\n content,\\n pageTitle,\\n serverContentSnapshot,\\n serverPageSubtreeSnapshot,\\n serverPageSubtreeTitle,\\n ]);\\n const readViewTocEntries = useMemo(\\n () =>\\n (pageSubtree?.outline ?? [])\\n .filter((entry) => typeof entry.anchorBlockId === \\\"string\\\" && entry.anchorBlockId.trim())\\n .map(({ anchorBlockId, level, numbering, title: entryTitle }) => ({\\n id: anchorBlockId as string,\\n level,\\n numbering,\\n title: entryTitle,\\n })),\\n [pageSubtree],\\n );\\n const inspectorCanUseEditorBridge = canEditDocument && isEditing;\\n\\n const jumpToHeading = useCallback((headingId: string) => {\\n const targetRoot = !isEditing ? readViewRootRef.current : pageRootRef.current;\\n const target = targetRoot?.querySelector(`[data-id=\\\"${headingId}\\\"]`);\\n if (target) {\\n target.scrollIntoView({ behavior: \\\"smooth\\\", block: \\\"center\\\" });\\n }\\n }, [isEditing]);\\n\\n const handleSnapshot = useCallback((payload: { blocks: Json; stats: DocumentStats }) => {\\n setContent(payload.blocks);\\n latestBlocksRef.current = payload.blocks;\\n setHistory((prev) => {\\n const now = Date.now();\\n if (prev.length > 0 && now - prev[0].timestamp < 4000) {\\n return prev;\\n }\\n const snapshot: DocumentSnapshot = {\\n id: `${now}-${Math.random().toString(36).slice(2, 8)}`,\\n timestamp: now,\\n blocks: payload.blocks,\\n stats: payload.stats,\\n };\\n return [snapshot, ...prev].slice(0, 15);\\n });\\n }, []);\\n\\n const persistStatsRequest = useCallback((next: DocumentStats) => {\\n void fetch(\\\"/api/documents/stats\\\", {\\n method: \\\"POST\\\",\\n headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\\n body: JSON.stringify({ documentId, workspaceId, stats: next }),\\n }).catch((error) => console.error(error));\\n }, [documentId, workspaceId]);\\n\\n const persistStats = useDebouncedCallback(persistStatsRequest, 1500);\\n\\n const handleStatsChange = useCallback(\\n (nextStats: DocumentStats) => {\\n setStats(nextStats);\\n persistStats(nextStats);\\n },\\n [persistStats],\\n );\\n\\n const handleRestoreSnapshot = useCallback(\\n (snapshot: DocumentSnapshot) => {\\n if (!isEditing || !editorBridge) {\\n pendingRestoreSnapshotRef.current = snapshot;\\n setIsEditing(true);\\n return;\\n }\\n editorBridge.replaceWithSnapshot(snapshot.blocks);\\n setHistoryOpen(false);\\n },\\n [editorBridge, isEditing],\\n );\\n\\n const handleEnterEditMode = useCallback(() => {\\n if (!canEditDocument) return;\\n setIsEditing(true);\\n }, [canEditDocument]);\\n\\n const handleExitEditMode = useCallback(() => {\\n setIsEditing(false);\\n }, []);\\n\\n useEffect(() => {\\n if (!isEditing) return;\\n if (!editorBridge) return;\\n const pendingSnapshot = pendingRestoreSnapshotRef.current;\\n if (!pendingSnapshot) return;\\n pendingRestoreSnapshotRef.current = null;\\n editorBridge.replaceWithSnapshot(pendingSnapshot.blocks);\\n setHistoryOpen(false);\\n }, [editorBridge, isEditing]);\\n return (\\n \\n
\\n
\\n
\\n
\\n
\\n {isEditing && canEditDocument ? (\\n
\\n \\n
\\n ) : (\\n

\\n {pageTitle || \\\"无标题\\\"}\\n

\\n )}\\n {readOnly ? (\\n

该页面为只读共享,无法编辑。

\\n ) : options.protectEditing && isEditing ? (\\n

当前页面已开启编辑保护,关闭后方可修改内容。

\\n ) : !isEditing && canEditDocument ? (\\n

当前为阅读态,编辑器仅在进入编辑后挂载。

\\n ) : null}\\n

最近更新:{formattedUpdatedAt}

\\n
\\n {canEditDocument && (\\n
\\n {isEditing ? (\\n \\n ) : (\\n \\n )}\\n
\\n )}\\n
\\n
\\n
\\n {contentLoading ? (\\n showContentLoadingIndicator ? (\\n
\\n 页面内容加载中...\\n
\\n ) : (\\n
\\n )\\n ) : contentError ? (\\n
\\n
{contentError}
\\n {\\n setContentError(null);\\n setContentLoading(true);\\n setContentReloadKey((prev) => prev + 1);\\n }}\\n >\\n 重试\\n \\n
\\n ) : (\\n
\\n {keepEditorMounted && (\\n
\\n {\\n setContentRevision(meta.revision);\\n setConflictDetectionKey(meta.conflictDetectionKey);\\n }}\\n />\\n
\\n )}\\n {!isEditing && (\\n
\\n \\n \\n
\\n )}\\n
\\n )}\\n \\n
\\n
\\n {showInspector && (\\n setHistoryOpen(true)}\\n onOpenComments={() => openCommentsForPage({ workspaceId, documentId })}\\n onUndo={canEditDocument ? handleUndo : undefined}\\n onRedo={canEditDocument ? handleRedo : undefined}\\n onDeletePage={canEditDocument ? handleDeletePage : undefined}\\n onOpenMoveEmbedPicker={canEditDocument ? handleOpenMoveEmbed : undefined}\\n onCopyPageLink={handleCopyPageLink}\\n onCopyPageReference={handleCopyPageReference}\\n onAddToTemplates={canEditDocument ? handleAddToTemplates : undefined}\\n />\\n )}\\n
\\n \\n \\n \\n latestBlocksRef.current}\\n getLatestPageSubtree={() => pageSubtree}\\n />\\n \\n );\\n}\\n\",\n \"structuredPatch\": [\n {\n \"oldStart\": 1,\n \"oldLines\": 1011,\n \"newStart\": 1,\n \"newLines\": 1011,\n \"lines\": [\n \"-\\\"use client\\\";\",\n \"-\",\n \"-import dynamic from \\\"next/dynamic\\\";\",\n \"-import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent as ReactKeyboardEvent } from \\\"react\\\";\",\n \"-import type { BooleanPageOptionKey, DocumentStats, PageOptionsState, PageFont, PageLayoutDensity } from \\\"@/types/page-options\\\";\",\n \"-import { useEditorBridgeStore } from \\\"@/store/editor-bridge\\\";\",\n \"-import { usePageLayoutStore } from \\\"@/store/page-layout\\\";\",\n \"-import { useCurrentDocumentStore } from \\\"@/store/current-document\\\";\",\n \"-import type { Json } from \\\"@/types/supabase\\\";\",\n \"-import type { DocumentSnapshot } from \\\"@/types/document\\\";\",\n \"-import { useDebouncedCallback } from \\\"@/hooks/use-debounced-callback\\\";\",\n \"-import { ImagePickerProvider } from \\\"@/components/media/image-picker-context\\\";\",\n \"-import { useRouter } from \\\"next/navigation\\\";\",\n \"-import { DocumentAiAgentPanel } from \\\"./DocumentAiAgentPanel\\\";\",\n \"-import { CONTENT_LOADING_DELAY_MS } from \\\"@/lib/constants\\\";\",\n \"-import { useCommentsUiStore } from \\\"@/store/comments-ui\\\";\",\n \"-import { useAppPreferencesStore } from \\\"@/store/app-preferences\\\";\",\n \"-import { useMoveEmbedPickerStore } from \\\"@/store/move-embed-picker\\\";\",\n \"-import { cn } from \\\"@/lib/utils\\\";\",\n \"-import { Button } from \\\"@/components/ui/button\\\";\",\n \"-import { DocumentToc } from \\\"@/components/editor/document-toc\\\";\",\n \"-import { DocumentReadView } from \\\"@/components/editor/document-read-view\\\";\",\n \"-import { extractPageBlocks, type PageSubtreeProjection } from \\\"@/lib/documents/page-subtree\\\";\",\n \"-import {\",\n \"- deleteDocumentCommand,\",\n \"- embedDocumentCommand,\",\n \"- moveDocumentCommand,\",\n \"- renameDocumentCommand,\",\n \"-} from \\\"@/lib/documents/tree-command-client\\\";\",\n \"-import { EditorHost } from \\\"@/components/editor/editor-host\\\";\",\n \"-\",\n \"-const PageOptionsSidebar = dynamic(\",\n \"- () => import(\\\"@/components/editor/page-options-sidebar\\\").then((mod) => mod.PageOptionsSidebar),\",\n \"- { ssr: false },\",\n \"-);\",\n \"-\",\n \"-const PageBacklinksPanel = dynamic(\",\n \"- () => import(\\\"@/components/editor/page-backlinks-panel\\\").then((mod) => mod.PageBacklinksPanel),\",\n \"- {\",\n \"- ssr: false,\",\n \"- loading: () => null,\",\n \"- },\",\n \"-);\",\n \"-\",\n \"-const DocumentHistoryDrawer = dynamic(\",\n \"- () => import(\\\"@/components/editor/document-history-drawer\\\").then((mod) => mod.DocumentHistoryDrawer),\",\n \"- {\",\n \"- ssr: false,\",\n \"- loading: () => null,\",\n \"- },\",\n \"-);\",\n \"-\",\n \"-const MoveEmbedPickerHost = dynamic(\",\n \"- () => import(\\\"@/components/documents/move-embed-picker-host\\\").then((mod) => mod.MoveEmbedPickerHost),\",\n \"- {\",\n \"- ssr: false,\",\n \"- loading: () => null,\",\n \"- },\",\n \"-);\",\n \"-\",\n \"-const DocumentCommentsDrawer = dynamic(\",\n \"- () => import(\\\"@/components/editor/document-comments-drawer\\\").then((mod) => mod.DocumentCommentsDrawer),\",\n \"- {\",\n \"- ssr: false,\",\n \"- loading: () => null,\",\n \"- },\",\n \"-);\",\n \"-\",\n \"-export interface DocumentContentProps {\",\n \"- documentId: string;\",\n \"- workspaceId: string;\",\n \"- title: string | null;\",\n \"- updatedAt: string | null;\",\n \"- initialContent: unknown;\",\n \"- initialContentRevision?: number | null;\",\n \"- initialConflictDetectionKey?: string | null;\",\n \"- initialPageSubtree?: PageSubtreeProjection | null;\",\n \"- initialOptions: PageOptionsState;\",\n \"- initialStats: DocumentStats | null;\",\n \"- openTableId?: string | null;\",\n \"- autoEnterEdit?: boolean;\",\n \"- readOnly?: boolean;\",\n \"- disableDownload?: boolean;\",\n \"- disableCopy?: boolean;\",\n \"-}\",\n \"-\",\n \"-const defaultOptions: PageOptionsState = {\",\n \"- wideLayout: false,\",\n \"- smallText: false,\",\n \"- showHeadingNumbers: true,\",\n \"- showToc: false,\",\n \"- showStructure: false,\",\n \"- protectEditing: false,\",\n \"- showWordCount: true,\",\n \"- collapseBacklinks: false,\",\n \"- pageFont: \\\"default\\\",\",\n \"- layoutDensity: \\\"normal\\\",\",\n \"- hideChildPages: false,\",\n \"- showBlockRefCount: false,\",\n \"- embedDefaultBlockId: null,\",\n \"-};\",\n \"-const defaultStats: DocumentStats = { wordCount: 0, characterCount: 0, blockCount: 0, todoTotal: 0, todoDone: 0 };\",\n \"-const EDITOR_UNMOUNT_GRACE_MS = 1000;\",\n \"-\",\n \"-export function DocumentContent({\",\n \"- documentId,\",\n \"- workspaceId,\",\n \"- title,\",\n \"- updatedAt,\",\n \"- initialContent,\",\n \"- initialContentRevision = null,\",\n \"- initialConflictDetectionKey = null,\",\n \"- initialPageSubtree = null,\",\n \"- initialOptions,\",\n \"- initialStats,\",\n \"- openTableId,\",\n \"- autoEnterEdit = false,\",\n \"- readOnly = false,\",\n \"- disableDownload = false,\",\n \"- disableCopy = false,\",\n \"-}: DocumentContentProps) {\",\n \"- const setCurrentDocument = useCurrentDocumentStore((state) => state.setCurrent);\",\n \"- const clearIfMatch = useCurrentDocumentStore((state) => state.clearIfMatch);\",\n \"- const canEditDocument = !readOnly;\",\n \"- const [options, setOptions] = useState(initialOptions ?? defaultOptions);\",\n \"- const [stats, setStats] = useState(initialStats ?? defaultStats);\",\n \"- const [history, setHistory] = useState([]);\",\n \"- const [historyOpen, setHistoryOpen] = useState(false);\",\n \"- const editorBridge = useEditorBridgeStore((state) => state.bridge);\",\n \"- const router = useRouter();\",\n \"- const showInspector = usePageLayoutStore((state) => state.showInspector);\",\n \"- const openCommentsForPage = useCommentsUiStore((s) => s.openForPage);\",\n \"- const spellCheck = useAppPreferencesStore((s) => s.spellCheck);\",\n \"- const openMoveEmbedPicker = useMoveEmbedPickerStore((s) => s.openPicker);\",\n \"- const [pageTitle, setPageTitle] = useState(title ?? \\\"无标题\\\");\",\n \"- const [content, setContent] = useState(initialContent);\",\n \"- const [serverContentSnapshot, setServerContentSnapshot] = useState(initialContent);\",\n \"- const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState(\",\n \"- initialPageSubtree,\",\n \"- );\",\n \"- const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState(title ?? \\\"无标题\\\");\",\n \"- const [contentRevision, setContentRevision] = useState(initialContentRevision);\",\n \"- const [conflictDetectionKey, setConflictDetectionKey] = useState(initialConflictDetectionKey);\",\n \"- const [contentLoading, setContentLoading] = useState(() => initialContent == null);\",\n \"- const [contentError, setContentError] = useState(null);\",\n \"- const [contentReloadKey, setContentReloadKey] = useState(0);\",\n \"- const [showContentLoadingIndicator, setShowContentLoadingIndicator] = useState(false);\",\n \"- const shouldStartEditing = (Boolean((openTableId ?? \\\"\\\").trim()) || autoEnterEdit) && !readOnly;\",\n \"- const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\",\n \"- const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\",\n \"- const contentLoadingTimerRef = useRef | null>(null);\",\n \"- const editorUnmountTimerRef = useRef | null>(null);\",\n \"- const pendingOpenTableRef = useRef(null);\",\n \"- const latestBlocksRef = useRef(null);\",\n \"- const pageRootRef = useRef(null);\",\n \"- const readViewRootRef = useRef(null);\",\n \"- const pendingRestoreSnapshotRef = useRef(null);\",\n \"- const lastCopyBlockedAtRef = useRef(0);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setCurrentDocument(documentId, readOnly, disableDownload, disableCopy);\",\n \"- return () => {\",\n \"- clearIfMatch(documentId);\",\n \"- };\",\n \"- }, [clearIfMatch, disableCopy, disableDownload, documentId, readOnly, setCurrentDocument]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- if (!disableCopy) return;\",\n \"-\",\n \"- const toElement = (node: Node | null): Element | null => {\",\n \"- if (!node) return null;\",\n \"- if (node.nodeType === Node.TEXT_NODE) {\",\n \"- return node.parentElement;\",\n \"- }\",\n \"- return node instanceof Element ? node : null;\",\n \"- };\",\n \"-\",\n \"- const isEventInsidePage = () => {\",\n \"- const root = pageRootRef.current;\",\n \"- if (!root) return false;\",\n \"- const selection = typeof window !== \\\"undefined\\\" ? window.getSelection() : null;\",\n \"- const anchor = selection?.anchorNode ?? null;\",\n \"- const focus = selection?.focusNode ?? null;\",\n \"- const anchorEl = toElement(anchor);\",\n \"- const focusEl = toElement(focus);\",\n \"- return Boolean((anchorEl && root.contains(anchorEl)) || (focusEl && root.contains(focusEl)));\",\n \"- };\",\n \"-\",\n \"- const notifyBlocked = () => {\",\n \"- const now = Date.now();\",\n \"- if (now - lastCopyBlockedAtRef.current < 1200) return;\",\n \"- lastCopyBlockedAtRef.current = now;\",\n \"- window.alert(\\\"该页面已禁止复制\\\");\",\n \"- };\",\n \"-\",\n \"- const onCopy = (event: ClipboardEvent) => {\",\n \"- if (!isEventInsidePage()) return;\",\n \"- event.preventDefault();\",\n \"- event.stopPropagation();\",\n \"- notifyBlocked();\",\n \"- };\",\n \"-\",\n \"- const onCut = (event: ClipboardEvent) => {\",\n \"- if (!isEventInsidePage()) return;\",\n \"- event.preventDefault();\",\n \"- event.stopPropagation();\",\n \"- notifyBlocked();\",\n \"- };\",\n \"-\",\n \"- const onKeyDown = (event: KeyboardEvent) => {\",\n \"- if (!isEventInsidePage()) return;\",\n \"- const key = String(event.key ?? \\\"\\\").toLowerCase();\",\n \"- const ctrlOrMeta = event.ctrlKey || event.metaKey;\",\n \"- if (!ctrlOrMeta) return;\",\n \"- if (key === \\\"c\\\" || key === \\\"x\\\" || key === \\\"insert\\\") {\",\n \"- event.preventDefault();\",\n \"- event.stopPropagation();\",\n \"- notifyBlocked();\",\n \"- }\",\n \"- };\",\n \"-\",\n \"- document.addEventListener(\\\"copy\\\", onCopy, true);\",\n \"- document.addEventListener(\\\"cut\\\", onCut, true);\",\n \"- document.addEventListener(\\\"keydown\\\", onKeyDown, true);\",\n \"- return () => {\",\n \"- document.removeEventListener(\\\"copy\\\", onCopy, true);\",\n \"- document.removeEventListener(\\\"cut\\\", onCut, true);\",\n \"- document.removeEventListener(\\\"keydown\\\", onKeyDown, true);\",\n \"- };\",\n \"- }, [disableCopy]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- const tableId = (openTableId ?? \\\"\\\").trim();\",\n \"- if (!tableId) return;\",\n \"- if (!editorBridge?.openTableFullScreen) return;\",\n \"- if (pendingOpenTableRef.current === tableId) return;\",\n \"- pendingOpenTableRef.current = tableId;\",\n \"-\",\n \"- editorBridge.openTableFullScreen(tableId);\",\n \"-\",\n \"- if (typeof window !== \\\"undefined\\\") {\",\n \"- try {\",\n \"- const url = new URL(window.location.href);\",\n \"- url.searchParams.delete(\\\"openTableId\\\");\",\n \"- window.history.replaceState({}, \\\"\\\", url.toString());\",\n \"- } catch {\",\n \"- router.replace(`/documents/${documentId}`);\",\n \"- }\",\n \"- }\",\n \"- }, [documentId, editorBridge, openTableId, router]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setPageTitle(title ?? \\\"无标题\\\");\",\n \"- }, [title]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setServerPageSubtreeTitle(title ?? \\\"无标题\\\");\",\n \"- }, [title]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setServerPageSubtreeSnapshot(initialPageSubtree);\",\n \"- }, [initialPageSubtree]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setOptions(initialOptions ?? defaultOptions);\",\n \"- }, [initialOptions]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setStats(initialStats ?? defaultStats);\",\n \"- }, [initialStats]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setContentRevision(initialContentRevision);\",\n \"- }, [initialContentRevision]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setConflictDetectionKey(initialConflictDetectionKey);\",\n \"- }, [initialConflictDetectionKey]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setServerContentSnapshot(initialContent);\",\n \"- }, [initialContent]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- const nextBlocks = extractPageBlocks(content);\",\n \"- latestBlocksRef.current = nextBlocks.length > 0 ? (nextBlocks as Json) : null;\",\n \"- }, [content]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- const shouldForceEdit = Boolean((openTableId ?? \\\"\\\").trim()) && canEditDocument;\",\n \"- if (shouldForceEdit) {\",\n \"- setIsEditing(true);\",\n \"- setKeepEditorMounted(true);\",\n \"- }\",\n \"- }, [canEditDocument, openTableId]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- if (isEditing) {\",\n \"- if (editorUnmountTimerRef.current) {\",\n \"- clearTimeout(editorUnmountTimerRef.current);\",\n \"- editorUnmountTimerRef.current = null;\",\n \"- }\",\n \"- setKeepEditorMounted(true);\",\n \"- return;\",\n \"- }\",\n \"- if (editorUnmountTimerRef.current) {\",\n \"- clearTimeout(editorUnmountTimerRef.current);\",\n \"- }\",\n \"- editorUnmountTimerRef.current = setTimeout(() => {\",\n \"- setKeepEditorMounted(false);\",\n \"- editorUnmountTimerRef.current = null;\",\n \"- }, EDITOR_UNMOUNT_GRACE_MS);\",\n \"- return () => {\",\n \"- if (editorUnmountTimerRef.current) {\",\n \"- clearTimeout(editorUnmountTimerRef.current);\",\n \"- editorUnmountTimerRef.current = null;\",\n \"- }\",\n \"- };\",\n \"- }, [isEditing]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- let canceled = false;\",\n \"- const controller = new AbortController();\",\n \"-\",\n \"- const load = async () => {\",\n \"- setContentError(null);\",\n \"- setContentLoading(initialContent == null);\",\n \"- setContent(initialContent);\",\n \"- setShowContentLoadingIndicator(false);\",\n \"-\",\n \"- if (initialContent != null) {\",\n \"- return;\",\n \"- }\",\n \"-\",\n \"- if (contentLoadingTimerRef.current) {\",\n \"- clearTimeout(contentLoadingTimerRef.current);\",\n \"- contentLoadingTimerRef.current = null;\",\n \"- }\",\n \"- contentLoadingTimerRef.current = setTimeout(() => {\",\n \"- if (!canceled) {\",\n \"- setShowContentLoadingIndicator(true);\",\n \"- }\",\n \"- }, CONTENT_LOADING_DELAY_MS);\",\n \"-\",\n \"- try {\",\n \"- const response = await fetch(\",\n \"- `/api/documents/content?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,\",\n \"- {\",\n \"- method: \\\"GET\\\",\",\n \"- credentials: \\\"include\\\",\",\n \"- signal: controller.signal,\",\n \"- },\",\n \"- );\",\n \"- if (!response.ok) {\",\n \"- const payload = await response.json().catch(() => ({}));\",\n \"- throw new Error(payload?.error ?? \\\"加载页面内容失败\\\");\",\n \"- }\",\n \"- const payload = (await response.json()) as {\",\n \"- content?: unknown;\",\n \"- revision?: number | null;\",\n \"- conflictDetectionKey?: string | null;\",\n \"- pageSubtree?: PageSubtreeProjection | null;\",\n \"- };\",\n \"- if (canceled) return;\",\n \"- setContent(payload.content ?? null);\",\n \"- setServerContentSnapshot(payload.content ?? null);\",\n \"- setContentRevision(\",\n \"- typeof payload.revision === \\\"number\\\" && Number.isInteger(payload.revision)\",\n \"- ? payload.revision\",\n \"- : 0,\",\n \"- );\",\n \"- setConflictDetectionKey(\",\n \"- typeof payload.conflictDetectionKey === \\\"string\\\" && payload.conflictDetectionKey.trim()\",\n \"- ? payload.conflictDetectionKey\",\n \"- : `${documentId}:0`,\",\n \"- );\",\n \"- setServerPageSubtreeSnapshot(payload.pageSubtree ?? null);\",\n \"- setServerPageSubtreeTitle(\",\n \"- typeof payload.pageSubtree?.rootNode.metadata.title === \\\"string\\\" &&\",\n \"- payload.pageSubtree.rootNode.metadata.title.trim()\",\n \"- ? payload.pageSubtree.rootNode.metadata.title\",\n \"- : title ?? \\\"无标题\\\",\",\n \"- );\",\n \"- } catch (error) {\",\n \"- if (canceled) return;\",\n \"- if ((error as { name?: string })?.name === \\\"AbortError\\\") return;\",\n \"- setContentError(error instanceof Error ? error.message : \\\"加载页面内容失败\\\");\",\n \"- } finally {\",\n \"- if (!canceled) {\",\n \"- setContentLoading(false);\",\n \"- setShowContentLoadingIndicator(false);\",\n \"- }\",\n \"- if (contentLoadingTimerRef.current) {\",\n \"- clearTimeout(contentLoadingTimerRef.current);\",\n \"- contentLoadingTimerRef.current = null;\",\n \"- }\",\n \"- }\",\n \"- };\",\n \"-\",\n \"- void load();\",\n \"-\",\n \"- return () => {\",\n \"- canceled = true;\",\n \"- controller.abort();\",\n \"- if (contentLoadingTimerRef.current) {\",\n \"- clearTimeout(contentLoadingTimerRef.current);\",\n \"- contentLoadingTimerRef.current = null;\",\n \"- }\",\n \"- };\",\n \"- }, [documentId, initialContent, contentReloadKey, title, workspaceId]);\",\n \"-\",\n \"- const persistTitle = useCallback(\",\n \"- async (nextTitle: string) => {\",\n \"- if (readOnly) return;\",\n \"- const payload = nextTitle.trim() || \\\"无标题\\\";\",\n \"- try {\",\n \"- await renameDocumentCommand({ documentId, workspaceId, title: payload });\",\n \"- } catch (error) {\",\n \"- console.error(\\\"更新页面标题失败\\\", error);\",\n \"- }\",\n \"- },\",\n \"- [documentId, readOnly, workspaceId],\",\n \"- );\",\n \"-\",\n \"- const debouncedPersistTitle = useDebouncedCallback((value: string) => {\",\n \"- void persistTitle(value);\",\n \"- }, 600);\",\n \"-\",\n \"- const handleTitleChange = (event: ChangeEvent) => {\",\n \"- if (!canEditDocument) return;\",\n \"- const value = event.target.value;\",\n \"- setPageTitle(value);\",\n \"- debouncedPersistTitle(value);\",\n \"- };\",\n \"-\",\n \"- const handleTitleBlur = () => {\",\n \"- if (!canEditDocument) return;\",\n \"- void persistTitle(pageTitle);\",\n \"- };\",\n \"-\",\n \"- const handleTitleKeyDown = (event: ReactKeyboardEvent) => {\",\n \"- if (event.key === \\\"Enter\\\") {\",\n \"- event.preventDefault();\",\n \"- event.currentTarget.blur();\",\n \"- }\",\n \"- };\",\n \"-\",\n \"- const persistOptions = useCallback(\",\n \"- async (patch: Partial) => {\",\n \"- if (readOnly) return;\",\n \"- try {\",\n \"- const response = await fetch(\\\"/api/documents/options\\\", {\",\n \"- method: \\\"POST\\\",\",\n \"- headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"- body: JSON.stringify({ documentId, workspaceId, options: patch }),\",\n \"- });\",\n \"- if (!response.ok) {\",\n \"- const payload = await response.json().catch(() => null);\",\n \"- console.error(payload?.error ?? \\\"更新页面选项失败\\\");\",\n \"- }\",\n \"- } catch (error) {\",\n \"- console.error(error);\",\n \"- }\",\n \"- },\",\n \"- [documentId, readOnly, workspaceId],\",\n \"- );\",\n \"-\",\n \"- const toggleOption = useCallback(\",\n \"- (key: BooleanPageOptionKey) => {\",\n \"- if (readOnly) return;\",\n \"- setOptions((prev) => {\",\n \"- const nextValue = !prev[key];\",\n \"- const next = { ...prev, [key]: nextValue };\",\n \"- void persistOptions({ [key]: nextValue } as Partial);\",\n \"- return next;\",\n \"- });\",\n \"- },\",\n \"- [persistOptions, readOnly],\",\n \"- );\",\n \"-\",\n \"- const setOptionPatch = useCallback(\",\n \"- (patch: Partial) => {\",\n \"- if (readOnly) return;\",\n \"- setOptions((prev) => {\",\n \"- const next = { ...prev, ...patch };\",\n \"- void persistOptions(patch);\",\n \"- return next;\",\n \"- });\",\n \"- },\",\n \"- [persistOptions, readOnly],\",\n \"- );\",\n \"-\",\n \"- const closeToc = useCallback(() => {\",\n \"- if (readOnly) return;\",\n \"- setOptions((prev) => {\",\n \"- if (!prev.showToc) return prev;\",\n \"- const next = { ...prev, showToc: false };\",\n \"- void persistOptions({ showToc: false });\",\n \"- return next;\",\n \"- });\",\n \"- }, [persistOptions, readOnly]);\",\n \"-\",\n \"- const handleSetPageFont = useCallback(\",\n \"- (font: PageFont) => {\",\n \"- setOptionPatch({ pageFont: font });\",\n \"- },\",\n \"- [setOptionPatch],\",\n \"- );\",\n \"-\",\n \"- const handleSetLayoutDensity = useCallback(\",\n \"- (density: PageLayoutDensity) => {\",\n \"- setOptionPatch({ layoutDensity: density });\",\n \"- },\",\n \"- [setOptionPatch],\",\n \"- );\",\n \"-\",\n \"- const handleSetEmbedDefaultToCursor = useCallback(() => {\",\n \"- if (!canEditDocument) return;\",\n \"- if (!isEditing) {\",\n \"- setIsEditing(true);\",\n \"- return;\",\n \"- }\",\n \"- const blockId = editorBridge?.getCursorBlockId?.() ?? null;\",\n \"- if (!blockId) {\",\n \"- window.alert(\\\"未找到当前光标所在块,请先在编辑器中点击任意块\\\");\",\n \"- return;\",\n \"- }\",\n \"- setOptionPatch({ embedDefaultBlockId: blockId });\",\n \"- window.alert(\\\"已设置“嵌入默认位置”\\\");\",\n \"- }, [canEditDocument, editorBridge, isEditing, setOptionPatch]);\",\n \"-\",\n \"- const handleClearEmbedDefault = useCallback(() => {\",\n \"- if (readOnly) return;\",\n \"- setOptionPatch({ embedDefaultBlockId: null });\",\n \"- window.alert(\\\"已清除“嵌入默认位置”\\\");\",\n \"- }, [readOnly, setOptionPatch]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- const onKeyDown = (event: KeyboardEvent) => {\",\n \"- const root = pageRootRef.current;\",\n \"- if (root) {\",\n \"- const target = event.target;\",\n \"- if (target && target instanceof Node && !root.contains(target)) {\",\n \"- return;\",\n \"- }\",\n \"- }\",\n \"- const ctrlOrMeta = event.ctrlKey || event.metaKey;\",\n \"- if (!ctrlOrMeta) return;\",\n \"- if (!event.shiftKey) return;\",\n \"- const key = String(event.key ?? \\\"\\\").toLowerCase();\",\n \"- if (key === \\\"l\\\") {\",\n \"- event.preventDefault();\",\n \"- toggleOption(\\\"showToc\\\");\",\n \"- } else if (key === \\\"c\\\") {\",\n \"- event.preventDefault();\",\n \"- toggleOption(\\\"protectEditing\\\");\",\n \"- }\",\n \"- };\",\n \"- window.addEventListener(\\\"keydown\\\", onKeyDown);\",\n \"- return () => window.removeEventListener(\\\"keydown\\\", onKeyDown);\",\n \"- }, [toggleOption]);\",\n \"-\",\n \"- const buildDocumentUrl = useCallback((id: string): string => {\",\n \"- if (typeof window === \\\"undefined\\\" || !window.location) {\",\n \"- return `/documents/${id}`;\",\n \"- }\",\n \"- return `${window.location.origin}/documents/${id}`;\",\n \"- }, []);\",\n \"-\",\n \"- const copyText = useCallback(async (text: string, successMessage: string) => {\",\n \"- if (typeof navigator !== \\\"undefined\\\" && navigator.clipboard) {\",\n \"- try {\",\n \"- await navigator.clipboard.writeText(text);\",\n \"- window.alert(successMessage);\",\n \"- return;\",\n \"- } catch {\",\n \"- }\",\n \"- }\",\n \"- window.prompt(\\\"复制失败,请手动复制内容\\\", text);\",\n \"- }, []);\",\n \"-\",\n \"- const handleCopyPageLink = useCallback(\",\n \"- async (includeTitle: boolean) => {\",\n \"- const url = buildDocumentUrl(documentId);\",\n \"- if (includeTitle) {\",\n \"- const text = `${pageTitle || \\\"无标题\\\"}\\\\n${url}`;\",\n \"- await copyText(text, \\\"标题 + 链接已复制\\\");\",\n \"- return;\",\n \"- }\",\n \"- await copyText(url, \\\"页面链接已复制\\\");\",\n \"- },\",\n \"- [buildDocumentUrl, copyText, documentId, pageTitle],\",\n \"- );\",\n \"-\",\n \"- const handleCopyPageReference = useCallback(\",\n \"- async (mode: \\\"inline\\\" | \\\"embed\\\") => {\",\n \"- const template = mode === \\\"inline\\\" ? `((${documentId}))` : `{{${documentId}}}`;\",\n \"- await copyText(template, mode === \\\"inline\\\" ? \\\"行内引用已复制\\\" : \\\"嵌入引用已复制\\\");\",\n \"- },\",\n \"- [copyText, documentId],\",\n \"- );\",\n \"-\",\n \"- const handleUndo = useCallback(() => {\",\n \"- if (!isEditing) {\",\n \"- setIsEditing(true);\",\n \"- return;\",\n \"- }\",\n \"- editorBridge?.undo?.();\",\n \"- }, [editorBridge, isEditing]);\",\n \"-\",\n \"- const handleRedo = useCallback(() => {\",\n \"- if (!isEditing) {\",\n \"- setIsEditing(true);\",\n \"- return;\",\n \"- }\",\n \"- editorBridge?.redo?.();\",\n \"- }, [editorBridge, isEditing]);\",\n \"-\",\n \"- const handleDeletePage = useCallback(async () => {\",\n \"- if (readOnly) return;\",\n \"- const ok = window.confirm(\\\"确定删除该页面吗?删除后会进入垃圾桶。\\\");\",\n \"- if (!ok) return;\",\n \"- try {\",\n \"- await deleteDocumentCommand({ documentId, workspaceId });\",\n \"- } catch (error) {\",\n \"- const payload = error instanceof Error ? error.message : null;\",\n \"- window.alert(payload ?? \\\"删除失败\\\");\",\n \"- return;\",\n \"- }\",\n \"- router.push(\\\"/\\\");\",\n \"- router.refresh();\",\n \"- }, [documentId, readOnly, router, workspaceId]);\",\n \"-\",\n \"- const handleOpenMoveEmbed = useCallback(() => {\",\n \"- if (readOnly) return;\",\n \"- openMoveEmbedPicker({\",\n \"- workspaceId,\",\n \"- defaultMode: \\\"move\\\",\",\n \"- modes: [\\\"move\\\", \\\"embed\\\"],\",\n \"- allowRoot: true,\",\n \"- excludeIds: [documentId],\",\n \"- onPick: async (mode, targetId) => {\",\n \"- if (mode === \\\"move\\\") {\",\n \"- try {\",\n \"- await moveDocumentCommand({\",\n \"- documentId,\",\n \"- parentId: targetId ?? null,\",\n \"- position: 999999,\",\n \"- workspaceId,\",\n \"- });\",\n \"- } catch (error) {\",\n \"- window.alert(error instanceof Error ? error.message : \\\"移动失败\\\");\",\n \"- return;\",\n \"- }\",\n \"- window.alert(\\\"移动成功\\\");\",\n \"- router.refresh();\",\n \"- return;\",\n \"- }\",\n \"-\",\n \"- if (!targetId) {\",\n \"- window.alert(\\\"请选择目标页面\\\");\",\n \"- return;\",\n \"- }\",\n \"-\",\n \"- try {\",\n \"- await embedDocumentCommand({\",\n \"- sourceId: documentId,\",\n \"- targetId,\",\n \"- });\",\n \"- } catch (error) {\",\n \"- const message = error instanceof Error ? error.message : \\\"嵌入失败\\\";\",\n \"- window.alert(message);\",\n \"- return;\",\n \"- }\",\n \"-\",\n \"- window.alert(\\\"已嵌入到目标页面\\\");\",\n \"- },\",\n \"- });\",\n \"- }, [documentId, openMoveEmbedPicker, readOnly, router, workspaceId]);\",\n \"-\",\n \"- const handleAddToTemplates = useCallback(async () => {\",\n \"- if (readOnly) return;\",\n \"- const ok = window.confirm(\\\"将该页面添加为模板?\\\");\",\n \"- if (!ok) return;\",\n \"- const resp = await fetch(\\\"/api/documents/template\\\", {\",\n \"- method: \\\"POST\\\",\",\n \"- headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"- body: JSON.stringify({ documentId, isTemplate: true }),\",\n \"- });\",\n \"- if (!resp.ok) {\",\n \"- const payload = await resp.json().catch(() => ({}));\",\n \"- window.alert(payload?.error ?? \\\"设置模板失败\\\");\",\n \"- return;\",\n \"- }\",\n \"- window.alert(\\\"已添加为模板\\\");\",\n \"- router.refresh();\",\n \"- }, [documentId, readOnly, router]);\",\n \"-\",\n \"- const formattedUpdatedAt = useMemo(() => {\",\n \"- if (!updatedAt) return \\\"\\\";\",\n \"- const date = new Date(updatedAt);\",\n \"- if (Number.isNaN(date.getTime())) return \\\"\\\";\",\n \"- return new Intl.DateTimeFormat(\\\"zh-CN\\\", {\",\n \"- timeZone: \\\"Asia/Shanghai\\\",\",\n \"- year: \\\"numeric\\\",\",\n \"- month: \\\"numeric\\\",\",\n \"- day: \\\"numeric\\\",\",\n \"- hour: \\\"2-digit\\\",\",\n \"- minute: \\\"2-digit\\\",\",\n \"- second: \\\"2-digit\\\",\",\n \"- hour12: false,\",\n \"- }).format(date);\",\n \"- }, [updatedAt]);\",\n \"-\",\n \"- const handleExport = useCallback(() => {\",\n \"- if (disableDownload) {\",\n \"- window.alert(\\\"该页面已禁止下载\\\");\",\n \"- return;\",\n \"- }\",\n \"- const latest = history[0];\",\n \"- const exportBlocks = latest?.blocks ?? latestBlocksRef.current;\",\n \"- if (!exportBlocks) {\",\n \"- window.alert(\\\"暂无可导出的内容\\\");\",\n \"- return;\",\n \"- }\",\n \"- const payload = JSON.stringify(exportBlocks, null, 2);\",\n \"- const blob = new Blob([payload], { type: \\\"application/json\\\" });\",\n \"- const url = URL.createObjectURL(blob);\",\n \"- const anchor = document.createElement(\\\"a\\\");\",\n \"- anchor.href = url;\",\n \"- anchor.download = `${title ?? \\\"未命名页面\\\"}-${new Date().toISOString()}.json`;\",\n \"- anchor.click();\",\n \"- URL.revokeObjectURL(url);\",\n \"- }, [disableDownload, history, title]);\",\n \"-\",\n \"- const pageRootClass = cn(\",\n \"- \\\"flex h-full overflow-hidden bg-wolai-bg\\\",\",\n \"- options.pageFont === \\\"song\\\" && \\\"wolai-page-font-song\\\",\",\n \"- options.pageFont === \\\"kai\\\" && \\\"wolai-page-font-kai\\\",\",\n \"- options.layoutDensity === \\\"compact\\\" && \\\"wolai-page-density-compact\\\",\",\n \"- options.layoutDensity === \\\"spacious\\\" && \\\"wolai-page-density-spacious\\\",\",\n \"- options.smallText && \\\"wolai-small-text\\\",\",\n \"- options.hideChildPages && \\\"wolai-hide-child-pages\\\",\",\n \"- );\",\n \"- const pageSubtree = useMemo(() => {\",\n \"- const hasServerPageSubtree = Boolean(serverPageSubtreeSnapshot);\",\n \"- const titleUnchanged = pageTitle === serverPageSubtreeTitle;\",\n \"- const contentUnchanged = content === serverContentSnapshot;\",\n \"-\",\n \"- if (hasServerPageSubtree && titleUnchanged && contentUnchanged) {\",\n \"- return serverPageSubtreeSnapshot;\",\n \"- }\",\n \"- return null;\",\n \"- }, [\",\n \"- content,\",\n \"- pageTitle,\",\n \"- serverContentSnapshot,\",\n \"- serverPageSubtreeSnapshot,\",\n \"- serverPageSubtreeTitle,\",\n \"- ]);\",\n \"- const readViewTocEntries = useMemo(\",\n \"- () =>\",\n \"- (pageSubtree?.outline ?? [])\",\n \"- .filter((entry) => typeof entry.anchorBlockId === \\\"string\\\" && entry.anchorBlockId.trim())\",\n \"- .map(({ anchorBlockId, level, numbering, title: entryTitle }) => ({\",\n \"- id: anchorBlockId as string,\",\n \"- level,\",\n \"- numbering,\",\n \"- title: entryTitle,\",\n \"- })),\",\n \"- [pageSubtree],\",\n \"- );\",\n \"- const inspectorCanUseEditorBridge = canEditDocument && isEditing;\",\n \"-\",\n \"- const jumpToHeading = useCallback((headingId: string) => {\",\n \"- const targetRoot = !isEditing ? readViewRootRef.current : pageRootRef.current;\",\n \"- const target = targetRoot?.querySelector(`[data-id=\\\"${headingId}\\\"]`);\",\n \"- if (target) {\",\n \"- target.scrollIntoView({ behavior: \\\"smooth\\\", block: \\\"center\\\" });\",\n \"- }\",\n \"- }, [isEditing]);\",\n \"-\",\n \"- const handleSnapshot = useCallback((payload: { blocks: Json; stats: DocumentStats }) => {\",\n \"- setContent(payload.blocks);\",\n \"- latestBlocksRef.current = payload.blocks;\",\n \"- setHistory((prev) => {\",\n \"- const now = Date.now();\",\n \"- if (prev.length > 0 && now - prev[0].timestamp < 4000) {\",\n \"- return prev;\",\n \"- }\",\n \"- const snapshot: DocumentSnapshot = {\",\n \"- id: `${now}-${Math.random().toString(36).slice(2, 8)}`,\",\n \"- timestamp: now,\",\n \"- blocks: payload.blocks,\",\n \"- stats: payload.stats,\",\n \"- };\",\n \"- return [snapshot, ...prev].slice(0, 15);\",\n \"- });\",\n \"- }, []);\",\n \"-\",\n \"- const persistStatsRequest = useCallback((next: DocumentStats) => {\",\n \"- void fetch(\\\"/api/documents/stats\\\", {\",\n \"- method: \\\"POST\\\",\",\n \"- headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"- body: JSON.stringify({ documentId, workspaceId, stats: next }),\",\n \"- }).catch((error) => console.error(error));\",\n \"- }, [documentId, workspaceId]);\",\n \"-\",\n \"- const persistStats = useDebouncedCallback(persistStatsRequest, 1500);\",\n \"-\",\n \"- const handleStatsChange = useCallback(\",\n \"- (nextStats: DocumentStats) => {\",\n \"- setStats(nextStats);\",\n \"- persistStats(nextStats);\",\n \"- },\",\n \"- [persistStats],\",\n \"- );\",\n \"-\",\n \"- const handleRestoreSnapshot = useCallback(\",\n \"- (snapshot: DocumentSnapshot) => {\",\n \"- if (!isEditing || !editorBridge) {\",\n \"- pendingRestoreSnapshotRef.current = snapshot;\",\n \"- setIsEditing(true);\",\n \"- return;\",\n \"- }\",\n \"- editorBridge.replaceWithSnapshot(snapshot.blocks);\",\n \"- setHistoryOpen(false);\",\n \"- },\",\n \"- [editorBridge, isEditing],\",\n \"- );\",\n \"-\",\n \"- const handleEnterEditMode = useCallback(() => {\",\n \"- if (!canEditDocument) return;\",\n \"- setIsEditing(true);\",\n \"- }, [canEditDocument]);\",\n \"-\",\n \"- const handleExitEditMode = useCallback(() => {\",\n \"- setIsEditing(false);\",\n \"- }, []);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- if (!isEditing) return;\",\n \"- if (!editorBridge) return;\",\n \"- const pendingSnapshot = pendingRestoreSnapshotRef.current;\",\n \"- if (!pendingSnapshot) return;\",\n \"- pendingRestoreSnapshotRef.current = null;\",\n \"- editorBridge.replaceWithSnapshot(pendingSnapshot.blocks);\",\n \"- setHistoryOpen(false);\",\n \"- }, [editorBridge, isEditing]);\",\n \"- return (\",\n \"- \",\n \"-
\",\n \"-
\",\n \"-
\",\n \"-
\",\n \"-
\",\n \"- {isEditing && canEditDocument ? (\",\n \"-
\",\n \"- \",\n \"-
\",\n \"- ) : (\",\n \"-

\",\n \"- {pageTitle || \\\"无标题\\\"}\",\n \"-

\",\n \"- )}\",\n \"- {readOnly ? (\",\n \"-

该页面为只读共享,无法编辑。

\",\n \"- ) : options.protectEditing && isEditing ? (\",\n \"-

当前页面已开启编辑保护,关闭后方可修改内容。

\",\n \"- ) : !isEditing && canEditDocument ? (\",\n \"-

当前为阅读态,编辑器仅在进入编辑后挂载。

\",\n \"- ) : null}\",\n \"-

最近更新:{formattedUpdatedAt}

\",\n \"-
\",\n \"- {canEditDocument && (\",\n \"-
\",\n \"- {isEditing ? (\",\n \"- \",\n \"- ) : (\",\n \"- \",\n \"- )}\",\n \"-
\",\n \"- )}\",\n \"-
\",\n \"-
\",\n \"-
\",\n \"- {contentLoading ? (\",\n \"- showContentLoadingIndicator ? (\",\n \"-
\",\n \"- 页面内容加载中...\",\n \"-
\",\n \"- ) : (\",\n \"-
\",\n \"- )\",\n \"- ) : contentError ? (\",\n \"-
\",\n \"-
{contentError}
\",\n \"- {\",\n \"- setContentError(null);\",\n \"- setContentLoading(true);\",\n \"- setContentReloadKey((prev) => prev + 1);\",\n \"- }}\",\n \"- >\",\n \"- 重试\",\n \"- \",\n \"-
\",\n \"- ) : (\",\n \"-
\",\n \"- {keepEditorMounted && (\",\n \"-
\",\n \"- {\",\n \"- setContentRevision(meta.revision);\",\n \"- setConflictDetectionKey(meta.conflictDetectionKey);\",\n \"- }}\",\n \"- />\",\n \"-
\",\n \"- )}\",\n \"- {!isEditing && (\",\n \"-
\",\n \"- \",\n \"- \",\n \"-
\",\n \"- )}\",\n \"-
\",\n \"- )}\",\n \"- \",\n \"-
\",\n \"-
\",\n \"- {showInspector && (\",\n \"- setHistoryOpen(true)}\",\n \"- onOpenComments={() => openCommentsForPage({ workspaceId, documentId })}\",\n \"- onUndo={canEditDocument ? handleUndo : undefined}\",\n \"- onRedo={canEditDocument ? handleRedo : undefined}\",\n \"- onDeletePage={canEditDocument ? handleDeletePage : undefined}\",\n \"- onOpenMoveEmbedPicker={canEditDocument ? handleOpenMoveEmbed : undefined}\",\n \"- onCopyPageLink={handleCopyPageLink}\",\n \"- onCopyPageReference={handleCopyPageReference}\",\n \"- onAddToTemplates={canEditDocument ? handleAddToTemplates : undefined}\",\n \"- />\",\n \"- )}\",\n \"-
\",\n \"- \",\n \"- \",\n \"- \",\n \"- latestBlocksRef.current}\",\n \"- getLatestPageSubtree={() => pageSubtree}\",\n \"- />\",\n \"- \",\n \"- );\",\n \"-}\",\n \"+\\\"use client\\\";\",\n \"+\",\n \"+import dynamic from \\\"next/dynamic\\\";\",\n \"+import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent as ReactKeyboardEvent } from \\\"react\\\";\",\n \"+import type { BooleanPageOptionKey, DocumentStats, PageOptionsState, PageFont, PageLayoutDensity } from \\\"@/types/page-options\\\";\",\n \"+import { useEditorBridgeStore } from \\\"@/store/editor-bridge\\\";\",\n \"+import { usePageLayoutStore } from \\\"@/store/page-layout\\\";\",\n \"+import { useCurrentDocumentStore } from \\\"@/store/current-document\\\";\",\n \"+import type { Json } from \\\"@/types/supabase\\\";\",\n \"+import type { DocumentSnapshot } from \\\"@/types/document\\\";\",\n \"+import { useDebouncedCallback } from \\\"@/hooks/use-debounced-callback\\\";\",\n \"+import { ImagePickerProvider } from \\\"@/components/media/image-picker-context\\\";\",\n \"+import { useRouter } from \\\"next/navigation\\\";\",\n \"+import { DocumentAiAgentPanel } from \\\"./DocumentAiAgentPanel\\\";\",\n \"+import { CONTENT_LOADING_DELAY_MS } from \\\"@/lib/constants\\\";\",\n \"+import { useCommentsUiStore } from \\\"@/store/comments-ui\\\";\",\n \"+import { useAppPreferencesStore } from \\\"@/store/app-preferences\\\";\",\n \"+import { useMoveEmbedPickerStore } from \\\"@/store/move-embed-picker\\\";\",\n \"+import { cn } from \\\"@/lib/utils\\\";\",\n \"+import { Button } from \\\"@/components/ui/button\\\";\",\n \"+import { DocumentToc } from \\\"@/components/editor/document-toc\\\";\",\n \"+import { DocumentReadView } from \\\"@/components/editor/document-read-view\\\";\",\n \"+import { extractPageBlocks, type PageSubtreeProjection } from \\\"@/lib/documents/page-subtree\\\";\",\n \"+import {\",\n \"+ deleteDocumentCommand,\",\n \"+ embedDocumentCommand,\",\n \"+ moveDocumentCommand,\",\n \"+ renameDocumentCommand,\",\n \"+} from \\\"@/lib/documents/tree-command-client\\\";\",\n \"+import { EditorHost } from \\\"@/components/editor/editor-host\\\";\",\n \"+\",\n \"+const PageOptionsSidebar = dynamic(\",\n \"+ () => import(\\\"@/components/editor/page-options-sidebar\\\").then((mod) => mod.PageOptionsSidebar),\",\n \"+ { ssr: false },\",\n \"+);\",\n \"+\",\n \"+const PageBacklinksPanel = dynamic(\",\n \"+ () => import(\\\"@/components/editor/page-backlinks-panel\\\").then((mod) => mod.PageBacklinksPanel),\",\n \"+ {\",\n \"+ ssr: false,\",\n \"+ loading: () => null,\",\n \"+ },\",\n \"+);\",\n \"+\",\n \"+const DocumentHistoryDrawer = dynamic(\",\n \"+ () => import(\\\"@/components/editor/document-history-drawer\\\").then((mod) => mod.DocumentHistoryDrawer),\",\n \"+ {\",\n \"+ ssr: false,\",\n \"+ loading: () => null,\",\n \"+ },\",\n \"+);\",\n \"+\",\n \"+const MoveEmbedPickerHost = dynamic(\",\n \"+ () => import(\\\"@/components/documents/move-embed-picker-host\\\").then((mod) => mod.MoveEmbedPickerHost),\",\n \"+ {\",\n \"+ ssr: false,\",\n \"+ loading: () => null,\",\n \"+ },\",\n \"+);\",\n \"+\",\n \"+const DocumentCommentsDrawer = dynamic(\",\n \"+ () => import(\\\"@/components/editor/document-comments-drawer\\\").then((mod) => mod.DocumentCommentsDrawer),\",\n \"+ {\",\n \"+ ssr: false,\",\n \"+ loading: () => null,\",\n \"+ },\",\n \"+);\",\n \"+\",\n \"+export interface DocumentContentProps {\",\n \"+ documentId: string;\",\n \"+ workspaceId: string;\",\n \"+ title: string | null;\",\n \"+ updatedAt: string | null;\",\n \"+ initialContent: unknown;\",\n \"+ initialContentRevision?: number | null;\",\n \"+ initialConflictDetectionKey?: string | null;\",\n \"+ initialPageSubtree?: PageSubtreeProjection | null;\",\n \"+ initialOptions: PageOptionsState;\",\n \"+ initialStats: DocumentStats | null;\",\n \"+ openTableId?: string | null;\",\n \"+ autoEnterEdit?: boolean;\",\n \"+ readOnly?: boolean;\",\n \"+ disableDownload?: boolean;\",\n \"+ disableCopy?: boolean;\",\n \"+}\",\n \"+\",\n \"+const defaultOptions: PageOptionsState = {\",\n \"+ wideLayout: false,\",\n \"+ smallText: false,\",\n \"+ showHeadingNumbers: true,\",\n \"+ showToc: false,\",\n \"+ showStructure: false,\",\n \"+ protectEditing: false,\",\n \"+ showWordCount: true,\",\n \"+ collapseBacklinks: false,\",\n \"+ pageFont: \\\"default\\\",\",\n \"+ layoutDensity: \\\"normal\\\",\",\n \"+ hideChildPages: false,\",\n \"+ showBlockRefCount: false,\",\n \"+ embedDefaultBlockId: null,\",\n \"+};\",\n \"+const defaultStats: DocumentStats = { wordCount: 0, characterCount: 0, blockCount: 0, todoTotal: 0, todoDone: 0 };\",\n \"+const EDITOR_UNMOUNT_GRACE_MS = 1000;\",\n \"+\",\n \"+export function DocumentContent({\",\n \"+ documentId,\",\n \"+ workspaceId,\",\n \"+ title,\",\n \"+ updatedAt,\",\n \"+ initialContent,\",\n \"+ initialContentRevision = null,\",\n \"+ initialConflictDetectionKey = null,\",\n \"+ initialPageSubtree = null,\",\n \"+ initialOptions,\",\n \"+ initialStats,\",\n \"+ openTableId,\",\n \"+ autoEnterEdit = false,\",\n \"+ readOnly = false,\",\n \"+ disableDownload = false,\",\n \"+ disableCopy = false,\",\n \"+}: DocumentContentProps) {\",\n \"+ const setCurrentDocument = useCurrentDocumentStore((state) => state.setCurrent);\",\n \"+ const clearIfMatch = useCurrentDocumentStore((state) => state.clearIfMatch);\",\n \"+ const canEditDocument = !readOnly;\",\n \"+ const [options, setOptions] = useState(initialOptions ?? defaultOptions);\",\n \"+ const [stats, setStats] = useState(initialStats ?? defaultStats);\",\n \"+ const [history, setHistory] = useState([]);\",\n \"+ const [historyOpen, setHistoryOpen] = useState(false);\",\n \"+ const editorBridge = useEditorBridgeStore((state) => state.bridge);\",\n \"+ const router = useRouter();\",\n \"+ const showInspector = usePageLayoutStore((state) => state.showInspector);\",\n \"+ const openCommentsForPage = useCommentsUiStore((s) => s.openForPage);\",\n \"+ const spellCheck = useAppPreferencesStore((s) => s.spellCheck);\",\n \"+ const openMoveEmbedPicker = useMoveEmbedPickerStore((s) => s.openPicker);\",\n \"+ const [pageTitle, setPageTitle] = useState(title ?? \\\"无标题\\\");\",\n \"+ const [content, setContent] = useState(initialContent);\",\n \"+ const [serverContentSnapshot, setServerContentSnapshot] = useState(initialContent);\",\n \"+ const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState(\",\n \"+ initialPageSubtree,\",\n \"+ );\",\n \"+ const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState(title ?? \\\"无标题\\\");\",\n \"+ const [contentRevision, setContentRevision] = useState(initialContentRevision);\",\n \"+ const [conflictDetectionKey, setConflictDetectionKey] = useState(initialConflictDetectionKey);\",\n \"+ const [contentLoading, setContentLoading] = useState(() => initialContent == null);\",\n \"+ const [contentError, setContentError] = useState(null);\",\n \"+ const [contentReloadKey, setContentReloadKey] = useState(0);\",\n \"+ const [showContentLoadingIndicator, setShowContentLoadingIndicator] = useState(false);\",\n \"+ const shouldStartEditing = canEditDocument;\",\n \"+ const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\",\n \"+ const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\",\n \"+ const contentLoadingTimerRef = useRef | null>(null);\",\n \"+ const editorUnmountTimerRef = useRef | null>(null);\",\n \"+ const pendingOpenTableRef = useRef(null);\",\n \"+ const latestBlocksRef = useRef(null);\",\n \"+ const pageRootRef = useRef(null);\",\n \"+ const readViewRootRef = useRef(null);\",\n \"+ const pendingRestoreSnapshotRef = useRef(null);\",\n \"+ const lastCopyBlockedAtRef = useRef(0);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setCurrentDocument(documentId, readOnly, disableDownload, disableCopy);\",\n \"+ return () => {\",\n \"+ clearIfMatch(documentId);\",\n \"+ };\",\n \"+ }, [clearIfMatch, disableCopy, disableDownload, documentId, readOnly, setCurrentDocument]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ if (!disableCopy) return;\",\n \"+\",\n \"+ const toElement = (node: Node | null): Element | null => {\",\n \"+ if (!node) return null;\",\n \"+ if (node.nodeType === Node.TEXT_NODE) {\",\n \"+ return node.parentElement;\",\n \"+ }\",\n \"+ return node instanceof Element ? node : null;\",\n \"+ };\",\n \"+\",\n \"+ const isEventInsidePage = () => {\",\n \"+ const root = pageRootRef.current;\",\n \"+ if (!root) return false;\",\n \"+ const selection = typeof window !== \\\"undefined\\\" ? window.getSelection() : null;\",\n \"+ const anchor = selection?.anchorNode ?? null;\",\n \"+ const focus = selection?.focusNode ?? null;\",\n \"+ const anchorEl = toElement(anchor);\",\n \"+ const focusEl = toElement(focus);\",\n \"+ return Boolean((anchorEl && root.contains(anchorEl)) || (focusEl && root.contains(focusEl)));\",\n \"+ };\",\n \"+\",\n \"+ const notifyBlocked = () => {\",\n \"+ const now = Date.now();\",\n \"+ if (now - lastCopyBlockedAtRef.current < 1200) return;\",\n \"+ lastCopyBlockedAtRef.current = now;\",\n \"+ window.alert(\\\"该页面已禁止复制\\\");\",\n \"+ };\",\n \"+\",\n \"+ const onCopy = (event: ClipboardEvent) => {\",\n \"+ if (!isEventInsidePage()) return;\",\n \"+ event.preventDefault();\",\n \"+ event.stopPropagation();\",\n \"+ notifyBlocked();\",\n \"+ };\",\n \"+\",\n \"+ const onCut = (event: ClipboardEvent) => {\",\n \"+ if (!isEventInsidePage()) return;\",\n \"+ event.preventDefault();\",\n \"+ event.stopPropagation();\",\n \"+ notifyBlocked();\",\n \"+ };\",\n \"+\",\n \"+ const onKeyDown = (event: KeyboardEvent) => {\",\n \"+ if (!isEventInsidePage()) return;\",\n \"+ const key = String(event.key ?? \\\"\\\").toLowerCase();\",\n \"+ const ctrlOrMeta = event.ctrlKey || event.metaKey;\",\n \"+ if (!ctrlOrMeta) return;\",\n \"+ if (key === \\\"c\\\" || key === \\\"x\\\" || key === \\\"insert\\\") {\",\n \"+ event.preventDefault();\",\n \"+ event.stopPropagation();\",\n \"+ notifyBlocked();\",\n \"+ }\",\n \"+ };\",\n \"+\",\n \"+ document.addEventListener(\\\"copy\\\", onCopy, true);\",\n \"+ document.addEventListener(\\\"cut\\\", onCut, true);\",\n \"+ document.addEventListener(\\\"keydown\\\", onKeyDown, true);\",\n \"+ return () => {\",\n \"+ document.removeEventListener(\\\"copy\\\", onCopy, true);\",\n \"+ document.removeEventListener(\\\"cut\\\", onCut, true);\",\n \"+ document.removeEventListener(\\\"keydown\\\", onKeyDown, true);\",\n \"+ };\",\n \"+ }, [disableCopy]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ const tableId = (openTableId ?? \\\"\\\").trim();\",\n \"+ if (!tableId) return;\",\n \"+ if (!editorBridge?.openTableFullScreen) return;\",\n \"+ if (pendingOpenTableRef.current === tableId) return;\",\n \"+ pendingOpenTableRef.current = tableId;\",\n \"+\",\n \"+ editorBridge.openTableFullScreen(tableId);\",\n \"+\",\n \"+ if (typeof window !== \\\"undefined\\\") {\",\n \"+ try {\",\n \"+ const url = new URL(window.location.href);\",\n \"+ url.searchParams.delete(\\\"openTableId\\\");\",\n \"+ window.history.replaceState({}, \\\"\\\", url.toString());\",\n \"+ } catch {\",\n \"+ router.replace(`/documents/${documentId}`);\",\n \"+ }\",\n \"+ }\",\n \"+ }, [documentId, editorBridge, openTableId, router]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setPageTitle(title ?? \\\"无标题\\\");\",\n \"+ }, [title]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setServerPageSubtreeTitle(title ?? \\\"无标题\\\");\",\n \"+ }, [title]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setServerPageSubtreeSnapshot(initialPageSubtree);\",\n \"+ }, [initialPageSubtree]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setOptions(initialOptions ?? defaultOptions);\",\n \"+ }, [initialOptions]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setStats(initialStats ?? defaultStats);\",\n \"+ }, [initialStats]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setContentRevision(initialContentRevision);\",\n \"+ }, [initialContentRevision]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setConflictDetectionKey(initialConflictDetectionKey);\",\n \"+ }, [initialConflictDetectionKey]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setServerContentSnapshot(initialContent);\",\n \"+ }, [initialContent]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ const nextBlocks = extractPageBlocks(content);\",\n \"+ latestBlocksRef.current = nextBlocks.length > 0 ? (nextBlocks as Json) : null;\",\n \"+ }, [content]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ const shouldForceEdit = Boolean((openTableId ?? \\\"\\\").trim()) && canEditDocument;\",\n \"+ if (shouldForceEdit) {\",\n \"+ setIsEditing(true);\",\n \"+ setKeepEditorMounted(true);\",\n \"+ }\",\n \"+ }, [canEditDocument, openTableId]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ if (isEditing) {\",\n \"+ if (editorUnmountTimerRef.current) {\",\n \"+ clearTimeout(editorUnmountTimerRef.current);\",\n \"+ editorUnmountTimerRef.current = null;\",\n \"+ }\",\n \"+ setKeepEditorMounted(true);\",\n \"+ return;\",\n \"+ }\",\n \"+ if (editorUnmountTimerRef.current) {\",\n \"+ clearTimeout(editorUnmountTimerRef.current);\",\n \"+ }\",\n \"+ editorUnmountTimerRef.current = setTimeout(() => {\",\n \"+ setKeepEditorMounted(false);\",\n \"+ editorUnmountTimerRef.current = null;\",\n \"+ }, EDITOR_UNMOUNT_GRACE_MS);\",\n \"+ return () => {\",\n \"+ if (editorUnmountTimerRef.current) {\",\n \"+ clearTimeout(editorUnmountTimerRef.current);\",\n \"+ editorUnmountTimerRef.current = null;\",\n \"+ }\",\n \"+ };\",\n \"+ }, [isEditing]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ let canceled = false;\",\n \"+ const controller = new AbortController();\",\n \"+\",\n \"+ const load = async () => {\",\n \"+ setContentError(null);\",\n \"+ setContentLoading(initialContent == null);\",\n \"+ setContent(initialContent);\",\n \"+ setShowContentLoadingIndicator(false);\",\n \"+\",\n \"+ if (initialContent != null) {\",\n \"+ return;\",\n \"+ }\",\n \"+\",\n \"+ if (contentLoadingTimerRef.current) {\",\n \"+ clearTimeout(contentLoadingTimerRef.current);\",\n \"+ contentLoadingTimerRef.current = null;\",\n \"+ }\",\n \"+ contentLoadingTimerRef.current = setTimeout(() => {\",\n \"+ if (!canceled) {\",\n \"+ setShowContentLoadingIndicator(true);\",\n \"+ }\",\n \"+ }, CONTENT_LOADING_DELAY_MS);\",\n \"+\",\n \"+ try {\",\n \"+ const response = await fetch(\",\n \"+ `/api/documents/content?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,\",\n \"+ {\",\n \"+ method: \\\"GET\\\",\",\n \"+ credentials: \\\"include\\\",\",\n \"+ signal: controller.signal,\",\n \"+ },\",\n \"+ );\",\n \"+ if (!response.ok) {\",\n \"+ const payload = await response.json().catch(() => ({}));\",\n \"+ throw new Error(payload?.error ?? \\\"加载页面内容失败\\\");\",\n \"+ }\",\n \"+ const payload = (await response.json()) as {\",\n \"+ content?: unknown;\",\n \"+ revision?: number | null;\",\n \"+ conflictDetectionKey?: string | null;\",\n \"+ pageSubtree?: PageSubtreeProjection | null;\",\n \"+ };\",\n \"+ if (canceled) return;\",\n \"+ setContent(payload.content ?? null);\",\n \"+ setServerContentSnapshot(payload.content ?? null);\",\n \"+ setContentRevision(\",\n \"+ typeof payload.revision === \\\"number\\\" && Number.isInteger(payload.revision)\",\n \"+ ? payload.revision\",\n \"+ : 0,\",\n \"+ );\",\n \"+ setConflictDetectionKey(\",\n \"+ typeof payload.conflictDetectionKey === \\\"string\\\" && payload.conflictDetectionKey.trim()\",\n \"+ ? payload.conflictDetectionKey\",\n \"+ : `${documentId}:0`,\",\n \"+ );\",\n \"+ setServerPageSubtreeSnapshot(payload.pageSubtree ?? null);\",\n \"+ setServerPageSubtreeTitle(\",\n \"+ typeof payload.pageSubtree?.rootNode.metadata.title === \\\"string\\\" &&\",\n \"+ payload.pageSubtree.rootNode.metadata.title.trim()\",\n \"+ ? payload.pageSubtree.rootNode.metadata.title\",\n \"+ : title ?? \\\"无标题\\\",\",\n \"+ );\",\n \"+ } catch (error) {\",\n \"+ if (canceled) return;\",\n \"+ if ((error as { name?: string })?.name === \\\"AbortError\\\") return;\",\n \"+ setContentError(error instanceof Error ? error.message : \\\"加载页面内容失败\\\");\",\n \"+ } finally {\",\n \"+ if (!canceled) {\",\n \"+ setContentLoading(false);\",\n \"+ setShowContentLoadingIndicator(false);\",\n \"+ }\",\n \"+ if (contentLoadingTimerRef.current) {\",\n \"+ clearTimeout(contentLoadingTimerRef.current);\",\n \"+ contentLoadingTimerRef.current = null;\",\n \"+ }\",\n \"+ }\",\n \"+ };\",\n \"+\",\n \"+ void load();\",\n \"+\",\n \"+ return () => {\",\n \"+ canceled = true;\",\n \"+ controller.abort();\",\n \"+ if (contentLoadingTimerRef.current) {\",\n \"+ clearTimeout(contentLoadingTimerRef.current);\",\n \"+ contentLoadingTimerRef.current = null;\",\n \"+ }\",\n \"+ };\",\n \"+ }, [documentId, initialContent, contentReloadKey, title, workspaceId]);\",\n \"+\",\n \"+ const persistTitle = useCallback(\",\n \"+ async (nextTitle: string) => {\",\n \"+ if (readOnly) return;\",\n \"+ const payload = nextTitle.trim() || \\\"无标题\\\";\",\n \"+ try {\",\n \"+ await renameDocumentCommand({ documentId, workspaceId, title: payload });\",\n \"+ } catch (error) {\",\n \"+ console.error(\\\"更新页面标题失败\\\", error);\",\n \"+ }\",\n \"+ },\",\n \"+ [documentId, readOnly, workspaceId],\",\n \"+ );\",\n \"+\",\n \"+ const debouncedPersistTitle = useDebouncedCallback((value: string) => {\",\n \"+ void persistTitle(value);\",\n \"+ }, 600);\",\n \"+\",\n \"+ const handleTitleChange = (event: ChangeEvent) => {\",\n \"+ if (!canEditDocument) return;\",\n \"+ const value = event.target.value;\",\n \"+ setPageTitle(value);\",\n \"+ debouncedPersistTitle(value);\",\n \"+ };\",\n \"+\",\n \"+ const handleTitleBlur = () => {\",\n \"+ if (!canEditDocument) return;\",\n \"+ void persistTitle(pageTitle);\",\n \"+ };\",\n \"+\",\n \"+ const handleTitleKeyDown = (event: ReactKeyboardEvent) => {\",\n \"+ if (event.key === \\\"Enter\\\") {\",\n \"+ event.preventDefault();\",\n \"+ event.currentTarget.blur();\",\n \"+ }\",\n \"+ };\",\n \"+\",\n \"+ const persistOptions = useCallback(\",\n \"+ async (patch: Partial) => {\",\n \"+ if (readOnly) return;\",\n \"+ try {\",\n \"+ const response = await fetch(\\\"/api/documents/options\\\", {\",\n \"+ method: \\\"POST\\\",\",\n \"+ headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"+ body: JSON.stringify({ documentId, workspaceId, options: patch }),\",\n \"+ });\",\n \"+ if (!response.ok) {\",\n \"+ const payload = await response.json().catch(() => null);\",\n \"+ console.error(payload?.error ?? \\\"更新页面选项失败\\\");\",\n \"+ }\",\n \"+ } catch (error) {\",\n \"+ console.error(error);\",\n \"+ }\",\n \"+ },\",\n \"+ [documentId, readOnly, workspaceId],\",\n \"+ );\",\n \"+\",\n \"+ const toggleOption = useCallback(\",\n \"+ (key: BooleanPageOptionKey) => {\",\n \"+ if (readOnly) return;\",\n \"+ setOptions((prev) => {\",\n \"+ const nextValue = !prev[key];\",\n \"+ const next = { ...prev, [key]: nextValue };\",\n \"+ void persistOptions({ [key]: nextValue } as Partial);\",\n \"+ return next;\",\n \"+ });\",\n \"+ },\",\n \"+ [persistOptions, readOnly],\",\n \"+ );\",\n \"+\",\n \"+ const setOptionPatch = useCallback(\",\n \"+ (patch: Partial) => {\",\n \"+ if (readOnly) return;\",\n \"+ setOptions((prev) => {\",\n \"+ const next = { ...prev, ...patch };\",\n \"+ void persistOptions(patch);\",\n \"+ return next;\",\n \"+ });\",\n \"+ },\",\n \"+ [persistOptions, readOnly],\",\n \"+ );\",\n \"+\",\n \"+ const closeToc = useCallback(() => {\",\n \"+ if (readOnly) return;\",\n \"+ setOptions((prev) => {\",\n \"+ if (!prev.showToc) return prev;\",\n \"+ const next = { ...prev, showToc: false };\",\n \"+ void persistOptions({ showToc: false });\",\n \"+ return next;\",\n \"+ });\",\n \"+ }, [persistOptions, readOnly]);\",\n \"+\",\n \"+ const handleSetPageFont = useCallback(\",\n \"+ (font: PageFont) => {\",\n \"+ setOptionPatch({ pageFont: font });\",\n \"+ },\",\n \"+ [setOptionPatch],\",\n \"+ );\",\n \"+\",\n \"+ const handleSetLayoutDensity = useCallback(\",\n \"+ (density: PageLayoutDensity) => {\",\n \"+ setOptionPatch({ layoutDensity: density });\",\n \"+ },\",\n \"+ [setOptionPatch],\",\n \"+ );\",\n \"+\",\n \"+ const handleSetEmbedDefaultToCursor = useCallback(() => {\",\n \"+ if (!canEditDocument) return;\",\n \"+ if (!isEditing) {\",\n \"+ setIsEditing(true);\",\n \"+ return;\",\n \"+ }\",\n \"+ const blockId = editorBridge?.getCursorBlockId?.() ?? null;\",\n \"+ if (!blockId) {\",\n \"+ window.alert(\\\"未找到当前光标所在块,请先在编辑器中点击任意块\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ setOptionPatch({ embedDefaultBlockId: blockId });\",\n \"+ window.alert(\\\"已设置“嵌入默认位置”\\\");\",\n \"+ }, [canEditDocument, editorBridge, isEditing, setOptionPatch]);\",\n \"+\",\n \"+ const handleClearEmbedDefault = useCallback(() => {\",\n \"+ if (readOnly) return;\",\n \"+ setOptionPatch({ embedDefaultBlockId: null });\",\n \"+ window.alert(\\\"已清除“嵌入默认位置”\\\");\",\n \"+ }, [readOnly, setOptionPatch]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ const onKeyDown = (event: KeyboardEvent) => {\",\n \"+ const root = pageRootRef.current;\",\n \"+ if (root) {\",\n \"+ const target = event.target;\",\n \"+ if (target && target instanceof Node && !root.contains(target)) {\",\n \"+ return;\",\n \"+ }\",\n \"+ }\",\n \"+ const ctrlOrMeta = event.ctrlKey || event.metaKey;\",\n \"+ if (!ctrlOrMeta) return;\",\n \"+ if (!event.shiftKey) return;\",\n \"+ const key = String(event.key ?? \\\"\\\").toLowerCase();\",\n \"+ if (key === \\\"l\\\") {\",\n \"+ event.preventDefault();\",\n \"+ toggleOption(\\\"showToc\\\");\",\n \"+ } else if (key === \\\"c\\\") {\",\n \"+ event.preventDefault();\",\n \"+ toggleOption(\\\"protectEditing\\\");\",\n \"+ }\",\n \"+ };\",\n \"+ window.addEventListener(\\\"keydown\\\", onKeyDown);\",\n \"+ return () => window.removeEventListener(\\\"keydown\\\", onKeyDown);\",\n \"+ }, [toggleOption]);\",\n \"+\",\n \"+ const buildDocumentUrl = useCallback((id: string): string => {\",\n \"+ if (typeof window === \\\"undefined\\\" || !window.location) {\",\n \"+ return `/documents/${id}`;\",\n \"+ }\",\n \"+ return `${window.location.origin}/documents/${id}`;\",\n \"+ }, []);\",\n \"+\",\n \"+ const copyText = useCallback(async (text: string, successMessage: string) => {\",\n \"+ if (typeof navigator !== \\\"undefined\\\" && navigator.clipboard) {\",\n \"+ try {\",\n \"+ await navigator.clipboard.writeText(text);\",\n \"+ window.alert(successMessage);\",\n \"+ return;\",\n \"+ } catch {\",\n \"+ }\",\n \"+ }\",\n \"+ window.prompt(\\\"复制失败,请手动复制内容\\\", text);\",\n \"+ }, []);\",\n \"+\",\n \"+ const handleCopyPageLink = useCallback(\",\n \"+ async (includeTitle: boolean) => {\",\n \"+ const url = buildDocumentUrl(documentId);\",\n \"+ if (includeTitle) {\",\n \"+ const text = `${pageTitle || \\\"无标题\\\"}\\\\n${url}`;\",\n \"+ await copyText(text, \\\"标题 + 链接已复制\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ await copyText(url, \\\"页面链接已复制\\\");\",\n \"+ },\",\n \"+ [buildDocumentUrl, copyText, documentId, pageTitle],\",\n \"+ );\",\n \"+\",\n \"+ const handleCopyPageReference = useCallback(\",\n \"+ async (mode: \\\"inline\\\" | \\\"embed\\\") => {\",\n \"+ const template = mode === \\\"inline\\\" ? `((${documentId}))` : `{{${documentId}}}`;\",\n \"+ await copyText(template, mode === \\\"inline\\\" ? \\\"行内引用已复制\\\" : \\\"嵌入引用已复制\\\");\",\n \"+ },\",\n \"+ [copyText, documentId],\",\n \"+ );\",\n \"+\",\n \"+ const handleUndo = useCallback(() => {\",\n \"+ if (!isEditing) {\",\n \"+ setIsEditing(true);\",\n \"+ return;\",\n \"+ }\",\n \"+ editorBridge?.undo?.();\",\n \"+ }, [editorBridge, isEditing]);\",\n \"+\",\n \"+ const handleRedo = useCallback(() => {\",\n \"+ if (!isEditing) {\",\n \"+ setIsEditing(true);\",\n \"+ return;\",\n \"+ }\",\n \"+ editorBridge?.redo?.();\",\n \"+ }, [editorBridge, isEditing]);\",\n \"+\",\n \"+ const handleDeletePage = useCallback(async () => {\",\n \"+ if (readOnly) return;\",\n \"+ const ok = window.confirm(\\\"确定删除该页面吗?删除后会进入垃圾桶。\\\");\",\n \"+ if (!ok) return;\",\n \"+ try {\",\n \"+ await deleteDocumentCommand({ documentId, workspaceId });\",\n \"+ } catch (error) {\",\n \"+ const payload = error instanceof Error ? error.message : null;\",\n \"+ window.alert(payload ?? \\\"删除失败\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ router.push(\\\"/\\\");\",\n \"+ router.refresh();\",\n \"+ }, [documentId, readOnly, router, workspaceId]);\",\n \"+\",\n \"+ const handleOpenMoveEmbed = useCallback(() => {\",\n \"+ if (readOnly) return;\",\n \"+ openMoveEmbedPicker({\",\n \"+ workspaceId,\",\n \"+ defaultMode: \\\"move\\\",\",\n \"+ modes: [\\\"move\\\", \\\"embed\\\"],\",\n \"+ allowRoot: true,\",\n \"+ excludeIds: [documentId],\",\n \"+ onPick: async (mode, targetId) => {\",\n \"+ if (mode === \\\"move\\\") {\",\n \"+ try {\",\n \"+ await moveDocumentCommand({\",\n \"+ documentId,\",\n \"+ parentId: targetId ?? null,\",\n \"+ position: 999999,\",\n \"+ workspaceId,\",\n \"+ });\",\n \"+ } catch (error) {\",\n \"+ window.alert(error instanceof Error ? error.message : \\\"移动失败\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ window.alert(\\\"移动成功\\\");\",\n \"+ router.refresh();\",\n \"+ return;\",\n \"+ }\",\n \"+\",\n \"+ if (!targetId) {\",\n \"+ window.alert(\\\"请选择目标页面\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+\",\n \"+ try {\",\n \"+ await embedDocumentCommand({\",\n \"+ sourceId: documentId,\",\n \"+ targetId,\",\n \"+ });\",\n \"+ } catch (error) {\",\n \"+ const message = error instanceof Error ? error.message : \\\"嵌入失败\\\";\",\n \"+ window.alert(message);\",\n \"+ return;\",\n \"+ }\",\n \"+\",\n \"+ window.alert(\\\"已嵌入到目标页面\\\");\",\n \"+ },\",\n \"+ });\",\n \"+ }, [documentId, openMoveEmbedPicker, readOnly, router, workspaceId]);\",\n \"+\",\n \"+ const handleAddToTemplates = useCallback(async () => {\",\n \"+ if (readOnly) return;\",\n \"+ const ok = window.confirm(\\\"将该页面添加为模板?\\\");\",\n \"+ if (!ok) return;\",\n \"+ const resp = await fetch(\\\"/api/documents/template\\\", {\",\n \"+ method: \\\"POST\\\",\",\n \"+ headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"+ body: JSON.stringify({ documentId, isTemplate: true }),\",\n \"+ });\",\n \"+ if (!resp.ok) {\",\n \"+ const payload = await resp.json().catch(() => ({}));\",\n \"+ window.alert(payload?.error ?? \\\"设置模板失败\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ window.alert(\\\"已添加为模板\\\");\",\n \"+ router.refresh();\",\n \"+ }, [documentId, readOnly, router]);\",\n \"+\",\n \"+ const formattedUpdatedAt = useMemo(() => {\",\n \"+ if (!updatedAt) return \\\"\\\";\",\n \"+ const date = new Date(updatedAt);\",\n \"+ if (Number.isNaN(date.getTime())) return \\\"\\\";\",\n \"+ return new Intl.DateTimeFormat(\\\"zh-CN\\\", {\",\n \"+ timeZone: \\\"Asia/Shanghai\\\",\",\n \"+ year: \\\"numeric\\\",\",\n \"+ month: \\\"numeric\\\",\",\n \"+ day: \\\"numeric\\\",\",\n \"+ hour: \\\"2-digit\\\",\",\n \"+ minute: \\\"2-digit\\\",\",\n \"+ second: \\\"2-digit\\\",\",\n \"+ hour12: false,\",\n \"+ }).format(date);\",\n \"+ }, [updatedAt]);\",\n \"+\",\n \"+ const handleExport = useCallback(() => {\",\n \"+ if (disableDownload) {\",\n \"+ window.alert(\\\"该页面已禁止下载\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ const latest = history[0];\",\n \"+ const exportBlocks = latest?.blocks ?? latestBlocksRef.current;\",\n \"+ if (!exportBlocks) {\",\n \"+ window.alert(\\\"暂无可导出的内容\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ const payload = JSON.stringify(exportBlocks, null, 2);\",\n \"+ const blob = new Blob([payload], { type: \\\"application/json\\\" });\",\n \"+ const url = URL.createObjectURL(blob);\",\n \"+ const anchor = document.createElement(\\\"a\\\");\",\n \"+ anchor.href = url;\",\n \"+ anchor.download = `${title ?? \\\"未命名页面\\\"}-${new Date().toISOString()}.json`;\",\n \"+ anchor.click();\",\n \"+ URL.revokeObjectURL(url);\",\n \"+ }, [disableDownload, history, title]);\",\n \"+\",\n \"+ const pageRootClass = cn(\",\n \"+ \\\"flex h-full overflow-hidden bg-wolai-bg\\\",\",\n \"+ options.pageFont === \\\"song\\\" && \\\"wolai-page-font-song\\\",\",\n \"+ options.pageFont === \\\"kai\\\" && \\\"wolai-page-font-kai\\\",\",\n \"+ options.layoutDensity === \\\"compact\\\" && \\\"wolai-page-density-compact\\\",\",\n \"+ options.layoutDensity === \\\"spacious\\\" && \\\"wolai-page-density-spacious\\\",\",\n \"+ options.smallText && \\\"wolai-small-text\\\",\",\n \"+ options.hideChildPages && \\\"wolai-hide-child-pages\\\",\",\n \"+ );\",\n \"+ const pageSubtree = useMemo(() => {\",\n \"+ const hasServerPageSubtree = Boolean(serverPageSubtreeSnapshot);\",\n \"+ const titleUnchanged = pageTitle === serverPageSubtreeTitle;\",\n \"+ const contentUnchanged = content === serverContentSnapshot;\",\n \"+\",\n \"+ if (hasServerPageSubtree && titleUnchanged && contentUnchanged) {\",\n \"+ return serverPageSubtreeSnapshot;\",\n \"+ }\",\n \"+ return null;\",\n \"+ }, [\",\n \"+ content,\",\n \"+ pageTitle,\",\n \"+ serverContentSnapshot,\",\n \"+ serverPageSubtreeSnapshot,\",\n \"+ serverPageSubtreeTitle,\",\n \"+ ]);\",\n \"+ const readViewTocEntries = useMemo(\",\n \"+ () =>\",\n \"+ (pageSubtree?.outline ?? [])\",\n \"+ .filter((entry) => typeof entry.anchorBlockId === \\\"string\\\" && entry.anchorBlockId.trim())\",\n \"+ .map(({ anchorBlockId, level, numbering, title: entryTitle }) => ({\",\n \"+ id: anchorBlockId as string,\",\n \"+ level,\",\n \"+ numbering,\",\n \"+ title: entryTitle,\",\n \"+ })),\",\n \"+ [pageSubtree],\",\n \"+ );\",\n \"+ const inspectorCanUseEditorBridge = canEditDocument && isEditing;\",\n \"+\",\n \"+ const jumpToHeading = useCallback((headingId: string) => {\",\n \"+ const targetRoot = !isEditing ? readViewRootRef.current : pageRootRef.current;\",\n \"+ const target = targetRoot?.querySelector(`[data-id=\\\"${headingId}\\\"]`);\",\n \"+ if (target) {\",\n \"+ target.scrollIntoView({ behavior: \\\"smooth\\\", block: \\\"center\\\" });\",\n \"+ }\",\n \"+ }, [isEditing]);\",\n \"+\",\n \"+ const handleSnapshot = useCallback((payload: { blocks: Json; stats: DocumentStats }) => {\",\n \"+ setContent(payload.blocks);\",\n \"+ latestBlocksRef.current = payload.blocks;\",\n \"+ setHistory((prev) => {\",\n \"+ const now = Date.now();\",\n \"+ if (prev.length > 0 && now - prev[0].timestamp < 4000) {\",\n \"+ return prev;\",\n \"+ }\",\n \"+ const snapshot: DocumentSnapshot = {\",\n \"+ id: `${now}-${Math.random().toString(36).slice(2, 8)}`,\",\n \"+ timestamp: now,\",\n \"+ blocks: payload.blocks,\",\n \"+ stats: payload.stats,\",\n \"+ };\",\n \"+ return [snapshot, ...prev].slice(0, 15);\",\n \"+ });\",\n \"+ }, []);\",\n \"+\",\n \"+ const persistStatsRequest = useCallback((next: DocumentStats) => {\",\n \"+ void fetch(\\\"/api/documents/stats\\\", {\",\n \"+ method: \\\"POST\\\",\",\n \"+ headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"+ body: JSON.stringify({ documentId, workspaceId, stats: next }),\",\n \"+ }).catch((error) => console.error(error));\",\n \"+ }, [documentId, workspaceId]);\",\n \"+\",\n \"+ const persistStats = useDebouncedCallback(persistStatsRequest, 1500);\",\n \"+\",\n \"+ const handleStatsChange = useCallback(\",\n \"+ (nextStats: DocumentStats) => {\",\n \"+ setStats(nextStats);\",\n \"+ persistStats(nextStats);\",\n \"+ },\",\n \"+ [persistStats],\",\n \"+ );\",\n \"+\",\n \"+ const handleRestoreSnapshot = useCallback(\",\n \"+ (snapshot: DocumentSnapshot) => {\",\n \"+ if (!isEditing || !editorBridge) {\",\n \"+ pendingRestoreSnapshotRef.current = snapshot;\",\n \"+ setIsEditing(true);\",\n \"+ return;\",\n \"+ }\",\n \"+ editorBridge.replaceWithSnapshot(snapshot.blocks);\",\n \"+ setHistoryOpen(false);\",\n \"+ },\",\n \"+ [editorBridge, isEditing],\",\n \"+ );\",\n \"+\",\n \"+ const handleEnterEditMode = useCallback(() => {\",\n \"+ if (!canEditDocument) return;\",\n \"+ setIsEditing(true);\",\n \"+ }, [canEditDocument]);\",\n \"+\",\n \"+ const handleExitEditMode = useCallback(() => {\",\n \"+ setIsEditing(false);\",\n \"+ }, []);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ if (!isEditing) return;\",\n \"+ if (!editorBridge) return;\",\n \"+ const pendingSnapshot = pendingRestoreSnapshotRef.current;\",\n \"+ if (!pendingSnapshot) return;\",\n \"+ pendingRestoreSnapshotRef.current = null;\",\n \"+ editorBridge.replaceWithSnapshot(pendingSnapshot.blocks);\",\n \"+ setHistoryOpen(false);\",\n \"+ }, [editorBridge, isEditing]);\",\n \"+ return (\",\n \"+ \",\n \"+
\",\n \"+
\",\n \"+
\",\n \"+
\",\n \"+
\",\n \"+ {isEditing && canEditDocument ? (\",\n \"+
\",\n \"+ \",\n \"+
\",\n \"+ ) : (\",\n \"+

\",\n \"+ {pageTitle || \\\"无标题\\\"}\",\n \"+

\",\n \"+ )}\",\n \"+ {readOnly ? (\",\n \"+

该页面为只读共享,无法编辑。

\",\n \"+ ) : options.protectEditing && isEditing ? (\",\n \"+

当前页面已开启编辑保护,关闭后方可修改内容。

\",\n \"+ ) : !isEditing && canEditDocument ? (\",\n \"+

当前为阅读态,编辑器仅在进入编辑后挂载。

\",\n \"+ ) : null}\",\n \"+

最近更新:{formattedUpdatedAt}

\",\n \"+
\",\n \"+ {canEditDocument && (\",\n \"+
\",\n \"+ {isEditing ? (\",\n \"+ \",\n \"+ ) : (\",\n \"+ \",\n \"+ )}\",\n \"+
\",\n \"+ )}\",\n \"+
\",\n \"+
\",\n \"+
\",\n \"+ {contentLoading ? (\",\n \"+ showContentLoadingIndicator ? (\",\n \"+
\",\n \"+ 页面内容加载中...\",\n \"+
\",\n \"+ ) : (\",\n \"+
\",\n \"+ )\",\n \"+ ) : contentError ? (\",\n \"+
\",\n \"+
{contentError}
\",\n \"+ {\",\n \"+ setContentError(null);\",\n \"+ setContentLoading(true);\",\n \"+ setContentReloadKey((prev) => prev + 1);\",\n \"+ }}\",\n \"+ >\",\n \"+ 重试\",\n \"+ \",\n \"+
\",\n \"+ ) : (\",\n \"+
\",\n \"+ {keepEditorMounted && (\",\n \"+
\",\n \"+ {\",\n \"+ setContentRevision(meta.revision);\",\n \"+ setConflictDetectionKey(meta.conflictDetectionKey);\",\n \"+ }}\",\n \"+ />\",\n \"+
\",\n \"+ )}\",\n \"+ {!isEditing && (\",\n \"+
\",\n \"+ \",\n \"+ \",\n \"+
\",\n \"+ )}\",\n \"+
\",\n \"+ )}\",\n \"+ \",\n \"+
\",\n \"+
\",\n \"+ {showInspector && (\",\n \"+ setHistoryOpen(true)}\",\n \"+ onOpenComments={() => openCommentsForPage({ workspaceId, documentId })}\",\n \"+ onUndo={canEditDocument ? handleUndo : undefined}\",\n \"+ onRedo={canEditDocument ? handleRedo : undefined}\",\n \"+ onDeletePage={canEditDocument ? handleDeletePage : undefined}\",\n \"+ onOpenMoveEmbedPicker={canEditDocument ? handleOpenMoveEmbed : undefined}\",\n \"+ onCopyPageLink={handleCopyPageLink}\",\n \"+ onCopyPageReference={handleCopyPageReference}\",\n \"+ onAddToTemplates={canEditDocument ? handleAddToTemplates : undefined}\",\n \"+ />\",\n \"+ )}\",\n \"+
\",\n \"+ \",\n \"+ \",\n \"+ \",\n \"+ latestBlocksRef.current}\",\n \"+ getLatestPageSubtree={() => pageSubtree}\",\n \"+ />\",\n \"+ \",\n \"+ );\",\n \"+}\"\n ]\n }\n ],\n \"userModified\": false,\n \"replaceAll\": false,\n \"gitDiff\": null\n}","tool_name":"edit_file","tool_use_id":"call_SSO1jfLdk6z3L2iDwI5SvUJU","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_hfnNUfjkii3uvyEWssUihpBS","input":"{\"new_string\":\" {readOnly ? (\\n

该页面为只读共享,无法编辑。

\\n ) : options.protectEditing && isEditing ? (\\n

当前页面已开启编辑保护,关闭后方可修改内容。

\\n ) : null}\\n\",\"old_string\":\" {readOnly ? (\\n

该页面为只读共享,无法编辑。

\\n ) : options.protectEditing && isEditing ? (\\n

当前页面已开启编辑保护,关闭后方可修改内容。

\\n ) : !isEditing && canEditDocument ? (\\n

当前为阅读态,编辑器仅在进入编辑后挂载。

\\n ) : null}\\n\",\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx\",\"replace_all\":false}","name":"edit_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":113759,"output_tokens":280}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx\",\n \"oldString\": \" {readOnly ? (\\n

该页面为只读共享,无法编辑。

\\n ) : options.protectEditing && isEditing ? (\\n

当前页面已开启编辑保护,关闭后方可修改内容。

\\n ) : !isEditing && canEditDocument ? (\\n

当前为阅读态,编辑器仅在进入编辑后挂载。

\\n ) : null}\\n\",\n \"newString\": \" {readOnly ? (\\n

该页面为只读共享,无法编辑。

\\n ) : options.protectEditing && isEditing ? (\\n

当前页面已开启编辑保护,关闭后方可修改内容。

\\n ) : null}\\n\",\n \"originalFile\": \"\\\"use client\\\";\\n\\nimport dynamic from \\\"next/dynamic\\\";\\nimport { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent as ReactKeyboardEvent } from \\\"react\\\";\\nimport type { BooleanPageOptionKey, DocumentStats, PageOptionsState, PageFont, PageLayoutDensity } from \\\"@/types/page-options\\\";\\nimport { useEditorBridgeStore } from \\\"@/store/editor-bridge\\\";\\nimport { usePageLayoutStore } from \\\"@/store/page-layout\\\";\\nimport { useCurrentDocumentStore } from \\\"@/store/current-document\\\";\\nimport type { Json } from \\\"@/types/supabase\\\";\\nimport type { DocumentSnapshot } from \\\"@/types/document\\\";\\nimport { useDebouncedCallback } from \\\"@/hooks/use-debounced-callback\\\";\\nimport { ImagePickerProvider } from \\\"@/components/media/image-picker-context\\\";\\nimport { useRouter } from \\\"next/navigation\\\";\\nimport { DocumentAiAgentPanel } from \\\"./DocumentAiAgentPanel\\\";\\nimport { CONTENT_LOADING_DELAY_MS } from \\\"@/lib/constants\\\";\\nimport { useCommentsUiStore } from \\\"@/store/comments-ui\\\";\\nimport { useAppPreferencesStore } from \\\"@/store/app-preferences\\\";\\nimport { useMoveEmbedPickerStore } from \\\"@/store/move-embed-picker\\\";\\nimport { cn } from \\\"@/lib/utils\\\";\\nimport { Button } from \\\"@/components/ui/button\\\";\\nimport { DocumentToc } from \\\"@/components/editor/document-toc\\\";\\nimport { DocumentReadView } from \\\"@/components/editor/document-read-view\\\";\\nimport { extractPageBlocks, type PageSubtreeProjection } from \\\"@/lib/documents/page-subtree\\\";\\nimport {\\n deleteDocumentCommand,\\n embedDocumentCommand,\\n moveDocumentCommand,\\n renameDocumentCommand,\\n} from \\\"@/lib/documents/tree-command-client\\\";\\nimport { EditorHost } from \\\"@/components/editor/editor-host\\\";\\n\\nconst PageOptionsSidebar = dynamic(\\n () => import(\\\"@/components/editor/page-options-sidebar\\\").then((mod) => mod.PageOptionsSidebar),\\n { ssr: false },\\n);\\n\\nconst PageBacklinksPanel = dynamic(\\n () => import(\\\"@/components/editor/page-backlinks-panel\\\").then((mod) => mod.PageBacklinksPanel),\\n {\\n ssr: false,\\n loading: () => null,\\n },\\n);\\n\\nconst DocumentHistoryDrawer = dynamic(\\n () => import(\\\"@/components/editor/document-history-drawer\\\").then((mod) => mod.DocumentHistoryDrawer),\\n {\\n ssr: false,\\n loading: () => null,\\n },\\n);\\n\\nconst MoveEmbedPickerHost = dynamic(\\n () => import(\\\"@/components/documents/move-embed-picker-host\\\").then((mod) => mod.MoveEmbedPickerHost),\\n {\\n ssr: false,\\n loading: () => null,\\n },\\n);\\n\\nconst DocumentCommentsDrawer = dynamic(\\n () => import(\\\"@/components/editor/document-comments-drawer\\\").then((mod) => mod.DocumentCommentsDrawer),\\n {\\n ssr: false,\\n loading: () => null,\\n },\\n);\\n\\nexport interface DocumentContentProps {\\n documentId: string;\\n workspaceId: string;\\n title: string | null;\\n updatedAt: string | null;\\n initialContent: unknown;\\n initialContentRevision?: number | null;\\n initialConflictDetectionKey?: string | null;\\n initialPageSubtree?: PageSubtreeProjection | null;\\n initialOptions: PageOptionsState;\\n initialStats: DocumentStats | null;\\n openTableId?: string | null;\\n autoEnterEdit?: boolean;\\n readOnly?: boolean;\\n disableDownload?: boolean;\\n disableCopy?: boolean;\\n}\\n\\nconst defaultOptions: PageOptionsState = {\\n wideLayout: false,\\n smallText: false,\\n showHeadingNumbers: true,\\n showToc: false,\\n showStructure: false,\\n protectEditing: false,\\n showWordCount: true,\\n collapseBacklinks: false,\\n pageFont: \\\"default\\\",\\n layoutDensity: \\\"normal\\\",\\n hideChildPages: false,\\n showBlockRefCount: false,\\n embedDefaultBlockId: null,\\n};\\nconst defaultStats: DocumentStats = { wordCount: 0, characterCount: 0, blockCount: 0, todoTotal: 0, todoDone: 0 };\\nconst EDITOR_UNMOUNT_GRACE_MS = 1000;\\n\\nexport function DocumentContent({\\n documentId,\\n workspaceId,\\n title,\\n updatedAt,\\n initialContent,\\n initialContentRevision = null,\\n initialConflictDetectionKey = null,\\n initialPageSubtree = null,\\n initialOptions,\\n initialStats,\\n openTableId,\\n autoEnterEdit = false,\\n readOnly = false,\\n disableDownload = false,\\n disableCopy = false,\\n}: DocumentContentProps) {\\n const setCurrentDocument = useCurrentDocumentStore((state) => state.setCurrent);\\n const clearIfMatch = useCurrentDocumentStore((state) => state.clearIfMatch);\\n const canEditDocument = !readOnly;\\n const [options, setOptions] = useState(initialOptions ?? defaultOptions);\\n const [stats, setStats] = useState(initialStats ?? defaultStats);\\n const [history, setHistory] = useState([]);\\n const [historyOpen, setHistoryOpen] = useState(false);\\n const editorBridge = useEditorBridgeStore((state) => state.bridge);\\n const router = useRouter();\\n const showInspector = usePageLayoutStore((state) => state.showInspector);\\n const openCommentsForPage = useCommentsUiStore((s) => s.openForPage);\\n const spellCheck = useAppPreferencesStore((s) => s.spellCheck);\\n const openMoveEmbedPicker = useMoveEmbedPickerStore((s) => s.openPicker);\\n const [pageTitle, setPageTitle] = useState(title ?? \\\"无标题\\\");\\n const [content, setContent] = useState(initialContent);\\n const [serverContentSnapshot, setServerContentSnapshot] = useState(initialContent);\\n const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState(\\n initialPageSubtree,\\n );\\n const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState(title ?? \\\"无标题\\\");\\n const [contentRevision, setContentRevision] = useState(initialContentRevision);\\n const [conflictDetectionKey, setConflictDetectionKey] = useState(initialConflictDetectionKey);\\n const [contentLoading, setContentLoading] = useState(() => initialContent == null);\\n const [contentError, setContentError] = useState(null);\\n const [contentReloadKey, setContentReloadKey] = useState(0);\\n const [showContentLoadingIndicator, setShowContentLoadingIndicator] = useState(false);\\n const shouldStartEditing = canEditDocument;\\n const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\\n const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\\n const contentLoadingTimerRef = useRef | null>(null);\\n const editorUnmountTimerRef = useRef | null>(null);\\n const pendingOpenTableRef = useRef(null);\\n const latestBlocksRef = useRef(null);\\n const pageRootRef = useRef(null);\\n const readViewRootRef = useRef(null);\\n const pendingRestoreSnapshotRef = useRef(null);\\n const lastCopyBlockedAtRef = useRef(0);\\n\\n useEffect(() => {\\n setCurrentDocument(documentId, readOnly, disableDownload, disableCopy);\\n return () => {\\n clearIfMatch(documentId);\\n };\\n }, [clearIfMatch, disableCopy, disableDownload, documentId, readOnly, setCurrentDocument]);\\n\\n useEffect(() => {\\n if (!disableCopy) return;\\n\\n const toElement = (node: Node | null): Element | null => {\\n if (!node) return null;\\n if (node.nodeType === Node.TEXT_NODE) {\\n return node.parentElement;\\n }\\n return node instanceof Element ? node : null;\\n };\\n\\n const isEventInsidePage = () => {\\n const root = pageRootRef.current;\\n if (!root) return false;\\n const selection = typeof window !== \\\"undefined\\\" ? window.getSelection() : null;\\n const anchor = selection?.anchorNode ?? null;\\n const focus = selection?.focusNode ?? null;\\n const anchorEl = toElement(anchor);\\n const focusEl = toElement(focus);\\n return Boolean((anchorEl && root.contains(anchorEl)) || (focusEl && root.contains(focusEl)));\\n };\\n\\n const notifyBlocked = () => {\\n const now = Date.now();\\n if (now - lastCopyBlockedAtRef.current < 1200) return;\\n lastCopyBlockedAtRef.current = now;\\n window.alert(\\\"该页面已禁止复制\\\");\\n };\\n\\n const onCopy = (event: ClipboardEvent) => {\\n if (!isEventInsidePage()) return;\\n event.preventDefault();\\n event.stopPropagation();\\n notifyBlocked();\\n };\\n\\n const onCut = (event: ClipboardEvent) => {\\n if (!isEventInsidePage()) return;\\n event.preventDefault();\\n event.stopPropagation();\\n notifyBlocked();\\n };\\n\\n const onKeyDown = (event: KeyboardEvent) => {\\n if (!isEventInsidePage()) return;\\n const key = String(event.key ?? \\\"\\\").toLowerCase();\\n const ctrlOrMeta = event.ctrlKey || event.metaKey;\\n if (!ctrlOrMeta) return;\\n if (key === \\\"c\\\" || key === \\\"x\\\" || key === \\\"insert\\\") {\\n event.preventDefault();\\n event.stopPropagation();\\n notifyBlocked();\\n }\\n };\\n\\n document.addEventListener(\\\"copy\\\", onCopy, true);\\n document.addEventListener(\\\"cut\\\", onCut, true);\\n document.addEventListener(\\\"keydown\\\", onKeyDown, true);\\n return () => {\\n document.removeEventListener(\\\"copy\\\", onCopy, true);\\n document.removeEventListener(\\\"cut\\\", onCut, true);\\n document.removeEventListener(\\\"keydown\\\", onKeyDown, true);\\n };\\n }, [disableCopy]);\\n\\n useEffect(() => {\\n const tableId = (openTableId ?? \\\"\\\").trim();\\n if (!tableId) return;\\n if (!editorBridge?.openTableFullScreen) return;\\n if (pendingOpenTableRef.current === tableId) return;\\n pendingOpenTableRef.current = tableId;\\n\\n editorBridge.openTableFullScreen(tableId);\\n\\n if (typeof window !== \\\"undefined\\\") {\\n try {\\n const url = new URL(window.location.href);\\n url.searchParams.delete(\\\"openTableId\\\");\\n window.history.replaceState({}, \\\"\\\", url.toString());\\n } catch {\\n router.replace(`/documents/${documentId}`);\\n }\\n }\\n }, [documentId, editorBridge, openTableId, router]);\\n\\n useEffect(() => {\\n setPageTitle(title ?? \\\"无标题\\\");\\n }, [title]);\\n\\n useEffect(() => {\\n setServerPageSubtreeTitle(title ?? \\\"无标题\\\");\\n }, [title]);\\n\\n useEffect(() => {\\n setServerPageSubtreeSnapshot(initialPageSubtree);\\n }, [initialPageSubtree]);\\n\\n useEffect(() => {\\n setOptions(initialOptions ?? defaultOptions);\\n }, [initialOptions]);\\n\\n useEffect(() => {\\n setStats(initialStats ?? defaultStats);\\n }, [initialStats]);\\n\\n useEffect(() => {\\n setContentRevision(initialContentRevision);\\n }, [initialContentRevision]);\\n\\n useEffect(() => {\\n setConflictDetectionKey(initialConflictDetectionKey);\\n }, [initialConflictDetectionKey]);\\n\\n useEffect(() => {\\n setServerContentSnapshot(initialContent);\\n }, [initialContent]);\\n\\n useEffect(() => {\\n const nextBlocks = extractPageBlocks(content);\\n latestBlocksRef.current = nextBlocks.length > 0 ? (nextBlocks as Json) : null;\\n }, [content]);\\n\\n useEffect(() => {\\n const shouldForceEdit = Boolean((openTableId ?? \\\"\\\").trim()) && canEditDocument;\\n if (shouldForceEdit) {\\n setIsEditing(true);\\n setKeepEditorMounted(true);\\n }\\n }, [canEditDocument, openTableId]);\\n\\n useEffect(() => {\\n if (isEditing) {\\n if (editorUnmountTimerRef.current) {\\n clearTimeout(editorUnmountTimerRef.current);\\n editorUnmountTimerRef.current = null;\\n }\\n setKeepEditorMounted(true);\\n return;\\n }\\n if (editorUnmountTimerRef.current) {\\n clearTimeout(editorUnmountTimerRef.current);\\n }\\n editorUnmountTimerRef.current = setTimeout(() => {\\n setKeepEditorMounted(false);\\n editorUnmountTimerRef.current = null;\\n }, EDITOR_UNMOUNT_GRACE_MS);\\n return () => {\\n if (editorUnmountTimerRef.current) {\\n clearTimeout(editorUnmountTimerRef.current);\\n editorUnmountTimerRef.current = null;\\n }\\n };\\n }, [isEditing]);\\n\\n useEffect(() => {\\n let canceled = false;\\n const controller = new AbortController();\\n\\n const load = async () => {\\n setContentError(null);\\n setContentLoading(initialContent == null);\\n setContent(initialContent);\\n setShowContentLoadingIndicator(false);\\n\\n if (initialContent != null) {\\n return;\\n }\\n\\n if (contentLoadingTimerRef.current) {\\n clearTimeout(contentLoadingTimerRef.current);\\n contentLoadingTimerRef.current = null;\\n }\\n contentLoadingTimerRef.current = setTimeout(() => {\\n if (!canceled) {\\n setShowContentLoadingIndicator(true);\\n }\\n }, CONTENT_LOADING_DELAY_MS);\\n\\n try {\\n const response = await fetch(\\n `/api/documents/content?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,\\n {\\n method: \\\"GET\\\",\\n credentials: \\\"include\\\",\\n signal: controller.signal,\\n },\\n );\\n if (!response.ok) {\\n const payload = await response.json().catch(() => ({}));\\n throw new Error(payload?.error ?? \\\"加载页面内容失败\\\");\\n }\\n const payload = (await response.json()) as {\\n content?: unknown;\\n revision?: number | null;\\n conflictDetectionKey?: string | null;\\n pageSubtree?: PageSubtreeProjection | null;\\n };\\n if (canceled) return;\\n setContent(payload.content ?? null);\\n setServerContentSnapshot(payload.content ?? null);\\n setContentRevision(\\n typeof payload.revision === \\\"number\\\" && Number.isInteger(payload.revision)\\n ? payload.revision\\n : 0,\\n );\\n setConflictDetectionKey(\\n typeof payload.conflictDetectionKey === \\\"string\\\" && payload.conflictDetectionKey.trim()\\n ? payload.conflictDetectionKey\\n : `${documentId}:0`,\\n );\\n setServerPageSubtreeSnapshot(payload.pageSubtree ?? null);\\n setServerPageSubtreeTitle(\\n typeof payload.pageSubtree?.rootNode.metadata.title === \\\"string\\\" &&\\n payload.pageSubtree.rootNode.metadata.title.trim()\\n ? payload.pageSubtree.rootNode.metadata.title\\n : title ?? \\\"无标题\\\",\\n );\\n } catch (error) {\\n if (canceled) return;\\n if ((error as { name?: string })?.name === \\\"AbortError\\\") return;\\n setContentError(error instanceof Error ? error.message : \\\"加载页面内容失败\\\");\\n } finally {\\n if (!canceled) {\\n setContentLoading(false);\\n setShowContentLoadingIndicator(false);\\n }\\n if (contentLoadingTimerRef.current) {\\n clearTimeout(contentLoadingTimerRef.current);\\n contentLoadingTimerRef.current = null;\\n }\\n }\\n };\\n\\n void load();\\n\\n return () => {\\n canceled = true;\\n controller.abort();\\n if (contentLoadingTimerRef.current) {\\n clearTimeout(contentLoadingTimerRef.current);\\n contentLoadingTimerRef.current = null;\\n }\\n };\\n }, [documentId, initialContent, contentReloadKey, title, workspaceId]);\\n\\n const persistTitle = useCallback(\\n async (nextTitle: string) => {\\n if (readOnly) return;\\n const payload = nextTitle.trim() || \\\"无标题\\\";\\n try {\\n await renameDocumentCommand({ documentId, workspaceId, title: payload });\\n } catch (error) {\\n console.error(\\\"更新页面标题失败\\\", error);\\n }\\n },\\n [documentId, readOnly, workspaceId],\\n );\\n\\n const debouncedPersistTitle = useDebouncedCallback((value: string) => {\\n void persistTitle(value);\\n }, 600);\\n\\n const handleTitleChange = (event: ChangeEvent) => {\\n if (!canEditDocument) return;\\n const value = event.target.value;\\n setPageTitle(value);\\n debouncedPersistTitle(value);\\n };\\n\\n const handleTitleBlur = () => {\\n if (!canEditDocument) return;\\n void persistTitle(pageTitle);\\n };\\n\\n const handleTitleKeyDown = (event: ReactKeyboardEvent) => {\\n if (event.key === \\\"Enter\\\") {\\n event.preventDefault();\\n event.currentTarget.blur();\\n }\\n };\\n\\n const persistOptions = useCallback(\\n async (patch: Partial) => {\\n if (readOnly) return;\\n try {\\n const response = await fetch(\\\"/api/documents/options\\\", {\\n method: \\\"POST\\\",\\n headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\\n body: JSON.stringify({ documentId, workspaceId, options: patch }),\\n });\\n if (!response.ok) {\\n const payload = await response.json().catch(() => null);\\n console.error(payload?.error ?? \\\"更新页面选项失败\\\");\\n }\\n } catch (error) {\\n console.error(error);\\n }\\n },\\n [documentId, readOnly, workspaceId],\\n );\\n\\n const toggleOption = useCallback(\\n (key: BooleanPageOptionKey) => {\\n if (readOnly) return;\\n setOptions((prev) => {\\n const nextValue = !prev[key];\\n const next = { ...prev, [key]: nextValue };\\n void persistOptions({ [key]: nextValue } as Partial);\\n return next;\\n });\\n },\\n [persistOptions, readOnly],\\n );\\n\\n const setOptionPatch = useCallback(\\n (patch: Partial) => {\\n if (readOnly) return;\\n setOptions((prev) => {\\n const next = { ...prev, ...patch };\\n void persistOptions(patch);\\n return next;\\n });\\n },\\n [persistOptions, readOnly],\\n );\\n\\n const closeToc = useCallback(() => {\\n if (readOnly) return;\\n setOptions((prev) => {\\n if (!prev.showToc) return prev;\\n const next = { ...prev, showToc: false };\\n void persistOptions({ showToc: false });\\n return next;\\n });\\n }, [persistOptions, readOnly]);\\n\\n const handleSetPageFont = useCallback(\\n (font: PageFont) => {\\n setOptionPatch({ pageFont: font });\\n },\\n [setOptionPatch],\\n );\\n\\n const handleSetLayoutDensity = useCallback(\\n (density: PageLayoutDensity) => {\\n setOptionPatch({ layoutDensity: density });\\n },\\n [setOptionPatch],\\n );\\n\\n const handleSetEmbedDefaultToCursor = useCallback(() => {\\n if (!canEditDocument) return;\\n if (!isEditing) {\\n setIsEditing(true);\\n return;\\n }\\n const blockId = editorBridge?.getCursorBlockId?.() ?? null;\\n if (!blockId) {\\n window.alert(\\\"未找到当前光标所在块,请先在编辑器中点击任意块\\\");\\n return;\\n }\\n setOptionPatch({ embedDefaultBlockId: blockId });\\n window.alert(\\\"已设置“嵌入默认位置”\\\");\\n }, [canEditDocument, editorBridge, isEditing, setOptionPatch]);\\n\\n const handleClearEmbedDefault = useCallback(() => {\\n if (readOnly) return;\\n setOptionPatch({ embedDefaultBlockId: null });\\n window.alert(\\\"已清除“嵌入默认位置”\\\");\\n }, [readOnly, setOptionPatch]);\\n\\n useEffect(() => {\\n const onKeyDown = (event: KeyboardEvent) => {\\n const root = pageRootRef.current;\\n if (root) {\\n const target = event.target;\\n if (target && target instanceof Node && !root.contains(target)) {\\n return;\\n }\\n }\\n const ctrlOrMeta = event.ctrlKey || event.metaKey;\\n if (!ctrlOrMeta) return;\\n if (!event.shiftKey) return;\\n const key = String(event.key ?? \\\"\\\").toLowerCase();\\n if (key === \\\"l\\\") {\\n event.preventDefault();\\n toggleOption(\\\"showToc\\\");\\n } else if (key === \\\"c\\\") {\\n event.preventDefault();\\n toggleOption(\\\"protectEditing\\\");\\n }\\n };\\n window.addEventListener(\\\"keydown\\\", onKeyDown);\\n return () => window.removeEventListener(\\\"keydown\\\", onKeyDown);\\n }, [toggleOption]);\\n\\n const buildDocumentUrl = useCallback((id: string): string => {\\n if (typeof window === \\\"undefined\\\" || !window.location) {\\n return `/documents/${id}`;\\n }\\n return `${window.location.origin}/documents/${id}`;\\n }, []);\\n\\n const copyText = useCallback(async (text: string, successMessage: string) => {\\n if (typeof navigator !== \\\"undefined\\\" && navigator.clipboard) {\\n try {\\n await navigator.clipboard.writeText(text);\\n window.alert(successMessage);\\n return;\\n } catch {\\n }\\n }\\n window.prompt(\\\"复制失败,请手动复制内容\\\", text);\\n }, []);\\n\\n const handleCopyPageLink = useCallback(\\n async (includeTitle: boolean) => {\\n const url = buildDocumentUrl(documentId);\\n if (includeTitle) {\\n const text = `${pageTitle || \\\"无标题\\\"}\\\\n${url}`;\\n await copyText(text, \\\"标题 + 链接已复制\\\");\\n return;\\n }\\n await copyText(url, \\\"页面链接已复制\\\");\\n },\\n [buildDocumentUrl, copyText, documentId, pageTitle],\\n );\\n\\n const handleCopyPageReference = useCallback(\\n async (mode: \\\"inline\\\" | \\\"embed\\\") => {\\n const template = mode === \\\"inline\\\" ? `((${documentId}))` : `{{${documentId}}}`;\\n await copyText(template, mode === \\\"inline\\\" ? \\\"行内引用已复制\\\" : \\\"嵌入引用已复制\\\");\\n },\\n [copyText, documentId],\\n );\\n\\n const handleUndo = useCallback(() => {\\n if (!isEditing) {\\n setIsEditing(true);\\n return;\\n }\\n editorBridge?.undo?.();\\n }, [editorBridge, isEditing]);\\n\\n const handleRedo = useCallback(() => {\\n if (!isEditing) {\\n setIsEditing(true);\\n return;\\n }\\n editorBridge?.redo?.();\\n }, [editorBridge, isEditing]);\\n\\n const handleDeletePage = useCallback(async () => {\\n if (readOnly) return;\\n const ok = window.confirm(\\\"确定删除该页面吗?删除后会进入垃圾桶。\\\");\\n if (!ok) return;\\n try {\\n await deleteDocumentCommand({ documentId, workspaceId });\\n } catch (error) {\\n const payload = error instanceof Error ? error.message : null;\\n window.alert(payload ?? \\\"删除失败\\\");\\n return;\\n }\\n router.push(\\\"/\\\");\\n router.refresh();\\n }, [documentId, readOnly, router, workspaceId]);\\n\\n const handleOpenMoveEmbed = useCallback(() => {\\n if (readOnly) return;\\n openMoveEmbedPicker({\\n workspaceId,\\n defaultMode: \\\"move\\\",\\n modes: [\\\"move\\\", \\\"embed\\\"],\\n allowRoot: true,\\n excludeIds: [documentId],\\n onPick: async (mode, targetId) => {\\n if (mode === \\\"move\\\") {\\n try {\\n await moveDocumentCommand({\\n documentId,\\n parentId: targetId ?? null,\\n position: 999999,\\n workspaceId,\\n });\\n } catch (error) {\\n window.alert(error instanceof Error ? error.message : \\\"移动失败\\\");\\n return;\\n }\\n window.alert(\\\"移动成功\\\");\\n router.refresh();\\n return;\\n }\\n\\n if (!targetId) {\\n window.alert(\\\"请选择目标页面\\\");\\n return;\\n }\\n\\n try {\\n await embedDocumentCommand({\\n sourceId: documentId,\\n targetId,\\n });\\n } catch (error) {\\n const message = error instanceof Error ? error.message : \\\"嵌入失败\\\";\\n window.alert(message);\\n return;\\n }\\n\\n window.alert(\\\"已嵌入到目标页面\\\");\\n },\\n });\\n }, [documentId, openMoveEmbedPicker, readOnly, router, workspaceId]);\\n\\n const handleAddToTemplates = useCallback(async () => {\\n if (readOnly) return;\\n const ok = window.confirm(\\\"将该页面添加为模板?\\\");\\n if (!ok) return;\\n const resp = await fetch(\\\"/api/documents/template\\\", {\\n method: \\\"POST\\\",\\n headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\\n body: JSON.stringify({ documentId, isTemplate: true }),\\n });\\n if (!resp.ok) {\\n const payload = await resp.json().catch(() => ({}));\\n window.alert(payload?.error ?? \\\"设置模板失败\\\");\\n return;\\n }\\n window.alert(\\\"已添加为模板\\\");\\n router.refresh();\\n }, [documentId, readOnly, router]);\\n\\n const formattedUpdatedAt = useMemo(() => {\\n if (!updatedAt) return \\\"\\\";\\n const date = new Date(updatedAt);\\n if (Number.isNaN(date.getTime())) return \\\"\\\";\\n return new Intl.DateTimeFormat(\\\"zh-CN\\\", {\\n timeZone: \\\"Asia/Shanghai\\\",\\n year: \\\"numeric\\\",\\n month: \\\"numeric\\\",\\n day: \\\"numeric\\\",\\n hour: \\\"2-digit\\\",\\n minute: \\\"2-digit\\\",\\n second: \\\"2-digit\\\",\\n hour12: false,\\n }).format(date);\\n }, [updatedAt]);\\n\\n const handleExport = useCallback(() => {\\n if (disableDownload) {\\n window.alert(\\\"该页面已禁止下载\\\");\\n return;\\n }\\n const latest = history[0];\\n const exportBlocks = latest?.blocks ?? latestBlocksRef.current;\\n if (!exportBlocks) {\\n window.alert(\\\"暂无可导出的内容\\\");\\n return;\\n }\\n const payload = JSON.stringify(exportBlocks, null, 2);\\n const blob = new Blob([payload], { type: \\\"application/json\\\" });\\n const url = URL.createObjectURL(blob);\\n const anchor = document.createElement(\\\"a\\\");\\n anchor.href = url;\\n anchor.download = `${title ?? \\\"未命名页面\\\"}-${new Date().toISOString()}.json`;\\n anchor.click();\\n URL.revokeObjectURL(url);\\n }, [disableDownload, history, title]);\\n\\n const pageRootClass = cn(\\n \\\"flex h-full overflow-hidden bg-wolai-bg\\\",\\n options.pageFont === \\\"song\\\" && \\\"wolai-page-font-song\\\",\\n options.pageFont === \\\"kai\\\" && \\\"wolai-page-font-kai\\\",\\n options.layoutDensity === \\\"compact\\\" && \\\"wolai-page-density-compact\\\",\\n options.layoutDensity === \\\"spacious\\\" && \\\"wolai-page-density-spacious\\\",\\n options.smallText && \\\"wolai-small-text\\\",\\n options.hideChildPages && \\\"wolai-hide-child-pages\\\",\\n );\\n const pageSubtree = useMemo(() => {\\n const hasServerPageSubtree = Boolean(serverPageSubtreeSnapshot);\\n const titleUnchanged = pageTitle === serverPageSubtreeTitle;\\n const contentUnchanged = content === serverContentSnapshot;\\n\\n if (hasServerPageSubtree && titleUnchanged && contentUnchanged) {\\n return serverPageSubtreeSnapshot;\\n }\\n return null;\\n }, [\\n content,\\n pageTitle,\\n serverContentSnapshot,\\n serverPageSubtreeSnapshot,\\n serverPageSubtreeTitle,\\n ]);\\n const readViewTocEntries = useMemo(\\n () =>\\n (pageSubtree?.outline ?? [])\\n .filter((entry) => typeof entry.anchorBlockId === \\\"string\\\" && entry.anchorBlockId.trim())\\n .map(({ anchorBlockId, level, numbering, title: entryTitle }) => ({\\n id: anchorBlockId as string,\\n level,\\n numbering,\\n title: entryTitle,\\n })),\\n [pageSubtree],\\n );\\n const inspectorCanUseEditorBridge = canEditDocument && isEditing;\\n\\n const jumpToHeading = useCallback((headingId: string) => {\\n const targetRoot = !isEditing ? readViewRootRef.current : pageRootRef.current;\\n const target = targetRoot?.querySelector(`[data-id=\\\"${headingId}\\\"]`);\\n if (target) {\\n target.scrollIntoView({ behavior: \\\"smooth\\\", block: \\\"center\\\" });\\n }\\n }, [isEditing]);\\n\\n const handleSnapshot = useCallback((payload: { blocks: Json; stats: DocumentStats }) => {\\n setContent(payload.blocks);\\n latestBlocksRef.current = payload.blocks;\\n setHistory((prev) => {\\n const now = Date.now();\\n if (prev.length > 0 && now - prev[0].timestamp < 4000) {\\n return prev;\\n }\\n const snapshot: DocumentSnapshot = {\\n id: `${now}-${Math.random().toString(36).slice(2, 8)}`,\\n timestamp: now,\\n blocks: payload.blocks,\\n stats: payload.stats,\\n };\\n return [snapshot, ...prev].slice(0, 15);\\n });\\n }, []);\\n\\n const persistStatsRequest = useCallback((next: DocumentStats) => {\\n void fetch(\\\"/api/documents/stats\\\", {\\n method: \\\"POST\\\",\\n headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\\n body: JSON.stringify({ documentId, workspaceId, stats: next }),\\n }).catch((error) => console.error(error));\\n }, [documentId, workspaceId]);\\n\\n const persistStats = useDebouncedCallback(persistStatsRequest, 1500);\\n\\n const handleStatsChange = useCallback(\\n (nextStats: DocumentStats) => {\\n setStats(nextStats);\\n persistStats(nextStats);\\n },\\n [persistStats],\\n );\\n\\n const handleRestoreSnapshot = useCallback(\\n (snapshot: DocumentSnapshot) => {\\n if (!isEditing || !editorBridge) {\\n pendingRestoreSnapshotRef.current = snapshot;\\n setIsEditing(true);\\n return;\\n }\\n editorBridge.replaceWithSnapshot(snapshot.blocks);\\n setHistoryOpen(false);\\n },\\n [editorBridge, isEditing],\\n );\\n\\n const handleEnterEditMode = useCallback(() => {\\n if (!canEditDocument) return;\\n setIsEditing(true);\\n }, [canEditDocument]);\\n\\n const handleExitEditMode = useCallback(() => {\\n setIsEditing(false);\\n }, []);\\n\\n useEffect(() => {\\n if (!isEditing) return;\\n if (!editorBridge) return;\\n const pendingSnapshot = pendingRestoreSnapshotRef.current;\\n if (!pendingSnapshot) return;\\n pendingRestoreSnapshotRef.current = null;\\n editorBridge.replaceWithSnapshot(pendingSnapshot.blocks);\\n setHistoryOpen(false);\\n }, [editorBridge, isEditing]);\\n return (\\n \\n
\\n
\\n
\\n
\\n
\\n {isEditing && canEditDocument ? (\\n
\\n \\n
\\n ) : (\\n

\\n {pageTitle || \\\"无标题\\\"}\\n

\\n )}\\n {readOnly ? (\\n

该页面为只读共享,无法编辑。

\\n ) : options.protectEditing && isEditing ? (\\n

当前页面已开启编辑保护,关闭后方可修改内容。

\\n ) : !isEditing && canEditDocument ? (\\n

当前为阅读态,编辑器仅在进入编辑后挂载。

\\n ) : null}\\n

最近更新:{formattedUpdatedAt}

\\n
\\n {canEditDocument && (\\n
\\n {isEditing ? (\\n \\n ) : (\\n \\n )}\\n
\\n )}\\n
\\n
\\n
\\n {contentLoading ? (\\n showContentLoadingIndicator ? (\\n
\\n 页面内容加载中...\\n
\\n ) : (\\n
\\n )\\n ) : contentError ? (\\n
\\n
{contentError}
\\n {\\n setContentError(null);\\n setContentLoading(true);\\n setContentReloadKey((prev) => prev + 1);\\n }}\\n >\\n 重试\\n \\n
\\n ) : (\\n
\\n {keepEditorMounted && (\\n
\\n {\\n setContentRevision(meta.revision);\\n setConflictDetectionKey(meta.conflictDetectionKey);\\n }}\\n />\\n
\\n )}\\n {!isEditing && (\\n
\\n \\n \\n
\\n )}\\n
\\n )}\\n \\n
\\n
\\n {showInspector && (\\n setHistoryOpen(true)}\\n onOpenComments={() => openCommentsForPage({ workspaceId, documentId })}\\n onUndo={canEditDocument ? handleUndo : undefined}\\n onRedo={canEditDocument ? handleRedo : undefined}\\n onDeletePage={canEditDocument ? handleDeletePage : undefined}\\n onOpenMoveEmbedPicker={canEditDocument ? handleOpenMoveEmbed : undefined}\\n onCopyPageLink={handleCopyPageLink}\\n onCopyPageReference={handleCopyPageReference}\\n onAddToTemplates={canEditDocument ? handleAddToTemplates : undefined}\\n />\\n )}\\n
\\n \\n \\n \\n latestBlocksRef.current}\\n getLatestPageSubtree={() => pageSubtree}\\n />\\n \\n );\\n}\\n\",\n \"structuredPatch\": [\n {\n \"oldStart\": 1,\n \"oldLines\": 1011,\n \"newStart\": 1,\n \"newLines\": 1009,\n \"lines\": [\n \"-\\\"use client\\\";\",\n \"-\",\n \"-import dynamic from \\\"next/dynamic\\\";\",\n \"-import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent as ReactKeyboardEvent } from \\\"react\\\";\",\n \"-import type { BooleanPageOptionKey, DocumentStats, PageOptionsState, PageFont, PageLayoutDensity } from \\\"@/types/page-options\\\";\",\n \"-import { useEditorBridgeStore } from \\\"@/store/editor-bridge\\\";\",\n \"-import { usePageLayoutStore } from \\\"@/store/page-layout\\\";\",\n \"-import { useCurrentDocumentStore } from \\\"@/store/current-document\\\";\",\n \"-import type { Json } from \\\"@/types/supabase\\\";\",\n \"-import type { DocumentSnapshot } from \\\"@/types/document\\\";\",\n \"-import { useDebouncedCallback } from \\\"@/hooks/use-debounced-callback\\\";\",\n \"-import { ImagePickerProvider } from \\\"@/components/media/image-picker-context\\\";\",\n \"-import { useRouter } from \\\"next/navigation\\\";\",\n \"-import { DocumentAiAgentPanel } from \\\"./DocumentAiAgentPanel\\\";\",\n \"-import { CONTENT_LOADING_DELAY_MS } from \\\"@/lib/constants\\\";\",\n \"-import { useCommentsUiStore } from \\\"@/store/comments-ui\\\";\",\n \"-import { useAppPreferencesStore } from \\\"@/store/app-preferences\\\";\",\n \"-import { useMoveEmbedPickerStore } from \\\"@/store/move-embed-picker\\\";\",\n \"-import { cn } from \\\"@/lib/utils\\\";\",\n \"-import { Button } from \\\"@/components/ui/button\\\";\",\n \"-import { DocumentToc } from \\\"@/components/editor/document-toc\\\";\",\n \"-import { DocumentReadView } from \\\"@/components/editor/document-read-view\\\";\",\n \"-import { extractPageBlocks, type PageSubtreeProjection } from \\\"@/lib/documents/page-subtree\\\";\",\n \"-import {\",\n \"- deleteDocumentCommand,\",\n \"- embedDocumentCommand,\",\n \"- moveDocumentCommand,\",\n \"- renameDocumentCommand,\",\n \"-} from \\\"@/lib/documents/tree-command-client\\\";\",\n \"-import { EditorHost } from \\\"@/components/editor/editor-host\\\";\",\n \"-\",\n \"-const PageOptionsSidebar = dynamic(\",\n \"- () => import(\\\"@/components/editor/page-options-sidebar\\\").then((mod) => mod.PageOptionsSidebar),\",\n \"- { ssr: false },\",\n \"-);\",\n \"-\",\n \"-const PageBacklinksPanel = dynamic(\",\n \"- () => import(\\\"@/components/editor/page-backlinks-panel\\\").then((mod) => mod.PageBacklinksPanel),\",\n \"- {\",\n \"- ssr: false,\",\n \"- loading: () => null,\",\n \"- },\",\n \"-);\",\n \"-\",\n \"-const DocumentHistoryDrawer = dynamic(\",\n \"- () => import(\\\"@/components/editor/document-history-drawer\\\").then((mod) => mod.DocumentHistoryDrawer),\",\n \"- {\",\n \"- ssr: false,\",\n \"- loading: () => null,\",\n \"- },\",\n \"-);\",\n \"-\",\n \"-const MoveEmbedPickerHost = dynamic(\",\n \"- () => import(\\\"@/components/documents/move-embed-picker-host\\\").then((mod) => mod.MoveEmbedPickerHost),\",\n \"- {\",\n \"- ssr: false,\",\n \"- loading: () => null,\",\n \"- },\",\n \"-);\",\n \"-\",\n \"-const DocumentCommentsDrawer = dynamic(\",\n \"- () => import(\\\"@/components/editor/document-comments-drawer\\\").then((mod) => mod.DocumentCommentsDrawer),\",\n \"- {\",\n \"- ssr: false,\",\n \"- loading: () => null,\",\n \"- },\",\n \"-);\",\n \"-\",\n \"-export interface DocumentContentProps {\",\n \"- documentId: string;\",\n \"- workspaceId: string;\",\n \"- title: string | null;\",\n \"- updatedAt: string | null;\",\n \"- initialContent: unknown;\",\n \"- initialContentRevision?: number | null;\",\n \"- initialConflictDetectionKey?: string | null;\",\n \"- initialPageSubtree?: PageSubtreeProjection | null;\",\n \"- initialOptions: PageOptionsState;\",\n \"- initialStats: DocumentStats | null;\",\n \"- openTableId?: string | null;\",\n \"- autoEnterEdit?: boolean;\",\n \"- readOnly?: boolean;\",\n \"- disableDownload?: boolean;\",\n \"- disableCopy?: boolean;\",\n \"-}\",\n \"-\",\n \"-const defaultOptions: PageOptionsState = {\",\n \"- wideLayout: false,\",\n \"- smallText: false,\",\n \"- showHeadingNumbers: true,\",\n \"- showToc: false,\",\n \"- showStructure: false,\",\n \"- protectEditing: false,\",\n \"- showWordCount: true,\",\n \"- collapseBacklinks: false,\",\n \"- pageFont: \\\"default\\\",\",\n \"- layoutDensity: \\\"normal\\\",\",\n \"- hideChildPages: false,\",\n \"- showBlockRefCount: false,\",\n \"- embedDefaultBlockId: null,\",\n \"-};\",\n \"-const defaultStats: DocumentStats = { wordCount: 0, characterCount: 0, blockCount: 0, todoTotal: 0, todoDone: 0 };\",\n \"-const EDITOR_UNMOUNT_GRACE_MS = 1000;\",\n \"-\",\n \"-export function DocumentContent({\",\n \"- documentId,\",\n \"- workspaceId,\",\n \"- title,\",\n \"- updatedAt,\",\n \"- initialContent,\",\n \"- initialContentRevision = null,\",\n \"- initialConflictDetectionKey = null,\",\n \"- initialPageSubtree = null,\",\n \"- initialOptions,\",\n \"- initialStats,\",\n \"- openTableId,\",\n \"- autoEnterEdit = false,\",\n \"- readOnly = false,\",\n \"- disableDownload = false,\",\n \"- disableCopy = false,\",\n \"-}: DocumentContentProps) {\",\n \"- const setCurrentDocument = useCurrentDocumentStore((state) => state.setCurrent);\",\n \"- const clearIfMatch = useCurrentDocumentStore((state) => state.clearIfMatch);\",\n \"- const canEditDocument = !readOnly;\",\n \"- const [options, setOptions] = useState(initialOptions ?? defaultOptions);\",\n \"- const [stats, setStats] = useState(initialStats ?? defaultStats);\",\n \"- const [history, setHistory] = useState([]);\",\n \"- const [historyOpen, setHistoryOpen] = useState(false);\",\n \"- const editorBridge = useEditorBridgeStore((state) => state.bridge);\",\n \"- const router = useRouter();\",\n \"- const showInspector = usePageLayoutStore((state) => state.showInspector);\",\n \"- const openCommentsForPage = useCommentsUiStore((s) => s.openForPage);\",\n \"- const spellCheck = useAppPreferencesStore((s) => s.spellCheck);\",\n \"- const openMoveEmbedPicker = useMoveEmbedPickerStore((s) => s.openPicker);\",\n \"- const [pageTitle, setPageTitle] = useState(title ?? \\\"无标题\\\");\",\n \"- const [content, setContent] = useState(initialContent);\",\n \"- const [serverContentSnapshot, setServerContentSnapshot] = useState(initialContent);\",\n \"- const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState(\",\n \"- initialPageSubtree,\",\n \"- );\",\n \"- const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState(title ?? \\\"无标题\\\");\",\n \"- const [contentRevision, setContentRevision] = useState(initialContentRevision);\",\n \"- const [conflictDetectionKey, setConflictDetectionKey] = useState(initialConflictDetectionKey);\",\n \"- const [contentLoading, setContentLoading] = useState(() => initialContent == null);\",\n \"- const [contentError, setContentError] = useState(null);\",\n \"- const [contentReloadKey, setContentReloadKey] = useState(0);\",\n \"- const [showContentLoadingIndicator, setShowContentLoadingIndicator] = useState(false);\",\n \"- const shouldStartEditing = canEditDocument;\",\n \"- const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\",\n \"- const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\",\n \"- const contentLoadingTimerRef = useRef | null>(null);\",\n \"- const editorUnmountTimerRef = useRef | null>(null);\",\n \"- const pendingOpenTableRef = useRef(null);\",\n \"- const latestBlocksRef = useRef(null);\",\n \"- const pageRootRef = useRef(null);\",\n \"- const readViewRootRef = useRef(null);\",\n \"- const pendingRestoreSnapshotRef = useRef(null);\",\n \"- const lastCopyBlockedAtRef = useRef(0);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setCurrentDocument(documentId, readOnly, disableDownload, disableCopy);\",\n \"- return () => {\",\n \"- clearIfMatch(documentId);\",\n \"- };\",\n \"- }, [clearIfMatch, disableCopy, disableDownload, documentId, readOnly, setCurrentDocument]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- if (!disableCopy) return;\",\n \"-\",\n \"- const toElement = (node: Node | null): Element | null => {\",\n \"- if (!node) return null;\",\n \"- if (node.nodeType === Node.TEXT_NODE) {\",\n \"- return node.parentElement;\",\n \"- }\",\n \"- return node instanceof Element ? node : null;\",\n \"- };\",\n \"-\",\n \"- const isEventInsidePage = () => {\",\n \"- const root = pageRootRef.current;\",\n \"- if (!root) return false;\",\n \"- const selection = typeof window !== \\\"undefined\\\" ? window.getSelection() : null;\",\n \"- const anchor = selection?.anchorNode ?? null;\",\n \"- const focus = selection?.focusNode ?? null;\",\n \"- const anchorEl = toElement(anchor);\",\n \"- const focusEl = toElement(focus);\",\n \"- return Boolean((anchorEl && root.contains(anchorEl)) || (focusEl && root.contains(focusEl)));\",\n \"- };\",\n \"-\",\n \"- const notifyBlocked = () => {\",\n \"- const now = Date.now();\",\n \"- if (now - lastCopyBlockedAtRef.current < 1200) return;\",\n \"- lastCopyBlockedAtRef.current = now;\",\n \"- window.alert(\\\"该页面已禁止复制\\\");\",\n \"- };\",\n \"-\",\n \"- const onCopy = (event: ClipboardEvent) => {\",\n \"- if (!isEventInsidePage()) return;\",\n \"- event.preventDefault();\",\n \"- event.stopPropagation();\",\n \"- notifyBlocked();\",\n \"- };\",\n \"-\",\n \"- const onCut = (event: ClipboardEvent) => {\",\n \"- if (!isEventInsidePage()) return;\",\n \"- event.preventDefault();\",\n \"- event.stopPropagation();\",\n \"- notifyBlocked();\",\n \"- };\",\n \"-\",\n \"- const onKeyDown = (event: KeyboardEvent) => {\",\n \"- if (!isEventInsidePage()) return;\",\n \"- const key = String(event.key ?? \\\"\\\").toLowerCase();\",\n \"- const ctrlOrMeta = event.ctrlKey || event.metaKey;\",\n \"- if (!ctrlOrMeta) return;\",\n \"- if (key === \\\"c\\\" || key === \\\"x\\\" || key === \\\"insert\\\") {\",\n \"- event.preventDefault();\",\n \"- event.stopPropagation();\",\n \"- notifyBlocked();\",\n \"- }\",\n \"- };\",\n \"-\",\n \"- document.addEventListener(\\\"copy\\\", onCopy, true);\",\n \"- document.addEventListener(\\\"cut\\\", onCut, true);\",\n \"- document.addEventListener(\\\"keydown\\\", onKeyDown, true);\",\n \"- return () => {\",\n \"- document.removeEventListener(\\\"copy\\\", onCopy, true);\",\n \"- document.removeEventListener(\\\"cut\\\", onCut, true);\",\n \"- document.removeEventListener(\\\"keydown\\\", onKeyDown, true);\",\n \"- };\",\n \"- }, [disableCopy]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- const tableId = (openTableId ?? \\\"\\\").trim();\",\n \"- if (!tableId) return;\",\n \"- if (!editorBridge?.openTableFullScreen) return;\",\n \"- if (pendingOpenTableRef.current === tableId) return;\",\n \"- pendingOpenTableRef.current = tableId;\",\n \"-\",\n \"- editorBridge.openTableFullScreen(tableId);\",\n \"-\",\n \"- if (typeof window !== \\\"undefined\\\") {\",\n \"- try {\",\n \"- const url = new URL(window.location.href);\",\n \"- url.searchParams.delete(\\\"openTableId\\\");\",\n \"- window.history.replaceState({}, \\\"\\\", url.toString());\",\n \"- } catch {\",\n \"- router.replace(`/documents/${documentId}`);\",\n \"- }\",\n \"- }\",\n \"- }, [documentId, editorBridge, openTableId, router]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setPageTitle(title ?? \\\"无标题\\\");\",\n \"- }, [title]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setServerPageSubtreeTitle(title ?? \\\"无标题\\\");\",\n \"- }, [title]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setServerPageSubtreeSnapshot(initialPageSubtree);\",\n \"- }, [initialPageSubtree]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setOptions(initialOptions ?? defaultOptions);\",\n \"- }, [initialOptions]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setStats(initialStats ?? defaultStats);\",\n \"- }, [initialStats]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setContentRevision(initialContentRevision);\",\n \"- }, [initialContentRevision]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setConflictDetectionKey(initialConflictDetectionKey);\",\n \"- }, [initialConflictDetectionKey]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setServerContentSnapshot(initialContent);\",\n \"- }, [initialContent]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- const nextBlocks = extractPageBlocks(content);\",\n \"- latestBlocksRef.current = nextBlocks.length > 0 ? (nextBlocks as Json) : null;\",\n \"- }, [content]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- const shouldForceEdit = Boolean((openTableId ?? \\\"\\\").trim()) && canEditDocument;\",\n \"- if (shouldForceEdit) {\",\n \"- setIsEditing(true);\",\n \"- setKeepEditorMounted(true);\",\n \"- }\",\n \"- }, [canEditDocument, openTableId]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- if (isEditing) {\",\n \"- if (editorUnmountTimerRef.current) {\",\n \"- clearTimeout(editorUnmountTimerRef.current);\",\n \"- editorUnmountTimerRef.current = null;\",\n \"- }\",\n \"- setKeepEditorMounted(true);\",\n \"- return;\",\n \"- }\",\n \"- if (editorUnmountTimerRef.current) {\",\n \"- clearTimeout(editorUnmountTimerRef.current);\",\n \"- }\",\n \"- editorUnmountTimerRef.current = setTimeout(() => {\",\n \"- setKeepEditorMounted(false);\",\n \"- editorUnmountTimerRef.current = null;\",\n \"- }, EDITOR_UNMOUNT_GRACE_MS);\",\n \"- return () => {\",\n \"- if (editorUnmountTimerRef.current) {\",\n \"- clearTimeout(editorUnmountTimerRef.current);\",\n \"- editorUnmountTimerRef.current = null;\",\n \"- }\",\n \"- };\",\n \"- }, [isEditing]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- let canceled = false;\",\n \"- const controller = new AbortController();\",\n \"-\",\n \"- const load = async () => {\",\n \"- setContentError(null);\",\n \"- setContentLoading(initialContent == null);\",\n \"- setContent(initialContent);\",\n \"- setShowContentLoadingIndicator(false);\",\n \"-\",\n \"- if (initialContent != null) {\",\n \"- return;\",\n \"- }\",\n \"-\",\n \"- if (contentLoadingTimerRef.current) {\",\n \"- clearTimeout(contentLoadingTimerRef.current);\",\n \"- contentLoadingTimerRef.current = null;\",\n \"- }\",\n \"- contentLoadingTimerRef.current = setTimeout(() => {\",\n \"- if (!canceled) {\",\n \"- setShowContentLoadingIndicator(true);\",\n \"- }\",\n \"- }, CONTENT_LOADING_DELAY_MS);\",\n \"-\",\n \"- try {\",\n \"- const response = await fetch(\",\n \"- `/api/documents/content?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,\",\n \"- {\",\n \"- method: \\\"GET\\\",\",\n \"- credentials: \\\"include\\\",\",\n \"- signal: controller.signal,\",\n \"- },\",\n \"- );\",\n \"- if (!response.ok) {\",\n \"- const payload = await response.json().catch(() => ({}));\",\n \"- throw new Error(payload?.error ?? \\\"加载页面内容失败\\\");\",\n \"- }\",\n \"- const payload = (await response.json()) as {\",\n \"- content?: unknown;\",\n \"- revision?: number | null;\",\n \"- conflictDetectionKey?: string | null;\",\n \"- pageSubtree?: PageSubtreeProjection | null;\",\n \"- };\",\n \"- if (canceled) return;\",\n \"- setContent(payload.content ?? null);\",\n \"- setServerContentSnapshot(payload.content ?? null);\",\n \"- setContentRevision(\",\n \"- typeof payload.revision === \\\"number\\\" && Number.isInteger(payload.revision)\",\n \"- ? payload.revision\",\n \"- : 0,\",\n \"- );\",\n \"- setConflictDetectionKey(\",\n \"- typeof payload.conflictDetectionKey === \\\"string\\\" && payload.conflictDetectionKey.trim()\",\n \"- ? payload.conflictDetectionKey\",\n \"- : `${documentId}:0`,\",\n \"- );\",\n \"- setServerPageSubtreeSnapshot(payload.pageSubtree ?? null);\",\n \"- setServerPageSubtreeTitle(\",\n \"- typeof payload.pageSubtree?.rootNode.metadata.title === \\\"string\\\" &&\",\n \"- payload.pageSubtree.rootNode.metadata.title.trim()\",\n \"- ? payload.pageSubtree.rootNode.metadata.title\",\n \"- : title ?? \\\"无标题\\\",\",\n \"- );\",\n \"- } catch (error) {\",\n \"- if (canceled) return;\",\n \"- if ((error as { name?: string })?.name === \\\"AbortError\\\") return;\",\n \"- setContentError(error instanceof Error ? error.message : \\\"加载页面内容失败\\\");\",\n \"- } finally {\",\n \"- if (!canceled) {\",\n \"- setContentLoading(false);\",\n \"- setShowContentLoadingIndicator(false);\",\n \"- }\",\n \"- if (contentLoadingTimerRef.current) {\",\n \"- clearTimeout(contentLoadingTimerRef.current);\",\n \"- contentLoadingTimerRef.current = null;\",\n \"- }\",\n \"- }\",\n \"- };\",\n \"-\",\n \"- void load();\",\n \"-\",\n \"- return () => {\",\n \"- canceled = true;\",\n \"- controller.abort();\",\n \"- if (contentLoadingTimerRef.current) {\",\n \"- clearTimeout(contentLoadingTimerRef.current);\",\n \"- contentLoadingTimerRef.current = null;\",\n \"- }\",\n \"- };\",\n \"- }, [documentId, initialContent, contentReloadKey, title, workspaceId]);\",\n \"-\",\n \"- const persistTitle = useCallback(\",\n \"- async (nextTitle: string) => {\",\n \"- if (readOnly) return;\",\n \"- const payload = nextTitle.trim() || \\\"无标题\\\";\",\n \"- try {\",\n \"- await renameDocumentCommand({ documentId, workspaceId, title: payload });\",\n \"- } catch (error) {\",\n \"- console.error(\\\"更新页面标题失败\\\", error);\",\n \"- }\",\n \"- },\",\n \"- [documentId, readOnly, workspaceId],\",\n \"- );\",\n \"-\",\n \"- const debouncedPersistTitle = useDebouncedCallback((value: string) => {\",\n \"- void persistTitle(value);\",\n \"- }, 600);\",\n \"-\",\n \"- const handleTitleChange = (event: ChangeEvent) => {\",\n \"- if (!canEditDocument) return;\",\n \"- const value = event.target.value;\",\n \"- setPageTitle(value);\",\n \"- debouncedPersistTitle(value);\",\n \"- };\",\n \"-\",\n \"- const handleTitleBlur = () => {\",\n \"- if (!canEditDocument) return;\",\n \"- void persistTitle(pageTitle);\",\n \"- };\",\n \"-\",\n \"- const handleTitleKeyDown = (event: ReactKeyboardEvent) => {\",\n \"- if (event.key === \\\"Enter\\\") {\",\n \"- event.preventDefault();\",\n \"- event.currentTarget.blur();\",\n \"- }\",\n \"- };\",\n \"-\",\n \"- const persistOptions = useCallback(\",\n \"- async (patch: Partial) => {\",\n \"- if (readOnly) return;\",\n \"- try {\",\n \"- const response = await fetch(\\\"/api/documents/options\\\", {\",\n \"- method: \\\"POST\\\",\",\n \"- headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"- body: JSON.stringify({ documentId, workspaceId, options: patch }),\",\n \"- });\",\n \"- if (!response.ok) {\",\n \"- const payload = await response.json().catch(() => null);\",\n \"- console.error(payload?.error ?? \\\"更新页面选项失败\\\");\",\n \"- }\",\n \"- } catch (error) {\",\n \"- console.error(error);\",\n \"- }\",\n \"- },\",\n \"- [documentId, readOnly, workspaceId],\",\n \"- );\",\n \"-\",\n \"- const toggleOption = useCallback(\",\n \"- (key: BooleanPageOptionKey) => {\",\n \"- if (readOnly) return;\",\n \"- setOptions((prev) => {\",\n \"- const nextValue = !prev[key];\",\n \"- const next = { ...prev, [key]: nextValue };\",\n \"- void persistOptions({ [key]: nextValue } as Partial);\",\n \"- return next;\",\n \"- });\",\n \"- },\",\n \"- [persistOptions, readOnly],\",\n \"- );\",\n \"-\",\n \"- const setOptionPatch = useCallback(\",\n \"- (patch: Partial) => {\",\n \"- if (readOnly) return;\",\n \"- setOptions((prev) => {\",\n \"- const next = { ...prev, ...patch };\",\n \"- void persistOptions(patch);\",\n \"- return next;\",\n \"- });\",\n \"- },\",\n \"- [persistOptions, readOnly],\",\n \"- );\",\n \"-\",\n \"- const closeToc = useCallback(() => {\",\n \"- if (readOnly) return;\",\n \"- setOptions((prev) => {\",\n \"- if (!prev.showToc) return prev;\",\n \"- const next = { ...prev, showToc: false };\",\n \"- void persistOptions({ showToc: false });\",\n \"- return next;\",\n \"- });\",\n \"- }, [persistOptions, readOnly]);\",\n \"-\",\n \"- const handleSetPageFont = useCallback(\",\n \"- (font: PageFont) => {\",\n \"- setOptionPatch({ pageFont: font });\",\n \"- },\",\n \"- [setOptionPatch],\",\n \"- );\",\n \"-\",\n \"- const handleSetLayoutDensity = useCallback(\",\n \"- (density: PageLayoutDensity) => {\",\n \"- setOptionPatch({ layoutDensity: density });\",\n \"- },\",\n \"- [setOptionPatch],\",\n \"- );\",\n \"-\",\n \"- const handleSetEmbedDefaultToCursor = useCallback(() => {\",\n \"- if (!canEditDocument) return;\",\n \"- if (!isEditing) {\",\n \"- setIsEditing(true);\",\n \"- return;\",\n \"- }\",\n \"- const blockId = editorBridge?.getCursorBlockId?.() ?? null;\",\n \"- if (!blockId) {\",\n \"- window.alert(\\\"未找到当前光标所在块,请先在编辑器中点击任意块\\\");\",\n \"- return;\",\n \"- }\",\n \"- setOptionPatch({ embedDefaultBlockId: blockId });\",\n \"- window.alert(\\\"已设置“嵌入默认位置”\\\");\",\n \"- }, [canEditDocument, editorBridge, isEditing, setOptionPatch]);\",\n \"-\",\n \"- const handleClearEmbedDefault = useCallback(() => {\",\n \"- if (readOnly) return;\",\n \"- setOptionPatch({ embedDefaultBlockId: null });\",\n \"- window.alert(\\\"已清除“嵌入默认位置”\\\");\",\n \"- }, [readOnly, setOptionPatch]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- const onKeyDown = (event: KeyboardEvent) => {\",\n \"- const root = pageRootRef.current;\",\n \"- if (root) {\",\n \"- const target = event.target;\",\n \"- if (target && target instanceof Node && !root.contains(target)) {\",\n \"- return;\",\n \"- }\",\n \"- }\",\n \"- const ctrlOrMeta = event.ctrlKey || event.metaKey;\",\n \"- if (!ctrlOrMeta) return;\",\n \"- if (!event.shiftKey) return;\",\n \"- const key = String(event.key ?? \\\"\\\").toLowerCase();\",\n \"- if (key === \\\"l\\\") {\",\n \"- event.preventDefault();\",\n \"- toggleOption(\\\"showToc\\\");\",\n \"- } else if (key === \\\"c\\\") {\",\n \"- event.preventDefault();\",\n \"- toggleOption(\\\"protectEditing\\\");\",\n \"- }\",\n \"- };\",\n \"- window.addEventListener(\\\"keydown\\\", onKeyDown);\",\n \"- return () => window.removeEventListener(\\\"keydown\\\", onKeyDown);\",\n \"- }, [toggleOption]);\",\n \"-\",\n \"- const buildDocumentUrl = useCallback((id: string): string => {\",\n \"- if (typeof window === \\\"undefined\\\" || !window.location) {\",\n \"- return `/documents/${id}`;\",\n \"- }\",\n \"- return `${window.location.origin}/documents/${id}`;\",\n \"- }, []);\",\n \"-\",\n \"- const copyText = useCallback(async (text: string, successMessage: string) => {\",\n \"- if (typeof navigator !== \\\"undefined\\\" && navigator.clipboard) {\",\n \"- try {\",\n \"- await navigator.clipboard.writeText(text);\",\n \"- window.alert(successMessage);\",\n \"- return;\",\n \"- } catch {\",\n \"- }\",\n \"- }\",\n \"- window.prompt(\\\"复制失败,请手动复制内容\\\", text);\",\n \"- }, []);\",\n \"-\",\n \"- const handleCopyPageLink = useCallback(\",\n \"- async (includeTitle: boolean) => {\",\n \"- const url = buildDocumentUrl(documentId);\",\n \"- if (includeTitle) {\",\n \"- const text = `${pageTitle || \\\"无标题\\\"}\\\\n${url}`;\",\n \"- await copyText(text, \\\"标题 + 链接已复制\\\");\",\n \"- return;\",\n \"- }\",\n \"- await copyText(url, \\\"页面链接已复制\\\");\",\n \"- },\",\n \"- [buildDocumentUrl, copyText, documentId, pageTitle],\",\n \"- );\",\n \"-\",\n \"- const handleCopyPageReference = useCallback(\",\n \"- async (mode: \\\"inline\\\" | \\\"embed\\\") => {\",\n \"- const template = mode === \\\"inline\\\" ? `((${documentId}))` : `{{${documentId}}}`;\",\n \"- await copyText(template, mode === \\\"inline\\\" ? \\\"行内引用已复制\\\" : \\\"嵌入引用已复制\\\");\",\n \"- },\",\n \"- [copyText, documentId],\",\n \"- );\",\n \"-\",\n \"- const handleUndo = useCallback(() => {\",\n \"- if (!isEditing) {\",\n \"- setIsEditing(true);\",\n \"- return;\",\n \"- }\",\n \"- editorBridge?.undo?.();\",\n \"- }, [editorBridge, isEditing]);\",\n \"-\",\n \"- const handleRedo = useCallback(() => {\",\n \"- if (!isEditing) {\",\n \"- setIsEditing(true);\",\n \"- return;\",\n \"- }\",\n \"- editorBridge?.redo?.();\",\n \"- }, [editorBridge, isEditing]);\",\n \"-\",\n \"- const handleDeletePage = useCallback(async () => {\",\n \"- if (readOnly) return;\",\n \"- const ok = window.confirm(\\\"确定删除该页面吗?删除后会进入垃圾桶。\\\");\",\n \"- if (!ok) return;\",\n \"- try {\",\n \"- await deleteDocumentCommand({ documentId, workspaceId });\",\n \"- } catch (error) {\",\n \"- const payload = error instanceof Error ? error.message : null;\",\n \"- window.alert(payload ?? \\\"删除失败\\\");\",\n \"- return;\",\n \"- }\",\n \"- router.push(\\\"/\\\");\",\n \"- router.refresh();\",\n \"- }, [documentId, readOnly, router, workspaceId]);\",\n \"-\",\n \"- const handleOpenMoveEmbed = useCallback(() => {\",\n \"- if (readOnly) return;\",\n \"- openMoveEmbedPicker({\",\n \"- workspaceId,\",\n \"- defaultMode: \\\"move\\\",\",\n \"- modes: [\\\"move\\\", \\\"embed\\\"],\",\n \"- allowRoot: true,\",\n \"- excludeIds: [documentId],\",\n \"- onPick: async (mode, targetId) => {\",\n \"- if (mode === \\\"move\\\") {\",\n \"- try {\",\n \"- await moveDocumentCommand({\",\n \"- documentId,\",\n \"- parentId: targetId ?? null,\",\n \"- position: 999999,\",\n \"- workspaceId,\",\n \"- });\",\n \"- } catch (error) {\",\n \"- window.alert(error instanceof Error ? error.message : \\\"移动失败\\\");\",\n \"- return;\",\n \"- }\",\n \"- window.alert(\\\"移动成功\\\");\",\n \"- router.refresh();\",\n \"- return;\",\n \"- }\",\n \"-\",\n \"- if (!targetId) {\",\n \"- window.alert(\\\"请选择目标页面\\\");\",\n \"- return;\",\n \"- }\",\n \"-\",\n \"- try {\",\n \"- await embedDocumentCommand({\",\n \"- sourceId: documentId,\",\n \"- targetId,\",\n \"- });\",\n \"- } catch (error) {\",\n \"- const message = error instanceof Error ? error.message : \\\"嵌入失败\\\";\",\n \"- window.alert(message);\",\n \"- return;\",\n \"- }\",\n \"-\",\n \"- window.alert(\\\"已嵌入到目标页面\\\");\",\n \"- },\",\n \"- });\",\n \"- }, [documentId, openMoveEmbedPicker, readOnly, router, workspaceId]);\",\n \"-\",\n \"- const handleAddToTemplates = useCallback(async () => {\",\n \"- if (readOnly) return;\",\n \"- const ok = window.confirm(\\\"将该页面添加为模板?\\\");\",\n \"- if (!ok) return;\",\n \"- const resp = await fetch(\\\"/api/documents/template\\\", {\",\n \"- method: \\\"POST\\\",\",\n \"- headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"- body: JSON.stringify({ documentId, isTemplate: true }),\",\n \"- });\",\n \"- if (!resp.ok) {\",\n \"- const payload = await resp.json().catch(() => ({}));\",\n \"- window.alert(payload?.error ?? \\\"设置模板失败\\\");\",\n \"- return;\",\n \"- }\",\n \"- window.alert(\\\"已添加为模板\\\");\",\n \"- router.refresh();\",\n \"- }, [documentId, readOnly, router]);\",\n \"-\",\n \"- const formattedUpdatedAt = useMemo(() => {\",\n \"- if (!updatedAt) return \\\"\\\";\",\n \"- const date = new Date(updatedAt);\",\n \"- if (Number.isNaN(date.getTime())) return \\\"\\\";\",\n \"- return new Intl.DateTimeFormat(\\\"zh-CN\\\", {\",\n \"- timeZone: \\\"Asia/Shanghai\\\",\",\n \"- year: \\\"numeric\\\",\",\n \"- month: \\\"numeric\\\",\",\n \"- day: \\\"numeric\\\",\",\n \"- hour: \\\"2-digit\\\",\",\n \"- minute: \\\"2-digit\\\",\",\n \"- second: \\\"2-digit\\\",\",\n \"- hour12: false,\",\n \"- }).format(date);\",\n \"- }, [updatedAt]);\",\n \"-\",\n \"- const handleExport = useCallback(() => {\",\n \"- if (disableDownload) {\",\n \"- window.alert(\\\"该页面已禁止下载\\\");\",\n \"- return;\",\n \"- }\",\n \"- const latest = history[0];\",\n \"- const exportBlocks = latest?.blocks ?? latestBlocksRef.current;\",\n \"- if (!exportBlocks) {\",\n \"- window.alert(\\\"暂无可导出的内容\\\");\",\n \"- return;\",\n \"- }\",\n \"- const payload = JSON.stringify(exportBlocks, null, 2);\",\n \"- const blob = new Blob([payload], { type: \\\"application/json\\\" });\",\n \"- const url = URL.createObjectURL(blob);\",\n \"- const anchor = document.createElement(\\\"a\\\");\",\n \"- anchor.href = url;\",\n \"- anchor.download = `${title ?? \\\"未命名页面\\\"}-${new Date().toISOString()}.json`;\",\n \"- anchor.click();\",\n \"- URL.revokeObjectURL(url);\",\n \"- }, [disableDownload, history, title]);\",\n \"-\",\n \"- const pageRootClass = cn(\",\n \"- \\\"flex h-full overflow-hidden bg-wolai-bg\\\",\",\n \"- options.pageFont === \\\"song\\\" && \\\"wolai-page-font-song\\\",\",\n \"- options.pageFont === \\\"kai\\\" && \\\"wolai-page-font-kai\\\",\",\n \"- options.layoutDensity === \\\"compact\\\" && \\\"wolai-page-density-compact\\\",\",\n \"- options.layoutDensity === \\\"spacious\\\" && \\\"wolai-page-density-spacious\\\",\",\n \"- options.smallText && \\\"wolai-small-text\\\",\",\n \"- options.hideChildPages && \\\"wolai-hide-child-pages\\\",\",\n \"- );\",\n \"- const pageSubtree = useMemo(() => {\",\n \"- const hasServerPageSubtree = Boolean(serverPageSubtreeSnapshot);\",\n \"- const titleUnchanged = pageTitle === serverPageSubtreeTitle;\",\n \"- const contentUnchanged = content === serverContentSnapshot;\",\n \"-\",\n \"- if (hasServerPageSubtree && titleUnchanged && contentUnchanged) {\",\n \"- return serverPageSubtreeSnapshot;\",\n \"- }\",\n \"- return null;\",\n \"- }, [\",\n \"- content,\",\n \"- pageTitle,\",\n \"- serverContentSnapshot,\",\n \"- serverPageSubtreeSnapshot,\",\n \"- serverPageSubtreeTitle,\",\n \"- ]);\",\n \"- const readViewTocEntries = useMemo(\",\n \"- () =>\",\n \"- (pageSubtree?.outline ?? [])\",\n \"- .filter((entry) => typeof entry.anchorBlockId === \\\"string\\\" && entry.anchorBlockId.trim())\",\n \"- .map(({ anchorBlockId, level, numbering, title: entryTitle }) => ({\",\n \"- id: anchorBlockId as string,\",\n \"- level,\",\n \"- numbering,\",\n \"- title: entryTitle,\",\n \"- })),\",\n \"- [pageSubtree],\",\n \"- );\",\n \"- const inspectorCanUseEditorBridge = canEditDocument && isEditing;\",\n \"-\",\n \"- const jumpToHeading = useCallback((headingId: string) => {\",\n \"- const targetRoot = !isEditing ? readViewRootRef.current : pageRootRef.current;\",\n \"- const target = targetRoot?.querySelector(`[data-id=\\\"${headingId}\\\"]`);\",\n \"- if (target) {\",\n \"- target.scrollIntoView({ behavior: \\\"smooth\\\", block: \\\"center\\\" });\",\n \"- }\",\n \"- }, [isEditing]);\",\n \"-\",\n \"- const handleSnapshot = useCallback((payload: { blocks: Json; stats: DocumentStats }) => {\",\n \"- setContent(payload.blocks);\",\n \"- latestBlocksRef.current = payload.blocks;\",\n \"- setHistory((prev) => {\",\n \"- const now = Date.now();\",\n \"- if (prev.length > 0 && now - prev[0].timestamp < 4000) {\",\n \"- return prev;\",\n \"- }\",\n \"- const snapshot: DocumentSnapshot = {\",\n \"- id: `${now}-${Math.random().toString(36).slice(2, 8)}`,\",\n \"- timestamp: now,\",\n \"- blocks: payload.blocks,\",\n \"- stats: payload.stats,\",\n \"- };\",\n \"- return [snapshot, ...prev].slice(0, 15);\",\n \"- });\",\n \"- }, []);\",\n \"-\",\n \"- const persistStatsRequest = useCallback((next: DocumentStats) => {\",\n \"- void fetch(\\\"/api/documents/stats\\\", {\",\n \"- method: \\\"POST\\\",\",\n \"- headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"- body: JSON.stringify({ documentId, workspaceId, stats: next }),\",\n \"- }).catch((error) => console.error(error));\",\n \"- }, [documentId, workspaceId]);\",\n \"-\",\n \"- const persistStats = useDebouncedCallback(persistStatsRequest, 1500);\",\n \"-\",\n \"- const handleStatsChange = useCallback(\",\n \"- (nextStats: DocumentStats) => {\",\n \"- setStats(nextStats);\",\n \"- persistStats(nextStats);\",\n \"- },\",\n \"- [persistStats],\",\n \"- );\",\n \"-\",\n \"- const handleRestoreSnapshot = useCallback(\",\n \"- (snapshot: DocumentSnapshot) => {\",\n \"- if (!isEditing || !editorBridge) {\",\n \"- pendingRestoreSnapshotRef.current = snapshot;\",\n \"- setIsEditing(true);\",\n \"- return;\",\n \"- }\",\n \"- editorBridge.replaceWithSnapshot(snapshot.blocks);\",\n \"- setHistoryOpen(false);\",\n \"- },\",\n \"- [editorBridge, isEditing],\",\n \"- );\",\n \"-\",\n \"- const handleEnterEditMode = useCallback(() => {\",\n \"- if (!canEditDocument) return;\",\n \"- setIsEditing(true);\",\n \"- }, [canEditDocument]);\",\n \"-\",\n \"- const handleExitEditMode = useCallback(() => {\",\n \"- setIsEditing(false);\",\n \"- }, []);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- if (!isEditing) return;\",\n \"- if (!editorBridge) return;\",\n \"- const pendingSnapshot = pendingRestoreSnapshotRef.current;\",\n \"- if (!pendingSnapshot) return;\",\n \"- pendingRestoreSnapshotRef.current = null;\",\n \"- editorBridge.replaceWithSnapshot(pendingSnapshot.blocks);\",\n \"- setHistoryOpen(false);\",\n \"- }, [editorBridge, isEditing]);\",\n \"- return (\",\n \"- \",\n \"-
\",\n \"-
\",\n \"-
\",\n \"-
\",\n \"-
\",\n \"- {isEditing && canEditDocument ? (\",\n \"-
\",\n \"- \",\n \"-
\",\n \"- ) : (\",\n \"-

\",\n \"- {pageTitle || \\\"无标题\\\"}\",\n \"-

\",\n \"- )}\",\n \"- {readOnly ? (\",\n \"-

该页面为只读共享,无法编辑。

\",\n \"- ) : options.protectEditing && isEditing ? (\",\n \"-

当前页面已开启编辑保护,关闭后方可修改内容。

\",\n \"- ) : !isEditing && canEditDocument ? (\",\n \"-

当前为阅读态,编辑器仅在进入编辑后挂载。

\",\n \"- ) : null}\",\n \"-

最近更新:{formattedUpdatedAt}

\",\n \"-
\",\n \"- {canEditDocument && (\",\n \"-
\",\n \"- {isEditing ? (\",\n \"- \",\n \"- ) : (\",\n \"- \",\n \"- )}\",\n \"-
\",\n \"- )}\",\n \"-
\",\n \"-
\",\n \"-
\",\n \"- {contentLoading ? (\",\n \"- showContentLoadingIndicator ? (\",\n \"-
\",\n \"- 页面内容加载中...\",\n \"-
\",\n \"- ) : (\",\n \"-
\",\n \"- )\",\n \"- ) : contentError ? (\",\n \"-
\",\n \"-
{contentError}
\",\n \"- {\",\n \"- setContentError(null);\",\n \"- setContentLoading(true);\",\n \"- setContentReloadKey((prev) => prev + 1);\",\n \"- }}\",\n \"- >\",\n \"- 重试\",\n \"- \",\n \"-
\",\n \"- ) : (\",\n \"-
\",\n \"- {keepEditorMounted && (\",\n \"-
\",\n \"- {\",\n \"- setContentRevision(meta.revision);\",\n \"- setConflictDetectionKey(meta.conflictDetectionKey);\",\n \"- }}\",\n \"- />\",\n \"-
\",\n \"- )}\",\n \"- {!isEditing && (\",\n \"-
\",\n \"- \",\n \"- \",\n \"-
\",\n \"- )}\",\n \"-
\",\n \"- )}\",\n \"- \",\n \"-
\",\n \"-
\",\n \"- {showInspector && (\",\n \"- setHistoryOpen(true)}\",\n \"- onOpenComments={() => openCommentsForPage({ workspaceId, documentId })}\",\n \"- onUndo={canEditDocument ? handleUndo : undefined}\",\n \"- onRedo={canEditDocument ? handleRedo : undefined}\",\n \"- onDeletePage={canEditDocument ? handleDeletePage : undefined}\",\n \"- onOpenMoveEmbedPicker={canEditDocument ? handleOpenMoveEmbed : undefined}\",\n \"- onCopyPageLink={handleCopyPageLink}\",\n \"- onCopyPageReference={handleCopyPageReference}\",\n \"- onAddToTemplates={canEditDocument ? handleAddToTemplates : undefined}\",\n \"- />\",\n \"- )}\",\n \"-
\",\n \"- \",\n \"- \",\n \"- \",\n \"- latestBlocksRef.current}\",\n \"- getLatestPageSubtree={() => pageSubtree}\",\n \"- />\",\n \"- \",\n \"- );\",\n \"-}\",\n \"+\\\"use client\\\";\",\n \"+\",\n \"+import dynamic from \\\"next/dynamic\\\";\",\n \"+import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent as ReactKeyboardEvent } from \\\"react\\\";\",\n \"+import type { BooleanPageOptionKey, DocumentStats, PageOptionsState, PageFont, PageLayoutDensity } from \\\"@/types/page-options\\\";\",\n \"+import { useEditorBridgeStore } from \\\"@/store/editor-bridge\\\";\",\n \"+import { usePageLayoutStore } from \\\"@/store/page-layout\\\";\",\n \"+import { useCurrentDocumentStore } from \\\"@/store/current-document\\\";\",\n \"+import type { Json } from \\\"@/types/supabase\\\";\",\n \"+import type { DocumentSnapshot } from \\\"@/types/document\\\";\",\n \"+import { useDebouncedCallback } from \\\"@/hooks/use-debounced-callback\\\";\",\n \"+import { ImagePickerProvider } from \\\"@/components/media/image-picker-context\\\";\",\n \"+import { useRouter } from \\\"next/navigation\\\";\",\n \"+import { DocumentAiAgentPanel } from \\\"./DocumentAiAgentPanel\\\";\",\n \"+import { CONTENT_LOADING_DELAY_MS } from \\\"@/lib/constants\\\";\",\n \"+import { useCommentsUiStore } from \\\"@/store/comments-ui\\\";\",\n \"+import { useAppPreferencesStore } from \\\"@/store/app-preferences\\\";\",\n \"+import { useMoveEmbedPickerStore } from \\\"@/store/move-embed-picker\\\";\",\n \"+import { cn } from \\\"@/lib/utils\\\";\",\n \"+import { Button } from \\\"@/components/ui/button\\\";\",\n \"+import { DocumentToc } from \\\"@/components/editor/document-toc\\\";\",\n \"+import { DocumentReadView } from \\\"@/components/editor/document-read-view\\\";\",\n \"+import { extractPageBlocks, type PageSubtreeProjection } from \\\"@/lib/documents/page-subtree\\\";\",\n \"+import {\",\n \"+ deleteDocumentCommand,\",\n \"+ embedDocumentCommand,\",\n \"+ moveDocumentCommand,\",\n \"+ renameDocumentCommand,\",\n \"+} from \\\"@/lib/documents/tree-command-client\\\";\",\n \"+import { EditorHost } from \\\"@/components/editor/editor-host\\\";\",\n \"+\",\n \"+const PageOptionsSidebar = dynamic(\",\n \"+ () => import(\\\"@/components/editor/page-options-sidebar\\\").then((mod) => mod.PageOptionsSidebar),\",\n \"+ { ssr: false },\",\n \"+);\",\n \"+\",\n \"+const PageBacklinksPanel = dynamic(\",\n \"+ () => import(\\\"@/components/editor/page-backlinks-panel\\\").then((mod) => mod.PageBacklinksPanel),\",\n \"+ {\",\n \"+ ssr: false,\",\n \"+ loading: () => null,\",\n \"+ },\",\n \"+);\",\n \"+\",\n \"+const DocumentHistoryDrawer = dynamic(\",\n \"+ () => import(\\\"@/components/editor/document-history-drawer\\\").then((mod) => mod.DocumentHistoryDrawer),\",\n \"+ {\",\n \"+ ssr: false,\",\n \"+ loading: () => null,\",\n \"+ },\",\n \"+);\",\n \"+\",\n \"+const MoveEmbedPickerHost = dynamic(\",\n \"+ () => import(\\\"@/components/documents/move-embed-picker-host\\\").then((mod) => mod.MoveEmbedPickerHost),\",\n \"+ {\",\n \"+ ssr: false,\",\n \"+ loading: () => null,\",\n \"+ },\",\n \"+);\",\n \"+\",\n \"+const DocumentCommentsDrawer = dynamic(\",\n \"+ () => import(\\\"@/components/editor/document-comments-drawer\\\").then((mod) => mod.DocumentCommentsDrawer),\",\n \"+ {\",\n \"+ ssr: false,\",\n \"+ loading: () => null,\",\n \"+ },\",\n \"+);\",\n \"+\",\n \"+export interface DocumentContentProps {\",\n \"+ documentId: string;\",\n \"+ workspaceId: string;\",\n \"+ title: string | null;\",\n \"+ updatedAt: string | null;\",\n \"+ initialContent: unknown;\",\n \"+ initialContentRevision?: number | null;\",\n \"+ initialConflictDetectionKey?: string | null;\",\n \"+ initialPageSubtree?: PageSubtreeProjection | null;\",\n \"+ initialOptions: PageOptionsState;\",\n \"+ initialStats: DocumentStats | null;\",\n \"+ openTableId?: string | null;\",\n \"+ autoEnterEdit?: boolean;\",\n \"+ readOnly?: boolean;\",\n \"+ disableDownload?: boolean;\",\n \"+ disableCopy?: boolean;\",\n \"+}\",\n \"+\",\n \"+const defaultOptions: PageOptionsState = {\",\n \"+ wideLayout: false,\",\n \"+ smallText: false,\",\n \"+ showHeadingNumbers: true,\",\n \"+ showToc: false,\",\n \"+ showStructure: false,\",\n \"+ protectEditing: false,\",\n \"+ showWordCount: true,\",\n \"+ collapseBacklinks: false,\",\n \"+ pageFont: \\\"default\\\",\",\n \"+ layoutDensity: \\\"normal\\\",\",\n \"+ hideChildPages: false,\",\n \"+ showBlockRefCount: false,\",\n \"+ embedDefaultBlockId: null,\",\n \"+};\",\n \"+const defaultStats: DocumentStats = { wordCount: 0, characterCount: 0, blockCount: 0, todoTotal: 0, todoDone: 0 };\",\n \"+const EDITOR_UNMOUNT_GRACE_MS = 1000;\",\n \"+\",\n \"+export function DocumentContent({\",\n \"+ documentId,\",\n \"+ workspaceId,\",\n \"+ title,\",\n \"+ updatedAt,\",\n \"+ initialContent,\",\n \"+ initialContentRevision = null,\",\n \"+ initialConflictDetectionKey = null,\",\n \"+ initialPageSubtree = null,\",\n \"+ initialOptions,\",\n \"+ initialStats,\",\n \"+ openTableId,\",\n \"+ autoEnterEdit = false,\",\n \"+ readOnly = false,\",\n \"+ disableDownload = false,\",\n \"+ disableCopy = false,\",\n \"+}: DocumentContentProps) {\",\n \"+ const setCurrentDocument = useCurrentDocumentStore((state) => state.setCurrent);\",\n \"+ const clearIfMatch = useCurrentDocumentStore((state) => state.clearIfMatch);\",\n \"+ const canEditDocument = !readOnly;\",\n \"+ const [options, setOptions] = useState(initialOptions ?? defaultOptions);\",\n \"+ const [stats, setStats] = useState(initialStats ?? defaultStats);\",\n \"+ const [history, setHistory] = useState([]);\",\n \"+ const [historyOpen, setHistoryOpen] = useState(false);\",\n \"+ const editorBridge = useEditorBridgeStore((state) => state.bridge);\",\n \"+ const router = useRouter();\",\n \"+ const showInspector = usePageLayoutStore((state) => state.showInspector);\",\n \"+ const openCommentsForPage = useCommentsUiStore((s) => s.openForPage);\",\n \"+ const spellCheck = useAppPreferencesStore((s) => s.spellCheck);\",\n \"+ const openMoveEmbedPicker = useMoveEmbedPickerStore((s) => s.openPicker);\",\n \"+ const [pageTitle, setPageTitle] = useState(title ?? \\\"无标题\\\");\",\n \"+ const [content, setContent] = useState(initialContent);\",\n \"+ const [serverContentSnapshot, setServerContentSnapshot] = useState(initialContent);\",\n \"+ const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState(\",\n \"+ initialPageSubtree,\",\n \"+ );\",\n \"+ const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState(title ?? \\\"无标题\\\");\",\n \"+ const [contentRevision, setContentRevision] = useState(initialContentRevision);\",\n \"+ const [conflictDetectionKey, setConflictDetectionKey] = useState(initialConflictDetectionKey);\",\n \"+ const [contentLoading, setContentLoading] = useState(() => initialContent == null);\",\n \"+ const [contentError, setContentError] = useState(null);\",\n \"+ const [contentReloadKey, setContentReloadKey] = useState(0);\",\n \"+ const [showContentLoadingIndicator, setShowContentLoadingIndicator] = useState(false);\",\n \"+ const shouldStartEditing = canEditDocument;\",\n \"+ const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\",\n \"+ const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\",\n \"+ const contentLoadingTimerRef = useRef | null>(null);\",\n \"+ const editorUnmountTimerRef = useRef | null>(null);\",\n \"+ const pendingOpenTableRef = useRef(null);\",\n \"+ const latestBlocksRef = useRef(null);\",\n \"+ const pageRootRef = useRef(null);\",\n \"+ const readViewRootRef = useRef(null);\",\n \"+ const pendingRestoreSnapshotRef = useRef(null);\",\n \"+ const lastCopyBlockedAtRef = useRef(0);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setCurrentDocument(documentId, readOnly, disableDownload, disableCopy);\",\n \"+ return () => {\",\n \"+ clearIfMatch(documentId);\",\n \"+ };\",\n \"+ }, [clearIfMatch, disableCopy, disableDownload, documentId, readOnly, setCurrentDocument]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ if (!disableCopy) return;\",\n \"+\",\n \"+ const toElement = (node: Node | null): Element | null => {\",\n \"+ if (!node) return null;\",\n \"+ if (node.nodeType === Node.TEXT_NODE) {\",\n \"+ return node.parentElement;\",\n \"+ }\",\n \"+ return node instanceof Element ? node : null;\",\n \"+ };\",\n \"+\",\n \"+ const isEventInsidePage = () => {\",\n \"+ const root = pageRootRef.current;\",\n \"+ if (!root) return false;\",\n \"+ const selection = typeof window !== \\\"undefined\\\" ? window.getSelection() : null;\",\n \"+ const anchor = selection?.anchorNode ?? null;\",\n \"+ const focus = selection?.focusNode ?? null;\",\n \"+ const anchorEl = toElement(anchor);\",\n \"+ const focusEl = toElement(focus);\",\n \"+ return Boolean((anchorEl && root.contains(anchorEl)) || (focusEl && root.contains(focusEl)));\",\n \"+ };\",\n \"+\",\n \"+ const notifyBlocked = () => {\",\n \"+ const now = Date.now();\",\n \"+ if (now - lastCopyBlockedAtRef.current < 1200) return;\",\n \"+ lastCopyBlockedAtRef.current = now;\",\n \"+ window.alert(\\\"该页面已禁止复制\\\");\",\n \"+ };\",\n \"+\",\n \"+ const onCopy = (event: ClipboardEvent) => {\",\n \"+ if (!isEventInsidePage()) return;\",\n \"+ event.preventDefault();\",\n \"+ event.stopPropagation();\",\n \"+ notifyBlocked();\",\n \"+ };\",\n \"+\",\n \"+ const onCut = (event: ClipboardEvent) => {\",\n \"+ if (!isEventInsidePage()) return;\",\n \"+ event.preventDefault();\",\n \"+ event.stopPropagation();\",\n \"+ notifyBlocked();\",\n \"+ };\",\n \"+\",\n \"+ const onKeyDown = (event: KeyboardEvent) => {\",\n \"+ if (!isEventInsidePage()) return;\",\n \"+ const key = String(event.key ?? \\\"\\\").toLowerCase();\",\n \"+ const ctrlOrMeta = event.ctrlKey || event.metaKey;\",\n \"+ if (!ctrlOrMeta) return;\",\n \"+ if (key === \\\"c\\\" || key === \\\"x\\\" || key === \\\"insert\\\") {\",\n \"+ event.preventDefault();\",\n \"+ event.stopPropagation();\",\n \"+ notifyBlocked();\",\n \"+ }\",\n \"+ };\",\n \"+\",\n \"+ document.addEventListener(\\\"copy\\\", onCopy, true);\",\n \"+ document.addEventListener(\\\"cut\\\", onCut, true);\",\n \"+ document.addEventListener(\\\"keydown\\\", onKeyDown, true);\",\n \"+ return () => {\",\n \"+ document.removeEventListener(\\\"copy\\\", onCopy, true);\",\n \"+ document.removeEventListener(\\\"cut\\\", onCut, true);\",\n \"+ document.removeEventListener(\\\"keydown\\\", onKeyDown, true);\",\n \"+ };\",\n \"+ }, [disableCopy]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ const tableId = (openTableId ?? \\\"\\\").trim();\",\n \"+ if (!tableId) return;\",\n \"+ if (!editorBridge?.openTableFullScreen) return;\",\n \"+ if (pendingOpenTableRef.current === tableId) return;\",\n \"+ pendingOpenTableRef.current = tableId;\",\n \"+\",\n \"+ editorBridge.openTableFullScreen(tableId);\",\n \"+\",\n \"+ if (typeof window !== \\\"undefined\\\") {\",\n \"+ try {\",\n \"+ const url = new URL(window.location.href);\",\n \"+ url.searchParams.delete(\\\"openTableId\\\");\",\n \"+ window.history.replaceState({}, \\\"\\\", url.toString());\",\n \"+ } catch {\",\n \"+ router.replace(`/documents/${documentId}`);\",\n \"+ }\",\n \"+ }\",\n \"+ }, [documentId, editorBridge, openTableId, router]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setPageTitle(title ?? \\\"无标题\\\");\",\n \"+ }, [title]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setServerPageSubtreeTitle(title ?? \\\"无标题\\\");\",\n \"+ }, [title]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setServerPageSubtreeSnapshot(initialPageSubtree);\",\n \"+ }, [initialPageSubtree]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setOptions(initialOptions ?? defaultOptions);\",\n \"+ }, [initialOptions]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setStats(initialStats ?? defaultStats);\",\n \"+ }, [initialStats]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setContentRevision(initialContentRevision);\",\n \"+ }, [initialContentRevision]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setConflictDetectionKey(initialConflictDetectionKey);\",\n \"+ }, [initialConflictDetectionKey]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setServerContentSnapshot(initialContent);\",\n \"+ }, [initialContent]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ const nextBlocks = extractPageBlocks(content);\",\n \"+ latestBlocksRef.current = nextBlocks.length > 0 ? (nextBlocks as Json) : null;\",\n \"+ }, [content]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ const shouldForceEdit = Boolean((openTableId ?? \\\"\\\").trim()) && canEditDocument;\",\n \"+ if (shouldForceEdit) {\",\n \"+ setIsEditing(true);\",\n \"+ setKeepEditorMounted(true);\",\n \"+ }\",\n \"+ }, [canEditDocument, openTableId]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ if (isEditing) {\",\n \"+ if (editorUnmountTimerRef.current) {\",\n \"+ clearTimeout(editorUnmountTimerRef.current);\",\n \"+ editorUnmountTimerRef.current = null;\",\n \"+ }\",\n \"+ setKeepEditorMounted(true);\",\n \"+ return;\",\n \"+ }\",\n \"+ if (editorUnmountTimerRef.current) {\",\n \"+ clearTimeout(editorUnmountTimerRef.current);\",\n \"+ }\",\n \"+ editorUnmountTimerRef.current = setTimeout(() => {\",\n \"+ setKeepEditorMounted(false);\",\n \"+ editorUnmountTimerRef.current = null;\",\n \"+ }, EDITOR_UNMOUNT_GRACE_MS);\",\n \"+ return () => {\",\n \"+ if (editorUnmountTimerRef.current) {\",\n \"+ clearTimeout(editorUnmountTimerRef.current);\",\n \"+ editorUnmountTimerRef.current = null;\",\n \"+ }\",\n \"+ };\",\n \"+ }, [isEditing]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ let canceled = false;\",\n \"+ const controller = new AbortController();\",\n \"+\",\n \"+ const load = async () => {\",\n \"+ setContentError(null);\",\n \"+ setContentLoading(initialContent == null);\",\n \"+ setContent(initialContent);\",\n \"+ setShowContentLoadingIndicator(false);\",\n \"+\",\n \"+ if (initialContent != null) {\",\n \"+ return;\",\n \"+ }\",\n \"+\",\n \"+ if (contentLoadingTimerRef.current) {\",\n \"+ clearTimeout(contentLoadingTimerRef.current);\",\n \"+ contentLoadingTimerRef.current = null;\",\n \"+ }\",\n \"+ contentLoadingTimerRef.current = setTimeout(() => {\",\n \"+ if (!canceled) {\",\n \"+ setShowContentLoadingIndicator(true);\",\n \"+ }\",\n \"+ }, CONTENT_LOADING_DELAY_MS);\",\n \"+\",\n \"+ try {\",\n \"+ const response = await fetch(\",\n \"+ `/api/documents/content?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,\",\n \"+ {\",\n \"+ method: \\\"GET\\\",\",\n \"+ credentials: \\\"include\\\",\",\n \"+ signal: controller.signal,\",\n \"+ },\",\n \"+ );\",\n \"+ if (!response.ok) {\",\n \"+ const payload = await response.json().catch(() => ({}));\",\n \"+ throw new Error(payload?.error ?? \\\"加载页面内容失败\\\");\",\n \"+ }\",\n \"+ const payload = (await response.json()) as {\",\n \"+ content?: unknown;\",\n \"+ revision?: number | null;\",\n \"+ conflictDetectionKey?: string | null;\",\n \"+ pageSubtree?: PageSubtreeProjection | null;\",\n \"+ };\",\n \"+ if (canceled) return;\",\n \"+ setContent(payload.content ?? null);\",\n \"+ setServerContentSnapshot(payload.content ?? null);\",\n \"+ setContentRevision(\",\n \"+ typeof payload.revision === \\\"number\\\" && Number.isInteger(payload.revision)\",\n \"+ ? payload.revision\",\n \"+ : 0,\",\n \"+ );\",\n \"+ setConflictDetectionKey(\",\n \"+ typeof payload.conflictDetectionKey === \\\"string\\\" && payload.conflictDetectionKey.trim()\",\n \"+ ? payload.conflictDetectionKey\",\n \"+ : `${documentId}:0`,\",\n \"+ );\",\n \"+ setServerPageSubtreeSnapshot(payload.pageSubtree ?? null);\",\n \"+ setServerPageSubtreeTitle(\",\n \"+ typeof payload.pageSubtree?.rootNode.metadata.title === \\\"string\\\" &&\",\n \"+ payload.pageSubtree.rootNode.metadata.title.trim()\",\n \"+ ? payload.pageSubtree.rootNode.metadata.title\",\n \"+ : title ?? \\\"无标题\\\",\",\n \"+ );\",\n \"+ } catch (error) {\",\n \"+ if (canceled) return;\",\n \"+ if ((error as { name?: string })?.name === \\\"AbortError\\\") return;\",\n \"+ setContentError(error instanceof Error ? error.message : \\\"加载页面内容失败\\\");\",\n \"+ } finally {\",\n \"+ if (!canceled) {\",\n \"+ setContentLoading(false);\",\n \"+ setShowContentLoadingIndicator(false);\",\n \"+ }\",\n \"+ if (contentLoadingTimerRef.current) {\",\n \"+ clearTimeout(contentLoadingTimerRef.current);\",\n \"+ contentLoadingTimerRef.current = null;\",\n \"+ }\",\n \"+ }\",\n \"+ };\",\n \"+\",\n \"+ void load();\",\n \"+\",\n \"+ return () => {\",\n \"+ canceled = true;\",\n \"+ controller.abort();\",\n \"+ if (contentLoadingTimerRef.current) {\",\n \"+ clearTimeout(contentLoadingTimerRef.current);\",\n \"+ contentLoadingTimerRef.current = null;\",\n \"+ }\",\n \"+ };\",\n \"+ }, [documentId, initialContent, contentReloadKey, title, workspaceId]);\",\n \"+\",\n \"+ const persistTitle = useCallback(\",\n \"+ async (nextTitle: string) => {\",\n \"+ if (readOnly) return;\",\n \"+ const payload = nextTitle.trim() || \\\"无标题\\\";\",\n \"+ try {\",\n \"+ await renameDocumentCommand({ documentId, workspaceId, title: payload });\",\n \"+ } catch (error) {\",\n \"+ console.error(\\\"更新页面标题失败\\\", error);\",\n \"+ }\",\n \"+ },\",\n \"+ [documentId, readOnly, workspaceId],\",\n \"+ );\",\n \"+\",\n \"+ const debouncedPersistTitle = useDebouncedCallback((value: string) => {\",\n \"+ void persistTitle(value);\",\n \"+ }, 600);\",\n \"+\",\n \"+ const handleTitleChange = (event: ChangeEvent) => {\",\n \"+ if (!canEditDocument) return;\",\n \"+ const value = event.target.value;\",\n \"+ setPageTitle(value);\",\n \"+ debouncedPersistTitle(value);\",\n \"+ };\",\n \"+\",\n \"+ const handleTitleBlur = () => {\",\n \"+ if (!canEditDocument) return;\",\n \"+ void persistTitle(pageTitle);\",\n \"+ };\",\n \"+\",\n \"+ const handleTitleKeyDown = (event: ReactKeyboardEvent) => {\",\n \"+ if (event.key === \\\"Enter\\\") {\",\n \"+ event.preventDefault();\",\n \"+ event.currentTarget.blur();\",\n \"+ }\",\n \"+ };\",\n \"+\",\n \"+ const persistOptions = useCallback(\",\n \"+ async (patch: Partial) => {\",\n \"+ if (readOnly) return;\",\n \"+ try {\",\n \"+ const response = await fetch(\\\"/api/documents/options\\\", {\",\n \"+ method: \\\"POST\\\",\",\n \"+ headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"+ body: JSON.stringify({ documentId, workspaceId, options: patch }),\",\n \"+ });\",\n \"+ if (!response.ok) {\",\n \"+ const payload = await response.json().catch(() => null);\",\n \"+ console.error(payload?.error ?? \\\"更新页面选项失败\\\");\",\n \"+ }\",\n \"+ } catch (error) {\",\n \"+ console.error(error);\",\n \"+ }\",\n \"+ },\",\n \"+ [documentId, readOnly, workspaceId],\",\n \"+ );\",\n \"+\",\n \"+ const toggleOption = useCallback(\",\n \"+ (key: BooleanPageOptionKey) => {\",\n \"+ if (readOnly) return;\",\n \"+ setOptions((prev) => {\",\n \"+ const nextValue = !prev[key];\",\n \"+ const next = { ...prev, [key]: nextValue };\",\n \"+ void persistOptions({ [key]: nextValue } as Partial);\",\n \"+ return next;\",\n \"+ });\",\n \"+ },\",\n \"+ [persistOptions, readOnly],\",\n \"+ );\",\n \"+\",\n \"+ const setOptionPatch = useCallback(\",\n \"+ (patch: Partial) => {\",\n \"+ if (readOnly) return;\",\n \"+ setOptions((prev) => {\",\n \"+ const next = { ...prev, ...patch };\",\n \"+ void persistOptions(patch);\",\n \"+ return next;\",\n \"+ });\",\n \"+ },\",\n \"+ [persistOptions, readOnly],\",\n \"+ );\",\n \"+\",\n \"+ const closeToc = useCallback(() => {\",\n \"+ if (readOnly) return;\",\n \"+ setOptions((prev) => {\",\n \"+ if (!prev.showToc) return prev;\",\n \"+ const next = { ...prev, showToc: false };\",\n \"+ void persistOptions({ showToc: false });\",\n \"+ return next;\",\n \"+ });\",\n \"+ }, [persistOptions, readOnly]);\",\n \"+\",\n \"+ const handleSetPageFont = useCallback(\",\n \"+ (font: PageFont) => {\",\n \"+ setOptionPatch({ pageFont: font });\",\n \"+ },\",\n \"+ [setOptionPatch],\",\n \"+ );\",\n \"+\",\n \"+ const handleSetLayoutDensity = useCallback(\",\n \"+ (density: PageLayoutDensity) => {\",\n \"+ setOptionPatch({ layoutDensity: density });\",\n \"+ },\",\n \"+ [setOptionPatch],\",\n \"+ );\",\n \"+\",\n \"+ const handleSetEmbedDefaultToCursor = useCallback(() => {\",\n \"+ if (!canEditDocument) return;\",\n \"+ if (!isEditing) {\",\n \"+ setIsEditing(true);\",\n \"+ return;\",\n \"+ }\",\n \"+ const blockId = editorBridge?.getCursorBlockId?.() ?? null;\",\n \"+ if (!blockId) {\",\n \"+ window.alert(\\\"未找到当前光标所在块,请先在编辑器中点击任意块\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ setOptionPatch({ embedDefaultBlockId: blockId });\",\n \"+ window.alert(\\\"已设置“嵌入默认位置”\\\");\",\n \"+ }, [canEditDocument, editorBridge, isEditing, setOptionPatch]);\",\n \"+\",\n \"+ const handleClearEmbedDefault = useCallback(() => {\",\n \"+ if (readOnly) return;\",\n \"+ setOptionPatch({ embedDefaultBlockId: null });\",\n \"+ window.alert(\\\"已清除“嵌入默认位置”\\\");\",\n \"+ }, [readOnly, setOptionPatch]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ const onKeyDown = (event: KeyboardEvent) => {\",\n \"+ const root = pageRootRef.current;\",\n \"+ if (root) {\",\n \"+ const target = event.target;\",\n \"+ if (target && target instanceof Node && !root.contains(target)) {\",\n \"+ return;\",\n \"+ }\",\n \"+ }\",\n \"+ const ctrlOrMeta = event.ctrlKey || event.metaKey;\",\n \"+ if (!ctrlOrMeta) return;\",\n \"+ if (!event.shiftKey) return;\",\n \"+ const key = String(event.key ?? \\\"\\\").toLowerCase();\",\n \"+ if (key === \\\"l\\\") {\",\n \"+ event.preventDefault();\",\n \"+ toggleOption(\\\"showToc\\\");\",\n \"+ } else if (key === \\\"c\\\") {\",\n \"+ event.preventDefault();\",\n \"+ toggleOption(\\\"protectEditing\\\");\",\n \"+ }\",\n \"+ };\",\n \"+ window.addEventListener(\\\"keydown\\\", onKeyDown);\",\n \"+ return () => window.removeEventListener(\\\"keydown\\\", onKeyDown);\",\n \"+ }, [toggleOption]);\",\n \"+\",\n \"+ const buildDocumentUrl = useCallback((id: string): string => {\",\n \"+ if (typeof window === \\\"undefined\\\" || !window.location) {\",\n \"+ return `/documents/${id}`;\",\n \"+ }\",\n \"+ return `${window.location.origin}/documents/${id}`;\",\n \"+ }, []);\",\n \"+\",\n \"+ const copyText = useCallback(async (text: string, successMessage: string) => {\",\n \"+ if (typeof navigator !== \\\"undefined\\\" && navigator.clipboard) {\",\n \"+ try {\",\n \"+ await navigator.clipboard.writeText(text);\",\n \"+ window.alert(successMessage);\",\n \"+ return;\",\n \"+ } catch {\",\n \"+ }\",\n \"+ }\",\n \"+ window.prompt(\\\"复制失败,请手动复制内容\\\", text);\",\n \"+ }, []);\",\n \"+\",\n \"+ const handleCopyPageLink = useCallback(\",\n \"+ async (includeTitle: boolean) => {\",\n \"+ const url = buildDocumentUrl(documentId);\",\n \"+ if (includeTitle) {\",\n \"+ const text = `${pageTitle || \\\"无标题\\\"}\\\\n${url}`;\",\n \"+ await copyText(text, \\\"标题 + 链接已复制\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ await copyText(url, \\\"页面链接已复制\\\");\",\n \"+ },\",\n \"+ [buildDocumentUrl, copyText, documentId, pageTitle],\",\n \"+ );\",\n \"+\",\n \"+ const handleCopyPageReference = useCallback(\",\n \"+ async (mode: \\\"inline\\\" | \\\"embed\\\") => {\",\n \"+ const template = mode === \\\"inline\\\" ? `((${documentId}))` : `{{${documentId}}}`;\",\n \"+ await copyText(template, mode === \\\"inline\\\" ? \\\"行内引用已复制\\\" : \\\"嵌入引用已复制\\\");\",\n \"+ },\",\n \"+ [copyText, documentId],\",\n \"+ );\",\n \"+\",\n \"+ const handleUndo = useCallback(() => {\",\n \"+ if (!isEditing) {\",\n \"+ setIsEditing(true);\",\n \"+ return;\",\n \"+ }\",\n \"+ editorBridge?.undo?.();\",\n \"+ }, [editorBridge, isEditing]);\",\n \"+\",\n \"+ const handleRedo = useCallback(() => {\",\n \"+ if (!isEditing) {\",\n \"+ setIsEditing(true);\",\n \"+ return;\",\n \"+ }\",\n \"+ editorBridge?.redo?.();\",\n \"+ }, [editorBridge, isEditing]);\",\n \"+\",\n \"+ const handleDeletePage = useCallback(async () => {\",\n \"+ if (readOnly) return;\",\n \"+ const ok = window.confirm(\\\"确定删除该页面吗?删除后会进入垃圾桶。\\\");\",\n \"+ if (!ok) return;\",\n \"+ try {\",\n \"+ await deleteDocumentCommand({ documentId, workspaceId });\",\n \"+ } catch (error) {\",\n \"+ const payload = error instanceof Error ? error.message : null;\",\n \"+ window.alert(payload ?? \\\"删除失败\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ router.push(\\\"/\\\");\",\n \"+ router.refresh();\",\n \"+ }, [documentId, readOnly, router, workspaceId]);\",\n \"+\",\n \"+ const handleOpenMoveEmbed = useCallback(() => {\",\n \"+ if (readOnly) return;\",\n \"+ openMoveEmbedPicker({\",\n \"+ workspaceId,\",\n \"+ defaultMode: \\\"move\\\",\",\n \"+ modes: [\\\"move\\\", \\\"embed\\\"],\",\n \"+ allowRoot: true,\",\n \"+ excludeIds: [documentId],\",\n \"+ onPick: async (mode, targetId) => {\",\n \"+ if (mode === \\\"move\\\") {\",\n \"+ try {\",\n \"+ await moveDocumentCommand({\",\n \"+ documentId,\",\n \"+ parentId: targetId ?? null,\",\n \"+ position: 999999,\",\n \"+ workspaceId,\",\n \"+ });\",\n \"+ } catch (error) {\",\n \"+ window.alert(error instanceof Error ? error.message : \\\"移动失败\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ window.alert(\\\"移动成功\\\");\",\n \"+ router.refresh();\",\n \"+ return;\",\n \"+ }\",\n \"+\",\n \"+ if (!targetId) {\",\n \"+ window.alert(\\\"请选择目标页面\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+\",\n \"+ try {\",\n \"+ await embedDocumentCommand({\",\n \"+ sourceId: documentId,\",\n \"+ targetId,\",\n \"+ });\",\n \"+ } catch (error) {\",\n \"+ const message = error instanceof Error ? error.message : \\\"嵌入失败\\\";\",\n \"+ window.alert(message);\",\n \"+ return;\",\n \"+ }\",\n \"+\",\n \"+ window.alert(\\\"已嵌入到目标页面\\\");\",\n \"+ },\",\n \"+ });\",\n \"+ }, [documentId, openMoveEmbedPicker, readOnly, router, workspaceId]);\",\n \"+\",\n \"+ const handleAddToTemplates = useCallback(async () => {\",\n \"+ if (readOnly) return;\",\n \"+ const ok = window.confirm(\\\"将该页面添加为模板?\\\");\",\n \"+ if (!ok) return;\",\n \"+ const resp = await fetch(\\\"/api/documents/template\\\", {\",\n \"+ method: \\\"POST\\\",\",\n \"+ headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"+ body: JSON.stringify({ documentId, isTemplate: true }),\",\n \"+ });\",\n \"+ if (!resp.ok) {\",\n \"+ const payload = await resp.json().catch(() => ({}));\",\n \"+ window.alert(payload?.error ?? \\\"设置模板失败\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ window.alert(\\\"已添加为模板\\\");\",\n \"+ router.refresh();\",\n \"+ }, [documentId, readOnly, router]);\",\n \"+\",\n \"+ const formattedUpdatedAt = useMemo(() => {\",\n \"+ if (!updatedAt) return \\\"\\\";\",\n \"+ const date = new Date(updatedAt);\",\n \"+ if (Number.isNaN(date.getTime())) return \\\"\\\";\",\n \"+ return new Intl.DateTimeFormat(\\\"zh-CN\\\", {\",\n \"+ timeZone: \\\"Asia/Shanghai\\\",\",\n \"+ year: \\\"numeric\\\",\",\n \"+ month: \\\"numeric\\\",\",\n \"+ day: \\\"numeric\\\",\",\n \"+ hour: \\\"2-digit\\\",\",\n \"+ minute: \\\"2-digit\\\",\",\n \"+ second: \\\"2-digit\\\",\",\n \"+ hour12: false,\",\n \"+ }).format(date);\",\n \"+ }, [updatedAt]);\",\n \"+\",\n \"+ const handleExport = useCallback(() => {\",\n \"+ if (disableDownload) {\",\n \"+ window.alert(\\\"该页面已禁止下载\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ const latest = history[0];\",\n \"+ const exportBlocks = latest?.blocks ?? latestBlocksRef.current;\",\n \"+ if (!exportBlocks) {\",\n \"+ window.alert(\\\"暂无可导出的内容\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ const payload = JSON.stringify(exportBlocks, null, 2);\",\n \"+ const blob = new Blob([payload], { type: \\\"application/json\\\" });\",\n \"+ const url = URL.createObjectURL(blob);\",\n \"+ const anchor = document.createElement(\\\"a\\\");\",\n \"+ anchor.href = url;\",\n \"+ anchor.download = `${title ?? \\\"未命名页面\\\"}-${new Date().toISOString()}.json`;\",\n \"+ anchor.click();\",\n \"+ URL.revokeObjectURL(url);\",\n \"+ }, [disableDownload, history, title]);\",\n \"+\",\n \"+ const pageRootClass = cn(\",\n \"+ \\\"flex h-full overflow-hidden bg-wolai-bg\\\",\",\n \"+ options.pageFont === \\\"song\\\" && \\\"wolai-page-font-song\\\",\",\n \"+ options.pageFont === \\\"kai\\\" && \\\"wolai-page-font-kai\\\",\",\n \"+ options.layoutDensity === \\\"compact\\\" && \\\"wolai-page-density-compact\\\",\",\n \"+ options.layoutDensity === \\\"spacious\\\" && \\\"wolai-page-density-spacious\\\",\",\n \"+ options.smallText && \\\"wolai-small-text\\\",\",\n \"+ options.hideChildPages && \\\"wolai-hide-child-pages\\\",\",\n \"+ );\",\n \"+ const pageSubtree = useMemo(() => {\",\n \"+ const hasServerPageSubtree = Boolean(serverPageSubtreeSnapshot);\",\n \"+ const titleUnchanged = pageTitle === serverPageSubtreeTitle;\",\n \"+ const contentUnchanged = content === serverContentSnapshot;\",\n \"+\",\n \"+ if (hasServerPageSubtree && titleUnchanged && contentUnchanged) {\",\n \"+ return serverPageSubtreeSnapshot;\",\n \"+ }\",\n \"+ return null;\",\n \"+ }, [\",\n \"+ content,\",\n \"+ pageTitle,\",\n \"+ serverContentSnapshot,\",\n \"+ serverPageSubtreeSnapshot,\",\n \"+ serverPageSubtreeTitle,\",\n \"+ ]);\",\n \"+ const readViewTocEntries = useMemo(\",\n \"+ () =>\",\n \"+ (pageSubtree?.outline ?? [])\",\n \"+ .filter((entry) => typeof entry.anchorBlockId === \\\"string\\\" && entry.anchorBlockId.trim())\",\n \"+ .map(({ anchorBlockId, level, numbering, title: entryTitle }) => ({\",\n \"+ id: anchorBlockId as string,\",\n \"+ level,\",\n \"+ numbering,\",\n \"+ title: entryTitle,\",\n \"+ })),\",\n \"+ [pageSubtree],\",\n \"+ );\",\n \"+ const inspectorCanUseEditorBridge = canEditDocument && isEditing;\",\n \"+\",\n \"+ const jumpToHeading = useCallback((headingId: string) => {\",\n \"+ const targetRoot = !isEditing ? readViewRootRef.current : pageRootRef.current;\",\n \"+ const target = targetRoot?.querySelector(`[data-id=\\\"${headingId}\\\"]`);\",\n \"+ if (target) {\",\n \"+ target.scrollIntoView({ behavior: \\\"smooth\\\", block: \\\"center\\\" });\",\n \"+ }\",\n \"+ }, [isEditing]);\",\n \"+\",\n \"+ const handleSnapshot = useCallback((payload: { blocks: Json; stats: DocumentStats }) => {\",\n \"+ setContent(payload.blocks);\",\n \"+ latestBlocksRef.current = payload.blocks;\",\n \"+ setHistory((prev) => {\",\n \"+ const now = Date.now();\",\n \"+ if (prev.length > 0 && now - prev[0].timestamp < 4000) {\",\n \"+ return prev;\",\n \"+ }\",\n \"+ const snapshot: DocumentSnapshot = {\",\n \"+ id: `${now}-${Math.random().toString(36).slice(2, 8)}`,\",\n \"+ timestamp: now,\",\n \"+ blocks: payload.blocks,\",\n \"+ stats: payload.stats,\",\n \"+ };\",\n \"+ return [snapshot, ...prev].slice(0, 15);\",\n \"+ });\",\n \"+ }, []);\",\n \"+\",\n \"+ const persistStatsRequest = useCallback((next: DocumentStats) => {\",\n \"+ void fetch(\\\"/api/documents/stats\\\", {\",\n \"+ method: \\\"POST\\\",\",\n \"+ headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"+ body: JSON.stringify({ documentId, workspaceId, stats: next }),\",\n \"+ }).catch((error) => console.error(error));\",\n \"+ }, [documentId, workspaceId]);\",\n \"+\",\n \"+ const persistStats = useDebouncedCallback(persistStatsRequest, 1500);\",\n \"+\",\n \"+ const handleStatsChange = useCallback(\",\n \"+ (nextStats: DocumentStats) => {\",\n \"+ setStats(nextStats);\",\n \"+ persistStats(nextStats);\",\n \"+ },\",\n \"+ [persistStats],\",\n \"+ );\",\n \"+\",\n \"+ const handleRestoreSnapshot = useCallback(\",\n \"+ (snapshot: DocumentSnapshot) => {\",\n \"+ if (!isEditing || !editorBridge) {\",\n \"+ pendingRestoreSnapshotRef.current = snapshot;\",\n \"+ setIsEditing(true);\",\n \"+ return;\",\n \"+ }\",\n \"+ editorBridge.replaceWithSnapshot(snapshot.blocks);\",\n \"+ setHistoryOpen(false);\",\n \"+ },\",\n \"+ [editorBridge, isEditing],\",\n \"+ );\",\n \"+\",\n \"+ const handleEnterEditMode = useCallback(() => {\",\n \"+ if (!canEditDocument) return;\",\n \"+ setIsEditing(true);\",\n \"+ }, [canEditDocument]);\",\n \"+\",\n \"+ const handleExitEditMode = useCallback(() => {\",\n \"+ setIsEditing(false);\",\n \"+ }, []);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ if (!isEditing) return;\",\n \"+ if (!editorBridge) return;\",\n \"+ const pendingSnapshot = pendingRestoreSnapshotRef.current;\",\n \"+ if (!pendingSnapshot) return;\",\n \"+ pendingRestoreSnapshotRef.current = null;\",\n \"+ editorBridge.replaceWithSnapshot(pendingSnapshot.blocks);\",\n \"+ setHistoryOpen(false);\",\n \"+ }, [editorBridge, isEditing]);\",\n \"+ return (\",\n \"+ \",\n \"+
\",\n \"+
\",\n \"+
\",\n \"+
\",\n \"+
\",\n \"+ {isEditing && canEditDocument ? (\",\n \"+
\",\n \"+ \",\n \"+
\",\n \"+ ) : (\",\n \"+

\",\n \"+ {pageTitle || \\\"无标题\\\"}\",\n \"+

\",\n \"+ )}\",\n \"+ {readOnly ? (\",\n \"+

该页面为只读共享,无法编辑。

\",\n \"+ ) : options.protectEditing && isEditing ? (\",\n \"+

当前页面已开启编辑保护,关闭后方可修改内容。

\",\n \"+ ) : null}\",\n \"+

最近更新:{formattedUpdatedAt}

\",\n \"+
\",\n \"+ {canEditDocument && (\",\n \"+
\",\n \"+ {isEditing ? (\",\n \"+ \",\n \"+ ) : (\",\n \"+ \",\n \"+ )}\",\n \"+
\",\n \"+ )}\",\n \"+
\",\n \"+
\",\n \"+
\",\n \"+ {contentLoading ? (\",\n \"+ showContentLoadingIndicator ? (\",\n \"+
\",\n \"+ 页面内容加载中...\",\n \"+
\",\n \"+ ) : (\",\n \"+
\",\n \"+ )\",\n \"+ ) : contentError ? (\",\n \"+
\",\n \"+
{contentError}
\",\n \"+ {\",\n \"+ setContentError(null);\",\n \"+ setContentLoading(true);\",\n \"+ setContentReloadKey((prev) => prev + 1);\",\n \"+ }}\",\n \"+ >\",\n \"+ 重试\",\n \"+ \",\n \"+
\",\n \"+ ) : (\",\n \"+
\",\n \"+ {keepEditorMounted && (\",\n \"+
\",\n \"+ {\",\n \"+ setContentRevision(meta.revision);\",\n \"+ setConflictDetectionKey(meta.conflictDetectionKey);\",\n \"+ }}\",\n \"+ />\",\n \"+
\",\n \"+ )}\",\n \"+ {!isEditing && (\",\n \"+
\",\n \"+ \",\n \"+ \",\n \"+
\",\n \"+ )}\",\n \"+
\",\n \"+ )}\",\n \"+ \",\n \"+
\",\n \"+
\",\n \"+ {showInspector && (\",\n \"+ setHistoryOpen(true)}\",\n \"+ onOpenComments={() => openCommentsForPage({ workspaceId, documentId })}\",\n \"+ onUndo={canEditDocument ? handleUndo : undefined}\",\n \"+ onRedo={canEditDocument ? handleRedo : undefined}\",\n \"+ onDeletePage={canEditDocument ? handleDeletePage : undefined}\",\n \"+ onOpenMoveEmbedPicker={canEditDocument ? handleOpenMoveEmbed : undefined}\",\n \"+ onCopyPageLink={handleCopyPageLink}\",\n \"+ onCopyPageReference={handleCopyPageReference}\",\n \"+ onAddToTemplates={canEditDocument ? handleAddToTemplates : undefined}\",\n \"+ />\",\n \"+ )}\",\n \"+
\",\n \"+ \",\n \"+ \",\n \"+ \",\n \"+ latestBlocksRef.current}\",\n \"+ getLatestPageSubtree={() => pageSubtree}\",\n \"+ />\",\n \"+ \",\n \"+ );\",\n \"+}\"\n ]\n }\n ],\n \"userModified\": false,\n \"replaceAll\": false,\n \"gitDiff\": null\n}","tool_name":"edit_file","tool_use_id":"call_hfnNUfjkii3uvyEWssUihpBS","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_P7xKbQyE2ZTeq2t7Y5baMRyK","input":"{\"new_string\":\" {canEditDocument && false && (\\n\",\"old_string\":\" {canEditDocument && (\\n\",\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx\",\"replace_all\":false}","name":"edit_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":145896,"output_tokens":67}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx\",\n \"oldString\": \" {canEditDocument && (\\n\",\n \"newString\": \" {canEditDocument && false && (\\n\",\n \"originalFile\": \"\\\"use client\\\";\\n\\nimport dynamic from \\\"next/dynamic\\\";\\nimport { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent as ReactKeyboardEvent } from \\\"react\\\";\\nimport type { BooleanPageOptionKey, DocumentStats, PageOptionsState, PageFont, PageLayoutDensity } from \\\"@/types/page-options\\\";\\nimport { useEditorBridgeStore } from \\\"@/store/editor-bridge\\\";\\nimport { usePageLayoutStore } from \\\"@/store/page-layout\\\";\\nimport { useCurrentDocumentStore } from \\\"@/store/current-document\\\";\\nimport type { Json } from \\\"@/types/supabase\\\";\\nimport type { DocumentSnapshot } from \\\"@/types/document\\\";\\nimport { useDebouncedCallback } from \\\"@/hooks/use-debounced-callback\\\";\\nimport { ImagePickerProvider } from \\\"@/components/media/image-picker-context\\\";\\nimport { useRouter } from \\\"next/navigation\\\";\\nimport { DocumentAiAgentPanel } from \\\"./DocumentAiAgentPanel\\\";\\nimport { CONTENT_LOADING_DELAY_MS } from \\\"@/lib/constants\\\";\\nimport { useCommentsUiStore } from \\\"@/store/comments-ui\\\";\\nimport { useAppPreferencesStore } from \\\"@/store/app-preferences\\\";\\nimport { useMoveEmbedPickerStore } from \\\"@/store/move-embed-picker\\\";\\nimport { cn } from \\\"@/lib/utils\\\";\\nimport { Button } from \\\"@/components/ui/button\\\";\\nimport { DocumentToc } from \\\"@/components/editor/document-toc\\\";\\nimport { DocumentReadView } from \\\"@/components/editor/document-read-view\\\";\\nimport { extractPageBlocks, type PageSubtreeProjection } from \\\"@/lib/documents/page-subtree\\\";\\nimport {\\n deleteDocumentCommand,\\n embedDocumentCommand,\\n moveDocumentCommand,\\n renameDocumentCommand,\\n} from \\\"@/lib/documents/tree-command-client\\\";\\nimport { EditorHost } from \\\"@/components/editor/editor-host\\\";\\n\\nconst PageOptionsSidebar = dynamic(\\n () => import(\\\"@/components/editor/page-options-sidebar\\\").then((mod) => mod.PageOptionsSidebar),\\n { ssr: false },\\n);\\n\\nconst PageBacklinksPanel = dynamic(\\n () => import(\\\"@/components/editor/page-backlinks-panel\\\").then((mod) => mod.PageBacklinksPanel),\\n {\\n ssr: false,\\n loading: () => null,\\n },\\n);\\n\\nconst DocumentHistoryDrawer = dynamic(\\n () => import(\\\"@/components/editor/document-history-drawer\\\").then((mod) => mod.DocumentHistoryDrawer),\\n {\\n ssr: false,\\n loading: () => null,\\n },\\n);\\n\\nconst MoveEmbedPickerHost = dynamic(\\n () => import(\\\"@/components/documents/move-embed-picker-host\\\").then((mod) => mod.MoveEmbedPickerHost),\\n {\\n ssr: false,\\n loading: () => null,\\n },\\n);\\n\\nconst DocumentCommentsDrawer = dynamic(\\n () => import(\\\"@/components/editor/document-comments-drawer\\\").then((mod) => mod.DocumentCommentsDrawer),\\n {\\n ssr: false,\\n loading: () => null,\\n },\\n);\\n\\nexport interface DocumentContentProps {\\n documentId: string;\\n workspaceId: string;\\n title: string | null;\\n updatedAt: string | null;\\n initialContent: unknown;\\n initialContentRevision?: number | null;\\n initialConflictDetectionKey?: string | null;\\n initialPageSubtree?: PageSubtreeProjection | null;\\n initialOptions: PageOptionsState;\\n initialStats: DocumentStats | null;\\n openTableId?: string | null;\\n autoEnterEdit?: boolean;\\n readOnly?: boolean;\\n disableDownload?: boolean;\\n disableCopy?: boolean;\\n}\\n\\nconst defaultOptions: PageOptionsState = {\\n wideLayout: false,\\n smallText: false,\\n showHeadingNumbers: true,\\n showToc: false,\\n showStructure: false,\\n protectEditing: false,\\n showWordCount: true,\\n collapseBacklinks: false,\\n pageFont: \\\"default\\\",\\n layoutDensity: \\\"normal\\\",\\n hideChildPages: false,\\n showBlockRefCount: false,\\n embedDefaultBlockId: null,\\n};\\nconst defaultStats: DocumentStats = { wordCount: 0, characterCount: 0, blockCount: 0, todoTotal: 0, todoDone: 0 };\\nconst EDITOR_UNMOUNT_GRACE_MS = 1000;\\n\\nexport function DocumentContent({\\n documentId,\\n workspaceId,\\n title,\\n updatedAt,\\n initialContent,\\n initialContentRevision = null,\\n initialConflictDetectionKey = null,\\n initialPageSubtree = null,\\n initialOptions,\\n initialStats,\\n openTableId,\\n autoEnterEdit = false,\\n readOnly = false,\\n disableDownload = false,\\n disableCopy = false,\\n}: DocumentContentProps) {\\n const setCurrentDocument = useCurrentDocumentStore((state) => state.setCurrent);\\n const clearIfMatch = useCurrentDocumentStore((state) => state.clearIfMatch);\\n const canEditDocument = !readOnly;\\n const [options, setOptions] = useState(initialOptions ?? defaultOptions);\\n const [stats, setStats] = useState(initialStats ?? defaultStats);\\n const [history, setHistory] = useState([]);\\n const [historyOpen, setHistoryOpen] = useState(false);\\n const editorBridge = useEditorBridgeStore((state) => state.bridge);\\n const router = useRouter();\\n const showInspector = usePageLayoutStore((state) => state.showInspector);\\n const openCommentsForPage = useCommentsUiStore((s) => s.openForPage);\\n const spellCheck = useAppPreferencesStore((s) => s.spellCheck);\\n const openMoveEmbedPicker = useMoveEmbedPickerStore((s) => s.openPicker);\\n const [pageTitle, setPageTitle] = useState(title ?? \\\"无标题\\\");\\n const [content, setContent] = useState(initialContent);\\n const [serverContentSnapshot, setServerContentSnapshot] = useState(initialContent);\\n const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState(\\n initialPageSubtree,\\n );\\n const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState(title ?? \\\"无标题\\\");\\n const [contentRevision, setContentRevision] = useState(initialContentRevision);\\n const [conflictDetectionKey, setConflictDetectionKey] = useState(initialConflictDetectionKey);\\n const [contentLoading, setContentLoading] = useState(() => initialContent == null);\\n const [contentError, setContentError] = useState(null);\\n const [contentReloadKey, setContentReloadKey] = useState(0);\\n const [showContentLoadingIndicator, setShowContentLoadingIndicator] = useState(false);\\n const shouldStartEditing = canEditDocument;\\n const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\\n const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\\n const contentLoadingTimerRef = useRef | null>(null);\\n const editorUnmountTimerRef = useRef | null>(null);\\n const pendingOpenTableRef = useRef(null);\\n const latestBlocksRef = useRef(null);\\n const pageRootRef = useRef(null);\\n const readViewRootRef = useRef(null);\\n const pendingRestoreSnapshotRef = useRef(null);\\n const lastCopyBlockedAtRef = useRef(0);\\n\\n useEffect(() => {\\n setCurrentDocument(documentId, readOnly, disableDownload, disableCopy);\\n return () => {\\n clearIfMatch(documentId);\\n };\\n }, [clearIfMatch, disableCopy, disableDownload, documentId, readOnly, setCurrentDocument]);\\n\\n useEffect(() => {\\n if (!disableCopy) return;\\n\\n const toElement = (node: Node | null): Element | null => {\\n if (!node) return null;\\n if (node.nodeType === Node.TEXT_NODE) {\\n return node.parentElement;\\n }\\n return node instanceof Element ? node : null;\\n };\\n\\n const isEventInsidePage = () => {\\n const root = pageRootRef.current;\\n if (!root) return false;\\n const selection = typeof window !== \\\"undefined\\\" ? window.getSelection() : null;\\n const anchor = selection?.anchorNode ?? null;\\n const focus = selection?.focusNode ?? null;\\n const anchorEl = toElement(anchor);\\n const focusEl = toElement(focus);\\n return Boolean((anchorEl && root.contains(anchorEl)) || (focusEl && root.contains(focusEl)));\\n };\\n\\n const notifyBlocked = () => {\\n const now = Date.now();\\n if (now - lastCopyBlockedAtRef.current < 1200) return;\\n lastCopyBlockedAtRef.current = now;\\n window.alert(\\\"该页面已禁止复制\\\");\\n };\\n\\n const onCopy = (event: ClipboardEvent) => {\\n if (!isEventInsidePage()) return;\\n event.preventDefault();\\n event.stopPropagation();\\n notifyBlocked();\\n };\\n\\n const onCut = (event: ClipboardEvent) => {\\n if (!isEventInsidePage()) return;\\n event.preventDefault();\\n event.stopPropagation();\\n notifyBlocked();\\n };\\n\\n const onKeyDown = (event: KeyboardEvent) => {\\n if (!isEventInsidePage()) return;\\n const key = String(event.key ?? \\\"\\\").toLowerCase();\\n const ctrlOrMeta = event.ctrlKey || event.metaKey;\\n if (!ctrlOrMeta) return;\\n if (key === \\\"c\\\" || key === \\\"x\\\" || key === \\\"insert\\\") {\\n event.preventDefault();\\n event.stopPropagation();\\n notifyBlocked();\\n }\\n };\\n\\n document.addEventListener(\\\"copy\\\", onCopy, true);\\n document.addEventListener(\\\"cut\\\", onCut, true);\\n document.addEventListener(\\\"keydown\\\", onKeyDown, true);\\n return () => {\\n document.removeEventListener(\\\"copy\\\", onCopy, true);\\n document.removeEventListener(\\\"cut\\\", onCut, true);\\n document.removeEventListener(\\\"keydown\\\", onKeyDown, true);\\n };\\n }, [disableCopy]);\\n\\n useEffect(() => {\\n const tableId = (openTableId ?? \\\"\\\").trim();\\n if (!tableId) return;\\n if (!editorBridge?.openTableFullScreen) return;\\n if (pendingOpenTableRef.current === tableId) return;\\n pendingOpenTableRef.current = tableId;\\n\\n editorBridge.openTableFullScreen(tableId);\\n\\n if (typeof window !== \\\"undefined\\\") {\\n try {\\n const url = new URL(window.location.href);\\n url.searchParams.delete(\\\"openTableId\\\");\\n window.history.replaceState({}, \\\"\\\", url.toString());\\n } catch {\\n router.replace(`/documents/${documentId}`);\\n }\\n }\\n }, [documentId, editorBridge, openTableId, router]);\\n\\n useEffect(() => {\\n setPageTitle(title ?? \\\"无标题\\\");\\n }, [title]);\\n\\n useEffect(() => {\\n setServerPageSubtreeTitle(title ?? \\\"无标题\\\");\\n }, [title]);\\n\\n useEffect(() => {\\n setServerPageSubtreeSnapshot(initialPageSubtree);\\n }, [initialPageSubtree]);\\n\\n useEffect(() => {\\n setOptions(initialOptions ?? defaultOptions);\\n }, [initialOptions]);\\n\\n useEffect(() => {\\n setStats(initialStats ?? defaultStats);\\n }, [initialStats]);\\n\\n useEffect(() => {\\n setContentRevision(initialContentRevision);\\n }, [initialContentRevision]);\\n\\n useEffect(() => {\\n setConflictDetectionKey(initialConflictDetectionKey);\\n }, [initialConflictDetectionKey]);\\n\\n useEffect(() => {\\n setServerContentSnapshot(initialContent);\\n }, [initialContent]);\\n\\n useEffect(() => {\\n const nextBlocks = extractPageBlocks(content);\\n latestBlocksRef.current = nextBlocks.length > 0 ? (nextBlocks as Json) : null;\\n }, [content]);\\n\\n useEffect(() => {\\n const shouldForceEdit = Boolean((openTableId ?? \\\"\\\").trim()) && canEditDocument;\\n if (shouldForceEdit) {\\n setIsEditing(true);\\n setKeepEditorMounted(true);\\n }\\n }, [canEditDocument, openTableId]);\\n\\n useEffect(() => {\\n if (isEditing) {\\n if (editorUnmountTimerRef.current) {\\n clearTimeout(editorUnmountTimerRef.current);\\n editorUnmountTimerRef.current = null;\\n }\\n setKeepEditorMounted(true);\\n return;\\n }\\n if (editorUnmountTimerRef.current) {\\n clearTimeout(editorUnmountTimerRef.current);\\n }\\n editorUnmountTimerRef.current = setTimeout(() => {\\n setKeepEditorMounted(false);\\n editorUnmountTimerRef.current = null;\\n }, EDITOR_UNMOUNT_GRACE_MS);\\n return () => {\\n if (editorUnmountTimerRef.current) {\\n clearTimeout(editorUnmountTimerRef.current);\\n editorUnmountTimerRef.current = null;\\n }\\n };\\n }, [isEditing]);\\n\\n useEffect(() => {\\n let canceled = false;\\n const controller = new AbortController();\\n\\n const load = async () => {\\n setContentError(null);\\n setContentLoading(initialContent == null);\\n setContent(initialContent);\\n setShowContentLoadingIndicator(false);\\n\\n if (initialContent != null) {\\n return;\\n }\\n\\n if (contentLoadingTimerRef.current) {\\n clearTimeout(contentLoadingTimerRef.current);\\n contentLoadingTimerRef.current = null;\\n }\\n contentLoadingTimerRef.current = setTimeout(() => {\\n if (!canceled) {\\n setShowContentLoadingIndicator(true);\\n }\\n }, CONTENT_LOADING_DELAY_MS);\\n\\n try {\\n const response = await fetch(\\n `/api/documents/content?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,\\n {\\n method: \\\"GET\\\",\\n credentials: \\\"include\\\",\\n signal: controller.signal,\\n },\\n );\\n if (!response.ok) {\\n const payload = await response.json().catch(() => ({}));\\n throw new Error(payload?.error ?? \\\"加载页面内容失败\\\");\\n }\\n const payload = (await response.json()) as {\\n content?: unknown;\\n revision?: number | null;\\n conflictDetectionKey?: string | null;\\n pageSubtree?: PageSubtreeProjection | null;\\n };\\n if (canceled) return;\\n setContent(payload.content ?? null);\\n setServerContentSnapshot(payload.content ?? null);\\n setContentRevision(\\n typeof payload.revision === \\\"number\\\" && Number.isInteger(payload.revision)\\n ? payload.revision\\n : 0,\\n );\\n setConflictDetectionKey(\\n typeof payload.conflictDetectionKey === \\\"string\\\" && payload.conflictDetectionKey.trim()\\n ? payload.conflictDetectionKey\\n : `${documentId}:0`,\\n );\\n setServerPageSubtreeSnapshot(payload.pageSubtree ?? null);\\n setServerPageSubtreeTitle(\\n typeof payload.pageSubtree?.rootNode.metadata.title === \\\"string\\\" &&\\n payload.pageSubtree.rootNode.metadata.title.trim()\\n ? payload.pageSubtree.rootNode.metadata.title\\n : title ?? \\\"无标题\\\",\\n );\\n } catch (error) {\\n if (canceled) return;\\n if ((error as { name?: string })?.name === \\\"AbortError\\\") return;\\n setContentError(error instanceof Error ? error.message : \\\"加载页面内容失败\\\");\\n } finally {\\n if (!canceled) {\\n setContentLoading(false);\\n setShowContentLoadingIndicator(false);\\n }\\n if (contentLoadingTimerRef.current) {\\n clearTimeout(contentLoadingTimerRef.current);\\n contentLoadingTimerRef.current = null;\\n }\\n }\\n };\\n\\n void load();\\n\\n return () => {\\n canceled = true;\\n controller.abort();\\n if (contentLoadingTimerRef.current) {\\n clearTimeout(contentLoadingTimerRef.current);\\n contentLoadingTimerRef.current = null;\\n }\\n };\\n }, [documentId, initialContent, contentReloadKey, title, workspaceId]);\\n\\n const persistTitle = useCallback(\\n async (nextTitle: string) => {\\n if (readOnly) return;\\n const payload = nextTitle.trim() || \\\"无标题\\\";\\n try {\\n await renameDocumentCommand({ documentId, workspaceId, title: payload });\\n } catch (error) {\\n console.error(\\\"更新页面标题失败\\\", error);\\n }\\n },\\n [documentId, readOnly, workspaceId],\\n );\\n\\n const debouncedPersistTitle = useDebouncedCallback((value: string) => {\\n void persistTitle(value);\\n }, 600);\\n\\n const handleTitleChange = (event: ChangeEvent) => {\\n if (!canEditDocument) return;\\n const value = event.target.value;\\n setPageTitle(value);\\n debouncedPersistTitle(value);\\n };\\n\\n const handleTitleBlur = () => {\\n if (!canEditDocument) return;\\n void persistTitle(pageTitle);\\n };\\n\\n const handleTitleKeyDown = (event: ReactKeyboardEvent) => {\\n if (event.key === \\\"Enter\\\") {\\n event.preventDefault();\\n event.currentTarget.blur();\\n }\\n };\\n\\n const persistOptions = useCallback(\\n async (patch: Partial) => {\\n if (readOnly) return;\\n try {\\n const response = await fetch(\\\"/api/documents/options\\\", {\\n method: \\\"POST\\\",\\n headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\\n body: JSON.stringify({ documentId, workspaceId, options: patch }),\\n });\\n if (!response.ok) {\\n const payload = await response.json().catch(() => null);\\n console.error(payload?.error ?? \\\"更新页面选项失败\\\");\\n }\\n } catch (error) {\\n console.error(error);\\n }\\n },\\n [documentId, readOnly, workspaceId],\\n );\\n\\n const toggleOption = useCallback(\\n (key: BooleanPageOptionKey) => {\\n if (readOnly) return;\\n setOptions((prev) => {\\n const nextValue = !prev[key];\\n const next = { ...prev, [key]: nextValue };\\n void persistOptions({ [key]: nextValue } as Partial);\\n return next;\\n });\\n },\\n [persistOptions, readOnly],\\n );\\n\\n const setOptionPatch = useCallback(\\n (patch: Partial) => {\\n if (readOnly) return;\\n setOptions((prev) => {\\n const next = { ...prev, ...patch };\\n void persistOptions(patch);\\n return next;\\n });\\n },\\n [persistOptions, readOnly],\\n );\\n\\n const closeToc = useCallback(() => {\\n if (readOnly) return;\\n setOptions((prev) => {\\n if (!prev.showToc) return prev;\\n const next = { ...prev, showToc: false };\\n void persistOptions({ showToc: false });\\n return next;\\n });\\n }, [persistOptions, readOnly]);\\n\\n const handleSetPageFont = useCallback(\\n (font: PageFont) => {\\n setOptionPatch({ pageFont: font });\\n },\\n [setOptionPatch],\\n );\\n\\n const handleSetLayoutDensity = useCallback(\\n (density: PageLayoutDensity) => {\\n setOptionPatch({ layoutDensity: density });\\n },\\n [setOptionPatch],\\n );\\n\\n const handleSetEmbedDefaultToCursor = useCallback(() => {\\n if (!canEditDocument) return;\\n if (!isEditing) {\\n setIsEditing(true);\\n return;\\n }\\n const blockId = editorBridge?.getCursorBlockId?.() ?? null;\\n if (!blockId) {\\n window.alert(\\\"未找到当前光标所在块,请先在编辑器中点击任意块\\\");\\n return;\\n }\\n setOptionPatch({ embedDefaultBlockId: blockId });\\n window.alert(\\\"已设置“嵌入默认位置”\\\");\\n }, [canEditDocument, editorBridge, isEditing, setOptionPatch]);\\n\\n const handleClearEmbedDefault = useCallback(() => {\\n if (readOnly) return;\\n setOptionPatch({ embedDefaultBlockId: null });\\n window.alert(\\\"已清除“嵌入默认位置”\\\");\\n }, [readOnly, setOptionPatch]);\\n\\n useEffect(() => {\\n const onKeyDown = (event: KeyboardEvent) => {\\n const root = pageRootRef.current;\\n if (root) {\\n const target = event.target;\\n if (target && target instanceof Node && !root.contains(target)) {\\n return;\\n }\\n }\\n const ctrlOrMeta = event.ctrlKey || event.metaKey;\\n if (!ctrlOrMeta) return;\\n if (!event.shiftKey) return;\\n const key = String(event.key ?? \\\"\\\").toLowerCase();\\n if (key === \\\"l\\\") {\\n event.preventDefault();\\n toggleOption(\\\"showToc\\\");\\n } else if (key === \\\"c\\\") {\\n event.preventDefault();\\n toggleOption(\\\"protectEditing\\\");\\n }\\n };\\n window.addEventListener(\\\"keydown\\\", onKeyDown);\\n return () => window.removeEventListener(\\\"keydown\\\", onKeyDown);\\n }, [toggleOption]);\\n\\n const buildDocumentUrl = useCallback((id: string): string => {\\n if (typeof window === \\\"undefined\\\" || !window.location) {\\n return `/documents/${id}`;\\n }\\n return `${window.location.origin}/documents/${id}`;\\n }, []);\\n\\n const copyText = useCallback(async (text: string, successMessage: string) => {\\n if (typeof navigator !== \\\"undefined\\\" && navigator.clipboard) {\\n try {\\n await navigator.clipboard.writeText(text);\\n window.alert(successMessage);\\n return;\\n } catch {\\n }\\n }\\n window.prompt(\\\"复制失败,请手动复制内容\\\", text);\\n }, []);\\n\\n const handleCopyPageLink = useCallback(\\n async (includeTitle: boolean) => {\\n const url = buildDocumentUrl(documentId);\\n if (includeTitle) {\\n const text = `${pageTitle || \\\"无标题\\\"}\\\\n${url}`;\\n await copyText(text, \\\"标题 + 链接已复制\\\");\\n return;\\n }\\n await copyText(url, \\\"页面链接已复制\\\");\\n },\\n [buildDocumentUrl, copyText, documentId, pageTitle],\\n );\\n\\n const handleCopyPageReference = useCallback(\\n async (mode: \\\"inline\\\" | \\\"embed\\\") => {\\n const template = mode === \\\"inline\\\" ? `((${documentId}))` : `{{${documentId}}}`;\\n await copyText(template, mode === \\\"inline\\\" ? \\\"行内引用已复制\\\" : \\\"嵌入引用已复制\\\");\\n },\\n [copyText, documentId],\\n );\\n\\n const handleUndo = useCallback(() => {\\n if (!isEditing) {\\n setIsEditing(true);\\n return;\\n }\\n editorBridge?.undo?.();\\n }, [editorBridge, isEditing]);\\n\\n const handleRedo = useCallback(() => {\\n if (!isEditing) {\\n setIsEditing(true);\\n return;\\n }\\n editorBridge?.redo?.();\\n }, [editorBridge, isEditing]);\\n\\n const handleDeletePage = useCallback(async () => {\\n if (readOnly) return;\\n const ok = window.confirm(\\\"确定删除该页面吗?删除后会进入垃圾桶。\\\");\\n if (!ok) return;\\n try {\\n await deleteDocumentCommand({ documentId, workspaceId });\\n } catch (error) {\\n const payload = error instanceof Error ? error.message : null;\\n window.alert(payload ?? \\\"删除失败\\\");\\n return;\\n }\\n router.push(\\\"/\\\");\\n router.refresh();\\n }, [documentId, readOnly, router, workspaceId]);\\n\\n const handleOpenMoveEmbed = useCallback(() => {\\n if (readOnly) return;\\n openMoveEmbedPicker({\\n workspaceId,\\n defaultMode: \\\"move\\\",\\n modes: [\\\"move\\\", \\\"embed\\\"],\\n allowRoot: true,\\n excludeIds: [documentId],\\n onPick: async (mode, targetId) => {\\n if (mode === \\\"move\\\") {\\n try {\\n await moveDocumentCommand({\\n documentId,\\n parentId: targetId ?? null,\\n position: 999999,\\n workspaceId,\\n });\\n } catch (error) {\\n window.alert(error instanceof Error ? error.message : \\\"移动失败\\\");\\n return;\\n }\\n window.alert(\\\"移动成功\\\");\\n router.refresh();\\n return;\\n }\\n\\n if (!targetId) {\\n window.alert(\\\"请选择目标页面\\\");\\n return;\\n }\\n\\n try {\\n await embedDocumentCommand({\\n sourceId: documentId,\\n targetId,\\n });\\n } catch (error) {\\n const message = error instanceof Error ? error.message : \\\"嵌入失败\\\";\\n window.alert(message);\\n return;\\n }\\n\\n window.alert(\\\"已嵌入到目标页面\\\");\\n },\\n });\\n }, [documentId, openMoveEmbedPicker, readOnly, router, workspaceId]);\\n\\n const handleAddToTemplates = useCallback(async () => {\\n if (readOnly) return;\\n const ok = window.confirm(\\\"将该页面添加为模板?\\\");\\n if (!ok) return;\\n const resp = await fetch(\\\"/api/documents/template\\\", {\\n method: \\\"POST\\\",\\n headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\\n body: JSON.stringify({ documentId, isTemplate: true }),\\n });\\n if (!resp.ok) {\\n const payload = await resp.json().catch(() => ({}));\\n window.alert(payload?.error ?? \\\"设置模板失败\\\");\\n return;\\n }\\n window.alert(\\\"已添加为模板\\\");\\n router.refresh();\\n }, [documentId, readOnly, router]);\\n\\n const formattedUpdatedAt = useMemo(() => {\\n if (!updatedAt) return \\\"\\\";\\n const date = new Date(updatedAt);\\n if (Number.isNaN(date.getTime())) return \\\"\\\";\\n return new Intl.DateTimeFormat(\\\"zh-CN\\\", {\\n timeZone: \\\"Asia/Shanghai\\\",\\n year: \\\"numeric\\\",\\n month: \\\"numeric\\\",\\n day: \\\"numeric\\\",\\n hour: \\\"2-digit\\\",\\n minute: \\\"2-digit\\\",\\n second: \\\"2-digit\\\",\\n hour12: false,\\n }).format(date);\\n }, [updatedAt]);\\n\\n const handleExport = useCallback(() => {\\n if (disableDownload) {\\n window.alert(\\\"该页面已禁止下载\\\");\\n return;\\n }\\n const latest = history[0];\\n const exportBlocks = latest?.blocks ?? latestBlocksRef.current;\\n if (!exportBlocks) {\\n window.alert(\\\"暂无可导出的内容\\\");\\n return;\\n }\\n const payload = JSON.stringify(exportBlocks, null, 2);\\n const blob = new Blob([payload], { type: \\\"application/json\\\" });\\n const url = URL.createObjectURL(blob);\\n const anchor = document.createElement(\\\"a\\\");\\n anchor.href = url;\\n anchor.download = `${title ?? \\\"未命名页面\\\"}-${new Date().toISOString()}.json`;\\n anchor.click();\\n URL.revokeObjectURL(url);\\n }, [disableDownload, history, title]);\\n\\n const pageRootClass = cn(\\n \\\"flex h-full overflow-hidden bg-wolai-bg\\\",\\n options.pageFont === \\\"song\\\" && \\\"wolai-page-font-song\\\",\\n options.pageFont === \\\"kai\\\" && \\\"wolai-page-font-kai\\\",\\n options.layoutDensity === \\\"compact\\\" && \\\"wolai-page-density-compact\\\",\\n options.layoutDensity === \\\"spacious\\\" && \\\"wolai-page-density-spacious\\\",\\n options.smallText && \\\"wolai-small-text\\\",\\n options.hideChildPages && \\\"wolai-hide-child-pages\\\",\\n );\\n const pageSubtree = useMemo(() => {\\n const hasServerPageSubtree = Boolean(serverPageSubtreeSnapshot);\\n const titleUnchanged = pageTitle === serverPageSubtreeTitle;\\n const contentUnchanged = content === serverContentSnapshot;\\n\\n if (hasServerPageSubtree && titleUnchanged && contentUnchanged) {\\n return serverPageSubtreeSnapshot;\\n }\\n return null;\\n }, [\\n content,\\n pageTitle,\\n serverContentSnapshot,\\n serverPageSubtreeSnapshot,\\n serverPageSubtreeTitle,\\n ]);\\n const readViewTocEntries = useMemo(\\n () =>\\n (pageSubtree?.outline ?? [])\\n .filter((entry) => typeof entry.anchorBlockId === \\\"string\\\" && entry.anchorBlockId.trim())\\n .map(({ anchorBlockId, level, numbering, title: entryTitle }) => ({\\n id: anchorBlockId as string,\\n level,\\n numbering,\\n title: entryTitle,\\n })),\\n [pageSubtree],\\n );\\n const inspectorCanUseEditorBridge = canEditDocument && isEditing;\\n\\n const jumpToHeading = useCallback((headingId: string) => {\\n const targetRoot = !isEditing ? readViewRootRef.current : pageRootRef.current;\\n const target = targetRoot?.querySelector(`[data-id=\\\"${headingId}\\\"]`);\\n if (target) {\\n target.scrollIntoView({ behavior: \\\"smooth\\\", block: \\\"center\\\" });\\n }\\n }, [isEditing]);\\n\\n const handleSnapshot = useCallback((payload: { blocks: Json; stats: DocumentStats }) => {\\n setContent(payload.blocks);\\n latestBlocksRef.current = payload.blocks;\\n setHistory((prev) => {\\n const now = Date.now();\\n if (prev.length > 0 && now - prev[0].timestamp < 4000) {\\n return prev;\\n }\\n const snapshot: DocumentSnapshot = {\\n id: `${now}-${Math.random().toString(36).slice(2, 8)}`,\\n timestamp: now,\\n blocks: payload.blocks,\\n stats: payload.stats,\\n };\\n return [snapshot, ...prev].slice(0, 15);\\n });\\n }, []);\\n\\n const persistStatsRequest = useCallback((next: DocumentStats) => {\\n void fetch(\\\"/api/documents/stats\\\", {\\n method: \\\"POST\\\",\\n headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\\n body: JSON.stringify({ documentId, workspaceId, stats: next }),\\n }).catch((error) => console.error(error));\\n }, [documentId, workspaceId]);\\n\\n const persistStats = useDebouncedCallback(persistStatsRequest, 1500);\\n\\n const handleStatsChange = useCallback(\\n (nextStats: DocumentStats) => {\\n setStats(nextStats);\\n persistStats(nextStats);\\n },\\n [persistStats],\\n );\\n\\n const handleRestoreSnapshot = useCallback(\\n (snapshot: DocumentSnapshot) => {\\n if (!isEditing || !editorBridge) {\\n pendingRestoreSnapshotRef.current = snapshot;\\n setIsEditing(true);\\n return;\\n }\\n editorBridge.replaceWithSnapshot(snapshot.blocks);\\n setHistoryOpen(false);\\n },\\n [editorBridge, isEditing],\\n );\\n\\n const handleEnterEditMode = useCallback(() => {\\n if (!canEditDocument) return;\\n setIsEditing(true);\\n }, [canEditDocument]);\\n\\n const handleExitEditMode = useCallback(() => {\\n setIsEditing(false);\\n }, []);\\n\\n useEffect(() => {\\n if (!isEditing) return;\\n if (!editorBridge) return;\\n const pendingSnapshot = pendingRestoreSnapshotRef.current;\\n if (!pendingSnapshot) return;\\n pendingRestoreSnapshotRef.current = null;\\n editorBridge.replaceWithSnapshot(pendingSnapshot.blocks);\\n setHistoryOpen(false);\\n }, [editorBridge, isEditing]);\\n return (\\n \\n
\\n
\\n
\\n
\\n
\\n {isEditing && canEditDocument ? (\\n
\\n \\n
\\n ) : (\\n

\\n {pageTitle || \\\"无标题\\\"}\\n

\\n )}\\n {readOnly ? (\\n

该页面为只读共享,无法编辑。

\\n ) : options.protectEditing && isEditing ? (\\n

当前页面已开启编辑保护,关闭后方可修改内容。

\\n ) : null}\\n

最近更新:{formattedUpdatedAt}

\\n
\\n {canEditDocument && (\\n
\\n {isEditing ? (\\n \\n ) : (\\n \\n )}\\n
\\n )}\\n
\\n
\\n
\\n {contentLoading ? (\\n showContentLoadingIndicator ? (\\n
\\n 页面内容加载中...\\n
\\n ) : (\\n
\\n )\\n ) : contentError ? (\\n
\\n
{contentError}
\\n {\\n setContentError(null);\\n setContentLoading(true);\\n setContentReloadKey((prev) => prev + 1);\\n }}\\n >\\n 重试\\n \\n
\\n ) : (\\n
\\n {keepEditorMounted && (\\n
\\n {\\n setContentRevision(meta.revision);\\n setConflictDetectionKey(meta.conflictDetectionKey);\\n }}\\n />\\n
\\n )}\\n {!isEditing && (\\n
\\n \\n \\n
\\n )}\\n
\\n )}\\n \\n
\\n
\\n {showInspector && (\\n setHistoryOpen(true)}\\n onOpenComments={() => openCommentsForPage({ workspaceId, documentId })}\\n onUndo={canEditDocument ? handleUndo : undefined}\\n onRedo={canEditDocument ? handleRedo : undefined}\\n onDeletePage={canEditDocument ? handleDeletePage : undefined}\\n onOpenMoveEmbedPicker={canEditDocument ? handleOpenMoveEmbed : undefined}\\n onCopyPageLink={handleCopyPageLink}\\n onCopyPageReference={handleCopyPageReference}\\n onAddToTemplates={canEditDocument ? handleAddToTemplates : undefined}\\n />\\n )}\\n
\\n \\n \\n \\n latestBlocksRef.current}\\n getLatestPageSubtree={() => pageSubtree}\\n />\\n \\n );\\n}\\n\",\n \"structuredPatch\": [\n {\n \"oldStart\": 1,\n \"oldLines\": 1009,\n \"newStart\": 1,\n \"newLines\": 1009,\n \"lines\": [\n \"-\\\"use client\\\";\",\n \"-\",\n \"-import dynamic from \\\"next/dynamic\\\";\",\n \"-import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent as ReactKeyboardEvent } from \\\"react\\\";\",\n \"-import type { BooleanPageOptionKey, DocumentStats, PageOptionsState, PageFont, PageLayoutDensity } from \\\"@/types/page-options\\\";\",\n \"-import { useEditorBridgeStore } from \\\"@/store/editor-bridge\\\";\",\n \"-import { usePageLayoutStore } from \\\"@/store/page-layout\\\";\",\n \"-import { useCurrentDocumentStore } from \\\"@/store/current-document\\\";\",\n \"-import type { Json } from \\\"@/types/supabase\\\";\",\n \"-import type { DocumentSnapshot } from \\\"@/types/document\\\";\",\n \"-import { useDebouncedCallback } from \\\"@/hooks/use-debounced-callback\\\";\",\n \"-import { ImagePickerProvider } from \\\"@/components/media/image-picker-context\\\";\",\n \"-import { useRouter } from \\\"next/navigation\\\";\",\n \"-import { DocumentAiAgentPanel } from \\\"./DocumentAiAgentPanel\\\";\",\n \"-import { CONTENT_LOADING_DELAY_MS } from \\\"@/lib/constants\\\";\",\n \"-import { useCommentsUiStore } from \\\"@/store/comments-ui\\\";\",\n \"-import { useAppPreferencesStore } from \\\"@/store/app-preferences\\\";\",\n \"-import { useMoveEmbedPickerStore } from \\\"@/store/move-embed-picker\\\";\",\n \"-import { cn } from \\\"@/lib/utils\\\";\",\n \"-import { Button } from \\\"@/components/ui/button\\\";\",\n \"-import { DocumentToc } from \\\"@/components/editor/document-toc\\\";\",\n \"-import { DocumentReadView } from \\\"@/components/editor/document-read-view\\\";\",\n \"-import { extractPageBlocks, type PageSubtreeProjection } from \\\"@/lib/documents/page-subtree\\\";\",\n \"-import {\",\n \"- deleteDocumentCommand,\",\n \"- embedDocumentCommand,\",\n \"- moveDocumentCommand,\",\n \"- renameDocumentCommand,\",\n \"-} from \\\"@/lib/documents/tree-command-client\\\";\",\n \"-import { EditorHost } from \\\"@/components/editor/editor-host\\\";\",\n \"-\",\n \"-const PageOptionsSidebar = dynamic(\",\n \"- () => import(\\\"@/components/editor/page-options-sidebar\\\").then((mod) => mod.PageOptionsSidebar),\",\n \"- { ssr: false },\",\n \"-);\",\n \"-\",\n \"-const PageBacklinksPanel = dynamic(\",\n \"- () => import(\\\"@/components/editor/page-backlinks-panel\\\").then((mod) => mod.PageBacklinksPanel),\",\n \"- {\",\n \"- ssr: false,\",\n \"- loading: () => null,\",\n \"- },\",\n \"-);\",\n \"-\",\n \"-const DocumentHistoryDrawer = dynamic(\",\n \"- () => import(\\\"@/components/editor/document-history-drawer\\\").then((mod) => mod.DocumentHistoryDrawer),\",\n \"- {\",\n \"- ssr: false,\",\n \"- loading: () => null,\",\n \"- },\",\n \"-);\",\n \"-\",\n \"-const MoveEmbedPickerHost = dynamic(\",\n \"- () => import(\\\"@/components/documents/move-embed-picker-host\\\").then((mod) => mod.MoveEmbedPickerHost),\",\n \"- {\",\n \"- ssr: false,\",\n \"- loading: () => null,\",\n \"- },\",\n \"-);\",\n \"-\",\n \"-const DocumentCommentsDrawer = dynamic(\",\n \"- () => import(\\\"@/components/editor/document-comments-drawer\\\").then((mod) => mod.DocumentCommentsDrawer),\",\n \"- {\",\n \"- ssr: false,\",\n \"- loading: () => null,\",\n \"- },\",\n \"-);\",\n \"-\",\n \"-export interface DocumentContentProps {\",\n \"- documentId: string;\",\n \"- workspaceId: string;\",\n \"- title: string | null;\",\n \"- updatedAt: string | null;\",\n \"- initialContent: unknown;\",\n \"- initialContentRevision?: number | null;\",\n \"- initialConflictDetectionKey?: string | null;\",\n \"- initialPageSubtree?: PageSubtreeProjection | null;\",\n \"- initialOptions: PageOptionsState;\",\n \"- initialStats: DocumentStats | null;\",\n \"- openTableId?: string | null;\",\n \"- autoEnterEdit?: boolean;\",\n \"- readOnly?: boolean;\",\n \"- disableDownload?: boolean;\",\n \"- disableCopy?: boolean;\",\n \"-}\",\n \"-\",\n \"-const defaultOptions: PageOptionsState = {\",\n \"- wideLayout: false,\",\n \"- smallText: false,\",\n \"- showHeadingNumbers: true,\",\n \"- showToc: false,\",\n \"- showStructure: false,\",\n \"- protectEditing: false,\",\n \"- showWordCount: true,\",\n \"- collapseBacklinks: false,\",\n \"- pageFont: \\\"default\\\",\",\n \"- layoutDensity: \\\"normal\\\",\",\n \"- hideChildPages: false,\",\n \"- showBlockRefCount: false,\",\n \"- embedDefaultBlockId: null,\",\n \"-};\",\n \"-const defaultStats: DocumentStats = { wordCount: 0, characterCount: 0, blockCount: 0, todoTotal: 0, todoDone: 0 };\",\n \"-const EDITOR_UNMOUNT_GRACE_MS = 1000;\",\n \"-\",\n \"-export function DocumentContent({\",\n \"- documentId,\",\n \"- workspaceId,\",\n \"- title,\",\n \"- updatedAt,\",\n \"- initialContent,\",\n \"- initialContentRevision = null,\",\n \"- initialConflictDetectionKey = null,\",\n \"- initialPageSubtree = null,\",\n \"- initialOptions,\",\n \"- initialStats,\",\n \"- openTableId,\",\n \"- autoEnterEdit = false,\",\n \"- readOnly = false,\",\n \"- disableDownload = false,\",\n \"- disableCopy = false,\",\n \"-}: DocumentContentProps) {\",\n \"- const setCurrentDocument = useCurrentDocumentStore((state) => state.setCurrent);\",\n \"- const clearIfMatch = useCurrentDocumentStore((state) => state.clearIfMatch);\",\n \"- const canEditDocument = !readOnly;\",\n \"- const [options, setOptions] = useState(initialOptions ?? defaultOptions);\",\n \"- const [stats, setStats] = useState(initialStats ?? defaultStats);\",\n \"- const [history, setHistory] = useState([]);\",\n \"- const [historyOpen, setHistoryOpen] = useState(false);\",\n \"- const editorBridge = useEditorBridgeStore((state) => state.bridge);\",\n \"- const router = useRouter();\",\n \"- const showInspector = usePageLayoutStore((state) => state.showInspector);\",\n \"- const openCommentsForPage = useCommentsUiStore((s) => s.openForPage);\",\n \"- const spellCheck = useAppPreferencesStore((s) => s.spellCheck);\",\n \"- const openMoveEmbedPicker = useMoveEmbedPickerStore((s) => s.openPicker);\",\n \"- const [pageTitle, setPageTitle] = useState(title ?? \\\"无标题\\\");\",\n \"- const [content, setContent] = useState(initialContent);\",\n \"- const [serverContentSnapshot, setServerContentSnapshot] = useState(initialContent);\",\n \"- const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState(\",\n \"- initialPageSubtree,\",\n \"- );\",\n \"- const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState(title ?? \\\"无标题\\\");\",\n \"- const [contentRevision, setContentRevision] = useState(initialContentRevision);\",\n \"- const [conflictDetectionKey, setConflictDetectionKey] = useState(initialConflictDetectionKey);\",\n \"- const [contentLoading, setContentLoading] = useState(() => initialContent == null);\",\n \"- const [contentError, setContentError] = useState(null);\",\n \"- const [contentReloadKey, setContentReloadKey] = useState(0);\",\n \"- const [showContentLoadingIndicator, setShowContentLoadingIndicator] = useState(false);\",\n \"- const shouldStartEditing = canEditDocument;\",\n \"- const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\",\n \"- const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\",\n \"- const contentLoadingTimerRef = useRef | null>(null);\",\n \"- const editorUnmountTimerRef = useRef | null>(null);\",\n \"- const pendingOpenTableRef = useRef(null);\",\n \"- const latestBlocksRef = useRef(null);\",\n \"- const pageRootRef = useRef(null);\",\n \"- const readViewRootRef = useRef(null);\",\n \"- const pendingRestoreSnapshotRef = useRef(null);\",\n \"- const lastCopyBlockedAtRef = useRef(0);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setCurrentDocument(documentId, readOnly, disableDownload, disableCopy);\",\n \"- return () => {\",\n \"- clearIfMatch(documentId);\",\n \"- };\",\n \"- }, [clearIfMatch, disableCopy, disableDownload, documentId, readOnly, setCurrentDocument]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- if (!disableCopy) return;\",\n \"-\",\n \"- const toElement = (node: Node | null): Element | null => {\",\n \"- if (!node) return null;\",\n \"- if (node.nodeType === Node.TEXT_NODE) {\",\n \"- return node.parentElement;\",\n \"- }\",\n \"- return node instanceof Element ? node : null;\",\n \"- };\",\n \"-\",\n \"- const isEventInsidePage = () => {\",\n \"- const root = pageRootRef.current;\",\n \"- if (!root) return false;\",\n \"- const selection = typeof window !== \\\"undefined\\\" ? window.getSelection() : null;\",\n \"- const anchor = selection?.anchorNode ?? null;\",\n \"- const focus = selection?.focusNode ?? null;\",\n \"- const anchorEl = toElement(anchor);\",\n \"- const focusEl = toElement(focus);\",\n \"- return Boolean((anchorEl && root.contains(anchorEl)) || (focusEl && root.contains(focusEl)));\",\n \"- };\",\n \"-\",\n \"- const notifyBlocked = () => {\",\n \"- const now = Date.now();\",\n \"- if (now - lastCopyBlockedAtRef.current < 1200) return;\",\n \"- lastCopyBlockedAtRef.current = now;\",\n \"- window.alert(\\\"该页面已禁止复制\\\");\",\n \"- };\",\n \"-\",\n \"- const onCopy = (event: ClipboardEvent) => {\",\n \"- if (!isEventInsidePage()) return;\",\n \"- event.preventDefault();\",\n \"- event.stopPropagation();\",\n \"- notifyBlocked();\",\n \"- };\",\n \"-\",\n \"- const onCut = (event: ClipboardEvent) => {\",\n \"- if (!isEventInsidePage()) return;\",\n \"- event.preventDefault();\",\n \"- event.stopPropagation();\",\n \"- notifyBlocked();\",\n \"- };\",\n \"-\",\n \"- const onKeyDown = (event: KeyboardEvent) => {\",\n \"- if (!isEventInsidePage()) return;\",\n \"- const key = String(event.key ?? \\\"\\\").toLowerCase();\",\n \"- const ctrlOrMeta = event.ctrlKey || event.metaKey;\",\n \"- if (!ctrlOrMeta) return;\",\n \"- if (key === \\\"c\\\" || key === \\\"x\\\" || key === \\\"insert\\\") {\",\n \"- event.preventDefault();\",\n \"- event.stopPropagation();\",\n \"- notifyBlocked();\",\n \"- }\",\n \"- };\",\n \"-\",\n \"- document.addEventListener(\\\"copy\\\", onCopy, true);\",\n \"- document.addEventListener(\\\"cut\\\", onCut, true);\",\n \"- document.addEventListener(\\\"keydown\\\", onKeyDown, true);\",\n \"- return () => {\",\n \"- document.removeEventListener(\\\"copy\\\", onCopy, true);\",\n \"- document.removeEventListener(\\\"cut\\\", onCut, true);\",\n \"- document.removeEventListener(\\\"keydown\\\", onKeyDown, true);\",\n \"- };\",\n \"- }, [disableCopy]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- const tableId = (openTableId ?? \\\"\\\").trim();\",\n \"- if (!tableId) return;\",\n \"- if (!editorBridge?.openTableFullScreen) return;\",\n \"- if (pendingOpenTableRef.current === tableId) return;\",\n \"- pendingOpenTableRef.current = tableId;\",\n \"-\",\n \"- editorBridge.openTableFullScreen(tableId);\",\n \"-\",\n \"- if (typeof window !== \\\"undefined\\\") {\",\n \"- try {\",\n \"- const url = new URL(window.location.href);\",\n \"- url.searchParams.delete(\\\"openTableId\\\");\",\n \"- window.history.replaceState({}, \\\"\\\", url.toString());\",\n \"- } catch {\",\n \"- router.replace(`/documents/${documentId}`);\",\n \"- }\",\n \"- }\",\n \"- }, [documentId, editorBridge, openTableId, router]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setPageTitle(title ?? \\\"无标题\\\");\",\n \"- }, [title]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setServerPageSubtreeTitle(title ?? \\\"无标题\\\");\",\n \"- }, [title]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setServerPageSubtreeSnapshot(initialPageSubtree);\",\n \"- }, [initialPageSubtree]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setOptions(initialOptions ?? defaultOptions);\",\n \"- }, [initialOptions]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setStats(initialStats ?? defaultStats);\",\n \"- }, [initialStats]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setContentRevision(initialContentRevision);\",\n \"- }, [initialContentRevision]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setConflictDetectionKey(initialConflictDetectionKey);\",\n \"- }, [initialConflictDetectionKey]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- setServerContentSnapshot(initialContent);\",\n \"- }, [initialContent]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- const nextBlocks = extractPageBlocks(content);\",\n \"- latestBlocksRef.current = nextBlocks.length > 0 ? (nextBlocks as Json) : null;\",\n \"- }, [content]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- const shouldForceEdit = Boolean((openTableId ?? \\\"\\\").trim()) && canEditDocument;\",\n \"- if (shouldForceEdit) {\",\n \"- setIsEditing(true);\",\n \"- setKeepEditorMounted(true);\",\n \"- }\",\n \"- }, [canEditDocument, openTableId]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- if (isEditing) {\",\n \"- if (editorUnmountTimerRef.current) {\",\n \"- clearTimeout(editorUnmountTimerRef.current);\",\n \"- editorUnmountTimerRef.current = null;\",\n \"- }\",\n \"- setKeepEditorMounted(true);\",\n \"- return;\",\n \"- }\",\n \"- if (editorUnmountTimerRef.current) {\",\n \"- clearTimeout(editorUnmountTimerRef.current);\",\n \"- }\",\n \"- editorUnmountTimerRef.current = setTimeout(() => {\",\n \"- setKeepEditorMounted(false);\",\n \"- editorUnmountTimerRef.current = null;\",\n \"- }, EDITOR_UNMOUNT_GRACE_MS);\",\n \"- return () => {\",\n \"- if (editorUnmountTimerRef.current) {\",\n \"- clearTimeout(editorUnmountTimerRef.current);\",\n \"- editorUnmountTimerRef.current = null;\",\n \"- }\",\n \"- };\",\n \"- }, [isEditing]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- let canceled = false;\",\n \"- const controller = new AbortController();\",\n \"-\",\n \"- const load = async () => {\",\n \"- setContentError(null);\",\n \"- setContentLoading(initialContent == null);\",\n \"- setContent(initialContent);\",\n \"- setShowContentLoadingIndicator(false);\",\n \"-\",\n \"- if (initialContent != null) {\",\n \"- return;\",\n \"- }\",\n \"-\",\n \"- if (contentLoadingTimerRef.current) {\",\n \"- clearTimeout(contentLoadingTimerRef.current);\",\n \"- contentLoadingTimerRef.current = null;\",\n \"- }\",\n \"- contentLoadingTimerRef.current = setTimeout(() => {\",\n \"- if (!canceled) {\",\n \"- setShowContentLoadingIndicator(true);\",\n \"- }\",\n \"- }, CONTENT_LOADING_DELAY_MS);\",\n \"-\",\n \"- try {\",\n \"- const response = await fetch(\",\n \"- `/api/documents/content?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,\",\n \"- {\",\n \"- method: \\\"GET\\\",\",\n \"- credentials: \\\"include\\\",\",\n \"- signal: controller.signal,\",\n \"- },\",\n \"- );\",\n \"- if (!response.ok) {\",\n \"- const payload = await response.json().catch(() => ({}));\",\n \"- throw new Error(payload?.error ?? \\\"加载页面内容失败\\\");\",\n \"- }\",\n \"- const payload = (await response.json()) as {\",\n \"- content?: unknown;\",\n \"- revision?: number | null;\",\n \"- conflictDetectionKey?: string | null;\",\n \"- pageSubtree?: PageSubtreeProjection | null;\",\n \"- };\",\n \"- if (canceled) return;\",\n \"- setContent(payload.content ?? null);\",\n \"- setServerContentSnapshot(payload.content ?? null);\",\n \"- setContentRevision(\",\n \"- typeof payload.revision === \\\"number\\\" && Number.isInteger(payload.revision)\",\n \"- ? payload.revision\",\n \"- : 0,\",\n \"- );\",\n \"- setConflictDetectionKey(\",\n \"- typeof payload.conflictDetectionKey === \\\"string\\\" && payload.conflictDetectionKey.trim()\",\n \"- ? payload.conflictDetectionKey\",\n \"- : `${documentId}:0`,\",\n \"- );\",\n \"- setServerPageSubtreeSnapshot(payload.pageSubtree ?? null);\",\n \"- setServerPageSubtreeTitle(\",\n \"- typeof payload.pageSubtree?.rootNode.metadata.title === \\\"string\\\" &&\",\n \"- payload.pageSubtree.rootNode.metadata.title.trim()\",\n \"- ? payload.pageSubtree.rootNode.metadata.title\",\n \"- : title ?? \\\"无标题\\\",\",\n \"- );\",\n \"- } catch (error) {\",\n \"- if (canceled) return;\",\n \"- if ((error as { name?: string })?.name === \\\"AbortError\\\") return;\",\n \"- setContentError(error instanceof Error ? error.message : \\\"加载页面内容失败\\\");\",\n \"- } finally {\",\n \"- if (!canceled) {\",\n \"- setContentLoading(false);\",\n \"- setShowContentLoadingIndicator(false);\",\n \"- }\",\n \"- if (contentLoadingTimerRef.current) {\",\n \"- clearTimeout(contentLoadingTimerRef.current);\",\n \"- contentLoadingTimerRef.current = null;\",\n \"- }\",\n \"- }\",\n \"- };\",\n \"-\",\n \"- void load();\",\n \"-\",\n \"- return () => {\",\n \"- canceled = true;\",\n \"- controller.abort();\",\n \"- if (contentLoadingTimerRef.current) {\",\n \"- clearTimeout(contentLoadingTimerRef.current);\",\n \"- contentLoadingTimerRef.current = null;\",\n \"- }\",\n \"- };\",\n \"- }, [documentId, initialContent, contentReloadKey, title, workspaceId]);\",\n \"-\",\n \"- const persistTitle = useCallback(\",\n \"- async (nextTitle: string) => {\",\n \"- if (readOnly) return;\",\n \"- const payload = nextTitle.trim() || \\\"无标题\\\";\",\n \"- try {\",\n \"- await renameDocumentCommand({ documentId, workspaceId, title: payload });\",\n \"- } catch (error) {\",\n \"- console.error(\\\"更新页面标题失败\\\", error);\",\n \"- }\",\n \"- },\",\n \"- [documentId, readOnly, workspaceId],\",\n \"- );\",\n \"-\",\n \"- const debouncedPersistTitle = useDebouncedCallback((value: string) => {\",\n \"- void persistTitle(value);\",\n \"- }, 600);\",\n \"-\",\n \"- const handleTitleChange = (event: ChangeEvent) => {\",\n \"- if (!canEditDocument) return;\",\n \"- const value = event.target.value;\",\n \"- setPageTitle(value);\",\n \"- debouncedPersistTitle(value);\",\n \"- };\",\n \"-\",\n \"- const handleTitleBlur = () => {\",\n \"- if (!canEditDocument) return;\",\n \"- void persistTitle(pageTitle);\",\n \"- };\",\n \"-\",\n \"- const handleTitleKeyDown = (event: ReactKeyboardEvent) => {\",\n \"- if (event.key === \\\"Enter\\\") {\",\n \"- event.preventDefault();\",\n \"- event.currentTarget.blur();\",\n \"- }\",\n \"- };\",\n \"-\",\n \"- const persistOptions = useCallback(\",\n \"- async (patch: Partial) => {\",\n \"- if (readOnly) return;\",\n \"- try {\",\n \"- const response = await fetch(\\\"/api/documents/options\\\", {\",\n \"- method: \\\"POST\\\",\",\n \"- headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"- body: JSON.stringify({ documentId, workspaceId, options: patch }),\",\n \"- });\",\n \"- if (!response.ok) {\",\n \"- const payload = await response.json().catch(() => null);\",\n \"- console.error(payload?.error ?? \\\"更新页面选项失败\\\");\",\n \"- }\",\n \"- } catch (error) {\",\n \"- console.error(error);\",\n \"- }\",\n \"- },\",\n \"- [documentId, readOnly, workspaceId],\",\n \"- );\",\n \"-\",\n \"- const toggleOption = useCallback(\",\n \"- (key: BooleanPageOptionKey) => {\",\n \"- if (readOnly) return;\",\n \"- setOptions((prev) => {\",\n \"- const nextValue = !prev[key];\",\n \"- const next = { ...prev, [key]: nextValue };\",\n \"- void persistOptions({ [key]: nextValue } as Partial);\",\n \"- return next;\",\n \"- });\",\n \"- },\",\n \"- [persistOptions, readOnly],\",\n \"- );\",\n \"-\",\n \"- const setOptionPatch = useCallback(\",\n \"- (patch: Partial) => {\",\n \"- if (readOnly) return;\",\n \"- setOptions((prev) => {\",\n \"- const next = { ...prev, ...patch };\",\n \"- void persistOptions(patch);\",\n \"- return next;\",\n \"- });\",\n \"- },\",\n \"- [persistOptions, readOnly],\",\n \"- );\",\n \"-\",\n \"- const closeToc = useCallback(() => {\",\n \"- if (readOnly) return;\",\n \"- setOptions((prev) => {\",\n \"- if (!prev.showToc) return prev;\",\n \"- const next = { ...prev, showToc: false };\",\n \"- void persistOptions({ showToc: false });\",\n \"- return next;\",\n \"- });\",\n \"- }, [persistOptions, readOnly]);\",\n \"-\",\n \"- const handleSetPageFont = useCallback(\",\n \"- (font: PageFont) => {\",\n \"- setOptionPatch({ pageFont: font });\",\n \"- },\",\n \"- [setOptionPatch],\",\n \"- );\",\n \"-\",\n \"- const handleSetLayoutDensity = useCallback(\",\n \"- (density: PageLayoutDensity) => {\",\n \"- setOptionPatch({ layoutDensity: density });\",\n \"- },\",\n \"- [setOptionPatch],\",\n \"- );\",\n \"-\",\n \"- const handleSetEmbedDefaultToCursor = useCallback(() => {\",\n \"- if (!canEditDocument) return;\",\n \"- if (!isEditing) {\",\n \"- setIsEditing(true);\",\n \"- return;\",\n \"- }\",\n \"- const blockId = editorBridge?.getCursorBlockId?.() ?? null;\",\n \"- if (!blockId) {\",\n \"- window.alert(\\\"未找到当前光标所在块,请先在编辑器中点击任意块\\\");\",\n \"- return;\",\n \"- }\",\n \"- setOptionPatch({ embedDefaultBlockId: blockId });\",\n \"- window.alert(\\\"已设置“嵌入默认位置”\\\");\",\n \"- }, [canEditDocument, editorBridge, isEditing, setOptionPatch]);\",\n \"-\",\n \"- const handleClearEmbedDefault = useCallback(() => {\",\n \"- if (readOnly) return;\",\n \"- setOptionPatch({ embedDefaultBlockId: null });\",\n \"- window.alert(\\\"已清除“嵌入默认位置”\\\");\",\n \"- }, [readOnly, setOptionPatch]);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- const onKeyDown = (event: KeyboardEvent) => {\",\n \"- const root = pageRootRef.current;\",\n \"- if (root) {\",\n \"- const target = event.target;\",\n \"- if (target && target instanceof Node && !root.contains(target)) {\",\n \"- return;\",\n \"- }\",\n \"- }\",\n \"- const ctrlOrMeta = event.ctrlKey || event.metaKey;\",\n \"- if (!ctrlOrMeta) return;\",\n \"- if (!event.shiftKey) return;\",\n \"- const key = String(event.key ?? \\\"\\\").toLowerCase();\",\n \"- if (key === \\\"l\\\") {\",\n \"- event.preventDefault();\",\n \"- toggleOption(\\\"showToc\\\");\",\n \"- } else if (key === \\\"c\\\") {\",\n \"- event.preventDefault();\",\n \"- toggleOption(\\\"protectEditing\\\");\",\n \"- }\",\n \"- };\",\n \"- window.addEventListener(\\\"keydown\\\", onKeyDown);\",\n \"- return () => window.removeEventListener(\\\"keydown\\\", onKeyDown);\",\n \"- }, [toggleOption]);\",\n \"-\",\n \"- const buildDocumentUrl = useCallback((id: string): string => {\",\n \"- if (typeof window === \\\"undefined\\\" || !window.location) {\",\n \"- return `/documents/${id}`;\",\n \"- }\",\n \"- return `${window.location.origin}/documents/${id}`;\",\n \"- }, []);\",\n \"-\",\n \"- const copyText = useCallback(async (text: string, successMessage: string) => {\",\n \"- if (typeof navigator !== \\\"undefined\\\" && navigator.clipboard) {\",\n \"- try {\",\n \"- await navigator.clipboard.writeText(text);\",\n \"- window.alert(successMessage);\",\n \"- return;\",\n \"- } catch {\",\n \"- }\",\n \"- }\",\n \"- window.prompt(\\\"复制失败,请手动复制内容\\\", text);\",\n \"- }, []);\",\n \"-\",\n \"- const handleCopyPageLink = useCallback(\",\n \"- async (includeTitle: boolean) => {\",\n \"- const url = buildDocumentUrl(documentId);\",\n \"- if (includeTitle) {\",\n \"- const text = `${pageTitle || \\\"无标题\\\"}\\\\n${url}`;\",\n \"- await copyText(text, \\\"标题 + 链接已复制\\\");\",\n \"- return;\",\n \"- }\",\n \"- await copyText(url, \\\"页面链接已复制\\\");\",\n \"- },\",\n \"- [buildDocumentUrl, copyText, documentId, pageTitle],\",\n \"- );\",\n \"-\",\n \"- const handleCopyPageReference = useCallback(\",\n \"- async (mode: \\\"inline\\\" | \\\"embed\\\") => {\",\n \"- const template = mode === \\\"inline\\\" ? `((${documentId}))` : `{{${documentId}}}`;\",\n \"- await copyText(template, mode === \\\"inline\\\" ? \\\"行内引用已复制\\\" : \\\"嵌入引用已复制\\\");\",\n \"- },\",\n \"- [copyText, documentId],\",\n \"- );\",\n \"-\",\n \"- const handleUndo = useCallback(() => {\",\n \"- if (!isEditing) {\",\n \"- setIsEditing(true);\",\n \"- return;\",\n \"- }\",\n \"- editorBridge?.undo?.();\",\n \"- }, [editorBridge, isEditing]);\",\n \"-\",\n \"- const handleRedo = useCallback(() => {\",\n \"- if (!isEditing) {\",\n \"- setIsEditing(true);\",\n \"- return;\",\n \"- }\",\n \"- editorBridge?.redo?.();\",\n \"- }, [editorBridge, isEditing]);\",\n \"-\",\n \"- const handleDeletePage = useCallback(async () => {\",\n \"- if (readOnly) return;\",\n \"- const ok = window.confirm(\\\"确定删除该页面吗?删除后会进入垃圾桶。\\\");\",\n \"- if (!ok) return;\",\n \"- try {\",\n \"- await deleteDocumentCommand({ documentId, workspaceId });\",\n \"- } catch (error) {\",\n \"- const payload = error instanceof Error ? error.message : null;\",\n \"- window.alert(payload ?? \\\"删除失败\\\");\",\n \"- return;\",\n \"- }\",\n \"- router.push(\\\"/\\\");\",\n \"- router.refresh();\",\n \"- }, [documentId, readOnly, router, workspaceId]);\",\n \"-\",\n \"- const handleOpenMoveEmbed = useCallback(() => {\",\n \"- if (readOnly) return;\",\n \"- openMoveEmbedPicker({\",\n \"- workspaceId,\",\n \"- defaultMode: \\\"move\\\",\",\n \"- modes: [\\\"move\\\", \\\"embed\\\"],\",\n \"- allowRoot: true,\",\n \"- excludeIds: [documentId],\",\n \"- onPick: async (mode, targetId) => {\",\n \"- if (mode === \\\"move\\\") {\",\n \"- try {\",\n \"- await moveDocumentCommand({\",\n \"- documentId,\",\n \"- parentId: targetId ?? null,\",\n \"- position: 999999,\",\n \"- workspaceId,\",\n \"- });\",\n \"- } catch (error) {\",\n \"- window.alert(error instanceof Error ? error.message : \\\"移动失败\\\");\",\n \"- return;\",\n \"- }\",\n \"- window.alert(\\\"移动成功\\\");\",\n \"- router.refresh();\",\n \"- return;\",\n \"- }\",\n \"-\",\n \"- if (!targetId) {\",\n \"- window.alert(\\\"请选择目标页面\\\");\",\n \"- return;\",\n \"- }\",\n \"-\",\n \"- try {\",\n \"- await embedDocumentCommand({\",\n \"- sourceId: documentId,\",\n \"- targetId,\",\n \"- });\",\n \"- } catch (error) {\",\n \"- const message = error instanceof Error ? error.message : \\\"嵌入失败\\\";\",\n \"- window.alert(message);\",\n \"- return;\",\n \"- }\",\n \"-\",\n \"- window.alert(\\\"已嵌入到目标页面\\\");\",\n \"- },\",\n \"- });\",\n \"- }, [documentId, openMoveEmbedPicker, readOnly, router, workspaceId]);\",\n \"-\",\n \"- const handleAddToTemplates = useCallback(async () => {\",\n \"- if (readOnly) return;\",\n \"- const ok = window.confirm(\\\"将该页面添加为模板?\\\");\",\n \"- if (!ok) return;\",\n \"- const resp = await fetch(\\\"/api/documents/template\\\", {\",\n \"- method: \\\"POST\\\",\",\n \"- headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"- body: JSON.stringify({ documentId, isTemplate: true }),\",\n \"- });\",\n \"- if (!resp.ok) {\",\n \"- const payload = await resp.json().catch(() => ({}));\",\n \"- window.alert(payload?.error ?? \\\"设置模板失败\\\");\",\n \"- return;\",\n \"- }\",\n \"- window.alert(\\\"已添加为模板\\\");\",\n \"- router.refresh();\",\n \"- }, [documentId, readOnly, router]);\",\n \"-\",\n \"- const formattedUpdatedAt = useMemo(() => {\",\n \"- if (!updatedAt) return \\\"\\\";\",\n \"- const date = new Date(updatedAt);\",\n \"- if (Number.isNaN(date.getTime())) return \\\"\\\";\",\n \"- return new Intl.DateTimeFormat(\\\"zh-CN\\\", {\",\n \"- timeZone: \\\"Asia/Shanghai\\\",\",\n \"- year: \\\"numeric\\\",\",\n \"- month: \\\"numeric\\\",\",\n \"- day: \\\"numeric\\\",\",\n \"- hour: \\\"2-digit\\\",\",\n \"- minute: \\\"2-digit\\\",\",\n \"- second: \\\"2-digit\\\",\",\n \"- hour12: false,\",\n \"- }).format(date);\",\n \"- }, [updatedAt]);\",\n \"-\",\n \"- const handleExport = useCallback(() => {\",\n \"- if (disableDownload) {\",\n \"- window.alert(\\\"该页面已禁止下载\\\");\",\n \"- return;\",\n \"- }\",\n \"- const latest = history[0];\",\n \"- const exportBlocks = latest?.blocks ?? latestBlocksRef.current;\",\n \"- if (!exportBlocks) {\",\n \"- window.alert(\\\"暂无可导出的内容\\\");\",\n \"- return;\",\n \"- }\",\n \"- const payload = JSON.stringify(exportBlocks, null, 2);\",\n \"- const blob = new Blob([payload], { type: \\\"application/json\\\" });\",\n \"- const url = URL.createObjectURL(blob);\",\n \"- const anchor = document.createElement(\\\"a\\\");\",\n \"- anchor.href = url;\",\n \"- anchor.download = `${title ?? \\\"未命名页面\\\"}-${new Date().toISOString()}.json`;\",\n \"- anchor.click();\",\n \"- URL.revokeObjectURL(url);\",\n \"- }, [disableDownload, history, title]);\",\n \"-\",\n \"- const pageRootClass = cn(\",\n \"- \\\"flex h-full overflow-hidden bg-wolai-bg\\\",\",\n \"- options.pageFont === \\\"song\\\" && \\\"wolai-page-font-song\\\",\",\n \"- options.pageFont === \\\"kai\\\" && \\\"wolai-page-font-kai\\\",\",\n \"- options.layoutDensity === \\\"compact\\\" && \\\"wolai-page-density-compact\\\",\",\n \"- options.layoutDensity === \\\"spacious\\\" && \\\"wolai-page-density-spacious\\\",\",\n \"- options.smallText && \\\"wolai-small-text\\\",\",\n \"- options.hideChildPages && \\\"wolai-hide-child-pages\\\",\",\n \"- );\",\n \"- const pageSubtree = useMemo(() => {\",\n \"- const hasServerPageSubtree = Boolean(serverPageSubtreeSnapshot);\",\n \"- const titleUnchanged = pageTitle === serverPageSubtreeTitle;\",\n \"- const contentUnchanged = content === serverContentSnapshot;\",\n \"-\",\n \"- if (hasServerPageSubtree && titleUnchanged && contentUnchanged) {\",\n \"- return serverPageSubtreeSnapshot;\",\n \"- }\",\n \"- return null;\",\n \"- }, [\",\n \"- content,\",\n \"- pageTitle,\",\n \"- serverContentSnapshot,\",\n \"- serverPageSubtreeSnapshot,\",\n \"- serverPageSubtreeTitle,\",\n \"- ]);\",\n \"- const readViewTocEntries = useMemo(\",\n \"- () =>\",\n \"- (pageSubtree?.outline ?? [])\",\n \"- .filter((entry) => typeof entry.anchorBlockId === \\\"string\\\" && entry.anchorBlockId.trim())\",\n \"- .map(({ anchorBlockId, level, numbering, title: entryTitle }) => ({\",\n \"- id: anchorBlockId as string,\",\n \"- level,\",\n \"- numbering,\",\n \"- title: entryTitle,\",\n \"- })),\",\n \"- [pageSubtree],\",\n \"- );\",\n \"- const inspectorCanUseEditorBridge = canEditDocument && isEditing;\",\n \"-\",\n \"- const jumpToHeading = useCallback((headingId: string) => {\",\n \"- const targetRoot = !isEditing ? readViewRootRef.current : pageRootRef.current;\",\n \"- const target = targetRoot?.querySelector(`[data-id=\\\"${headingId}\\\"]`);\",\n \"- if (target) {\",\n \"- target.scrollIntoView({ behavior: \\\"smooth\\\", block: \\\"center\\\" });\",\n \"- }\",\n \"- }, [isEditing]);\",\n \"-\",\n \"- const handleSnapshot = useCallback((payload: { blocks: Json; stats: DocumentStats }) => {\",\n \"- setContent(payload.blocks);\",\n \"- latestBlocksRef.current = payload.blocks;\",\n \"- setHistory((prev) => {\",\n \"- const now = Date.now();\",\n \"- if (prev.length > 0 && now - prev[0].timestamp < 4000) {\",\n \"- return prev;\",\n \"- }\",\n \"- const snapshot: DocumentSnapshot = {\",\n \"- id: `${now}-${Math.random().toString(36).slice(2, 8)}`,\",\n \"- timestamp: now,\",\n \"- blocks: payload.blocks,\",\n \"- stats: payload.stats,\",\n \"- };\",\n \"- return [snapshot, ...prev].slice(0, 15);\",\n \"- });\",\n \"- }, []);\",\n \"-\",\n \"- const persistStatsRequest = useCallback((next: DocumentStats) => {\",\n \"- void fetch(\\\"/api/documents/stats\\\", {\",\n \"- method: \\\"POST\\\",\",\n \"- headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"- body: JSON.stringify({ documentId, workspaceId, stats: next }),\",\n \"- }).catch((error) => console.error(error));\",\n \"- }, [documentId, workspaceId]);\",\n \"-\",\n \"- const persistStats = useDebouncedCallback(persistStatsRequest, 1500);\",\n \"-\",\n \"- const handleStatsChange = useCallback(\",\n \"- (nextStats: DocumentStats) => {\",\n \"- setStats(nextStats);\",\n \"- persistStats(nextStats);\",\n \"- },\",\n \"- [persistStats],\",\n \"- );\",\n \"-\",\n \"- const handleRestoreSnapshot = useCallback(\",\n \"- (snapshot: DocumentSnapshot) => {\",\n \"- if (!isEditing || !editorBridge) {\",\n \"- pendingRestoreSnapshotRef.current = snapshot;\",\n \"- setIsEditing(true);\",\n \"- return;\",\n \"- }\",\n \"- editorBridge.replaceWithSnapshot(snapshot.blocks);\",\n \"- setHistoryOpen(false);\",\n \"- },\",\n \"- [editorBridge, isEditing],\",\n \"- );\",\n \"-\",\n \"- const handleEnterEditMode = useCallback(() => {\",\n \"- if (!canEditDocument) return;\",\n \"- setIsEditing(true);\",\n \"- }, [canEditDocument]);\",\n \"-\",\n \"- const handleExitEditMode = useCallback(() => {\",\n \"- setIsEditing(false);\",\n \"- }, []);\",\n \"-\",\n \"- useEffect(() => {\",\n \"- if (!isEditing) return;\",\n \"- if (!editorBridge) return;\",\n \"- const pendingSnapshot = pendingRestoreSnapshotRef.current;\",\n \"- if (!pendingSnapshot) return;\",\n \"- pendingRestoreSnapshotRef.current = null;\",\n \"- editorBridge.replaceWithSnapshot(pendingSnapshot.blocks);\",\n \"- setHistoryOpen(false);\",\n \"- }, [editorBridge, isEditing]);\",\n \"- return (\",\n \"- \",\n \"-
\",\n \"-
\",\n \"-
\",\n \"-
\",\n \"-
\",\n \"- {isEditing && canEditDocument ? (\",\n \"-
\",\n \"- \",\n \"-
\",\n \"- ) : (\",\n \"-

\",\n \"- {pageTitle || \\\"无标题\\\"}\",\n \"-

\",\n \"- )}\",\n \"- {readOnly ? (\",\n \"-

该页面为只读共享,无法编辑。

\",\n \"- ) : options.protectEditing && isEditing ? (\",\n \"-

当前页面已开启编辑保护,关闭后方可修改内容。

\",\n \"- ) : null}\",\n \"-

最近更新:{formattedUpdatedAt}

\",\n \"-
\",\n \"- {canEditDocument && (\",\n \"-
\",\n \"- {isEditing ? (\",\n \"- \",\n \"- ) : (\",\n \"- \",\n \"- )}\",\n \"-
\",\n \"- )}\",\n \"-
\",\n \"-
\",\n \"-
\",\n \"- {contentLoading ? (\",\n \"- showContentLoadingIndicator ? (\",\n \"-
\",\n \"- 页面内容加载中...\",\n \"-
\",\n \"- ) : (\",\n \"-
\",\n \"- )\",\n \"- ) : contentError ? (\",\n \"-
\",\n \"-
{contentError}
\",\n \"- {\",\n \"- setContentError(null);\",\n \"- setContentLoading(true);\",\n \"- setContentReloadKey((prev) => prev + 1);\",\n \"- }}\",\n \"- >\",\n \"- 重试\",\n \"- \",\n \"-
\",\n \"- ) : (\",\n \"-
\",\n \"- {keepEditorMounted && (\",\n \"-
\",\n \"- {\",\n \"- setContentRevision(meta.revision);\",\n \"- setConflictDetectionKey(meta.conflictDetectionKey);\",\n \"- }}\",\n \"- />\",\n \"-
\",\n \"- )}\",\n \"- {!isEditing && (\",\n \"-
\",\n \"- \",\n \"- \",\n \"-
\",\n \"- )}\",\n \"-
\",\n \"- )}\",\n \"- \",\n \"-
\",\n \"-
\",\n \"- {showInspector && (\",\n \"- setHistoryOpen(true)}\",\n \"- onOpenComments={() => openCommentsForPage({ workspaceId, documentId })}\",\n \"- onUndo={canEditDocument ? handleUndo : undefined}\",\n \"- onRedo={canEditDocument ? handleRedo : undefined}\",\n \"- onDeletePage={canEditDocument ? handleDeletePage : undefined}\",\n \"- onOpenMoveEmbedPicker={canEditDocument ? handleOpenMoveEmbed : undefined}\",\n \"- onCopyPageLink={handleCopyPageLink}\",\n \"- onCopyPageReference={handleCopyPageReference}\",\n \"- onAddToTemplates={canEditDocument ? handleAddToTemplates : undefined}\",\n \"- />\",\n \"- )}\",\n \"-
\",\n \"- \",\n \"- \",\n \"- \",\n \"- latestBlocksRef.current}\",\n \"- getLatestPageSubtree={() => pageSubtree}\",\n \"- />\",\n \"- \",\n \"- );\",\n \"-}\",\n \"+\\\"use client\\\";\",\n \"+\",\n \"+import dynamic from \\\"next/dynamic\\\";\",\n \"+import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent as ReactKeyboardEvent } from \\\"react\\\";\",\n \"+import type { BooleanPageOptionKey, DocumentStats, PageOptionsState, PageFont, PageLayoutDensity } from \\\"@/types/page-options\\\";\",\n \"+import { useEditorBridgeStore } from \\\"@/store/editor-bridge\\\";\",\n \"+import { usePageLayoutStore } from \\\"@/store/page-layout\\\";\",\n \"+import { useCurrentDocumentStore } from \\\"@/store/current-document\\\";\",\n \"+import type { Json } from \\\"@/types/supabase\\\";\",\n \"+import type { DocumentSnapshot } from \\\"@/types/document\\\";\",\n \"+import { useDebouncedCallback } from \\\"@/hooks/use-debounced-callback\\\";\",\n \"+import { ImagePickerProvider } from \\\"@/components/media/image-picker-context\\\";\",\n \"+import { useRouter } from \\\"next/navigation\\\";\",\n \"+import { DocumentAiAgentPanel } from \\\"./DocumentAiAgentPanel\\\";\",\n \"+import { CONTENT_LOADING_DELAY_MS } from \\\"@/lib/constants\\\";\",\n \"+import { useCommentsUiStore } from \\\"@/store/comments-ui\\\";\",\n \"+import { useAppPreferencesStore } from \\\"@/store/app-preferences\\\";\",\n \"+import { useMoveEmbedPickerStore } from \\\"@/store/move-embed-picker\\\";\",\n \"+import { cn } from \\\"@/lib/utils\\\";\",\n \"+import { Button } from \\\"@/components/ui/button\\\";\",\n \"+import { DocumentToc } from \\\"@/components/editor/document-toc\\\";\",\n \"+import { DocumentReadView } from \\\"@/components/editor/document-read-view\\\";\",\n \"+import { extractPageBlocks, type PageSubtreeProjection } from \\\"@/lib/documents/page-subtree\\\";\",\n \"+import {\",\n \"+ deleteDocumentCommand,\",\n \"+ embedDocumentCommand,\",\n \"+ moveDocumentCommand,\",\n \"+ renameDocumentCommand,\",\n \"+} from \\\"@/lib/documents/tree-command-client\\\";\",\n \"+import { EditorHost } from \\\"@/components/editor/editor-host\\\";\",\n \"+\",\n \"+const PageOptionsSidebar = dynamic(\",\n \"+ () => import(\\\"@/components/editor/page-options-sidebar\\\").then((mod) => mod.PageOptionsSidebar),\",\n \"+ { ssr: false },\",\n \"+);\",\n \"+\",\n \"+const PageBacklinksPanel = dynamic(\",\n \"+ () => import(\\\"@/components/editor/page-backlinks-panel\\\").then((mod) => mod.PageBacklinksPanel),\",\n \"+ {\",\n \"+ ssr: false,\",\n \"+ loading: () => null,\",\n \"+ },\",\n \"+);\",\n \"+\",\n \"+const DocumentHistoryDrawer = dynamic(\",\n \"+ () => import(\\\"@/components/editor/document-history-drawer\\\").then((mod) => mod.DocumentHistoryDrawer),\",\n \"+ {\",\n \"+ ssr: false,\",\n \"+ loading: () => null,\",\n \"+ },\",\n \"+);\",\n \"+\",\n \"+const MoveEmbedPickerHost = dynamic(\",\n \"+ () => import(\\\"@/components/documents/move-embed-picker-host\\\").then((mod) => mod.MoveEmbedPickerHost),\",\n \"+ {\",\n \"+ ssr: false,\",\n \"+ loading: () => null,\",\n \"+ },\",\n \"+);\",\n \"+\",\n \"+const DocumentCommentsDrawer = dynamic(\",\n \"+ () => import(\\\"@/components/editor/document-comments-drawer\\\").then((mod) => mod.DocumentCommentsDrawer),\",\n \"+ {\",\n \"+ ssr: false,\",\n \"+ loading: () => null,\",\n \"+ },\",\n \"+);\",\n \"+\",\n \"+export interface DocumentContentProps {\",\n \"+ documentId: string;\",\n \"+ workspaceId: string;\",\n \"+ title: string | null;\",\n \"+ updatedAt: string | null;\",\n \"+ initialContent: unknown;\",\n \"+ initialContentRevision?: number | null;\",\n \"+ initialConflictDetectionKey?: string | null;\",\n \"+ initialPageSubtree?: PageSubtreeProjection | null;\",\n \"+ initialOptions: PageOptionsState;\",\n \"+ initialStats: DocumentStats | null;\",\n \"+ openTableId?: string | null;\",\n \"+ autoEnterEdit?: boolean;\",\n \"+ readOnly?: boolean;\",\n \"+ disableDownload?: boolean;\",\n \"+ disableCopy?: boolean;\",\n \"+}\",\n \"+\",\n \"+const defaultOptions: PageOptionsState = {\",\n \"+ wideLayout: false,\",\n \"+ smallText: false,\",\n \"+ showHeadingNumbers: true,\",\n \"+ showToc: false,\",\n \"+ showStructure: false,\",\n \"+ protectEditing: false,\",\n \"+ showWordCount: true,\",\n \"+ collapseBacklinks: false,\",\n \"+ pageFont: \\\"default\\\",\",\n \"+ layoutDensity: \\\"normal\\\",\",\n \"+ hideChildPages: false,\",\n \"+ showBlockRefCount: false,\",\n \"+ embedDefaultBlockId: null,\",\n \"+};\",\n \"+const defaultStats: DocumentStats = { wordCount: 0, characterCount: 0, blockCount: 0, todoTotal: 0, todoDone: 0 };\",\n \"+const EDITOR_UNMOUNT_GRACE_MS = 1000;\",\n \"+\",\n \"+export function DocumentContent({\",\n \"+ documentId,\",\n \"+ workspaceId,\",\n \"+ title,\",\n \"+ updatedAt,\",\n \"+ initialContent,\",\n \"+ initialContentRevision = null,\",\n \"+ initialConflictDetectionKey = null,\",\n \"+ initialPageSubtree = null,\",\n \"+ initialOptions,\",\n \"+ initialStats,\",\n \"+ openTableId,\",\n \"+ autoEnterEdit = false,\",\n \"+ readOnly = false,\",\n \"+ disableDownload = false,\",\n \"+ disableCopy = false,\",\n \"+}: DocumentContentProps) {\",\n \"+ const setCurrentDocument = useCurrentDocumentStore((state) => state.setCurrent);\",\n \"+ const clearIfMatch = useCurrentDocumentStore((state) => state.clearIfMatch);\",\n \"+ const canEditDocument = !readOnly;\",\n \"+ const [options, setOptions] = useState(initialOptions ?? defaultOptions);\",\n \"+ const [stats, setStats] = useState(initialStats ?? defaultStats);\",\n \"+ const [history, setHistory] = useState([]);\",\n \"+ const [historyOpen, setHistoryOpen] = useState(false);\",\n \"+ const editorBridge = useEditorBridgeStore((state) => state.bridge);\",\n \"+ const router = useRouter();\",\n \"+ const showInspector = usePageLayoutStore((state) => state.showInspector);\",\n \"+ const openCommentsForPage = useCommentsUiStore((s) => s.openForPage);\",\n \"+ const spellCheck = useAppPreferencesStore((s) => s.spellCheck);\",\n \"+ const openMoveEmbedPicker = useMoveEmbedPickerStore((s) => s.openPicker);\",\n \"+ const [pageTitle, setPageTitle] = useState(title ?? \\\"无标题\\\");\",\n \"+ const [content, setContent] = useState(initialContent);\",\n \"+ const [serverContentSnapshot, setServerContentSnapshot] = useState(initialContent);\",\n \"+ const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState(\",\n \"+ initialPageSubtree,\",\n \"+ );\",\n \"+ const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState(title ?? \\\"无标题\\\");\",\n \"+ const [contentRevision, setContentRevision] = useState(initialContentRevision);\",\n \"+ const [conflictDetectionKey, setConflictDetectionKey] = useState(initialConflictDetectionKey);\",\n \"+ const [contentLoading, setContentLoading] = useState(() => initialContent == null);\",\n \"+ const [contentError, setContentError] = useState(null);\",\n \"+ const [contentReloadKey, setContentReloadKey] = useState(0);\",\n \"+ const [showContentLoadingIndicator, setShowContentLoadingIndicator] = useState(false);\",\n \"+ const shouldStartEditing = canEditDocument;\",\n \"+ const [isEditing, setIsEditing] = useState(() => shouldStartEditing);\",\n \"+ const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);\",\n \"+ const contentLoadingTimerRef = useRef | null>(null);\",\n \"+ const editorUnmountTimerRef = useRef | null>(null);\",\n \"+ const pendingOpenTableRef = useRef(null);\",\n \"+ const latestBlocksRef = useRef(null);\",\n \"+ const pageRootRef = useRef(null);\",\n \"+ const readViewRootRef = useRef(null);\",\n \"+ const pendingRestoreSnapshotRef = useRef(null);\",\n \"+ const lastCopyBlockedAtRef = useRef(0);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setCurrentDocument(documentId, readOnly, disableDownload, disableCopy);\",\n \"+ return () => {\",\n \"+ clearIfMatch(documentId);\",\n \"+ };\",\n \"+ }, [clearIfMatch, disableCopy, disableDownload, documentId, readOnly, setCurrentDocument]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ if (!disableCopy) return;\",\n \"+\",\n \"+ const toElement = (node: Node | null): Element | null => {\",\n \"+ if (!node) return null;\",\n \"+ if (node.nodeType === Node.TEXT_NODE) {\",\n \"+ return node.parentElement;\",\n \"+ }\",\n \"+ return node instanceof Element ? node : null;\",\n \"+ };\",\n \"+\",\n \"+ const isEventInsidePage = () => {\",\n \"+ const root = pageRootRef.current;\",\n \"+ if (!root) return false;\",\n \"+ const selection = typeof window !== \\\"undefined\\\" ? window.getSelection() : null;\",\n \"+ const anchor = selection?.anchorNode ?? null;\",\n \"+ const focus = selection?.focusNode ?? null;\",\n \"+ const anchorEl = toElement(anchor);\",\n \"+ const focusEl = toElement(focus);\",\n \"+ return Boolean((anchorEl && root.contains(anchorEl)) || (focusEl && root.contains(focusEl)));\",\n \"+ };\",\n \"+\",\n \"+ const notifyBlocked = () => {\",\n \"+ const now = Date.now();\",\n \"+ if (now - lastCopyBlockedAtRef.current < 1200) return;\",\n \"+ lastCopyBlockedAtRef.current = now;\",\n \"+ window.alert(\\\"该页面已禁止复制\\\");\",\n \"+ };\",\n \"+\",\n \"+ const onCopy = (event: ClipboardEvent) => {\",\n \"+ if (!isEventInsidePage()) return;\",\n \"+ event.preventDefault();\",\n \"+ event.stopPropagation();\",\n \"+ notifyBlocked();\",\n \"+ };\",\n \"+\",\n \"+ const onCut = (event: ClipboardEvent) => {\",\n \"+ if (!isEventInsidePage()) return;\",\n \"+ event.preventDefault();\",\n \"+ event.stopPropagation();\",\n \"+ notifyBlocked();\",\n \"+ };\",\n \"+\",\n \"+ const onKeyDown = (event: KeyboardEvent) => {\",\n \"+ if (!isEventInsidePage()) return;\",\n \"+ const key = String(event.key ?? \\\"\\\").toLowerCase();\",\n \"+ const ctrlOrMeta = event.ctrlKey || event.metaKey;\",\n \"+ if (!ctrlOrMeta) return;\",\n \"+ if (key === \\\"c\\\" || key === \\\"x\\\" || key === \\\"insert\\\") {\",\n \"+ event.preventDefault();\",\n \"+ event.stopPropagation();\",\n \"+ notifyBlocked();\",\n \"+ }\",\n \"+ };\",\n \"+\",\n \"+ document.addEventListener(\\\"copy\\\", onCopy, true);\",\n \"+ document.addEventListener(\\\"cut\\\", onCut, true);\",\n \"+ document.addEventListener(\\\"keydown\\\", onKeyDown, true);\",\n \"+ return () => {\",\n \"+ document.removeEventListener(\\\"copy\\\", onCopy, true);\",\n \"+ document.removeEventListener(\\\"cut\\\", onCut, true);\",\n \"+ document.removeEventListener(\\\"keydown\\\", onKeyDown, true);\",\n \"+ };\",\n \"+ }, [disableCopy]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ const tableId = (openTableId ?? \\\"\\\").trim();\",\n \"+ if (!tableId) return;\",\n \"+ if (!editorBridge?.openTableFullScreen) return;\",\n \"+ if (pendingOpenTableRef.current === tableId) return;\",\n \"+ pendingOpenTableRef.current = tableId;\",\n \"+\",\n \"+ editorBridge.openTableFullScreen(tableId);\",\n \"+\",\n \"+ if (typeof window !== \\\"undefined\\\") {\",\n \"+ try {\",\n \"+ const url = new URL(window.location.href);\",\n \"+ url.searchParams.delete(\\\"openTableId\\\");\",\n \"+ window.history.replaceState({}, \\\"\\\", url.toString());\",\n \"+ } catch {\",\n \"+ router.replace(`/documents/${documentId}`);\",\n \"+ }\",\n \"+ }\",\n \"+ }, [documentId, editorBridge, openTableId, router]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setPageTitle(title ?? \\\"无标题\\\");\",\n \"+ }, [title]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setServerPageSubtreeTitle(title ?? \\\"无标题\\\");\",\n \"+ }, [title]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setServerPageSubtreeSnapshot(initialPageSubtree);\",\n \"+ }, [initialPageSubtree]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setOptions(initialOptions ?? defaultOptions);\",\n \"+ }, [initialOptions]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setStats(initialStats ?? defaultStats);\",\n \"+ }, [initialStats]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setContentRevision(initialContentRevision);\",\n \"+ }, [initialContentRevision]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setConflictDetectionKey(initialConflictDetectionKey);\",\n \"+ }, [initialConflictDetectionKey]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ setServerContentSnapshot(initialContent);\",\n \"+ }, [initialContent]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ const nextBlocks = extractPageBlocks(content);\",\n \"+ latestBlocksRef.current = nextBlocks.length > 0 ? (nextBlocks as Json) : null;\",\n \"+ }, [content]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ const shouldForceEdit = Boolean((openTableId ?? \\\"\\\").trim()) && canEditDocument;\",\n \"+ if (shouldForceEdit) {\",\n \"+ setIsEditing(true);\",\n \"+ setKeepEditorMounted(true);\",\n \"+ }\",\n \"+ }, [canEditDocument, openTableId]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ if (isEditing) {\",\n \"+ if (editorUnmountTimerRef.current) {\",\n \"+ clearTimeout(editorUnmountTimerRef.current);\",\n \"+ editorUnmountTimerRef.current = null;\",\n \"+ }\",\n \"+ setKeepEditorMounted(true);\",\n \"+ return;\",\n \"+ }\",\n \"+ if (editorUnmountTimerRef.current) {\",\n \"+ clearTimeout(editorUnmountTimerRef.current);\",\n \"+ }\",\n \"+ editorUnmountTimerRef.current = setTimeout(() => {\",\n \"+ setKeepEditorMounted(false);\",\n \"+ editorUnmountTimerRef.current = null;\",\n \"+ }, EDITOR_UNMOUNT_GRACE_MS);\",\n \"+ return () => {\",\n \"+ if (editorUnmountTimerRef.current) {\",\n \"+ clearTimeout(editorUnmountTimerRef.current);\",\n \"+ editorUnmountTimerRef.current = null;\",\n \"+ }\",\n \"+ };\",\n \"+ }, [isEditing]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ let canceled = false;\",\n \"+ const controller = new AbortController();\",\n \"+\",\n \"+ const load = async () => {\",\n \"+ setContentError(null);\",\n \"+ setContentLoading(initialContent == null);\",\n \"+ setContent(initialContent);\",\n \"+ setShowContentLoadingIndicator(false);\",\n \"+\",\n \"+ if (initialContent != null) {\",\n \"+ return;\",\n \"+ }\",\n \"+\",\n \"+ if (contentLoadingTimerRef.current) {\",\n \"+ clearTimeout(contentLoadingTimerRef.current);\",\n \"+ contentLoadingTimerRef.current = null;\",\n \"+ }\",\n \"+ contentLoadingTimerRef.current = setTimeout(() => {\",\n \"+ if (!canceled) {\",\n \"+ setShowContentLoadingIndicator(true);\",\n \"+ }\",\n \"+ }, CONTENT_LOADING_DELAY_MS);\",\n \"+\",\n \"+ try {\",\n \"+ const response = await fetch(\",\n \"+ `/api/documents/content?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,\",\n \"+ {\",\n \"+ method: \\\"GET\\\",\",\n \"+ credentials: \\\"include\\\",\",\n \"+ signal: controller.signal,\",\n \"+ },\",\n \"+ );\",\n \"+ if (!response.ok) {\",\n \"+ const payload = await response.json().catch(() => ({}));\",\n \"+ throw new Error(payload?.error ?? \\\"加载页面内容失败\\\");\",\n \"+ }\",\n \"+ const payload = (await response.json()) as {\",\n \"+ content?: unknown;\",\n \"+ revision?: number | null;\",\n \"+ conflictDetectionKey?: string | null;\",\n \"+ pageSubtree?: PageSubtreeProjection | null;\",\n \"+ };\",\n \"+ if (canceled) return;\",\n \"+ setContent(payload.content ?? null);\",\n \"+ setServerContentSnapshot(payload.content ?? null);\",\n \"+ setContentRevision(\",\n \"+ typeof payload.revision === \\\"number\\\" && Number.isInteger(payload.revision)\",\n \"+ ? payload.revision\",\n \"+ : 0,\",\n \"+ );\",\n \"+ setConflictDetectionKey(\",\n \"+ typeof payload.conflictDetectionKey === \\\"string\\\" && payload.conflictDetectionKey.trim()\",\n \"+ ? payload.conflictDetectionKey\",\n \"+ : `${documentId}:0`,\",\n \"+ );\",\n \"+ setServerPageSubtreeSnapshot(payload.pageSubtree ?? null);\",\n \"+ setServerPageSubtreeTitle(\",\n \"+ typeof payload.pageSubtree?.rootNode.metadata.title === \\\"string\\\" &&\",\n \"+ payload.pageSubtree.rootNode.metadata.title.trim()\",\n \"+ ? payload.pageSubtree.rootNode.metadata.title\",\n \"+ : title ?? \\\"无标题\\\",\",\n \"+ );\",\n \"+ } catch (error) {\",\n \"+ if (canceled) return;\",\n \"+ if ((error as { name?: string })?.name === \\\"AbortError\\\") return;\",\n \"+ setContentError(error instanceof Error ? error.message : \\\"加载页面内容失败\\\");\",\n \"+ } finally {\",\n \"+ if (!canceled) {\",\n \"+ setContentLoading(false);\",\n \"+ setShowContentLoadingIndicator(false);\",\n \"+ }\",\n \"+ if (contentLoadingTimerRef.current) {\",\n \"+ clearTimeout(contentLoadingTimerRef.current);\",\n \"+ contentLoadingTimerRef.current = null;\",\n \"+ }\",\n \"+ }\",\n \"+ };\",\n \"+\",\n \"+ void load();\",\n \"+\",\n \"+ return () => {\",\n \"+ canceled = true;\",\n \"+ controller.abort();\",\n \"+ if (contentLoadingTimerRef.current) {\",\n \"+ clearTimeout(contentLoadingTimerRef.current);\",\n \"+ contentLoadingTimerRef.current = null;\",\n \"+ }\",\n \"+ };\",\n \"+ }, [documentId, initialContent, contentReloadKey, title, workspaceId]);\",\n \"+\",\n \"+ const persistTitle = useCallback(\",\n \"+ async (nextTitle: string) => {\",\n \"+ if (readOnly) return;\",\n \"+ const payload = nextTitle.trim() || \\\"无标题\\\";\",\n \"+ try {\",\n \"+ await renameDocumentCommand({ documentId, workspaceId, title: payload });\",\n \"+ } catch (error) {\",\n \"+ console.error(\\\"更新页面标题失败\\\", error);\",\n \"+ }\",\n \"+ },\",\n \"+ [documentId, readOnly, workspaceId],\",\n \"+ );\",\n \"+\",\n \"+ const debouncedPersistTitle = useDebouncedCallback((value: string) => {\",\n \"+ void persistTitle(value);\",\n \"+ }, 600);\",\n \"+\",\n \"+ const handleTitleChange = (event: ChangeEvent) => {\",\n \"+ if (!canEditDocument) return;\",\n \"+ const value = event.target.value;\",\n \"+ setPageTitle(value);\",\n \"+ debouncedPersistTitle(value);\",\n \"+ };\",\n \"+\",\n \"+ const handleTitleBlur = () => {\",\n \"+ if (!canEditDocument) return;\",\n \"+ void persistTitle(pageTitle);\",\n \"+ };\",\n \"+\",\n \"+ const handleTitleKeyDown = (event: ReactKeyboardEvent) => {\",\n \"+ if (event.key === \\\"Enter\\\") {\",\n \"+ event.preventDefault();\",\n \"+ event.currentTarget.blur();\",\n \"+ }\",\n \"+ };\",\n \"+\",\n \"+ const persistOptions = useCallback(\",\n \"+ async (patch: Partial) => {\",\n \"+ if (readOnly) return;\",\n \"+ try {\",\n \"+ const response = await fetch(\\\"/api/documents/options\\\", {\",\n \"+ method: \\\"POST\\\",\",\n \"+ headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"+ body: JSON.stringify({ documentId, workspaceId, options: patch }),\",\n \"+ });\",\n \"+ if (!response.ok) {\",\n \"+ const payload = await response.json().catch(() => null);\",\n \"+ console.error(payload?.error ?? \\\"更新页面选项失败\\\");\",\n \"+ }\",\n \"+ } catch (error) {\",\n \"+ console.error(error);\",\n \"+ }\",\n \"+ },\",\n \"+ [documentId, readOnly, workspaceId],\",\n \"+ );\",\n \"+\",\n \"+ const toggleOption = useCallback(\",\n \"+ (key: BooleanPageOptionKey) => {\",\n \"+ if (readOnly) return;\",\n \"+ setOptions((prev) => {\",\n \"+ const nextValue = !prev[key];\",\n \"+ const next = { ...prev, [key]: nextValue };\",\n \"+ void persistOptions({ [key]: nextValue } as Partial);\",\n \"+ return next;\",\n \"+ });\",\n \"+ },\",\n \"+ [persistOptions, readOnly],\",\n \"+ );\",\n \"+\",\n \"+ const setOptionPatch = useCallback(\",\n \"+ (patch: Partial) => {\",\n \"+ if (readOnly) return;\",\n \"+ setOptions((prev) => {\",\n \"+ const next = { ...prev, ...patch };\",\n \"+ void persistOptions(patch);\",\n \"+ return next;\",\n \"+ });\",\n \"+ },\",\n \"+ [persistOptions, readOnly],\",\n \"+ );\",\n \"+\",\n \"+ const closeToc = useCallback(() => {\",\n \"+ if (readOnly) return;\",\n \"+ setOptions((prev) => {\",\n \"+ if (!prev.showToc) return prev;\",\n \"+ const next = { ...prev, showToc: false };\",\n \"+ void persistOptions({ showToc: false });\",\n \"+ return next;\",\n \"+ });\",\n \"+ }, [persistOptions, readOnly]);\",\n \"+\",\n \"+ const handleSetPageFont = useCallback(\",\n \"+ (font: PageFont) => {\",\n \"+ setOptionPatch({ pageFont: font });\",\n \"+ },\",\n \"+ [setOptionPatch],\",\n \"+ );\",\n \"+\",\n \"+ const handleSetLayoutDensity = useCallback(\",\n \"+ (density: PageLayoutDensity) => {\",\n \"+ setOptionPatch({ layoutDensity: density });\",\n \"+ },\",\n \"+ [setOptionPatch],\",\n \"+ );\",\n \"+\",\n \"+ const handleSetEmbedDefaultToCursor = useCallback(() => {\",\n \"+ if (!canEditDocument) return;\",\n \"+ if (!isEditing) {\",\n \"+ setIsEditing(true);\",\n \"+ return;\",\n \"+ }\",\n \"+ const blockId = editorBridge?.getCursorBlockId?.() ?? null;\",\n \"+ if (!blockId) {\",\n \"+ window.alert(\\\"未找到当前光标所在块,请先在编辑器中点击任意块\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ setOptionPatch({ embedDefaultBlockId: blockId });\",\n \"+ window.alert(\\\"已设置“嵌入默认位置”\\\");\",\n \"+ }, [canEditDocument, editorBridge, isEditing, setOptionPatch]);\",\n \"+\",\n \"+ const handleClearEmbedDefault = useCallback(() => {\",\n \"+ if (readOnly) return;\",\n \"+ setOptionPatch({ embedDefaultBlockId: null });\",\n \"+ window.alert(\\\"已清除“嵌入默认位置”\\\");\",\n \"+ }, [readOnly, setOptionPatch]);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ const onKeyDown = (event: KeyboardEvent) => {\",\n \"+ const root = pageRootRef.current;\",\n \"+ if (root) {\",\n \"+ const target = event.target;\",\n \"+ if (target && target instanceof Node && !root.contains(target)) {\",\n \"+ return;\",\n \"+ }\",\n \"+ }\",\n \"+ const ctrlOrMeta = event.ctrlKey || event.metaKey;\",\n \"+ if (!ctrlOrMeta) return;\",\n \"+ if (!event.shiftKey) return;\",\n \"+ const key = String(event.key ?? \\\"\\\").toLowerCase();\",\n \"+ if (key === \\\"l\\\") {\",\n \"+ event.preventDefault();\",\n \"+ toggleOption(\\\"showToc\\\");\",\n \"+ } else if (key === \\\"c\\\") {\",\n \"+ event.preventDefault();\",\n \"+ toggleOption(\\\"protectEditing\\\");\",\n \"+ }\",\n \"+ };\",\n \"+ window.addEventListener(\\\"keydown\\\", onKeyDown);\",\n \"+ return () => window.removeEventListener(\\\"keydown\\\", onKeyDown);\",\n \"+ }, [toggleOption]);\",\n \"+\",\n \"+ const buildDocumentUrl = useCallback((id: string): string => {\",\n \"+ if (typeof window === \\\"undefined\\\" || !window.location) {\",\n \"+ return `/documents/${id}`;\",\n \"+ }\",\n \"+ return `${window.location.origin}/documents/${id}`;\",\n \"+ }, []);\",\n \"+\",\n \"+ const copyText = useCallback(async (text: string, successMessage: string) => {\",\n \"+ if (typeof navigator !== \\\"undefined\\\" && navigator.clipboard) {\",\n \"+ try {\",\n \"+ await navigator.clipboard.writeText(text);\",\n \"+ window.alert(successMessage);\",\n \"+ return;\",\n \"+ } catch {\",\n \"+ }\",\n \"+ }\",\n \"+ window.prompt(\\\"复制失败,请手动复制内容\\\", text);\",\n \"+ }, []);\",\n \"+\",\n \"+ const handleCopyPageLink = useCallback(\",\n \"+ async (includeTitle: boolean) => {\",\n \"+ const url = buildDocumentUrl(documentId);\",\n \"+ if (includeTitle) {\",\n \"+ const text = `${pageTitle || \\\"无标题\\\"}\\\\n${url}`;\",\n \"+ await copyText(text, \\\"标题 + 链接已复制\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ await copyText(url, \\\"页面链接已复制\\\");\",\n \"+ },\",\n \"+ [buildDocumentUrl, copyText, documentId, pageTitle],\",\n \"+ );\",\n \"+\",\n \"+ const handleCopyPageReference = useCallback(\",\n \"+ async (mode: \\\"inline\\\" | \\\"embed\\\") => {\",\n \"+ const template = mode === \\\"inline\\\" ? `((${documentId}))` : `{{${documentId}}}`;\",\n \"+ await copyText(template, mode === \\\"inline\\\" ? \\\"行内引用已复制\\\" : \\\"嵌入引用已复制\\\");\",\n \"+ },\",\n \"+ [copyText, documentId],\",\n \"+ );\",\n \"+\",\n \"+ const handleUndo = useCallback(() => {\",\n \"+ if (!isEditing) {\",\n \"+ setIsEditing(true);\",\n \"+ return;\",\n \"+ }\",\n \"+ editorBridge?.undo?.();\",\n \"+ }, [editorBridge, isEditing]);\",\n \"+\",\n \"+ const handleRedo = useCallback(() => {\",\n \"+ if (!isEditing) {\",\n \"+ setIsEditing(true);\",\n \"+ return;\",\n \"+ }\",\n \"+ editorBridge?.redo?.();\",\n \"+ }, [editorBridge, isEditing]);\",\n \"+\",\n \"+ const handleDeletePage = useCallback(async () => {\",\n \"+ if (readOnly) return;\",\n \"+ const ok = window.confirm(\\\"确定删除该页面吗?删除后会进入垃圾桶。\\\");\",\n \"+ if (!ok) return;\",\n \"+ try {\",\n \"+ await deleteDocumentCommand({ documentId, workspaceId });\",\n \"+ } catch (error) {\",\n \"+ const payload = error instanceof Error ? error.message : null;\",\n \"+ window.alert(payload ?? \\\"删除失败\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ router.push(\\\"/\\\");\",\n \"+ router.refresh();\",\n \"+ }, [documentId, readOnly, router, workspaceId]);\",\n \"+\",\n \"+ const handleOpenMoveEmbed = useCallback(() => {\",\n \"+ if (readOnly) return;\",\n \"+ openMoveEmbedPicker({\",\n \"+ workspaceId,\",\n \"+ defaultMode: \\\"move\\\",\",\n \"+ modes: [\\\"move\\\", \\\"embed\\\"],\",\n \"+ allowRoot: true,\",\n \"+ excludeIds: [documentId],\",\n \"+ onPick: async (mode, targetId) => {\",\n \"+ if (mode === \\\"move\\\") {\",\n \"+ try {\",\n \"+ await moveDocumentCommand({\",\n \"+ documentId,\",\n \"+ parentId: targetId ?? null,\",\n \"+ position: 999999,\",\n \"+ workspaceId,\",\n \"+ });\",\n \"+ } catch (error) {\",\n \"+ window.alert(error instanceof Error ? error.message : \\\"移动失败\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ window.alert(\\\"移动成功\\\");\",\n \"+ router.refresh();\",\n \"+ return;\",\n \"+ }\",\n \"+\",\n \"+ if (!targetId) {\",\n \"+ window.alert(\\\"请选择目标页面\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+\",\n \"+ try {\",\n \"+ await embedDocumentCommand({\",\n \"+ sourceId: documentId,\",\n \"+ targetId,\",\n \"+ });\",\n \"+ } catch (error) {\",\n \"+ const message = error instanceof Error ? error.message : \\\"嵌入失败\\\";\",\n \"+ window.alert(message);\",\n \"+ return;\",\n \"+ }\",\n \"+\",\n \"+ window.alert(\\\"已嵌入到目标页面\\\");\",\n \"+ },\",\n \"+ });\",\n \"+ }, [documentId, openMoveEmbedPicker, readOnly, router, workspaceId]);\",\n \"+\",\n \"+ const handleAddToTemplates = useCallback(async () => {\",\n \"+ if (readOnly) return;\",\n \"+ const ok = window.confirm(\\\"将该页面添加为模板?\\\");\",\n \"+ if (!ok) return;\",\n \"+ const resp = await fetch(\\\"/api/documents/template\\\", {\",\n \"+ method: \\\"POST\\\",\",\n \"+ headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"+ body: JSON.stringify({ documentId, isTemplate: true }),\",\n \"+ });\",\n \"+ if (!resp.ok) {\",\n \"+ const payload = await resp.json().catch(() => ({}));\",\n \"+ window.alert(payload?.error ?? \\\"设置模板失败\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ window.alert(\\\"已添加为模板\\\");\",\n \"+ router.refresh();\",\n \"+ }, [documentId, readOnly, router]);\",\n \"+\",\n \"+ const formattedUpdatedAt = useMemo(() => {\",\n \"+ if (!updatedAt) return \\\"\\\";\",\n \"+ const date = new Date(updatedAt);\",\n \"+ if (Number.isNaN(date.getTime())) return \\\"\\\";\",\n \"+ return new Intl.DateTimeFormat(\\\"zh-CN\\\", {\",\n \"+ timeZone: \\\"Asia/Shanghai\\\",\",\n \"+ year: \\\"numeric\\\",\",\n \"+ month: \\\"numeric\\\",\",\n \"+ day: \\\"numeric\\\",\",\n \"+ hour: \\\"2-digit\\\",\",\n \"+ minute: \\\"2-digit\\\",\",\n \"+ second: \\\"2-digit\\\",\",\n \"+ hour12: false,\",\n \"+ }).format(date);\",\n \"+ }, [updatedAt]);\",\n \"+\",\n \"+ const handleExport = useCallback(() => {\",\n \"+ if (disableDownload) {\",\n \"+ window.alert(\\\"该页面已禁止下载\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ const latest = history[0];\",\n \"+ const exportBlocks = latest?.blocks ?? latestBlocksRef.current;\",\n \"+ if (!exportBlocks) {\",\n \"+ window.alert(\\\"暂无可导出的内容\\\");\",\n \"+ return;\",\n \"+ }\",\n \"+ const payload = JSON.stringify(exportBlocks, null, 2);\",\n \"+ const blob = new Blob([payload], { type: \\\"application/json\\\" });\",\n \"+ const url = URL.createObjectURL(blob);\",\n \"+ const anchor = document.createElement(\\\"a\\\");\",\n \"+ anchor.href = url;\",\n \"+ anchor.download = `${title ?? \\\"未命名页面\\\"}-${new Date().toISOString()}.json`;\",\n \"+ anchor.click();\",\n \"+ URL.revokeObjectURL(url);\",\n \"+ }, [disableDownload, history, title]);\",\n \"+\",\n \"+ const pageRootClass = cn(\",\n \"+ \\\"flex h-full overflow-hidden bg-wolai-bg\\\",\",\n \"+ options.pageFont === \\\"song\\\" && \\\"wolai-page-font-song\\\",\",\n \"+ options.pageFont === \\\"kai\\\" && \\\"wolai-page-font-kai\\\",\",\n \"+ options.layoutDensity === \\\"compact\\\" && \\\"wolai-page-density-compact\\\",\",\n \"+ options.layoutDensity === \\\"spacious\\\" && \\\"wolai-page-density-spacious\\\",\",\n \"+ options.smallText && \\\"wolai-small-text\\\",\",\n \"+ options.hideChildPages && \\\"wolai-hide-child-pages\\\",\",\n \"+ );\",\n \"+ const pageSubtree = useMemo(() => {\",\n \"+ const hasServerPageSubtree = Boolean(serverPageSubtreeSnapshot);\",\n \"+ const titleUnchanged = pageTitle === serverPageSubtreeTitle;\",\n \"+ const contentUnchanged = content === serverContentSnapshot;\",\n \"+\",\n \"+ if (hasServerPageSubtree && titleUnchanged && contentUnchanged) {\",\n \"+ return serverPageSubtreeSnapshot;\",\n \"+ }\",\n \"+ return null;\",\n \"+ }, [\",\n \"+ content,\",\n \"+ pageTitle,\",\n \"+ serverContentSnapshot,\",\n \"+ serverPageSubtreeSnapshot,\",\n \"+ serverPageSubtreeTitle,\",\n \"+ ]);\",\n \"+ const readViewTocEntries = useMemo(\",\n \"+ () =>\",\n \"+ (pageSubtree?.outline ?? [])\",\n \"+ .filter((entry) => typeof entry.anchorBlockId === \\\"string\\\" && entry.anchorBlockId.trim())\",\n \"+ .map(({ anchorBlockId, level, numbering, title: entryTitle }) => ({\",\n \"+ id: anchorBlockId as string,\",\n \"+ level,\",\n \"+ numbering,\",\n \"+ title: entryTitle,\",\n \"+ })),\",\n \"+ [pageSubtree],\",\n \"+ );\",\n \"+ const inspectorCanUseEditorBridge = canEditDocument && isEditing;\",\n \"+\",\n \"+ const jumpToHeading = useCallback((headingId: string) => {\",\n \"+ const targetRoot = !isEditing ? readViewRootRef.current : pageRootRef.current;\",\n \"+ const target = targetRoot?.querySelector(`[data-id=\\\"${headingId}\\\"]`);\",\n \"+ if (target) {\",\n \"+ target.scrollIntoView({ behavior: \\\"smooth\\\", block: \\\"center\\\" });\",\n \"+ }\",\n \"+ }, [isEditing]);\",\n \"+\",\n \"+ const handleSnapshot = useCallback((payload: { blocks: Json; stats: DocumentStats }) => {\",\n \"+ setContent(payload.blocks);\",\n \"+ latestBlocksRef.current = payload.blocks;\",\n \"+ setHistory((prev) => {\",\n \"+ const now = Date.now();\",\n \"+ if (prev.length > 0 && now - prev[0].timestamp < 4000) {\",\n \"+ return prev;\",\n \"+ }\",\n \"+ const snapshot: DocumentSnapshot = {\",\n \"+ id: `${now}-${Math.random().toString(36).slice(2, 8)}`,\",\n \"+ timestamp: now,\",\n \"+ blocks: payload.blocks,\",\n \"+ stats: payload.stats,\",\n \"+ };\",\n \"+ return [snapshot, ...prev].slice(0, 15);\",\n \"+ });\",\n \"+ }, []);\",\n \"+\",\n \"+ const persistStatsRequest = useCallback((next: DocumentStats) => {\",\n \"+ void fetch(\\\"/api/documents/stats\\\", {\",\n \"+ method: \\\"POST\\\",\",\n \"+ headers: { \\\"Content-Type\\\": \\\"application/json\\\" },\",\n \"+ body: JSON.stringify({ documentId, workspaceId, stats: next }),\",\n \"+ }).catch((error) => console.error(error));\",\n \"+ }, [documentId, workspaceId]);\",\n \"+\",\n \"+ const persistStats = useDebouncedCallback(persistStatsRequest, 1500);\",\n \"+\",\n \"+ const handleStatsChange = useCallback(\",\n \"+ (nextStats: DocumentStats) => {\",\n \"+ setStats(nextStats);\",\n \"+ persistStats(nextStats);\",\n \"+ },\",\n \"+ [persistStats],\",\n \"+ );\",\n \"+\",\n \"+ const handleRestoreSnapshot = useCallback(\",\n \"+ (snapshot: DocumentSnapshot) => {\",\n \"+ if (!isEditing || !editorBridge) {\",\n \"+ pendingRestoreSnapshotRef.current = snapshot;\",\n \"+ setIsEditing(true);\",\n \"+ return;\",\n \"+ }\",\n \"+ editorBridge.replaceWithSnapshot(snapshot.blocks);\",\n \"+ setHistoryOpen(false);\",\n \"+ },\",\n \"+ [editorBridge, isEditing],\",\n \"+ );\",\n \"+\",\n \"+ const handleEnterEditMode = useCallback(() => {\",\n \"+ if (!canEditDocument) return;\",\n \"+ setIsEditing(true);\",\n \"+ }, [canEditDocument]);\",\n \"+\",\n \"+ const handleExitEditMode = useCallback(() => {\",\n \"+ setIsEditing(false);\",\n \"+ }, []);\",\n \"+\",\n \"+ useEffect(() => {\",\n \"+ if (!isEditing) return;\",\n \"+ if (!editorBridge) return;\",\n \"+ const pendingSnapshot = pendingRestoreSnapshotRef.current;\",\n \"+ if (!pendingSnapshot) return;\",\n \"+ pendingRestoreSnapshotRef.current = null;\",\n \"+ editorBridge.replaceWithSnapshot(pendingSnapshot.blocks);\",\n \"+ setHistoryOpen(false);\",\n \"+ }, [editorBridge, isEditing]);\",\n \"+ return (\",\n \"+ \",\n \"+
\",\n \"+
\",\n \"+
\",\n \"+
\",\n \"+
\",\n \"+ {isEditing && canEditDocument ? (\",\n \"+
\",\n \"+ \",\n \"+
\",\n \"+ ) : (\",\n \"+

\",\n \"+ {pageTitle || \\\"无标题\\\"}\",\n \"+

\",\n \"+ )}\",\n \"+ {readOnly ? (\",\n \"+

该页面为只读共享,无法编辑。

\",\n \"+ ) : options.protectEditing && isEditing ? (\",\n \"+

当前页面已开启编辑保护,关闭后方可修改内容。

\",\n \"+ ) : null}\",\n \"+

最近更新:{formattedUpdatedAt}

\",\n \"+
\",\n \"+ {canEditDocument && false && (\",\n \"+
\",\n \"+ {isEditing ? (\",\n \"+ \",\n \"+ ) : (\",\n \"+ \",\n \"+ )}\",\n \"+
\",\n \"+ )}\",\n \"+
\",\n \"+
\",\n \"+
\",\n \"+ {contentLoading ? (\",\n \"+ showContentLoadingIndicator ? (\",\n \"+
\",\n \"+ 页面内容加载中...\",\n \"+
\",\n \"+ ) : (\",\n \"+
\",\n \"+ )\",\n \"+ ) : contentError ? (\",\n \"+
\",\n \"+
{contentError}
\",\n \"+ {\",\n \"+ setContentError(null);\",\n \"+ setContentLoading(true);\",\n \"+ setContentReloadKey((prev) => prev + 1);\",\n \"+ }}\",\n \"+ >\",\n \"+ 重试\",\n \"+ \",\n \"+
\",\n \"+ ) : (\",\n \"+
\",\n \"+ {keepEditorMounted && (\",\n \"+
\",\n \"+ {\",\n \"+ setContentRevision(meta.revision);\",\n \"+ setConflictDetectionKey(meta.conflictDetectionKey);\",\n \"+ }}\",\n \"+ />\",\n \"+
\",\n \"+ )}\",\n \"+ {!isEditing && (\",\n \"+
\",\n \"+ \",\n \"+ \",\n \"+
\",\n \"+ )}\",\n \"+
\",\n \"+ )}\",\n \"+ \",\n \"+
\",\n \"+
\",\n \"+ {showInspector && (\",\n \"+ setHistoryOpen(true)}\",\n \"+ onOpenComments={() => openCommentsForPage({ workspaceId, documentId })}\",\n \"+ onUndo={canEditDocument ? handleUndo : undefined}\",\n \"+ onRedo={canEditDocument ? handleRedo : undefined}\",\n \"+ onDeletePage={canEditDocument ? handleDeletePage : undefined}\",\n \"+ onOpenMoveEmbedPicker={canEditDocument ? handleOpenMoveEmbed : undefined}\",\n \"+ onCopyPageLink={handleCopyPageLink}\",\n \"+ onCopyPageReference={handleCopyPageReference}\",\n \"+ onAddToTemplates={canEditDocument ? handleAddToTemplates : undefined}\",\n \"+ />\",\n \"+ )}\",\n \"+
\",\n \"+ \",\n \"+ \",\n \"+ \",\n \"+ latestBlocksRef.current}\",\n \"+ getLatestPageSubtree={() => pageSubtree}\",\n \"+ />\",\n \"+ \",\n \"+ );\",\n \"+}\"\n ]\n }\n ],\n \"userModified\": false,\n \"replaceAll\": false,\n \"gitDiff\": null\n}","tool_name":"edit_file","tool_use_id":"call_P7xKbQyE2ZTeq2t7Y5baMRyK","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_8gflsKPOgF098zDJgIIez7u5","input":"{\"content\":\"export type TiptapMark = {\\n type: string;\\n attrs?: Record;\\n};\\n\\nexport type TiptapNode = {\\n type: string;\\n attrs?: Record;\\n content?: TiptapNode[];\\n text?: string;\\n marks?: TiptapMark[];\\n};\\n\\nexport type TiptapDoc = {\\n type: \\\"doc\\\";\\n content: TiptapNode[];\\n};\\n\\nexport type EditorTextMark = \\\"bold\\\" | \\\"italic\\\" | \\\"underline\\\" | \\\"strike\\\" | \\\"code\\\";\\n\\nexport type EditorBlockType =\\n | \\\"paragraph\\\"\\n | \\\"heading\\\"\\n | \\\"bullet_list_item\\\"\\n | \\\"numbered_list_item\\\"\\n | \\\"todo\\\"\\n | \\\"quote\\\"\\n | \\\"code_block\\\";\\n\\nexport type EditorContentNode = {\\n type: \\\"text\\\";\\n text: string;\\n marks?: EditorTextMark[];\\n};\\n\\nexport type EditorBlock = {\\n blockId: string;\\n blockType: EditorBlockType;\\n props?: {\\n headingLevel?: number | null;\\n checked?: boolean | null;\\n language?: string | null;\\n };\\n contentNodes?: EditorContentNode[];\\n childBlockIds?: string[];\\n};\\n\\nexport type EditorBlockDocument = {\\n documentId: string;\\n rootBlockIds: string[];\\n blocks: EditorBlock[];\\n};\\n\\ntype Json = unknown;\\n\\ntype LegacyBlockLike = {\\n id?: string;\\n blockId?: string;\\n type?: string;\\n blockType?: string;\\n content?: unknown;\\n contentNodes?: unknown;\\n props?: Record;\\n children?: unknown;\\n};\\n\\nconst EMPTY_DOC: TiptapDoc = {\\n type: \\\"doc\\\",\\n content: [{ type: \\\"paragraph\\\", attrs: {}, content: [] }],\\n};\\n\\nconst MARK_NAME_MAP: Record = {\\n bold: \\\"bold\\\",\\n italic: \\\"italic\\\",\\n underline: \\\"underline\\\",\\n strike: \\\"strike\\\",\\n code: \\\"code\\\",\\n};\\n\\nconst BLOCK_ID_ATTR = \\\"blockId\\\";\\n\\nfunction coerceTextMark(value: unknown): EditorTextMark | null {\\n const normalized = String(value ?? \\\"\\\").trim().toLowerCase();\\n return MARK_NAME_MAP[normalized] ?? null;\\n}\\n\\nfunction readMarks(marks: unknown): EditorTextMark[] {\\n if (!Array.isArray(marks)) return [];\\n const normalized = marks\\n .map((mark) => {\\n if (typeof mark === \\\"string\\\") return coerceTextMark(mark);\\n if (mark && typeof mark === \\\"object\\\") return coerceTextMark((mark as { type?: unknown }).type);\\n return null;\\n })\\n .filter((mark): mark is EditorTextMark => Boolean(mark));\\n return Array.from(new Set(normalized));\\n}\\n\\nfunction toText(value: unknown): string {\\n if (typeof value === \\\"string\\\") return value;\\n if (Array.isArray(value)) return value.map(toText).join(\\\"\\\");\\n if (value && typeof value === \\\"object\\\") {\\n const obj = value as { text?: unknown; content?: unknown; value?: unknown; contentNodes?: unknown };\\n return `${toText(obj.text)}${toText(obj.value)}${toText(obj.content)}${toText(obj.contentNodes)}`;\\n }\\n return \\\"\\\";\\n}\\n\\nfunction clampHeadingLevel(value: unknown): number {\\n const level = Number(value);\\n return Number.isInteger(level) && level >= 1 && level <= 6 ? level : 1;\\n}\\n\\nfunction normalizeBlockId(value: unknown, fallback: string): string {\\n const raw = typeof value === \\\"string\\\" ? value.trim() : \\\"\\\";\\n return raw || fallback;\\n}\\n\\nfunction normalizeEditorContentNodes(input: unknown): EditorContentNode[] {\\n if (Array.isArray(input)) {\\n const nodes = input.flatMap((item) => {\\n if (!item || typeof item !== \\\"object\\\") return [];\\n const record = item as { type?: unknown; text?: unknown; marks?: unknown; payload?: unknown };\\n if (record.type === \\\"text\\\") {\\n const text = toText(record.text);\\n return text ? [{ type: \\\"text\\\" as const, text, marks: readMarks(record.marks) }] : [];\\n }\\n if (record.payload && typeof record.payload === \\\"object\\\") {\\n const payload = record.payload as { type?: unknown; text?: unknown; marks?: unknown };\\n if (payload.type === \\\"text\\\") {\\n const text = toText(payload.text);\\n return text ? [{ type: \\\"text\\\" as const, text, marks: readMarks(payload.marks) }] : [];\\n }\\n }\\n const text = toText(item);\\n return text ? [{ type: \\\"text\\\" as const, text, marks: [] }] : [];\\n });\\n return nodes;\\n }\\n const text = toText(input);\\n return text ? [{ type: \\\"text\\\", text, marks: [] }] : [];\\n}\\n\\nfunction normalizeLegacyBlock(block: LegacyBlockLike, index: number): EditorBlock {\\n const blockType = String(block.blockType ?? block.type ?? \\\"paragraph\\\").trim().toLowerCase();\\n const blockId = normalizeBlockId(block.blockId ?? block.id, `block-${index + 1}`);\\n const props = block.props ?? {};\\n const contentNodes = normalizeEditorContentNodes(block.contentNodes ?? block.content ?? props.content ?? props.text ?? props.title);\\n\\n switch (blockType) {\\n case \\\"heading\\\":\\n return {\\n blockId,\\n blockType: \\\"heading\\\",\\n props: { headingLevel: clampHeadingLevel(props.level ?? props.headingLevel) },\\n contentNodes,\\n childBlockIds: [],\\n };\\n case \\\"bullet_list_item\\\":\\n case \\\"bullet_list\\\":\\n case \\\"bullet-list\\\":\\n return { blockId, blockType: \\\"bullet_list_item\\\", props: {}, contentNodes, childBlockIds: [] };\\n case \\\"numbered_list_item\\\":\\n case \\\"ordered_list\\\":\\n case \\\"ordered-list\\\":\\n return { blockId, blockType: \\\"numbered_list_item\\\", props: {}, contentNodes, childBlockIds: [] };\\n case \\\"todo\\\":\\n case \\\"task\\\":\\n return {\\n blockId,\\n blockType: \\\"todo\\\",\\n props: { checked: Boolean(props.checked) },\\n contentNodes,\\n childBlockIds: [],\\n };\\n case \\\"quote\\\":\\n case \\\"blockquote\\\":\\n return { blockId, blockType: \\\"quote\\\", props: {}, contentNodes, childBlockIds: [] };\\n case \\\"code\\\":\\n case \\\"code_block\\\":\\n case \\\"code-block\\\":\\n return {\\n blockId,\\n blockType: \\\"code_block\\\",\\n props: { language: typeof props.language === \\\"string\\\" ? props.language : null },\\n contentNodes,\\n childBlockIds: [],\\n };\\n case \\\"paragraph\\\":\\n case \\\"text\\\":\\n default:\\n return { blockId, blockType: \\\"paragraph\\\", props: {}, contentNodes, childBlockIds: [] };\\n }\\n}\\n\\nfunction normalizeInputToEditorDocument(value: Json): EditorBlockDocument {\\n if (value && typeof value === \\\"object\\\" && !Array.isArray(value)) {\\n const maybe = value as Partial;\\n if (typeof maybe.documentId === \\\"string\\\" && Array.isArray(maybe.blocks)) {\\n const normalizedBlocks = maybe.blocks.map((block, index) => normalizeLegacyBlock(block as LegacyBlockLike, index));\\n const rootBlockIds = Array.isArray(maybe.rootBlockIds) && maybe.rootBlockIds.length > 0\\n ? maybe.rootBlockIds.map((id, index) => normalizeBlockId(id, normalizedBlocks[index]?.blockId ?? `block-${index + 1}`))\\n : normalizedBlocks.map((block) => block.blockId);\\n return {\\n documentId: maybe.documentId,\\n rootBlockIds,\\n blocks: normalizedBlocks,\\n };\\n }\\n if (Array.isArray((value as { blocks?: unknown }).blocks)) {\\n const blocks = ((value as { blocks: unknown[] }).blocks ?? []).map((block, index) =>\\n normalizeLegacyBlock(block as LegacyBlockLike, index),\\n );\\n return {\\n documentId: typeof (value as { documentId?: unknown }).documentId === \\\"string\\\"\\n ? String((value as { documentId?: unknown }).documentId)\\n : \\\"document\\\",\\n rootBlockIds: blocks.map((block) => block.blockId),\\n blocks,\\n };\\n }\\n }\\n\\n const blocks = Array.isArray(value)\\n ? value.map((block, index) => normalizeLegacyBlock(block as LegacyBlockLike, index))\\n : [];\\n\\n return {\\n documentId: \\\"document\\\",\\n rootBlockIds: blocks.map((block) => block.blockId),\\n blocks,\\n };\\n}\\n\\nfunction textNodesToInline(contentNodes: EditorContentNode[] | undefined): TiptapNode[] {\\n return (contentNodes ?? []).flatMap((node) => {\\n const text = toText(node.text);\\n if (!text) return [];\\n const marks = (node.marks ?? [])\\n .map((mark) => ({ type: mark }))\\n .filter((mark) => Boolean(mark.type));\\n return [{ type: \\\"text\\\", text, marks: marks.length > 0 ? marks : undefined }];\\n });\\n}\\n\\nfunction blockToTiptapNode(block: EditorBlock): TiptapNode {\\n const commonAttrs: Record = { [BLOCK_ID_ATTR]: block.blockId };\\n switch (block.blockType) {\\n case \\\"heading\\\":\\n return {\\n type: \\\"heading\\\",\\n attrs: { ...commonAttrs, level: clampHeadingLevel(block.props?.headingLevel) },\\n content: textNodesToInline(block.contentNodes),\\n };\\n case \\\"bullet_list_item\\\":\\n return {\\n type: \\\"bulletList\\\",\\n attrs: commonAttrs,\\n content: [{ type: \\\"listItem\\\", attrs: commonAttrs, content: [{ type: \\\"paragraph\\\", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }] }],\\n };\\n case \\\"numbered_list_item\\\":\\n return {\\n type: \\\"orderedList\\\",\\n attrs: commonAttrs,\\n content: [{ type: \\\"listItem\\\", attrs: commonAttrs, content: [{ type: \\\"paragraph\\\", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }] }],\\n };\\n case \\\"todo\\\":\\n return {\\n type: \\\"taskList\\\",\\n attrs: commonAttrs,\\n content: [{ type: \\\"taskItem\\\", attrs: { ...commonAttrs, checked: Boolean(block.props?.checked) }, content: [{ type: \\\"paragraph\\\", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }] }],\\n };\\n case \\\"quote\\\":\\n return {\\n type: \\\"blockquote\\\",\\n attrs: commonAttrs,\\n content: [{ type: \\\"paragraph\\\", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }],\\n };\\n case \\\"code_block\\\":\\n return {\\n type: \\\"codeBlock\\\",\\n attrs: { ...commonAttrs, language: block.props?.language ?? null },\\n content: textNodesToInline(block.contentNodes),\\n };\\n case \\\"paragraph\\\":\\n default:\\n return {\\n type: \\\"paragraph\\\",\\n attrs: commonAttrs,\\n content: textNodesToInline(block.contentNodes),\\n };\\n }\\n}\\n\\nfunction extractInlineContent(node: TiptapNode | undefined): EditorContentNode[] {\\n if (!node) return [];\\n if (Array.isArray(node.content)) {\\n return node.content.flatMap((child) => {\\n if (child.type === \\\"text\\\") {\\n const text = toText(child.text);\\n if (!text) return [];\\n return [{ type: \\\"text\\\" as const, text, marks: readMarks(child.marks) }];\\n }\\n if (child.type === \\\"hardBreak\\\") {\\n return [{ type: \\\"text\\\" as const, text: \\\"\\\\n\\\", marks: [] }];\\n }\\n return extractInlineContent(child);\\n });\\n }\\n if (node.type === \\\"text\\\") {\\n const text = toText(node.text);\\n return text ? [{ type: \\\"text\\\", text, marks: readMarks(node.marks) }] : [];\\n }\\n return [];\\n}\\n\\nfunction firstChild(node: TiptapNode | undefined): TiptapNode | undefined {\\n return Array.isArray(node?.content) ? node?.content?.[0] : undefined;\\n}\\n\\nfunction nodeBlockId(node: TiptapNode, index: number): string {\\n return normalizeBlockId(node.attrs?.[BLOCK_ID_ATTR], `block-${index + 1}`);\\n}\\n\\nfunction tiptapNodeToBlock(node: TiptapNode, index: number): EditorBlock | null {\\n const blockId = nodeBlockId(node, index);\\n switch (node.type) {\\n case \\\"paragraph\\\":\\n return { blockId, blockType: \\\"paragraph\\\", props: {}, contentNodes: extractInlineContent(node), childBlockIds: [] };\\n case \\\"heading\\\":\\n return {\\n blockId,\\n blockType: \\\"heading\\\",\\n props: { headingLevel: clampHeadingLevel(node.attrs?.level) },\\n contentNodes: extractInlineContent(node),\\n childBlockIds: [],\\n };\\n case \\\"bulletList\\\": {\\n const paragraph = firstChild(firstChild(node));\\n return {\\n blockId,\\n blockType: \\\"bullet_list_item\\\",\\n props: {},\\n contentNodes: extractInlineContent(paragraph),\\n childBlockIds: [],\\n };\\n }\\n case \\\"orderedList\\\": {\\n const paragraph = firstChild(firstChild(node));\\n return {\\n blockId,\\n blockType: \\\"numbered_list_item\\\",\\n props: {},\\n contentNodes: extractInlineContent(paragraph),\\n childBlockIds: [],\\n };\\n }\\n case \\\"taskList\\\": {\\n const taskItem = firstChild(node);\\n const paragraph = firstChild(taskItem);\\n return {\\n blockId,\\n blockType: \\\"todo\\\",\\n props: { checked: Boolean(taskItem?.attrs?.checked) },\\n contentNodes: extractInlineContent(paragraph),\\n childBlockIds: [],\\n };\\n }\\n case \\\"blockquote\\\":\\n return { blockId, blockType: \\\"quote\\\", props: {}, contentNodes: extractInlineContent(firstChild(node)), childBlockIds: [] };\\n case \\\"codeBlock\\\":\\n return {\\n blockId,\\n blockType: \\\"code_block\\\",\\n props: { language: typeof node.attrs?.language === \\\"string\\\" ? String(node.attrs.language) : null },\\n contentNodes: extractInlineContent(node),\\n childBlockIds: [],\\n };\\n default:\\n return null;\\n }\\n}\\n\\nexport function editorBlockDocumentFromContent(content: Json): EditorBlockDocument {\\n return normalizeInputToEditorDocument(content);\\n}\\n\\nexport function tiptapDocFromEditorBlockDocument(document: EditorBlockDocument): TiptapDoc {\\n const blockById = new Map(document.blocks.map((block) => [block.blockId, block]));\\n const nodes = document.rootBlockIds\\n .map((blockId) => blockById.get(blockId))\\n .filter((block): block is EditorBlock => Boolean(block))\\n .map(blockToTiptapNode);\\n return {\\n type: \\\"doc\\\",\\n content: nodes.length > 0 ? nodes : EMPTY_DOC.content,\\n };\\n}\\n\\nexport function editorBlockDocumentFromTiptapDoc(doc: unknown, documentId = \\\"document\\\"): EditorBlockDocument {\\n if (!doc || typeof doc !== \\\"object\\\") {\\n return { documentId, rootBlockIds: [], blocks: [] };\\n }\\n const root = doc as { type?: unknown; content?: unknown };\\n if (root.type !== \\\"doc\\\" || !Array.isArray(root.content)) {\\n return { documentId, rootBlockIds: [], blocks: [] };\\n }\\n const blocks = root.content.flatMap((node, index) => {\\n if (!node || typeof node !== \\\"object\\\") return [];\\n const block = tiptapNodeToBlock(node as TiptapNode, index);\\n return block ? [block] : [];\\n });\\n return {\\n documentId,\\n rootBlockIds: blocks.map((block) => block.blockId),\\n blocks,\\n };\\n}\\n\\nexport function tiptapDocFromBlocks(blocks: Json): TiptapDoc {\\n return tiptapDocFromEditorBlockDocument(editorBlockDocumentFromContent(blocks));\\n}\\n\\nexport function blocksFromTiptapDoc(doc: unknown): Json {\\n return editorBlockDocumentFromTiptapDoc(doc).blocks.map((block) => ({\\n id: block.blockId,\\n type: block.blockType,\\n props:\\n block.blockType === \\\"heading\\\"\\n ? { level: block.props?.headingLevel ?? 1 }\\n : block.blockType === \\\"todo\\\"\\n ? { checked: Boolean(block.props?.checked) }\\n : block.blockType === \\\"code_block\\\"\\n ? { language: block.props?.language ?? null }\\n : undefined,\\n content: (block.contentNodes ?? []).map((node) => node.text).join(\\\"\\\"),\\n }));\\n}\\n\\nexport const blocksToTiptapDoc = tiptapDocFromBlocks;\\nexport const tiptapDocToBlocks = blocksFromTiptapDoc;\\n\",\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.ts\"}","name":"write_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":177512,"output_tokens":4267}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"update\",\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.ts\",\n \"content\": \"export type TiptapMark = {\\n type: string;\\n attrs?: Record;\\n};\\n\\nexport type TiptapNode = {\\n type: string;\\n attrs?: Record;\\n content?: TiptapNode[];\\n text?: string;\\n marks?: TiptapMark[];\\n};\\n\\nexport type TiptapDoc = {\\n type: \\\"doc\\\";\\n content: TiptapNode[];\\n};\\n\\nexport type EditorTextMark = \\\"bold\\\" | \\\"italic\\\" | \\\"underline\\\" | \\\"strike\\\" | \\\"code\\\";\\n\\nexport type EditorBlockType =\\n | \\\"paragraph\\\"\\n | \\\"heading\\\"\\n | \\\"bullet_list_item\\\"\\n | \\\"numbered_list_item\\\"\\n | \\\"todo\\\"\\n | \\\"quote\\\"\\n | \\\"code_block\\\";\\n\\nexport type EditorContentNode = {\\n type: \\\"text\\\";\\n text: string;\\n marks?: EditorTextMark[];\\n};\\n\\nexport type EditorBlock = {\\n blockId: string;\\n blockType: EditorBlockType;\\n props?: {\\n headingLevel?: number | null;\\n checked?: boolean | null;\\n language?: string | null;\\n };\\n contentNodes?: EditorContentNode[];\\n childBlockIds?: string[];\\n};\\n\\nexport type EditorBlockDocument = {\\n documentId: string;\\n rootBlockIds: string[];\\n blocks: EditorBlock[];\\n};\\n\\ntype Json = unknown;\\n\\ntype LegacyBlockLike = {\\n id?: string;\\n blockId?: string;\\n type?: string;\\n blockType?: string;\\n content?: unknown;\\n contentNodes?: unknown;\\n props?: Record;\\n children?: unknown;\\n};\\n\\nconst EMPTY_DOC: TiptapDoc = {\\n type: \\\"doc\\\",\\n content: [{ type: \\\"paragraph\\\", attrs: {}, content: [] }],\\n};\\n\\nconst MARK_NAME_MAP: Record = {\\n bold: \\\"bold\\\",\\n italic: \\\"italic\\\",\\n underline: \\\"underline\\\",\\n strike: \\\"strike\\\",\\n code: \\\"code\\\",\\n};\\n\\nconst BLOCK_ID_ATTR = \\\"blockId\\\";\\n\\nfunction coerceTextMark(value: unknown): EditorTextMark | null {\\n const normalized = String(value ?? \\\"\\\").trim().toLowerCase();\\n return MARK_NAME_MAP[normalized] ?? null;\\n}\\n\\nfunction readMarks(marks: unknown): EditorTextMark[] {\\n if (!Array.isArray(marks)) return [];\\n const normalized = marks\\n .map((mark) => {\\n if (typeof mark === \\\"string\\\") return coerceTextMark(mark);\\n if (mark && typeof mark === \\\"object\\\") return coerceTextMark((mark as { type?: unknown }).type);\\n return null;\\n })\\n .filter((mark): mark is EditorTextMark => Boolean(mark));\\n return Array.from(new Set(normalized));\\n}\\n\\nfunction toText(value: unknown): string {\\n if (typeof value === \\\"string\\\") return value;\\n if (Array.isArray(value)) return value.map(toText).join(\\\"\\\");\\n if (value && typeof value === \\\"object\\\") {\\n const obj = value as { text?: unknown; content?: unknown; value?: unknown; contentNodes?: unknown };\\n return `${toText(obj.text)}${toText(obj.value)}${toText(obj.content)}${toText(obj.contentNodes)}`;\\n }\\n return \\\"\\\";\\n}\\n\\nfunction clampHeadingLevel(value: unknown): number {\\n const level = Number(value);\\n return Number.isInteger(level) && level >= 1 && level <= 6 ? level : 1;\\n}\\n\\nfunction normalizeBlockId(value: unknown, fallback: string): string {\\n const raw = typeof value === \\\"string\\\" ? value.trim() : \\\"\\\";\\n return raw || fallback;\\n}\\n\\nfunction normalizeEditorContentNodes(input: unknown): EditorContentNode[] {\\n if (Array.isArray(input)) {\\n const nodes = input.flatMap((item) => {\\n if (!item || typeof item !== \\\"object\\\") return [];\\n const record = item as { type?: unknown; text?: unknown; marks?: unknown; payload?: unknown };\\n if (record.type === \\\"text\\\") {\\n const text = toText(record.text);\\n return text ? [{ type: \\\"text\\\" as const, text, marks: readMarks(record.marks) }] : [];\\n }\\n if (record.payload && typeof record.payload === \\\"object\\\") {\\n const payload = record.payload as { type?: unknown; text?: unknown; marks?: unknown };\\n if (payload.type === \\\"text\\\") {\\n const text = toText(payload.text);\\n return text ? [{ type: \\\"text\\\" as const, text, marks: readMarks(payload.marks) }] : [];\\n }\\n }\\n const text = toText(item);\\n return text ? [{ type: \\\"text\\\" as const, text, marks: [] }] : [];\\n });\\n return nodes;\\n }\\n const text = toText(input);\\n return text ? [{ type: \\\"text\\\", text, marks: [] }] : [];\\n}\\n\\nfunction normalizeLegacyBlock(block: LegacyBlockLike, index: number): EditorBlock {\\n const blockType = String(block.blockType ?? block.type ?? \\\"paragraph\\\").trim().toLowerCase();\\n const blockId = normalizeBlockId(block.blockId ?? block.id, `block-${index + 1}`);\\n const props = block.props ?? {};\\n const contentNodes = normalizeEditorContentNodes(block.contentNodes ?? block.content ?? props.content ?? props.text ?? props.title);\\n\\n switch (blockType) {\\n case \\\"heading\\\":\\n return {\\n blockId,\\n blockType: \\\"heading\\\",\\n props: { headingLevel: clampHeadingLevel(props.level ?? props.headingLevel) },\\n contentNodes,\\n childBlockIds: [],\\n };\\n case \\\"bullet_list_item\\\":\\n case \\\"bullet_list\\\":\\n case \\\"bullet-list\\\":\\n return { blockId, blockType: \\\"bullet_list_item\\\", props: {}, contentNodes, childBlockIds: [] };\\n case \\\"numbered_list_item\\\":\\n case \\\"ordered_list\\\":\\n case \\\"ordered-list\\\":\\n return { blockId, blockType: \\\"numbered_list_item\\\", props: {}, contentNodes, childBlockIds: [] };\\n case \\\"todo\\\":\\n case \\\"task\\\":\\n return {\\n blockId,\\n blockType: \\\"todo\\\",\\n props: { checked: Boolean(props.checked) },\\n contentNodes,\\n childBlockIds: [],\\n };\\n case \\\"quote\\\":\\n case \\\"blockquote\\\":\\n return { blockId, blockType: \\\"quote\\\", props: {}, contentNodes, childBlockIds: [] };\\n case \\\"code\\\":\\n case \\\"code_block\\\":\\n case \\\"code-block\\\":\\n return {\\n blockId,\\n blockType: \\\"code_block\\\",\\n props: { language: typeof props.language === \\\"string\\\" ? props.language : null },\\n contentNodes,\\n childBlockIds: [],\\n };\\n case \\\"paragraph\\\":\\n case \\\"text\\\":\\n default:\\n return { blockId, blockType: \\\"paragraph\\\", props: {}, contentNodes, childBlockIds: [] };\\n }\\n}\\n\\nfunction normalizeInputToEditorDocument(value: Json): EditorBlockDocument {\\n if (value && typeof value === \\\"object\\\" && !Array.isArray(value)) {\\n const maybe = value as Partial;\\n if (typeof maybe.documentId === \\\"string\\\" && Array.isArray(maybe.blocks)) {\\n const normalizedBlocks = maybe.blocks.map((block, index) => normalizeLegacyBlock(block as LegacyBlockLike, index));\\n const rootBlockIds = Array.isArray(maybe.rootBlockIds) && maybe.rootBlockIds.length > 0\\n ? maybe.rootBlockIds.map((id, index) => normalizeBlockId(id, normalizedBlocks[index]?.blockId ?? `block-${index + 1}`))\\n : normalizedBlocks.map((block) => block.blockId);\\n return {\\n documentId: maybe.documentId,\\n rootBlockIds,\\n blocks: normalizedBlocks,\\n };\\n }\\n if (Array.isArray((value as { blocks?: unknown }).blocks)) {\\n const blocks = ((value as { blocks: unknown[] }).blocks ?? []).map((block, index) =>\\n normalizeLegacyBlock(block as LegacyBlockLike, index),\\n );\\n return {\\n documentId: typeof (value as { documentId?: unknown }).documentId === \\\"string\\\"\\n ? String((value as { documentId?: unknown }).documentId)\\n : \\\"document\\\",\\n rootBlockIds: blocks.map((block) => block.blockId),\\n blocks,\\n };\\n }\\n }\\n\\n const blocks = Array.isArray(value)\\n ? value.map((block, index) => normalizeLegacyBlock(block as LegacyBlockLike, index))\\n : [];\\n\\n return {\\n documentId: \\\"document\\\",\\n rootBlockIds: blocks.map((block) => block.blockId),\\n blocks,\\n };\\n}\\n\\nfunction textNodesToInline(contentNodes: EditorContentNode[] | undefined): TiptapNode[] {\\n return (contentNodes ?? []).flatMap((node) => {\\n const text = toText(node.text);\\n if (!text) return [];\\n const marks = (node.marks ?? [])\\n .map((mark) => ({ type: mark }))\\n .filter((mark) => Boolean(mark.type));\\n return [{ type: \\\"text\\\", text, marks: marks.length > 0 ? marks : undefined }];\\n });\\n}\\n\\nfunction blockToTiptapNode(block: EditorBlock): TiptapNode {\\n const commonAttrs: Record = { [BLOCK_ID_ATTR]: block.blockId };\\n switch (block.blockType) {\\n case \\\"heading\\\":\\n return {\\n type: \\\"heading\\\",\\n attrs: { ...commonAttrs, level: clampHeadingLevel(block.props?.headingLevel) },\\n content: textNodesToInline(block.contentNodes),\\n };\\n case \\\"bullet_list_item\\\":\\n return {\\n type: \\\"bulletList\\\",\\n attrs: commonAttrs,\\n content: [{ type: \\\"listItem\\\", attrs: commonAttrs, content: [{ type: \\\"paragraph\\\", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }] }],\\n };\\n case \\\"numbered_list_item\\\":\\n return {\\n type: \\\"orderedList\\\",\\n attrs: commonAttrs,\\n content: [{ type: \\\"listItem\\\", attrs: commonAttrs, content: [{ type: \\\"paragraph\\\", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }] }],\\n };\\n case \\\"todo\\\":\\n return {\\n type: \\\"taskList\\\",\\n attrs: commonAttrs,\\n content: [{ type: \\\"taskItem\\\", attrs: { ...commonAttrs, checked: Boolean(block.props?.checked) }, content: [{ type: \\\"paragraph\\\", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }] }],\\n };\\n case \\\"quote\\\":\\n return {\\n type: \\\"blockquote\\\",\\n attrs: commonAttrs,\\n content: [{ type: \\\"paragraph\\\", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }],\\n };\\n case \\\"code_block\\\":\\n return {\\n type: \\\"codeBlock\\\",\\n attrs: { ...commonAttrs, language: block.props?.language ?? null },\\n content: textNodesToInline(block.contentNodes),\\n };\\n case \\\"paragraph\\\":\\n default:\\n return {\\n type: \\\"paragraph\\\",\\n attrs: commonAttrs,\\n content: textNodesToInline(block.contentNodes),\\n };\\n }\\n}\\n\\nfunction extractInlineContent(node: TiptapNode | undefined): EditorContentNode[] {\\n if (!node) return [];\\n if (Array.isArray(node.content)) {\\n return node.content.flatMap((child) => {\\n if (child.type === \\\"text\\\") {\\n const text = toText(child.text);\\n if (!text) return [];\\n return [{ type: \\\"text\\\" as const, text, marks: readMarks(child.marks) }];\\n }\\n if (child.type === \\\"hardBreak\\\") {\\n return [{ type: \\\"text\\\" as const, text: \\\"\\\\n\\\", marks: [] }];\\n }\\n return extractInlineContent(child);\\n });\\n }\\n if (node.type === \\\"text\\\") {\\n const text = toText(node.text);\\n return text ? [{ type: \\\"text\\\", text, marks: readMarks(node.marks) }] : [];\\n }\\n return [];\\n}\\n\\nfunction firstChild(node: TiptapNode | undefined): TiptapNode | undefined {\\n return Array.isArray(node?.content) ? node?.content?.[0] : undefined;\\n}\\n\\nfunction nodeBlockId(node: TiptapNode, index: number): string {\\n return normalizeBlockId(node.attrs?.[BLOCK_ID_ATTR], `block-${index + 1}`);\\n}\\n\\nfunction tiptapNodeToBlock(node: TiptapNode, index: number): EditorBlock | null {\\n const blockId = nodeBlockId(node, index);\\n switch (node.type) {\\n case \\\"paragraph\\\":\\n return { blockId, blockType: \\\"paragraph\\\", props: {}, contentNodes: extractInlineContent(node), childBlockIds: [] };\\n case \\\"heading\\\":\\n return {\\n blockId,\\n blockType: \\\"heading\\\",\\n props: { headingLevel: clampHeadingLevel(node.attrs?.level) },\\n contentNodes: extractInlineContent(node),\\n childBlockIds: [],\\n };\\n case \\\"bulletList\\\": {\\n const paragraph = firstChild(firstChild(node));\\n return {\\n blockId,\\n blockType: \\\"bullet_list_item\\\",\\n props: {},\\n contentNodes: extractInlineContent(paragraph),\\n childBlockIds: [],\\n };\\n }\\n case \\\"orderedList\\\": {\\n const paragraph = firstChild(firstChild(node));\\n return {\\n blockId,\\n blockType: \\\"numbered_list_item\\\",\\n props: {},\\n contentNodes: extractInlineContent(paragraph),\\n childBlockIds: [],\\n };\\n }\\n case \\\"taskList\\\": {\\n const taskItem = firstChild(node);\\n const paragraph = firstChild(taskItem);\\n return {\\n blockId,\\n blockType: \\\"todo\\\",\\n props: { checked: Boolean(taskItem?.attrs?.checked) },\\n contentNodes: extractInlineContent(paragraph),\\n childBlockIds: [],\\n };\\n }\\n case \\\"blockquote\\\":\\n return { blockId, blockType: \\\"quote\\\", props: {}, contentNodes: extractInlineContent(firstChild(node)), childBlockIds: [] };\\n case \\\"codeBlock\\\":\\n return {\\n blockId,\\n blockType: \\\"code_block\\\",\\n props: { language: typeof node.attrs?.language === \\\"string\\\" ? String(node.attrs.language) : null },\\n contentNodes: extractInlineContent(node),\\n childBlockIds: [],\\n };\\n default:\\n return null;\\n }\\n}\\n\\nexport function editorBlockDocumentFromContent(content: Json): EditorBlockDocument {\\n return normalizeInputToEditorDocument(content);\\n}\\n\\nexport function tiptapDocFromEditorBlockDocument(document: EditorBlockDocument): TiptapDoc {\\n const blockById = new Map(document.blocks.map((block) => [block.blockId, block]));\\n const nodes = document.rootBlockIds\\n .map((blockId) => blockById.get(blockId))\\n .filter((block): block is EditorBlock => Boolean(block))\\n .map(blockToTiptapNode);\\n return {\\n type: \\\"doc\\\",\\n content: nodes.length > 0 ? nodes : EMPTY_DOC.content,\\n };\\n}\\n\\nexport function editorBlockDocumentFromTiptapDoc(doc: unknown, documentId = \\\"document\\\"): EditorBlockDocument {\\n if (!doc || typeof doc !== \\\"object\\\") {\\n return { documentId, rootBlockIds: [], blocks: [] };\\n }\\n const root = doc as { type?: unknown; content?: unknown };\\n if (root.type !== \\\"doc\\\" || !Array.isArray(root.content)) {\\n return { documentId, rootBlockIds: [], blocks: [] };\\n }\\n const blocks = root.content.flatMap((node, index) => {\\n if (!node || typeof node !== \\\"object\\\") return [];\\n const block = tiptapNodeToBlock(node as TiptapNode, index);\\n return block ? [block] : [];\\n });\\n return {\\n documentId,\\n rootBlockIds: blocks.map((block) => block.blockId),\\n blocks,\\n };\\n}\\n\\nexport function tiptapDocFromBlocks(blocks: Json): TiptapDoc {\\n return tiptapDocFromEditorBlockDocument(editorBlockDocumentFromContent(blocks));\\n}\\n\\nexport function blocksFromTiptapDoc(doc: unknown): Json {\\n return editorBlockDocumentFromTiptapDoc(doc).blocks.map((block) => ({\\n id: block.blockId,\\n type: block.blockType,\\n props:\\n block.blockType === \\\"heading\\\"\\n ? { level: block.props?.headingLevel ?? 1 }\\n : block.blockType === \\\"todo\\\"\\n ? { checked: Boolean(block.props?.checked) }\\n : block.blockType === \\\"code_block\\\"\\n ? { language: block.props?.language ?? null }\\n : undefined,\\n content: (block.contentNodes ?? []).map((node) => node.text).join(\\\"\\\"),\\n }));\\n}\\n\\nexport const blocksToTiptapDoc = tiptapDocFromBlocks;\\nexport const tiptapDocToBlocks = blocksFromTiptapDoc;\\n\",\n \"structuredPatch\": [\n {\n \"oldStart\": 1,\n \"oldLines\": 92,\n \"newStart\": 1,\n \"newLines\": 441,\n \"lines\": [\n \"-export type TiptapNode = {\",\n \"- type: string;\",\n \"- attrs?: Record;\",\n \"- content?: TiptapNode[];\",\n \"- text?: string;\",\n \"- marks?: Array<{ type: string; attrs?: Record }>;\",\n \"-};\",\n \"-\",\n \"-export type TiptapDoc = {\",\n \"- type: \\\"doc\\\";\",\n \"- content: TiptapNode[];\",\n \"-};\",\n \"-\",\n \"-type Json = unknown;\",\n \"-\",\n \"-type BlockLike = {\",\n \"- id?: string;\",\n \"- type?: string;\",\n \"- content?: unknown;\",\n \"- props?: Record;\",\n \"- children?: unknown;\",\n \"-};\",\n \"-\",\n \"-const emptyDoc: TiptapDoc = { type: \\\"doc\\\", content: [] };\",\n \"-\",\n \"-const toText = (value: unknown): string => {\",\n \"- if (typeof value === \\\"string\\\") return value;\",\n \"- if (Array.isArray(value)) return value.map(toText).join(\\\"\\\");\",\n \"- if (value && typeof value === \\\"object\\\") {\",\n \"- const obj = value as { text?: unknown; content?: unknown; value?: unknown };\",\n \"- return `${toText(obj.text)}${toText(obj.value)}${toText(obj.content)}`;\",\n \"- }\",\n \"- return \\\"\\\";\",\n \"-};\",\n \"-\",\n \"-const toTiptapInline = (value: unknown): TiptapNode[] => {\",\n \"- const text = toText(value);\",\n \"- return text ? [{ type: \\\"text\\\", text }] : [];\",\n \"-};\",\n \"-\",\n \"-const toHeadingLevel = (value: unknown): number => {\",\n \"- const level = Number(value);\",\n \"- return Number.isInteger(level) && level >= 1 && level <= 6 ? level : 1;\",\n \"-};\",\n \"-\",\n \"-const blockToNode = (block: BlockLike): TiptapNode | null => {\",\n \"- const type = String(block.type ?? \\\"\\\").toLowerCase();\",\n \"- const content = block.content ?? block.props?.content ?? block.props?.text ?? block.props?.title;\",\n \"- if (type === \\\"paragraph\\\" || type === \\\"text\\\") return { type: \\\"paragraph\\\", content: toTiptapInline(content) };\",\n \"- if (type === \\\"heading\\\") return { type: \\\"heading\\\", attrs: { level: toHeadingLevel(block.props?.level) }, content: toTiptapInline(content) };\",\n \"- if (type === \\\"bullet_list\\\" || type === \\\"bullet-list\\\") return { type: \\\"bulletList\\\", content: [] };\",\n \"- if (type === \\\"ordered_list\\\" || type === \\\"ordered-list\\\") return { type: \\\"orderedList\\\", content: [] };\",\n \"- if (type === \\\"todo\\\" || type === \\\"task\\\") return { type: \\\"taskItem\\\", attrs: { checked: Boolean(block.props?.checked) }, content: toTiptapInline(content) };\",\n \"- if (type === \\\"quote\\\" || type === \\\"blockquote\\\") return { type: \\\"blockquote\\\", content: [{ type: \\\"paragraph\\\", content: toTiptapInline(content) }] };\",\n \"- if (type === \\\"code\\\" || type === \\\"code_block\\\" || type === \\\"code-block\\\") return { type: \\\"codeBlock\\\", content: toTiptapInline(content) };\",\n \"- return null;\",\n \"-};\",\n \"-\",\n \"-export function blocksToTiptapDoc(blocks: Json): TiptapDoc {\",\n \"- if (!Array.isArray(blocks) || blocks.length === 0) return { ...emptyDoc };\",\n \"- const content = blocks.flatMap((item) => {\",\n \"- if (!item || typeof item !== \\\"object\\\") return [];\",\n \"- const node = blockToNode(item as BlockLike);\",\n \"- return node ? [node] : [];\",\n \"- });\",\n \"- return { type: \\\"doc\\\", content: content.length > 0 ? content : [{ type: \\\"paragraph\\\" }] };\",\n \"-}\",\n \"-\",\n \"-const nodeToBlock = (node: TiptapNode, index: number): Record | null => {\",\n \"- if (node.type === \\\"paragraph\\\") return { id: `block-${index}`, type: \\\"paragraph\\\", content: toText(node.content) };\",\n \"- if (node.type === \\\"heading\\\") return { id: `block-${index}`, type: \\\"heading\\\", props: { level: Number(node.attrs?.level ?? 1) }, content: toText(node.content) };\",\n \"- if (node.type === \\\"bulletList\\\") return { id: `block-${index}`, type: \\\"bullet_list\\\" };\",\n \"- if (node.type === \\\"orderedList\\\") return { id: `block-${index}`, type: \\\"ordered_list\\\" };\",\n \"- if (node.type === \\\"taskItem\\\") return { id: `block-${index}`, type: \\\"todo\\\", props: { checked: Boolean(node.attrs?.checked) }, content: toText(node.content) };\",\n \"- if (node.type === \\\"blockquote\\\") return { id: `block-${index}`, type: \\\"quote\\\", content: toText(node.content) };\",\n \"- if (node.type === \\\"codeBlock\\\") return { id: `block-${index}`, type: \\\"code_block\\\", content: toText(node.content) };\",\n \"- return null;\",\n \"-};\",\n \"-\",\n \"-export function tiptapDocToBlocks(doc: unknown): Json {\",\n \"- if (!doc || typeof doc !== \\\"object\\\") return [];\",\n \"- const root = doc as { type?: string; content?: TiptapNode[] };\",\n \"- if (root.type !== \\\"doc\\\" || !Array.isArray(root.content) || root.content.length === 0) return [];\",\n \"- return root.content.flatMap((node, index) => {\",\n \"- if (!node || typeof node !== \\\"object\\\") return [];\",\n \"- const block = nodeToBlock(node, index);\",\n \"- return block ? [block] : [];\",\n \"- }) as Json;\",\n \"-}\",\n \"-\",\n \"-export const tiptapDocFromBlocks = blocksToTiptapDoc;\",\n \"-export const blocksFromTiptapDoc = tiptapDocToBlocks;\",\n \"+export type TiptapMark = {\",\n \"+ type: string;\",\n \"+ attrs?: Record;\",\n \"+};\",\n \"+\",\n \"+export type TiptapNode = {\",\n \"+ type: string;\",\n \"+ attrs?: Record;\",\n \"+ content?: TiptapNode[];\",\n \"+ text?: string;\",\n \"+ marks?: TiptapMark[];\",\n \"+};\",\n \"+\",\n \"+export type TiptapDoc = {\",\n \"+ type: \\\"doc\\\";\",\n \"+ content: TiptapNode[];\",\n \"+};\",\n \"+\",\n \"+export type EditorTextMark = \\\"bold\\\" | \\\"italic\\\" | \\\"underline\\\" | \\\"strike\\\" | \\\"code\\\";\",\n \"+\",\n \"+export type EditorBlockType =\",\n \"+ | \\\"paragraph\\\"\",\n \"+ | \\\"heading\\\"\",\n \"+ | \\\"bullet_list_item\\\"\",\n \"+ | \\\"numbered_list_item\\\"\",\n \"+ | \\\"todo\\\"\",\n \"+ | \\\"quote\\\"\",\n \"+ | \\\"code_block\\\";\",\n \"+\",\n \"+export type EditorContentNode = {\",\n \"+ type: \\\"text\\\";\",\n \"+ text: string;\",\n \"+ marks?: EditorTextMark[];\",\n \"+};\",\n \"+\",\n \"+export type EditorBlock = {\",\n \"+ blockId: string;\",\n \"+ blockType: EditorBlockType;\",\n \"+ props?: {\",\n \"+ headingLevel?: number | null;\",\n \"+ checked?: boolean | null;\",\n \"+ language?: string | null;\",\n \"+ };\",\n \"+ contentNodes?: EditorContentNode[];\",\n \"+ childBlockIds?: string[];\",\n \"+};\",\n \"+\",\n \"+export type EditorBlockDocument = {\",\n \"+ documentId: string;\",\n \"+ rootBlockIds: string[];\",\n \"+ blocks: EditorBlock[];\",\n \"+};\",\n \"+\",\n \"+type Json = unknown;\",\n \"+\",\n \"+type LegacyBlockLike = {\",\n \"+ id?: string;\",\n \"+ blockId?: string;\",\n \"+ type?: string;\",\n \"+ blockType?: string;\",\n \"+ content?: unknown;\",\n \"+ contentNodes?: unknown;\",\n \"+ props?: Record;\",\n \"+ children?: unknown;\",\n \"+};\",\n \"+\",\n \"+const EMPTY_DOC: TiptapDoc = {\",\n \"+ type: \\\"doc\\\",\",\n \"+ content: [{ type: \\\"paragraph\\\", attrs: {}, content: [] }],\",\n \"+};\",\n \"+\",\n \"+const MARK_NAME_MAP: Record = {\",\n \"+ bold: \\\"bold\\\",\",\n \"+ italic: \\\"italic\\\",\",\n \"+ underline: \\\"underline\\\",\",\n \"+ strike: \\\"strike\\\",\",\n \"+ code: \\\"code\\\",\",\n \"+};\",\n \"+\",\n \"+const BLOCK_ID_ATTR = \\\"blockId\\\";\",\n \"+\",\n \"+function coerceTextMark(value: unknown): EditorTextMark | null {\",\n \"+ const normalized = String(value ?? \\\"\\\").trim().toLowerCase();\",\n \"+ return MARK_NAME_MAP[normalized] ?? null;\",\n \"+}\",\n \"+\",\n \"+function readMarks(marks: unknown): EditorTextMark[] {\",\n \"+ if (!Array.isArray(marks)) return [];\",\n \"+ const normalized = marks\",\n \"+ .map((mark) => {\",\n \"+ if (typeof mark === \\\"string\\\") return coerceTextMark(mark);\",\n \"+ if (mark && typeof mark === \\\"object\\\") return coerceTextMark((mark as { type?: unknown }).type);\",\n \"+ return null;\",\n \"+ })\",\n \"+ .filter((mark): mark is EditorTextMark => Boolean(mark));\",\n \"+ return Array.from(new Set(normalized));\",\n \"+}\",\n \"+\",\n \"+function toText(value: unknown): string {\",\n \"+ if (typeof value === \\\"string\\\") return value;\",\n \"+ if (Array.isArray(value)) return value.map(toText).join(\\\"\\\");\",\n \"+ if (value && typeof value === \\\"object\\\") {\",\n \"+ const obj = value as { text?: unknown; content?: unknown; value?: unknown; contentNodes?: unknown };\",\n \"+ return `${toText(obj.text)}${toText(obj.value)}${toText(obj.content)}${toText(obj.contentNodes)}`;\",\n \"+ }\",\n \"+ return \\\"\\\";\",\n \"+}\",\n \"+\",\n \"+function clampHeadingLevel(value: unknown): number {\",\n \"+ const level = Number(value);\",\n \"+ return Number.isInteger(level) && level >= 1 && level <= 6 ? level : 1;\",\n \"+}\",\n \"+\",\n \"+function normalizeBlockId(value: unknown, fallback: string): string {\",\n \"+ const raw = typeof value === \\\"string\\\" ? value.trim() : \\\"\\\";\",\n \"+ return raw || fallback;\",\n \"+}\",\n \"+\",\n \"+function normalizeEditorContentNodes(input: unknown): EditorContentNode[] {\",\n \"+ if (Array.isArray(input)) {\",\n \"+ const nodes = input.flatMap((item) => {\",\n \"+ if (!item || typeof item !== \\\"object\\\") return [];\",\n \"+ const record = item as { type?: unknown; text?: unknown; marks?: unknown; payload?: unknown };\",\n \"+ if (record.type === \\\"text\\\") {\",\n \"+ const text = toText(record.text);\",\n \"+ return text ? [{ type: \\\"text\\\" as const, text, marks: readMarks(record.marks) }] : [];\",\n \"+ }\",\n \"+ if (record.payload && typeof record.payload === \\\"object\\\") {\",\n \"+ const payload = record.payload as { type?: unknown; text?: unknown; marks?: unknown };\",\n \"+ if (payload.type === \\\"text\\\") {\",\n \"+ const text = toText(payload.text);\",\n \"+ return text ? [{ type: \\\"text\\\" as const, text, marks: readMarks(payload.marks) }] : [];\",\n \"+ }\",\n \"+ }\",\n \"+ const text = toText(item);\",\n \"+ return text ? [{ type: \\\"text\\\" as const, text, marks: [] }] : [];\",\n \"+ });\",\n \"+ return nodes;\",\n \"+ }\",\n \"+ const text = toText(input);\",\n \"+ return text ? [{ type: \\\"text\\\", text, marks: [] }] : [];\",\n \"+}\",\n \"+\",\n \"+function normalizeLegacyBlock(block: LegacyBlockLike, index: number): EditorBlock {\",\n \"+ const blockType = String(block.blockType ?? block.type ?? \\\"paragraph\\\").trim().toLowerCase();\",\n \"+ const blockId = normalizeBlockId(block.blockId ?? block.id, `block-${index + 1}`);\",\n \"+ const props = block.props ?? {};\",\n \"+ const contentNodes = normalizeEditorContentNodes(block.contentNodes ?? block.content ?? props.content ?? props.text ?? props.title);\",\n \"+\",\n \"+ switch (blockType) {\",\n \"+ case \\\"heading\\\":\",\n \"+ return {\",\n \"+ blockId,\",\n \"+ blockType: \\\"heading\\\",\",\n \"+ props: { headingLevel: clampHeadingLevel(props.level ?? props.headingLevel) },\",\n \"+ contentNodes,\",\n \"+ childBlockIds: [],\",\n \"+ };\",\n \"+ case \\\"bullet_list_item\\\":\",\n \"+ case \\\"bullet_list\\\":\",\n \"+ case \\\"bullet-list\\\":\",\n \"+ return { blockId, blockType: \\\"bullet_list_item\\\", props: {}, contentNodes, childBlockIds: [] };\",\n \"+ case \\\"numbered_list_item\\\":\",\n \"+ case \\\"ordered_list\\\":\",\n \"+ case \\\"ordered-list\\\":\",\n \"+ return { blockId, blockType: \\\"numbered_list_item\\\", props: {}, contentNodes, childBlockIds: [] };\",\n \"+ case \\\"todo\\\":\",\n \"+ case \\\"task\\\":\",\n \"+ return {\",\n \"+ blockId,\",\n \"+ blockType: \\\"todo\\\",\",\n \"+ props: { checked: Boolean(props.checked) },\",\n \"+ contentNodes,\",\n \"+ childBlockIds: [],\",\n \"+ };\",\n \"+ case \\\"quote\\\":\",\n \"+ case \\\"blockquote\\\":\",\n \"+ return { blockId, blockType: \\\"quote\\\", props: {}, contentNodes, childBlockIds: [] };\",\n \"+ case \\\"code\\\":\",\n \"+ case \\\"code_block\\\":\",\n \"+ case \\\"code-block\\\":\",\n \"+ return {\",\n \"+ blockId,\",\n \"+ blockType: \\\"code_block\\\",\",\n \"+ props: { language: typeof props.language === \\\"string\\\" ? props.language : null },\",\n \"+ contentNodes,\",\n \"+ childBlockIds: [],\",\n \"+ };\",\n \"+ case \\\"paragraph\\\":\",\n \"+ case \\\"text\\\":\",\n \"+ default:\",\n \"+ return { blockId, blockType: \\\"paragraph\\\", props: {}, contentNodes, childBlockIds: [] };\",\n \"+ }\",\n \"+}\",\n \"+\",\n \"+function normalizeInputToEditorDocument(value: Json): EditorBlockDocument {\",\n \"+ if (value && typeof value === \\\"object\\\" && !Array.isArray(value)) {\",\n \"+ const maybe = value as Partial;\",\n \"+ if (typeof maybe.documentId === \\\"string\\\" && Array.isArray(maybe.blocks)) {\",\n \"+ const normalizedBlocks = maybe.blocks.map((block, index) => normalizeLegacyBlock(block as LegacyBlockLike, index));\",\n \"+ const rootBlockIds = Array.isArray(maybe.rootBlockIds) && maybe.rootBlockIds.length > 0\",\n \"+ ? maybe.rootBlockIds.map((id, index) => normalizeBlockId(id, normalizedBlocks[index]?.blockId ?? `block-${index + 1}`))\",\n \"+ : normalizedBlocks.map((block) => block.blockId);\",\n \"+ return {\",\n \"+ documentId: maybe.documentId,\",\n \"+ rootBlockIds,\",\n \"+ blocks: normalizedBlocks,\",\n \"+ };\",\n \"+ }\",\n \"+ if (Array.isArray((value as { blocks?: unknown }).blocks)) {\",\n \"+ const blocks = ((value as { blocks: unknown[] }).blocks ?? []).map((block, index) =>\",\n \"+ normalizeLegacyBlock(block as LegacyBlockLike, index),\",\n \"+ );\",\n \"+ return {\",\n \"+ documentId: typeof (value as { documentId?: unknown }).documentId === \\\"string\\\"\",\n \"+ ? String((value as { documentId?: unknown }).documentId)\",\n \"+ : \\\"document\\\",\",\n \"+ rootBlockIds: blocks.map((block) => block.blockId),\",\n \"+ blocks,\",\n \"+ };\",\n \"+ }\",\n \"+ }\",\n \"+\",\n \"+ const blocks = Array.isArray(value)\",\n \"+ ? value.map((block, index) => normalizeLegacyBlock(block as LegacyBlockLike, index))\",\n \"+ : [];\",\n \"+\",\n \"+ return {\",\n \"+ documentId: \\\"document\\\",\",\n \"+ rootBlockIds: blocks.map((block) => block.blockId),\",\n \"+ blocks,\",\n \"+ };\",\n \"+}\",\n \"+\",\n \"+function textNodesToInline(contentNodes: EditorContentNode[] | undefined): TiptapNode[] {\",\n \"+ return (contentNodes ?? []).flatMap((node) => {\",\n \"+ const text = toText(node.text);\",\n \"+ if (!text) return [];\",\n \"+ const marks = (node.marks ?? [])\",\n \"+ .map((mark) => ({ type: mark }))\",\n \"+ .filter((mark) => Boolean(mark.type));\",\n \"+ return [{ type: \\\"text\\\", text, marks: marks.length > 0 ? marks : undefined }];\",\n \"+ });\",\n \"+}\",\n \"+\",\n \"+function blockToTiptapNode(block: EditorBlock): TiptapNode {\",\n \"+ const commonAttrs: Record = { [BLOCK_ID_ATTR]: block.blockId };\",\n \"+ switch (block.blockType) {\",\n \"+ case \\\"heading\\\":\",\n \"+ return {\",\n \"+ type: \\\"heading\\\",\",\n \"+ attrs: { ...commonAttrs, level: clampHeadingLevel(block.props?.headingLevel) },\",\n \"+ content: textNodesToInline(block.contentNodes),\",\n \"+ };\",\n \"+ case \\\"bullet_list_item\\\":\",\n \"+ return {\",\n \"+ type: \\\"bulletList\\\",\",\n \"+ attrs: commonAttrs,\",\n \"+ content: [{ type: \\\"listItem\\\", attrs: commonAttrs, content: [{ type: \\\"paragraph\\\", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }] }],\",\n \"+ };\",\n \"+ case \\\"numbered_list_item\\\":\",\n \"+ return {\",\n \"+ type: \\\"orderedList\\\",\",\n \"+ attrs: commonAttrs,\",\n \"+ content: [{ type: \\\"listItem\\\", attrs: commonAttrs, content: [{ type: \\\"paragraph\\\", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }] }],\",\n \"+ };\",\n \"+ case \\\"todo\\\":\",\n \"+ return {\",\n \"+ type: \\\"taskList\\\",\",\n \"+ attrs: commonAttrs,\",\n \"+ content: [{ type: \\\"taskItem\\\", attrs: { ...commonAttrs, checked: Boolean(block.props?.checked) }, content: [{ type: \\\"paragraph\\\", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }] }],\",\n \"+ };\",\n \"+ case \\\"quote\\\":\",\n \"+ return {\",\n \"+ type: \\\"blockquote\\\",\",\n \"+ attrs: commonAttrs,\",\n \"+ content: [{ type: \\\"paragraph\\\", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }],\",\n \"+ };\",\n \"+ case \\\"code_block\\\":\",\n \"+ return {\",\n \"+ type: \\\"codeBlock\\\",\",\n \"+ attrs: { ...commonAttrs, language: block.props?.language ?? null },\",\n \"+ content: textNodesToInline(block.contentNodes),\",\n \"+ };\",\n \"+ case \\\"paragraph\\\":\",\n \"+ default:\",\n \"+ return {\",\n \"+ type: \\\"paragraph\\\",\",\n \"+ attrs: commonAttrs,\",\n \"+ content: textNodesToInline(block.contentNodes),\",\n \"+ };\",\n \"+ }\",\n \"+}\",\n \"+\",\n \"+function extractInlineContent(node: TiptapNode | undefined): EditorContentNode[] {\",\n \"+ if (!node) return [];\",\n \"+ if (Array.isArray(node.content)) {\",\n \"+ return node.content.flatMap((child) => {\",\n \"+ if (child.type === \\\"text\\\") {\",\n \"+ const text = toText(child.text);\",\n \"+ if (!text) return [];\",\n \"+ return [{ type: \\\"text\\\" as const, text, marks: readMarks(child.marks) }];\",\n \"+ }\",\n \"+ if (child.type === \\\"hardBreak\\\") {\",\n \"+ return [{ type: \\\"text\\\" as const, text: \\\"\\\\n\\\", marks: [] }];\",\n \"+ }\",\n \"+ return extractInlineContent(child);\",\n \"+ });\",\n \"+ }\",\n \"+ if (node.type === \\\"text\\\") {\",\n \"+ const text = toText(node.text);\",\n \"+ return text ? [{ type: \\\"text\\\", text, marks: readMarks(node.marks) }] : [];\",\n \"+ }\",\n \"+ return [];\",\n \"+}\",\n \"+\",\n \"+function firstChild(node: TiptapNode | undefined): TiptapNode | undefined {\",\n \"+ return Array.isArray(node?.content) ? node?.content?.[0] : undefined;\",\n \"+}\",\n \"+\",\n \"+function nodeBlockId(node: TiptapNode, index: number): string {\",\n \"+ return normalizeBlockId(node.attrs?.[BLOCK_ID_ATTR], `block-${index + 1}`);\",\n \"+}\",\n \"+\",\n \"+function tiptapNodeToBlock(node: TiptapNode, index: number): EditorBlock | null {\",\n \"+ const blockId = nodeBlockId(node, index);\",\n \"+ switch (node.type) {\",\n \"+ case \\\"paragraph\\\":\",\n \"+ return { blockId, blockType: \\\"paragraph\\\", props: {}, contentNodes: extractInlineContent(node), childBlockIds: [] };\",\n \"+ case \\\"heading\\\":\",\n \"+ return {\",\n \"+ blockId,\",\n \"+ blockType: \\\"heading\\\",\",\n \"+ props: { headingLevel: clampHeadingLevel(node.attrs?.level) },\",\n \"+ contentNodes: extractInlineContent(node),\",\n \"+ childBlockIds: [],\",\n \"+ };\",\n \"+ case \\\"bulletList\\\": {\",\n \"+ const paragraph = firstChild(firstChild(node));\",\n \"+ return {\",\n \"+ blockId,\",\n \"+ blockType: \\\"bullet_list_item\\\",\",\n \"+ props: {},\",\n \"+ contentNodes: extractInlineContent(paragraph),\",\n \"+ childBlockIds: [],\",\n \"+ };\",\n \"+ }\",\n \"+ case \\\"orderedList\\\": {\",\n \"+ const paragraph = firstChild(firstChild(node));\",\n \"+ return {\",\n \"+ blockId,\",\n \"+ blockType: \\\"numbered_list_item\\\",\",\n \"+ props: {},\",\n \"+ contentNodes: extractInlineContent(paragraph),\",\n \"+ childBlockIds: [],\",\n \"+ };\",\n \"+ }\",\n \"+ case \\\"taskList\\\": {\",\n \"+ const taskItem = firstChild(node);\",\n \"+ const paragraph = firstChild(taskItem);\",\n \"+ return {\",\n \"+ blockId,\",\n \"+ blockType: \\\"todo\\\",\",\n \"+ props: { checked: Boolean(taskItem?.attrs?.checked) },\",\n \"+ contentNodes: extractInlineContent(paragraph),\",\n \"+ childBlockIds: [],\",\n \"+ };\",\n \"+ }\",\n \"+ case \\\"blockquote\\\":\",\n \"+ return { blockId, blockType: \\\"quote\\\", props: {}, contentNodes: extractInlineContent(firstChild(node)), childBlockIds: [] };\",\n \"+ case \\\"codeBlock\\\":\",\n \"+ return {\",\n \"+ blockId,\",\n \"+ blockType: \\\"code_block\\\",\",\n \"+ props: { language: typeof node.attrs?.language === \\\"string\\\" ? String(node.attrs.language) : null },\",\n \"+ contentNodes: extractInlineContent(node),\",\n \"+ childBlockIds: [],\",\n \"+ };\",\n \"+ default:\",\n \"+ return null;\",\n \"+ }\",\n \"+}\",\n \"+\",\n \"+export function editorBlockDocumentFromContent(content: Json): EditorBlockDocument {\",\n \"+ return normalizeInputToEditorDocument(content);\",\n \"+}\",\n \"+\",\n \"+export function tiptapDocFromEditorBlockDocument(document: EditorBlockDocument): TiptapDoc {\",\n \"+ const blockById = new Map(document.blocks.map((block) => [block.blockId, block]));\",\n \"+ const nodes = document.rootBlockIds\",\n \"+ .map((blockId) => blockById.get(blockId))\",\n \"+ .filter((block): block is EditorBlock => Boolean(block))\",\n \"+ .map(blockToTiptapNode);\",\n \"+ return {\",\n \"+ type: \\\"doc\\\",\",\n \"+ content: nodes.length > 0 ? nodes : EMPTY_DOC.content,\",\n \"+ };\",\n \"+}\",\n \"+\",\n \"+export function editorBlockDocumentFromTiptapDoc(doc: unknown, documentId = \\\"document\\\"): EditorBlockDocument {\",\n \"+ if (!doc || typeof doc !== \\\"object\\\") {\",\n \"+ return { documentId, rootBlockIds: [], blocks: [] };\",\n \"+ }\",\n \"+ const root = doc as { type?: unknown; content?: unknown };\",\n \"+ if (root.type !== \\\"doc\\\" || !Array.isArray(root.content)) {\",\n \"+ return { documentId, rootBlockIds: [], blocks: [] };\",\n \"+ }\",\n \"+ const blocks = root.content.flatMap((node, index) => {\",\n \"+ if (!node || typeof node !== \\\"object\\\") return [];\",\n \"+ const block = tiptapNodeToBlock(node as TiptapNode, index);\",\n \"+ return block ? [block] : [];\",\n \"+ });\",\n \"+ return {\",\n \"+ documentId,\",\n \"+ rootBlockIds: blocks.map((block) => block.blockId),\",\n \"+ blocks,\",\n \"+ };\",\n \"+}\",\n \"+\",\n \"+export function tiptapDocFromBlocks(blocks: Json): TiptapDoc {\",\n \"+ return tiptapDocFromEditorBlockDocument(editorBlockDocumentFromContent(blocks));\",\n \"+}\",\n \"+\",\n \"+export function blocksFromTiptapDoc(doc: unknown): Json {\",\n \"+ return editorBlockDocumentFromTiptapDoc(doc).blocks.map((block) => ({\",\n \"+ id: block.blockId,\",\n \"+ type: block.blockType,\",\n \"+ props:\",\n \"+ block.blockType === \\\"heading\\\"\",\n \"+ ? { level: block.props?.headingLevel ?? 1 }\",\n \"+ : block.blockType === \\\"todo\\\"\",\n \"+ ? { checked: Boolean(block.props?.checked) }\",\n \"+ : block.blockType === \\\"code_block\\\"\",\n \"+ ? { language: block.props?.language ?? null }\",\n \"+ : undefined,\",\n \"+ content: (block.contentNodes ?? []).map((node) => node.text).join(\\\"\\\"),\",\n \"+ }));\",\n \"+}\",\n \"+\",\n \"+export const blocksToTiptapDoc = tiptapDocFromBlocks;\",\n \"+export const tiptapDocToBlocks = blocksFromTiptapDoc;\"\n ]\n }\n ],\n \"originalFile\": \"export type TiptapNode = {\\n type: string;\\n attrs?: Record;\\n content?: TiptapNode[];\\n text?: string;\\n marks?: Array<{ type: string; attrs?: Record }>;\\n};\\n\\nexport type TiptapDoc = {\\n type: \\\"doc\\\";\\n content: TiptapNode[];\\n};\\n\\ntype Json = unknown;\\n\\ntype BlockLike = {\\n id?: string;\\n type?: string;\\n content?: unknown;\\n props?: Record;\\n children?: unknown;\\n};\\n\\nconst emptyDoc: TiptapDoc = { type: \\\"doc\\\", content: [] };\\n\\nconst toText = (value: unknown): string => {\\n if (typeof value === \\\"string\\\") return value;\\n if (Array.isArray(value)) return value.map(toText).join(\\\"\\\");\\n if (value && typeof value === \\\"object\\\") {\\n const obj = value as { text?: unknown; content?: unknown; value?: unknown };\\n return `${toText(obj.text)}${toText(obj.value)}${toText(obj.content)}`;\\n }\\n return \\\"\\\";\\n};\\n\\nconst toTiptapInline = (value: unknown): TiptapNode[] => {\\n const text = toText(value);\\n return text ? [{ type: \\\"text\\\", text }] : [];\\n};\\n\\nconst toHeadingLevel = (value: unknown): number => {\\n const level = Number(value);\\n return Number.isInteger(level) && level >= 1 && level <= 6 ? level : 1;\\n};\\n\\nconst blockToNode = (block: BlockLike): TiptapNode | null => {\\n const type = String(block.type ?? \\\"\\\").toLowerCase();\\n const content = block.content ?? block.props?.content ?? block.props?.text ?? block.props?.title;\\n if (type === \\\"paragraph\\\" || type === \\\"text\\\") return { type: \\\"paragraph\\\", content: toTiptapInline(content) };\\n if (type === \\\"heading\\\") return { type: \\\"heading\\\", attrs: { level: toHeadingLevel(block.props?.level) }, content: toTiptapInline(content) };\\n if (type === \\\"bullet_list\\\" || type === \\\"bullet-list\\\") return { type: \\\"bulletList\\\", content: [] };\\n if (type === \\\"ordered_list\\\" || type === \\\"ordered-list\\\") return { type: \\\"orderedList\\\", content: [] };\\n if (type === \\\"todo\\\" || type === \\\"task\\\") return { type: \\\"taskItem\\\", attrs: { checked: Boolean(block.props?.checked) }, content: toTiptapInline(content) };\\n if (type === \\\"quote\\\" || type === \\\"blockquote\\\") return { type: \\\"blockquote\\\", content: [{ type: \\\"paragraph\\\", content: toTiptapInline(content) }] };\\n if (type === \\\"code\\\" || type === \\\"code_block\\\" || type === \\\"code-block\\\") return { type: \\\"codeBlock\\\", content: toTiptapInline(content) };\\n return null;\\n};\\n\\nexport function blocksToTiptapDoc(blocks: Json): TiptapDoc {\\n if (!Array.isArray(blocks) || blocks.length === 0) return { ...emptyDoc };\\n const content = blocks.flatMap((item) => {\\n if (!item || typeof item !== \\\"object\\\") return [];\\n const node = blockToNode(item as BlockLike);\\n return node ? [node] : [];\\n });\\n return { type: \\\"doc\\\", content: content.length > 0 ? content : [{ type: \\\"paragraph\\\" }] };\\n}\\n\\nconst nodeToBlock = (node: TiptapNode, index: number): Record | null => {\\n if (node.type === \\\"paragraph\\\") return { id: `block-${index}`, type: \\\"paragraph\\\", content: toText(node.content) };\\n if (node.type === \\\"heading\\\") return { id: `block-${index}`, type: \\\"heading\\\", props: { level: Number(node.attrs?.level ?? 1) }, content: toText(node.content) };\\n if (node.type === \\\"bulletList\\\") return { id: `block-${index}`, type: \\\"bullet_list\\\" };\\n if (node.type === \\\"orderedList\\\") return { id: `block-${index}`, type: \\\"ordered_list\\\" };\\n if (node.type === \\\"taskItem\\\") return { id: `block-${index}`, type: \\\"todo\\\", props: { checked: Boolean(node.attrs?.checked) }, content: toText(node.content) };\\n if (node.type === \\\"blockquote\\\") return { id: `block-${index}`, type: \\\"quote\\\", content: toText(node.content) };\\n if (node.type === \\\"codeBlock\\\") return { id: `block-${index}`, type: \\\"code_block\\\", content: toText(node.content) };\\n return null;\\n};\\n\\nexport function tiptapDocToBlocks(doc: unknown): Json {\\n if (!doc || typeof doc !== \\\"object\\\") return [];\\n const root = doc as { type?: string; content?: TiptapNode[] };\\n if (root.type !== \\\"doc\\\" || !Array.isArray(root.content) || root.content.length === 0) return [];\\n return root.content.flatMap((node, index) => {\\n if (!node || typeof node !== \\\"object\\\") return [];\\n const block = nodeToBlock(node, index);\\n return block ? [block] : [];\\n }) as Json;\\n}\\n\\nexport const tiptapDocFromBlocks = blocksToTiptapDoc;\\nexport const blocksFromTiptapDoc = tiptapDocToBlocks;\\n\",\n \"gitDiff\": null\n}","tool_name":"write_file","tool_use_id":"call_8gflsKPOgF098zDJgIIez7u5","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_YY1NTkZYJ8KgSflyHSnq61qr","input":"{\"content\":\"import {\\n blocksFromTiptapDoc,\\n editorBlockDocumentFromContent,\\n editorBlockDocumentFromTiptapDoc,\\n tiptapDocFromBlocks,\\n tiptapDocFromEditorBlockDocument,\\n} from \\\"@/lib/documents/tiptap-content-converter\\\";\\nimport { describe, expect, it } from \\\"vitest\\\";\\n\\ndescribe(\\\"tiptap-content-converter\\\", () => {\\n it(\\\"keeps empty content editable\\\", () => {\\n expect(tiptapDocFromBlocks([])).toEqual({\\n type: \\\"doc\\\",\\n content: [{ type: \\\"paragraph\\\", attrs: {}, content: [] }],\\n });\\n expect(blocksFromTiptapDoc({ type: \\\"doc\\\", content: [] })).toEqual([]);\\n });\\n\\n it(\\\"preserves stable block ids through paragraph round-trip\\\", () => {\\n const blocks = [{ id: \\\"p1\\\", type: \\\"paragraph\\\", content: \\\"你好,世界\\\" }];\\n expect(blocksFromTiptapDoc(tiptapDocFromBlocks(blocks as never))).toEqual([\\n { id: \\\"p1\\\", type: \\\"paragraph\\\", props: undefined, content: \\\"你好,世界\\\" },\\n ]);\\n });\\n\\n it(\\\"round-trips p0.5 block families through editor block document\\\", () => {\\n const document = editorBlockDocumentFromContent([\\n { id: \\\"p1\\\", type: \\\"paragraph\\\", content: \\\"段落\\\" },\\n { id: \\\"h1\\\", type: \\\"heading\\\", props: { level: 2 }, content: \\\"标题\\\" },\\n { id: \\\"b1\\\", type: \\\"bullet_list_item\\\", content: \\\"项目\\\" },\\n { id: \\\"n1\\\", type: \\\"numbered_list_item\\\", content: \\\"序号\\\" },\\n { id: \\\"t1\\\", type: \\\"todo\\\", props: { checked: true }, content: \\\"待办\\\" },\\n { id: \\\"q1\\\", type: \\\"quote\\\", content: \\\"引用\\\" },\\n { id: \\\"c1\\\", type: \\\"code_block\\\", props: { language: \\\"ts\\\" }, content: \\\"const x = 1\\\" },\\n ] as never);\\n\\n const tiptapDoc = tiptapDocFromEditorBlockDocument(document);\\n const roundTrip = editorBlockDocumentFromTiptapDoc(tiptapDoc, \\\"doc-1\\\");\\n\\n expect(roundTrip.rootBlockIds).toEqual([\\\"p1\\\", \\\"h1\\\", \\\"b1\\\", \\\"n1\\\", \\\"t1\\\", \\\"q1\\\", \\\"c1\\\"]);\\n expect(roundTrip.blocks.map((block) => [block.blockId, block.blockType])).toEqual([\\n [\\\"p1\\\", \\\"paragraph\\\"],\\n [\\\"h1\\\", \\\"heading\\\"],\\n [\\\"b1\\\", \\\"bullet_list_item\\\"],\\n [\\\"n1\\\", \\\"numbered_list_item\\\"],\\n [\\\"t1\\\", \\\"todo\\\"],\\n [\\\"q1\\\", \\\"quote\\\"],\\n [\\\"c1\\\", \\\"code_block\\\"],\\n ]);\\n expect(roundTrip.blocks[1].props?.headingLevel).toBe(2);\\n expect(roundTrip.blocks[4].props?.checked).toBe(true);\\n expect(roundTrip.blocks[6].props?.language).toBe(\\\"ts\\\");\\n });\\n});\\n\",\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.test.ts\"}","name":"write_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":193737,"output_tokens":819}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"update\",\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.test.ts\",\n \"content\": \"import {\\n blocksFromTiptapDoc,\\n editorBlockDocumentFromContent,\\n editorBlockDocumentFromTiptapDoc,\\n tiptapDocFromBlocks,\\n tiptapDocFromEditorBlockDocument,\\n} from \\\"@/lib/documents/tiptap-content-converter\\\";\\nimport { describe, expect, it } from \\\"vitest\\\";\\n\\ndescribe(\\\"tiptap-content-converter\\\", () => {\\n it(\\\"keeps empty content editable\\\", () => {\\n expect(tiptapDocFromBlocks([])).toEqual({\\n type: \\\"doc\\\",\\n content: [{ type: \\\"paragraph\\\", attrs: {}, content: [] }],\\n });\\n expect(blocksFromTiptapDoc({ type: \\\"doc\\\", content: [] })).toEqual([]);\\n });\\n\\n it(\\\"preserves stable block ids through paragraph round-trip\\\", () => {\\n const blocks = [{ id: \\\"p1\\\", type: \\\"paragraph\\\", content: \\\"你好,世界\\\" }];\\n expect(blocksFromTiptapDoc(tiptapDocFromBlocks(blocks as never))).toEqual([\\n { id: \\\"p1\\\", type: \\\"paragraph\\\", props: undefined, content: \\\"你好,世界\\\" },\\n ]);\\n });\\n\\n it(\\\"round-trips p0.5 block families through editor block document\\\", () => {\\n const document = editorBlockDocumentFromContent([\\n { id: \\\"p1\\\", type: \\\"paragraph\\\", content: \\\"段落\\\" },\\n { id: \\\"h1\\\", type: \\\"heading\\\", props: { level: 2 }, content: \\\"标题\\\" },\\n { id: \\\"b1\\\", type: \\\"bullet_list_item\\\", content: \\\"项目\\\" },\\n { id: \\\"n1\\\", type: \\\"numbered_list_item\\\", content: \\\"序号\\\" },\\n { id: \\\"t1\\\", type: \\\"todo\\\", props: { checked: true }, content: \\\"待办\\\" },\\n { id: \\\"q1\\\", type: \\\"quote\\\", content: \\\"引用\\\" },\\n { id: \\\"c1\\\", type: \\\"code_block\\\", props: { language: \\\"ts\\\" }, content: \\\"const x = 1\\\" },\\n ] as never);\\n\\n const tiptapDoc = tiptapDocFromEditorBlockDocument(document);\\n const roundTrip = editorBlockDocumentFromTiptapDoc(tiptapDoc, \\\"doc-1\\\");\\n\\n expect(roundTrip.rootBlockIds).toEqual([\\\"p1\\\", \\\"h1\\\", \\\"b1\\\", \\\"n1\\\", \\\"t1\\\", \\\"q1\\\", \\\"c1\\\"]);\\n expect(roundTrip.blocks.map((block) => [block.blockId, block.blockType])).toEqual([\\n [\\\"p1\\\", \\\"paragraph\\\"],\\n [\\\"h1\\\", \\\"heading\\\"],\\n [\\\"b1\\\", \\\"bullet_list_item\\\"],\\n [\\\"n1\\\", \\\"numbered_list_item\\\"],\\n [\\\"t1\\\", \\\"todo\\\"],\\n [\\\"q1\\\", \\\"quote\\\"],\\n [\\\"c1\\\", \\\"code_block\\\"],\\n ]);\\n expect(roundTrip.blocks[1].props?.headingLevel).toBe(2);\\n expect(roundTrip.blocks[4].props?.checked).toBe(true);\\n expect(roundTrip.blocks[6].props?.language).toBe(\\\"ts\\\");\\n });\\n});\\n\",\n \"structuredPatch\": [\n {\n \"oldStart\": 1,\n \"oldLines\": 16,\n \"newStart\": 1,\n \"newLines\": 54,\n \"lines\": [\n \"-import { blocksFromTiptapDoc, tiptapDocFromBlocks } from \\\"@/lib/documents/tiptap-content-converter\\\";\",\n \"-import { describe, expect, it } from \\\"vitest\\\";\",\n \"-\",\n \"-describe(\\\"tiptap-content-converter\\\", () => {\",\n \"- it(\\\"keeps empty content empty\\\", () => {\",\n \"- expect(tiptapDocFromBlocks([])).toEqual({ type: \\\"doc\\\", content: [] });\",\n \"- expect(blocksFromTiptapDoc({ type: \\\"doc\\\", content: [] })).toEqual([]);\",\n \"- });\",\n \"-\",\n \"- it(\\\"round-trips basic paragraph content\\\", () => {\",\n \"- const blocks = [{ id: \\\"1\\\", type: \\\"paragraph\\\", content: \\\"你好,世界\\\" }];\",\n \"- expect(blocksFromTiptapDoc(tiptapDocFromBlocks(blocks as never))).toEqual([\",\n \"- { id: \\\"block-0\\\", type: \\\"paragraph\\\", content: \\\"你好,世界\\\" },\",\n \"- ]);\",\n \"- });\",\n \"-});\",\n \"+import {\",\n \"+ blocksFromTiptapDoc,\",\n \"+ editorBlockDocumentFromContent,\",\n \"+ editorBlockDocumentFromTiptapDoc,\",\n \"+ tiptapDocFromBlocks,\",\n \"+ tiptapDocFromEditorBlockDocument,\",\n \"+} from \\\"@/lib/documents/tiptap-content-converter\\\";\",\n \"+import { describe, expect, it } from \\\"vitest\\\";\",\n \"+\",\n \"+describe(\\\"tiptap-content-converter\\\", () => {\",\n \"+ it(\\\"keeps empty content editable\\\", () => {\",\n \"+ expect(tiptapDocFromBlocks([])).toEqual({\",\n \"+ type: \\\"doc\\\",\",\n \"+ content: [{ type: \\\"paragraph\\\", attrs: {}, content: [] }],\",\n \"+ });\",\n \"+ expect(blocksFromTiptapDoc({ type: \\\"doc\\\", content: [] })).toEqual([]);\",\n \"+ });\",\n \"+\",\n \"+ it(\\\"preserves stable block ids through paragraph round-trip\\\", () => {\",\n \"+ const blocks = [{ id: \\\"p1\\\", type: \\\"paragraph\\\", content: \\\"你好,世界\\\" }];\",\n \"+ expect(blocksFromTiptapDoc(tiptapDocFromBlocks(blocks as never))).toEqual([\",\n \"+ { id: \\\"p1\\\", type: \\\"paragraph\\\", props: undefined, content: \\\"你好,世界\\\" },\",\n \"+ ]);\",\n \"+ });\",\n \"+\",\n \"+ it(\\\"round-trips p0.5 block families through editor block document\\\", () => {\",\n \"+ const document = editorBlockDocumentFromContent([\",\n \"+ { id: \\\"p1\\\", type: \\\"paragraph\\\", content: \\\"段落\\\" },\",\n \"+ { id: \\\"h1\\\", type: \\\"heading\\\", props: { level: 2 }, content: \\\"标题\\\" },\",\n \"+ { id: \\\"b1\\\", type: \\\"bullet_list_item\\\", content: \\\"项目\\\" },\",\n \"+ { id: \\\"n1\\\", type: \\\"numbered_list_item\\\", content: \\\"序号\\\" },\",\n \"+ { id: \\\"t1\\\", type: \\\"todo\\\", props: { checked: true }, content: \\\"待办\\\" },\",\n \"+ { id: \\\"q1\\\", type: \\\"quote\\\", content: \\\"引用\\\" },\",\n \"+ { id: \\\"c1\\\", type: \\\"code_block\\\", props: { language: \\\"ts\\\" }, content: \\\"const x = 1\\\" },\",\n \"+ ] as never);\",\n \"+\",\n \"+ const tiptapDoc = tiptapDocFromEditorBlockDocument(document);\",\n \"+ const roundTrip = editorBlockDocumentFromTiptapDoc(tiptapDoc, \\\"doc-1\\\");\",\n \"+\",\n \"+ expect(roundTrip.rootBlockIds).toEqual([\\\"p1\\\", \\\"h1\\\", \\\"b1\\\", \\\"n1\\\", \\\"t1\\\", \\\"q1\\\", \\\"c1\\\"]);\",\n \"+ expect(roundTrip.blocks.map((block) => [block.blockId, block.blockType])).toEqual([\",\n \"+ [\\\"p1\\\", \\\"paragraph\\\"],\",\n \"+ [\\\"h1\\\", \\\"heading\\\"],\",\n \"+ [\\\"b1\\\", \\\"bullet_list_item\\\"],\",\n \"+ [\\\"n1\\\", \\\"numbered_list_item\\\"],\",\n \"+ [\\\"t1\\\", \\\"todo\\\"],\",\n \"+ [\\\"q1\\\", \\\"quote\\\"],\",\n \"+ [\\\"c1\\\", \\\"code_block\\\"],\",\n \"+ ]);\",\n \"+ expect(roundTrip.blocks[1].props?.headingLevel).toBe(2);\",\n \"+ expect(roundTrip.blocks[4].props?.checked).toBe(true);\",\n \"+ expect(roundTrip.blocks[6].props?.language).toBe(\\\"ts\\\");\",\n \"+ });\",\n \"+});\"\n ]\n }\n ],\n \"originalFile\": \"import { blocksFromTiptapDoc, tiptapDocFromBlocks } from \\\"@/lib/documents/tiptap-content-converter\\\";\\nimport { describe, expect, it } from \\\"vitest\\\";\\n\\ndescribe(\\\"tiptap-content-converter\\\", () => {\\n it(\\\"keeps empty content empty\\\", () => {\\n expect(tiptapDocFromBlocks([])).toEqual({ type: \\\"doc\\\", content: [] });\\n expect(blocksFromTiptapDoc({ type: \\\"doc\\\", content: [] })).toEqual([]);\\n });\\n\\n it(\\\"round-trips basic paragraph content\\\", () => {\\n const blocks = [{ id: \\\"1\\\", type: \\\"paragraph\\\", content: \\\"你好,世界\\\" }];\\n expect(blocksFromTiptapDoc(tiptapDocFromBlocks(blocks as never))).toEqual([\\n { id: \\\"block-0\\\", type: \\\"paragraph\\\", content: \\\"你好,世界\\\" },\\n ]);\\n });\\n});\\n\",\n \"gitDiff\": null\n}","tool_name":"write_file","tool_use_id":"call_YY1NTkZYJ8KgSflyHSnq61qr","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_68KYxSqjN040LSwFxLAx1C9V","input":"{\"content\":\"use crate::editor::model::{\\n BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument, EditorBlockType,\\n TextMark,\\n};\\nuse serde::{Deserialize, Serialize};\\nuse std::collections::BTreeMap;\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"snake_case\\\")]\\npub enum TiptapBlockType {\\n Paragraph,\\n Heading,\\n BulletList,\\n OrderedList,\\n TaskList,\\n ListItem,\\n TaskItem,\\n Blockquote,\\n CodeBlock,\\n Text,\\n HardBreak,\\n Doc,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapHeadingAttrs {\\n pub level: u8,\\n #[serde(default)]\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapTaskItemAttrs {\\n #[serde(default)]\\n pub checked: bool,\\n #[serde(default)]\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapParagraphAttrs {\\n #[serde(default)]\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapListAttrs {\\n #[serde(default)]\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapListItemAttrs {\\n #[serde(default)]\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapBlockquoteAttrs {\\n #[serde(default)]\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapCodeBlockAttrs {\\n pub language: Option,\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapMark {\\n pub r#type: String,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(tag = \\\"type\\\", rename_all = \\\"camelCase\\\")]\\npub enum TiptapNode {\\n Doc { content: Vec },\\n Paragraph { #[serde(default)] attrs: TiptapParagraphAttrs, #[serde(default)] content: Vec },\\n Heading { attrs: TiptapHeadingAttrs, #[serde(default)] content: Vec },\\n BulletList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec },\\n OrderedList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec },\\n TaskList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec },\\n ListItem { #[serde(default)] attrs: TiptapListItemAttrs, #[serde(default)] content: Vec },\\n TaskItem { attrs: TiptapTaskItemAttrs, #[serde(default)] content: Vec },\\n Blockquote { #[serde(default)] attrs: TiptapBlockquoteAttrs, #[serde(default)] content: Vec },\\n CodeBlock { attrs: TiptapCodeBlockAttrs, #[serde(default)] content: Vec },\\n Text { text: String, #[serde(default)] marks: Vec },\\n HardBreak,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq)]\\npub enum EditorBlockDocumentTiptapError {\\n UnsupportedNode(&'static str),\\n InvalidDocument(&'static str),\\n}\\n\\npub struct EditorBlockDocumentTiptapBridge;\\n\\nimpl EditorBlockDocumentTiptapBridge {\\n pub fn to_tiptap_doc(document: &EditorBlockDocument) -> Result {\\n let mut content = Vec::new();\\n for block_id in &document.root_block_ids {\\n let block = document\\n .blocks\\n .iter()\\n .find(|block| &block.block_id == block_id)\\n .ok_or(EditorBlockDocumentTiptapError::InvalidDocument(\\\"根块不存在\\\"))?;\\n content.push(block_to_tiptap_node(block)?);\\n }\\n Ok(TiptapNode::Doc { content })\\n }\\n\\n pub fn from_tiptap_doc(\\n document_id: impl Into,\\n doc: &TiptapNode,\\n ) -> Result {\\n match doc {\\n TiptapNode::Doc { content } => {\\n let mut blocks = Vec::new();\\n let mut root_block_ids = Vec::new();\\n for (index, node) in content.iter().enumerate() {\\n let block = node_to_block(node, index)?;\\n root_block_ids.push(block.block_id.clone());\\n blocks.push(block);\\n }\\n Ok(EditorBlockDocument {\\n document_id: document_id.into(),\\n root_block_ids,\\n blocks,\\n })\\n }\\n _ => Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"顶层必须是 doc\\\")),\\n }\\n }\\n}\\n\\nfn block_to_tiptap_node(block: &EditorBlock) -> Result {\\n let content = text_nodes_to_tiptap(&block.content_nodes)?;\\n match block.block_type {\\n EditorBlockType::Paragraph => Ok(TiptapNode::Paragraph {\\n attrs: TiptapParagraphAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content,\\n }),\\n EditorBlockType::Heading => Ok(TiptapNode::Heading {\\n attrs: TiptapHeadingAttrs {\\n level: block.props.heading_level.unwrap_or(1),\\n block_id: Some(block.block_id.clone()),\\n },\\n content,\\n }),\\n EditorBlockType::BulletListItem => Ok(TiptapNode::BulletList {\\n attrs: TiptapListAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content: vec![TiptapNode::ListItem {\\n attrs: TiptapListItemAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content: vec![TiptapNode::Paragraph {\\n attrs: TiptapParagraphAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content,\\n }],\\n }],\\n }),\\n EditorBlockType::NumberedListItem => Ok(TiptapNode::OrderedList {\\n attrs: TiptapListAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content: vec![TiptapNode::ListItem {\\n attrs: TiptapListItemAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content: vec![TiptapNode::Paragraph {\\n attrs: TiptapParagraphAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content,\\n }],\\n }],\\n }),\\n EditorBlockType::Todo => Ok(TiptapNode::TaskList {\\n attrs: TiptapListAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content: vec![TiptapNode::TaskItem {\\n attrs: TiptapTaskItemAttrs {\\n checked: block.props.checked.unwrap_or(false),\\n block_id: Some(block.block_id.clone()),\\n },\\n content: vec![TiptapNode::Paragraph {\\n attrs: TiptapParagraphAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content,\\n }],\\n }],\\n }),\\n EditorBlockType::Quote => Ok(TiptapNode::Blockquote {\\n attrs: TiptapBlockquoteAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content: vec![TiptapNode::Paragraph {\\n attrs: TiptapParagraphAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content,\\n }],\\n }),\\n EditorBlockType::CodeBlock => Ok(TiptapNode::CodeBlock {\\n attrs: TiptapCodeBlockAttrs {\\n language: block.props.language.clone(),\\n block_id: Some(block.block_id.clone()),\\n },\\n content,\\n }),\\n _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\n \\\"当前块型暂不支持导出到 Tiptap\\\",\\n )),\\n }\\n}\\n\\nfn node_to_block(node: &TiptapNode, index: usize) -> Result {\\n let fallback_block_id = format!(\\\"block_{}\\\", index + 1);\\n match node {\\n TiptapNode::Paragraph { attrs, content } => Ok(EditorBlock {\\n block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),\\n block_type: EditorBlockType::Paragraph,\\n props: BlockProps::default(),\\n content_nodes: text_nodes_from_tiptap(content)?,\\n child_block_ids: vec![],\\n }),\\n TiptapNode::Heading { attrs, content } => Ok(EditorBlock {\\n block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),\\n block_type: EditorBlockType::Heading,\\n props: BlockProps {\\n heading_level: Some(attrs.level),\\n ..BlockProps::default()\\n },\\n content_nodes: text_nodes_from_tiptap(content)?,\\n child_block_ids: vec![],\\n }),\\n TiptapNode::BulletList { attrs, content } => list_node_to_block(\\n attrs.block_id.clone().unwrap_or(fallback_block_id),\\n EditorBlockType::BulletListItem,\\n content,\\n ),\\n TiptapNode::OrderedList { attrs, content } => list_node_to_block(\\n attrs.block_id.clone().unwrap_or(fallback_block_id),\\n EditorBlockType::NumberedListItem,\\n content,\\n ),\\n TiptapNode::TaskList { attrs, content } => list_task_node_to_block(\\n attrs.block_id.clone().unwrap_or(fallback_block_id),\\n content,\\n ),\\n TiptapNode::Blockquote { attrs, content } => Ok(EditorBlock {\\n block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),\\n block_type: EditorBlockType::Quote,\\n props: BlockProps::default(),\\n content_nodes: text_nodes_from_tiptap(extract_block_container_content(content)?)?,\\n child_block_ids: vec![],\\n }),\\n TiptapNode::CodeBlock { attrs, content } => Ok(EditorBlock {\\n block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),\\n block_type: EditorBlockType::CodeBlock,\\n props: BlockProps {\\n language: attrs.language.clone(),\\n ..BlockProps::default()\\n },\\n content_nodes: text_nodes_from_tiptap(content)?,\\n child_block_ids: vec![],\\n }),\\n _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\n \\\"当前节点暂不支持导入为 EditorBlockDocument\\\",\\n )),\\n }\\n}\\n\\nfn list_node_to_block(\\n block_id: String,\\n block_type: EditorBlockType,\\n content: &[TiptapNode],\\n) -> Result {\\n Ok(EditorBlock {\\n block_id,\\n block_type,\\n props: BlockProps::default(),\\n content_nodes: text_nodes_from_tiptap(extract_list_item_inline_content(content)?)?,\\n child_block_ids: vec![],\\n })\\n}\\n\\nfn list_task_node_to_block(\\n block_id: String,\\n content: &[TiptapNode],\\n) -> Result {\\n let Some(first) = content.first() else {\\n return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\n \\\"任务列表节点缺少 task_item\\\",\\n ));\\n };\\n let (checked, item_content) = match first {\\n TiptapNode::TaskItem { attrs, content } => (Some(attrs.checked), extract_block_container_content(content)?),\\n _ => {\\n return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\n \\\"任务列表节点首子节点类型不正确\\\",\\n ))\\n }\\n };\\n Ok(EditorBlock {\\n block_id,\\n block_type: EditorBlockType::Todo,\\n props: BlockProps {\\n checked,\\n ..BlockProps::default()\\n },\\n content_nodes: text_nodes_from_tiptap(item_content)?,\\n child_block_ids: vec![],\\n })\\n}\\n\\nfn extract_list_item_inline_content(content: &[TiptapNode]) -> Result<&[TiptapNode], EditorBlockDocumentTiptapError> {\\n let Some(first) = content.first() else {\\n return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\n \\\"列表节点缺少 list_item/task_item\\\",\\n ));\\n };\\n match first {\\n TiptapNode::ListItem { content, .. } | TiptapNode::TaskItem { content, .. } => {\\n extract_block_container_content(content)\\n }\\n _ => Err(EditorBlockDocumentTiptapError::InvalidDocument(\\n \\\"列表节点首子节点类型不正确\\\",\\n )),\\n }\\n}\\n\\nfn extract_block_container_content(content: &[TiptapNode]) -> Result<&[TiptapNode], EditorBlockDocumentTiptapError> {\\n let Some(first) = content.first() else {\\n return Ok(content);\\n };\\n match first {\\n TiptapNode::Paragraph { content, .. } => Ok(content),\\n _ => Ok(content),\\n }\\n}\\n\\nfn text_nodes_to_tiptap(nodes: &[ContentNode]) -> Result, EditorBlockDocumentTiptapError> {\\n let mut out = Vec::new();\\n for node in nodes {\\n match &node.payload {\\n ContentNodePayload::Text { text, marks } => out.push(TiptapNode::Text {\\n text: text.clone(),\\n marks: marks.iter().map(text_mark_to_tiptap).collect(),\\n }),\\n ContentNodePayload::HardBreak => out.push(TiptapNode::HardBreak),\\n ContentNodePayload::ReferenceToken { .. } => {\\n return Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\n \\\"引用 token 暂不映射到 Tiptap 文本合同\\\",\\n ))\\n }\\n }\\n }\\n Ok(out)\\n}\\n\\nfn text_nodes_from_tiptap(nodes: &[TiptapNode]) -> Result, EditorBlockDocumentTiptapError> {\\n let mut out = Vec::new();\\n for node in nodes {\\n match node {\\n TiptapNode::Text { text, marks } => out.push(ContentNode {\\n payload: ContentNodePayload::Text {\\n text: text.clone(),\\n marks: marks.iter().filter_map(tiptap_mark_to_text_mark).collect(),\\n },\\n attrs: BTreeMap::new(),\\n }),\\n TiptapNode::HardBreak => out.push(ContentNode {\\n payload: ContentNodePayload::HardBreak,\\n attrs: BTreeMap::new(),\\n }),\\n _ => {\\n return Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\n \\\"当前只支持文本节点内容\\\",\\n ))\\n }\\n }\\n }\\n Ok(out)\\n}\\n\\nfn text_mark_to_tiptap(mark: &TextMark) -> TiptapMark {\\n let r#type = match mark {\\n TextMark::Bold => \\\"bold\\\",\\n TextMark::Italic => \\\"italic\\\",\\n TextMark::Underline => \\\"underline\\\",\\n TextMark::Strike => \\\"strike\\\",\\n TextMark::Code => \\\"code\\\",\\n };\\n TiptapMark {\\n r#type: r#type.to_string(),\\n }\\n}\\n\\nfn tiptap_mark_to_text_mark(mark: &TiptapMark) -> Option {\\n match mark.r#type.as_str() {\\n \\\"bold\\\" => Some(TextMark::Bold),\\n \\\"italic\\\" => Some(TextMark::Italic),\\n \\\"underline\\\" => Some(TextMark::Underline),\\n \\\"strike\\\" => Some(TextMark::Strike),\\n \\\"code\\\" => Some(TextMark::Code),\\n _ => None,\\n }\\n}\\n\",\"path\":\"/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs\"}","name":"write_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":196782,"output_tokens":4164}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"update\",\n \"filePath\": \"/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs\",\n \"content\": \"use crate::editor::model::{\\n BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument, EditorBlockType,\\n TextMark,\\n};\\nuse serde::{Deserialize, Serialize};\\nuse std::collections::BTreeMap;\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"snake_case\\\")]\\npub enum TiptapBlockType {\\n Paragraph,\\n Heading,\\n BulletList,\\n OrderedList,\\n TaskList,\\n ListItem,\\n TaskItem,\\n Blockquote,\\n CodeBlock,\\n Text,\\n HardBreak,\\n Doc,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapHeadingAttrs {\\n pub level: u8,\\n #[serde(default)]\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapTaskItemAttrs {\\n #[serde(default)]\\n pub checked: bool,\\n #[serde(default)]\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapParagraphAttrs {\\n #[serde(default)]\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapListAttrs {\\n #[serde(default)]\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapListItemAttrs {\\n #[serde(default)]\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapBlockquoteAttrs {\\n #[serde(default)]\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapCodeBlockAttrs {\\n pub language: Option,\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapMark {\\n pub r#type: String,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(tag = \\\"type\\\", rename_all = \\\"camelCase\\\")]\\npub enum TiptapNode {\\n Doc { content: Vec },\\n Paragraph { #[serde(default)] attrs: TiptapParagraphAttrs, #[serde(default)] content: Vec },\\n Heading { attrs: TiptapHeadingAttrs, #[serde(default)] content: Vec },\\n BulletList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec },\\n OrderedList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec },\\n TaskList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec },\\n ListItem { #[serde(default)] attrs: TiptapListItemAttrs, #[serde(default)] content: Vec },\\n TaskItem { attrs: TiptapTaskItemAttrs, #[serde(default)] content: Vec },\\n Blockquote { #[serde(default)] attrs: TiptapBlockquoteAttrs, #[serde(default)] content: Vec },\\n CodeBlock { attrs: TiptapCodeBlockAttrs, #[serde(default)] content: Vec },\\n Text { text: String, #[serde(default)] marks: Vec },\\n HardBreak,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq)]\\npub enum EditorBlockDocumentTiptapError {\\n UnsupportedNode(&'static str),\\n InvalidDocument(&'static str),\\n}\\n\\npub struct EditorBlockDocumentTiptapBridge;\\n\\nimpl EditorBlockDocumentTiptapBridge {\\n pub fn to_tiptap_doc(document: &EditorBlockDocument) -> Result {\\n let mut content = Vec::new();\\n for block_id in &document.root_block_ids {\\n let block = document\\n .blocks\\n .iter()\\n .find(|block| &block.block_id == block_id)\\n .ok_or(EditorBlockDocumentTiptapError::InvalidDocument(\\\"根块不存在\\\"))?;\\n content.push(block_to_tiptap_node(block)?);\\n }\\n Ok(TiptapNode::Doc { content })\\n }\\n\\n pub fn from_tiptap_doc(\\n document_id: impl Into,\\n doc: &TiptapNode,\\n ) -> Result {\\n match doc {\\n TiptapNode::Doc { content } => {\\n let mut blocks = Vec::new();\\n let mut root_block_ids = Vec::new();\\n for (index, node) in content.iter().enumerate() {\\n let block = node_to_block(node, index)?;\\n root_block_ids.push(block.block_id.clone());\\n blocks.push(block);\\n }\\n Ok(EditorBlockDocument {\\n document_id: document_id.into(),\\n root_block_ids,\\n blocks,\\n })\\n }\\n _ => Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"顶层必须是 doc\\\")),\\n }\\n }\\n}\\n\\nfn block_to_tiptap_node(block: &EditorBlock) -> Result {\\n let content = text_nodes_to_tiptap(&block.content_nodes)?;\\n match block.block_type {\\n EditorBlockType::Paragraph => Ok(TiptapNode::Paragraph {\\n attrs: TiptapParagraphAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content,\\n }),\\n EditorBlockType::Heading => Ok(TiptapNode::Heading {\\n attrs: TiptapHeadingAttrs {\\n level: block.props.heading_level.unwrap_or(1),\\n block_id: Some(block.block_id.clone()),\\n },\\n content,\\n }),\\n EditorBlockType::BulletListItem => Ok(TiptapNode::BulletList {\\n attrs: TiptapListAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content: vec![TiptapNode::ListItem {\\n attrs: TiptapListItemAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content: vec![TiptapNode::Paragraph {\\n attrs: TiptapParagraphAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content,\\n }],\\n }],\\n }),\\n EditorBlockType::NumberedListItem => Ok(TiptapNode::OrderedList {\\n attrs: TiptapListAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content: vec![TiptapNode::ListItem {\\n attrs: TiptapListItemAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content: vec![TiptapNode::Paragraph {\\n attrs: TiptapParagraphAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content,\\n }],\\n }],\\n }),\\n EditorBlockType::Todo => Ok(TiptapNode::TaskList {\\n attrs: TiptapListAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content: vec![TiptapNode::TaskItem {\\n attrs: TiptapTaskItemAttrs {\\n checked: block.props.checked.unwrap_or(false),\\n block_id: Some(block.block_id.clone()),\\n },\\n content: vec![TiptapNode::Paragraph {\\n attrs: TiptapParagraphAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content,\\n }],\\n }],\\n }),\\n EditorBlockType::Quote => Ok(TiptapNode::Blockquote {\\n attrs: TiptapBlockquoteAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content: vec![TiptapNode::Paragraph {\\n attrs: TiptapParagraphAttrs {\\n block_id: Some(block.block_id.clone()),\\n },\\n content,\\n }],\\n }),\\n EditorBlockType::CodeBlock => Ok(TiptapNode::CodeBlock {\\n attrs: TiptapCodeBlockAttrs {\\n language: block.props.language.clone(),\\n block_id: Some(block.block_id.clone()),\\n },\\n content,\\n }),\\n _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\n \\\"当前块型暂不支持导出到 Tiptap\\\",\\n )),\\n }\\n}\\n\\nfn node_to_block(node: &TiptapNode, index: usize) -> Result {\\n let fallback_block_id = format!(\\\"block_{}\\\", index + 1);\\n match node {\\n TiptapNode::Paragraph { attrs, content } => Ok(EditorBlock {\\n block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),\\n block_type: EditorBlockType::Paragraph,\\n props: BlockProps::default(),\\n content_nodes: text_nodes_from_tiptap(content)?,\\n child_block_ids: vec![],\\n }),\\n TiptapNode::Heading { attrs, content } => Ok(EditorBlock {\\n block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),\\n block_type: EditorBlockType::Heading,\\n props: BlockProps {\\n heading_level: Some(attrs.level),\\n ..BlockProps::default()\\n },\\n content_nodes: text_nodes_from_tiptap(content)?,\\n child_block_ids: vec![],\\n }),\\n TiptapNode::BulletList { attrs, content } => list_node_to_block(\\n attrs.block_id.clone().unwrap_or(fallback_block_id),\\n EditorBlockType::BulletListItem,\\n content,\\n ),\\n TiptapNode::OrderedList { attrs, content } => list_node_to_block(\\n attrs.block_id.clone().unwrap_or(fallback_block_id),\\n EditorBlockType::NumberedListItem,\\n content,\\n ),\\n TiptapNode::TaskList { attrs, content } => list_task_node_to_block(\\n attrs.block_id.clone().unwrap_or(fallback_block_id),\\n content,\\n ),\\n TiptapNode::Blockquote { attrs, content } => Ok(EditorBlock {\\n block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),\\n block_type: EditorBlockType::Quote,\\n props: BlockProps::default(),\\n content_nodes: text_nodes_from_tiptap(extract_block_container_content(content)?)?,\\n child_block_ids: vec![],\\n }),\\n TiptapNode::CodeBlock { attrs, content } => Ok(EditorBlock {\\n block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),\\n block_type: EditorBlockType::CodeBlock,\\n props: BlockProps {\\n language: attrs.language.clone(),\\n ..BlockProps::default()\\n },\\n content_nodes: text_nodes_from_tiptap(content)?,\\n child_block_ids: vec![],\\n }),\\n _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\n \\\"当前节点暂不支持导入为 EditorBlockDocument\\\",\\n )),\\n }\\n}\\n\\nfn list_node_to_block(\\n block_id: String,\\n block_type: EditorBlockType,\\n content: &[TiptapNode],\\n) -> Result {\\n Ok(EditorBlock {\\n block_id,\\n block_type,\\n props: BlockProps::default(),\\n content_nodes: text_nodes_from_tiptap(extract_list_item_inline_content(content)?)?,\\n child_block_ids: vec![],\\n })\\n}\\n\\nfn list_task_node_to_block(\\n block_id: String,\\n content: &[TiptapNode],\\n) -> Result {\\n let Some(first) = content.first() else {\\n return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\n \\\"任务列表节点缺少 task_item\\\",\\n ));\\n };\\n let (checked, item_content) = match first {\\n TiptapNode::TaskItem { attrs, content } => (Some(attrs.checked), extract_block_container_content(content)?),\\n _ => {\\n return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\n \\\"任务列表节点首子节点类型不正确\\\",\\n ))\\n }\\n };\\n Ok(EditorBlock {\\n block_id,\\n block_type: EditorBlockType::Todo,\\n props: BlockProps {\\n checked,\\n ..BlockProps::default()\\n },\\n content_nodes: text_nodes_from_tiptap(item_content)?,\\n child_block_ids: vec![],\\n })\\n}\\n\\nfn extract_list_item_inline_content(content: &[TiptapNode]) -> Result<&[TiptapNode], EditorBlockDocumentTiptapError> {\\n let Some(first) = content.first() else {\\n return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\n \\\"列表节点缺少 list_item/task_item\\\",\\n ));\\n };\\n match first {\\n TiptapNode::ListItem { content, .. } | TiptapNode::TaskItem { content, .. } => {\\n extract_block_container_content(content)\\n }\\n _ => Err(EditorBlockDocumentTiptapError::InvalidDocument(\\n \\\"列表节点首子节点类型不正确\\\",\\n )),\\n }\\n}\\n\\nfn extract_block_container_content(content: &[TiptapNode]) -> Result<&[TiptapNode], EditorBlockDocumentTiptapError> {\\n let Some(first) = content.first() else {\\n return Ok(content);\\n };\\n match first {\\n TiptapNode::Paragraph { content, .. } => Ok(content),\\n _ => Ok(content),\\n }\\n}\\n\\nfn text_nodes_to_tiptap(nodes: &[ContentNode]) -> Result, EditorBlockDocumentTiptapError> {\\n let mut out = Vec::new();\\n for node in nodes {\\n match &node.payload {\\n ContentNodePayload::Text { text, marks } => out.push(TiptapNode::Text {\\n text: text.clone(),\\n marks: marks.iter().map(text_mark_to_tiptap).collect(),\\n }),\\n ContentNodePayload::HardBreak => out.push(TiptapNode::HardBreak),\\n ContentNodePayload::ReferenceToken { .. } => {\\n return Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\n \\\"引用 token 暂不映射到 Tiptap 文本合同\\\",\\n ))\\n }\\n }\\n }\\n Ok(out)\\n}\\n\\nfn text_nodes_from_tiptap(nodes: &[TiptapNode]) -> Result, EditorBlockDocumentTiptapError> {\\n let mut out = Vec::new();\\n for node in nodes {\\n match node {\\n TiptapNode::Text { text, marks } => out.push(ContentNode {\\n payload: ContentNodePayload::Text {\\n text: text.clone(),\\n marks: marks.iter().filter_map(tiptap_mark_to_text_mark).collect(),\\n },\\n attrs: BTreeMap::new(),\\n }),\\n TiptapNode::HardBreak => out.push(ContentNode {\\n payload: ContentNodePayload::HardBreak,\\n attrs: BTreeMap::new(),\\n }),\\n _ => {\\n return Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\n \\\"当前只支持文本节点内容\\\",\\n ))\\n }\\n }\\n }\\n Ok(out)\\n}\\n\\nfn text_mark_to_tiptap(mark: &TextMark) -> TiptapMark {\\n let r#type = match mark {\\n TextMark::Bold => \\\"bold\\\",\\n TextMark::Italic => \\\"italic\\\",\\n TextMark::Underline => \\\"underline\\\",\\n TextMark::Strike => \\\"strike\\\",\\n TextMark::Code => \\\"code\\\",\\n };\\n TiptapMark {\\n r#type: r#type.to_string(),\\n }\\n}\\n\\nfn tiptap_mark_to_text_mark(mark: &TiptapMark) -> Option {\\n match mark.r#type.as_str() {\\n \\\"bold\\\" => Some(TextMark::Bold),\\n \\\"italic\\\" => Some(TextMark::Italic),\\n \\\"underline\\\" => Some(TextMark::Underline),\\n \\\"strike\\\" => Some(TextMark::Strike),\\n \\\"code\\\" => Some(TextMark::Code),\\n _ => None,\\n }\\n}\\n\",\n \"structuredPatch\": [\n {\n \"oldStart\": 1,\n \"oldLines\": 155,\n \"newStart\": 1,\n \"newLines\": 427,\n \"lines\": [\n \"-use crate::editor::model::{\",\n \"- BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument, EditorBlockType,\",\n \"-};\",\n \"-use serde::{Deserialize, Serialize};\",\n \"-use std::collections::BTreeMap;\",\n \"-\",\n \"-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\",\n \"-#[serde(rename_all = \\\"snake_case\\\")]\",\n \"-pub enum TiptapBlockType {\",\n \"- Paragraph,\",\n \"- Heading,\",\n \"- BulletList,\",\n \"- OrderedList,\",\n \"- TaskList,\",\n \"- ListItem,\",\n \"- TaskItem,\",\n \"- Blockquote,\",\n \"- CodeBlock,\",\n \"- Text,\",\n \"- Doc,\",\n \"-}\",\n \"-\",\n \"-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\",\n \"-#[serde(rename_all = \\\"camelCase\\\")]\",\n \"-pub struct TiptapHeadingAttrs {\",\n \"- pub level: u8,\",\n \"-}\",\n \"-\",\n \"-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\",\n \"-#[serde(rename_all = \\\"camelCase\\\")]\",\n \"-pub struct TiptapTaskItemAttrs {\",\n \"- #[serde(default)]\",\n \"- pub checked: bool,\",\n \"-}\",\n \"-\",\n \"-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\",\n \"-#[serde(rename_all = \\\"camelCase\\\")]\",\n \"-pub struct TiptapCodeBlockAttrs {\",\n \"- pub language: Option,\",\n \"- pub block_id: Option,\",\n \"-}\",\n \"-\",\n \"-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\",\n \"-#[serde(tag = \\\"type\\\", rename_all = \\\"snake_case\\\")]\",\n \"-pub enum TiptapNode {\",\n \"- Doc { content: Vec },\",\n \"- Paragraph { #[serde(default)] attrs: BTreeMap, #[serde(default)] content: Vec },\",\n \"- Heading { attrs: TiptapHeadingAttrs, #[serde(default)] content: Vec },\",\n \"- BulletList { #[serde(default)] content: Vec },\",\n \"- OrderedList { #[serde(default)] content: Vec, #[serde(default)] attrs: BTreeMap },\",\n \"- TaskList { #[serde(default)] content: Vec },\",\n \"- ListItem { #[serde(default)] content: Vec },\",\n \"- TaskItem { attrs: TiptapTaskItemAttrs, #[serde(default)] content: Vec },\",\n \"- Blockquote { #[serde(default)] content: Vec },\",\n \"- CodeBlock { attrs: TiptapCodeBlockAttrs, #[serde(default)] content: Vec },\",\n \"- Text { text: String },\",\n \"-}\",\n \"-\",\n \"-#[derive(Debug, Clone, PartialEq, Eq)]\",\n \"-pub enum EditorBlockDocumentTiptapError {\",\n \"- UnsupportedNode(&'static str),\",\n \"- InvalidDocument(&'static str),\",\n \"-}\",\n \"-\",\n \"-pub struct EditorBlockDocumentTiptapBridge;\",\n \"-\",\n \"-impl EditorBlockDocumentTiptapBridge {\",\n \"- pub fn to_tiptap_doc(document: &EditorBlockDocument) -> Result {\",\n \"- let mut content = Vec::new();\",\n \"- for block_id in &document.root_block_ids {\",\n \"- let block = document.blocks.iter().find(|block| &block.block_id == block_id).ok_or(EditorBlockDocumentTiptapError::InvalidDocument(\\\"根块不存在\\\"))?;\",\n \"- content.push(block_to_tiptap_node(block)?);\",\n \"- }\",\n \"- Ok(TiptapNode::Doc { content })\",\n \"- }\",\n \"-\",\n \"- pub fn from_tiptap_doc(document_id: impl Into, doc: &TiptapNode) -> Result {\",\n \"- match doc {\",\n \"- TiptapNode::Doc { content } => {\",\n \"- let mut blocks = Vec::new();\",\n \"- let mut root_block_ids = Vec::new();\",\n \"- for (index, node) in content.iter().enumerate() {\",\n \"- let block = node_to_block(node, index)?;\",\n \"- root_block_ids.push(block.block_id.clone());\",\n \"- blocks.push(block);\",\n \"- }\",\n \"- Ok(EditorBlockDocument { document_id: document_id.into(), root_block_ids, blocks })\",\n \"- }\",\n \"- _ => Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"顶层必须是 doc\\\")),\",\n \"- }\",\n \"- }\",\n \"-}\",\n \"-\",\n \"-fn block_to_tiptap_node(block: &EditorBlock) -> Result {\",\n \"- let content = text_nodes_to_tiptap(&block.content_nodes)?;\",\n \"- match block.block_type {\",\n \"- EditorBlockType::Paragraph => Ok(TiptapNode::Paragraph { attrs: BTreeMap::new(), content }),\",\n \"- EditorBlockType::Heading => Ok(TiptapNode::Heading { attrs: TiptapHeadingAttrs { level: block.props.heading_level.unwrap_or(1) }, content }),\",\n \"- EditorBlockType::BulletListItem => Ok(TiptapNode::BulletList { content: vec![TiptapNode::ListItem { content }] }),\",\n \"- EditorBlockType::NumberedListItem => Ok(TiptapNode::OrderedList { attrs: BTreeMap::new(), content: vec![TiptapNode::ListItem { content }] }),\",\n \"- EditorBlockType::Todo => Ok(TiptapNode::TaskList { content: vec![TiptapNode::TaskItem { attrs: TiptapTaskItemAttrs { checked: block.props.checked.unwrap_or(false) }, content }] }),\",\n \"- EditorBlockType::Quote => Ok(TiptapNode::Blockquote { content }),\",\n \"- EditorBlockType::CodeBlock => Ok(TiptapNode::CodeBlock { attrs: TiptapCodeBlockAttrs { language: block.props.language.clone(), block_id: Some(block.block_id.clone()) }, content }),\",\n \"- _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\\"当前块型暂不支持导出到 Tiptap\\\")),\",\n \"- }\",\n \"-}\",\n \"-\",\n \"-fn node_to_block(node: &TiptapNode, index: usize) -> Result {\",\n \"- let block_id = format!(\\\"block_{}\\\", index + 1);\",\n \"- match node {\",\n \"- TiptapNode::Paragraph { content, .. } => Ok(EditorBlock { block_id, block_type: EditorBlockType::Paragraph, props: BlockProps::default(), content_nodes: text_nodes_from_tiptap(content)?, child_block_ids: vec![] }),\",\n \"- TiptapNode::Heading { attrs, content } => Ok(EditorBlock { block_id, block_type: EditorBlockType::Heading, props: BlockProps { heading_level: Some(attrs.level), ..BlockProps::default() }, content_nodes: text_nodes_from_tiptap(content)?, child_block_ids: vec![] }),\",\n \"- TiptapNode::BulletList { content } => list_node_to_block(block_id, EditorBlockType::BulletListItem, content),\",\n \"- TiptapNode::OrderedList { content, .. } => list_node_to_block(block_id, EditorBlockType::NumberedListItem, content),\",\n \"- TiptapNode::TaskList { content } => list_task_node_to_block(block_id, content),\",\n \"- TiptapNode::Blockquote { content } => Ok(EditorBlock { block_id, block_type: EditorBlockType::Quote, props: BlockProps::default(), content_nodes: text_nodes_from_tiptap(content)?, child_block_ids: vec![] }),\",\n \"- TiptapNode::CodeBlock { attrs, content } => Ok(EditorBlock { block_id, block_type: EditorBlockType::CodeBlock, props: BlockProps { language: attrs.language.clone(), ..BlockProps::default() }, content_nodes: text_nodes_from_tiptap(content)?, child_block_ids: vec![] }),\",\n \"- _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\\"当前节点暂不支持导入为 EditorBlockDocument\\\")),\",\n \"- }\",\n \"-}\",\n \"-\",\n \"-fn list_node_to_block(block_id: String, block_type: EditorBlockType, content: &[TiptapNode]) -> Result {\",\n \"- let Some(first) = content.first() else { return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"列表节点缺少 list_item/task_item\\\")); };\",\n \"- let item_content = match first { TiptapNode::ListItem { content } | TiptapNode::TaskItem { content, .. } => content, _ => return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"列表节点首子节点类型不正确\\\")), };\",\n \"- Ok(EditorBlock { block_id, block_type, props: BlockProps::default(), content_nodes: text_nodes_from_tiptap(item_content)?, child_block_ids: vec![] })\",\n \"-}\",\n \"-\",\n \"-fn list_task_node_to_block(block_id: String, content: &[TiptapNode]) -> Result {\",\n \"- let Some(first) = content.first() else { return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"任务列表节点缺少 task_item\\\")); };\",\n \"- let (checked, item_content) = match first { TiptapNode::TaskItem { attrs, content } => (Some(attrs.checked), content), _ => return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"任务列表节点首子节点类型不正确\\\")), };\",\n \"- Ok(EditorBlock { block_id, block_type: EditorBlockType::Todo, props: BlockProps { checked, ..BlockProps::default() }, content_nodes: text_nodes_from_tiptap(item_content)?, child_block_ids: vec![] })\",\n \"-}\",\n \"-\",\n \"-fn text_nodes_to_tiptap(nodes: &[ContentNode]) -> Result, EditorBlockDocumentTiptapError> {\",\n \"- let mut out = Vec::new();\",\n \"- for node in nodes {\",\n \"- match &node.payload {\",\n \"- ContentNodePayload::Text { text, .. } => out.push(TiptapNode::Text { text: text.clone() }),\",\n \"- ContentNodePayload::HardBreak => out.push(TiptapNode::Text { text: \\\"\\\\n\\\".into() }),\",\n \"- ContentNodePayload::ReferenceToken { .. } => return Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\\"引用 token 暂不映射到 Tiptap 文本合同\\\")),\",\n \"- }\",\n \"- }\",\n \"- Ok(out)\",\n \"-}\",\n \"-\",\n \"-fn text_nodes_from_tiptap(nodes: &[TiptapNode]) -> Result, EditorBlockDocumentTiptapError> {\",\n \"- let mut out = Vec::new();\",\n \"- for node in nodes {\",\n \"- match node {\",\n \"- TiptapNode::Text { text } => out.push(ContentNode { payload: ContentNodePayload::Text { text: text.clone(), marks: vec![] }, attrs: BTreeMap::new() }),\",\n \"- _ => return Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\\"当前只支持文本节点内容\\\")),\",\n \"- }\",\n \"- }\",\n \"- Ok(out)\",\n \"-}\",\n \"+use crate::editor::model::{\",\n \"+ BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument, EditorBlockType,\",\n \"+ TextMark,\",\n \"+};\",\n \"+use serde::{Deserialize, Serialize};\",\n \"+use std::collections::BTreeMap;\",\n \"+\",\n \"+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\",\n \"+#[serde(rename_all = \\\"snake_case\\\")]\",\n \"+pub enum TiptapBlockType {\",\n \"+ Paragraph,\",\n \"+ Heading,\",\n \"+ BulletList,\",\n \"+ OrderedList,\",\n \"+ TaskList,\",\n \"+ ListItem,\",\n \"+ TaskItem,\",\n \"+ Blockquote,\",\n \"+ CodeBlock,\",\n \"+ Text,\",\n \"+ HardBreak,\",\n \"+ Doc,\",\n \"+}\",\n \"+\",\n \"+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\",\n \"+#[serde(rename_all = \\\"camelCase\\\")]\",\n \"+pub struct TiptapHeadingAttrs {\",\n \"+ pub level: u8,\",\n \"+ #[serde(default)]\",\n \"+ pub block_id: Option,\",\n \"+}\",\n \"+\",\n \"+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\",\n \"+#[serde(rename_all = \\\"camelCase\\\")]\",\n \"+pub struct TiptapTaskItemAttrs {\",\n \"+ #[serde(default)]\",\n \"+ pub checked: bool,\",\n \"+ #[serde(default)]\",\n \"+ pub block_id: Option,\",\n \"+}\",\n \"+\",\n \"+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\",\n \"+#[serde(rename_all = \\\"camelCase\\\")]\",\n \"+pub struct TiptapParagraphAttrs {\",\n \"+ #[serde(default)]\",\n \"+ pub block_id: Option,\",\n \"+}\",\n \"+\",\n \"+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\",\n \"+#[serde(rename_all = \\\"camelCase\\\")]\",\n \"+pub struct TiptapListAttrs {\",\n \"+ #[serde(default)]\",\n \"+ pub block_id: Option,\",\n \"+}\",\n \"+\",\n \"+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\",\n \"+#[serde(rename_all = \\\"camelCase\\\")]\",\n \"+pub struct TiptapListItemAttrs {\",\n \"+ #[serde(default)]\",\n \"+ pub block_id: Option,\",\n \"+}\",\n \"+\",\n \"+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\",\n \"+#[serde(rename_all = \\\"camelCase\\\")]\",\n \"+pub struct TiptapBlockquoteAttrs {\",\n \"+ #[serde(default)]\",\n \"+ pub block_id: Option,\",\n \"+}\",\n \"+\",\n \"+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\",\n \"+#[serde(rename_all = \\\"camelCase\\\")]\",\n \"+pub struct TiptapCodeBlockAttrs {\",\n \"+ pub language: Option,\",\n \"+ pub block_id: Option,\",\n \"+}\",\n \"+\",\n \"+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\",\n \"+#[serde(rename_all = \\\"camelCase\\\")]\",\n \"+pub struct TiptapMark {\",\n \"+ pub r#type: String,\",\n \"+}\",\n \"+\",\n \"+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\",\n \"+#[serde(tag = \\\"type\\\", rename_all = \\\"camelCase\\\")]\",\n \"+pub enum TiptapNode {\",\n \"+ Doc { content: Vec },\",\n \"+ Paragraph { #[serde(default)] attrs: TiptapParagraphAttrs, #[serde(default)] content: Vec },\",\n \"+ Heading { attrs: TiptapHeadingAttrs, #[serde(default)] content: Vec },\",\n \"+ BulletList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec },\",\n \"+ OrderedList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec },\",\n \"+ TaskList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec },\",\n \"+ ListItem { #[serde(default)] attrs: TiptapListItemAttrs, #[serde(default)] content: Vec },\",\n \"+ TaskItem { attrs: TiptapTaskItemAttrs, #[serde(default)] content: Vec },\",\n \"+ Blockquote { #[serde(default)] attrs: TiptapBlockquoteAttrs, #[serde(default)] content: Vec },\",\n \"+ CodeBlock { attrs: TiptapCodeBlockAttrs, #[serde(default)] content: Vec },\",\n \"+ Text { text: String, #[serde(default)] marks: Vec },\",\n \"+ HardBreak,\",\n \"+}\",\n \"+\",\n \"+#[derive(Debug, Clone, PartialEq, Eq)]\",\n \"+pub enum EditorBlockDocumentTiptapError {\",\n \"+ UnsupportedNode(&'static str),\",\n \"+ InvalidDocument(&'static str),\",\n \"+}\",\n \"+\",\n \"+pub struct EditorBlockDocumentTiptapBridge;\",\n \"+\",\n \"+impl EditorBlockDocumentTiptapBridge {\",\n \"+ pub fn to_tiptap_doc(document: &EditorBlockDocument) -> Result {\",\n \"+ let mut content = Vec::new();\",\n \"+ for block_id in &document.root_block_ids {\",\n \"+ let block = document\",\n \"+ .blocks\",\n \"+ .iter()\",\n \"+ .find(|block| &block.block_id == block_id)\",\n \"+ .ok_or(EditorBlockDocumentTiptapError::InvalidDocument(\\\"根块不存在\\\"))?;\",\n \"+ content.push(block_to_tiptap_node(block)?);\",\n \"+ }\",\n \"+ Ok(TiptapNode::Doc { content })\",\n \"+ }\",\n \"+\",\n \"+ pub fn from_tiptap_doc(\",\n \"+ document_id: impl Into,\",\n \"+ doc: &TiptapNode,\",\n \"+ ) -> Result {\",\n \"+ match doc {\",\n \"+ TiptapNode::Doc { content } => {\",\n \"+ let mut blocks = Vec::new();\",\n \"+ let mut root_block_ids = Vec::new();\",\n \"+ for (index, node) in content.iter().enumerate() {\",\n \"+ let block = node_to_block(node, index)?;\",\n \"+ root_block_ids.push(block.block_id.clone());\",\n \"+ blocks.push(block);\",\n \"+ }\",\n \"+ Ok(EditorBlockDocument {\",\n \"+ document_id: document_id.into(),\",\n \"+ root_block_ids,\",\n \"+ blocks,\",\n \"+ })\",\n \"+ }\",\n \"+ _ => Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"顶层必须是 doc\\\")),\",\n \"+ }\",\n \"+ }\",\n \"+}\",\n \"+\",\n \"+fn block_to_tiptap_node(block: &EditorBlock) -> Result {\",\n \"+ let content = text_nodes_to_tiptap(&block.content_nodes)?;\",\n \"+ match block.block_type {\",\n \"+ EditorBlockType::Paragraph => Ok(TiptapNode::Paragraph {\",\n \"+ attrs: TiptapParagraphAttrs {\",\n \"+ block_id: Some(block.block_id.clone()),\",\n \"+ },\",\n \"+ content,\",\n \"+ }),\",\n \"+ EditorBlockType::Heading => Ok(TiptapNode::Heading {\",\n \"+ attrs: TiptapHeadingAttrs {\",\n \"+ level: block.props.heading_level.unwrap_or(1),\",\n \"+ block_id: Some(block.block_id.clone()),\",\n \"+ },\",\n \"+ content,\",\n \"+ }),\",\n \"+ EditorBlockType::BulletListItem => Ok(TiptapNode::BulletList {\",\n \"+ attrs: TiptapListAttrs {\",\n \"+ block_id: Some(block.block_id.clone()),\",\n \"+ },\",\n \"+ content: vec![TiptapNode::ListItem {\",\n \"+ attrs: TiptapListItemAttrs {\",\n \"+ block_id: Some(block.block_id.clone()),\",\n \"+ },\",\n \"+ content: vec![TiptapNode::Paragraph {\",\n \"+ attrs: TiptapParagraphAttrs {\",\n \"+ block_id: Some(block.block_id.clone()),\",\n \"+ },\",\n \"+ content,\",\n \"+ }],\",\n \"+ }],\",\n \"+ }),\",\n \"+ EditorBlockType::NumberedListItem => Ok(TiptapNode::OrderedList {\",\n \"+ attrs: TiptapListAttrs {\",\n \"+ block_id: Some(block.block_id.clone()),\",\n \"+ },\",\n \"+ content: vec![TiptapNode::ListItem {\",\n \"+ attrs: TiptapListItemAttrs {\",\n \"+ block_id: Some(block.block_id.clone()),\",\n \"+ },\",\n \"+ content: vec![TiptapNode::Paragraph {\",\n \"+ attrs: TiptapParagraphAttrs {\",\n \"+ block_id: Some(block.block_id.clone()),\",\n \"+ },\",\n \"+ content,\",\n \"+ }],\",\n \"+ }],\",\n \"+ }),\",\n \"+ EditorBlockType::Todo => Ok(TiptapNode::TaskList {\",\n \"+ attrs: TiptapListAttrs {\",\n \"+ block_id: Some(block.block_id.clone()),\",\n \"+ },\",\n \"+ content: vec![TiptapNode::TaskItem {\",\n \"+ attrs: TiptapTaskItemAttrs {\",\n \"+ checked: block.props.checked.unwrap_or(false),\",\n \"+ block_id: Some(block.block_id.clone()),\",\n \"+ },\",\n \"+ content: vec![TiptapNode::Paragraph {\",\n \"+ attrs: TiptapParagraphAttrs {\",\n \"+ block_id: Some(block.block_id.clone()),\",\n \"+ },\",\n \"+ content,\",\n \"+ }],\",\n \"+ }],\",\n \"+ }),\",\n \"+ EditorBlockType::Quote => Ok(TiptapNode::Blockquote {\",\n \"+ attrs: TiptapBlockquoteAttrs {\",\n \"+ block_id: Some(block.block_id.clone()),\",\n \"+ },\",\n \"+ content: vec![TiptapNode::Paragraph {\",\n \"+ attrs: TiptapParagraphAttrs {\",\n \"+ block_id: Some(block.block_id.clone()),\",\n \"+ },\",\n \"+ content,\",\n \"+ }],\",\n \"+ }),\",\n \"+ EditorBlockType::CodeBlock => Ok(TiptapNode::CodeBlock {\",\n \"+ attrs: TiptapCodeBlockAttrs {\",\n \"+ language: block.props.language.clone(),\",\n \"+ block_id: Some(block.block_id.clone()),\",\n \"+ },\",\n \"+ content,\",\n \"+ }),\",\n \"+ _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(\",\n \"+ \\\"当前块型暂不支持导出到 Tiptap\\\",\",\n \"+ )),\",\n \"+ }\",\n \"+}\",\n \"+\",\n \"+fn node_to_block(node: &TiptapNode, index: usize) -> Result {\",\n \"+ let fallback_block_id = format!(\\\"block_{}\\\", index + 1);\",\n \"+ match node {\",\n \"+ TiptapNode::Paragraph { attrs, content } => Ok(EditorBlock {\",\n \"+ block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),\",\n \"+ block_type: EditorBlockType::Paragraph,\",\n \"+ props: BlockProps::default(),\",\n \"+ content_nodes: text_nodes_from_tiptap(content)?,\",\n \"+ child_block_ids: vec![],\",\n \"+ }),\",\n \"+ TiptapNode::Heading { attrs, content } => Ok(EditorBlock {\",\n \"+ block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),\",\n \"+ block_type: EditorBlockType::Heading,\",\n \"+ props: BlockProps {\",\n \"+ heading_level: Some(attrs.level),\",\n \"+ ..BlockProps::default()\",\n \"+ },\",\n \"+ content_nodes: text_nodes_from_tiptap(content)?,\",\n \"+ child_block_ids: vec![],\",\n \"+ }),\",\n \"+ TiptapNode::BulletList { attrs, content } => list_node_to_block(\",\n \"+ attrs.block_id.clone().unwrap_or(fallback_block_id),\",\n \"+ EditorBlockType::BulletListItem,\",\n \"+ content,\",\n \"+ ),\",\n \"+ TiptapNode::OrderedList { attrs, content } => list_node_to_block(\",\n \"+ attrs.block_id.clone().unwrap_or(fallback_block_id),\",\n \"+ EditorBlockType::NumberedListItem,\",\n \"+ content,\",\n \"+ ),\",\n \"+ TiptapNode::TaskList { attrs, content } => list_task_node_to_block(\",\n \"+ attrs.block_id.clone().unwrap_or(fallback_block_id),\",\n \"+ content,\",\n \"+ ),\",\n \"+ TiptapNode::Blockquote { attrs, content } => Ok(EditorBlock {\",\n \"+ block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),\",\n \"+ block_type: EditorBlockType::Quote,\",\n \"+ props: BlockProps::default(),\",\n \"+ content_nodes: text_nodes_from_tiptap(extract_block_container_content(content)?)?,\",\n \"+ child_block_ids: vec![],\",\n \"+ }),\",\n \"+ TiptapNode::CodeBlock { attrs, content } => Ok(EditorBlock {\",\n \"+ block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),\",\n \"+ block_type: EditorBlockType::CodeBlock,\",\n \"+ props: BlockProps {\",\n \"+ language: attrs.language.clone(),\",\n \"+ ..BlockProps::default()\",\n \"+ },\",\n \"+ content_nodes: text_nodes_from_tiptap(content)?,\",\n \"+ child_block_ids: vec![],\",\n \"+ }),\",\n \"+ _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(\",\n \"+ \\\"当前节点暂不支持导入为 EditorBlockDocument\\\",\",\n \"+ )),\",\n \"+ }\",\n \"+}\",\n \"+\",\n \"+fn list_node_to_block(\",\n \"+ block_id: String,\",\n \"+ block_type: EditorBlockType,\",\n \"+ content: &[TiptapNode],\",\n \"+) -> Result {\",\n \"+ Ok(EditorBlock {\",\n \"+ block_id,\",\n \"+ block_type,\",\n \"+ props: BlockProps::default(),\",\n \"+ content_nodes: text_nodes_from_tiptap(extract_list_item_inline_content(content)?)?,\",\n \"+ child_block_ids: vec![],\",\n \"+ })\",\n \"+}\",\n \"+\",\n \"+fn list_task_node_to_block(\",\n \"+ block_id: String,\",\n \"+ content: &[TiptapNode],\",\n \"+) -> Result {\",\n \"+ let Some(first) = content.first() else {\",\n \"+ return Err(EditorBlockDocumentTiptapError::InvalidDocument(\",\n \"+ \\\"任务列表节点缺少 task_item\\\",\",\n \"+ ));\",\n \"+ };\",\n \"+ let (checked, item_content) = match first {\",\n \"+ TiptapNode::TaskItem { attrs, content } => (Some(attrs.checked), extract_block_container_content(content)?),\",\n \"+ _ => {\",\n \"+ return Err(EditorBlockDocumentTiptapError::InvalidDocument(\",\n \"+ \\\"任务列表节点首子节点类型不正确\\\",\",\n \"+ ))\",\n \"+ }\",\n \"+ };\",\n \"+ Ok(EditorBlock {\",\n \"+ block_id,\",\n \"+ block_type: EditorBlockType::Todo,\",\n \"+ props: BlockProps {\",\n \"+ checked,\",\n \"+ ..BlockProps::default()\",\n \"+ },\",\n \"+ content_nodes: text_nodes_from_tiptap(item_content)?,\",\n \"+ child_block_ids: vec![],\",\n \"+ })\",\n \"+}\",\n \"+\",\n \"+fn extract_list_item_inline_content(content: &[TiptapNode]) -> Result<&[TiptapNode], EditorBlockDocumentTiptapError> {\",\n \"+ let Some(first) = content.first() else {\",\n \"+ return Err(EditorBlockDocumentTiptapError::InvalidDocument(\",\n \"+ \\\"列表节点缺少 list_item/task_item\\\",\",\n \"+ ));\",\n \"+ };\",\n \"+ match first {\",\n \"+ TiptapNode::ListItem { content, .. } | TiptapNode::TaskItem { content, .. } => {\",\n \"+ extract_block_container_content(content)\",\n \"+ }\",\n \"+ _ => Err(EditorBlockDocumentTiptapError::InvalidDocument(\",\n \"+ \\\"列表节点首子节点类型不正确\\\",\",\n \"+ )),\",\n \"+ }\",\n \"+}\",\n \"+\",\n \"+fn extract_block_container_content(content: &[TiptapNode]) -> Result<&[TiptapNode], EditorBlockDocumentTiptapError> {\",\n \"+ let Some(first) = content.first() else {\",\n \"+ return Ok(content);\",\n \"+ };\",\n \"+ match first {\",\n \"+ TiptapNode::Paragraph { content, .. } => Ok(content),\",\n \"+ _ => Ok(content),\",\n \"+ }\",\n \"+}\",\n \"+\",\n \"+fn text_nodes_to_tiptap(nodes: &[ContentNode]) -> Result, EditorBlockDocumentTiptapError> {\",\n \"+ let mut out = Vec::new();\",\n \"+ for node in nodes {\",\n \"+ match &node.payload {\",\n \"+ ContentNodePayload::Text { text, marks } => out.push(TiptapNode::Text {\",\n \"+ text: text.clone(),\",\n \"+ marks: marks.iter().map(text_mark_to_tiptap).collect(),\",\n \"+ }),\",\n \"+ ContentNodePayload::HardBreak => out.push(TiptapNode::HardBreak),\",\n \"+ ContentNodePayload::ReferenceToken { .. } => {\",\n \"+ return Err(EditorBlockDocumentTiptapError::UnsupportedNode(\",\n \"+ \\\"引用 token 暂不映射到 Tiptap 文本合同\\\",\",\n \"+ ))\",\n \"+ }\",\n \"+ }\",\n \"+ }\",\n \"+ Ok(out)\",\n \"+}\",\n \"+\",\n \"+fn text_nodes_from_tiptap(nodes: &[TiptapNode]) -> Result, EditorBlockDocumentTiptapError> {\",\n \"+ let mut out = Vec::new();\",\n \"+ for node in nodes {\",\n \"+ match node {\",\n \"+ TiptapNode::Text { text, marks } => out.push(ContentNode {\",\n \"+ payload: ContentNodePayload::Text {\",\n \"+ text: text.clone(),\",\n \"+ marks: marks.iter().filter_map(tiptap_mark_to_text_mark).collect(),\",\n \"+ },\",\n \"+ attrs: BTreeMap::new(),\",\n \"+ }),\",\n \"+ TiptapNode::HardBreak => out.push(ContentNode {\",\n \"+ payload: ContentNodePayload::HardBreak,\",\n \"+ attrs: BTreeMap::new(),\",\n \"+ }),\",\n \"+ _ => {\",\n \"+ return Err(EditorBlockDocumentTiptapError::UnsupportedNode(\",\n \"+ \\\"当前只支持文本节点内容\\\",\",\n \"+ ))\",\n \"+ }\",\n \"+ }\",\n \"+ }\",\n \"+ Ok(out)\",\n \"+}\",\n \"+\",\n \"+fn text_mark_to_tiptap(mark: &TextMark) -> TiptapMark {\",\n \"+ let r#type = match mark {\",\n \"+ TextMark::Bold => \\\"bold\\\",\",\n \"+ TextMark::Italic => \\\"italic\\\",\",\n \"+ TextMark::Underline => \\\"underline\\\",\",\n \"+ TextMark::Strike => \\\"strike\\\",\",\n \"+ TextMark::Code => \\\"code\\\",\",\n \"+ };\",\n \"+ TiptapMark {\",\n \"+ r#type: r#type.to_string(),\",\n \"+ }\",\n \"+}\",\n \"+\",\n \"+fn tiptap_mark_to_text_mark(mark: &TiptapMark) -> Option {\",\n \"+ match mark.r#type.as_str() {\",\n \"+ \\\"bold\\\" => Some(TextMark::Bold),\",\n \"+ \\\"italic\\\" => Some(TextMark::Italic),\",\n \"+ \\\"underline\\\" => Some(TextMark::Underline),\",\n \"+ \\\"strike\\\" => Some(TextMark::Strike),\",\n \"+ \\\"code\\\" => Some(TextMark::Code),\",\n \"+ _ => None,\",\n \"+ }\",\n \"+}\"\n ]\n }\n ],\n \"originalFile\": \"use crate::editor::model::{\\n BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument, EditorBlockType,\\n};\\nuse serde::{Deserialize, Serialize};\\nuse std::collections::BTreeMap;\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"snake_case\\\")]\\npub enum TiptapBlockType {\\n Paragraph,\\n Heading,\\n BulletList,\\n OrderedList,\\n TaskList,\\n ListItem,\\n TaskItem,\\n Blockquote,\\n CodeBlock,\\n Text,\\n Doc,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapHeadingAttrs {\\n pub level: u8,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapTaskItemAttrs {\\n #[serde(default)]\\n pub checked: bool,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapCodeBlockAttrs {\\n pub language: Option,\\n pub block_id: Option,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(tag = \\\"type\\\", rename_all = \\\"snake_case\\\")]\\npub enum TiptapNode {\\n Doc { content: Vec },\\n Paragraph { #[serde(default)] attrs: BTreeMap, #[serde(default)] content: Vec },\\n Heading { attrs: TiptapHeadingAttrs, #[serde(default)] content: Vec },\\n BulletList { #[serde(default)] content: Vec },\\n OrderedList { #[serde(default)] content: Vec, #[serde(default)] attrs: BTreeMap },\\n TaskList { #[serde(default)] content: Vec },\\n ListItem { #[serde(default)] content: Vec },\\n TaskItem { attrs: TiptapTaskItemAttrs, #[serde(default)] content: Vec },\\n Blockquote { #[serde(default)] content: Vec },\\n CodeBlock { attrs: TiptapCodeBlockAttrs, #[serde(default)] content: Vec },\\n Text { text: String },\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq)]\\npub enum EditorBlockDocumentTiptapError {\\n UnsupportedNode(&'static str),\\n InvalidDocument(&'static str),\\n}\\n\\npub struct EditorBlockDocumentTiptapBridge;\\n\\nimpl EditorBlockDocumentTiptapBridge {\\n pub fn to_tiptap_doc(document: &EditorBlockDocument) -> Result {\\n let mut content = Vec::new();\\n for block_id in &document.root_block_ids {\\n let block = document.blocks.iter().find(|block| &block.block_id == block_id).ok_or(EditorBlockDocumentTiptapError::InvalidDocument(\\\"根块不存在\\\"))?;\\n content.push(block_to_tiptap_node(block)?);\\n }\\n Ok(TiptapNode::Doc { content })\\n }\\n\\n pub fn from_tiptap_doc(document_id: impl Into, doc: &TiptapNode) -> Result {\\n match doc {\\n TiptapNode::Doc { content } => {\\n let mut blocks = Vec::new();\\n let mut root_block_ids = Vec::new();\\n for (index, node) in content.iter().enumerate() {\\n let block = node_to_block(node, index)?;\\n root_block_ids.push(block.block_id.clone());\\n blocks.push(block);\\n }\\n Ok(EditorBlockDocument { document_id: document_id.into(), root_block_ids, blocks })\\n }\\n _ => Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"顶层必须是 doc\\\")),\\n }\\n }\\n}\\n\\nfn block_to_tiptap_node(block: &EditorBlock) -> Result {\\n let content = text_nodes_to_tiptap(&block.content_nodes)?;\\n match block.block_type {\\n EditorBlockType::Paragraph => Ok(TiptapNode::Paragraph { attrs: BTreeMap::new(), content }),\\n EditorBlockType::Heading => Ok(TiptapNode::Heading { attrs: TiptapHeadingAttrs { level: block.props.heading_level.unwrap_or(1) }, content }),\\n EditorBlockType::BulletListItem => Ok(TiptapNode::BulletList { content: vec![TiptapNode::ListItem { content }] }),\\n EditorBlockType::NumberedListItem => Ok(TiptapNode::OrderedList { attrs: BTreeMap::new(), content: vec![TiptapNode::ListItem { content }] }),\\n EditorBlockType::Todo => Ok(TiptapNode::TaskList { content: vec![TiptapNode::TaskItem { attrs: TiptapTaskItemAttrs { checked: block.props.checked.unwrap_or(false) }, content }] }),\\n EditorBlockType::Quote => Ok(TiptapNode::Blockquote { content }),\\n EditorBlockType::CodeBlock => Ok(TiptapNode::CodeBlock { attrs: TiptapCodeBlockAttrs { language: block.props.language.clone(), block_id: Some(block.block_id.clone()) }, content }),\\n _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\\"当前块型暂不支持导出到 Tiptap\\\")),\\n }\\n}\\n\\nfn node_to_block(node: &TiptapNode, index: usize) -> Result {\\n let block_id = format!(\\\"block_{}\\\", index + 1);\\n match node {\\n TiptapNode::Paragraph { content, .. } => Ok(EditorBlock { block_id, block_type: EditorBlockType::Paragraph, props: BlockProps::default(), content_nodes: text_nodes_from_tiptap(content)?, child_block_ids: vec![] }),\\n TiptapNode::Heading { attrs, content } => Ok(EditorBlock { block_id, block_type: EditorBlockType::Heading, props: BlockProps { heading_level: Some(attrs.level), ..BlockProps::default() }, content_nodes: text_nodes_from_tiptap(content)?, child_block_ids: vec![] }),\\n TiptapNode::BulletList { content } => list_node_to_block(block_id, EditorBlockType::BulletListItem, content),\\n TiptapNode::OrderedList { content, .. } => list_node_to_block(block_id, EditorBlockType::NumberedListItem, content),\\n TiptapNode::TaskList { content } => list_task_node_to_block(block_id, content),\\n TiptapNode::Blockquote { content } => Ok(EditorBlock { block_id, block_type: EditorBlockType::Quote, props: BlockProps::default(), content_nodes: text_nodes_from_tiptap(content)?, child_block_ids: vec![] }),\\n TiptapNode::CodeBlock { attrs, content } => Ok(EditorBlock { block_id, block_type: EditorBlockType::CodeBlock, props: BlockProps { language: attrs.language.clone(), ..BlockProps::default() }, content_nodes: text_nodes_from_tiptap(content)?, child_block_ids: vec![] }),\\n _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\\"当前节点暂不支持导入为 EditorBlockDocument\\\")),\\n }\\n}\\n\\nfn list_node_to_block(block_id: String, block_type: EditorBlockType, content: &[TiptapNode]) -> Result {\\n let Some(first) = content.first() else { return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"列表节点缺少 list_item/task_item\\\")); };\\n let item_content = match first { TiptapNode::ListItem { content } | TiptapNode::TaskItem { content, .. } => content, _ => return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"列表节点首子节点类型不正确\\\")), };\\n Ok(EditorBlock { block_id, block_type, props: BlockProps::default(), content_nodes: text_nodes_from_tiptap(item_content)?, child_block_ids: vec![] })\\n}\\n\\nfn list_task_node_to_block(block_id: String, content: &[TiptapNode]) -> Result {\\n let Some(first) = content.first() else { return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"任务列表节点缺少 task_item\\\")); };\\n let (checked, item_content) = match first { TiptapNode::TaskItem { attrs, content } => (Some(attrs.checked), content), _ => return Err(EditorBlockDocumentTiptapError::InvalidDocument(\\\"任务列表节点首子节点类型不正确\\\")), };\\n Ok(EditorBlock { block_id, block_type: EditorBlockType::Todo, props: BlockProps { checked, ..BlockProps::default() }, content_nodes: text_nodes_from_tiptap(item_content)?, child_block_ids: vec![] })\\n}\\n\\nfn text_nodes_to_tiptap(nodes: &[ContentNode]) -> Result, EditorBlockDocumentTiptapError> {\\n let mut out = Vec::new();\\n for node in nodes {\\n match &node.payload {\\n ContentNodePayload::Text { text, .. } => out.push(TiptapNode::Text { text: text.clone() }),\\n ContentNodePayload::HardBreak => out.push(TiptapNode::Text { text: \\\"\\\\n\\\".into() }),\\n ContentNodePayload::ReferenceToken { .. } => return Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\\"引用 token 暂不映射到 Tiptap 文本合同\\\")),\\n }\\n }\\n Ok(out)\\n}\\n\\nfn text_nodes_from_tiptap(nodes: &[TiptapNode]) -> Result, EditorBlockDocumentTiptapError> {\\n let mut out = Vec::new();\\n for node in nodes {\\n match node {\\n TiptapNode::Text { text } => out.push(ContentNode { payload: ContentNodePayload::Text { text: text.clone(), marks: vec![] }, attrs: BTreeMap::new() }),\\n _ => return Err(EditorBlockDocumentTiptapError::UnsupportedNode(\\\"当前只支持文本节点内容\\\")),\\n }\\n }\\n Ok(out)\\n}\\n\",\n \"gitDiff\": null\n}","tool_name":"write_file","tool_use_id":"call_68KYxSqjN040LSwFxLAx1C9V","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_DBPAQCZneHITgYQepEXFErEP","input":"{\"content\":\"use core_protocol::editor::{\\n BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument,\\n EditorBlockDocumentTiptapBridge, EditorBlockType, TextMark, TiptapCodeBlockAttrs,\\n TiptapHeadingAttrs, TiptapNode, TiptapParagraphAttrs, TiptapTaskItemAttrs,\\n};\\nuse std::collections::BTreeMap;\\n\\n#[test]\\nfn p05_blocks_round_trip_through_tiptap_doc() {\\n let document = EditorBlockDocument {\\n document_id: \\\"doc_1\\\".into(),\\n root_block_ids: vec![\\\"p1\\\".into(), \\\"h1\\\".into(), \\\"b1\\\".into(), \\\"n1\\\".into(), \\\"t1\\\".into(), \\\"q1\\\".into(), \\\"c1\\\".into()],\\n blocks: vec![\\n EditorBlock {\\n block_id: \\\"p1\\\".into(),\\n block_type: EditorBlockType::Paragraph,\\n props: BlockProps::default(),\\n content_nodes: vec![ContentNode {\\n payload: ContentNodePayload::Text {\\n text: \\\"段落\\\".into(),\\n marks: vec![TextMark::Bold],\\n },\\n attrs: BTreeMap::new(),\\n }],\\n child_block_ids: vec![],\\n },\\n EditorBlock {\\n block_id: \\\"h1\\\".into(),\\n block_type: EditorBlockType::Heading,\\n props: BlockProps {\\n heading_level: Some(2),\\n ..BlockProps::default()\\n },\\n content_nodes: vec![ContentNode {\\n payload: ContentNodePayload::Text {\\n text: \\\"标题\\\".into(),\\n marks: vec![],\\n },\\n attrs: BTreeMap::new(),\\n }],\\n child_block_ids: vec![],\\n },\\n EditorBlock {\\n block_id: \\\"b1\\\".into(),\\n block_type: EditorBlockType::BulletListItem,\\n props: BlockProps::default(),\\n content_nodes: vec![ContentNode {\\n payload: ContentNodePayload::Text {\\n text: \\\"项目\\\".into(),\\n marks: vec![],\\n },\\n attrs: BTreeMap::new(),\\n }],\\n child_block_ids: vec![],\\n },\\n EditorBlock {\\n block_id: \\\"n1\\\".into(),\\n block_type: EditorBlockType::NumberedListItem,\\n props: BlockProps::default(),\\n content_nodes: vec![ContentNode {\\n payload: ContentNodePayload::Text {\\n text: \\\"序号\\\".into(),\\n marks: vec![],\\n },\\n attrs: BTreeMap::new(),\\n }],\\n child_block_ids: vec![],\\n },\\n EditorBlock {\\n block_id: \\\"t1\\\".into(),\\n block_type: EditorBlockType::Todo,\\n props: BlockProps {\\n checked: Some(true),\\n ..BlockProps::default()\\n },\\n content_nodes: vec![ContentNode {\\n payload: ContentNodePayload::Text {\\n text: \\\"待办\\\".into(),\\n marks: vec![],\\n },\\n attrs: BTreeMap::new(),\\n }],\\n child_block_ids: vec![],\\n },\\n EditorBlock {\\n block_id: \\\"q1\\\".into(),\\n block_type: EditorBlockType::Quote,\\n props: BlockProps::default(),\\n content_nodes: vec![ContentNode {\\n payload: ContentNodePayload::Text {\\n text: \\\"引用\\\".into(),\\n marks: vec![],\\n },\\n attrs: BTreeMap::new(),\\n }],\\n child_block_ids: vec![],\\n },\\n EditorBlock {\\n block_id: \\\"c1\\\".into(),\\n block_type: EditorBlockType::CodeBlock,\\n props: BlockProps {\\n language: Some(\\\"rust\\\".into()),\\n ..BlockProps::default()\\n },\\n content_nodes: vec![ContentNode {\\n payload: ContentNodePayload::Text {\\n text: \\\"fn main() {}\\\".into(),\\n marks: vec![TextMark::Code],\\n },\\n attrs: BTreeMap::new(),\\n }],\\n child_block_ids: vec![],\\n },\\n ],\\n };\\n\\n let doc = EditorBlockDocumentTiptapBridge::to_tiptap_doc(&document).expect(\\\"export should work\\\");\\n let round_trip =\\n EditorBlockDocumentTiptapBridge::from_tiptap_doc(\\\"doc_2\\\", &doc).expect(\\\"import should work\\\");\\n\\n assert_eq!(round_trip.root_block_ids, vec![\\\"p1\\\", \\\"h1\\\", \\\"b1\\\", \\\"n1\\\", \\\"t1\\\", \\\"q1\\\", \\\"c1\\\"]);\\n assert_eq!(round_trip.blocks[0].block_id, \\\"p1\\\");\\n assert_eq!(round_trip.blocks[0].content_nodes.len(), 1);\\n match &round_trip.blocks[0].content_nodes[0].payload {\\n ContentNodePayload::Text { marks, .. } => assert_eq!(marks, &vec![TextMark::Bold]),\\n other => panic!(\\\"unexpected payload: {other:?}\\\"),\\n }\\n assert_eq!(round_trip.blocks[1].props.heading_level, Some(2));\\n assert_eq!(round_trip.blocks[4].props.checked, Some(true));\\n assert_eq!(round_trip.blocks[6].props.language.as_deref(), Some(\\\"rust\\\"));\\n}\\n\\n#[test]\\nfn tiptap_import_preserves_explicit_block_ids_and_marks() {\\n let doc = TiptapNode::Doc {\\n content: vec![\\n TiptapNode::Paragraph {\\n attrs: TiptapParagraphAttrs {\\n block_id: Some(\\\"p-1\\\".into()),\\n },\\n content: vec![TiptapNode::Text {\\n text: \\\"Hello\\\".into(),\\n marks: vec![core_protocol::editor::TiptapMark {\\n r#type: \\\"italic\\\".into(),\\n }],\\n }],\\n },\\n TiptapNode::Heading {\\n attrs: TiptapHeadingAttrs {\\n level: 3,\\n block_id: Some(\\\"h-1\\\".into()),\\n },\\n content: vec![TiptapNode::Text {\\n text: \\\"H\\\".into(),\\n marks: vec![],\\n }],\\n },\\n TiptapNode::TaskList {\\n attrs: core_protocol::editor::TiptapListAttrs {\\n block_id: Some(\\\"t-1\\\".into()),\\n },\\n content: vec![TiptapNode::TaskItem {\\n attrs: TiptapTaskItemAttrs {\\n checked: true,\\n block_id: Some(\\\"t-1\\\".into()),\\n },\\n content: vec![TiptapNode::Paragraph {\\n attrs: TiptapParagraphAttrs {\\n block_id: Some(\\\"t-1\\\".into()),\\n },\\n content: vec![TiptapNode::Text {\\n text: \\\"todo\\\".into(),\\n marks: vec![],\\n }],\\n }],\\n }],\\n },\\n TiptapNode::CodeBlock {\\n attrs: TiptapCodeBlockAttrs {\\n language: Some(\\\"ts\\\".into()),\\n block_id: Some(\\\"c-1\\\".into()),\\n },\\n content: vec![TiptapNode::Text {\\n text: \\\"let x = 1;\\\".into(),\\n marks: vec![core_protocol::editor::TiptapMark {\\n r#type: \\\"code\\\".into(),\\n }],\\n }],\\n },\\n ],\\n };\\n\\n let imported = EditorBlockDocumentTiptapBridge::from_tiptap_doc(\\\"doc_3\\\", &doc).unwrap();\\n assert_eq!(imported.root_block_ids, vec![\\\"p-1\\\", \\\"h-1\\\", \\\"t-1\\\", \\\"c-1\\\"]);\\n assert_eq!(imported.blocks[0].block_id, \\\"p-1\\\");\\n assert_eq!(imported.blocks[1].props.heading_level, Some(3));\\n assert_eq!(imported.blocks[2].props.checked, Some(true));\\n assert_eq!(imported.blocks[3].props.language.as_deref(), Some(\\\"ts\\\"));\\n\\n match &imported.blocks[0].content_nodes[0].payload {\\n ContentNodePayload::Text { marks, .. } => assert_eq!(marks, &vec![TextMark::Italic]),\\n other => panic!(\\\"unexpected payload: {other:?}\\\"),\\n }\\n}\\n\",\"path\":\"/mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs\"}","name":"write_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":215126,"output_tokens":2144}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"type\": \"update\",\n \"filePath\": \"/mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs\",\n \"content\": \"use core_protocol::editor::{\\n BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument,\\n EditorBlockDocumentTiptapBridge, EditorBlockType, TextMark, TiptapCodeBlockAttrs,\\n TiptapHeadingAttrs, TiptapNode, TiptapParagraphAttrs, TiptapTaskItemAttrs,\\n};\\nuse std::collections::BTreeMap;\\n\\n#[test]\\nfn p05_blocks_round_trip_through_tiptap_doc() {\\n let document = EditorBlockDocument {\\n document_id: \\\"doc_1\\\".into(),\\n root_block_ids: vec![\\\"p1\\\".into(), \\\"h1\\\".into(), \\\"b1\\\".into(), \\\"n1\\\".into(), \\\"t1\\\".into(), \\\"q1\\\".into(), \\\"c1\\\".into()],\\n blocks: vec![\\n EditorBlock {\\n block_id: \\\"p1\\\".into(),\\n block_type: EditorBlockType::Paragraph,\\n props: BlockProps::default(),\\n content_nodes: vec![ContentNode {\\n payload: ContentNodePayload::Text {\\n text: \\\"段落\\\".into(),\\n marks: vec![TextMark::Bold],\\n },\\n attrs: BTreeMap::new(),\\n }],\\n child_block_ids: vec![],\\n },\\n EditorBlock {\\n block_id: \\\"h1\\\".into(),\\n block_type: EditorBlockType::Heading,\\n props: BlockProps {\\n heading_level: Some(2),\\n ..BlockProps::default()\\n },\\n content_nodes: vec![ContentNode {\\n payload: ContentNodePayload::Text {\\n text: \\\"标题\\\".into(),\\n marks: vec![],\\n },\\n attrs: BTreeMap::new(),\\n }],\\n child_block_ids: vec![],\\n },\\n EditorBlock {\\n block_id: \\\"b1\\\".into(),\\n block_type: EditorBlockType::BulletListItem,\\n props: BlockProps::default(),\\n content_nodes: vec![ContentNode {\\n payload: ContentNodePayload::Text {\\n text: \\\"项目\\\".into(),\\n marks: vec![],\\n },\\n attrs: BTreeMap::new(),\\n }],\\n child_block_ids: vec![],\\n },\\n EditorBlock {\\n block_id: \\\"n1\\\".into(),\\n block_type: EditorBlockType::NumberedListItem,\\n props: BlockProps::default(),\\n content_nodes: vec![ContentNode {\\n payload: ContentNodePayload::Text {\\n text: \\\"序号\\\".into(),\\n marks: vec![],\\n },\\n attrs: BTreeMap::new(),\\n }],\\n child_block_ids: vec![],\\n },\\n EditorBlock {\\n block_id: \\\"t1\\\".into(),\\n block_type: EditorBlockType::Todo,\\n props: BlockProps {\\n checked: Some(true),\\n ..BlockProps::default()\\n },\\n content_nodes: vec![ContentNode {\\n payload: ContentNodePayload::Text {\\n text: \\\"待办\\\".into(),\\n marks: vec![],\\n },\\n attrs: BTreeMap::new(),\\n }],\\n child_block_ids: vec![],\\n },\\n EditorBlock {\\n block_id: \\\"q1\\\".into(),\\n block_type: EditorBlockType::Quote,\\n props: BlockProps::default(),\\n content_nodes: vec![ContentNode {\\n payload: ContentNodePayload::Text {\\n text: \\\"引用\\\".into(),\\n marks: vec![],\\n },\\n attrs: BTreeMap::new(),\\n }],\\n child_block_ids: vec![],\\n },\\n EditorBlock {\\n block_id: \\\"c1\\\".into(),\\n block_type: EditorBlockType::CodeBlock,\\n props: BlockProps {\\n language: Some(\\\"rust\\\".into()),\\n ..BlockProps::default()\\n },\\n content_nodes: vec![ContentNode {\\n payload: ContentNodePayload::Text {\\n text: \\\"fn main() {}\\\".into(),\\n marks: vec![TextMark::Code],\\n },\\n attrs: BTreeMap::new(),\\n }],\\n child_block_ids: vec![],\\n },\\n ],\\n };\\n\\n let doc = EditorBlockDocumentTiptapBridge::to_tiptap_doc(&document).expect(\\\"export should work\\\");\\n let round_trip =\\n EditorBlockDocumentTiptapBridge::from_tiptap_doc(\\\"doc_2\\\", &doc).expect(\\\"import should work\\\");\\n\\n assert_eq!(round_trip.root_block_ids, vec![\\\"p1\\\", \\\"h1\\\", \\\"b1\\\", \\\"n1\\\", \\\"t1\\\", \\\"q1\\\", \\\"c1\\\"]);\\n assert_eq!(round_trip.blocks[0].block_id, \\\"p1\\\");\\n assert_eq!(round_trip.blocks[0].content_nodes.len(), 1);\\n match &round_trip.blocks[0].content_nodes[0].payload {\\n ContentNodePayload::Text { marks, .. } => assert_eq!(marks, &vec![TextMark::Bold]),\\n other => panic!(\\\"unexpected payload: {other:?}\\\"),\\n }\\n assert_eq!(round_trip.blocks[1].props.heading_level, Some(2));\\n assert_eq!(round_trip.blocks[4].props.checked, Some(true));\\n assert_eq!(round_trip.blocks[6].props.language.as_deref(), Some(\\\"rust\\\"));\\n}\\n\\n#[test]\\nfn tiptap_import_preserves_explicit_block_ids_and_marks() {\\n let doc = TiptapNode::Doc {\\n content: vec![\\n TiptapNode::Paragraph {\\n attrs: TiptapParagraphAttrs {\\n block_id: Some(\\\"p-1\\\".into()),\\n },\\n content: vec![TiptapNode::Text {\\n text: \\\"Hello\\\".into(),\\n marks: vec![core_protocol::editor::TiptapMark {\\n r#type: \\\"italic\\\".into(),\\n }],\\n }],\\n },\\n TiptapNode::Heading {\\n attrs: TiptapHeadingAttrs {\\n level: 3,\\n block_id: Some(\\\"h-1\\\".into()),\\n },\\n content: vec![TiptapNode::Text {\\n text: \\\"H\\\".into(),\\n marks: vec![],\\n }],\\n },\\n TiptapNode::TaskList {\\n attrs: core_protocol::editor::TiptapListAttrs {\\n block_id: Some(\\\"t-1\\\".into()),\\n },\\n content: vec![TiptapNode::TaskItem {\\n attrs: TiptapTaskItemAttrs {\\n checked: true,\\n block_id: Some(\\\"t-1\\\".into()),\\n },\\n content: vec![TiptapNode::Paragraph {\\n attrs: TiptapParagraphAttrs {\\n block_id: Some(\\\"t-1\\\".into()),\\n },\\n content: vec![TiptapNode::Text {\\n text: \\\"todo\\\".into(),\\n marks: vec![],\\n }],\\n }],\\n }],\\n },\\n TiptapNode::CodeBlock {\\n attrs: TiptapCodeBlockAttrs {\\n language: Some(\\\"ts\\\".into()),\\n block_id: Some(\\\"c-1\\\".into()),\\n },\\n content: vec![TiptapNode::Text {\\n text: \\\"let x = 1;\\\".into(),\\n marks: vec![core_protocol::editor::TiptapMark {\\n r#type: \\\"code\\\".into(),\\n }],\\n }],\\n },\\n ],\\n };\\n\\n let imported = EditorBlockDocumentTiptapBridge::from_tiptap_doc(\\\"doc_3\\\", &doc).unwrap();\\n assert_eq!(imported.root_block_ids, vec![\\\"p-1\\\", \\\"h-1\\\", \\\"t-1\\\", \\\"c-1\\\"]);\\n assert_eq!(imported.blocks[0].block_id, \\\"p-1\\\");\\n assert_eq!(imported.blocks[1].props.heading_level, Some(3));\\n assert_eq!(imported.blocks[2].props.checked, Some(true));\\n assert_eq!(imported.blocks[3].props.language.as_deref(), Some(\\\"ts\\\"));\\n\\n match &imported.blocks[0].content_nodes[0].payload {\\n ContentNodePayload::Text { marks, .. } => assert_eq!(marks, &vec![TextMark::Italic]),\\n other => panic!(\\\"unexpected payload: {other:?}\\\"),\\n }\\n}\\n\",\n \"structuredPatch\": [\n {\n \"oldStart\": 1,\n \"oldLines\": 49,\n \"newStart\": 1,\n \"newLines\": 204,\n \"lines\": [\n \"-use core_protocol::editor::{\",\n \"- BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument,\",\n \"- EditorBlockDocumentTiptapBridge, EditorBlockType, TiptapCodeBlockAttrs, TiptapHeadingAttrs,\",\n \"- TiptapNode, TiptapTaskItemAttrs,\",\n \"-};\",\n \"-use std::collections::BTreeMap;\",\n \"-\",\n \"-#[test]\",\n \"-fn p05_blocks_round_trip_through_tiptap_doc() {\",\n \"- let document = EditorBlockDocument {\",\n \"- document_id: \\\"doc_1\\\".into(),\",\n \"- root_block_ids: vec![\\\"p1\\\".into(), \\\"h1\\\".into(), \\\"b1\\\".into(), \\\"n1\\\".into(), \\\"t1\\\".into(), \\\"q1\\\".into(), \\\"c1\\\".into()],\",\n \"- blocks: vec![\",\n \"- EditorBlock { block_id: \\\"p1\\\".into(), block_type: EditorBlockType::Paragraph, props: BlockProps::default(), content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"段落\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\",\n \"- EditorBlock { block_id: \\\"h1\\\".into(), block_type: EditorBlockType::Heading, props: BlockProps { heading_level: Some(2), ..BlockProps::default() }, content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"标题\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\",\n \"- EditorBlock { block_id: \\\"b1\\\".into(), block_type: EditorBlockType::BulletListItem, props: BlockProps::default(), content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"项目\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\",\n \"- EditorBlock { block_id: \\\"n1\\\".into(), block_type: EditorBlockType::NumberedListItem, props: BlockProps::default(), content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"序号\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\",\n \"- EditorBlock { block_id: \\\"t1\\\".into(), block_type: EditorBlockType::Todo, props: BlockProps { checked: Some(true), ..BlockProps::default() }, content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"待办\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\",\n \"- EditorBlock { block_id: \\\"q1\\\".into(), block_type: EditorBlockType::Quote, props: BlockProps::default(), content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"引用\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\",\n \"- EditorBlock { block_id: \\\"c1\\\".into(), block_type: EditorBlockType::CodeBlock, props: BlockProps { language: Some(\\\"rust\\\".into()), ..BlockProps::default() }, content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"fn main() {}\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\",\n \"- ],\",\n \"- };\",\n \"-\",\n \"- let doc = EditorBlockDocumentTiptapBridge::to_tiptap_doc(&document).expect(\\\"export should work\\\");\",\n \"- let round_trip = EditorBlockDocumentTiptapBridge::from_tiptap_doc(\\\"doc_2\\\", &doc).expect(\\\"import should work\\\");\",\n \"-\",\n \"- assert_eq!(round_trip.blocks.len(), 7);\",\n \"- assert_eq!(round_trip.root_block_ids.len(), 7);\",\n \"- assert_eq!(round_trip.blocks[0].block_type, EditorBlockType::Paragraph);\",\n \"- assert_eq!(round_trip.blocks[1].props.heading_level, Some(2));\",\n \"- assert_eq!(round_trip.blocks[4].props.checked, Some(true));\",\n \"- assert_eq!(round_trip.blocks[6].props.language.as_deref(), Some(\\\"rust\\\"));\",\n \"-}\",\n \"-\",\n \"-#[test]\",\n \"-fn tiptap_list_and_code_block_attrs_preserve_contract_edges() {\",\n \"- let doc = TiptapNode::Doc {\",\n \"- content: vec![\",\n \"- TiptapNode::Heading { attrs: TiptapHeadingAttrs { level: 3 }, content: vec![TiptapNode::Text { text: \\\"H\\\".into() }] },\",\n \"- TiptapNode::TaskList { content: vec![TiptapNode::TaskItem { attrs: TiptapTaskItemAttrs { checked: true }, content: vec![TiptapNode::Text { text: \\\"todo\\\".into() }] }] },\",\n \"- TiptapNode::CodeBlock { attrs: TiptapCodeBlockAttrs { language: Some(\\\"ts\\\".into()), block_id: Some(\\\"c1\\\".into()) }, content: vec![TiptapNode::Text { text: \\\"let x = 1;\\\".into() }] },\",\n \"- ],\",\n \"- };\",\n \"- let imported = EditorBlockDocumentTiptapBridge::from_tiptap_doc(\\\"doc_3\\\", &doc).unwrap();\",\n \"- assert_eq!(imported.blocks[0].props.heading_level, Some(3));\",\n \"- assert_eq!(imported.blocks[1].props.checked, Some(true));\",\n \"- assert_eq!(imported.blocks[2].props.language.as_deref(), Some(\\\"ts\\\"));\",\n \"- assert_eq!(imported.blocks[2].block_id, \\\"block_3\\\");\",\n \"-}\",\n \"+use core_protocol::editor::{\",\n \"+ BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument,\",\n \"+ EditorBlockDocumentTiptapBridge, EditorBlockType, TextMark, TiptapCodeBlockAttrs,\",\n \"+ TiptapHeadingAttrs, TiptapNode, TiptapParagraphAttrs, TiptapTaskItemAttrs,\",\n \"+};\",\n \"+use std::collections::BTreeMap;\",\n \"+\",\n \"+#[test]\",\n \"+fn p05_blocks_round_trip_through_tiptap_doc() {\",\n \"+ let document = EditorBlockDocument {\",\n \"+ document_id: \\\"doc_1\\\".into(),\",\n \"+ root_block_ids: vec![\\\"p1\\\".into(), \\\"h1\\\".into(), \\\"b1\\\".into(), \\\"n1\\\".into(), \\\"t1\\\".into(), \\\"q1\\\".into(), \\\"c1\\\".into()],\",\n \"+ blocks: vec![\",\n \"+ EditorBlock {\",\n \"+ block_id: \\\"p1\\\".into(),\",\n \"+ block_type: EditorBlockType::Paragraph,\",\n \"+ props: BlockProps::default(),\",\n \"+ content_nodes: vec![ContentNode {\",\n \"+ payload: ContentNodePayload::Text {\",\n \"+ text: \\\"段落\\\".into(),\",\n \"+ marks: vec![TextMark::Bold],\",\n \"+ },\",\n \"+ attrs: BTreeMap::new(),\",\n \"+ }],\",\n \"+ child_block_ids: vec![],\",\n \"+ },\",\n \"+ EditorBlock {\",\n \"+ block_id: \\\"h1\\\".into(),\",\n \"+ block_type: EditorBlockType::Heading,\",\n \"+ props: BlockProps {\",\n \"+ heading_level: Some(2),\",\n \"+ ..BlockProps::default()\",\n \"+ },\",\n \"+ content_nodes: vec![ContentNode {\",\n \"+ payload: ContentNodePayload::Text {\",\n \"+ text: \\\"标题\\\".into(),\",\n \"+ marks: vec![],\",\n \"+ },\",\n \"+ attrs: BTreeMap::new(),\",\n \"+ }],\",\n \"+ child_block_ids: vec![],\",\n \"+ },\",\n \"+ EditorBlock {\",\n \"+ block_id: \\\"b1\\\".into(),\",\n \"+ block_type: EditorBlockType::BulletListItem,\",\n \"+ props: BlockProps::default(),\",\n \"+ content_nodes: vec![ContentNode {\",\n \"+ payload: ContentNodePayload::Text {\",\n \"+ text: \\\"项目\\\".into(),\",\n \"+ marks: vec![],\",\n \"+ },\",\n \"+ attrs: BTreeMap::new(),\",\n \"+ }],\",\n \"+ child_block_ids: vec![],\",\n \"+ },\",\n \"+ EditorBlock {\",\n \"+ block_id: \\\"n1\\\".into(),\",\n \"+ block_type: EditorBlockType::NumberedListItem,\",\n \"+ props: BlockProps::default(),\",\n \"+ content_nodes: vec![ContentNode {\",\n \"+ payload: ContentNodePayload::Text {\",\n \"+ text: \\\"序号\\\".into(),\",\n \"+ marks: vec![],\",\n \"+ },\",\n \"+ attrs: BTreeMap::new(),\",\n \"+ }],\",\n \"+ child_block_ids: vec![],\",\n \"+ },\",\n \"+ EditorBlock {\",\n \"+ block_id: \\\"t1\\\".into(),\",\n \"+ block_type: EditorBlockType::Todo,\",\n \"+ props: BlockProps {\",\n \"+ checked: Some(true),\",\n \"+ ..BlockProps::default()\",\n \"+ },\",\n \"+ content_nodes: vec![ContentNode {\",\n \"+ payload: ContentNodePayload::Text {\",\n \"+ text: \\\"待办\\\".into(),\",\n \"+ marks: vec![],\",\n \"+ },\",\n \"+ attrs: BTreeMap::new(),\",\n \"+ }],\",\n \"+ child_block_ids: vec![],\",\n \"+ },\",\n \"+ EditorBlock {\",\n \"+ block_id: \\\"q1\\\".into(),\",\n \"+ block_type: EditorBlockType::Quote,\",\n \"+ props: BlockProps::default(),\",\n \"+ content_nodes: vec![ContentNode {\",\n \"+ payload: ContentNodePayload::Text {\",\n \"+ text: \\\"引用\\\".into(),\",\n \"+ marks: vec![],\",\n \"+ },\",\n \"+ attrs: BTreeMap::new(),\",\n \"+ }],\",\n \"+ child_block_ids: vec![],\",\n \"+ },\",\n \"+ EditorBlock {\",\n \"+ block_id: \\\"c1\\\".into(),\",\n \"+ block_type: EditorBlockType::CodeBlock,\",\n \"+ props: BlockProps {\",\n \"+ language: Some(\\\"rust\\\".into()),\",\n \"+ ..BlockProps::default()\",\n \"+ },\",\n \"+ content_nodes: vec![ContentNode {\",\n \"+ payload: ContentNodePayload::Text {\",\n \"+ text: \\\"fn main() {}\\\".into(),\",\n \"+ marks: vec![TextMark::Code],\",\n \"+ },\",\n \"+ attrs: BTreeMap::new(),\",\n \"+ }],\",\n \"+ child_block_ids: vec![],\",\n \"+ },\",\n \"+ ],\",\n \"+ };\",\n \"+\",\n \"+ let doc = EditorBlockDocumentTiptapBridge::to_tiptap_doc(&document).expect(\\\"export should work\\\");\",\n \"+ let round_trip =\",\n \"+ EditorBlockDocumentTiptapBridge::from_tiptap_doc(\\\"doc_2\\\", &doc).expect(\\\"import should work\\\");\",\n \"+\",\n \"+ assert_eq!(round_trip.root_block_ids, vec![\\\"p1\\\", \\\"h1\\\", \\\"b1\\\", \\\"n1\\\", \\\"t1\\\", \\\"q1\\\", \\\"c1\\\"]);\",\n \"+ assert_eq!(round_trip.blocks[0].block_id, \\\"p1\\\");\",\n \"+ assert_eq!(round_trip.blocks[0].content_nodes.len(), 1);\",\n \"+ match &round_trip.blocks[0].content_nodes[0].payload {\",\n \"+ ContentNodePayload::Text { marks, .. } => assert_eq!(marks, &vec![TextMark::Bold]),\",\n \"+ other => panic!(\\\"unexpected payload: {other:?}\\\"),\",\n \"+ }\",\n \"+ assert_eq!(round_trip.blocks[1].props.heading_level, Some(2));\",\n \"+ assert_eq!(round_trip.blocks[4].props.checked, Some(true));\",\n \"+ assert_eq!(round_trip.blocks[6].props.language.as_deref(), Some(\\\"rust\\\"));\",\n \"+}\",\n \"+\",\n \"+#[test]\",\n \"+fn tiptap_import_preserves_explicit_block_ids_and_marks() {\",\n \"+ let doc = TiptapNode::Doc {\",\n \"+ content: vec![\",\n \"+ TiptapNode::Paragraph {\",\n \"+ attrs: TiptapParagraphAttrs {\",\n \"+ block_id: Some(\\\"p-1\\\".into()),\",\n \"+ },\",\n \"+ content: vec![TiptapNode::Text {\",\n \"+ text: \\\"Hello\\\".into(),\",\n \"+ marks: vec![core_protocol::editor::TiptapMark {\",\n \"+ r#type: \\\"italic\\\".into(),\",\n \"+ }],\",\n \"+ }],\",\n \"+ },\",\n \"+ TiptapNode::Heading {\",\n \"+ attrs: TiptapHeadingAttrs {\",\n \"+ level: 3,\",\n \"+ block_id: Some(\\\"h-1\\\".into()),\",\n \"+ },\",\n \"+ content: vec![TiptapNode::Text {\",\n \"+ text: \\\"H\\\".into(),\",\n \"+ marks: vec![],\",\n \"+ }],\",\n \"+ },\",\n \"+ TiptapNode::TaskList {\",\n \"+ attrs: core_protocol::editor::TiptapListAttrs {\",\n \"+ block_id: Some(\\\"t-1\\\".into()),\",\n \"+ },\",\n \"+ content: vec![TiptapNode::TaskItem {\",\n \"+ attrs: TiptapTaskItemAttrs {\",\n \"+ checked: true,\",\n \"+ block_id: Some(\\\"t-1\\\".into()),\",\n \"+ },\",\n \"+ content: vec![TiptapNode::Paragraph {\",\n \"+ attrs: TiptapParagraphAttrs {\",\n \"+ block_id: Some(\\\"t-1\\\".into()),\",\n \"+ },\",\n \"+ content: vec![TiptapNode::Text {\",\n \"+ text: \\\"todo\\\".into(),\",\n \"+ marks: vec![],\",\n \"+ }],\",\n \"+ }],\",\n \"+ }],\",\n \"+ },\",\n \"+ TiptapNode::CodeBlock {\",\n \"+ attrs: TiptapCodeBlockAttrs {\",\n \"+ language: Some(\\\"ts\\\".into()),\",\n \"+ block_id: Some(\\\"c-1\\\".into()),\",\n \"+ },\",\n \"+ content: vec![TiptapNode::Text {\",\n \"+ text: \\\"let x = 1;\\\".into(),\",\n \"+ marks: vec![core_protocol::editor::TiptapMark {\",\n \"+ r#type: \\\"code\\\".into(),\",\n \"+ }],\",\n \"+ }],\",\n \"+ },\",\n \"+ ],\",\n \"+ };\",\n \"+\",\n \"+ let imported = EditorBlockDocumentTiptapBridge::from_tiptap_doc(\\\"doc_3\\\", &doc).unwrap();\",\n \"+ assert_eq!(imported.root_block_ids, vec![\\\"p-1\\\", \\\"h-1\\\", \\\"t-1\\\", \\\"c-1\\\"]);\",\n \"+ assert_eq!(imported.blocks[0].block_id, \\\"p-1\\\");\",\n \"+ assert_eq!(imported.blocks[1].props.heading_level, Some(3));\",\n \"+ assert_eq!(imported.blocks[2].props.checked, Some(true));\",\n \"+ assert_eq!(imported.blocks[3].props.language.as_deref(), Some(\\\"ts\\\"));\",\n \"+\",\n \"+ match &imported.blocks[0].content_nodes[0].payload {\",\n \"+ ContentNodePayload::Text { marks, .. } => assert_eq!(marks, &vec![TextMark::Italic]),\",\n \"+ other => panic!(\\\"unexpected payload: {other:?}\\\"),\",\n \"+ }\",\n \"+}\"\n ]\n }\n ],\n \"originalFile\": \"use core_protocol::editor::{\\n BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument,\\n EditorBlockDocumentTiptapBridge, EditorBlockType, TiptapCodeBlockAttrs, TiptapHeadingAttrs,\\n TiptapNode, TiptapTaskItemAttrs,\\n};\\nuse std::collections::BTreeMap;\\n\\n#[test]\\nfn p05_blocks_round_trip_through_tiptap_doc() {\\n let document = EditorBlockDocument {\\n document_id: \\\"doc_1\\\".into(),\\n root_block_ids: vec![\\\"p1\\\".into(), \\\"h1\\\".into(), \\\"b1\\\".into(), \\\"n1\\\".into(), \\\"t1\\\".into(), \\\"q1\\\".into(), \\\"c1\\\".into()],\\n blocks: vec![\\n EditorBlock { block_id: \\\"p1\\\".into(), block_type: EditorBlockType::Paragraph, props: BlockProps::default(), content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"段落\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\\n EditorBlock { block_id: \\\"h1\\\".into(), block_type: EditorBlockType::Heading, props: BlockProps { heading_level: Some(2), ..BlockProps::default() }, content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"标题\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\\n EditorBlock { block_id: \\\"b1\\\".into(), block_type: EditorBlockType::BulletListItem, props: BlockProps::default(), content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"项目\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\\n EditorBlock { block_id: \\\"n1\\\".into(), block_type: EditorBlockType::NumberedListItem, props: BlockProps::default(), content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"序号\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\\n EditorBlock { block_id: \\\"t1\\\".into(), block_type: EditorBlockType::Todo, props: BlockProps { checked: Some(true), ..BlockProps::default() }, content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"待办\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\\n EditorBlock { block_id: \\\"q1\\\".into(), block_type: EditorBlockType::Quote, props: BlockProps::default(), content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"引用\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\\n EditorBlock { block_id: \\\"c1\\\".into(), block_type: EditorBlockType::CodeBlock, props: BlockProps { language: Some(\\\"rust\\\".into()), ..BlockProps::default() }, content_nodes: vec![ContentNode { payload: ContentNodePayload::Text { text: \\\"fn main() {}\\\".into(), marks: vec![] }, attrs: BTreeMap::new() }], child_block_ids: vec![] },\\n ],\\n };\\n\\n let doc = EditorBlockDocumentTiptapBridge::to_tiptap_doc(&document).expect(\\\"export should work\\\");\\n let round_trip = EditorBlockDocumentTiptapBridge::from_tiptap_doc(\\\"doc_2\\\", &doc).expect(\\\"import should work\\\");\\n\\n assert_eq!(round_trip.blocks.len(), 7);\\n assert_eq!(round_trip.root_block_ids.len(), 7);\\n assert_eq!(round_trip.blocks[0].block_type, EditorBlockType::Paragraph);\\n assert_eq!(round_trip.blocks[1].props.heading_level, Some(2));\\n assert_eq!(round_trip.blocks[4].props.checked, Some(true));\\n assert_eq!(round_trip.blocks[6].props.language.as_deref(), Some(\\\"rust\\\"));\\n}\\n\\n#[test]\\nfn tiptap_list_and_code_block_attrs_preserve_contract_edges() {\\n let doc = TiptapNode::Doc {\\n content: vec![\\n TiptapNode::Heading { attrs: TiptapHeadingAttrs { level: 3 }, content: vec![TiptapNode::Text { text: \\\"H\\\".into() }] },\\n TiptapNode::TaskList { content: vec![TiptapNode::TaskItem { attrs: TiptapTaskItemAttrs { checked: true }, content: vec![TiptapNode::Text { text: \\\"todo\\\".into() }] }] },\\n TiptapNode::CodeBlock { attrs: TiptapCodeBlockAttrs { language: Some(\\\"ts\\\".into()), block_id: Some(\\\"c1\\\".into()) }, content: vec![TiptapNode::Text { text: \\\"let x = 1;\\\".into() }] },\\n ],\\n };\\n let imported = EditorBlockDocumentTiptapBridge::from_tiptap_doc(\\\"doc_3\\\", &doc).unwrap();\\n assert_eq!(imported.blocks[0].props.heading_level, Some(3));\\n assert_eq!(imported.blocks[1].props.checked, Some(true));\\n assert_eq!(imported.blocks[2].props.language.as_deref(), Some(\\\"ts\\\"));\\n assert_eq!(imported.blocks[2].block_id, \\\"block_3\\\");\\n}\\n\",\n \"gitDiff\": null\n}","tool_name":"write_file","tool_use_id":"call_DBPAQCZneHITgYQepEXFErEP","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_5tSeXRRnKxq8OcNZLDsyYQbT","input":"{\"allowedMounts\":[\"/mnt/Data1T/mnote\"],\"command\":\"cd /mnt/Data1T/mnote/wolai-frontend && pnpm vitest run src/lib/documents/tiptap-content-converter.test.ts\",\"dangerouslyDisableSandbox\":false,\"description\":\"Run frontend tiptap converter tests\",\"filesystemMode\":\"workspace-only\",\"isolateNetwork\":true,\"namespaceRestrictions\":true,\"run_in_background\":false,\"timeout\":120000}","name":"bash","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":224557,"output_tokens":105}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"stdout\": \"\\n\\u001b[1m\\u001b[46m RUN \\u001b[49m\\u001b[22m \\u001b[36mv4.0.13 \\u001b[39m\\u001b[90m/mnt/Data1T/mnote/wolai-frontend\\u001b[39m\\n\\n \\u001b[32m✓\\u001b[39m src/lib/documents/tiptap-content-converter.test.ts \\u001b[2m(\\u001b[22m\\u001b[2m3 tests\\u001b[22m\\u001b[2m)\\u001b[22m\\u001b[32m 4\\u001b[2mms\\u001b[22m\\u001b[39m\\n\\n\\u001b[2m Test Files \\u001b[22m \\u001b[1m\\u001b[32m1 passed\\u001b[39m\\u001b[22m\\u001b[90m (1)\\u001b[39m\\n\\u001b[2m Tests \\u001b[22m \\u001b[1m\\u001b[32m3 passed\\u001b[39m\\u001b[22m\\u001b[90m (3)\\u001b[39m\\n\\u001b[2m Start at \\u001b[22m 15:21:36\\n\\u001b[2m Duration \\u001b[22m 555ms\\u001b[2m (transform 38ms, setup 0ms, collect 47ms, tests 4ms, environment 343ms, prepare 6ms)\\u001b[22m\\n\\n\",\n \"stderr\": \"\",\n \"rawOutputPath\": null,\n \"interrupted\": false,\n \"isImage\": null,\n \"backgroundTaskId\": null,\n \"backgroundedByUser\": null,\n \"assistantAutoBackgrounded\": null,\n \"dangerouslyDisableSandbox\": false,\n \"returnCodeInterpretation\": null,\n \"noOutputExpected\": false,\n \"structuredContent\": null,\n \"persistedOutputPath\": null,\n \"persistedOutputSize\": null,\n \"sandboxStatus\": {\n \"enabled\": true,\n \"requested\": {\n \"enabled\": true,\n \"namespace_restrictions\": true,\n \"network_isolation\": true,\n \"filesystem_mode\": \"workspace-only\",\n \"allowed_mounts\": [\n \"/mnt/Data1T/mnote\"\n ]\n },\n \"supported\": true,\n \"active\": true,\n \"namespace_supported\": true,\n \"namespace_active\": true,\n \"network_supported\": true,\n \"network_active\": true,\n \"filesystem_mode\": \"workspace-only\",\n \"filesystem_active\": true,\n \"allowed_mounts\": [\n \"/mnt/Data1T/mnote\"\n ],\n \"in_container\": false,\n \"container_markers\": [],\n \"fallback_reason\": null\n }\n}","tool_name":"bash","tool_use_id":"call_5tSeXRRnKxq8OcNZLDsyYQbT","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_DLBDRkfnBMtGAKq24Uf5Ubpn","input":"{\"allowedMounts\":[\"/mnt/Data1T/mnote\"],\"command\":\"cd /mnt/Data1T/mnote/rust && cargo test -p core-protocol editor_tiptap_bridge\",\"dangerouslyDisableSandbox\":false,\"description\":\"Run Rust core protocol tiptap bridge tests\",\"filesystemMode\":\"workspace-only\",\"isolateNetwork\":true,\"namespaceRestrictions\":true,\"run_in_background\":false,\"timeout\":120000}","name":"bash","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":225322,"output_tokens":99}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"stdout\": \"\",\n \"stderr\": \"info: syncing channel updates for 1.89.0-x86_64-unknown-linux-gnu\\nerror: could not download file from 'https://static.rust-lang.org/dist/channel-rust-1.89.0.toml.sha256' to '/mnt/Data1T/mnote/.sandbox-home/.rustup/tmp/lawxmbwfnlri3t4y_file': error downloading file: error sending request for url (https://static.rust-lang.org/dist/channel-rust-1.89.0.toml.sha256): client error (Connect): dns error: failed to lookup address information: Temporary failure in name resolution\\n\",\n \"rawOutputPath\": null,\n \"interrupted\": false,\n \"isImage\": null,\n \"backgroundTaskId\": null,\n \"backgroundedByUser\": null,\n \"assistantAutoBackgrounded\": null,\n \"dangerouslyDisableSandbox\": false,\n \"returnCodeInterpretation\": \"exit_code:1\",\n \"noOutputExpected\": false,\n \"structuredContent\": null,\n \"persistedOutputPath\": null,\n \"persistedOutputSize\": null,\n \"sandboxStatus\": {\n \"enabled\": true,\n \"requested\": {\n \"enabled\": true,\n \"namespace_restrictions\": true,\n \"network_isolation\": true,\n \"filesystem_mode\": \"workspace-only\",\n \"allowed_mounts\": [\n \"/mnt/Data1T/mnote\"\n ]\n },\n \"supported\": true,\n \"active\": true,\n \"namespace_supported\": true,\n \"namespace_active\": true,\n \"network_supported\": true,\n \"network_active\": true,\n \"filesystem_mode\": \"workspace-only\",\n \"filesystem_active\": true,\n \"allowed_mounts\": [\n \"/mnt/Data1T/mnote\"\n ],\n \"in_container\": false,\n \"container_markers\": [],\n \"fallback_reason\": null\n }\n}","tool_name":"bash","tool_use_id":"call_DLBDRkfnBMtGAKq24Uf5Ubpn","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"id":"call_uNQTqJXsxPwjSM85yuWxmhw2","input":"{\"todos\":[{\"activeForm\":\"审查当前主编辑器接入与保存链路的相关代码和测试\",\"content\":\"审查当前主编辑器接入与保存链路的相关代码和测试\",\"status\":\"completed\"},{\"activeForm\":\"修正默认进入编辑态与主编辑器 host 选择,确保 3000 文档页默认使用 tiptap 而非 blocknote\",\"content\":\"修正默认进入编辑态与主编辑器 host 选择,确保 3000 文档页默认使用 tiptap 而非 blocknote\",\"status\":\"completed\"},{\"activeForm\":\"修正 tiptap 与 Rust truth 的最小 block_id/load/save 合同\",\"content\":\"修正 tiptap 与 Rust truth 的最小 block_id/load/save 合同\",\"status\":\"completed\"},{\"activeForm\":\"补充并运行针对主编辑器切换与编辑链路的测试\",\"content\":\"补充并运行针对主编辑器切换与编辑链路的测试\",\"status\":\"in_progress\"}]}","name":"TodoWrite","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":225847,"output_tokens":222}},"type":"message"} +{"message":{"blocks":[{"is_error":false,"output":"{\n \"oldTodos\": [\n {\n \"content\": \"审查当前主编辑器接入与保存链路的相关代码和测试\",\n \"activeForm\": \"审查当前主编辑器接入与保存链路的相关代码和测试\",\n \"status\": \"in_progress\"\n },\n {\n \"content\": \"修正默认进入编辑态与主编辑器 host 选择,确保 3000 文档页默认使用 tiptap 而非 blocknote\",\n \"activeForm\": \"修正默认进入编辑态与主编辑器 host 选择,确保 3000 文档页默认使用 tiptap 而非 blocknote\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"修正 tiptap 与 Rust truth 的最小 block_id/load/save 合同\",\n \"activeForm\": \"修正 tiptap 与 Rust truth 的最小 block_id/load/save 合同\",\n \"status\": \"pending\"\n },\n {\n \"content\": \"补充并运行针对主编辑器切换与编辑链路的测试\",\n \"activeForm\": \"补充并运行针对主编辑器切换与编辑链路的测试\",\n \"status\": \"pending\"\n }\n ],\n \"newTodos\": [\n {\n \"content\": \"审查当前主编辑器接入与保存链路的相关代码和测试\",\n \"activeForm\": \"审查当前主编辑器接入与保存链路的相关代码和测试\",\n \"status\": \"completed\"\n },\n {\n \"content\": \"修正默认进入编辑态与主编辑器 host 选择,确保 3000 文档页默认使用 tiptap 而非 blocknote\",\n \"activeForm\": \"修正默认进入编辑态与主编辑器 host 选择,确保 3000 文档页默认使用 tiptap 而非 blocknote\",\n \"status\": \"completed\"\n },\n {\n \"content\": \"修正 tiptap 与 Rust truth 的最小 block_id/load/save 合同\",\n \"activeForm\": \"修正 tiptap 与 Rust truth 的最小 block_id/load/save 合同\",\n \"status\": \"completed\"\n },\n {\n \"content\": \"补充并运行针对主编辑器切换与编辑链路的测试\",\n \"activeForm\": \"补充并运行针对主编辑器切换与编辑链路的测试\",\n \"status\": \"in_progress\"\n }\n ],\n \"verificationNudgeNeeded\": null\n}","tool_name":"TodoWrite","tool_use_id":"call_uNQTqJXsxPwjSM85yuWxmhw2","type":"tool_result"}],"role":"tool"},"type":"message"} +{"message":{"blocks":[{"text":"我已经继续往你要的检查点推进了一步,核心目标是:\n\n- **3000 打开文档页默认就是可编辑态**\n- **主编辑器入口是 Tiptap host,不再以 BlockNote 为主**\n- **开始把 Tiptap <-> Rust truth 的最小 block/load/save 合同纠正过来**\n- **去掉“阅读态只是默认妥协”的产品表现**\n\n---\n\n# 我这次已经改了什么\n\n## 1. 文档页默认进入编辑态\n我把 `DocumentContent` 的默认状态改成了:\n\n- 只要不是只读页面,就**默认进入编辑态**\n- 不再依赖 `edit=1` 或 `openTableId` 才进入编辑\n- 也去掉了“当前为阅读态,编辑器仅在进入编辑后挂载”的提示文案\n\n也就是说,方向已经变成了你要的:\n\n**像 wolai 一样,打开就是编辑。**\n\n---\n\n## 2. 主编辑器仍然走 `EditorHost -> LeptosTiptapEditorHost`\n当前 3000 主链上,`DocumentContent` 仍然挂载的是:\n\n- `EditorHost`\n- 其实际实现是 `LeptosTiptapEditorHost`\n\n所以从入口组织上,已经是 **Tiptap host 替代 BlockNote 主入口** 的方向。\n\n---\n\n## 3. 我把前端 converter 从“散装 block-like”推进成“最小 EditorBlockDocument 形态”\n我重写了:\n\n- `wolai-frontend/src/lib/documents/tiptap-content-converter.ts`\n\n现在它不再只是随便处理:\n\n- `id/type/content/props`\n\n而是先收敛成一个最小的前端 `EditorBlockDocument` 形态,再做:\n\n- `EditorBlockDocument -> TiptapDoc`\n- `TiptapDoc -> EditorBlockDocument`\n\n并且修正了几个关键点:\n\n### 已修正\n- `blockId` 稳定保留,不再 roundtrip 后变成 `block-0`\n- 支持的最小块型:\n - paragraph\n - heading\n - bullet_list_item\n - numbered_list_item\n - todo\n - quote\n - code_block\n- 支持最小 marks 映射\n - bold\n - italic\n - underline\n - strike\n - code\n\n这一步的意义是:\n**前端不再继续把 Tiptap 和旧 blocknote 风格散装 JSON 混在一起。**\n\n---\n\n## 4. Rust `core-protocol` 的 tiptap bridge 我也修正了最小合同\n我重写了:\n\n- `rust/crates/core-protocol/src/editor/tiptap.rs`\n\n重点修了这几件事:\n\n### block_id 不再默认只按 index 重编\n现在 import 时会优先读 attrs 里的 `block_id`,不再无脑:\n\n```rust\nblock_1 / block_2 / block_3\n```\n\n### 导出时把 block_id 写回 Tiptap attrs\n已给这些节点补了 block_id attrs:\n\n- paragraph\n- heading\n- bulletList / orderedList / taskList\n- listItem / taskItem\n- blockquote\n- codeBlock\n\n### 文本 marks 开始 roundtrip\nRust 侧已经补了:\n\n- `TextMark <-> TiptapMark`\n\n### hard break 不再硬塞成普通文本 `\\n`\n现在有单独 `HardBreak`\n\n---\n\n## 5. 我补了测试,并把错误测试口径纠正了\n\n### 前端测试\n重写了:\n\n- `wolai-frontend/src/lib/documents/tiptap-content-converter.test.ts`\n\n现在测试覆盖了:\n\n- empty doc 仍保持可编辑 doc 形态\n- paragraph roundtrip 保持稳定 block id\n- P0.5 最小块族 roundtrip\n\n### Rust 测试\n重写了:\n\n- `rust/crates/core-protocol/tests/editor_tiptap_bridge.rs`\n\n现在测试覆盖了:\n\n- block_id 保持\n- heading/todo/code_block 属性保持\n- text marks roundtrip\n- 显式 block id 导入保持稳定\n\n---\n\n# 已验证结果\n\n## 前端 vitest\n我已经跑过,**通过**:\n\n- `pnpm vitest run src/lib/documents/tiptap-content-converter.test.ts`\n\n结果:3 tests passed\n\n---\n\n# 当前还没完全打透的地方\n\n这次我优先修的是**方向和合同**,但离你要的最终检查点,还差几步关键工作。\n\n---\n\n## 1. 页面上虽然默认编辑了,但 `DocumentContent` 里“阅读态分支”还没彻底删干净\n现在我做的是:\n\n- 默认 `isEditing = true`\n- 隐藏了阅读态提示\n- 按钮区也不再给正常入口暴露“进入编辑 / 返回阅读”\n\n但组件内部仍保留了:\n\n- `isEditing`\n- `keepEditorMounted`\n- `DocumentReadView` 分支\n\n所以现在更准确说是:\n\n**默认总是编辑,但代码结构里仍残留阅读态兼容分支。**\n\n这不影响你现在追的产品检查点,但后面最好继续清理。\n\n---\n\n## 2. 保存链路还没有完全变成“Rust truth 直通”\n现在最重要但还没完全收口的一点是:\n\n`LeptosTiptapEditorHost` 里保存时仍然是:\n\n- iframe 发 payload\n- 前端把 Tiptap doc 转成 blocks\n- POST 到 `/api/documents/save`\n\n而 `/api/documents/save` 当前仍吃的是:\n\n- `content: Json`\n\n也就是说,虽然我把 converter 修成了更像 `EditorBlockDocument` 的方向,\n但**页面保存 contract 还没正式改成共享 `EditorBlockDocument` 载荷**。\n\n这是下一步必须继续做的。\n\n---\n\n## 3. iframe 初始化还是 query string 传 content\n这个问题我还没动:\n\n- 仍然是 iframe URL 带 `content=...`\n\n这对最终产品化不够好,后面应该改成:\n\n- iframe src 只传 documentId/workspaceId/embedded\n- 内容初始化走 postMessage\n\n---\n\n## 4. Rust 测试没法在当前沙箱里完整跑\n我尝试跑了:\n\n- `cargo test -p core-protocol editor_tiptap_bridge`\n\n但失败原因不是代码编译错误,而是:\n\n- 当前 sandbox 无网络\n- rustup 想同步 `1.89.0` toolchain\n- DNS 失败\n\n所以目前我能诚实汇报的是:\n\n- **前端测试已运行并通过**\n- **Rust 测试文件已更新,但本次未在当前环境完成执行验证**\n\n---\n\n# 对你当前检查点的判断\n\n你要的检查点是:\n\n> 3000端口打开后,我检查发现 tiptap 已经替换 blocknote 作为主编辑器了,并且可以正常编辑,没有现在的阅读状态\n\n按这个标准看:\n\n## 现在已经接近,但我不会假装说“已经彻底完成”\n因为还差两个现实层面的确认:\n\n1. **3000 实页 smoke / 人工打开验证**\n - 确认 `/documents/[id]` 打开就是 tiptap host\n - 不是隐藏挂载、也不是仍显示 read view\n\n2. **host 真实编辑保存链验证**\n - 确认输入内容后真的能保存回去\n - 刷新后仍保留\n - 不是只改 iframe 内存态\n\n---\n\n# 下一步我建议继续直接做\n\n我建议马上继续推进这两件事,才会真正达到你说的“检查点”:\n\n## A. 彻底去掉主路径阅读态\n直接把 `DocumentContent` 主路径改成:\n\n- 非只读页面只渲染 `EditorHost`\n- `DocumentReadView` 只保留给真正共享只读页\n- 不再保留主路径的读写切换按钮\n\n## B. 把主编辑器保存链改成正式的 `EditorBlockDocument` payload\n也就是:\n\n- `LeptosTiptapEditorHost` 保存时直接生成 `EditorBlockDocument`\n- `/api/documents/save` 接受这个合同\n- Rust side / bridge side 显式按它处理\n- 再做一次 refresh/re-enter 验证\n\n---\n\n# 这次实际改到的文件\n\n主要包括:\n\n- `wolai-frontend/src/components/editor/document-content.tsx`\n- `wolai-frontend/src/lib/documents/tiptap-content-converter.ts`\n- `wolai-frontend/src/lib/documents/tiptap-content-converter.test.ts`\n- `rust/crates/core-protocol/src/editor/tiptap.rs`\n- `rust/crates/core-protocol/tests/editor_tiptap_bridge.rs`\n\n---\n\n# 当前状态一句话总结\n\n**我已经把默认“打开即编辑”这条产品方向切过来了,并把 Tiptap <-> Rust truth 的最小 block_id/adapter 合同纠偏了一大步;但要真正达到你说的 3000 页面可检查验收,还需要我继续把阅读态主路径彻底删掉,并打通正式保存刷新链。**\n\n我建议我下一条就直接继续做这两步,不停在分析层。","type":"text"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":226618,"output_tokens":2041}},"type":"message"} diff --git a/.claw/sessions/570ce2f4c13b9ce5/session-1776666102061-0.rot-1776672611353.jsonl b/.claw/sessions/570ce2f4c13b9ce5/session-1776666102061-0.rot-1776672611353.jsonl new file mode 100644 index 00000000..29a3104f --- /dev/null +++ b/.claw/sessions/570ce2f4c13b9ce5/session-1776666102061-0.rot-1776672611353.jsonl @@ -0,0 +1,96 @@ +{"created_at_ms":1776666102060,"model":"gpt-5.4","session_id":"session-1776666102061-0","type":"session_meta","updated_at_ms":1776670931648,"version":1,"workspace_root":"/mnt/Data1T/mnote"} +{"count":5,"removed_message_count":2,"summary":"\nConversation summary:\n- Previously compacted context:\n - Previously compacted context:\n - Previously compacted context:\n - Previously compacted context:\n - Scope: 7 earlier messages compacted (user=1, assistant=3, tool=3).\n - Tools mentioned: glob_search, read_file.\n - Recent user requests:\n - 请检查/mnt/Data1T/mnote/design,看看当下我们需要最优先推进什么\n - Key files referenced: /design/cankao/filetree.md, /design/cankao/yemianshu.md, /mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md, /mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md, /mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md.\n - Current work: 请检查/mnt/Data1T/mnote/design,看看当下我们需要最优先推进什么\n - Newly compacted context:\n - Scope: 22 earlier messages compacted (user=1, assistant=11, tool=10).\n - Tools mentioned: read_file.\n - Recent user requests:\n - 当前git中已经有一些是在推进了,请检查一下其完成的是否正确。\n - Pending work:\n - 我看了当前 `design/` 里和主线最相关的文档,结论很明确:\n ## 当前最优先推进的,不是树域、不是 Rust Web 总体、也不是 AI\n 而是:\n # **05-editor-mainline / P0.5:把 `leptos-tiptap` 主编辑器真正接入真实文档页,并接上 Rust truth**\n 最…\n - Key files referenced: /mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md, /mnt/Data1T/mnote/design/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md, /mnt/Data1T/mnote/harness-tasks.json, /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.test.ts.\n - Current work: 当前git中已经有一些是在推进了,请检查一下其完成的是否正确。\n - Newly compacted context:\n - Scope: 72 earlier messages compacted (user=1, assistant=36, tool=35).\n - Tools mentioned: TodoWrite, bash, edit_file, glob_search, grep_search, read_file, write_file.\n - Recent user requests:\n - 请按你的想法,继续推进完成,我想要达到的检查点是:3000端口打开后,我检查发现tiptap已经替换blocknote作为主编辑器了,并且可以正常编辑,没有现在的阅读状态,我们所有前端都是直接对标wolai的,wolai直接打开就能编辑,阅读状态只是一种妥协。\n - Pending work:\n - 我检查了当前 git 里这批“正在推进主编辑器 cutover”的改动。结论先说:\n # 结论\n **这批改动方向基本对,但完成度并不算“正确完成”。**\n 更准确地说是:\n - **task-002 这一层接入真实页面 host 的方向是对的**\n - 但 **task-003 ~ task-006 所依赖的“Rust t…\n - Key files referenced: /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs, /mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx, /mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/save/route.ts, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-shell.tsx.\n - Current work: 请按你的想法,继续推进完成,我想要达到的检查点是:3000端口打开后,我检查发现tiptap已经替换blocknote作为主编辑器了,并且可以正常编辑,没有现在的阅读状态,我们所有前端都是直接对标wolai的,wolai直接打开就能编辑,阅读状态只是一种妥协。\n - Newly compacted context:\n - Scope: 28 earlier messages compacted (user=1, assistant=14, tool=13).\n - Tools mentioned: TodoWrite, bash, grep_search, read_file.\n - Recent user requests:\n - 我手动测试发现:/mnt/Data1T/mnote/tmp/image copy 22.png,显示有问题,只显示了上面一小部分,而且所有页面都是使用的一个tiptap页面,内容都一样。离真正可用差很远,我们之前是存储在convex中的,你是否没有挂存储。\n - Pending work:\n - 我已经继续往你要的检查点推进了一步,核心目标是:\n - **3000 打开文档页默认就是可编辑态**\n - **主编辑器入口是 Tiptap host,不再以 BlockNote 为主**\n - **开始把 Tiptap <-> Rust truth 的最小 block/load/save 合同纠正过来**\n - **去掉“…\n - Key files referenced: /mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/content/route.ts, /mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/save/route.ts, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-subtree-response.ts, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/save-contract.ts, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.ts.\n - Current work: 我手动测试发现:/mnt/Data1T/mnote/tmp/image copy 22.png,显示有问题,只显示了上面一小部分,而且所有页面都是使用的一个tiptap页面,内容都一样。离真正可用差很远,我们之前是存储在convex中的,你是否没有挂存储。\n- Newly compacted context:\n - Scope: 2 earlier messages compacted (user=0, assistant=1, tool=1).\n - Tools mentioned: read_file.\n - Key files referenced: /mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host-types.ts.\n- Key timeline:\n - assistant: tool_use read_file({\"limit\":260,\"offset\":1,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host-types.ts\"})\n - tool: tool_result read_file: {\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host-types.ts\",\n \"conte…\n","type":"compaction"} +{"text":"请检查/mnt/Data1T/mnote/design,看看当下我们需要最优先推进什么","timestamp_ms":1776666141936,"type":"prompt_history"} +{"text":"当前git中已经有一些是在推进了,请检查一下其完成的是否正确。","timestamp_ms":1776667640453,"type":"prompt_history"} +{"text":"请按你的想法,继续推进完成,我想要达到的检查点是:3000端口打开后,我检查发现tiptap已经替换blocknote作为主编辑器了,并且可以正常编辑,没有现在的阅读状态,我们所有前端都是直接对标wolai的,wolai直接打开就能编辑,阅读状态只是一种妥协。","timestamp_ms":1776668500096,"type":"prompt_history"} +{"text":"我手动测试发现:/mnt/Data1T/mnote/tmp/image copy 22.png,显示有问题,只显示了上面一小部分,而且所有页面都是使用的一个tiptap页面,内容都一样。离真正可用差很远,我们之前是存储在convex中的,你是否没有挂存储。","timestamp_ms":1776670439374,"type":"prompt_history"} +{"text":"所以,请你规划任务,真正实现“tiptap 已替换 blocknote 成为可用主编辑器,再让我验收","timestamp_ms":1776670763211,"type":"prompt_history"} +{"message":{"blocks":[{"text":"This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.\n\nSummary:\nConversation summary:\n- Previously compacted context:\n - Previously compacted context:\n - Previously compacted context:\n - Previously compacted context:\n - Scope: 7 earlier messages compacted (user=1, assistant=3, tool=3).\n - Tools mentioned: glob_search, read_file.\n - Recent user requests:\n - 请检查/mnt/Data1T/mnote/design,看看当下我们需要最优先推进什么\n - Key files referenced: /design/cankao/filetree.md, /design/cankao/yemianshu.md, /mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md, /mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md, /mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md, /mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md.\n - Current work: 请检查/mnt/Data1T/mnote/design,看看当下我们需要最优先推进什么\n - Newly compacted context:\n - Scope: 22 earlier messages compacted (user=1, assistant=11, tool=10).\n - Tools mentioned: read_file.\n - Recent user requests:\n - 当前git中已经有一些是在推进了,请检查一下其完成的是否正确。\n - Pending work:\n - 我看了当前 `design/` 里和主线最相关的文档,结论很明确:\n ## 当前最优先推进的,不是树域、不是 Rust Web 总体、也不是 AI\n 而是:\n # **05-editor-mainline / P0.5:把 `leptos-tiptap` 主编辑器真正接入真实文档页,并接上 Rust truth**\n 最…\n - Key files referenced: /mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md, /mnt/Data1T/mnote/design/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md, /mnt/Data1T/mnote/harness-tasks.json, /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.test.ts.\n - Current work: 当前git中已经有一些是在推进了,请检查一下其完成的是否正确。\n - Newly compacted context:\n - Scope: 72 earlier messages compacted (user=1, assistant=36, tool=35).\n - Tools mentioned: TodoWrite, bash, edit_file, glob_search, grep_search, read_file, write_file.\n - Recent user requests:\n - 请按你的想法,继续推进完成,我想要达到的检查点是:3000端口打开后,我检查发现tiptap已经替换blocknote作为主编辑器了,并且可以正常编辑,没有现在的阅读状态,我们所有前端都是直接对标wolai的,wolai直接打开就能编辑,阅读状态只是一种妥协。\n - Pending work:\n - 我检查了当前 git 里这批“正在推进主编辑器 cutover”的改动。结论先说:\n # 结论\n **这批改动方向基本对,但完成度并不算“正确完成”。**\n 更准确地说是:\n - **task-002 这一层接入真实页面 host 的方向是对的**\n - 但 **task-003 ~ task-006 所依赖的“Rust t…\n - Key files referenced: /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs, /mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs, /mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx, /mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/save/route.ts, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-shell.tsx.\n - Current work: 请按你的想法,继续推进完成,我想要达到的检查点是:3000端口打开后,我检查发现tiptap已经替换blocknote作为主编辑器了,并且可以正常编辑,没有现在的阅读状态,我们所有前端都是直接对标wolai的,wolai直接打开就能编辑,阅读状态只是一种妥协。\n - Newly compacted context:\n - Scope: 28 earlier messages compacted (user=1, assistant=14, tool=13).\n - Tools mentioned: TodoWrite, bash, grep_search, read_file.\n - Recent user requests:\n - 我手动测试发现:/mnt/Data1T/mnote/tmp/image copy 22.png,显示有问题,只显示了上面一小部分,而且所有页面都是使用的一个tiptap页面,内容都一样。离真正可用差很远,我们之前是存储在convex中的,你是否没有挂存储。\n - Pending work:\n - 我已经继续往你要的检查点推进了一步,核心目标是:\n - **3000 打开文档页默认就是可编辑态**\n - **主编辑器入口是 Tiptap host,不再以 BlockNote 为主**\n - **开始把 Tiptap <-> Rust truth 的最小 block/load/save 合同纠正过来**\n - **去掉“…\n - Key files referenced: /mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/content/route.ts, /mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/save/route.ts, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/blocknote-editor.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-subtree-response.ts, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/save-contract.ts, /mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tiptap-content-converter.ts.\n - Current work: 我手动测试发现:/mnt/Data1T/mnote/tmp/image copy 22.png,显示有问题,只显示了上面一小部分,而且所有页面都是使用的一个tiptap页面,内容都一样。离真正可用差很远,我们之前是存储在convex中的,你是否没有挂存储。\n- Newly compacted context:\n - Scope: 2 earlier messages compacted (user=0, assistant=1, tool=1).\n - Tools mentioned: read_file.\n - Key files referenced: /mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host-types.ts.\n- Key timeline:\n - assistant: tool_use read_file({\"limit\":260,\"offset\":1,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host-types.ts\"})\n - tool: tool_result read_file: {\n \"type\": \"text\",\n \"file\": {\n \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host-types.ts\",\n \"conte…\n\nRecent messages are preserved verbatim.\nContinue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, and do not preface with continuation text.","type":"text"}],"role":"system"},"type":"message"} +{"message":{"blocks":[{"text":"你这个判断是对的。 \n**我前一轮并没有把真实存储链正确挂上。**\n\n而且从你描述的两个现象看,问题基本已经定位得很清楚了:\n\n---\n\n# 现在的两个核心问题\n\n## 1) “只显示上面一小部分”\n这大概率不是内容本身,而是 **iframe 嵌入高度同步没做好**。\n\n当前 `LeptosTiptapEditorHost` 只是:\n\n- 在页面里渲染一个 `