Output:
{
  "type": "update",
  "filePath": "/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/tiptap.rs",
  "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",
  "structuredPatch": [
    {
      "oldStart": 1,
      "oldLines": 155,
      "newStart": 1,
      "newLines": 427,
      "lines": [
        "-use crate::editor::model::{",
        "-    BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument, EditorBlockType,",
        "-};",
        "-use serde::{Deserialize, Serialize};",
        "-use std::collections::BTreeMap;",
        "-",
        "-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]",
        "-#[serde(rename_all = \"snake_case\")]",
        "-pub enum TiptapBlockType {",
        "-    Paragraph,",
        "-    Heading,",
        "-    BulletList,",
        "-    OrderedList,",
        "-    TaskList,",
        "-    ListItem,",
        "-    TaskItem,",
        "-    Blockquote,",
        "-    CodeBlock,",
        "-    Text,",
        "-    Doc,",
        "-}",
        "-",
        "-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]",
        "-#[serde(rename_all = \"camelCase\")]",
        "-pub struct TiptapHeadingAttrs {",
        "-    pub level: u8,",
        "-}",
        "-",
        "-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]",
        "-#[serde(rename_all = \"camelCase\")]",
        "-pub struct TiptapTaskItemAttrs {",
        "-    #[serde(default)]",
        "-    pub checked: bool,",
        "-}",
        "-",
        "-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]",
        "-#[serde(rename_all = \"camelCase\")]",
        "-pub struct TiptapCodeBlockAttrs {",
        "-    pub language: Option<String>,",
        "-    pub block_id: Option<String>,",
        "-}",
        "-",
        "-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]",
        "-#[serde(tag = \"type\", rename_all = \"snake_case\")]",
        "-pub enum TiptapNode {",
        "-    Doc { content: Vec<TiptapNode> },",
        "-    Paragraph { #[serde(default)] attrs: BTreeMap<String, serde_json::Value>, #[serde(default)] content: Vec<TiptapNode> },",
        "-    Heading { attrs: TiptapHeadingAttrs, #[serde(default)] content: Vec<TiptapNode> },",
        "-    BulletList { #[serde(default)] content: Vec<TiptapNode> },",
        "-    OrderedList { #[serde(default)] content: Vec<TiptapNode>, #[serde(default)] attrs: BTreeMap<String, serde_json::Value> },",
        "-    TaskList { #[serde(default)] content: Vec<TiptapNode> },",
        "-    ListItem { #[serde(default)] content: Vec<TiptapNode> },",
        "-    TaskItem { attrs: TiptapTaskItemAttrs, #[serde(default)] content: Vec<TiptapNode> },",
        "-    Blockquote { #[serde(default)] content: Vec<TiptapNode> },",
        "-    CodeBlock { attrs: TiptapCodeBlockAttrs, #[serde(default)] content: Vec<TiptapNode> },",
        "-    Text { text: String },",
        "-}",
        "-",
        "-#[derive(Debug, Clone, PartialEq, Eq)]",
        "-pub enum EditorBlockDocumentTiptapError {",
        "-    UnsupportedNode(&'static str),",
        "-    InvalidDocument(&'static str),",
        "-}",
        "-",
        "-pub struct EditorBlockDocumentTiptapBridge;",
        "-",
        "-impl EditorBlockDocumentTiptapBridge {",
        "-    pub fn to_tiptap_doc(document: &EditorBlockDocument) -> Result<TiptapNode, EditorBlockDocumentTiptapError> {",
        "-        let mut content = Vec::new();",
        "-        for block_id in &document.root_block_ids {",
        "-            let block = document.blocks.iter().find(|block| &block.block_id == block_id).ok_or(EditorBlockDocumentTiptapError::InvalidDocument(\"根块不存在\"))?;",
        "-            content.push(block_to_tiptap_node(block)?);",
        "-        }",
        "-        Ok(TiptapNode::Doc { content })",
        "-    }",
        "-",
        "-    pub fn from_tiptap_doc(document_id: impl Into<String>, doc: &TiptapNode) -> Result<EditorBlockDocument, EditorBlockDocumentTiptapError> {",
        "-        match doc {",
        "-            TiptapNode::Doc { content } => {",
        "-                let mut blocks = Vec::new();",
        "-                let mut root_block_ids = Vec::new();",
        "-                for (index, node) in content.iter().enumerate() {",
        "-                    let block = node_to_block(node, index)?;",
        "-                    root_block_ids.push(block.block_id.clone());",
        "-                    blocks.push(block);",
        "-                }",
        "-                Ok(EditorBlockDocument { document_id: document_id.into(), root_block_ids, blocks })",
        "-            }",
        "-            _ => Err(EditorBlockDocumentTiptapError::InvalidDocument(\"顶层必须是 doc\")),",
        "-        }",
        "-    }",
        "-}",
        "-",
        "-fn block_to_tiptap_node(block: &EditorBlock) -> Result<TiptapNode, EditorBlockDocumentTiptapError> {",
        "-    let content = text_nodes_to_tiptap(&block.content_nodes)?;",
        "-    match block.block_type {",
        "-        EditorBlockType::Paragraph => Ok(TiptapNode::Paragraph { attrs: BTreeMap::new(), content }),",
        "-        EditorBlockType::Heading => Ok(TiptapNode::Heading { attrs: TiptapHeadingAttrs { level: block.props.heading_level.unwrap_or(1) }, content }),",
        "-        EditorBlockType::BulletListItem => Ok(TiptapNode::BulletList { content: vec![TiptapNode::ListItem { content }] }),",
        "-        EditorBlockType::NumberedListItem => Ok(TiptapNode::OrderedList { attrs: BTreeMap::new(), content: vec![TiptapNode::ListItem { content }] }),",
        "-        EditorBlockType::Todo => Ok(TiptapNode::TaskList { content: vec![TiptapNode::TaskItem { attrs: TiptapTaskItemAttrs { checked: block.props.checked.unwrap_or(false) }, content }] }),",
        "-        EditorBlockType::Quote => Ok(TiptapNode::Blockquote { content }),",
        "-        EditorBlockType::CodeBlock => Ok(TiptapNode::CodeBlock { attrs: TiptapCodeBlockAttrs { language: block.props.language.clone(), block_id: Some(block.block_id.clone()) }, content }),",
        "-        _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(\"当前块型暂不支持导出到 Tiptap\")),",
        "-    }",
        "-}",
        "-",
        "-fn node_to_block(node: &TiptapNode, index: usize) -> Result<EditorBlock, EditorBlockDocumentTiptapError> {",
        "-    let block_id = format!(\"block_{}\", index + 1);",
        "-    match node {",
        "-        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![] }),",
        "-        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![] }),",
        "-        TiptapNode::BulletList { content } => list_node_to_block(block_id, EditorBlockType::BulletListItem, content),",
        "-        TiptapNode::OrderedList { content, .. } => list_node_to_block(block_id, EditorBlockType::NumberedListItem, content),",
        "-        TiptapNode::TaskList { content } => list_task_node_to_block(block_id, content),",
        "-        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![] }),",
        "-        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![] }),",
        "-        _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(\"当前节点暂不支持导入为 EditorBlockDocument\")),",
        "-    }",
        "-}",
        "-",
        "-fn list_node_to_block(block_id: String, block_type: EditorBlockType, content: &[TiptapNode]) -> Result<EditorBlock, EditorBlockDocumentTiptapError> {",
        "-    let Some(first) = content.first() else { return Err(EditorBlockDocumentTiptapError::InvalidDocument(\"列表节点缺少 list_item/task_item\")); };",
        "-    let item_content = match first { TiptapNode::ListItem { content } | TiptapNode::TaskItem { content, .. } => content, _ => return Err(EditorBlockDocumentTiptapError::InvalidDocument(\"列表节点首子节点类型不正确\")), };",
        "-    Ok(EditorBlock { block_id, block_type, props: BlockProps::default(), content_nodes: text_nodes_from_tiptap(item_content)?, child_block_ids: vec![] })",
        "-}",
        "-",
        "-fn list_task_node_to_block(block_id: String, content: &[TiptapNode]) -> Result<EditorBlock, EditorBlockDocumentTiptapError> {",
        "-    let Some(first) = content.first() else { return Err(EditorBlockDocumentTiptapError::InvalidDocument(\"任务列表节点缺少 task_item\")); };",
        "-    let (checked, item_content) = match first { TiptapNode::TaskItem { attrs, content } => (Some(attrs.checked), content), _ => return Err(EditorBlockDocumentTiptapError::InvalidDocument(\"任务列表节点首子节点类型不正确\")), };",
        "-    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![] })",
        "-}",
        "-",
        "-fn text_nodes_to_tiptap(nodes: &[ContentNode]) -> Result<Vec<TiptapNode>, EditorBlockDocumentTiptapError> {",
        "-    let mut out = Vec::new();",
        "-    for node in nodes {",
        "-        match &node.payload {",
        "-            ContentNodePayload::Text { text, .. } => out.push(TiptapNode::Text { text: text.clone() }),",
        "-            ContentNodePayload::HardBreak => out.push(TiptapNode::Text { text: \"\\n\".into() }),",
        "-            ContentNodePayload::ReferenceToken { .. } => return Err(EditorBlockDocumentTiptapError::UnsupportedNode(\"引用 token 暂不映射到 Tiptap 文本合同\")),",
        "-        }",
        "-    }",
        "-    Ok(out)",
        "-}",
        "-",
        "-fn text_nodes_from_tiptap(nodes: &[TiptapNode]) -> Result<Vec<ContentNode>, EditorBlockDocumentTiptapError> {",
        "-    let mut out = Vec::new();",
        "-    for node in nodes {",
        "-        match node {",
        "-            TiptapNode::Text { text } => out.push(ContentNode { payload: ContentNodePayload::Text { text: text.clone(), marks: vec![] }, attrs: BTreeMap::new() }),",
        "-            _ => return Err(EditorBlockDocumentTiptapError::UnsupportedNode(\"当前只支持文本节点内容\")),",
        "-        }",
        "-    }",
        "-    Ok(out)",
        "-}",
        "+use crate::editor::model::{",
        "+    BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument, EditorBlockType,",
        "+    TextMark,",
        "+};",
        "+use serde::{Deserialize, Serialize};",
        "+use std::collections::BTreeMap;",
        "+",
        "+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]",
        "+#[serde(rename_all = \"snake_case\")]",
        "+pub enum TiptapBlockType {",
        "+    Paragraph,",
        "+    Heading,",
        "+    BulletList,",
        "+    OrderedList,",
        "+    TaskList,",
        "+    ListItem,",
        "+    TaskItem,",
        "+    Blockquote,",
        "+    CodeBlock,",
        "+    Text,",
        "+    HardBreak,",
        "+    Doc,",
        "+}",
        "+",
        "+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]",
        "+#[serde(rename_all = \"camelCase\")]",
        "+pub struct TiptapHeadingAttrs {",
        "+    pub level: u8,",
        "+    #[serde(default)]",
        "+    pub block_id: Option<String>,",
        "+}",
        "+",
        "+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]",
        "+#[serde(rename_all = \"camelCase\")]",
        "+pub struct TiptapTaskItemAttrs {",
        "+    #[serde(default)]",
        "+    pub checked: bool,",
        "+    #[serde(default)]",
        "+    pub block_id: Option<String>,",
        "+}",
        "+",
        "+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]",
        "+#[serde(rename_all = \"camelCase\")]",
        "+pub struct TiptapParagraphAttrs {",
        "+    #[serde(default)]",
        "+    pub block_id: Option<String>,",
        "+}",
        "+",
        "+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]",
        "+#[serde(rename_all = \"camelCase\")]",
        "+pub struct TiptapListAttrs {",
        "+    #[serde(default)]",
        "+    pub block_id: Option<String>,",
        "+}",
        "+",
        "+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]",
        "+#[serde(rename_all = \"camelCase\")]",
        "+pub struct TiptapListItemAttrs {",
        "+    #[serde(default)]",
        "+    pub block_id: Option<String>,",
        "+}",
        "+",
        "+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]",
        "+#[serde(rename_all = \"camelCase\")]",
        "+pub struct TiptapBlockquoteAttrs {",
        "+    #[serde(default)]",
        "+    pub block_id: Option<String>,",
        "+}",
        "+",
        "+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]",
        "+#[serde(rename_all = \"camelCase\")]",
        "+pub struct TiptapCodeBlockAttrs {",
        "+    pub language: Option<String>,",
        "+    pub block_id: Option<String>,",
        "+}",
        "+",
        "+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]",
        "+#[serde(rename_all = \"camelCase\")]",
        "+pub struct TiptapMark {",
        "+    pub r#type: String,",
        "+}",
        "+",
        "+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]",
        "+#[serde(tag = \"type\", rename_all = \"camelCase\")]",
        "+pub enum TiptapNode {",
        "+    Doc { content: Vec<TiptapNode> },",
        "+    Paragraph { #[serde(default)] attrs: TiptapParagraphAttrs, #[serde(default)] content: Vec<TiptapNode> },",
        "+    Heading { attrs: TiptapHeadingAttrs, #[serde(default)] content: Vec<TiptapNode> },",
        "+    BulletList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec<TiptapNode> },",
        "+    OrderedList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec<TiptapNode> },",
        "+    TaskList { #[serde(default)] attrs: TiptapListAttrs, #[serde(default)] content: Vec<TiptapNode> },",
        "+    ListItem { #[serde(default)] attrs: TiptapListItemAttrs, #[serde(default)] content: Vec<TiptapNode> },",
        "+    TaskItem { attrs: TiptapTaskItemAttrs, #[serde(default)] content: Vec<TiptapNode> },",
        "+    Blockquote { #[serde(default)] attrs: TiptapBlockquoteAttrs, #[serde(default)] content: Vec<TiptapNode> },",
        "+    CodeBlock { attrs: TiptapCodeBlockAttrs, #[serde(default)] content: Vec<TiptapNode> },",
        "+    Text { text: String, #[serde(default)] marks: Vec<TiptapMark> },",
        "+    HardBreak,",
        "+}",
        "+",
        "+#[derive(Debug, Clone, PartialEq, Eq)]",
        "+pub enum EditorBlockDocumentTiptapError {",
        "+    UnsupportedNode(&'static str),",
        "+    InvalidDocument(&'static str),",
        "+}",
        "+",
        "+pub struct EditorBlockDocumentTiptapBridge;",
        "+",
        "+impl EditorBlockDocumentTiptapBridge {",
        "+    pub fn to_tiptap_doc(document: &EditorBlockDocument) -> Result<TiptapNode, EditorBlockDocumentTiptapError> {",
        "+        let mut content = Vec::new();",
        "+        for block_id in &document.root_block_ids {",
        "+            let block = document",
        "+                .blocks",
        "+                .iter()",
        "+                .find(|block| &block.block_id == block_id)",
        "+                .ok_or(EditorBlockDocumentTiptapError::InvalidDocument(\"根块不存在\"))?;",
        "+            content.push(block_to_tiptap_node(block)?);",
        "+        }",
        "+        Ok(TiptapNode::Doc { content })",
        "+    }",
        "+",
        "+    pub fn from_tiptap_doc(",
        "+        document_id: impl Into<String>,",
        "+        doc: &TiptapNode,",
        "+    ) -> Result<EditorBlockDocument, EditorBlockDocumentTiptapError> {",
        "+        match doc {",
        "+            TiptapNode::Doc { content } => {",
        "+                let mut blocks = Vec::new();",
        "+                let mut root_block_ids = Vec::new();",
        "+                for (index, node) in content.iter().enumerate() {",
        "+                    let block = node_to_block(node, index)?;",
        "+                    root_block_ids.push(block.block_id.clone());",
        "+                    blocks.push(block);",
        "+                }",
        "+                Ok(EditorBlockDocument {",
        "+                    document_id: document_id.into(),",
        "+                    root_block_ids,",
        "+                    blocks,",
        "+                })",
        "+            }",
        "+            _ => Err(EditorBlockDocumentTiptapError::InvalidDocument(\"顶层必须是 doc\")),",
        "+        }",
        "+    }",
        "+}",
        "+",
        "+fn block_to_tiptap_node(block: &EditorBlock) -> Result<TiptapNode, EditorBlockDocumentTiptapError> {",
        "+    let content = text_nodes_to_tiptap(&block.content_nodes)?;",
        "+    match block.block_type {",
        "+        EditorBlockType::Paragraph => Ok(TiptapNode::Paragraph {",
        "+            attrs: TiptapParagraphAttrs {",
        "+                block_id: Some(block.block_id.clone()),",
        "+            },",
        "+            content,",
        "+        }),",
        "+        EditorBlockType::Heading => Ok(TiptapNode::Heading {",
        "+            attrs: TiptapHeadingAttrs {",
        "+                level: block.props.heading_level.unwrap_or(1),",
        "+                block_id: Some(block.block_id.clone()),",
        "+            },",
        "+            content,",
        "+        }),",
        "+        EditorBlockType::BulletListItem => Ok(TiptapNode::BulletList {",
        "+            attrs: TiptapListAttrs {",
        "+                block_id: Some(block.block_id.clone()),",
        "+            },",
        "+            content: vec![TiptapNode::ListItem {",
        "+                attrs: TiptapListItemAttrs {",
        "+                    block_id: Some(block.block_id.clone()),",
        "+                },",
        "+                content: vec![TiptapNode::Paragraph {",
        "+                    attrs: TiptapParagraphAttrs {",
        "+                        block_id: Some(block.block_id.clone()),",
        "+                    },",
        "+                    content,",
        "+                }],",
        "+            }],",
        "+        }),",
        "+        EditorBlockType::NumberedListItem => Ok(TiptapNode::OrderedList {",
        "+            attrs: TiptapListAttrs {",
        "+                block_id: Some(block.block_id.clone()),",
        "+            },",
        "+            content: vec![TiptapNode::ListItem {",
        "+                attrs: TiptapListItemAttrs {",
        "+                    block_id: Some(block.block_id.clone()),",
        "+                },",
        "+                content: vec![TiptapNode::Paragraph {",
        "+                    attrs: TiptapParagraphAttrs {",
        "+                        block_id: Some(block.block_id.clone()),",
        "+                    },",
        "+                    content,",
        "+                }],",
        "+            }],",
        "+        }),",
        "+        EditorBlockType::Todo => Ok(TiptapNode::TaskList {",
        "+            attrs: TiptapListAttrs {",
        "+                block_id: Some(block.block_id.clone()),",
        "+            },",
        "+            content: vec![TiptapNode::TaskItem {",
        "+                attrs: TiptapTaskItemAttrs {",
        "+                    checked: block.props.checked.unwrap_or(false),",
        "+                    block_id: Some(block.block_id.clone()),",
        "+                },",
        "+                content: vec![TiptapNode::Paragraph {",
        "+                    attrs: TiptapParagraphAttrs {",
        "+                        block_id: Some(block.block_id.clone()),",
        "+                    },",
        "+                    content,",
        "+                }],",
        "+            }],",
        "+        }),",
        "+        EditorBlockType::Quote => Ok(TiptapNode::Blockquote {",
        "+            attrs: TiptapBlockquoteAttrs {",
        "+                block_id: Some(block.block_id.clone()),",
        "+            },",
        "+            content: vec![TiptapNode::Paragraph {",
        "+                attrs: TiptapParagraphAttrs {",
        "+                    block_id: Some(block.block_id.clone()),",
        "+                },",
        "+                content,",
        "+            }],",
        "+        }),",
        "+        EditorBlockType::CodeBlock => Ok(TiptapNode::CodeBlock {",
        "+            attrs: TiptapCodeBlockAttrs {",
        "+                language: block.props.language.clone(),",
        "+                block_id: Some(block.block_id.clone()),",
        "+            },",
        "+            content,",
        "+        }),",
        "+        _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(",
        "+            \"当前块型暂不支持导出到 Tiptap\",",
        "+        )),",
        "+    }",
        "+}",
        "+",
        "+fn node_to_block(node: &TiptapNode, index: usize) -> Result<EditorBlock, EditorBlockDocumentTiptapError> {",
        "+    let fallback_block_id = format!(\"block_{}\", index + 1);",
        "+    match node {",
        "+        TiptapNode::Paragraph { attrs, content } => Ok(EditorBlock {",
        "+            block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),",
        "+            block_type: EditorBlockType::Paragraph,",
        "+            props: BlockProps::default(),",
        "+            content_nodes: text_nodes_from_tiptap(content)?,",
        "+            child_block_ids: vec![],",
        "+        }),",
        "+        TiptapNode::Heading { attrs, content } => Ok(EditorBlock {",
        "+            block_id: attrs.block_id.clone().unwrap_or(fallback_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![],",
        "+        }),",
        "+        TiptapNode::BulletList { attrs, content } => list_node_to_block(",
        "+            attrs.block_id.clone().unwrap_or(fallback_block_id),",
        "+            EditorBlockType::BulletListItem,",
        "+            content,",
        "+        ),",
        "+        TiptapNode::OrderedList { attrs, content } => list_node_to_block(",
        "+            attrs.block_id.clone().unwrap_or(fallback_block_id),",
        "+            EditorBlockType::NumberedListItem,",
        "+            content,",
        "+        ),",
        "+        TiptapNode::TaskList { attrs, content } => list_task_node_to_block(",
        "+            attrs.block_id.clone().unwrap_or(fallback_block_id),",
        "+            content,",
        "+        ),",
        "+        TiptapNode::Blockquote { attrs, content } => Ok(EditorBlock {",
        "+            block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),",
        "+            block_type: EditorBlockType::Quote,",
        "+            props: BlockProps::default(),",
        "+            content_nodes: text_nodes_from_tiptap(extract_block_container_content(content)?)?,",
        "+            child_block_ids: vec![],",
        "+        }),",
        "+        TiptapNode::CodeBlock { attrs, content } => Ok(EditorBlock {",
        "+            block_id: attrs.block_id.clone().unwrap_or(fallback_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![],",
        "+        }),",
        "+        _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(",
        "+            \"当前节点暂不支持导入为 EditorBlockDocument\",",
        "+        )),",
        "+    }",
        "+}",
        "+",
        "+fn list_node_to_block(",
        "+    block_id: String,",
        "+    block_type: EditorBlockType,",
        "+    content: &[TiptapNode],",
        "+) -> Result<EditorBlock, EditorBlockDocumentTiptapError> {",
        "+    Ok(EditorBlock {",
        "+        block_id,",
        "+        block_type,",
        "+        props: BlockProps::default(),",
        "+        content_nodes: text_nodes_from_tiptap(extract_list_item_inline_content(content)?)?,",
        "+        child_block_ids: vec![],",
        "+    })",
        "+}",
        "+",
        "+fn list_task_node_to_block(",
        "+    block_id: String,",
        "+    content: &[TiptapNode],",
        "+) -> Result<EditorBlock, EditorBlockDocumentTiptapError> {",
        "+    let Some(first) = content.first() else {",
        "+        return Err(EditorBlockDocumentTiptapError::InvalidDocument(",
        "+            \"任务列表节点缺少 task_item\",",
        "+        ));",
        "+    };",
        "+    let (checked, item_content) = match first {",
        "+        TiptapNode::TaskItem { attrs, content } => (Some(attrs.checked), extract_block_container_content(content)?),",
        "+        _ => {",
        "+            return Err(EditorBlockDocumentTiptapError::InvalidDocument(",
        "+                \"任务列表节点首子节点类型不正确\",",
        "+            ))",
        "+        }",
        "+    };",
        "+    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![],",
        "+    })",
        "+}",
        "+",
        "+fn extract_list_item_inline_content(content: &[TiptapNode]) -> Result<&[TiptapNode], EditorBlockDocumentTiptapError> {",
        "+    let Some(first) = content.first() else {",
        "+        return Err(EditorBlockDocumentTiptapError::InvalidDocument(",
        "+            \"列表节点缺少 list_item/task_item\",",
        "+        ));",
        "+    };",
        "+    match first {",
        "+        TiptapNode::ListItem { content, .. } | TiptapNode::TaskItem { content, .. } => {",
        "+            extract_block_container_content(content)",
        "+        }",
        "+        _ => Err(EditorBlockDocumentTiptapError::InvalidDocument(",
        "+            \"列表节点首子节点类型不正确\",",
        "+        )),",
        "+    }",
        "+}",
        "+",
        "+fn extract_block_container_content(content: &[TiptapNode]) -> Result<&[TiptapNode], EditorBlockDocumentTiptapError> {",
        "+    let Some(first) = content.first() else {",
        "+        return Ok(content);",
        "+    };",
        "+    match first {",
        "+        TiptapNode::Paragraph { content, .. } => Ok(content),",
        "+        _ => Ok(content),",
        "+    }",
        "+}",
        "+",
        "+fn text_nodes_to_tiptap(nodes: &[ContentNode]) -> Result<Vec<TiptapNode>, EditorBlockDocumentTiptapError> {",
        "+    let mut out = Vec::new();",
        "+    for node in nodes {",
        "+        match &node.payload {",
        "+            ContentNodePayload::Text { text, marks } => out.push(TiptapNode::Text {",
        "+                text: text.clone(),",
        "+                marks: marks.iter().map(text_mark_to_tiptap).collect(),",
        "+            }),",
        "+            ContentNodePayload::HardBreak => out.push(TiptapNode::HardBreak),",
        "+            ContentNodePayload::ReferenceToken { .. } => {",
        "+                return Err(EditorBlockDocumentTiptapError::UnsupportedNode(",
        "+                    \"引用 token 暂不映射到 Tiptap 文本合同\",",
        "+                ))",
        "+            }",
        "+        }",
        "+    }",
        "+    Ok(out)",
        "+}",
        "+",
        "+fn text_nodes_from_tiptap(nodes: &[TiptapNode]) -> Result<Vec<ContentNode>, EditorBlockDocumentTiptapError> {",
        "+    let mut out = Vec::new();",
        "+    for node in nodes {",
        "+        match node {",
        "+            TiptapNode::Text { text, marks } => out.push(ContentNode {",
        "+                payload: ContentNodePayload::Text {",
        "+                    text: text.clone(),",
        "+                    marks: marks.iter().filter_map(tiptap_mark_to_text_mark).collect(),",
        "+                },",
        "+                attrs: BTreeMap::new(),",
        "+            }),",
        "+            TiptapNode::HardBreak => out.push(ContentNode {",
        "+                payload: ContentNodePayload::HardBreak,",
        "+                attrs: BTreeMap::new(),",
        "+            }),",
        "+            _ => {",
        "+                return Err(EditorBlockDocumentTiptapError::UnsupportedNode(",
        "+                    \"当前只支持文本节点内容\",",
        "+                ))",
        "+            }",
        "+        }",
        "+    }",
        "+    Ok(out)",
        "+}",
        "+",
        "+fn text_mark_to_tiptap(mark: &TextMark) -> TiptapMark {",
        "+    let r#type = match mark {",
        "+        TextMark::Bold => \"bold\",",
        "+        TextMark::Italic => \"italic\",",
        "+        TextMark::Underline => \"underline\",",
        "+        TextMark::Strike => \"strike\",",
        "+        TextMark::Code => \"code\",",
        "+    };",
        "+    TiptapMark {",
        "+        r#type: r#type.to_string(),",
        "+    }",
        "+}",
        "+",
        "+fn tiptap_mark_to_text_mark(mark: &TiptapMark) -> Option<TextMark> {",
        "+    match mark.r#type.as_str() {",
        "+        \"bold\" => Some(TextMark::Bold),",
        "+        \"italic\" => Some(TextMark::Italic),",
        "+        \"underline\" => Some(TextMark::Underline),",
        "+        \"strike\" => Some(TextMark::Strike),",
        "+        \"code\" => Some(TextMark::Code),",
        "+        _ => None,",
        "+    }",
        "+}"
      ]
    }
  ],
  "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",
  "gitDiff": null
}


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"}


