{"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":"<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</summary>","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<string, unknown>;\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 <LeptosTiptapEditor {...props} />;\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<string, unknown>).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<CustomBlockSchema>): 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<CustomBlockSchema>[]): TocEntry[] => {\\n  const counters = [0, 0, 0, 0, 0];\\n  const entries: TocEntry[] = [];\\n\\n  const walk = (targetBlocks: Block<CustomBlockSchema>[]) => {\\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<CustomBlockSchema>[]);\\n      }\\n    });\\n  };\\n\\n  walk(blocks);\\n  return entries;\\n};\\n\\nconst findBlockById = (\\n  blocks: Block<CustomBlockSchema>[],\\n  id: string,\\n): Block<CustomBlockSchema> | 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<CustomBlockSchema>[], id);\\n      if (child) return child;\\n    }\\n  }\\n  return undefined;\\n};\\n\\nconst syncProgressMeters = (editorInstance: ReturnType<typeof useCreateBlockNote>) => {\\n  if (!editorInstance) {\\n    return;\\n  }\\n  const blocks = editorInstance.topLevelBlocks as Block<CustomBlockSchema>[];\\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<CustomBlockSchema>[]) => {\\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<CustomBlockSchema>[]);\\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<string | null>(null);\\n  const [tocEntries, setTocEntries] = useState<TocEntry[]>([]);\\n  const [fullScreenTableId, setFullScreenTableId] = useState<string | null>(null);\\n  const isFullScreenTableOpen = fullScreenTableId !== null;\\n  const revisionRef = useRef<number | null>(initialRevision);\\n  const conflictDetectionKeyRef = useRef<string | null>(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<string, number> = {};\\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      <div className=\\\"flex h-64 items-center justify-center text-sm text-gray-400\\\">编辑器加载中...</div>\\n    ),\\n  },\\n) as ComponentType<DocumentEditorHostProps>;\\n\\nexport function EditorHost(props: DocumentEditorHostProps) {\\n  return <LeptosTiptapEditor {...props} />;\\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<T = unknown> = {\\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<HTMLIFrameElement | null>(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<number | null>(props.initialRevision ?? null);\\n  const conflictDetectionKeyRef = useRef<string | null>(props.initialConflictDetectionKey ?? null);\\n  const [runtimeDoc, setRuntimeDoc] = useState<Json>(() => {\\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<LeptosTiptapHostBridgeState>({\\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<HostEnvelope>) => {\\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    <iframe\\n      key={`${props.documentId}:${reloadKey}`}\\n      ref={iframeRef}\\n      src={iframeSrc}\\n      title=\\\"Leptos Tiptap Editor\\\"\\n      className=\\\"h-full w-full border-0\\\"\\n      data-bridge-status={bridgeState.status}\\n      data-runtime-name={bridgeState.runtimeName}\\n      data-runtime-version={bridgeState.runtimeVersion}\\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<string, unknown>;\\n  content?: TiptapNode[];\\n  text?: string;\\n  marks?: Array<{ type: string; attrs?: Record<string, unknown> }>;\\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<string, unknown>;\\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<string, unknown> | 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<String>,\\n    pub block_id: Option<String>,\\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<TiptapNode> },\\n    Paragraph { #[serde(default)] attrs: BTreeMap<String, serde_json::Value>, #[serde(default)] content: Vec<TiptapNode> },\\n    Heading { attrs: TiptapHeadingAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    BulletList { #[serde(default)] content: Vec<TiptapNode> },\\n    OrderedList { #[serde(default)] content: Vec<TiptapNode>, #[serde(default)] attrs: BTreeMap<String, serde_json::Value> },\\n    TaskList { #[serde(default)] content: Vec<TiptapNode> },\\n    ListItem { #[serde(default)] content: Vec<TiptapNode> },\\n    TaskItem { attrs: TiptapTaskItemAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    Blockquote { #[serde(default)] content: Vec<TiptapNode> },\\n    CodeBlock { attrs: TiptapCodeBlockAttrs, #[serde(default)] content: Vec<TiptapNode> },\\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<TiptapNode, EditorBlockDocumentTiptapError> {\\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<String>, doc: &TiptapNode) -> Result<EditorBlockDocument, EditorBlockDocumentTiptapError> {\\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<TiptapNode, EditorBlockDocumentTiptapError> {\\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<EditorBlock, EditorBlockDocumentTiptapError> {\\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<EditorBlock, EditorBlockDocumentTiptapError> {\\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<EditorBlock, EditorBlockDocumentTiptapError> {\\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<Vec<TiptapNode>, 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<Vec<ContentNode>, 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\": \"                  <p className=\\\"mt-1 text-sm text-gray-500\\\">当前为阅读态，编辑器仅在进入编辑后挂载。</p>\\n                ) : null}\\n                <p className=\\\"text-sm text-wolai-text-secondary\\\">最近更新：{formattedUpdatedAt}</p>\\n              </div>\\n              {canEditDocument && (\\n                <div className=\\\"flex shrink-0 items-center gap-2\\\">\\n                  {isEditing ? (\\n                    <Button type=\\\"button\\\" variant=\\\"outline\\\" size=\\\"sm\\\" onClick={handleExitEditMode}>\\n                      返回阅读\\n                    </Button>\\n                  ) : (\\n                    <Button type=\\\"button\\\" size=\\\"sm\\\" onClick={handleEnterEditMode}>\\n                      进入编辑\\n                    </Button>\\n                  )}\\n                </div>\\n              )}\\n            </div>\\n          </div>\\n          <div className=\\\"relative flex-1 overflow-y-auto px-12 py-6\\\">\\n            {contentLoading ? (\\n              showContentLoadingIndicator ? (\\n                <div className=\\\"flex h-64 items-center justify-center text-sm text-gray-400\\\">\\n                  页面内容加载中...\\n                </div>\\n              ) : (\\n                <div className=\\\"h-64\\\" />\\n              )\\n            ) : contentError ? (\\n              <div className=\\\"flex h-64 flex-col items-center justify-center gap-2 text-sm text-red-600\\\">\\n                <div>{contentError}</div>\\n                <button\\n                  type=\\\"button\\\"\\n                  className=\\\"rounded-md border border-red-200 bg-red-50 px-3 py-1 text-sm text-red-700 hover:bg-red-100\\\"\\n                  onClick={() => {\\n                    setContentError(null);\\n                    setContentLoading(true);\\n                    setContentReloadKey((prev) => prev + 1);\\n                  }}\\n                >\\n                  重试\\n                </button>\\n              </div>\\n            ) : (\\n              <div className=\\\"relative\\\">\\n                {keepEditorMounted && (\\n                  <div className={cn(!isEditing && \\\"pointer-events-none absolute inset-0 opacity-0\\\")} aria-hidden={!isEditing}>\\n                    <EditorHost\\n                      documentId={documentId}\\n                      workspaceId={workspaceId}\\n                      initialContent={content}\\n                      initialRevision={contentRevision}\\n                      initialConflictDetectionKey={conflictDetectionKey}\\n                      pageOptions={options}\\n                      readOnly={readOnly}\\n                      onStatsChange={handleStatsChange}\\n                      onSnapshot={handleSnapshot}\\n                      onCloseToc={closeToc}\\n                      onPersistedMetaChange={(meta: { revision: number | null; conflictDetectionKey: string | null }) => {\\n                        setContentRevision(meta.revision);\\n                        setConflictDetectionKey(meta.conflictDetectionKey);\\n                      }}\\n                    />\\n                  </div>\\n                )}\\n                {!isEditing && (\\n                  <div className=\\\"relative\\\" ref={readViewRootRef}>\\n                    <DocumentReadView\\n                      content={content}\\n                      documentId={documentId}\\n                      options={options}\\n                      pageSubtree={pageSubtree}\\n                      className=\\\"mx-auto w-full max-w-[980px]\\\"\\n                    />\\n                    <DocumentToc\\n                      entries={readViewTocEntries}\\n                      visible={options.showToc}\\n                      onJump={jumpToHeading}\\n                      onClose={closeToc}\\n                    />\\n                  </div>\\n                )}\\n              </div>\\n            )}\\n            <PageBacklinksPanel\\n              className=\\\"mt-10\\\"\\n              workspaceId={workspaceId}\\n              documentId={documentId}\\n              defaultCollapsed={options.collapseBacklinks}\\n            />\\n          </div>\\n        </div>\\n        {showInspector && (\\n          <PageOptionsSidebar\\n            documentId={documentId}\\n            options={options}\\n            stats={stats}\\n            onToggle={toggleOption}\\n            onSetPageFont={handleSetPageFont}\\n            onSetLayoutDensity={handleSetLayoutDensity}\\n            onSetEmbedDefaultToCursor={inspectorCanUseEditorBridge ? handleSetEmbedDefaultToCursor : undefined}\\n            onClearEmbedDefault={handleClearEmbedDefault}\\n            onExport={handleExport}\\n            onOpenHistory={() => 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      </div>\\n      <DocumentHistoryDrawer\\n        open={historyOpen}\\n        onOpenChange={setHistoryOpen}\\n        history={history}\\n        onRestore={handleRestoreSnapshot}\\n      />\\n      <MoveEmbedPickerHost />\\n      <DocumentCommentsDrawer />\\n      <DocumentAiAgentPanel\\n        documentId={documentId}\\n        getLatestBlocks={() => latestBlocksRef.current}\\n        getLatestPageSubtree={() => pageSubtree}\\n      />\\n    </ImagePickerProvider>\\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<PageOptionsState>(initialOptions ?? defaultOptions);\\n  const [stats, setStats] = useState<DocumentStats>(initialStats ?? defaultStats);\\n  const [history, setHistory] = useState<DocumentSnapshot[]>([]);\\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<unknown>(initialContent);\\n  const [serverContentSnapshot, setServerContentSnapshot] = useState<unknown>(initialContent);\\n  const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState<PageSubtreeProjection | null>(\\n    initialPageSubtree,\\n  );\\n  const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState<string>(title ?? \\\"无标题\\\");\\n  const [contentRevision, setContentRevision] = useState<number | null>(initialContentRevision);\\n  const [conflictDetectionKey, setConflictDetectionKey] = useState<string | null>(initialConflictDetectionKey);\\n  const [contentLoading, setContentLoading] = useState(() => initialContent == null);\\n  const [contentError, setContentError] = useState<string | null>(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<ReturnType<typeof setTimeout> | null>(null);\\n  const editorUnmountTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\\n  const pendingOpenTableRef = useRef<string | null>(null);\\n  const latestBlocksRef = useRef<Json | null>(null);\\n  const pageRootRef = useRef<HTMLDivElement>(null);\\n  const readViewRootRef = useRef<HTMLDivElement>(null);\\n  const pendingRestoreSnapshotRef = useRef<DocumentSnapshot | null>(null);\\n  const lastCopyBlockedAtRef = useRef<number>(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<HTMLInputElement>) => {\\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<DocumentSavePayload> & { 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\\\\(|<DocumentContent\", \"type\":\"text\"}","name":"grep_search","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":78576,"output_tokens":102}},"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_FTTVLkxWLEFGJOcOlaXDZeYX","type":"tool_result"}],"role":"tool"},"type":"message"}
{"message":{"blocks":[{"id":"call_8P8kMkntqpnilo1oD3gZ7Pdf","input":"{\"limit\":240,\"offset\":150,\"path\":\"/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx\"}","name":"read_file","type":"tool_use"}],"role":"assistant","usage":{"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":78748,"output_tokens":49}},"type":"message"}
{"message":{"blocks":[{"is_error":false,"output":"{\n  \"type\": \"text\",\n  \"file\": {\n    \"filePath\": \"/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx\",\n    \"content\": \"      showHeadingNumbers: doc.show_heading_numbers ?? true,\\n      showToc: doc.show_toc ?? false,\\n      showStructure: doc.show_structure ?? false,\\n      protectEditing: doc.protect_editing ?? false,\\n      showWordCount: doc.show_word_count ?? true,\\n      collapseBacklinks: doc.collapse_backlinks ?? false,\\n      pageFont: doc.page_font ?? \\\"default\\\",\\n      layoutDensity: doc.layout_density ?? \\\"normal\\\",\\n      hideChildPages: doc.hide_child_pages ?? false,\\n      showBlockRefCount: doc.show_block_ref_count ?? false,\\n      embedDefaultBlockId: doc.embed_default_block_id ?? null,\\n    };\\n\\n    const initialStats: DocumentStats = {\\n      wordCount: doc.word_count ?? 0,\\n      characterCount: doc.character_count ?? 0,\\n      blockCount: doc.block_count ?? 0,\\n      todoTotal: doc.todo_total ?? doc.todo_total_count ?? 0,\\n      todoDone: doc.todo_done ?? doc.todo_done_count ?? 0,\\n    };\\n    const initialDocumentContent = await fetchDocumentContentOnServer({\\n      documentId: doc.id,\\n      workspaceId: doc.workspace_id,\\n      title: doc.title ?? \\\"无标题\\\",\\n    });\\n\\n    return (\\n      <div className=\\\"flex h-screen flex-col\\\">\\n        <div className=\\\"min-h-0 flex-1\\\">\\n          <DocumentShell\\n            documentId={doc.id}\\n            workspaceId={doc.workspace_id}\\n            title={doc.title ?? \\\"无标题\\\"}\\n            updatedAt={doc.updated_at}\\n            initialContent={initialDocumentContent.content}\\n            initialContentRevision={initialDocumentContent.revision}\\n            initialConflictDetectionKey={initialDocumentContent.conflictDetectionKey}\\n            initialPageSubtree={initialDocumentContent.pageSubtree}\\n            initialOptions={initialOptions}\\n            initialStats={initialStats}\\n            openTableId={openTableId}\\n            autoEnterEdit={autoEnterEdit}\\n            readOnly={readOnly}\\n            disableDownload={disableDownload}\\n            disableCopy={disableCopy}\\n          />\\n        </div>\\n      </div>\\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 <DocumentContent {...props} />;\\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<String>,\\n    pub raw_token: Option<String>,\\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<TextMark>,\\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<String, Value>,\\n}\\n\\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct BlockProps {\\n    pub indent: Option<u16>,\\n    pub heading_level: Option<u8>,\\n    pub checked: Option<bool>,\\n    pub collapsed: Option<bool>,\\n    pub language: Option<String>,\\n    pub reference_target_id: Option<String>,\\n    pub reference_label: Option<String>,\\n    pub reference_token_strategy: Option<ReferenceTokenStrategy>,\\n    #[serde(default)]\\n    pub extra: BTreeMap<String, Value>,\\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<ContentNode>,\\n    #[serde(default)]\\n    pub child_block_ids: Vec<String>,\\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<String>,\\n    #[serde(default)]\\n    pub blocks: Vec<EditorBlock>,\\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<PageOptionsState>(initialOptions ?? defaultOptions);\\n  const [stats, setStats] = useState<DocumentStats>(initialStats ?? defaultStats);\\n  const [history, setHistory] = useState<DocumentSnapshot[]>([]);\\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<unknown>(initialContent);\\n  const [serverContentSnapshot, setServerContentSnapshot] = useState<unknown>(initialContent);\\n  const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState<PageSubtreeProjection | null>(\\n    initialPageSubtree,\\n  );\\n  const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState<string>(title ?? \\\"无标题\\\");\\n  const [contentRevision, setContentRevision] = useState<number | null>(initialContentRevision);\\n  const [conflictDetectionKey, setConflictDetectionKey] = useState<string | null>(initialConflictDetectionKey);\\n  const [contentLoading, setContentLoading] = useState(() => initialContent == null);\\n  const [contentError, setContentError] = useState<string | null>(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<ReturnType<typeof setTimeout> | null>(null);\\n  const editorUnmountTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\\n  const pendingOpenTableRef = useRef<string | null>(null);\\n  const latestBlocksRef = useRef<Json | null>(null);\\n  const pageRootRef = useRef<HTMLDivElement>(null);\\n  const readViewRootRef = useRef<HTMLDivElement>(null);\\n  const pendingRestoreSnapshotRef = useRef<DocumentSnapshot | null>(null);\\n  const lastCopyBlockedAtRef = useRef<number>(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<HTMLInputElement>) => {\\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<HTMLInputElement>) => {\\n    if (event.key === \\\"Enter\\\") {\\n      event.preventDefault();\\n      event.currentTarget.blur();\\n    }\\n  };\\n\\n  const persistOptions = useCallback(\\n    async (patch: Partial<PageOptionsState>) => {\\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<PageOptionsState>);\\n        return next;\\n      });\\n    },\\n    [persistOptions, readOnly],\\n  );\\n\\n  const setOptionPatch = useCallback(\\n    (patch: Partial<PageOptionsState>) => {\\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<HTMLElement>(`[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    <ImagePickerProvider documentId={documentId} workspaceId={workspaceId}>\\n      <div className={pageRootClass} ref={pageRootRef}>\\n        <div className=\\\"flex h-full flex-1 flex-col overflow-hidden\\\">\\n          <div className=\\\"border-b border-wolai-border px-12 pb-6 pt-8\\\">\\n            <div className=\\\"flex items-start justify-between gap-6\\\">\\n              <div className=\\\"min-w-0 flex-1\\\">\\n                {isEditing && canEditDocument ? (\\n                  <div className=\\\"relative\\\">\\n                    <input\\n                      value={pageTitle}\\n                      onChange={handleTitleChange}\\n                      onBlur={handleTitleBlur}\\n                      onKeyDown={handleTitleKeyDown}\\n                      placeholder=\\\"无标题\\\"\\n                      className=\\\"w-full border-none bg-transparent text-3xl font-semibold text-wolai-text-primary outline-none focus:ring-0\\\"\\n                      aria-label=\\\"页面标题\\\"\\n                      disabled={options.protectEditing || readOnly}\\n                      spellCheck={spellCheck}\\n                    />\\n                  </div>\\n                ) : (\\n                  <h1 className=\\\"break-words text-3xl font-semibold text-wolai-text-primary\\\">\\n                    {pageTitle || \\\"无标题\\\"}\\n                  </h1>\\n                )}\\n                {readOnly ? (\\n                  <p className=\\\"mt-1 text-sm text-gray-500\\\">该页面为只读共享，无法编辑。</p>\\n                ) : options.protectEditing && isEditing ? (\\n                  <p className=\\\"mt-1 text-sm text-[#b91c1c]\\\">当前页面已开启编辑保护，关闭后方可修改内容。</p>\\n                ) : !isEditing && canEditDocument ? (\\n                  <p className=\\\"mt-1 text-sm text-gray-500\\\">当前为阅读态，编辑器仅在进入编辑后挂载。</p>\\n                ) : null}\\n                <p className=\\\"text-sm text-wolai-text-secondary\\\">最近更新：{formattedUpdatedAt}</p>\\n              </div>\\n              {canEditDocument && (\\n                <div className=\\\"flex shrink-0 items-center gap-2\\\">\\n                  {isEditing ? (\\n                    <Button type=\\\"button\\\" variant=\\\"outline\\\" size=\\\"sm\\\" onClick={handleExitEditMode}>\\n                      返回阅读\\n                    </Button>\\n                  ) : (\\n                    <Button type=\\\"button\\\" size=\\\"sm\\\" onClick={handleEnterEditMode}>\\n                      进入编辑\\n                    </Button>\\n                  )}\\n                </div>\\n              )}\\n            </div>\\n          </div>\\n          <div className=\\\"relative flex-1 overflow-y-auto px-12 py-6\\\">\\n            {contentLoading ? (\\n              showContentLoadingIndicator ? (\\n                <div className=\\\"flex h-64 items-center justify-center text-sm text-gray-400\\\">\\n                  页面内容加载中...\\n                </div>\\n              ) : (\\n                <div className=\\\"h-64\\\" />\\n              )\\n            ) : contentError ? (\\n              <div className=\\\"flex h-64 flex-col items-center justify-center gap-2 text-sm text-red-600\\\">\\n                <div>{contentError}</div>\\n                <button\\n                  type=\\\"button\\\"\\n                  className=\\\"rounded-md border border-red-200 bg-red-50 px-3 py-1 text-sm text-red-700 hover:bg-red-100\\\"\\n                  onClick={() => {\\n                    setContentError(null);\\n                    setContentLoading(true);\\n                    setContentReloadKey((prev) => prev + 1);\\n                  }}\\n                >\\n                  重试\\n                </button>\\n              </div>\\n            ) : (\\n              <div className=\\\"relative\\\">\\n                {keepEditorMounted && (\\n                  <div className={cn(!isEditing && \\\"pointer-events-none absolute inset-0 opacity-0\\\")} aria-hidden={!isEditing}>\\n                    <EditorHost\\n                      documentId={documentId}\\n                      workspaceId={workspaceId}\\n                      initialContent={content}\\n                      initialRevision={contentRevision}\\n                      initialConflictDetectionKey={conflictDetectionKey}\\n                      pageOptions={options}\\n                      readOnly={readOnly}\\n                      onStatsChange={handleStatsChange}\\n                      onSnapshot={handleSnapshot}\\n                      onCloseToc={closeToc}\\n                      onPersistedMetaChange={(meta: { revision: number | null; conflictDetectionKey: string | null }) => {\\n                        setContentRevision(meta.revision);\\n                        setConflictDetectionKey(meta.conflictDetectionKey);\\n                      }}\\n                    />\\n                  </div>\\n                )}\\n                {!isEditing && (\\n                  <div className=\\\"relative\\\" ref={readViewRootRef}>\\n                    <DocumentReadView\\n                      content={content}\\n                      documentId={documentId}\\n                      options={options}\\n                      pageSubtree={pageSubtree}\\n                      className=\\\"mx-auto w-full max-w-[980px]\\\"\\n                    />\\n                    <DocumentToc\\n                      entries={readViewTocEntries}\\n                      visible={options.showToc}\\n                      onJump={jumpToHeading}\\n                      onClose={closeToc}\\n                    />\\n                  </div>\\n                )}\\n              </div>\\n            )}\\n            <PageBacklinksPanel\\n              className=\\\"mt-10\\\"\\n              workspaceId={workspaceId}\\n              documentId={documentId}\\n              defaultCollapsed={options.collapseBacklinks}\\n            />\\n          </div>\\n        </div>\\n        {showInspector && (\\n          <PageOptionsSidebar\\n            documentId={documentId}\\n            options={options}\\n            stats={stats}\\n            onToggle={toggleOption}\\n            onSetPageFont={handleSetPageFont}\\n            onSetLayoutDensity={handleSetLayoutDensity}\\n            onSetEmbedDefaultToCursor={inspectorCanUseEditorBridge ? handleSetEmbedDefaultToCursor : undefined}\\n            onClearEmbedDefault={handleClearEmbedDefault}\\n            onExport={handleExport}\\n            onOpenHistory={() => 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      </div>\\n      <DocumentHistoryDrawer\\n        open={historyOpen}\\n        onOpenChange={setHistoryOpen}\\n        history={history}\\n        onRestore={handleRestoreSnapshot}\\n      />\\n      <MoveEmbedPickerHost />\\n      <DocumentCommentsDrawer />\\n      <DocumentAiAgentPanel\\n        documentId={documentId}\\n        getLatestBlocks={() => latestBlocksRef.current}\\n        getLatestPageSubtree={() => pageSubtree}\\n      />\\n    </ImagePickerProvider>\\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<PageOptionsState>(initialOptions ?? defaultOptions);\",\n        \"-  const [stats, setStats] = useState<DocumentStats>(initialStats ?? defaultStats);\",\n        \"-  const [history, setHistory] = useState<DocumentSnapshot[]>([]);\",\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<unknown>(initialContent);\",\n        \"-  const [serverContentSnapshot, setServerContentSnapshot] = useState<unknown>(initialContent);\",\n        \"-  const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState<PageSubtreeProjection | null>(\",\n        \"-    initialPageSubtree,\",\n        \"-  );\",\n        \"-  const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState<string>(title ?? \\\"无标题\\\");\",\n        \"-  const [contentRevision, setContentRevision] = useState<number | null>(initialContentRevision);\",\n        \"-  const [conflictDetectionKey, setConflictDetectionKey] = useState<string | null>(initialConflictDetectionKey);\",\n        \"-  const [contentLoading, setContentLoading] = useState(() => initialContent == null);\",\n        \"-  const [contentError, setContentError] = useState<string | null>(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<ReturnType<typeof setTimeout> | null>(null);\",\n        \"-  const editorUnmountTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\",\n        \"-  const pendingOpenTableRef = useRef<string | null>(null);\",\n        \"-  const latestBlocksRef = useRef<Json | null>(null);\",\n        \"-  const pageRootRef = useRef<HTMLDivElement>(null);\",\n        \"-  const readViewRootRef = useRef<HTMLDivElement>(null);\",\n        \"-  const pendingRestoreSnapshotRef = useRef<DocumentSnapshot | null>(null);\",\n        \"-  const lastCopyBlockedAtRef = useRef<number>(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<HTMLInputElement>) => {\",\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<HTMLInputElement>) => {\",\n        \"-    if (event.key === \\\"Enter\\\") {\",\n        \"-      event.preventDefault();\",\n        \"-      event.currentTarget.blur();\",\n        \"-    }\",\n        \"-  };\",\n        \"-\",\n        \"-  const persistOptions = useCallback(\",\n        \"-    async (patch: Partial<PageOptionsState>) => {\",\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<PageOptionsState>);\",\n        \"-        return next;\",\n        \"-      });\",\n        \"-    },\",\n        \"-    [persistOptions, readOnly],\",\n        \"-  );\",\n        \"-\",\n        \"-  const setOptionPatch = useCallback(\",\n        \"-    (patch: Partial<PageOptionsState>) => {\",\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<HTMLElement>(`[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        \"-    <ImagePickerProvider documentId={documentId} workspaceId={workspaceId}>\",\n        \"-      <div className={pageRootClass} ref={pageRootRef}>\",\n        \"-        <div className=\\\"flex h-full flex-1 flex-col overflow-hidden\\\">\",\n        \"-          <div className=\\\"border-b border-wolai-border px-12 pb-6 pt-8\\\">\",\n        \"-            <div className=\\\"flex items-start justify-between gap-6\\\">\",\n        \"-              <div className=\\\"min-w-0 flex-1\\\">\",\n        \"-                {isEditing && canEditDocument ? (\",\n        \"-                  <div className=\\\"relative\\\">\",\n        \"-                    <input\",\n        \"-                      value={pageTitle}\",\n        \"-                      onChange={handleTitleChange}\",\n        \"-                      onBlur={handleTitleBlur}\",\n        \"-                      onKeyDown={handleTitleKeyDown}\",\n        \"-                      placeholder=\\\"无标题\\\"\",\n        \"-                      className=\\\"w-full border-none bg-transparent text-3xl font-semibold text-wolai-text-primary outline-none focus:ring-0\\\"\",\n        \"-                      aria-label=\\\"页面标题\\\"\",\n        \"-                      disabled={options.protectEditing || readOnly}\",\n        \"-                      spellCheck={spellCheck}\",\n        \"-                    />\",\n        \"-                  </div>\",\n        \"-                ) : (\",\n        \"-                  <h1 className=\\\"break-words text-3xl font-semibold text-wolai-text-primary\\\">\",\n        \"-                    {pageTitle || \\\"无标题\\\"}\",\n        \"-                  </h1>\",\n        \"-                )}\",\n        \"-                {readOnly ? (\",\n        \"-                  <p className=\\\"mt-1 text-sm text-gray-500\\\">该页面为只读共享，无法编辑。</p>\",\n        \"-                ) : options.protectEditing && isEditing ? (\",\n        \"-                  <p className=\\\"mt-1 text-sm text-[#b91c1c]\\\">当前页面已开启编辑保护，关闭后方可修改内容。</p>\",\n        \"-                ) : !isEditing && canEditDocument ? (\",\n        \"-                  <p className=\\\"mt-1 text-sm text-gray-500\\\">当前为阅读态，编辑器仅在进入编辑后挂载。</p>\",\n        \"-                ) : null}\",\n        \"-                <p className=\\\"text-sm text-wolai-text-secondary\\\">最近更新：{formattedUpdatedAt}</p>\",\n        \"-              </div>\",\n        \"-              {canEditDocument && (\",\n        \"-                <div className=\\\"flex shrink-0 items-center gap-2\\\">\",\n        \"-                  {isEditing ? (\",\n        \"-                    <Button type=\\\"button\\\" variant=\\\"outline\\\" size=\\\"sm\\\" onClick={handleExitEditMode}>\",\n        \"-                      返回阅读\",\n        \"-                    </Button>\",\n        \"-                  ) : (\",\n        \"-                    <Button type=\\\"button\\\" size=\\\"sm\\\" onClick={handleEnterEditMode}>\",\n        \"-                      进入编辑\",\n        \"-                    </Button>\",\n        \"-                  )}\",\n        \"-                </div>\",\n        \"-              )}\",\n        \"-            </div>\",\n        \"-          </div>\",\n        \"-          <div className=\\\"relative flex-1 overflow-y-auto px-12 py-6\\\">\",\n        \"-            {contentLoading ? (\",\n        \"-              showContentLoadingIndicator ? (\",\n        \"-                <div className=\\\"flex h-64 items-center justify-center text-sm text-gray-400\\\">\",\n        \"-                  页面内容加载中...\",\n        \"-                </div>\",\n        \"-              ) : (\",\n        \"-                <div className=\\\"h-64\\\" />\",\n        \"-              )\",\n        \"-            ) : contentError ? (\",\n        \"-              <div className=\\\"flex h-64 flex-col items-center justify-center gap-2 text-sm text-red-600\\\">\",\n        \"-                <div>{contentError}</div>\",\n        \"-                <button\",\n        \"-                  type=\\\"button\\\"\",\n        \"-                  className=\\\"rounded-md border border-red-200 bg-red-50 px-3 py-1 text-sm text-red-700 hover:bg-red-100\\\"\",\n        \"-                  onClick={() => {\",\n        \"-                    setContentError(null);\",\n        \"-                    setContentLoading(true);\",\n        \"-                    setContentReloadKey((prev) => prev + 1);\",\n        \"-                  }}\",\n        \"-                >\",\n        \"-                  重试\",\n        \"-                </button>\",\n        \"-              </div>\",\n        \"-            ) : (\",\n        \"-              <div className=\\\"relative\\\">\",\n        \"-                {keepEditorMounted && (\",\n        \"-                  <div className={cn(!isEditing && \\\"pointer-events-none absolute inset-0 opacity-0\\\")} aria-hidden={!isEditing}>\",\n        \"-                    <EditorHost\",\n        \"-                      documentId={documentId}\",\n        \"-                      workspaceId={workspaceId}\",\n        \"-                      initialContent={content}\",\n        \"-                      initialRevision={contentRevision}\",\n        \"-                      initialConflictDetectionKey={conflictDetectionKey}\",\n        \"-                      pageOptions={options}\",\n        \"-                      readOnly={readOnly}\",\n        \"-                      onStatsChange={handleStatsChange}\",\n        \"-                      onSnapshot={handleSnapshot}\",\n        \"-                      onCloseToc={closeToc}\",\n        \"-                      onPersistedMetaChange={(meta: { revision: number | null; conflictDetectionKey: string | null }) => {\",\n        \"-                        setContentRevision(meta.revision);\",\n        \"-                        setConflictDetectionKey(meta.conflictDetectionKey);\",\n        \"-                      }}\",\n        \"-                    />\",\n        \"-                  </div>\",\n        \"-                )}\",\n        \"-                {!isEditing && (\",\n        \"-                  <div className=\\\"relative\\\" ref={readViewRootRef}>\",\n        \"-                    <DocumentReadView\",\n        \"-                      content={content}\",\n        \"-                      documentId={documentId}\",\n        \"-                      options={options}\",\n        \"-                      pageSubtree={pageSubtree}\",\n        \"-                      className=\\\"mx-auto w-full max-w-[980px]\\\"\",\n        \"-                    />\",\n        \"-                    <DocumentToc\",\n        \"-                      entries={readViewTocEntries}\",\n        \"-                      visible={options.showToc}\",\n        \"-                      onJump={jumpToHeading}\",\n        \"-                      onClose={closeToc}\",\n        \"-                    />\",\n        \"-                  </div>\",\n        \"-                )}\",\n        \"-              </div>\",\n        \"-            )}\",\n        \"-            <PageBacklinksPanel\",\n        \"-              className=\\\"mt-10\\\"\",\n        \"-              workspaceId={workspaceId}\",\n        \"-              documentId={documentId}\",\n        \"-              defaultCollapsed={options.collapseBacklinks}\",\n        \"-            />\",\n        \"-          </div>\",\n        \"-        </div>\",\n        \"-        {showInspector && (\",\n        \"-          <PageOptionsSidebar\",\n        \"-            documentId={documentId}\",\n        \"-            options={options}\",\n        \"-            stats={stats}\",\n        \"-            onToggle={toggleOption}\",\n        \"-            onSetPageFont={handleSetPageFont}\",\n        \"-            onSetLayoutDensity={handleSetLayoutDensity}\",\n        \"-            onSetEmbedDefaultToCursor={inspectorCanUseEditorBridge ? handleSetEmbedDefaultToCursor : undefined}\",\n        \"-            onClearEmbedDefault={handleClearEmbedDefault}\",\n        \"-            onExport={handleExport}\",\n        \"-            onOpenHistory={() => 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        \"-      </div>\",\n        \"-      <DocumentHistoryDrawer\",\n        \"-        open={historyOpen}\",\n        \"-        onOpenChange={setHistoryOpen}\",\n        \"-        history={history}\",\n        \"-        onRestore={handleRestoreSnapshot}\",\n        \"-      />\",\n        \"-      <MoveEmbedPickerHost />\",\n        \"-      <DocumentCommentsDrawer />\",\n        \"-      <DocumentAiAgentPanel\",\n        \"-        documentId={documentId}\",\n        \"-        getLatestBlocks={() => latestBlocksRef.current}\",\n        \"-        getLatestPageSubtree={() => pageSubtree}\",\n        \"-      />\",\n        \"-    </ImagePickerProvider>\",\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<PageOptionsState>(initialOptions ?? defaultOptions);\",\n        \"+  const [stats, setStats] = useState<DocumentStats>(initialStats ?? defaultStats);\",\n        \"+  const [history, setHistory] = useState<DocumentSnapshot[]>([]);\",\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<unknown>(initialContent);\",\n        \"+  const [serverContentSnapshot, setServerContentSnapshot] = useState<unknown>(initialContent);\",\n        \"+  const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState<PageSubtreeProjection | null>(\",\n        \"+    initialPageSubtree,\",\n        \"+  );\",\n        \"+  const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState<string>(title ?? \\\"无标题\\\");\",\n        \"+  const [contentRevision, setContentRevision] = useState<number | null>(initialContentRevision);\",\n        \"+  const [conflictDetectionKey, setConflictDetectionKey] = useState<string | null>(initialConflictDetectionKey);\",\n        \"+  const [contentLoading, setContentLoading] = useState(() => initialContent == null);\",\n        \"+  const [contentError, setContentError] = useState<string | null>(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<ReturnType<typeof setTimeout> | null>(null);\",\n        \"+  const editorUnmountTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\",\n        \"+  const pendingOpenTableRef = useRef<string | null>(null);\",\n        \"+  const latestBlocksRef = useRef<Json | null>(null);\",\n        \"+  const pageRootRef = useRef<HTMLDivElement>(null);\",\n        \"+  const readViewRootRef = useRef<HTMLDivElement>(null);\",\n        \"+  const pendingRestoreSnapshotRef = useRef<DocumentSnapshot | null>(null);\",\n        \"+  const lastCopyBlockedAtRef = useRef<number>(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<HTMLInputElement>) => {\",\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<HTMLInputElement>) => {\",\n        \"+    if (event.key === \\\"Enter\\\") {\",\n        \"+      event.preventDefault();\",\n        \"+      event.currentTarget.blur();\",\n        \"+    }\",\n        \"+  };\",\n        \"+\",\n        \"+  const persistOptions = useCallback(\",\n        \"+    async (patch: Partial<PageOptionsState>) => {\",\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<PageOptionsState>);\",\n        \"+        return next;\",\n        \"+      });\",\n        \"+    },\",\n        \"+    [persistOptions, readOnly],\",\n        \"+  );\",\n        \"+\",\n        \"+  const setOptionPatch = useCallback(\",\n        \"+    (patch: Partial<PageOptionsState>) => {\",\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<HTMLElement>(`[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        \"+    <ImagePickerProvider documentId={documentId} workspaceId={workspaceId}>\",\n        \"+      <div className={pageRootClass} ref={pageRootRef}>\",\n        \"+        <div className=\\\"flex h-full flex-1 flex-col overflow-hidden\\\">\",\n        \"+          <div className=\\\"border-b border-wolai-border px-12 pb-6 pt-8\\\">\",\n        \"+            <div className=\\\"flex items-start justify-between gap-6\\\">\",\n        \"+              <div className=\\\"min-w-0 flex-1\\\">\",\n        \"+                {isEditing && canEditDocument ? (\",\n        \"+                  <div className=\\\"relative\\\">\",\n        \"+                    <input\",\n        \"+                      value={pageTitle}\",\n        \"+                      onChange={handleTitleChange}\",\n        \"+                      onBlur={handleTitleBlur}\",\n        \"+                      onKeyDown={handleTitleKeyDown}\",\n        \"+                      placeholder=\\\"无标题\\\"\",\n        \"+                      className=\\\"w-full border-none bg-transparent text-3xl font-semibold text-wolai-text-primary outline-none focus:ring-0\\\"\",\n        \"+                      aria-label=\\\"页面标题\\\"\",\n        \"+                      disabled={options.protectEditing || readOnly}\",\n        \"+                      spellCheck={spellCheck}\",\n        \"+                    />\",\n        \"+                  </div>\",\n        \"+                ) : (\",\n        \"+                  <h1 className=\\\"break-words text-3xl font-semibold text-wolai-text-primary\\\">\",\n        \"+                    {pageTitle || \\\"无标题\\\"}\",\n        \"+                  </h1>\",\n        \"+                )}\",\n        \"+                {readOnly ? (\",\n        \"+                  <p className=\\\"mt-1 text-sm text-gray-500\\\">该页面为只读共享，无法编辑。</p>\",\n        \"+                ) : options.protectEditing && isEditing ? (\",\n        \"+                  <p className=\\\"mt-1 text-sm text-[#b91c1c]\\\">当前页面已开启编辑保护，关闭后方可修改内容。</p>\",\n        \"+                ) : !isEditing && canEditDocument ? (\",\n        \"+                  <p className=\\\"mt-1 text-sm text-gray-500\\\">当前为阅读态，编辑器仅在进入编辑后挂载。</p>\",\n        \"+                ) : null}\",\n        \"+                <p className=\\\"text-sm text-wolai-text-secondary\\\">最近更新：{formattedUpdatedAt}</p>\",\n        \"+              </div>\",\n        \"+              {canEditDocument && (\",\n        \"+                <div className=\\\"flex shrink-0 items-center gap-2\\\">\",\n        \"+                  {isEditing ? (\",\n        \"+                    <Button type=\\\"button\\\" variant=\\\"outline\\\" size=\\\"sm\\\" onClick={handleExitEditMode}>\",\n        \"+                      返回阅读\",\n        \"+                    </Button>\",\n        \"+                  ) : (\",\n        \"+                    <Button type=\\\"button\\\" size=\\\"sm\\\" onClick={handleEnterEditMode}>\",\n        \"+                      进入编辑\",\n        \"+                    </Button>\",\n        \"+                  )}\",\n        \"+                </div>\",\n        \"+              )}\",\n        \"+            </div>\",\n        \"+          </div>\",\n        \"+          <div className=\\\"relative flex-1 overflow-y-auto px-12 py-6\\\">\",\n        \"+            {contentLoading ? (\",\n        \"+              showContentLoadingIndicator ? (\",\n        \"+                <div className=\\\"flex h-64 items-center justify-center text-sm text-gray-400\\\">\",\n        \"+                  页面内容加载中...\",\n        \"+                </div>\",\n        \"+              ) : (\",\n        \"+                <div className=\\\"h-64\\\" />\",\n        \"+              )\",\n        \"+            ) : contentError ? (\",\n        \"+              <div className=\\\"flex h-64 flex-col items-center justify-center gap-2 text-sm text-red-600\\\">\",\n        \"+                <div>{contentError}</div>\",\n        \"+                <button\",\n        \"+                  type=\\\"button\\\"\",\n        \"+                  className=\\\"rounded-md border border-red-200 bg-red-50 px-3 py-1 text-sm text-red-700 hover:bg-red-100\\\"\",\n        \"+                  onClick={() => {\",\n        \"+                    setContentError(null);\",\n        \"+                    setContentLoading(true);\",\n        \"+                    setContentReloadKey((prev) => prev + 1);\",\n        \"+                  }}\",\n        \"+                >\",\n        \"+                  重试\",\n        \"+                </button>\",\n        \"+              </div>\",\n        \"+            ) : (\",\n        \"+              <div className=\\\"relative\\\">\",\n        \"+                {keepEditorMounted && (\",\n        \"+                  <div className={cn(!isEditing && \\\"pointer-events-none absolute inset-0 opacity-0\\\")} aria-hidden={!isEditing}>\",\n        \"+                    <EditorHost\",\n        \"+                      documentId={documentId}\",\n        \"+                      workspaceId={workspaceId}\",\n        \"+                      initialContent={content}\",\n        \"+                      initialRevision={contentRevision}\",\n        \"+                      initialConflictDetectionKey={conflictDetectionKey}\",\n        \"+                      pageOptions={options}\",\n        \"+                      readOnly={readOnly}\",\n        \"+                      onStatsChange={handleStatsChange}\",\n        \"+                      onSnapshot={handleSnapshot}\",\n        \"+                      onCloseToc={closeToc}\",\n        \"+                      onPersistedMetaChange={(meta: { revision: number | null; conflictDetectionKey: string | null }) => {\",\n        \"+                        setContentRevision(meta.revision);\",\n        \"+                        setConflictDetectionKey(meta.conflictDetectionKey);\",\n        \"+                      }}\",\n        \"+                    />\",\n        \"+                  </div>\",\n        \"+                )}\",\n        \"+                {!isEditing && (\",\n        \"+                  <div className=\\\"relative\\\" ref={readViewRootRef}>\",\n        \"+                    <DocumentReadView\",\n        \"+                      content={content}\",\n        \"+                      documentId={documentId}\",\n        \"+                      options={options}\",\n        \"+                      pageSubtree={pageSubtree}\",\n        \"+                      className=\\\"mx-auto w-full max-w-[980px]\\\"\",\n        \"+                    />\",\n        \"+                    <DocumentToc\",\n        \"+                      entries={readViewTocEntries}\",\n        \"+                      visible={options.showToc}\",\n        \"+                      onJump={jumpToHeading}\",\n        \"+                      onClose={closeToc}\",\n        \"+                    />\",\n        \"+                  </div>\",\n        \"+                )}\",\n        \"+              </div>\",\n        \"+            )}\",\n        \"+            <PageBacklinksPanel\",\n        \"+              className=\\\"mt-10\\\"\",\n        \"+              workspaceId={workspaceId}\",\n        \"+              documentId={documentId}\",\n        \"+              defaultCollapsed={options.collapseBacklinks}\",\n        \"+            />\",\n        \"+          </div>\",\n        \"+        </div>\",\n        \"+        {showInspector && (\",\n        \"+          <PageOptionsSidebar\",\n        \"+            documentId={documentId}\",\n        \"+            options={options}\",\n        \"+            stats={stats}\",\n        \"+            onToggle={toggleOption}\",\n        \"+            onSetPageFont={handleSetPageFont}\",\n        \"+            onSetLayoutDensity={handleSetLayoutDensity}\",\n        \"+            onSetEmbedDefaultToCursor={inspectorCanUseEditorBridge ? handleSetEmbedDefaultToCursor : undefined}\",\n        \"+            onClearEmbedDefault={handleClearEmbedDefault}\",\n        \"+            onExport={handleExport}\",\n        \"+            onOpenHistory={() => 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        \"+      </div>\",\n        \"+      <DocumentHistoryDrawer\",\n        \"+        open={historyOpen}\",\n        \"+        onOpenChange={setHistoryOpen}\",\n        \"+        history={history}\",\n        \"+        onRestore={handleRestoreSnapshot}\",\n        \"+      />\",\n        \"+      <MoveEmbedPickerHost />\",\n        \"+      <DocumentCommentsDrawer />\",\n        \"+      <DocumentAiAgentPanel\",\n        \"+        documentId={documentId}\",\n        \"+        getLatestBlocks={() => latestBlocksRef.current}\",\n        \"+        getLatestPageSubtree={() => pageSubtree}\",\n        \"+      />\",\n        \"+    </ImagePickerProvider>\",\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                  <p className=\\\"mt-1 text-sm text-gray-500\\\">该页面为只读共享，无法编辑。</p>\\n                ) : options.protectEditing && isEditing ? (\\n                  <p className=\\\"mt-1 text-sm text-[#b91c1c]\\\">当前页面已开启编辑保护，关闭后方可修改内容。</p>\\n                ) : null}\\n\",\"old_string\":\"                {readOnly ? (\\n                  <p className=\\\"mt-1 text-sm text-gray-500\\\">该页面为只读共享，无法编辑。</p>\\n                ) : options.protectEditing && isEditing ? (\\n                  <p className=\\\"mt-1 text-sm text-[#b91c1c]\\\">当前页面已开启编辑保护，关闭后方可修改内容。</p>\\n                ) : !isEditing && canEditDocument ? (\\n                  <p className=\\\"mt-1 text-sm text-gray-500\\\">当前为阅读态，编辑器仅在进入编辑后挂载。</p>\\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                  <p className=\\\"mt-1 text-sm text-gray-500\\\">该页面为只读共享，无法编辑。</p>\\n                ) : options.protectEditing && isEditing ? (\\n                  <p className=\\\"mt-1 text-sm text-[#b91c1c]\\\">当前页面已开启编辑保护，关闭后方可修改内容。</p>\\n                ) : !isEditing && canEditDocument ? (\\n                  <p className=\\\"mt-1 text-sm text-gray-500\\\">当前为阅读态，编辑器仅在进入编辑后挂载。</p>\\n                ) : null}\\n\",\n  \"newString\": \"                {readOnly ? (\\n                  <p className=\\\"mt-1 text-sm text-gray-500\\\">该页面为只读共享，无法编辑。</p>\\n                ) : options.protectEditing && isEditing ? (\\n                  <p className=\\\"mt-1 text-sm text-[#b91c1c]\\\">当前页面已开启编辑保护，关闭后方可修改内容。</p>\\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<PageOptionsState>(initialOptions ?? defaultOptions);\\n  const [stats, setStats] = useState<DocumentStats>(initialStats ?? defaultStats);\\n  const [history, setHistory] = useState<DocumentSnapshot[]>([]);\\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<unknown>(initialContent);\\n  const [serverContentSnapshot, setServerContentSnapshot] = useState<unknown>(initialContent);\\n  const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState<PageSubtreeProjection | null>(\\n    initialPageSubtree,\\n  );\\n  const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState<string>(title ?? \\\"无标题\\\");\\n  const [contentRevision, setContentRevision] = useState<number | null>(initialContentRevision);\\n  const [conflictDetectionKey, setConflictDetectionKey] = useState<string | null>(initialConflictDetectionKey);\\n  const [contentLoading, setContentLoading] = useState(() => initialContent == null);\\n  const [contentError, setContentError] = useState<string | null>(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<ReturnType<typeof setTimeout> | null>(null);\\n  const editorUnmountTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\\n  const pendingOpenTableRef = useRef<string | null>(null);\\n  const latestBlocksRef = useRef<Json | null>(null);\\n  const pageRootRef = useRef<HTMLDivElement>(null);\\n  const readViewRootRef = useRef<HTMLDivElement>(null);\\n  const pendingRestoreSnapshotRef = useRef<DocumentSnapshot | null>(null);\\n  const lastCopyBlockedAtRef = useRef<number>(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<HTMLInputElement>) => {\\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<HTMLInputElement>) => {\\n    if (event.key === \\\"Enter\\\") {\\n      event.preventDefault();\\n      event.currentTarget.blur();\\n    }\\n  };\\n\\n  const persistOptions = useCallback(\\n    async (patch: Partial<PageOptionsState>) => {\\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<PageOptionsState>);\\n        return next;\\n      });\\n    },\\n    [persistOptions, readOnly],\\n  );\\n\\n  const setOptionPatch = useCallback(\\n    (patch: Partial<PageOptionsState>) => {\\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<HTMLElement>(`[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    <ImagePickerProvider documentId={documentId} workspaceId={workspaceId}>\\n      <div className={pageRootClass} ref={pageRootRef}>\\n        <div className=\\\"flex h-full flex-1 flex-col overflow-hidden\\\">\\n          <div className=\\\"border-b border-wolai-border px-12 pb-6 pt-8\\\">\\n            <div className=\\\"flex items-start justify-between gap-6\\\">\\n              <div className=\\\"min-w-0 flex-1\\\">\\n                {isEditing && canEditDocument ? (\\n                  <div className=\\\"relative\\\">\\n                    <input\\n                      value={pageTitle}\\n                      onChange={handleTitleChange}\\n                      onBlur={handleTitleBlur}\\n                      onKeyDown={handleTitleKeyDown}\\n                      placeholder=\\\"无标题\\\"\\n                      className=\\\"w-full border-none bg-transparent text-3xl font-semibold text-wolai-text-primary outline-none focus:ring-0\\\"\\n                      aria-label=\\\"页面标题\\\"\\n                      disabled={options.protectEditing || readOnly}\\n                      spellCheck={spellCheck}\\n                    />\\n                  </div>\\n                ) : (\\n                  <h1 className=\\\"break-words text-3xl font-semibold text-wolai-text-primary\\\">\\n                    {pageTitle || \\\"无标题\\\"}\\n                  </h1>\\n                )}\\n                {readOnly ? (\\n                  <p className=\\\"mt-1 text-sm text-gray-500\\\">该页面为只读共享，无法编辑。</p>\\n                ) : options.protectEditing && isEditing ? (\\n                  <p className=\\\"mt-1 text-sm text-[#b91c1c]\\\">当前页面已开启编辑保护，关闭后方可修改内容。</p>\\n                ) : !isEditing && canEditDocument ? (\\n                  <p className=\\\"mt-1 text-sm text-gray-500\\\">当前为阅读态，编辑器仅在进入编辑后挂载。</p>\\n                ) : null}\\n                <p className=\\\"text-sm text-wolai-text-secondary\\\">最近更新：{formattedUpdatedAt}</p>\\n              </div>\\n              {canEditDocument && (\\n                <div className=\\\"flex shrink-0 items-center gap-2\\\">\\n                  {isEditing ? (\\n                    <Button type=\\\"button\\\" variant=\\\"outline\\\" size=\\\"sm\\\" onClick={handleExitEditMode}>\\n                      返回阅读\\n                    </Button>\\n                  ) : (\\n                    <Button type=\\\"button\\\" size=\\\"sm\\\" onClick={handleEnterEditMode}>\\n                      进入编辑\\n                    </Button>\\n                  )}\\n                </div>\\n              )}\\n            </div>\\n          </div>\\n          <div className=\\\"relative flex-1 overflow-y-auto px-12 py-6\\\">\\n            {contentLoading ? (\\n              showContentLoadingIndicator ? (\\n                <div className=\\\"flex h-64 items-center justify-center text-sm text-gray-400\\\">\\n                  页面内容加载中...\\n                </div>\\n              ) : (\\n                <div className=\\\"h-64\\\" />\\n              )\\n            ) : contentError ? (\\n              <div className=\\\"flex h-64 flex-col items-center justify-center gap-2 text-sm text-red-600\\\">\\n                <div>{contentError}</div>\\n                <button\\n                  type=\\\"button\\\"\\n                  className=\\\"rounded-md border border-red-200 bg-red-50 px-3 py-1 text-sm text-red-700 hover:bg-red-100\\\"\\n                  onClick={() => {\\n                    setContentError(null);\\n                    setContentLoading(true);\\n                    setContentReloadKey((prev) => prev + 1);\\n                  }}\\n                >\\n                  重试\\n                </button>\\n              </div>\\n            ) : (\\n              <div className=\\\"relative\\\">\\n                {keepEditorMounted && (\\n                  <div className={cn(!isEditing && \\\"pointer-events-none absolute inset-0 opacity-0\\\")} aria-hidden={!isEditing}>\\n                    <EditorHost\\n                      documentId={documentId}\\n                      workspaceId={workspaceId}\\n                      initialContent={content}\\n                      initialRevision={contentRevision}\\n                      initialConflictDetectionKey={conflictDetectionKey}\\n                      pageOptions={options}\\n                      readOnly={readOnly}\\n                      onStatsChange={handleStatsChange}\\n                      onSnapshot={handleSnapshot}\\n                      onCloseToc={closeToc}\\n                      onPersistedMetaChange={(meta: { revision: number | null; conflictDetectionKey: string | null }) => {\\n                        setContentRevision(meta.revision);\\n                        setConflictDetectionKey(meta.conflictDetectionKey);\\n                      }}\\n                    />\\n                  </div>\\n                )}\\n                {!isEditing && (\\n                  <div className=\\\"relative\\\" ref={readViewRootRef}>\\n                    <DocumentReadView\\n                      content={content}\\n                      documentId={documentId}\\n                      options={options}\\n                      pageSubtree={pageSubtree}\\n                      className=\\\"mx-auto w-full max-w-[980px]\\\"\\n                    />\\n                    <DocumentToc\\n                      entries={readViewTocEntries}\\n                      visible={options.showToc}\\n                      onJump={jumpToHeading}\\n                      onClose={closeToc}\\n                    />\\n                  </div>\\n                )}\\n              </div>\\n            )}\\n            <PageBacklinksPanel\\n              className=\\\"mt-10\\\"\\n              workspaceId={workspaceId}\\n              documentId={documentId}\\n              defaultCollapsed={options.collapseBacklinks}\\n            />\\n          </div>\\n        </div>\\n        {showInspector && (\\n          <PageOptionsSidebar\\n            documentId={documentId}\\n            options={options}\\n            stats={stats}\\n            onToggle={toggleOption}\\n            onSetPageFont={handleSetPageFont}\\n            onSetLayoutDensity={handleSetLayoutDensity}\\n            onSetEmbedDefaultToCursor={inspectorCanUseEditorBridge ? handleSetEmbedDefaultToCursor : undefined}\\n            onClearEmbedDefault={handleClearEmbedDefault}\\n            onExport={handleExport}\\n            onOpenHistory={() => 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      </div>\\n      <DocumentHistoryDrawer\\n        open={historyOpen}\\n        onOpenChange={setHistoryOpen}\\n        history={history}\\n        onRestore={handleRestoreSnapshot}\\n      />\\n      <MoveEmbedPickerHost />\\n      <DocumentCommentsDrawer />\\n      <DocumentAiAgentPanel\\n        documentId={documentId}\\n        getLatestBlocks={() => latestBlocksRef.current}\\n        getLatestPageSubtree={() => pageSubtree}\\n      />\\n    </ImagePickerProvider>\\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<PageOptionsState>(initialOptions ?? defaultOptions);\",\n        \"-  const [stats, setStats] = useState<DocumentStats>(initialStats ?? defaultStats);\",\n        \"-  const [history, setHistory] = useState<DocumentSnapshot[]>([]);\",\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<unknown>(initialContent);\",\n        \"-  const [serverContentSnapshot, setServerContentSnapshot] = useState<unknown>(initialContent);\",\n        \"-  const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState<PageSubtreeProjection | null>(\",\n        \"-    initialPageSubtree,\",\n        \"-  );\",\n        \"-  const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState<string>(title ?? \\\"无标题\\\");\",\n        \"-  const [contentRevision, setContentRevision] = useState<number | null>(initialContentRevision);\",\n        \"-  const [conflictDetectionKey, setConflictDetectionKey] = useState<string | null>(initialConflictDetectionKey);\",\n        \"-  const [contentLoading, setContentLoading] = useState(() => initialContent == null);\",\n        \"-  const [contentError, setContentError] = useState<string | null>(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<ReturnType<typeof setTimeout> | null>(null);\",\n        \"-  const editorUnmountTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\",\n        \"-  const pendingOpenTableRef = useRef<string | null>(null);\",\n        \"-  const latestBlocksRef = useRef<Json | null>(null);\",\n        \"-  const pageRootRef = useRef<HTMLDivElement>(null);\",\n        \"-  const readViewRootRef = useRef<HTMLDivElement>(null);\",\n        \"-  const pendingRestoreSnapshotRef = useRef<DocumentSnapshot | null>(null);\",\n        \"-  const lastCopyBlockedAtRef = useRef<number>(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<HTMLInputElement>) => {\",\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<HTMLInputElement>) => {\",\n        \"-    if (event.key === \\\"Enter\\\") {\",\n        \"-      event.preventDefault();\",\n        \"-      event.currentTarget.blur();\",\n        \"-    }\",\n        \"-  };\",\n        \"-\",\n        \"-  const persistOptions = useCallback(\",\n        \"-    async (patch: Partial<PageOptionsState>) => {\",\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<PageOptionsState>);\",\n        \"-        return next;\",\n        \"-      });\",\n        \"-    },\",\n        \"-    [persistOptions, readOnly],\",\n        \"-  );\",\n        \"-\",\n        \"-  const setOptionPatch = useCallback(\",\n        \"-    (patch: Partial<PageOptionsState>) => {\",\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<HTMLElement>(`[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        \"-    <ImagePickerProvider documentId={documentId} workspaceId={workspaceId}>\",\n        \"-      <div className={pageRootClass} ref={pageRootRef}>\",\n        \"-        <div className=\\\"flex h-full flex-1 flex-col overflow-hidden\\\">\",\n        \"-          <div className=\\\"border-b border-wolai-border px-12 pb-6 pt-8\\\">\",\n        \"-            <div className=\\\"flex items-start justify-between gap-6\\\">\",\n        \"-              <div className=\\\"min-w-0 flex-1\\\">\",\n        \"-                {isEditing && canEditDocument ? (\",\n        \"-                  <div className=\\\"relative\\\">\",\n        \"-                    <input\",\n        \"-                      value={pageTitle}\",\n        \"-                      onChange={handleTitleChange}\",\n        \"-                      onBlur={handleTitleBlur}\",\n        \"-                      onKeyDown={handleTitleKeyDown}\",\n        \"-                      placeholder=\\\"无标题\\\"\",\n        \"-                      className=\\\"w-full border-none bg-transparent text-3xl font-semibold text-wolai-text-primary outline-none focus:ring-0\\\"\",\n        \"-                      aria-label=\\\"页面标题\\\"\",\n        \"-                      disabled={options.protectEditing || readOnly}\",\n        \"-                      spellCheck={spellCheck}\",\n        \"-                    />\",\n        \"-                  </div>\",\n        \"-                ) : (\",\n        \"-                  <h1 className=\\\"break-words text-3xl font-semibold text-wolai-text-primary\\\">\",\n        \"-                    {pageTitle || \\\"无标题\\\"}\",\n        \"-                  </h1>\",\n        \"-                )}\",\n        \"-                {readOnly ? (\",\n        \"-                  <p className=\\\"mt-1 text-sm text-gray-500\\\">该页面为只读共享，无法编辑。</p>\",\n        \"-                ) : options.protectEditing && isEditing ? (\",\n        \"-                  <p className=\\\"mt-1 text-sm text-[#b91c1c]\\\">当前页面已开启编辑保护，关闭后方可修改内容。</p>\",\n        \"-                ) : !isEditing && canEditDocument ? (\",\n        \"-                  <p className=\\\"mt-1 text-sm text-gray-500\\\">当前为阅读态，编辑器仅在进入编辑后挂载。</p>\",\n        \"-                ) : null}\",\n        \"-                <p className=\\\"text-sm text-wolai-text-secondary\\\">最近更新：{formattedUpdatedAt}</p>\",\n        \"-              </div>\",\n        \"-              {canEditDocument && (\",\n        \"-                <div className=\\\"flex shrink-0 items-center gap-2\\\">\",\n        \"-                  {isEditing ? (\",\n        \"-                    <Button type=\\\"button\\\" variant=\\\"outline\\\" size=\\\"sm\\\" onClick={handleExitEditMode}>\",\n        \"-                      返回阅读\",\n        \"-                    </Button>\",\n        \"-                  ) : (\",\n        \"-                    <Button type=\\\"button\\\" size=\\\"sm\\\" onClick={handleEnterEditMode}>\",\n        \"-                      进入编辑\",\n        \"-                    </Button>\",\n        \"-                  )}\",\n        \"-                </div>\",\n        \"-              )}\",\n        \"-            </div>\",\n        \"-          </div>\",\n        \"-          <div className=\\\"relative flex-1 overflow-y-auto px-12 py-6\\\">\",\n        \"-            {contentLoading ? (\",\n        \"-              showContentLoadingIndicator ? (\",\n        \"-                <div className=\\\"flex h-64 items-center justify-center text-sm text-gray-400\\\">\",\n        \"-                  页面内容加载中...\",\n        \"-                </div>\",\n        \"-              ) : (\",\n        \"-                <div className=\\\"h-64\\\" />\",\n        \"-              )\",\n        \"-            ) : contentError ? (\",\n        \"-              <div className=\\\"flex h-64 flex-col items-center justify-center gap-2 text-sm text-red-600\\\">\",\n        \"-                <div>{contentError}</div>\",\n        \"-                <button\",\n        \"-                  type=\\\"button\\\"\",\n        \"-                  className=\\\"rounded-md border border-red-200 bg-red-50 px-3 py-1 text-sm text-red-700 hover:bg-red-100\\\"\",\n        \"-                  onClick={() => {\",\n        \"-                    setContentError(null);\",\n        \"-                    setContentLoading(true);\",\n        \"-                    setContentReloadKey((prev) => prev + 1);\",\n        \"-                  }}\",\n        \"-                >\",\n        \"-                  重试\",\n        \"-                </button>\",\n        \"-              </div>\",\n        \"-            ) : (\",\n        \"-              <div className=\\\"relative\\\">\",\n        \"-                {keepEditorMounted && (\",\n        \"-                  <div className={cn(!isEditing && \\\"pointer-events-none absolute inset-0 opacity-0\\\")} aria-hidden={!isEditing}>\",\n        \"-                    <EditorHost\",\n        \"-                      documentId={documentId}\",\n        \"-                      workspaceId={workspaceId}\",\n        \"-                      initialContent={content}\",\n        \"-                      initialRevision={contentRevision}\",\n        \"-                      initialConflictDetectionKey={conflictDetectionKey}\",\n        \"-                      pageOptions={options}\",\n        \"-                      readOnly={readOnly}\",\n        \"-                      onStatsChange={handleStatsChange}\",\n        \"-                      onSnapshot={handleSnapshot}\",\n        \"-                      onCloseToc={closeToc}\",\n        \"-                      onPersistedMetaChange={(meta: { revision: number | null; conflictDetectionKey: string | null }) => {\",\n        \"-                        setContentRevision(meta.revision);\",\n        \"-                        setConflictDetectionKey(meta.conflictDetectionKey);\",\n        \"-                      }}\",\n        \"-                    />\",\n        \"-                  </div>\",\n        \"-                )}\",\n        \"-                {!isEditing && (\",\n        \"-                  <div className=\\\"relative\\\" ref={readViewRootRef}>\",\n        \"-                    <DocumentReadView\",\n        \"-                      content={content}\",\n        \"-                      documentId={documentId}\",\n        \"-                      options={options}\",\n        \"-                      pageSubtree={pageSubtree}\",\n        \"-                      className=\\\"mx-auto w-full max-w-[980px]\\\"\",\n        \"-                    />\",\n        \"-                    <DocumentToc\",\n        \"-                      entries={readViewTocEntries}\",\n        \"-                      visible={options.showToc}\",\n        \"-                      onJump={jumpToHeading}\",\n        \"-                      onClose={closeToc}\",\n        \"-                    />\",\n        \"-                  </div>\",\n        \"-                )}\",\n        \"-              </div>\",\n        \"-            )}\",\n        \"-            <PageBacklinksPanel\",\n        \"-              className=\\\"mt-10\\\"\",\n        \"-              workspaceId={workspaceId}\",\n        \"-              documentId={documentId}\",\n        \"-              defaultCollapsed={options.collapseBacklinks}\",\n        \"-            />\",\n        \"-          </div>\",\n        \"-        </div>\",\n        \"-        {showInspector && (\",\n        \"-          <PageOptionsSidebar\",\n        \"-            documentId={documentId}\",\n        \"-            options={options}\",\n        \"-            stats={stats}\",\n        \"-            onToggle={toggleOption}\",\n        \"-            onSetPageFont={handleSetPageFont}\",\n        \"-            onSetLayoutDensity={handleSetLayoutDensity}\",\n        \"-            onSetEmbedDefaultToCursor={inspectorCanUseEditorBridge ? handleSetEmbedDefaultToCursor : undefined}\",\n        \"-            onClearEmbedDefault={handleClearEmbedDefault}\",\n        \"-            onExport={handleExport}\",\n        \"-            onOpenHistory={() => 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        \"-      </div>\",\n        \"-      <DocumentHistoryDrawer\",\n        \"-        open={historyOpen}\",\n        \"-        onOpenChange={setHistoryOpen}\",\n        \"-        history={history}\",\n        \"-        onRestore={handleRestoreSnapshot}\",\n        \"-      />\",\n        \"-      <MoveEmbedPickerHost />\",\n        \"-      <DocumentCommentsDrawer />\",\n        \"-      <DocumentAiAgentPanel\",\n        \"-        documentId={documentId}\",\n        \"-        getLatestBlocks={() => latestBlocksRef.current}\",\n        \"-        getLatestPageSubtree={() => pageSubtree}\",\n        \"-      />\",\n        \"-    </ImagePickerProvider>\",\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<PageOptionsState>(initialOptions ?? defaultOptions);\",\n        \"+  const [stats, setStats] = useState<DocumentStats>(initialStats ?? defaultStats);\",\n        \"+  const [history, setHistory] = useState<DocumentSnapshot[]>([]);\",\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<unknown>(initialContent);\",\n        \"+  const [serverContentSnapshot, setServerContentSnapshot] = useState<unknown>(initialContent);\",\n        \"+  const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState<PageSubtreeProjection | null>(\",\n        \"+    initialPageSubtree,\",\n        \"+  );\",\n        \"+  const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState<string>(title ?? \\\"无标题\\\");\",\n        \"+  const [contentRevision, setContentRevision] = useState<number | null>(initialContentRevision);\",\n        \"+  const [conflictDetectionKey, setConflictDetectionKey] = useState<string | null>(initialConflictDetectionKey);\",\n        \"+  const [contentLoading, setContentLoading] = useState(() => initialContent == null);\",\n        \"+  const [contentError, setContentError] = useState<string | null>(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<ReturnType<typeof setTimeout> | null>(null);\",\n        \"+  const editorUnmountTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\",\n        \"+  const pendingOpenTableRef = useRef<string | null>(null);\",\n        \"+  const latestBlocksRef = useRef<Json | null>(null);\",\n        \"+  const pageRootRef = useRef<HTMLDivElement>(null);\",\n        \"+  const readViewRootRef = useRef<HTMLDivElement>(null);\",\n        \"+  const pendingRestoreSnapshotRef = useRef<DocumentSnapshot | null>(null);\",\n        \"+  const lastCopyBlockedAtRef = useRef<number>(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<HTMLInputElement>) => {\",\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<HTMLInputElement>) => {\",\n        \"+    if (event.key === \\\"Enter\\\") {\",\n        \"+      event.preventDefault();\",\n        \"+      event.currentTarget.blur();\",\n        \"+    }\",\n        \"+  };\",\n        \"+\",\n        \"+  const persistOptions = useCallback(\",\n        \"+    async (patch: Partial<PageOptionsState>) => {\",\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<PageOptionsState>);\",\n        \"+        return next;\",\n        \"+      });\",\n        \"+    },\",\n        \"+    [persistOptions, readOnly],\",\n        \"+  );\",\n        \"+\",\n        \"+  const setOptionPatch = useCallback(\",\n        \"+    (patch: Partial<PageOptionsState>) => {\",\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<HTMLElement>(`[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        \"+    <ImagePickerProvider documentId={documentId} workspaceId={workspaceId}>\",\n        \"+      <div className={pageRootClass} ref={pageRootRef}>\",\n        \"+        <div className=\\\"flex h-full flex-1 flex-col overflow-hidden\\\">\",\n        \"+          <div className=\\\"border-b border-wolai-border px-12 pb-6 pt-8\\\">\",\n        \"+            <div className=\\\"flex items-start justify-between gap-6\\\">\",\n        \"+              <div className=\\\"min-w-0 flex-1\\\">\",\n        \"+                {isEditing && canEditDocument ? (\",\n        \"+                  <div className=\\\"relative\\\">\",\n        \"+                    <input\",\n        \"+                      value={pageTitle}\",\n        \"+                      onChange={handleTitleChange}\",\n        \"+                      onBlur={handleTitleBlur}\",\n        \"+                      onKeyDown={handleTitleKeyDown}\",\n        \"+                      placeholder=\\\"无标题\\\"\",\n        \"+                      className=\\\"w-full border-none bg-transparent text-3xl font-semibold text-wolai-text-primary outline-none focus:ring-0\\\"\",\n        \"+                      aria-label=\\\"页面标题\\\"\",\n        \"+                      disabled={options.protectEditing || readOnly}\",\n        \"+                      spellCheck={spellCheck}\",\n        \"+                    />\",\n        \"+                  </div>\",\n        \"+                ) : (\",\n        \"+                  <h1 className=\\\"break-words text-3xl font-semibold text-wolai-text-primary\\\">\",\n        \"+                    {pageTitle || \\\"无标题\\\"}\",\n        \"+                  </h1>\",\n        \"+                )}\",\n        \"+                {readOnly ? (\",\n        \"+                  <p className=\\\"mt-1 text-sm text-gray-500\\\">该页面为只读共享，无法编辑。</p>\",\n        \"+                ) : options.protectEditing && isEditing ? (\",\n        \"+                  <p className=\\\"mt-1 text-sm text-[#b91c1c]\\\">当前页面已开启编辑保护，关闭后方可修改内容。</p>\",\n        \"+                ) : null}\",\n        \"+                <p className=\\\"text-sm text-wolai-text-secondary\\\">最近更新：{formattedUpdatedAt}</p>\",\n        \"+              </div>\",\n        \"+              {canEditDocument && (\",\n        \"+                <div className=\\\"flex shrink-0 items-center gap-2\\\">\",\n        \"+                  {isEditing ? (\",\n        \"+                    <Button type=\\\"button\\\" variant=\\\"outline\\\" size=\\\"sm\\\" onClick={handleExitEditMode}>\",\n        \"+                      返回阅读\",\n        \"+                    </Button>\",\n        \"+                  ) : (\",\n        \"+                    <Button type=\\\"button\\\" size=\\\"sm\\\" onClick={handleEnterEditMode}>\",\n        \"+                      进入编辑\",\n        \"+                    </Button>\",\n        \"+                  )}\",\n        \"+                </div>\",\n        \"+              )}\",\n        \"+            </div>\",\n        \"+          </div>\",\n        \"+          <div className=\\\"relative flex-1 overflow-y-auto px-12 py-6\\\">\",\n        \"+            {contentLoading ? (\",\n        \"+              showContentLoadingIndicator ? (\",\n        \"+                <div className=\\\"flex h-64 items-center justify-center text-sm text-gray-400\\\">\",\n        \"+                  页面内容加载中...\",\n        \"+                </div>\",\n        \"+              ) : (\",\n        \"+                <div className=\\\"h-64\\\" />\",\n        \"+              )\",\n        \"+            ) : contentError ? (\",\n        \"+              <div className=\\\"flex h-64 flex-col items-center justify-center gap-2 text-sm text-red-600\\\">\",\n        \"+                <div>{contentError}</div>\",\n        \"+                <button\",\n        \"+                  type=\\\"button\\\"\",\n        \"+                  className=\\\"rounded-md border border-red-200 bg-red-50 px-3 py-1 text-sm text-red-700 hover:bg-red-100\\\"\",\n        \"+                  onClick={() => {\",\n        \"+                    setContentError(null);\",\n        \"+                    setContentLoading(true);\",\n        \"+                    setContentReloadKey((prev) => prev + 1);\",\n        \"+                  }}\",\n        \"+                >\",\n        \"+                  重试\",\n        \"+                </button>\",\n        \"+              </div>\",\n        \"+            ) : (\",\n        \"+              <div className=\\\"relative\\\">\",\n        \"+                {keepEditorMounted && (\",\n        \"+                  <div className={cn(!isEditing && \\\"pointer-events-none absolute inset-0 opacity-0\\\")} aria-hidden={!isEditing}>\",\n        \"+                    <EditorHost\",\n        \"+                      documentId={documentId}\",\n        \"+                      workspaceId={workspaceId}\",\n        \"+                      initialContent={content}\",\n        \"+                      initialRevision={contentRevision}\",\n        \"+                      initialConflictDetectionKey={conflictDetectionKey}\",\n        \"+                      pageOptions={options}\",\n        \"+                      readOnly={readOnly}\",\n        \"+                      onStatsChange={handleStatsChange}\",\n        \"+                      onSnapshot={handleSnapshot}\",\n        \"+                      onCloseToc={closeToc}\",\n        \"+                      onPersistedMetaChange={(meta: { revision: number | null; conflictDetectionKey: string | null }) => {\",\n        \"+                        setContentRevision(meta.revision);\",\n        \"+                        setConflictDetectionKey(meta.conflictDetectionKey);\",\n        \"+                      }}\",\n        \"+                    />\",\n        \"+                  </div>\",\n        \"+                )}\",\n        \"+                {!isEditing && (\",\n        \"+                  <div className=\\\"relative\\\" ref={readViewRootRef}>\",\n        \"+                    <DocumentReadView\",\n        \"+                      content={content}\",\n        \"+                      documentId={documentId}\",\n        \"+                      options={options}\",\n        \"+                      pageSubtree={pageSubtree}\",\n        \"+                      className=\\\"mx-auto w-full max-w-[980px]\\\"\",\n        \"+                    />\",\n        \"+                    <DocumentToc\",\n        \"+                      entries={readViewTocEntries}\",\n        \"+                      visible={options.showToc}\",\n        \"+                      onJump={jumpToHeading}\",\n        \"+                      onClose={closeToc}\",\n        \"+                    />\",\n        \"+                  </div>\",\n        \"+                )}\",\n        \"+              </div>\",\n        \"+            )}\",\n        \"+            <PageBacklinksPanel\",\n        \"+              className=\\\"mt-10\\\"\",\n        \"+              workspaceId={workspaceId}\",\n        \"+              documentId={documentId}\",\n        \"+              defaultCollapsed={options.collapseBacklinks}\",\n        \"+            />\",\n        \"+          </div>\",\n        \"+        </div>\",\n        \"+        {showInspector && (\",\n        \"+          <PageOptionsSidebar\",\n        \"+            documentId={documentId}\",\n        \"+            options={options}\",\n        \"+            stats={stats}\",\n        \"+            onToggle={toggleOption}\",\n        \"+            onSetPageFont={handleSetPageFont}\",\n        \"+            onSetLayoutDensity={handleSetLayoutDensity}\",\n        \"+            onSetEmbedDefaultToCursor={inspectorCanUseEditorBridge ? handleSetEmbedDefaultToCursor : undefined}\",\n        \"+            onClearEmbedDefault={handleClearEmbedDefault}\",\n        \"+            onExport={handleExport}\",\n        \"+            onOpenHistory={() => 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        \"+      </div>\",\n        \"+      <DocumentHistoryDrawer\",\n        \"+        open={historyOpen}\",\n        \"+        onOpenChange={setHistoryOpen}\",\n        \"+        history={history}\",\n        \"+        onRestore={handleRestoreSnapshot}\",\n        \"+      />\",\n        \"+      <MoveEmbedPickerHost />\",\n        \"+      <DocumentCommentsDrawer />\",\n        \"+      <DocumentAiAgentPanel\",\n        \"+        documentId={documentId}\",\n        \"+        getLatestBlocks={() => latestBlocksRef.current}\",\n        \"+        getLatestPageSubtree={() => pageSubtree}\",\n        \"+      />\",\n        \"+    </ImagePickerProvider>\",\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<PageOptionsState>(initialOptions ?? defaultOptions);\\n  const [stats, setStats] = useState<DocumentStats>(initialStats ?? defaultStats);\\n  const [history, setHistory] = useState<DocumentSnapshot[]>([]);\\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<unknown>(initialContent);\\n  const [serverContentSnapshot, setServerContentSnapshot] = useState<unknown>(initialContent);\\n  const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState<PageSubtreeProjection | null>(\\n    initialPageSubtree,\\n  );\\n  const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState<string>(title ?? \\\"无标题\\\");\\n  const [contentRevision, setContentRevision] = useState<number | null>(initialContentRevision);\\n  const [conflictDetectionKey, setConflictDetectionKey] = useState<string | null>(initialConflictDetectionKey);\\n  const [contentLoading, setContentLoading] = useState(() => initialContent == null);\\n  const [contentError, setContentError] = useState<string | null>(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<ReturnType<typeof setTimeout> | null>(null);\\n  const editorUnmountTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\\n  const pendingOpenTableRef = useRef<string | null>(null);\\n  const latestBlocksRef = useRef<Json | null>(null);\\n  const pageRootRef = useRef<HTMLDivElement>(null);\\n  const readViewRootRef = useRef<HTMLDivElement>(null);\\n  const pendingRestoreSnapshotRef = useRef<DocumentSnapshot | null>(null);\\n  const lastCopyBlockedAtRef = useRef<number>(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<HTMLInputElement>) => {\\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<HTMLInputElement>) => {\\n    if (event.key === \\\"Enter\\\") {\\n      event.preventDefault();\\n      event.currentTarget.blur();\\n    }\\n  };\\n\\n  const persistOptions = useCallback(\\n    async (patch: Partial<PageOptionsState>) => {\\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<PageOptionsState>);\\n        return next;\\n      });\\n    },\\n    [persistOptions, readOnly],\\n  );\\n\\n  const setOptionPatch = useCallback(\\n    (patch: Partial<PageOptionsState>) => {\\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<HTMLElement>(`[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    <ImagePickerProvider documentId={documentId} workspaceId={workspaceId}>\\n      <div className={pageRootClass} ref={pageRootRef}>\\n        <div className=\\\"flex h-full flex-1 flex-col overflow-hidden\\\">\\n          <div className=\\\"border-b border-wolai-border px-12 pb-6 pt-8\\\">\\n            <div className=\\\"flex items-start justify-between gap-6\\\">\\n              <div className=\\\"min-w-0 flex-1\\\">\\n                {isEditing && canEditDocument ? (\\n                  <div className=\\\"relative\\\">\\n                    <input\\n                      value={pageTitle}\\n                      onChange={handleTitleChange}\\n                      onBlur={handleTitleBlur}\\n                      onKeyDown={handleTitleKeyDown}\\n                      placeholder=\\\"无标题\\\"\\n                      className=\\\"w-full border-none bg-transparent text-3xl font-semibold text-wolai-text-primary outline-none focus:ring-0\\\"\\n                      aria-label=\\\"页面标题\\\"\\n                      disabled={options.protectEditing || readOnly}\\n                      spellCheck={spellCheck}\\n                    />\\n                  </div>\\n                ) : (\\n                  <h1 className=\\\"break-words text-3xl font-semibold text-wolai-text-primary\\\">\\n                    {pageTitle || \\\"无标题\\\"}\\n                  </h1>\\n                )}\\n                {readOnly ? (\\n                  <p className=\\\"mt-1 text-sm text-gray-500\\\">该页面为只读共享，无法编辑。</p>\\n                ) : options.protectEditing && isEditing ? (\\n                  <p className=\\\"mt-1 text-sm text-[#b91c1c]\\\">当前页面已开启编辑保护，关闭后方可修改内容。</p>\\n                ) : null}\\n                <p className=\\\"text-sm text-wolai-text-secondary\\\">最近更新：{formattedUpdatedAt}</p>\\n              </div>\\n              {canEditDocument && (\\n                <div className=\\\"flex shrink-0 items-center gap-2\\\">\\n                  {isEditing ? (\\n                    <Button type=\\\"button\\\" variant=\\\"outline\\\" size=\\\"sm\\\" onClick={handleExitEditMode}>\\n                      返回阅读\\n                    </Button>\\n                  ) : (\\n                    <Button type=\\\"button\\\" size=\\\"sm\\\" onClick={handleEnterEditMode}>\\n                      进入编辑\\n                    </Button>\\n                  )}\\n                </div>\\n              )}\\n            </div>\\n          </div>\\n          <div className=\\\"relative flex-1 overflow-y-auto px-12 py-6\\\">\\n            {contentLoading ? (\\n              showContentLoadingIndicator ? (\\n                <div className=\\\"flex h-64 items-center justify-center text-sm text-gray-400\\\">\\n                  页面内容加载中...\\n                </div>\\n              ) : (\\n                <div className=\\\"h-64\\\" />\\n              )\\n            ) : contentError ? (\\n              <div className=\\\"flex h-64 flex-col items-center justify-center gap-2 text-sm text-red-600\\\">\\n                <div>{contentError}</div>\\n                <button\\n                  type=\\\"button\\\"\\n                  className=\\\"rounded-md border border-red-200 bg-red-50 px-3 py-1 text-sm text-red-700 hover:bg-red-100\\\"\\n                  onClick={() => {\\n                    setContentError(null);\\n                    setContentLoading(true);\\n                    setContentReloadKey((prev) => prev + 1);\\n                  }}\\n                >\\n                  重试\\n                </button>\\n              </div>\\n            ) : (\\n              <div className=\\\"relative\\\">\\n                {keepEditorMounted && (\\n                  <div className={cn(!isEditing && \\\"pointer-events-none absolute inset-0 opacity-0\\\")} aria-hidden={!isEditing}>\\n                    <EditorHost\\n                      documentId={documentId}\\n                      workspaceId={workspaceId}\\n                      initialContent={content}\\n                      initialRevision={contentRevision}\\n                      initialConflictDetectionKey={conflictDetectionKey}\\n                      pageOptions={options}\\n                      readOnly={readOnly}\\n                      onStatsChange={handleStatsChange}\\n                      onSnapshot={handleSnapshot}\\n                      onCloseToc={closeToc}\\n                      onPersistedMetaChange={(meta: { revision: number | null; conflictDetectionKey: string | null }) => {\\n                        setContentRevision(meta.revision);\\n                        setConflictDetectionKey(meta.conflictDetectionKey);\\n                      }}\\n                    />\\n                  </div>\\n                )}\\n                {!isEditing && (\\n                  <div className=\\\"relative\\\" ref={readViewRootRef}>\\n                    <DocumentReadView\\n                      content={content}\\n                      documentId={documentId}\\n                      options={options}\\n                      pageSubtree={pageSubtree}\\n                      className=\\\"mx-auto w-full max-w-[980px]\\\"\\n                    />\\n                    <DocumentToc\\n                      entries={readViewTocEntries}\\n                      visible={options.showToc}\\n                      onJump={jumpToHeading}\\n                      onClose={closeToc}\\n                    />\\n                  </div>\\n                )}\\n              </div>\\n            )}\\n            <PageBacklinksPanel\\n              className=\\\"mt-10\\\"\\n              workspaceId={workspaceId}\\n              documentId={documentId}\\n              defaultCollapsed={options.collapseBacklinks}\\n            />\\n          </div>\\n        </div>\\n        {showInspector && (\\n          <PageOptionsSidebar\\n            documentId={documentId}\\n            options={options}\\n            stats={stats}\\n            onToggle={toggleOption}\\n            onSetPageFont={handleSetPageFont}\\n            onSetLayoutDensity={handleSetLayoutDensity}\\n            onSetEmbedDefaultToCursor={inspectorCanUseEditorBridge ? handleSetEmbedDefaultToCursor : undefined}\\n            onClearEmbedDefault={handleClearEmbedDefault}\\n            onExport={handleExport}\\n            onOpenHistory={() => 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      </div>\\n      <DocumentHistoryDrawer\\n        open={historyOpen}\\n        onOpenChange={setHistoryOpen}\\n        history={history}\\n        onRestore={handleRestoreSnapshot}\\n      />\\n      <MoveEmbedPickerHost />\\n      <DocumentCommentsDrawer />\\n      <DocumentAiAgentPanel\\n        documentId={documentId}\\n        getLatestBlocks={() => latestBlocksRef.current}\\n        getLatestPageSubtree={() => pageSubtree}\\n      />\\n    </ImagePickerProvider>\\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<PageOptionsState>(initialOptions ?? defaultOptions);\",\n        \"-  const [stats, setStats] = useState<DocumentStats>(initialStats ?? defaultStats);\",\n        \"-  const [history, setHistory] = useState<DocumentSnapshot[]>([]);\",\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<unknown>(initialContent);\",\n        \"-  const [serverContentSnapshot, setServerContentSnapshot] = useState<unknown>(initialContent);\",\n        \"-  const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState<PageSubtreeProjection | null>(\",\n        \"-    initialPageSubtree,\",\n        \"-  );\",\n        \"-  const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState<string>(title ?? \\\"无标题\\\");\",\n        \"-  const [contentRevision, setContentRevision] = useState<number | null>(initialContentRevision);\",\n        \"-  const [conflictDetectionKey, setConflictDetectionKey] = useState<string | null>(initialConflictDetectionKey);\",\n        \"-  const [contentLoading, setContentLoading] = useState(() => initialContent == null);\",\n        \"-  const [contentError, setContentError] = useState<string | null>(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<ReturnType<typeof setTimeout> | null>(null);\",\n        \"-  const editorUnmountTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\",\n        \"-  const pendingOpenTableRef = useRef<string | null>(null);\",\n        \"-  const latestBlocksRef = useRef<Json | null>(null);\",\n        \"-  const pageRootRef = useRef<HTMLDivElement>(null);\",\n        \"-  const readViewRootRef = useRef<HTMLDivElement>(null);\",\n        \"-  const pendingRestoreSnapshotRef = useRef<DocumentSnapshot | null>(null);\",\n        \"-  const lastCopyBlockedAtRef = useRef<number>(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<HTMLInputElement>) => {\",\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<HTMLInputElement>) => {\",\n        \"-    if (event.key === \\\"Enter\\\") {\",\n        \"-      event.preventDefault();\",\n        \"-      event.currentTarget.blur();\",\n        \"-    }\",\n        \"-  };\",\n        \"-\",\n        \"-  const persistOptions = useCallback(\",\n        \"-    async (patch: Partial<PageOptionsState>) => {\",\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<PageOptionsState>);\",\n        \"-        return next;\",\n        \"-      });\",\n        \"-    },\",\n        \"-    [persistOptions, readOnly],\",\n        \"-  );\",\n        \"-\",\n        \"-  const setOptionPatch = useCallback(\",\n        \"-    (patch: Partial<PageOptionsState>) => {\",\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<HTMLElement>(`[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        \"-    <ImagePickerProvider documentId={documentId} workspaceId={workspaceId}>\",\n        \"-      <div className={pageRootClass} ref={pageRootRef}>\",\n        \"-        <div className=\\\"flex h-full flex-1 flex-col overflow-hidden\\\">\",\n        \"-          <div className=\\\"border-b border-wolai-border px-12 pb-6 pt-8\\\">\",\n        \"-            <div className=\\\"flex items-start justify-between gap-6\\\">\",\n        \"-              <div className=\\\"min-w-0 flex-1\\\">\",\n        \"-                {isEditing && canEditDocument ? (\",\n        \"-                  <div className=\\\"relative\\\">\",\n        \"-                    <input\",\n        \"-                      value={pageTitle}\",\n        \"-                      onChange={handleTitleChange}\",\n        \"-                      onBlur={handleTitleBlur}\",\n        \"-                      onKeyDown={handleTitleKeyDown}\",\n        \"-                      placeholder=\\\"无标题\\\"\",\n        \"-                      className=\\\"w-full border-none bg-transparent text-3xl font-semibold text-wolai-text-primary outline-none focus:ring-0\\\"\",\n        \"-                      aria-label=\\\"页面标题\\\"\",\n        \"-                      disabled={options.protectEditing || readOnly}\",\n        \"-                      spellCheck={spellCheck}\",\n        \"-                    />\",\n        \"-                  </div>\",\n        \"-                ) : (\",\n        \"-                  <h1 className=\\\"break-words text-3xl font-semibold text-wolai-text-primary\\\">\",\n        \"-                    {pageTitle || \\\"无标题\\\"}\",\n        \"-                  </h1>\",\n        \"-                )}\",\n        \"-                {readOnly ? (\",\n        \"-                  <p className=\\\"mt-1 text-sm text-gray-500\\\">该页面为只读共享，无法编辑。</p>\",\n        \"-                ) : options.protectEditing && isEditing ? (\",\n        \"-                  <p className=\\\"mt-1 text-sm text-[#b91c1c]\\\">当前页面已开启编辑保护，关闭后方可修改内容。</p>\",\n        \"-                ) : null}\",\n        \"-                <p className=\\\"text-sm text-wolai-text-secondary\\\">最近更新：{formattedUpdatedAt}</p>\",\n        \"-              </div>\",\n        \"-              {canEditDocument && (\",\n        \"-                <div className=\\\"flex shrink-0 items-center gap-2\\\">\",\n        \"-                  {isEditing ? (\",\n        \"-                    <Button type=\\\"button\\\" variant=\\\"outline\\\" size=\\\"sm\\\" onClick={handleExitEditMode}>\",\n        \"-                      返回阅读\",\n        \"-                    </Button>\",\n        \"-                  ) : (\",\n        \"-                    <Button type=\\\"button\\\" size=\\\"sm\\\" onClick={handleEnterEditMode}>\",\n        \"-                      进入编辑\",\n        \"-                    </Button>\",\n        \"-                  )}\",\n        \"-                </div>\",\n        \"-              )}\",\n        \"-            </div>\",\n        \"-          </div>\",\n        \"-          <div className=\\\"relative flex-1 overflow-y-auto px-12 py-6\\\">\",\n        \"-            {contentLoading ? (\",\n        \"-              showContentLoadingIndicator ? (\",\n        \"-                <div className=\\\"flex h-64 items-center justify-center text-sm text-gray-400\\\">\",\n        \"-                  页面内容加载中...\",\n        \"-                </div>\",\n        \"-              ) : (\",\n        \"-                <div className=\\\"h-64\\\" />\",\n        \"-              )\",\n        \"-            ) : contentError ? (\",\n        \"-              <div className=\\\"flex h-64 flex-col items-center justify-center gap-2 text-sm text-red-600\\\">\",\n        \"-                <div>{contentError}</div>\",\n        \"-                <button\",\n        \"-                  type=\\\"button\\\"\",\n        \"-                  className=\\\"rounded-md border border-red-200 bg-red-50 px-3 py-1 text-sm text-red-700 hover:bg-red-100\\\"\",\n        \"-                  onClick={() => {\",\n        \"-                    setContentError(null);\",\n        \"-                    setContentLoading(true);\",\n        \"-                    setContentReloadKey((prev) => prev + 1);\",\n        \"-                  }}\",\n        \"-                >\",\n        \"-                  重试\",\n        \"-                </button>\",\n        \"-              </div>\",\n        \"-            ) : (\",\n        \"-              <div className=\\\"relative\\\">\",\n        \"-                {keepEditorMounted && (\",\n        \"-                  <div className={cn(!isEditing && \\\"pointer-events-none absolute inset-0 opacity-0\\\")} aria-hidden={!isEditing}>\",\n        \"-                    <EditorHost\",\n        \"-                      documentId={documentId}\",\n        \"-                      workspaceId={workspaceId}\",\n        \"-                      initialContent={content}\",\n        \"-                      initialRevision={contentRevision}\",\n        \"-                      initialConflictDetectionKey={conflictDetectionKey}\",\n        \"-                      pageOptions={options}\",\n        \"-                      readOnly={readOnly}\",\n        \"-                      onStatsChange={handleStatsChange}\",\n        \"-                      onSnapshot={handleSnapshot}\",\n        \"-                      onCloseToc={closeToc}\",\n        \"-                      onPersistedMetaChange={(meta: { revision: number | null; conflictDetectionKey: string | null }) => {\",\n        \"-                        setContentRevision(meta.revision);\",\n        \"-                        setConflictDetectionKey(meta.conflictDetectionKey);\",\n        \"-                      }}\",\n        \"-                    />\",\n        \"-                  </div>\",\n        \"-                )}\",\n        \"-                {!isEditing && (\",\n        \"-                  <div className=\\\"relative\\\" ref={readViewRootRef}>\",\n        \"-                    <DocumentReadView\",\n        \"-                      content={content}\",\n        \"-                      documentId={documentId}\",\n        \"-                      options={options}\",\n        \"-                      pageSubtree={pageSubtree}\",\n        \"-                      className=\\\"mx-auto w-full max-w-[980px]\\\"\",\n        \"-                    />\",\n        \"-                    <DocumentToc\",\n        \"-                      entries={readViewTocEntries}\",\n        \"-                      visible={options.showToc}\",\n        \"-                      onJump={jumpToHeading}\",\n        \"-                      onClose={closeToc}\",\n        \"-                    />\",\n        \"-                  </div>\",\n        \"-                )}\",\n        \"-              </div>\",\n        \"-            )}\",\n        \"-            <PageBacklinksPanel\",\n        \"-              className=\\\"mt-10\\\"\",\n        \"-              workspaceId={workspaceId}\",\n        \"-              documentId={documentId}\",\n        \"-              defaultCollapsed={options.collapseBacklinks}\",\n        \"-            />\",\n        \"-          </div>\",\n        \"-        </div>\",\n        \"-        {showInspector && (\",\n        \"-          <PageOptionsSidebar\",\n        \"-            documentId={documentId}\",\n        \"-            options={options}\",\n        \"-            stats={stats}\",\n        \"-            onToggle={toggleOption}\",\n        \"-            onSetPageFont={handleSetPageFont}\",\n        \"-            onSetLayoutDensity={handleSetLayoutDensity}\",\n        \"-            onSetEmbedDefaultToCursor={inspectorCanUseEditorBridge ? handleSetEmbedDefaultToCursor : undefined}\",\n        \"-            onClearEmbedDefault={handleClearEmbedDefault}\",\n        \"-            onExport={handleExport}\",\n        \"-            onOpenHistory={() => 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        \"-      </div>\",\n        \"-      <DocumentHistoryDrawer\",\n        \"-        open={historyOpen}\",\n        \"-        onOpenChange={setHistoryOpen}\",\n        \"-        history={history}\",\n        \"-        onRestore={handleRestoreSnapshot}\",\n        \"-      />\",\n        \"-      <MoveEmbedPickerHost />\",\n        \"-      <DocumentCommentsDrawer />\",\n        \"-      <DocumentAiAgentPanel\",\n        \"-        documentId={documentId}\",\n        \"-        getLatestBlocks={() => latestBlocksRef.current}\",\n        \"-        getLatestPageSubtree={() => pageSubtree}\",\n        \"-      />\",\n        \"-    </ImagePickerProvider>\",\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<PageOptionsState>(initialOptions ?? defaultOptions);\",\n        \"+  const [stats, setStats] = useState<DocumentStats>(initialStats ?? defaultStats);\",\n        \"+  const [history, setHistory] = useState<DocumentSnapshot[]>([]);\",\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<unknown>(initialContent);\",\n        \"+  const [serverContentSnapshot, setServerContentSnapshot] = useState<unknown>(initialContent);\",\n        \"+  const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState<PageSubtreeProjection | null>(\",\n        \"+    initialPageSubtree,\",\n        \"+  );\",\n        \"+  const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState<string>(title ?? \\\"无标题\\\");\",\n        \"+  const [contentRevision, setContentRevision] = useState<number | null>(initialContentRevision);\",\n        \"+  const [conflictDetectionKey, setConflictDetectionKey] = useState<string | null>(initialConflictDetectionKey);\",\n        \"+  const [contentLoading, setContentLoading] = useState(() => initialContent == null);\",\n        \"+  const [contentError, setContentError] = useState<string | null>(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<ReturnType<typeof setTimeout> | null>(null);\",\n        \"+  const editorUnmountTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\",\n        \"+  const pendingOpenTableRef = useRef<string | null>(null);\",\n        \"+  const latestBlocksRef = useRef<Json | null>(null);\",\n        \"+  const pageRootRef = useRef<HTMLDivElement>(null);\",\n        \"+  const readViewRootRef = useRef<HTMLDivElement>(null);\",\n        \"+  const pendingRestoreSnapshotRef = useRef<DocumentSnapshot | null>(null);\",\n        \"+  const lastCopyBlockedAtRef = useRef<number>(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<HTMLInputElement>) => {\",\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<HTMLInputElement>) => {\",\n        \"+    if (event.key === \\\"Enter\\\") {\",\n        \"+      event.preventDefault();\",\n        \"+      event.currentTarget.blur();\",\n        \"+    }\",\n        \"+  };\",\n        \"+\",\n        \"+  const persistOptions = useCallback(\",\n        \"+    async (patch: Partial<PageOptionsState>) => {\",\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<PageOptionsState>);\",\n        \"+        return next;\",\n        \"+      });\",\n        \"+    },\",\n        \"+    [persistOptions, readOnly],\",\n        \"+  );\",\n        \"+\",\n        \"+  const setOptionPatch = useCallback(\",\n        \"+    (patch: Partial<PageOptionsState>) => {\",\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<HTMLElement>(`[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        \"+    <ImagePickerProvider documentId={documentId} workspaceId={workspaceId}>\",\n        \"+      <div className={pageRootClass} ref={pageRootRef}>\",\n        \"+        <div className=\\\"flex h-full flex-1 flex-col overflow-hidden\\\">\",\n        \"+          <div className=\\\"border-b border-wolai-border px-12 pb-6 pt-8\\\">\",\n        \"+            <div className=\\\"flex items-start justify-between gap-6\\\">\",\n        \"+              <div className=\\\"min-w-0 flex-1\\\">\",\n        \"+                {isEditing && canEditDocument ? (\",\n        \"+                  <div className=\\\"relative\\\">\",\n        \"+                    <input\",\n        \"+                      value={pageTitle}\",\n        \"+                      onChange={handleTitleChange}\",\n        \"+                      onBlur={handleTitleBlur}\",\n        \"+                      onKeyDown={handleTitleKeyDown}\",\n        \"+                      placeholder=\\\"无标题\\\"\",\n        \"+                      className=\\\"w-full border-none bg-transparent text-3xl font-semibold text-wolai-text-primary outline-none focus:ring-0\\\"\",\n        \"+                      aria-label=\\\"页面标题\\\"\",\n        \"+                      disabled={options.protectEditing || readOnly}\",\n        \"+                      spellCheck={spellCheck}\",\n        \"+                    />\",\n        \"+                  </div>\",\n        \"+                ) : (\",\n        \"+                  <h1 className=\\\"break-words text-3xl font-semibold text-wolai-text-primary\\\">\",\n        \"+                    {pageTitle || \\\"无标题\\\"}\",\n        \"+                  </h1>\",\n        \"+                )}\",\n        \"+                {readOnly ? (\",\n        \"+                  <p className=\\\"mt-1 text-sm text-gray-500\\\">该页面为只读共享，无法编辑。</p>\",\n        \"+                ) : options.protectEditing && isEditing ? (\",\n        \"+                  <p className=\\\"mt-1 text-sm text-[#b91c1c]\\\">当前页面已开启编辑保护，关闭后方可修改内容。</p>\",\n        \"+                ) : null}\",\n        \"+                <p className=\\\"text-sm text-wolai-text-secondary\\\">最近更新：{formattedUpdatedAt}</p>\",\n        \"+              </div>\",\n        \"+              {canEditDocument && false && (\",\n        \"+                <div className=\\\"flex shrink-0 items-center gap-2\\\">\",\n        \"+                  {isEditing ? (\",\n        \"+                    <Button type=\\\"button\\\" variant=\\\"outline\\\" size=\\\"sm\\\" onClick={handleExitEditMode}>\",\n        \"+                      返回阅读\",\n        \"+                    </Button>\",\n        \"+                  ) : (\",\n        \"+                    <Button type=\\\"button\\\" size=\\\"sm\\\" onClick={handleEnterEditMode}>\",\n        \"+                      进入编辑\",\n        \"+                    </Button>\",\n        \"+                  )}\",\n        \"+                </div>\",\n        \"+              )}\",\n        \"+            </div>\",\n        \"+          </div>\",\n        \"+          <div className=\\\"relative flex-1 overflow-y-auto px-12 py-6\\\">\",\n        \"+            {contentLoading ? (\",\n        \"+              showContentLoadingIndicator ? (\",\n        \"+                <div className=\\\"flex h-64 items-center justify-center text-sm text-gray-400\\\">\",\n        \"+                  页面内容加载中...\",\n        \"+                </div>\",\n        \"+              ) : (\",\n        \"+                <div className=\\\"h-64\\\" />\",\n        \"+              )\",\n        \"+            ) : contentError ? (\",\n        \"+              <div className=\\\"flex h-64 flex-col items-center justify-center gap-2 text-sm text-red-600\\\">\",\n        \"+                <div>{contentError}</div>\",\n        \"+                <button\",\n        \"+                  type=\\\"button\\\"\",\n        \"+                  className=\\\"rounded-md border border-red-200 bg-red-50 px-3 py-1 text-sm text-red-700 hover:bg-red-100\\\"\",\n        \"+                  onClick={() => {\",\n        \"+                    setContentError(null);\",\n        \"+                    setContentLoading(true);\",\n        \"+                    setContentReloadKey((prev) => prev + 1);\",\n        \"+                  }}\",\n        \"+                >\",\n        \"+                  重试\",\n        \"+                </button>\",\n        \"+              </div>\",\n        \"+            ) : (\",\n        \"+              <div className=\\\"relative\\\">\",\n        \"+                {keepEditorMounted && (\",\n        \"+                  <div className={cn(!isEditing && \\\"pointer-events-none absolute inset-0 opacity-0\\\")} aria-hidden={!isEditing}>\",\n        \"+                    <EditorHost\",\n        \"+                      documentId={documentId}\",\n        \"+                      workspaceId={workspaceId}\",\n        \"+                      initialContent={content}\",\n        \"+                      initialRevision={contentRevision}\",\n        \"+                      initialConflictDetectionKey={conflictDetectionKey}\",\n        \"+                      pageOptions={options}\",\n        \"+                      readOnly={readOnly}\",\n        \"+                      onStatsChange={handleStatsChange}\",\n        \"+                      onSnapshot={handleSnapshot}\",\n        \"+                      onCloseToc={closeToc}\",\n        \"+                      onPersistedMetaChange={(meta: { revision: number | null; conflictDetectionKey: string | null }) => {\",\n        \"+                        setContentRevision(meta.revision);\",\n        \"+                        setConflictDetectionKey(meta.conflictDetectionKey);\",\n        \"+                      }}\",\n        \"+                    />\",\n        \"+                  </div>\",\n        \"+                )}\",\n        \"+                {!isEditing && (\",\n        \"+                  <div className=\\\"relative\\\" ref={readViewRootRef}>\",\n        \"+                    <DocumentReadView\",\n        \"+                      content={content}\",\n        \"+                      documentId={documentId}\",\n        \"+                      options={options}\",\n        \"+                      pageSubtree={pageSubtree}\",\n        \"+                      className=\\\"mx-auto w-full max-w-[980px]\\\"\",\n        \"+                    />\",\n        \"+                    <DocumentToc\",\n        \"+                      entries={readViewTocEntries}\",\n        \"+                      visible={options.showToc}\",\n        \"+                      onJump={jumpToHeading}\",\n        \"+                      onClose={closeToc}\",\n        \"+                    />\",\n        \"+                  </div>\",\n        \"+                )}\",\n        \"+              </div>\",\n        \"+            )}\",\n        \"+            <PageBacklinksPanel\",\n        \"+              className=\\\"mt-10\\\"\",\n        \"+              workspaceId={workspaceId}\",\n        \"+              documentId={documentId}\",\n        \"+              defaultCollapsed={options.collapseBacklinks}\",\n        \"+            />\",\n        \"+          </div>\",\n        \"+        </div>\",\n        \"+        {showInspector && (\",\n        \"+          <PageOptionsSidebar\",\n        \"+            documentId={documentId}\",\n        \"+            options={options}\",\n        \"+            stats={stats}\",\n        \"+            onToggle={toggleOption}\",\n        \"+            onSetPageFont={handleSetPageFont}\",\n        \"+            onSetLayoutDensity={handleSetLayoutDensity}\",\n        \"+            onSetEmbedDefaultToCursor={inspectorCanUseEditorBridge ? handleSetEmbedDefaultToCursor : undefined}\",\n        \"+            onClearEmbedDefault={handleClearEmbedDefault}\",\n        \"+            onExport={handleExport}\",\n        \"+            onOpenHistory={() => 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        \"+      </div>\",\n        \"+      <DocumentHistoryDrawer\",\n        \"+        open={historyOpen}\",\n        \"+        onOpenChange={setHistoryOpen}\",\n        \"+        history={history}\",\n        \"+        onRestore={handleRestoreSnapshot}\",\n        \"+      />\",\n        \"+      <MoveEmbedPickerHost />\",\n        \"+      <DocumentCommentsDrawer />\",\n        \"+      <DocumentAiAgentPanel\",\n        \"+        documentId={documentId}\",\n        \"+        getLatestBlocks={() => latestBlocksRef.current}\",\n        \"+        getLatestPageSubtree={() => pageSubtree}\",\n        \"+      />\",\n        \"+    </ImagePickerProvider>\",\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<string, unknown>;\\n};\\n\\nexport type TiptapNode = {\\n  type: string;\\n  attrs?: Record<string, unknown>;\\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<string, unknown>;\\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<string, EditorTextMark> = {\\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<EditorBlockDocument>;\\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<string, unknown> = { [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<string, unknown>;\\n};\\n\\nexport type TiptapNode = {\\n  type: string;\\n  attrs?: Record<string, unknown>;\\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<string, unknown>;\\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<string, EditorTextMark> = {\\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<EditorBlockDocument>;\\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<string, unknown> = { [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<string, unknown>;\",\n        \"-  content?: TiptapNode[];\",\n        \"-  text?: string;\",\n        \"-  marks?: Array<{ type: string; attrs?: Record<string, unknown> }>;\",\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<string, unknown>;\",\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<string, unknown> | 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<string, unknown>;\",\n        \"+};\",\n        \"+\",\n        \"+export type TiptapNode = {\",\n        \"+  type: string;\",\n        \"+  attrs?: Record<string, unknown>;\",\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<string, unknown>;\",\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<string, EditorTextMark> = {\",\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<EditorBlockDocument>;\",\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<string, unknown> = { [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<string, unknown>;\\n  content?: TiptapNode[];\\n  text?: string;\\n  marks?: Array<{ type: string; attrs?: Record<string, unknown> }>;\\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<string, unknown>;\\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<string, unknown> | 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<String>,\\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<String>,\\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<String>,\\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<String>,\\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<String>,\\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<String>,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapCodeBlockAttrs {\\n    pub language: Option<String>,\\n    pub block_id: Option<String>,\\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<TiptapNode> },\\n    Paragraph { #[serde(default)] attrs: TiptapParagraphAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    Heading { attrs: TiptapHeadingAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    BulletList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    OrderedList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    TaskList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    ListItem { #[serde(default)] attrs: TiptapListItemAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    TaskItem { attrs: TiptapTaskItemAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    Blockquote { #[serde(default)] attrs: TiptapBlockquoteAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    CodeBlock { attrs: TiptapCodeBlockAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    Text { text: String, #[serde(default)] marks: Vec<TiptapMark> },\\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<TiptapNode, EditorBlockDocumentTiptapError> {\\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<String>,\\n        doc: &TiptapNode,\\n    ) -> Result<EditorBlockDocument, EditorBlockDocumentTiptapError> {\\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<TiptapNode, EditorBlockDocumentTiptapError> {\\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<EditorBlock, EditorBlockDocumentTiptapError> {\\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<EditorBlock, EditorBlockDocumentTiptapError> {\\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<EditorBlock, EditorBlockDocumentTiptapError> {\\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<Vec<TiptapNode>, 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<Vec<ContentNode>, 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<TextMark> {\\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<String>,\\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<String>,\\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<String>,\\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<String>,\\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<String>,\\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<String>,\\n}\\n\\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\\n#[serde(rename_all = \\\"camelCase\\\")]\\npub struct TiptapCodeBlockAttrs {\\n    pub language: Option<String>,\\n    pub block_id: Option<String>,\\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<TiptapNode> },\\n    Paragraph { #[serde(default)] attrs: TiptapParagraphAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    Heading { attrs: TiptapHeadingAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    BulletList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    OrderedList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    TaskList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    ListItem { #[serde(default)] attrs: TiptapListItemAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    TaskItem { attrs: TiptapTaskItemAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    Blockquote { #[serde(default)] attrs: TiptapBlockquoteAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    CodeBlock { attrs: TiptapCodeBlockAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    Text { text: String, #[serde(default)] marks: Vec<TiptapMark> },\\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<TiptapNode, EditorBlockDocumentTiptapError> {\\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<String>,\\n        doc: &TiptapNode,\\n    ) -> Result<EditorBlockDocument, EditorBlockDocumentTiptapError> {\\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<TiptapNode, EditorBlockDocumentTiptapError> {\\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<EditorBlock, EditorBlockDocumentTiptapError> {\\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<EditorBlock, EditorBlockDocumentTiptapError> {\\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<EditorBlock, EditorBlockDocumentTiptapError> {\\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<Vec<TiptapNode>, 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<Vec<ContentNode>, 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<TextMark> {\\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<String>,\",\n        \"-    pub block_id: Option<String>,\",\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<TiptapNode> },\",\n        \"-    Paragraph { #[serde(default)] attrs: BTreeMap<String, serde_json::Value>, #[serde(default)] content: Vec<TiptapNode> },\",\n        \"-    Heading { attrs: TiptapHeadingAttrs, #[serde(default)] content: Vec<TiptapNode> },\",\n        \"-    BulletList { #[serde(default)] content: Vec<TiptapNode> },\",\n        \"-    OrderedList { #[serde(default)] content: Vec<TiptapNode>, #[serde(default)] attrs: BTreeMap<String, serde_json::Value> },\",\n        \"-    TaskList { #[serde(default)] content: Vec<TiptapNode> },\",\n        \"-    ListItem { #[serde(default)] content: Vec<TiptapNode> },\",\n        \"-    TaskItem { attrs: TiptapTaskItemAttrs, #[serde(default)] content: Vec<TiptapNode> },\",\n        \"-    Blockquote { #[serde(default)] content: Vec<TiptapNode> },\",\n        \"-    CodeBlock { attrs: TiptapCodeBlockAttrs, #[serde(default)] content: Vec<TiptapNode> },\",\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<TiptapNode, EditorBlockDocumentTiptapError> {\",\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<String>, doc: &TiptapNode) -> Result<EditorBlockDocument, EditorBlockDocumentTiptapError> {\",\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<TiptapNode, EditorBlockDocumentTiptapError> {\",\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<EditorBlock, EditorBlockDocumentTiptapError> {\",\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<EditorBlock, EditorBlockDocumentTiptapError> {\",\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<EditorBlock, EditorBlockDocumentTiptapError> {\",\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<Vec<TiptapNode>, 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<Vec<ContentNode>, 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<String>,\",\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<String>,\",\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<String>,\",\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<String>,\",\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<String>,\",\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<String>,\",\n        \"+}\",\n        \"+\",\n        \"+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\",\n        \"+#[serde(rename_all = \\\"camelCase\\\")]\",\n        \"+pub struct TiptapCodeBlockAttrs {\",\n        \"+    pub language: Option<String>,\",\n        \"+    pub block_id: Option<String>,\",\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<TiptapNode> },\",\n        \"+    Paragraph { #[serde(default)] attrs: TiptapParagraphAttrs, #[serde(default)] content: Vec<TiptapNode> },\",\n        \"+    Heading { attrs: TiptapHeadingAttrs, #[serde(default)] content: Vec<TiptapNode> },\",\n        \"+    BulletList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec<TiptapNode> },\",\n        \"+    OrderedList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec<TiptapNode> },\",\n        \"+    TaskList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec<TiptapNode> },\",\n        \"+    ListItem { #[serde(default)] attrs: TiptapListItemAttrs, #[serde(default)] content: Vec<TiptapNode> },\",\n        \"+    TaskItem { attrs: TiptapTaskItemAttrs, #[serde(default)] content: Vec<TiptapNode> },\",\n        \"+    Blockquote { #[serde(default)] attrs: TiptapBlockquoteAttrs, #[serde(default)] content: Vec<TiptapNode> },\",\n        \"+    CodeBlock { attrs: TiptapCodeBlockAttrs, #[serde(default)] content: Vec<TiptapNode> },\",\n        \"+    Text { text: String, #[serde(default)] marks: Vec<TiptapMark> },\",\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<TiptapNode, EditorBlockDocumentTiptapError> {\",\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<String>,\",\n        \"+        doc: &TiptapNode,\",\n        \"+    ) -> Result<EditorBlockDocument, EditorBlockDocumentTiptapError> {\",\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<TiptapNode, EditorBlockDocumentTiptapError> {\",\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<EditorBlock, EditorBlockDocumentTiptapError> {\",\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<EditorBlock, EditorBlockDocumentTiptapError> {\",\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<EditorBlock, EditorBlockDocumentTiptapError> {\",\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<Vec<TiptapNode>, 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<Vec<ContentNode>, 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<TextMark> {\",\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<String>,\\n    pub block_id: Option<String>,\\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<TiptapNode> },\\n    Paragraph { #[serde(default)] attrs: BTreeMap<String, serde_json::Value>, #[serde(default)] content: Vec<TiptapNode> },\\n    Heading { attrs: TiptapHeadingAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    BulletList { #[serde(default)] content: Vec<TiptapNode> },\\n    OrderedList { #[serde(default)] content: Vec<TiptapNode>, #[serde(default)] attrs: BTreeMap<String, serde_json::Value> },\\n    TaskList { #[serde(default)] content: Vec<TiptapNode> },\\n    ListItem { #[serde(default)] content: Vec<TiptapNode> },\\n    TaskItem { attrs: TiptapTaskItemAttrs, #[serde(default)] content: Vec<TiptapNode> },\\n    Blockquote { #[serde(default)] content: Vec<TiptapNode> },\\n    CodeBlock { attrs: TiptapCodeBlockAttrs, #[serde(default)] content: Vec<TiptapNode> },\\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<TiptapNode, EditorBlockDocumentTiptapError> {\\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<String>, doc: &TiptapNode) -> Result<EditorBlockDocument, EditorBlockDocumentTiptapError> {\\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<TiptapNode, EditorBlockDocumentTiptapError> {\\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<EditorBlock, EditorBlockDocumentTiptapError> {\\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<EditorBlock, EditorBlockDocumentTiptapError> {\\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<EditorBlock, EditorBlockDocumentTiptapError> {\\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<Vec<TiptapNode>, 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<Vec<ContentNode>, 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"}
