pub mod ai; pub mod command; pub mod common; pub mod editor; pub mod governance; pub mod kernel; pub mod mindmap; pub mod page_aggregate; pub mod query; pub mod search; pub mod tool; pub use ai::{ AiAccessPermissionLevel, AiAccessScope, AiEvent, AiEventKind, AiRuntimeOwner, AiSession, AiShareContext, AiStructuredWriteKind, AiStructuredWriteResult, AiToolCall, }; pub use command::{ check_when, evaluate_when, is_command_enabled, parse_when_expr, CommandContext, CommandContextValue, CommandEnvelope, CopyTreeDocumentPages, CreateDocumentPage, CreatePage, CreateWorkspace, DeleteBlock, DeleteDocumentPage, DuplicateDocumentPage, EmbedBlock, InsertBlock, MoveBlock, MoveDocumentPage, PatchBlock, PatchPageBlock, PutMindmap, ReplaceMediaAssetStorage, RestoreDocumentPage, SavePageContent, UpdateBlock, UpdatePageOptions, UpdatePageStats, UpdatePageTitle, WhenExprNode, WhenExprToken, }; pub use common::{ ActorPayload, AffectedObject, ErrorDetail, ErrorPayload, OkPayload, RequestMeta, ResponseMeta, SourcePayload, TargetRef, }; pub use editor::{ BlockProps, ContentNode, ContentNodePayload, EditorAttachReferenceToken, EditorBlock, EditorBlockDocument, EditorBlockType, EditorCommand, EditorCommandCatalog, EditorCommandDescriptor, EditorCommandKind, EditorDeleteBlock, EditorDetachReferenceToken, EditorIndentBlock, EditorInsertBlockAfter, EditorKernelCommandMapping, EditorMergeWithPrevious, EditorMoveBlock, EditorOutdentBlock, EditorReplaceBlock, EditorSplitBlock, EditorToggleHeadingCollapse, MarkdownExportBoundary, MarkdownExportMode, MarkdownExportOptions, MarkdownExportRequest, MarkdownExportResult, MarkdownFlavor, MarkdownImportBoundary, MarkdownImportMode, MarkdownImportOptions, MarkdownImportRequest, MarkdownImportResult, ReferenceToken, ReferenceTokenKind, ReferenceTokenStrategy, TextMark, }; pub use kernel::{ DocBufferDirtyState, DocumentBuffer, DocumentContentResult, DocumentReadEvidenceItem, DocumentReadEvidenceKind, DocumentReadNode, DocumentReadNodeMeta, DocumentReadNodeType, DocumentReadOutlineEntry, DocumentReadPageSubtree, DocumentReadStats, DocumentReadSubtree, KernelAttachEdge, KernelAuditStamp, KernelBlockAssetRelation, KernelContentPayload, KernelCreateNode, KernelDetachEdge, KernelEdge, KernelEdgeListResult, KernelEdgeType, KernelGetNode, KernelGetSubtree, KernelGraphDirection, KernelGraphTraversalResult, KernelGraphVisit, KernelListChildren, KernelListEdges, KernelMoveSubtree, KernelNode, KernelNodeMetadata, KernelNodeType, KernelObjectIdentity, KernelObjectKind, KernelProjectionAssetKind, KernelProjectionCapability, KernelProjectionFilter, KernelProjectionItem, KernelProjectionKind, KernelProjectionRequest, KernelProjectionResourceKind, KernelProjectionResourceMeta, KernelProjectionResult, KernelProjectionRowKind, KernelRefsPayload, KernelSubtreeRef, KernelSubtreeResult, KernelTraverseGraph, KernelUpdateNode, ObjectWorkspacePath, PageBodyWriteRequest, WorkspaceSource, WorkspaceSourceCapability, WorkspaceSourceKind, }; pub use mindmap::{ MindmapAdapterProjection, MindmapAssociativeLine, MindmapCommand, MindmapKernelCapabilities, MindmapKernelCommand, MindmapKernelEdge, MindmapKernelNode, MindmapKernelProjection, MindmapNodeData, MindmapNodeInput, MindmapNodeRef, MindmapOp, MindmapProjection, MindmapProjectionEdge, MindmapProjectionNode, MindmapProjectionOwner, MindmapProjectionSource, MindmapSummary, MindmapTreeNode, }; pub use page_aggregate::{ PageAggregateProjection, PageAggregateSource, PageBody, PageHead, PageIdentity, PageLayout, PageOptions, PagePermissions, PageStats, PageTree, }; pub use query::{ GetBlock, GetBridgeCommand, GetBridgeRequest, GetBridgeTrace, GetMindmap, GetPage, GetPageContent, GetPageMeta, ListBridgeWorkspaceOverview, ListPageBlocks, ListSidebarDataset, QueryEnvelope, SearchBlocks, SearchDocuments, SearchPages, SearchRecent, }; pub use search::{ SearchHighlight, SearchProjectionOwner, SearchQuery, SearchResultProjection, SearchScope, }; pub use tool::{ default_tool_registry, invocation_kind_label, tool_effect_label, tool_mode_label, InvocationKind, ToolEffect, ToolExecutionMode, ToolInvocation, ToolRegistry, ToolSetSpec, ToolSpec, BRIDGE_TOOL_COMMAND_GET, BRIDGE_TOOL_REQUEST_GET, BRIDGE_TOOL_TRACE_GET, DOCS_TOOLSET_READ, DOCS_TOOL_READ, DOCS_TOOL_SEARCH, DOC_TOOLSET_READ, DOC_TOOLSET_WRITE, DOC_TOOL_FIND, DOC_TOOL_GET, DOC_TOOL_INSERT_BLOCKS, DOC_TOOL_REPLACE_RANGE, INDEX_TOOL_REBUILD, MINDMAP_TOOLSET_READ, MINDMAP_TOOLSET_WRITE, MINDMAP_TOOL_APPLY_OPS, MINDMAP_TOOL_EMPTY_TRASH, MINDMAP_TOOL_EXPAND_NODE, MINDMAP_TOOL_GET, MINDMAP_TOOL_GET_SUBTREE, MINDMAP_TOOL_OUTLINE_TO_MINDMAP, MINDMAP_TOOL_PUT, OBSERVE_TOOLSET_READ, ONLYOFFICE_TOOLSET_SERVICE, ONLYOFFICE_TOOL_PREPARE_CALLBACK, ONLYOFFICE_TOOL_PREPARE_FORCESAVE, ONLYOFFICE_TOOL_PREPARE_PROXY, ONLYOFFICE_TOOL_SESSION_RESOLVE, ONLYOFFICE_TOOL_SIGN, RECOVERY_TOOLSET_JOB, REPLAY_TOOL_EVENTS, }; #[cfg(test)] mod tests { use super::*; #[test] fn command_envelope_keeps_validate_only_flag() { let envelope = CommandEnvelope:: { name: "create_workspace".into(), command_id: "cmd_1".into(), idempotency_key: Some("idem_1".into()), actor: ActorPayload { actor_type: "human".into(), actor_id: "user_1".into(), session_id: Some("session_1".into()), }, source: SourcePayload { channel: "cli".into(), client: "mnote-cli".into(), }, target: None, payload: CreateWorkspace { name: "demo".into(), slug: Some("demo".into()), }, reason: Some("初始化工作区".into()), refs: vec![], dry_run: false, validate_only: true, }; assert!(envelope.validate_only); } #[test] fn default_tool_registry_exposes_doc_tools() { let registry = default_tool_registry(); assert_eq!( registry.tool_names(), vec![ "search_web", "doc_get", "doc_find", "docs_search", "docs_read", "image_read", "doc_insert_blocks", "doc_replace_range", "slash_run", "mindmap_get", "mindmap_get_subtree", "mindmap_put", "mindmap_apply_ops", "mindmap_expand_node", "mindmap_empty_trash", "mindmap_outline_to_mindmap", "bridge_request_get", "bridge_trace_get", "bridge_command_get", "event_replay", "index_rebuild", "onlyoffice_session_resolve", "onlyoffice_sign", "onlyoffice_prepare_proxy", "onlyoffice_prepare_callback", "onlyoffice_prepare_forcesave", ] ); assert_eq!( registry .tool("search_web") .expect("search_web should exist") .toolset_id, "toolset.readonly" ); assert_eq!( registry .tool("image_read") .expect("image_read should exist") .toolset_id, "toolset.media_read" ); assert_eq!( registry .tool("slash_run") .expect("slash_run should exist") .toolset_id, "toolset.slash_write" ); let readonly = registry .toolset("toolset.readonly") .expect("readonly toolset should exist"); assert!(!readonly.write_toolset); assert_eq!(readonly.tool_names, &["search_web"]); let media = registry .toolset("toolset.media_read") .expect("media toolset should exist"); assert!(!media.write_toolset); assert_eq!(media.tool_names, &["image_read"]); let slash = registry .toolset("toolset.slash_write") .expect("slash toolset should exist"); assert!(slash.write_toolset); assert_eq!(slash.tool_names, &["slash_run"]); let docs_read = registry .toolset("toolset.docs_read") .expect("docs_read toolset should exist"); assert!(!docs_read.write_toolset); assert_eq!(docs_read.tool_names, &["docs_search", "docs_read"]); let doc_write = registry .toolset("toolset.doc_write") .expect("doc_write toolset should exist"); assert!(doc_write.write_toolset); assert_eq!( doc_write.tool_names, &["doc_insert_blocks", "doc_replace_range"] ); assert_eq!( registry .tool("doc_get") .expect("doc_get should exist") .toolset_id, "toolset.doc_read" ); let mindmap_write = registry .toolset("toolset.mindmap_write") .expect("mindmap_write toolset should exist"); assert!(mindmap_write.write_toolset); assert_eq!( mindmap_write.tool_names, &[ "mindmap_put", "mindmap_apply_ops", "mindmap_expand_node", "mindmap_empty_trash", "mindmap_outline_to_mindmap", ] ); let onlyoffice_service = registry .toolset("toolset.onlyoffice_service") .expect("onlyoffice service toolset should exist"); assert!(onlyoffice_service.write_toolset); assert_eq!( onlyoffice_service.tool_names, &[ "onlyoffice_session_resolve", "onlyoffice_sign", "onlyoffice_prepare_proxy", "onlyoffice_prepare_callback", "onlyoffice_prepare_forcesave", ] ); let observe = registry .toolset("toolset.observe_read") .expect("observe toolset should exist"); assert!(!observe.write_toolset); assert_eq!( observe.tool_names, &[ "bridge_request_get", "bridge_trace_get", "bridge_command_get" ] ); let recovery = registry .toolset("toolset.recovery_job") .expect("recovery toolset should exist"); assert!(recovery.write_toolset); assert_eq!(recovery.tool_names, &["event_replay", "index_rebuild"]); } #[test] fn page_aggregate_projection_contract_covers_kernel_owned_fields() { let projection = PageAggregateProjection { schema: "mnote.page_aggregate.v1".into(), projection_version: 1, source: PageAggregateSource::KernelProjection, page_id: "page_1".into(), parent_id: Some("root".into()), title: "Rust 页面".into(), path: vec!["root".into(), "page_1".into()], sidebar_tree_membership: vec!["workspace-sidebar".into()], body_ref: Some("page_1:body".into()), layout_options: serde_json::json!({"wideLayout": true}), updated_at: Some("2026-04-29T00:00:00Z".into()), identity: page_aggregate::PageIdentity { document_id: "page_1".into(), workspace_id: "ws_demo".into(), }, head: page_aggregate::PageHead { title: "Rust 页面".into(), updated_at: serde_json::json!("2026-04-29T00:00:00Z"), permissions: page_aggregate::PagePermissions { read_only: false, disable_download: false, disable_copy: false, }, }, layout: page_aggregate::PageLayout { page_options: page_aggregate::PageOptions::default(), }, body: page_aggregate::PageBody { content: serde_json::json!([]), revision: serde_json::json!(7), conflict_detection_key: serde_json::json!("page_1:7"), file_version: serde_json::json!(null), block_document: serde_json::json!({ "documentId": "page_1", "rootBlockIds": [], "blocks": [] }), block_projection_version: 1, projection_source: "fixture".into(), attachment_refs: serde_json::json!([]), }, tree: page_aggregate::PageTree { page_subtree: serde_json::json!({"rootNodeId": "page_1"}), }, stats: page_aggregate::PageStats::default(), }; assert_eq!(projection.source, PageAggregateSource::KernelProjection); assert_eq!(projection.projection_version, 1); assert_eq!(projection.page_id, "page_1"); assert_eq!(projection.identity.document_id, "page_1"); } #[test] fn search_projection_contract_marks_owner_and_scope() { let result = SearchResultProjection { id: "page_1".into(), title: "Rust 搜索".into(), snippet: "Rust Web".into(), updated_at: Some("2026-04-29T00:00:00Z".into()), created_at: None, match_field: "title".into(), has_ocr: false, public_path: "/documents/page_1".into(), node_id: Some("page_1".into()), subtree_root_id: Some("page_1".into()), evidence: vec![], score: 3.0, projection_owner: SearchProjectionOwner::RustKernel, }; let query = SearchQuery { query: "Rust".into(), workspace_id: "ws_demo".into(), scope: SearchScope::Workspace, page_id: None, limit: 20, }; assert_eq!(query.scope, SearchScope::Workspace); assert_eq!(result.projection_owner, SearchProjectionOwner::RustKernel); } #[test] fn ai_protocol_contract_exposes_structured_write_result() { let result = AiStructuredWriteResult { ok: true, write_kind: AiStructuredWriteKind::SummaryNode, command_name: "tree.node.create".into(), target_id: Some("summary_1".into()), revision: Some(2), }; assert_eq!(result.write_kind, AiStructuredWriteKind::SummaryNode); assert_eq!(result.command_name, "tree.node.create"); } #[test] fn mindmap_projection_and_command_contract_cover_kernel_truth() { let projection = MindmapProjection { schema: "mnote.mindmap_projection.v1".into(), map_id: "mind_1".into(), root_node: "root".into(), nodes: vec![], edges: vec![], layout_hints: serde_json::json!({"layout": "right"}), revision: 1, owner: MindmapProjectionOwner::RustKernel, }; let command = MindmapCommand::RenameNode { map_id: "mind_1".into(), node_id: "root".into(), title: "新标题".into(), }; assert_eq!(projection.owner, MindmapProjectionOwner::RustKernel); assert!(matches!(command, MindmapCommand::RenameNode { .. })); } #[test] fn mindmap_kernel_and_adapter_projection_serialize_as_camel_case() { let kernel_projection = MindmapKernelProjection { schema: "mnote.mindmap.kernel_projection.v1".into(), document_id: "doc_1".into(), mindmap_id: "mind_1".into(), root_node_id: "root".into(), revision: 7, nodes: vec![ MindmapKernelNode { node_id: "root".into(), parent_id: None, text: "KMIND".into(), order: 0, collapsed: false, style: Default::default(), refs: Default::default(), }, MindmapKernelNode { node_id: "child_1".into(), parent_id: Some("root".into()), text: "二级节点".into(), order: 1, collapsed: false, style: Default::default(), refs: Default::default(), }, ], edges: vec![MindmapKernelEdge { edge_id: "root->child_1".into(), from_node_id: "root".into(), to_node_id: "child_1".into(), edge_kind: "tree".into(), }], summaries: vec![MindmapSummary { summary_id: "summary_1".into(), parent_node_id: "child_1".into(), node_ids: vec!["child_1".into()], text: "概要".into(), style: Default::default(), }], associative_lines: vec![MindmapAssociativeLine { line_id: "line_1".into(), from_node_id: "root".into(), to_node_id: "child_1".into(), text: Some("关联".into()), style: Default::default(), }], layout: serde_json::json!({ "type": "logicalStructure" }), theme: serde_json::json!({ "template": "classic" }), view: serde_json::json!({ "zoom": 1.0 }), capabilities: MindmapKernelCapabilities { can_edit: true, can_edit_text: true, can_insert_child: true, can_insert_sibling_after: true, can_delete_node: true, can_move_node: true, can_patch_view: true, can_set_layout: true, can_set_theme: true, }, source: MindmapProjectionSource::Kernel, }; let adapter_projection = MindmapAdapterProjection { schema: "mnote.mindmap.simple_mind_map_scene.v1".into(), runtime: "simple-mind-map".into(), root: serde_json::json!({ "data": { "uid": "root", "text": "KMIND" }, "children": [] }), layout: serde_json::json!("logicalStructure"), theme: serde_json::json!("classic"), theme_config: serde_json::json!({}), view: serde_json::json!({ "scale": 1.0 }), config: serde_json::json!({}), compat_payload: serde_json::json!({}), kernel_revision: 7, }; let kernel_value = serde_json::to_value(kernel_projection).expect("kernel projection should serialize"); let adapter_value = serde_json::to_value(adapter_projection).expect("adapter projection should serialize"); assert_eq!(kernel_value["documentId"], serde_json::json!("doc_1")); assert_eq!(kernel_value["mindmapId"], serde_json::json!("mind_1")); assert_eq!(kernel_value["rootNodeId"], serde_json::json!("root")); assert_eq!( kernel_value["nodes"][1]["parentId"], serde_json::json!("root") ); assert_eq!( kernel_value["associativeLines"][0]["fromNodeId"], serde_json::json!("root") ); assert_eq!( kernel_value["capabilities"]["canEditText"], serde_json::json!(true) ); assert_eq!(kernel_value["source"], serde_json::json!("kernel")); assert_eq!( adapter_value["runtime"], serde_json::json!("simple-mind-map") ); assert_eq!(adapter_value["themeConfig"], serde_json::json!({})); assert_eq!(adapter_value["compatPayload"], serde_json::json!({})); assert_eq!(adapter_value["kernelRevision"], serde_json::json!(7)); } #[test] fn mindmap_kernel_command_contract_covers_minimum_write_surface() { let commands = vec![ MindmapKernelCommand::UpdateText { mindmap_id: "mind_1".into(), node_id: "root".into(), text: "新标题".into(), }, MindmapKernelCommand::InsertChild { mindmap_id: "mind_1".into(), parent_node_id: "root".into(), node: MindmapNodeInput { uid: Some("child_1".into()), text: "子节点".into(), hyperlink: None, note: None, refs: None, }, }, MindmapKernelCommand::InsertSiblingAfter { mindmap_id: "mind_1".into(), target_node_id: "child_1".into(), node: MindmapNodeInput { uid: Some("child_2".into()), text: "同级节点".into(), hyperlink: None, note: None, refs: None, }, }, MindmapKernelCommand::DeleteNode { mindmap_id: "mind_1".into(), node_id: "child_2".into(), }, MindmapKernelCommand::MoveNode { mindmap_id: "mind_1".into(), node_id: "child_1".into(), new_parent_node_id: "root".into(), order: Some(1), }, MindmapKernelCommand::PatchView { mindmap_id: "mind_1".into(), patch: serde_json::json!({ "scale": 1.2 }), }, MindmapKernelCommand::SetLayout { mindmap_id: "mind_1".into(), layout: serde_json::json!({ "type": "mindMap" }), }, MindmapKernelCommand::SetTheme { mindmap_id: "mind_1".into(), theme: serde_json::json!({ "template": "classic" }), theme_config: serde_json::json!({ "lineColor": "#f59e0b" }), }, ]; let encoded = serde_json::to_value(&commands).expect("commands should serialize"); assert_eq!(encoded[0]["type"], serde_json::json!("updateText")); assert_eq!(encoded[1]["type"], serde_json::json!("insertChild")); assert_eq!(encoded[2]["type"], serde_json::json!("insertSiblingAfter")); assert_eq!(encoded[3]["type"], serde_json::json!("deleteNode")); assert_eq!(encoded[4]["type"], serde_json::json!("moveNode")); assert_eq!(encoded[5]["type"], serde_json::json!("patchView")); assert_eq!(encoded[6]["type"], serde_json::json!("setLayout")); assert_eq!(encoded[7]["type"], serde_json::json!("setTheme")); assert_eq!(encoded[0]["mindmapId"], serde_json::json!("mind_1")); assert_eq!(encoded[1]["parentNodeId"], serde_json::json!("root")); assert_eq!(encoded[4]["newParentNodeId"], serde_json::json!("root")); assert_eq!( encoded[7]["themeConfig"]["lineColor"], serde_json::json!("#f59e0b") ); } }