Output:
{
  "type": "update",
  "filePath": "/mnt/Data1T/mnote/rust/crates/core-protocol/tests/editor_tiptap_bridge.rs",
  "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",
  "structuredPatch": [
    {
      "oldStart": 1,
      "oldLines": 49,
      "newStart": 1,
      "newLines": 204,
      "lines": [
        "-use core_protocol::editor::{",
        "-    BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument,",
        "-    EditorBlockDocumentTiptapBridge, EditorBlockType, TiptapCodeBlockAttrs, TiptapHeadingAttrs,",
        "-    TiptapNode, TiptapTaskItemAttrs,",
        "-};",
        "-use std::collections::BTreeMap;",
        "-",
        "-#[test]",
        "-fn p05_blocks_round_trip_through_tiptap_doc() {",
        "-    let document = EditorBlockDocument {",
        "-        document_id: \"doc_1\".into(),",
        "-        root_block_ids: vec![\"p1\".into(), \"h1\".into(), \"b1\".into(), \"n1\".into(), \"t1\".into(), \"q1\".into(), \"c1\".into()],",
        "-        blocks: vec![",
        "-            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![] },",
        "-            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![] },",
        "-            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![] },",
        "-            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![] },",
        "-            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![] },",
        "-            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![] },",
        "-            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![] },",
        "-        ],",
        "-    };",
        "-",
        "-    let doc = EditorBlockDocumentTiptapBridge::to_tiptap_doc(&document).expect(\"export should work\");",
        "-    let round_trip = EditorBlockDocumentTiptapBridge::from_tiptap_doc(\"doc_2\", &doc).expect(\"import should work\");",
        "-",
        "-    assert_eq!(round_trip.blocks.len(), 7);",
        "-    assert_eq!(round_trip.root_block_ids.len(), 7);",
        "-    assert_eq!(round_trip.blocks[0].block_type, EditorBlockType::Paragraph);",
        "-    assert_eq!(round_trip.blocks[1].props.heading_level, Some(2));",
        "-    assert_eq!(round_trip.blocks[4].props.checked, Some(true));",
        "-    assert_eq!(round_trip.blocks[6].props.language.as_deref(), Some(\"rust\"));",
        "-}",
        "-",
        "-#[test]",
        "-fn tiptap_list_and_code_block_attrs_preserve_contract_edges() {",
        "-    let doc = TiptapNode::Doc {",
        "-        content: vec![",
        "-            TiptapNode::Heading { attrs: TiptapHeadingAttrs { level: 3 }, content: vec![TiptapNode::Text { text: \"H\".into() }] },",
        "-            TiptapNode::TaskList { content: vec![TiptapNode::TaskItem { attrs: TiptapTaskItemAttrs { checked: true }, content: vec![TiptapNode::Text { text: \"todo\".into() }] }] },",
        "-            TiptapNode::CodeBlock { attrs: TiptapCodeBlockAttrs { language: Some(\"ts\".into()), block_id: Some(\"c1\".into()) }, content: vec![TiptapNode::Text { text: \"let x = 1;\".into() }] },",
        "-        ],",
        "-    };",
        "-    let imported = EditorBlockDocumentTiptapBridge::from_tiptap_doc(\"doc_3\", &doc).unwrap();",
        "-    assert_eq!(imported.blocks[0].props.heading_level, Some(3));",
        "-    assert_eq!(imported.blocks[1].props.checked, Some(true));",
        "-    assert_eq!(imported.blocks[2].props.language.as_deref(), Some(\"ts\"));",
        "-    assert_eq!(imported.blocks[2].block_id, \"block_3\");",
        "-}",
        "+use core_protocol::editor::{",
        "+    BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument,",
        "+    EditorBlockDocumentTiptapBridge, EditorBlockType, TextMark, TiptapCodeBlockAttrs,",
        "+    TiptapHeadingAttrs, TiptapNode, TiptapParagraphAttrs, TiptapTaskItemAttrs,",
        "+};",
        "+use std::collections::BTreeMap;",
        "+",
        "+#[test]",
        "+fn p05_blocks_round_trip_through_tiptap_doc() {",
        "+    let document = EditorBlockDocument {",
        "+        document_id: \"doc_1\".into(),",
        "+        root_block_ids: vec![\"p1\".into(), \"h1\".into(), \"b1\".into(), \"n1\".into(), \"t1\".into(), \"q1\".into(), \"c1\".into()],",
        "+        blocks: vec![",
        "+            EditorBlock {",
        "+                block_id: \"p1\".into(),",
        "+                block_type: EditorBlockType::Paragraph,",
        "+                props: BlockProps::default(),",
        "+                content_nodes: vec![ContentNode {",
        "+                    payload: ContentNodePayload::Text {",
        "+                        text: \"段落\".into(),",
        "+                        marks: vec![TextMark::Bold],",
        "+                    },",
        "+                    attrs: BTreeMap::new(),",
        "+                }],",
        "+                child_block_ids: vec![],",
        "+            },",
        "+            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![],",
        "+            },",
        "+            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![],",
        "+            },",
        "+            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![],",
        "+            },",
        "+            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![],",
        "+            },",
        "+            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![],",
        "+            },",
        "+            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![TextMark::Code],",
        "+                    },",
        "+                    attrs: BTreeMap::new(),",
        "+                }],",
        "+                child_block_ids: vec![],",
        "+            },",
        "+        ],",
        "+    };",
        "+",
        "+    let doc = EditorBlockDocumentTiptapBridge::to_tiptap_doc(&document).expect(\"export should work\");",
        "+    let round_trip =",
        "+        EditorBlockDocumentTiptapBridge::from_tiptap_doc(\"doc_2\", &doc).expect(\"import should work\");",
        "+",
        "+    assert_eq!(round_trip.root_block_ids, vec![\"p1\", \"h1\", \"b1\", \"n1\", \"t1\", \"q1\", \"c1\"]);",
        "+    assert_eq!(round_trip.blocks[0].block_id, \"p1\");",
        "+    assert_eq!(round_trip.blocks[0].content_nodes.len(), 1);",
        "+    match &round_trip.blocks[0].content_nodes[0].payload {",
        "+        ContentNodePayload::Text { marks, .. } => assert_eq!(marks, &vec![TextMark::Bold]),",
        "+        other => panic!(\"unexpected payload: {other:?}\"),",
        "+    }",
        "+    assert_eq!(round_trip.blocks[1].props.heading_level, Some(2));",
        "+    assert_eq!(round_trip.blocks[4].props.checked, Some(true));",
        "+    assert_eq!(round_trip.blocks[6].props.language.as_deref(), Some(\"rust\"));",
        "+}",
        "+",
        "+#[test]",
        "+fn tiptap_import_preserves_explicit_block_ids_and_marks() {",
        "+    let doc = TiptapNode::Doc {",
        "+        content: vec![",
        "+            TiptapNode::Paragraph {",
        "+                attrs: TiptapParagraphAttrs {",
        "+                    block_id: Some(\"p-1\".into()),",
        "+                },",
        "+                content: vec![TiptapNode::Text {",
        "+                    text: \"Hello\".into(),",
        "+                    marks: vec![core_protocol::editor::TiptapMark {",
        "+                        r#type: \"italic\".into(),",
        "+                    }],",
        "+                }],",
        "+            },",
        "+            TiptapNode::Heading {",
        "+                attrs: TiptapHeadingAttrs {",
        "+                    level: 3,",
        "+                    block_id: Some(\"h-1\".into()),",
        "+                },",
        "+                content: vec![TiptapNode::Text {",
        "+                    text: \"H\".into(),",
        "+                    marks: vec![],",
        "+                }],",
        "+            },",
        "+            TiptapNode::TaskList {",
        "+                attrs: core_protocol::editor::TiptapListAttrs {",
        "+                    block_id: Some(\"t-1\".into()),",
        "+                },",
        "+                content: vec![TiptapNode::TaskItem {",
        "+                    attrs: TiptapTaskItemAttrs {",
        "+                        checked: true,",
        "+                        block_id: Some(\"t-1\".into()),",
        "+                    },",
        "+                    content: vec![TiptapNode::Paragraph {",
        "+                        attrs: TiptapParagraphAttrs {",
        "+                            block_id: Some(\"t-1\".into()),",
        "+                        },",
        "+                        content: vec![TiptapNode::Text {",
        "+                            text: \"todo\".into(),",
        "+                            marks: vec![],",
        "+                        }],",
        "+                    }],",
        "+                }],",
        "+            },",
        "+            TiptapNode::CodeBlock {",
        "+                attrs: TiptapCodeBlockAttrs {",
        "+                    language: Some(\"ts\".into()),",
        "+                    block_id: Some(\"c-1\".into()),",
        "+                },",
        "+                content: vec![TiptapNode::Text {",
        "+                    text: \"let x = 1;\".into(),",
        "+                    marks: vec![core_protocol::editor::TiptapMark {",
        "+                        r#type: \"code\".into(),",
        "+                    }],",
        "+                }],",
        "+            },",
        "+        ],",
        "+    };",
        "+",
        "+    let imported = EditorBlockDocumentTiptapBridge::from_tiptap_doc(\"doc_3\", &doc).unwrap();",
        "+    assert_eq!(imported.root_block_ids, vec![\"p-1\", \"h-1\", \"t-1\", \"c-1\"]);",
        "+    assert_eq!(imported.blocks[0].block_id, \"p-1\");",
        "+    assert_eq!(imported.blocks[1].props.heading_level, Some(3));",
        "+    assert_eq!(imported.blocks[2].props.checked, Some(true));",
        "+    assert_eq!(imported.blocks[3].props.language.as_deref(), Some(\"ts\"));",
        "+",
        "+    match &imported.blocks[0].content_nodes[0].payload {",
        "+        ContentNodePayload::Text { marks, .. } => assert_eq!(marks, &vec![TextMark::Italic]),",
        "+        other => panic!(\"unexpected payload: {other:?}\"),",
        "+    }",
        "+}"
      ]
    }
  ],
  "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",
  "gitDiff": null
}


