Files
mnote/.claw/sessions/570ce2f4c13b9ce5/session-1776666102061-0.rot-1776669768652.jsonl
T
2026-04-21 06:26:35 +08:00

84 lines
744 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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## 问题 3Rust 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## 问题 6save 仍然 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## 问题 8Rust 测试也在接受错误 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"}