bash

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}


Output:
{
  "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",
  "stderr": "",
  "rawOutputPath": null,
  "interrupted": false,
  "isImage": null,
  "backgroundTaskId": null,
  "backgroundedByUser": null,
  "assistantAutoBackgrounded": null,
  "dangerouslyDisableSandbox": false,
  "returnCodeInterpretation": null,
  "noOutputExpected": false,
  "structuredContent": null,
  "persistedOutputPath": null,
  "persistedOutputSize": null,
  "sandboxStatus": {
    "enabled": true,
    "requested": {
      "enabled": true,
      "namespace_restrictions": true,
      "network_isolation": true,
      "filesystem_mode": "workspace-only",
      "allowed_mounts": [
        "/mnt/Data1T/mnote"
      ]
    },
    "supported": true,
    "active": true,
    "namespace_supported": true,
    "namespace_active": true,
    "network_supported": true,
    "network_active": true,
    "filesystem_mode": "workspace-only",
    "filesystem_active": true,
    "allowed_mounts": [
      "/mnt/Data1T/mnote"
    ],
    "in_container": false,
    "container_markers": [],
    "fallback_reason": null
  }
}


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}


Output:
{
  "stdout": "",
  "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",
  "rawOutputPath": null,
  "interrupted": false,
  "isImage": null,
  "backgroundTaskId": null,
  "backgroundedByUser": null,
  "assistantAutoBackgrounded": null,
  "dangerouslyDisableSandbox": false,
  "returnCodeInterpretation": "exit_code:1",
  "noOutputExpected": false,
  "structuredContent": null,
  "persistedOutputPath": null,
  "persistedOutputSize": null,
  "sandboxStatus": {
    "enabled": true,
    "requested": {
      "enabled": true,
      "namespace_restrictions": true,
      "network_isolation": true,
      "filesystem_mode": "workspace-only",
      "allowed_mounts": [
        "/mnt/Data1T/mnote"
      ]
    },
    "supported": true,
    "active": true,
    "namespace_supported": true,
    "namespace_active": true,
    "network_supported": true,
    "network_active": true,
    "filesystem_mode": "workspace-only",
    "filesystem_active": true,
    "allowed_mounts": [
      "/mnt/Data1T/mnote"
    ],
    "in_container": false,
    "container_markers": [],
    "fallback_reason": null
  }
}


TodoWrite

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"}]}


Output:
{
  "oldTodos": [
    {
      "content": "审查当前主编辑器接入与保存链路的相关代码和测试",
      "activeForm": "审查当前主编辑器接入与保存链路的相关代码和测试",
      "status": "in_progress"
    },
    {
      "content": "修正默认进入编辑态与主编辑器 host 选择，确保 3000 文档页默认使用 tiptap 而非 blocknote",
      "activeForm": "修正默认进入编辑态与主编辑器 host 选择，确保 3000 文档页默认使用 tiptap 而非 blocknote",
      "status": "pending"
    },
    {
      "content": "修正 tiptap 与 Rust truth 的最小 block_id/load/save 合同",
      "activeForm": "修正 tiptap 与 Rust truth 的最小 block_id/load/save 合同",
      "status": "pending"
    },
    {
      "content": "补充并运行针对主编辑器切换与编辑链路的测试",
      "activeForm": "补充并运行针对主编辑器切换与编辑链路的测试",
      "status": "pending"
    }
  ],
  "newTodos": [
    {
      "content": "审查当前主编辑器接入与保存链路的相关代码和测试",
      "activeForm": "审查当前主编辑器接入与保存链路的相关代码和测试",
      "status": "completed"
    },
    {
      "content": "修正默认进入编辑态与主编辑器 host 选择，确保 3000 文档页默认使用 tiptap 而非 blocknote",
      "activeForm": "修正默认进入编辑态与主编辑器 host 选择，确保 3000 文档页默认使用 tiptap 而非 blocknote",
      "status": "completed"
    },
    {
      "content": "修正 tiptap 与 Rust truth 的最小 block_id/load/save 合同",
      "activeForm": "修正 tiptap 与 Rust truth 的最小 block_id/load/save 合同",
      "status": "completed"
    },
    {
      "content": "补充并运行针对主编辑器切换与编辑链路的测试",
      "activeForm": "补充并运行针对主编辑器切换与编辑链路的测试",
      "status": "in_progress"
    }
  ],
  "verificationNudgeNeeded": null
}