use adapter_onlyoffice::{ prepare_callback, prepare_forcesave, prepare_proxy_request, resolve_session, sign_config, OnlyOfficeCallbackPreparationInput, OnlyOfficeForcesavePreparationInput, OnlyOfficeProxyPreparationInput, OnlyOfficeSessionResolveInput, }; use core_domain::Timestamp; use core_protocol::editor::{EditorBlockDocumentTiptapBridge, TiptapNode}; use core_protocol::{ default_tool_registry, invocation_kind_label, tool_effect_label, ActorPayload, BlockProps, CommandEnvelope, ContentNode, ContentNodePayload, DocumentContentResult, DocumentReadEvidenceItem, DocumentReadEvidenceKind, DocumentReadNode, DocumentReadNodeMeta, DocumentReadNodeType, DocumentReadOutlineEntry, DocumentReadPageSubtree, DocumentReadStats, DocumentReadSubtree, EditorBlock, EditorBlockDocument, EditorBlockType, EditorCommand, EditorInsertBlockAfter, EditorReplaceBlock, EmbedBlock, GetBlock, GetBridgeCommand, GetBridgeRequest, GetBridgeTrace, GetMindmap, InvocationKind, KernelAttachEdge, KernelAuditStamp, KernelContentPayload, KernelCreateNode, KernelDetachEdge, KernelEdge, KernelEdgeListResult, KernelEdgeType, KernelGetNode, KernelGetSubtree, KernelGraphDirection, KernelGraphTraversalResult, KernelGraphVisit, KernelListChildren, KernelListEdges, KernelMoveSubtree, KernelNode, KernelNodeMetadata, KernelNodeType, KernelProjectionAssetKind, KernelProjectionCapability, KernelProjectionFilter, KernelProjectionItem, KernelProjectionKind, KernelProjectionRequest, KernelProjectionResourceKind, KernelProjectionResourceMeta, KernelProjectionResult, KernelProjectionRowKind, KernelRefsPayload, KernelSubtreeRef, KernelSubtreeResult, KernelTraverseGraph, KernelUpdateNode, ListBridgeWorkspaceOverview, MindmapCommand, MindmapNodeData, MindmapNodeInput, MindmapNodeRef, MindmapOp, MindmapProjection, MindmapProjectionEdge, MindmapProjectionNode, MindmapProjectionOwner, MindmapTreeNode, MoveBlock, PageAggregateProjection, PageAggregateSource, PageBody, PageHead, PageIdentity, PageLayout, PageOptions, PagePermissions, PageStats, PageTree, PatchBlock, PutMindmap, QueryEnvelope, SearchDocuments, SearchRecent, SourcePayload, TargetRef, ToolExecutionMode, ToolInvocation, UpdatePageOptions, UpdatePageStats, WorkspaceSource, }; use event_log::DomainEventRecord; use index_fts::{ can_rebuild_from_events, evaluate_search_documents, rebuild_from_events, IndexCursor, MinimalWorkspaceProjector, SearchDocumentsDataset, SearchDocumentsEvaluation, SearchDocumentsRequest, }; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::collections::{BTreeMap, HashMap, VecDeque}; use std::env; use std::sync::atomic::{AtomicU64, Ordering}; use storage_convex_bridge::{ build_query_request, build_write_request, BridgeContext, BridgeError, BridgeErrorKind, }; static TOOL_BLOCK_COUNTER: AtomicU64 = AtomicU64::new(1); static MINDMAP_UID_COUNTER: AtomicU64 = AtomicU64::new(1); #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase", tag = "kind")] pub enum RuntimeInput { Query { context: RuntimeBridgeContextWire, query: RuntimeQueryEnvelopeWire, #[serde(default)] data: Option, }, Command { context: RuntimeBridgeContextWire, command: RuntimeCommandEnvelopeWire, }, CommandArtifact { context: RuntimeBridgeContextWire, command: RuntimeCommandEnvelopeWire, plan: RuntimeCommandExecutionPlan, result: Value, now: String, }, Tool { context: RuntimeBridgeContextWire, tool: RuntimeToolInvocationWire, #[serde(default)] data: Option, }, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RuntimeBridgeContextWire { pub deployment_id: Option, pub project_id: Option, pub workspace_id: Option, pub request_id: String, pub trace_id: String, pub actor: RuntimeActorWire, pub source: RuntimeSourceWire, pub tenant_id: Option, pub auth_token: Option, pub idempotency_key: Option, pub validate_only: bool, pub dry_run: bool, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RuntimeActorWire { pub actor_type: String, pub actor_id: String, pub session_id: Option, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RuntimeSourceWire { pub channel: String, pub client: String, #[serde(default)] pub source_kind: Option, #[serde(default)] pub root_uri: Option, #[serde(default)] pub workspace_id: Option, #[serde(default)] pub capabilities: Vec, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RuntimeTargetWire { pub workspace_id: Option, pub page_id: Option, pub block_id: Option, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RuntimeQueryEnvelopeWire { pub name: String, pub payload: Value, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RuntimeCommandEnvelopeWire { pub name: String, pub command_id: String, pub idempotency_key: Option, pub actor: RuntimeActorWire, pub source: RuntimeSourceWire, pub target: Option, pub payload: Value, #[serde(default)] pub preflight_data: Option, pub reason: Option, pub refs: Vec, pub dry_run: bool, pub validate_only: bool, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct WorkspaceSourceWatch { pub source_kind: String, pub active: bool, } pub trait WorkspaceSourceAdapter { fn source(&self) -> &WorkspaceSource; fn load_snapshot( &self, projection: KernelProjectionKind, ) -> Result; fn watch(&self) -> Result; fn preflight_command(&self, command: &RuntimeCommandEnvelopeWire) -> Result; fn execute_command(&self, command: &RuntimeCommandEnvelopeWire) -> Result; fn resolve_page_aggregate(&self, page_id: &str) -> Result; } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RuntimeToolInvocationWire { pub tool: String, pub kind: String, pub mode: Option, pub args_json: Value, pub target: Option, pub reason: Option, #[serde(default)] pub refs: Vec, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] struct MindmapEmptyTrashToolPayload { workspace_id: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct MindmapOutlineItemPayload { title: String, level: u32, page: u32, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] struct MindmapOutlineToolPayload { document_id: String, mindmap_id: String, root_title: String, page_link_pattern: String, outline: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct DocsSearchDatasetPayload { workspace_id: String, documents: Vec, mindmaps: Vec, tables: Vec, table_rows: Vec, assets: Vec, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] struct DocsSearchToolPayload { query: String, workspace_id: Option, limit: Option, include_deleted: Option, page_id: Option, title_only: Option, exact: Option, include_ocr: Option, time_range: Option, time_field: Option, custom_range_from: Option, custom_range_to: Option, #[serde(default)] datasets: Vec, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] struct DocsReadToolPayload { document_id: String, max_chars: Option, include_content: Option, title: Option, workspace_id: Option, parent_id: Option, updated_at: Option, raw_text_length: Option, raw_text: Option, content: Option, } #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RuntimeSuccess { pub ok: bool, pub plan: RuntimeExecutionPlan, } #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RuntimeArtifactSuccess { pub ok: bool, pub artifacts: Option, } #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RuntimeFailure { pub ok: bool, pub error: RuntimeErrorPayload, } #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "snake_case")] pub struct RuntimeErrorPayload { pub kind: String, pub message: String, } #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "snake_case", tag = "kind")] pub enum RuntimeExecutionPlan { Query(RuntimeQueryExecutionPlan), Command(RuntimeCommandExecutionPlan), Tool(RuntimeToolExecutionPlan), } #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RuntimeQueryResult { pub result: Value, } #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RuntimeQueryExecutionPlan { pub query_name: String, pub function_name: String, pub workspace_id: Option, pub request_id: String, pub trace_id: String, pub actor_id: String, pub payload_json: String, pub args_json: Value, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RuntimeCommandExecutionPlan { pub command_name: String, pub command_id: String, pub function_name: String, pub workspace_id: Option, pub request_id: String, pub trace_id: String, pub actor_id: String, pub idempotency_key: Option, pub source: Value, pub payload_json: String, pub args_json: Value, } #[derive(Debug, Clone, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RuntimeCommandArtifactPlan { pub command_log: RuntimeCommandLogArtifactPlan, pub domain_event: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub domain_events: Vec, } #[derive(Debug, Clone, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RuntimeCommandLogArtifactPlan { pub workspace_id: String, pub id: String, pub request_id: String, pub trace_id: String, pub command_id: String, pub command_name: String, pub actor_id: String, pub actor_type: String, pub source_channel: String, pub source_client: String, pub status: String, pub target_page_id: Option, pub target_block_id: Option, pub payload: Value, pub payload_summary: String, pub refs: Vec, pub idempotency_key: Option, pub error: Option, pub created_at: String, pub finished_at: Option, } #[derive(Debug, Clone, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RuntimeDomainEventArtifactPlan { pub workspace_id: String, pub id: String, pub request_id: String, pub trace_id: String, pub command_id: String, pub command_log_id: String, pub event_type: String, pub aggregate_type: String, pub aggregate_id: String, pub event_version: i64, pub status: String, pub actor_type: String, pub payload: Value, pub created_at: String, } #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RuntimeToolExecutionPlan { pub tool_name: String, pub invocation_kind: String, pub execution_mode: String, pub effect: String, pub toolset_id: String, pub requires_confirmation: bool, pub request_id: String, pub trace_id: String, pub actor_id: String, pub validate_only: bool, pub dry_run: bool, pub payload_json: String, pub args_json: Value, pub target: Option, pub steps: Vec, } #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RuntimeToolPlanStep { pub kind: String, pub name: String, pub function_name: Option, pub description: String, pub args_json: Value, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct BlockGetQueryPayload { block_id: String, workspace_id: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct BlockPatchCommandPayload { document_id: String, workspace_id: Option, block_id: String, next_block: Value, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct BlockMoveCommandPayload { source_document_id: String, block_id: String, target_document_id: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct BlockEmbedCommandPayload { source_document_id: String, block_id: String, target_document_id: String, target_block_id: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentContentQueryPayload { document_id: String, workspace_id: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct PageAggregateQueryPayload { document_id: String, workspace_id: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct MindmapGetQueryPayload { document_id: String, mindmap_id: String, workspace_id: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct SidebarDatasetQueryPayload { workspace_id: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct KernelGetNodeQueryPayload { node_id: String, workspace_id: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct KernelGetSubtreeQueryPayload { root_node_id: String, workspace_id: Option, depth: Option, include_edges: Option, node_types: Option>, edge_types: Option>, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct KernelListChildrenQueryPayload { parent_node_id: String, workspace_id: Option, node_types: Option>, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct KernelListEdgesQueryPayload { node_id: String, workspace_id: Option, edge_types: Option>, direction: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct KernelTraverseGraphQueryPayload { start_node_id: String, workspace_id: Option, edge_types: Option>, max_depth: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct KernelProjectViewQueryPayload { projection: KernelProjectionKind, workspace_id: Option, root_node_id: Option, depth: Option, include_content: Option, include_edges: Option, node_types: Option>, edge_types: Option>, query: Option, max_results: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct SearchDocumentsQueryPayload { query: String, workspace_id: String, page_id: Option, limit: Option, cursor: Option, title_only: Option, exact: Option, include_ocr: Option, time_range: Option, time_field: Option, custom_range_from: Option, custom_range_to: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct SearchRecentQueryPayload { workspace_id: String, limit: Option, cursor: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct BridgeRequestQueryPayload { workspace_id: String, request_id: String, command_id: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct BridgeTraceQueryPayload { workspace_id: String, trace_id: String, command_id: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct BridgeCommandQueryPayload { workspace_id: String, command_id: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct BridgeWorkspaceOverviewQueryPayload { workspace_id: String, limit: Option, cursor: Option, command_status: Option, event_status: Option, target_page_id: Option, target_block_id: Option, aggregate_type: Option, aggregate_id: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentTitleCommandPayload { document_id: String, title: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentSaveCommandPayload { document_id: String, workspace_id: Option, revision: Option, #[serde(default)] editor_document: Option, #[serde(default)] content: Value, #[serde(default)] tiptap_document: Option, conflict_detection_key: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentOptionsCommandPayload { document_id: String, options: DocumentOptionsPatchPayload, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentStatsCommandPayload { document_id: String, workspace_id: Option, stats: DocumentStatsPatchPayload, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentStatsPatchPayload { word_count: i64, character_count: i64, block_count: i64, todo_total: i64, todo_done: i64, } #[derive(Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] struct DocumentOptionsPatchPayload { wide_layout: Option, small_text: Option, show_heading_numbers: Option, show_toc: Option, show_structure: Option, protect_editing: Option, show_word_count: Option, collapse_backlinks: Option, page_font: Option, layout_density: Option, hide_child_pages: Option, show_block_ref_count: Option, embed_default_block_id: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct MediaAssetReplaceStorageCommandPayload { asset_id: String, document_id: String, workspace_id: Option, storage_id: String, user_id: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentEmbedCommandPayload { document_id: String, workspace_id: Option, revision: Option, #[serde(default)] content: Option, conflict_detection_key: Option, source_document_id: String, target_document_id: String, anchor_block_id: Option, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct KernelCreateNodeCommandPayload { node: KernelNode, position: Option, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct KernelUpdateNodeCommandPayload { node_id: String, metadata: Option, content: Option, refs: Option, audit: Option, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct KernelMoveSubtreeCommandPayload { subtree: KernelSubtreeRef, new_parent_node_id: Option, sort_order: Option, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct KernelAttachEdgeCommandPayload { edge: KernelEdge, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct KernelDetachEdgeCommandPayload { edge_id: Option, from_node_id: Option, to_node_id: Option, edge_type: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct MindmapPutCommandPayload { document_id: String, mindmap_id: String, data: Value, create_only: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct MindmapCommandApplyPayload { document_id: String, mindmap_id: String, #[serde(default)] workspace_id: Option, #[serde(default)] commands: Vec, #[serde(default)] projection_revision: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct MindmapDeleteCommandPayload { document_id: String, mindmap_id: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct MindmapRestoreCommandPayload { document_id: String, mindmap_id: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct MindmapPurgeCommandPayload { document_id: String, mindmap_id: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct MindmapEmptyTrashCommandPayload { workspace_id: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentCreateCommandPayload { document_id: String, workspace_id: String, parent_id: Option, title: String, access_scope: String, content: Value, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentMoveCommandPayload { document_id: String, parent_id: Option, sort_order: i64, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentMovePreflightDocument { id: String, workspace_id: Option, } #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] struct DocumentMoveSnapshotDocument { id: String, #[serde(default, alias = "workspace_id")] workspace_id: Option, #[serde(default, alias = "parent_id")] parent_id: Option, #[serde(default, alias = "sort_order")] sort_order: Option, #[serde(default, alias = "created_at")] created_at: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] struct DocumentMoveOrderPatch { document_id: String, parent_id: Option, sort_order: i64, moved: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] struct DocumentMoveOrderPlan { document_id: String, from_parent_id: Option, to_parent_id: Option, requested_sort_order: i64, normalized_sort_order: i64, patches: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentMovePreflightPayload { source_document: DocumentMovePreflightDocument, target_parent_document: Option, #[serde(default)] target_ancestor_ids: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentMoveSnapshotSidebarPayload { #[serde(default)] documents: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentMoveSnapshotPayload { #[serde(default)] documents: Vec, #[serde(default, alias = "sidebarSnapshot")] sidebar_snapshot: Option, } fn derive_document_move_preflight_from_snapshot( payload: &DocumentMoveCommandPayload, snapshot: &DocumentMoveSnapshotPayload, ) -> Result { let documents = if !snapshot.documents.is_empty() { snapshot.documents.clone() } else if let Some(sidebar_snapshot) = snapshot.sidebar_snapshot.as_ref() { sidebar_snapshot.documents.clone() } else { return Err(BridgeError::validation( "move preflightData 缺少 documents 快照", )); }; let document_by_id: HashMap = documents .into_iter() .map(|document| (document.id.clone(), document)) .collect(); let source_document = document_by_id .get(payload.document_id.as_str()) .ok_or_else(|| BridgeError::validation("源页面不存在或无权限"))?; let target_parent_document = payload .parent_id .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .map(|parent_id| { document_by_id .get(parent_id) .cloned() .ok_or_else(|| BridgeError::validation("目标父页面不存在或无权限")) }) .transpose()?; let mut target_ancestor_ids = Vec::new(); let mut cursor = target_parent_document .as_ref() .and_then(|document| document.parent_id.as_deref()) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned); let mut depth = 0; while let Some(parent_id) = cursor { if depth >= 256 { break; } target_ancestor_ids.push(parent_id.clone()); cursor = document_by_id .get(parent_id.as_str()) .and_then(|document| document.parent_id.as_deref()) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned); depth += 1; } Ok(DocumentMovePreflightPayload { source_document: DocumentMovePreflightDocument { id: source_document.id.clone(), workspace_id: source_document.workspace_id.clone(), }, target_parent_document: target_parent_document.map(|document| { DocumentMovePreflightDocument { id: document.id, workspace_id: document.workspace_id, } }), target_ancestor_ids, }) } fn normalize_parent_id(value: &Option) -> Option { value .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) } fn document_move_sort_key(document: &DocumentMoveSnapshotDocument) -> (i64, String, String) { ( normalize_document_move_sort_order(document.sort_order).unwrap_or(i64::MAX), document.created_at.clone().unwrap_or_default(), document.id.clone(), ) } fn normalize_document_move_sort_order(value: Option) -> Option { value .filter(|number| number.is_finite()) .map(|number| number.floor() as i64) } fn clamp_document_move_index(sort_order: i64, max: usize) -> usize { if sort_order < 0 { return 0; } (sort_order as usize).min(max) } fn append_document_move_order_patches( patches: &mut Vec, ordered: &[DocumentMoveSnapshotDocument], parent_id: Option, moved_document_id: &str, ) { for (index, document) in ordered.iter().enumerate() { let sort_order = index as i64; let moved = document.id == moved_document_id; if normalize_parent_id(&document.parent_id) == parent_id && normalize_document_move_sort_order(document.sort_order) == Some(sort_order) && !moved { continue; } patches.push(DocumentMoveOrderPatch { document_id: document.id.clone(), parent_id: parent_id.clone(), sort_order, moved, }); } } fn build_document_move_order_plan_from_snapshot( payload: &DocumentMoveCommandPayload, snapshot: &DocumentMoveSnapshotPayload, ) -> Result { let documents = if !snapshot.documents.is_empty() { snapshot.documents.clone() } else if let Some(sidebar_snapshot) = snapshot.sidebar_snapshot.as_ref() { sidebar_snapshot.documents.clone() } else { return Err(BridgeError::validation( "move preflightData 缺少 documents 快照", )); }; let document_by_id: HashMap = documents .iter() .cloned() .map(|document| (document.id.clone(), document)) .collect(); let source_document = document_by_id .get(payload.document_id.as_str()) .cloned() .ok_or_else(|| BridgeError::validation("源页面不存在或无权限"))?; let from_parent_id = normalize_parent_id(&source_document.parent_id); let to_parent_id = normalize_parent_id(&payload.parent_id); let mut sibling_by_parent = BTreeMap::, Vec>::new(); for document in documents { sibling_by_parent .entry(normalize_parent_id(&document.parent_id)) .or_default() .push(document); } for siblings in sibling_by_parent.values_mut() { siblings.sort_by_key(document_move_sort_key); } let mut patches = Vec::new(); let normalized_sort_order; if from_parent_id == to_parent_id { let mut siblings = sibling_by_parent .remove(&to_parent_id) .unwrap_or_default() .into_iter() .filter(|document| document.id != payload.document_id) .collect::>(); let position = clamp_document_move_index(payload.sort_order, siblings.len()); normalized_sort_order = position as i64; siblings.insert(position, source_document); append_document_move_order_patches( &mut patches, &siblings, to_parent_id.clone(), payload.document_id.as_str(), ); } else { let old_siblings = sibling_by_parent .remove(&from_parent_id) .unwrap_or_default() .into_iter() .filter(|document| document.id != payload.document_id) .collect::>(); append_document_move_order_patches( &mut patches, &old_siblings, from_parent_id.clone(), payload.document_id.as_str(), ); let mut new_siblings = sibling_by_parent .remove(&to_parent_id) .unwrap_or_default() .into_iter() .filter(|document| document.id != payload.document_id) .collect::>(); let position = clamp_document_move_index(payload.sort_order, new_siblings.len()); normalized_sort_order = position as i64; new_siblings.insert(position, source_document); append_document_move_order_patches( &mut patches, &new_siblings, to_parent_id.clone(), payload.document_id.as_str(), ); } Ok(DocumentMoveOrderPlan { document_id: payload.document_id.clone(), from_parent_id, to_parent_id, requested_sort_order: payload.sort_order, normalized_sort_order, patches, }) } fn resolve_document_move_order_plan( payload: &DocumentMoveCommandPayload, preflight_data: Option<&Value>, ) -> Result, BridgeError> { let Some(raw_preflight) = preflight_data else { return Ok(None); }; let snapshot = match serde_json::from_value::(raw_preflight.clone()) { Ok(snapshot) if !snapshot.documents.is_empty() || snapshot.sidebar_snapshot.is_some() => { snapshot } _ => return Ok(None), }; build_document_move_order_plan_from_snapshot(payload, &snapshot).map(Some) } fn document_move_write_operation( workspace_id: Option<&str>, plan: Option<&DocumentMoveOrderPlan>, ) -> Value { let Some(plan) = plan else { return Value::Null; }; json!({ "family": "tree", "schema": "mnote.tree.write_operation", "schemaVersion": 1, "operation": "tree.subtree.move.write", "workspaceId": workspace_id, "documentId": plan.document_id, "fromParentId": plan.from_parent_id, "toParentId": plan.to_parent_id, "requestedSortOrder": plan.requested_sort_order, "normalizedSortOrder": plan.normalized_sort_order, "patches": plan.patches, }) } fn resolve_document_move_preflight( payload: &DocumentMoveCommandPayload, preflight_data: Option<&Value>, ) -> Result, BridgeError> { let Some(raw_preflight) = preflight_data else { return Ok(None); }; if let Ok(preflight) = serde_json::from_value::(raw_preflight.clone()) { return Ok(Some(preflight)); } let snapshot = serde_json::from_value::(raw_preflight.clone()) .map_err(|error| BridgeError::validation(format!("move preflightData 非法: {error}")))?; derive_document_move_preflight_from_snapshot(payload, &snapshot).map(Some) } fn validate_document_move_legality( payload: &DocumentMoveCommandPayload, preflight_data: Option<&Value>, ) -> Result<(), BridgeError> { let Some(parent_id) = payload .parent_id .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) else { return Ok(()); }; if parent_id == payload.document_id { return Err(BridgeError::validation("不能把页面移动到自身下面")); } let Some(preflight) = resolve_document_move_preflight(payload, preflight_data)? else { return Ok(()); }; if preflight.source_document.id.trim() == parent_id { return Err(BridgeError::validation("不能把页面移动到自身下面")); } if preflight .target_ancestor_ids .iter() .any(|ancestor_id| ancestor_id.trim() == payload.document_id) { return Err(BridgeError::validation("不能把页面移动到自己的后代下面")); } if let Some(target_parent_document) = preflight.target_parent_document.as_ref() { let source_workspace_id = preflight .source_document .workspace_id .as_deref() .map(str::trim) .filter(|value| !value.is_empty()); let target_workspace_id = target_parent_document .workspace_id .as_deref() .map(str::trim) .filter(|value| !value.is_empty()); if source_workspace_id.is_some() && target_workspace_id.is_some() && source_workspace_id != target_workspace_id { return Err(BridgeError::validation("暂不支持跨工作空间移动页面")); } } Ok(()) } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentDeleteCommandPayload { document_id: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentRestoreCommandPayload { document_id: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentDuplicateCommandPayload { source_document_id: String, new_document_id: String, title: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentTemplateCommandPayload { document_id: String, is_template: bool, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentEmptyTrashCommandPayload { workspace_id: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentPurgeCommandPayload { document_id: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentCopyTreeItemPayload { document_id: String, recursive: bool, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DocumentCopyTreeCommandPayload { items: Vec, target_parent_id: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct ResourceTransferCommandPayload { asset_ids: Vec, target_document_id: String, target_sub_path: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct ResourceUploadCommandPayload { asset_id: String, workspace_id: String, target_document_id: String, target_sub_path: Option, file_name: Option, file_size: Option, mime_type: Option, asset_type: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct FileTreeDropPreflightCommandPayload { copy: bool, target_document_id: Option, target_row_id: Option, focused_row_id: Option, active_document_id: Option, #[serde(default)] row_ids: Vec, #[serde(default)] rows: Vec, #[serde(default)] document_parents: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct FileTreeDeletePreflightCommandPayload { #[serde(default)] row_ids: Vec, #[serde(default)] rows: Vec, #[serde(default)] document_parents: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct FileTreePastePreflightCommandPayload { target_document_id: Option, focused_row_id: Option, active_document_id: Option, #[serde(default)] row_ids: Vec, #[serde(default)] rows: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct FileTreeUploadTargetPreflightCommandPayload { workspace_id: Option, target_document_id: Option, target_row_id: Option, focused_row_id: Option, active_document_id: Option, #[serde(default)] rows: Vec, #[serde(default)] document_workspaces: Vec, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] struct FileTreeDropPreflightRow { row_id: String, row_kind: String, document_id: Option, asset_id: Option, asset_document_id: Option, asset_type: Option, storage_path: Option, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] struct FileTreeDropPreflightDocumentParent { document_id: String, parent_id: Option, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] struct FileTreeUploadTargetDocumentWorkspace { document_id: String, workspace_id: Option, } #[derive(Debug, Clone, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] struct FileTreeDropPlan { copy: bool, target_document_id: String, target_mindmap_id: Option, target_sub_path: Option, row_ids: Vec, doc_ids: Vec, top_level_doc_ids: Vec, copyable_asset_ids: Vec, source_asset_document_ids: Vec, #[serde(skip_serializing_if = "Option::is_none")] document_transfer_plan: Option, #[serde(skip_serializing_if = "Option::is_none")] resource_transfer_plan: Option, } #[derive(Debug, Clone, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] struct FileTreeDeletePlan { row_ids: Vec, doc_ids: Vec, asset_ids: Vec, asset_document_ids: Vec, } #[derive(Debug, Clone, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] struct FileTreePastePlan { target_document_id: String, target_mindmap_id: Option, target_sub_path: Option, row_ids: Vec, doc_items: Vec, copyable_asset_ids: Vec, resource_transfer_plan: Option, } #[derive(Debug, Clone, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] struct FileTreeUploadTargetPlan { workspace_id: String, target_document_id: String, target_mindmap_id: Option, target_sub_path: Option, } #[derive(Debug, Clone, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] struct FileTreeDropDocumentTransferPlan { action: String, target_parent_id: String, document_ids: Vec, top_level_document_ids: Vec, copy_items: Vec, } #[derive(Debug, Clone, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] struct DocumentCopyTreeItemPlan { document_id: String, recursive: bool, } #[derive(Debug, Clone, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] struct ResourceTransferPlan { action: String, asset_ids: Vec, target_document_id: String, target_sub_path: Option, } #[derive(Debug, Clone, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] struct ResourceUploadPlan { action: String, asset_id: String, workspace_id: String, target_document_id: String, target_sub_path: Option, file_name: Option, file_size: Option, mime_type: Option, asset_type: String, } fn normalize_resource_asset_ids(raw_asset_ids: &[String]) -> Vec { let mut asset_ids = Vec::new(); for raw in raw_asset_ids { let asset_id = raw.trim(); if asset_id.is_empty() || asset_ids.iter().any(|existing| existing == asset_id) { continue; } asset_ids.push(asset_id.to_string()); } asset_ids } fn sanitize_resource_target_sub_path(raw: Option<&str>) -> Option { let segments = raw? .replace('\\', "/") .split('/') .map(str::trim) .filter(|segment| !segment.is_empty() && *segment != "." && *segment != "..") .map(|segment| segment.replace(['\\', '/'], "_")) .take(8) .collect::>(); if segments.is_empty() { None } else { Some(segments.join("/")) } } fn build_resource_transfer_plan( action: &str, payload: &ResourceTransferCommandPayload, ) -> Result { let asset_ids = normalize_resource_asset_ids(&payload.asset_ids); if asset_ids.is_empty() { return Err(BridgeError::validation("resource command 缺少 assetIds")); } let target_document_id = payload.target_document_id.trim(); if target_document_id.is_empty() { return Err(BridgeError::validation( "resource command 缺少 targetDocumentId", )); } Ok(ResourceTransferPlan { action: action.into(), asset_ids, target_document_id: target_document_id.into(), target_sub_path: sanitize_resource_target_sub_path(payload.target_sub_path.as_deref()), }) } fn normalize_optional_resource_string(raw: Option<&str>) -> Option { let value = raw?.trim(); if value.is_empty() { None } else { Some(value.to_string()) } } fn normalize_resource_asset_type(raw: Option<&str>) -> String { match raw.unwrap_or("file").trim() { "image" => "image".into(), "video" => "video".into(), "audio" => "audio".into(), "mindmap" => "mindmap".into(), "luckysheet" => "luckysheet".into(), _ => "file".into(), } } fn build_resource_upload_plan( payload: &ResourceUploadCommandPayload, ) -> Result { let asset_id = payload.asset_id.trim(); if asset_id.is_empty() { return Err(BridgeError::validation("resource upload 缺少 assetId")); } let workspace_id = payload.workspace_id.trim(); if workspace_id.is_empty() { return Err(BridgeError::validation("resource upload 缺少 workspaceId")); } let target_document_id = payload.target_document_id.trim(); if target_document_id.is_empty() { return Err(BridgeError::validation( "resource upload 缺少 targetDocumentId", )); } Ok(ResourceUploadPlan { action: "upload".into(), asset_id: asset_id.into(), workspace_id: workspace_id.into(), target_document_id: target_document_id.into(), target_sub_path: sanitize_resource_target_sub_path(payload.target_sub_path.as_deref()), file_name: normalize_optional_resource_string(payload.file_name.as_deref()) .map(|value| value.replace(['\\', '/'], "_")), file_size: payload.file_size.filter(|value| *value >= 0), mime_type: normalize_optional_resource_string(payload.mime_type.as_deref()), asset_type: normalize_resource_asset_type(payload.asset_type.as_deref()), }) } fn normalize_optional_filetree_string(value: &Option) -> Option { value .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) } fn filetree_drop_row_document_id(row: &FileTreeDropPreflightRow) -> Option { normalize_optional_filetree_string(&row.document_id) } fn extract_filetree_drop_mindmap_id_from_storage_path(raw: Option<&str>) -> Option { let normalized = raw?.replace('\\', "/"); let prefix = "mindmaps/"; if let Some(rest) = normalized.strip_prefix(prefix) { return rest .split('/') .next() .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned); } let marker = "/mindmaps/"; let idx = normalized.find(marker)?; normalized[idx + marker.len()..] .split('/') .next() .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) } fn resolve_filetree_drop_mindmap_target_id( row: Option<&FileTreeDropPreflightRow>, ) -> Option { let row = row?; let row_kind = row.row_kind.trim(); let asset_type = row.asset_type.as_deref().map(str::trim).unwrap_or_default(); if row_kind == "asset-folder" && asset_type == "mindmap" { return normalize_optional_filetree_string(&row.asset_id); } if row_kind == "asset" { return extract_filetree_drop_mindmap_id_from_storage_path(row.storage_path.as_deref()); } None } fn ordered_unique_filetree_row_ids(row_ids: &[String]) -> Vec { let mut result = Vec::new(); for raw in row_ids { let row_id = raw.trim(); if row_id.is_empty() || result.iter().any(|existing| existing == row_id) { continue; } result.push(row_id.to_string()); } result } fn collect_filetree_doc_ancestors( document_id: &str, parent_by_document_id: &HashMap>, ) -> Vec { let mut ancestors = Vec::new(); let mut cursor = parent_by_document_id .get(document_id) .and_then(|parent_id| parent_id.clone()); let mut depth = 0; while let Some(parent_id) = cursor { if depth >= 256 || ancestors.iter().any(|existing| existing == &parent_id) { break; } ancestors.push(parent_id.clone()); cursor = parent_by_document_id .get(parent_id.as_str()) .and_then(|parent_id| parent_id.clone()); depth += 1; } ancestors } fn filter_top_level_filetree_doc_ids( doc_ids: &[String], parent_by_document_id: &HashMap>, ) -> Vec { let doc_set = doc_ids .iter() .cloned() .collect::>(); doc_ids .iter() .filter(|doc_id| { !collect_filetree_doc_ancestors(doc_id, parent_by_document_id) .iter() .any(|ancestor_id| doc_set.contains(ancestor_id)) }) .cloned() .collect() } fn is_filetree_drop_real_asset(row: &FileTreeDropPreflightRow) -> bool { normalize_optional_filetree_string(&row.asset_id).is_some() && normalize_optional_filetree_string(&row.storage_path).is_some() } fn validate_filetree_drop_doc_legality( plan: &FileTreeDropPlan, parent_by_document_id: &HashMap>, ) -> Result<(), BridgeError> { if plan.copy { return Ok(()); } for doc_id in &plan.top_level_doc_ids { if doc_id == &plan.target_document_id { return Err(BridgeError::validation("不能把页面移动到自身或后代下面")); } let target_ancestors = collect_filetree_doc_ancestors(plan.target_document_id.as_str(), parent_by_document_id); if target_ancestors .iter() .any(|ancestor_id| ancestor_id == doc_id) { return Err(BridgeError::validation("不能把页面移动到自身或后代下面")); } } Ok(()) } fn build_filetree_delete_plan( payload: &FileTreeDeletePreflightCommandPayload, ) -> Result { let row_by_id: HashMap = payload .rows .iter() .filter_map(|row| { let row_id = row.row_id.trim(); if row_id.is_empty() { None } else { Some((row_id.to_string(), row.clone())) } }) .collect(); let row_ids = ordered_unique_filetree_row_ids(&payload.row_ids) .into_iter() .filter(|row_id| row_by_id.contains_key(row_id.as_str())) .collect::>(); let selected_rows = row_ids .iter() .filter_map(|row_id| row_by_id.get(row_id.as_str())) .collect::>(); let mut doc_candidates = Vec::new(); let mut asset_candidates = Vec::new(); let mut asset_document_by_id = HashMap::::new(); for row in selected_rows { let row_kind = row.row_kind.trim(); if row_kind == "doc" || row_kind == "index" { if let Some(document_id) = filetree_drop_row_document_id(row) { doc_candidates.push(document_id); } continue; } if (row_kind == "asset" || row_kind == "asset-folder") && normalize_optional_filetree_string(&row.asset_id).is_some() { let asset_id = normalize_optional_filetree_string(&row.asset_id).unwrap_or_default(); if asset_id.is_empty() { continue; } asset_candidates.push(asset_id.clone()); if let Some(document_id) = normalize_optional_filetree_string(&row.asset_document_id) .or_else(|| filetree_drop_row_document_id(row)) { asset_document_by_id.insert(asset_id, document_id); } } } let parent_by_document_id: HashMap> = payload .document_parents .iter() .filter_map(|entry| { let document_id = entry.document_id.trim(); if document_id.is_empty() { return None; } Some(( document_id.to_string(), normalize_optional_filetree_string(&entry.parent_id), )) }) .collect(); let doc_ids = filter_top_level_filetree_doc_ids(&doc_candidates, &parent_by_document_id); let doc_id_set = doc_ids .iter() .cloned() .collect::>(); let mut asset_ids = Vec::new(); let mut asset_document_ids = Vec::new(); for asset_id in asset_candidates { if asset_ids.iter().any(|existing| existing == &asset_id) { continue; } let owner_document_id = asset_document_by_id.get(asset_id.as_str()).cloned(); if let Some(document_id) = owner_document_id.as_ref() { if doc_id_set.contains(document_id) || collect_filetree_doc_ancestors(document_id, &parent_by_document_id) .iter() .any(|ancestor_id| doc_id_set.contains(ancestor_id)) { continue; } } asset_ids.push(asset_id.clone()); if let Some(owner_document_id) = owner_document_id { if !asset_document_ids .iter() .any(|existing| existing == &owner_document_id) { asset_document_ids.push(owner_document_id); } } } if doc_ids.is_empty() && asset_ids.is_empty() { return Err(BridgeError::validation("没有可删除的对象")); } Ok(FileTreeDeletePlan { row_ids, doc_ids, asset_ids, asset_document_ids, }) } fn upsert_filetree_paste_doc_item( doc_items: &mut Vec, document_id: String, recursive: bool, ) { if let Some(existing) = doc_items .iter_mut() .find(|item| item.document_id == document_id) { existing.recursive = existing.recursive || recursive; return; } doc_items.push(DocumentCopyTreeItemPlan { document_id, recursive, }); } fn build_filetree_paste_plan( payload: &FileTreePastePreflightCommandPayload, ) -> Result { let row_by_id: HashMap = payload .rows .iter() .filter_map(|row| { let row_id = row.row_id.trim(); if row_id.is_empty() { None } else { Some((row_id.to_string(), row.clone())) } }) .collect(); let focused_row = normalize_optional_filetree_string(&payload.focused_row_id) .and_then(|row_id| row_by_id.get(row_id.as_str()).cloned()); let target_document_id = normalize_optional_filetree_string(&payload.target_document_id) .or_else(|| focused_row.as_ref().and_then(filetree_drop_row_document_id)) .or_else(|| normalize_optional_filetree_string(&payload.active_document_id)) .ok_or_else(|| BridgeError::validation("请选择一个目标页面后再粘贴"))?; let row_ids = ordered_unique_filetree_row_ids(&payload.row_ids) .into_iter() .filter(|row_id| row_by_id.contains_key(row_id.as_str())) .collect::>(); let selected_rows = row_ids .iter() .filter_map(|row_id| row_by_id.get(row_id.as_str())) .collect::>(); let mut doc_items = Vec::new(); let mut copyable_asset_ids = Vec::new(); for row in selected_rows { let row_kind = row.row_kind.trim(); if row_kind == "doc" { if let Some(document_id) = filetree_drop_row_document_id(row) { upsert_filetree_paste_doc_item(&mut doc_items, document_id, true); } continue; } if row_kind == "index" { if let Some(document_id) = filetree_drop_row_document_id(row) { upsert_filetree_paste_doc_item(&mut doc_items, document_id, false); } continue; } if row_kind == "asset" && is_filetree_drop_real_asset(row) { if let Some(asset_id) = normalize_optional_filetree_string(&row.asset_id) { if !copyable_asset_ids .iter() .any(|existing| existing == &asset_id) { copyable_asset_ids.push(asset_id); } } } } if doc_items.is_empty() && copyable_asset_ids.is_empty() { return Err(BridgeError::validation( "没有可粘贴的真实文件(mindmap.json 等虚拟附件暂不支持)", )); } let target_mindmap_id = resolve_filetree_drop_mindmap_target_id(focused_row.as_ref()); let target_sub_path = target_mindmap_id .as_deref() .map(|mindmap_id| format!("mindmaps/{mindmap_id}")); let resource_transfer_plan = if copyable_asset_ids.is_empty() { None } else { Some(ResourceTransferPlan { action: "copy".into(), asset_ids: copyable_asset_ids.clone(), target_document_id: target_document_id.clone(), target_sub_path: target_sub_path.clone(), }) }; Ok(FileTreePastePlan { target_document_id, target_mindmap_id, target_sub_path, row_ids, doc_items, copyable_asset_ids, resource_transfer_plan, }) } fn build_filetree_upload_target_plan( payload: &FileTreeUploadTargetPreflightCommandPayload, ) -> Result { let row_by_id: HashMap = payload .rows .iter() .filter_map(|row| { let row_id = row.row_id.trim(); if row_id.is_empty() { None } else { Some((row_id.to_string(), row.clone())) } }) .collect(); let target_row = normalize_optional_filetree_string(&payload.target_row_id) .and_then(|row_id| row_by_id.get(row_id.as_str()).cloned()); let focused_row = normalize_optional_filetree_string(&payload.focused_row_id) .and_then(|row_id| row_by_id.get(row_id.as_str()).cloned()); let target_document_id = normalize_optional_filetree_string(&payload.target_document_id) .or_else(|| target_row.as_ref().and_then(filetree_drop_row_document_id)) .or_else(|| focused_row.as_ref().and_then(filetree_drop_row_document_id)) .or_else(|| normalize_optional_filetree_string(&payload.active_document_id)) .ok_or_else(|| BridgeError::validation("请选择一个目标页面后再拖入文件"))?; let target_mindmap_id = resolve_filetree_drop_mindmap_target_id(target_row.as_ref().or(focused_row.as_ref())); let target_sub_path = target_mindmap_id .as_deref() .map(|mindmap_id| format!("mindmaps/{mindmap_id}")); let workspace_by_document_id: HashMap> = payload .document_workspaces .iter() .filter_map(|entry| { let document_id = entry.document_id.trim(); if document_id.is_empty() { return None; } Some(( document_id.to_string(), normalize_optional_filetree_string(&entry.workspace_id), )) }) .collect(); let workspace_id = workspace_by_document_id .get(target_document_id.as_str()) .and_then(|workspace_id| workspace_id.clone()) .or_else(|| normalize_optional_filetree_string(&payload.workspace_id)) .ok_or_else(|| BridgeError::validation("无法识别当前工作区,上传失败"))?; Ok(FileTreeUploadTargetPlan { workspace_id, target_document_id, target_mindmap_id, target_sub_path, }) } fn build_filetree_drop_plan( payload: &FileTreeDropPreflightCommandPayload, ) -> Result { let row_by_id: HashMap = payload .rows .iter() .filter_map(|row| { let row_id = row.row_id.trim(); if row_id.is_empty() { None } else { Some((row_id.to_string(), row.clone())) } }) .collect(); let target_row = normalize_optional_filetree_string(&payload.target_row_id) .and_then(|row_id| row_by_id.get(row_id.as_str()).cloned()); let target_document_id = normalize_optional_filetree_string(&payload.target_document_id) .or_else(|| target_row.as_ref().and_then(filetree_drop_row_document_id)) .or_else(|| { normalize_optional_filetree_string(&payload.focused_row_id).and_then(|row_id| { row_by_id .get(row_id.as_str()) .and_then(filetree_drop_row_document_id) }) }) .or_else(|| normalize_optional_filetree_string(&payload.active_document_id)) .ok_or_else(|| BridgeError::validation("无法识别拖拽目标页面"))?; let row_ids = ordered_unique_filetree_row_ids(&payload.row_ids) .into_iter() .filter(|row_id| row_by_id.contains_key(row_id.as_str())) .collect::>(); let selected_rows = row_ids .iter() .filter_map(|row_id| row_by_id.get(row_id.as_str())) .collect::>(); let mut doc_ids = Vec::new(); let mut copyable_asset_ids = Vec::new(); let mut source_asset_document_ids = Vec::new(); for row in selected_rows { let row_kind = row.row_kind.trim(); if row_kind == "doc" { if let Some(document_id) = filetree_drop_row_document_id(row) { doc_ids.push(document_id); } continue; } if row_kind == "asset" && is_filetree_drop_real_asset(row) { if let Some(asset_id) = normalize_optional_filetree_string(&row.asset_id) { if !copyable_asset_ids .iter() .any(|existing| existing == &asset_id) { copyable_asset_ids.push(asset_id); } } let source_document_id = normalize_optional_filetree_string(&row.asset_document_id) .or_else(|| filetree_drop_row_document_id(row)); if let Some(source_document_id) = source_document_id { if !source_asset_document_ids .iter() .any(|existing| existing == &source_document_id) { source_asset_document_ids.push(source_document_id); } } } } if doc_ids.is_empty() && copyable_asset_ids.is_empty() { return Err(BridgeError::validation( "没有可拖拽的对象(虚拟附件暂不支持)", )); } let parent_by_document_id: HashMap> = payload .document_parents .iter() .filter_map(|entry| { let document_id = entry.document_id.trim(); if document_id.is_empty() { return None; } Some(( document_id.to_string(), normalize_optional_filetree_string(&entry.parent_id), )) }) .collect(); let top_level_doc_ids = filter_top_level_filetree_doc_ids(&doc_ids, &parent_by_document_id); let target_mindmap_id = resolve_filetree_drop_mindmap_target_id(target_row.as_ref()); let target_sub_path = target_mindmap_id .as_deref() .map(|mindmap_id| format!("mindmaps/{mindmap_id}")); let document_transfer_plan = if doc_ids.is_empty() { None } else { Some(FileTreeDropDocumentTransferPlan { action: if payload.copy { "copy" } else { "move" }.into(), target_parent_id: target_document_id.clone(), document_ids: doc_ids.clone(), top_level_document_ids: top_level_doc_ids.clone(), copy_items: doc_ids .iter() .map(|document_id| DocumentCopyTreeItemPlan { document_id: document_id.clone(), recursive: true, }) .collect(), }) }; let resource_transfer_plan = if copyable_asset_ids.is_empty() { None } else { Some(ResourceTransferPlan { action: if payload.copy { "copy" } else { "move" }.into(), asset_ids: copyable_asset_ids.clone(), target_document_id: target_document_id.clone(), target_sub_path: target_sub_path.clone(), }) }; let plan = FileTreeDropPlan { copy: payload.copy, target_document_id, target_mindmap_id, target_sub_path, row_ids, doc_ids, top_level_doc_ids, copyable_asset_ids, source_asset_document_ids, document_transfer_plan, resource_transfer_plan, }; validate_filetree_drop_doc_legality(&plan, &parent_by_document_id)?; Ok(plan) } fn tree_stream_delta_hint(kind: &str, args: Value) -> Value { json!({ "family": "tree", "kind": kind, "args": args, }) } fn tree_document_result_hint(document_field: &str) -> Value { tree_stream_delta_hint( "document_result", json!({ "documentField": document_field, }), ) } fn tree_result_document_hint() -> Value { tree_stream_delta_hint("result_document", json!({})) } fn tree_domain_event_hint(event_type: &str) -> Value { json!({ "family": "tree", "eventType": event_type, }) } fn tree_domain_event_plan(event_type: &str, stream_delta_hint: Value) -> Value { json!({ "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": event_type, "streamDeltaHint": stream_delta_hint, }) } fn tree_domain_event_plan_with_payload( event_type: &str, payload: Value, stream_delta_hint: Value, ) -> Value { json!({ "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": event_type, "payload": payload, "streamDeltaHint": stream_delta_hint, }) } fn tree_resync_required_hint(reason: &str, args: Value) -> Value { let mut hint_args = serde_json::Map::new(); hint_args.insert("reason".into(), Value::String(reason.into())); if let Some(args) = args.as_object() { for (key, value) in args { hint_args.insert(key.clone(), value.clone()); } } tree_stream_delta_hint("resync_required", Value::Object(hint_args)) } fn read_trimmed_str_field<'a>(value: &'a Value, field: &str) -> Option<&'a str> { value .get(field) .and_then(Value::as_str) .map(str::trim) .filter(|item| !item.is_empty()) } fn read_record_field<'a>(value: &'a Value, field: &str) -> Option<&'a Value> { value.get(field).filter(|item| item.is_object()) } fn materialize_tree_stream_delta( plan: &RuntimeCommandExecutionPlan, result: &Value, ) -> Option { let hint = plan.args_json.get("streamDeltaHint")?; materialize_tree_stream_delta_from_hint(hint, result) } fn materialize_tree_stream_delta_from_hint(hint: &Value, result: &Value) -> Option { if hint.get("family").and_then(Value::as_str) != Some("tree") { return None; } let kind = hint.get("kind").and_then(Value::as_str)?.trim(); let args = hint.get("args").filter(|value| value.is_object())?; match kind { "noop" => Some(json!({ "op": "noop" })), "resync_required" => { let mut delta = serde_json::Map::new(); delta.insert("op".into(), Value::String("resync_required".into())); if let Some(reason) = read_trimmed_str_field(args, "reason") { delta.insert("reason".into(), Value::String(reason.into())); } if let Some(page_id) = read_trimmed_str_field(args, "pageId") { delta.insert("pageId".into(), Value::String(page_id.into())); } if let Some(document_id) = read_trimmed_str_field(args, "documentId") { delta.insert("documentId".into(), Value::String(document_id.into())); } if let Some(block_id) = read_trimmed_str_field(args, "blockId") { delta.insert("blockId".into(), Value::String(block_id.into())); } Some(Value::Object(delta)) } "remove_document" => { let document_id = read_trimmed_str_field(args, "documentId")?; Some(json!({ "op": "remove_document", "documentId": document_id, })) } "upsert_document_patch" => { let document_id = read_trimmed_str_field(args, "documentId")?; let patch = read_record_field(args, "patch")?; let mut document = serde_json::Map::new(); document.insert("id".into(), Value::String(document_id.into())); if let Some(patch_map) = patch.as_object() { for (key, value) in patch_map { document.insert(key.clone(), value.clone()); } } if !document.contains_key("updated_at") { if let Some(updated_at) = read_trimmed_str_field(result, "updated_at") { document.insert("updated_at".into(), Value::String(updated_at.into())); } } Some(json!({ "op": "upsert_document", "document": Value::Object(document), })) } "document_result" => { let document_field = read_trimmed_str_field(args, "documentField").unwrap_or("document"); let document = read_record_field(result, document_field)?; Some(json!({ "op": "upsert_document", "document": document, })) } "result_document" => { if !result.is_object() { return None; } Some(json!({ "op": "upsert_document", "document": result, })) } "copy_result" => { let items_field = read_trimmed_str_field(args, "itemsField").unwrap_or("items"); let document_field = read_trimmed_str_field(args, "documentField").unwrap_or("document"); let items = result.get(items_field)?.as_array()?; let documents = items .iter() .filter_map(|item| read_record_field(item, document_field).cloned()) .collect::>(); if documents.is_empty() { return None; } Some(json!({ "op": "upsert_documents", "upsertDocuments": documents, })) } "asset_result" => { let items_field = read_trimmed_str_field(args, "itemsField").unwrap_or("items"); let items = result.get(items_field)?.as_array()?; if items.is_empty() { return None; } Some(json!({ "op": "upsert_assets", "upsertAssets": items, })) } "move_document" => { let document_id = read_trimmed_str_field(args, "documentId")?; let sort_order = result .get("sort_order") .and_then(Value::as_i64) .or_else(|| args.get("sortOrder").and_then(Value::as_i64))?; let parent_id = if result.get("parent_id").is_some() { result.get("parent_id").cloned().unwrap_or(Value::Null) } else { args.get("parentId").cloned().unwrap_or(Value::Null) }; let mut delta = serde_json::Map::new(); delta.insert("op".into(), Value::String("move_document".into())); delta.insert("documentId".into(), Value::String(document_id.into())); delta.insert("parentId".into(), parent_id); delta.insert( "sortOrder".into(), Value::Number(serde_json::Number::from(sort_order)), ); if let Some(updated_at) = read_trimmed_str_field(result, "updated_at") { delta.insert("updatedAt".into(), Value::String(updated_at.into())); } Some(Value::Object(delta)) } _ => None, } } fn materialize_tree_domain_event_plan( plan: &RuntimeCommandExecutionPlan, result: &Value, ) -> Option<(String, Value)> { let event_plan = plan.args_json.get("domainEventPlan")?; materialize_tree_domain_event_value(event_plan, plan, result) } fn materialize_tree_domain_event_plans( plan: &RuntimeCommandExecutionPlan, result: &Value, ) -> Vec<(String, Value)> { plan.args_json .get("domainEventPlans") .and_then(Value::as_array) .map(|event_plans| { event_plans .iter() .filter_map(|event_plan| { materialize_tree_domain_event_value(event_plan, plan, result) }) .collect() }) .filter(|event_plans: &Vec<(String, Value)>| !event_plans.is_empty()) .or_else(|| { materialize_tree_domain_event_plan(plan, result).map(|event_plan| vec![event_plan]) }) .unwrap_or_default() } fn materialize_tree_domain_event_value( event_plan: &Value, plan: &RuntimeCommandExecutionPlan, result: &Value, ) -> Option<(String, Value)> { if event_plan.get("family").and_then(Value::as_str) != Some("tree") || event_plan.get("schema").and_then(Value::as_str) != Some("mnote.tree.domain_event") || event_plan.get("schemaVersion").and_then(Value::as_i64) != Some(1) { return None; } let event_type = read_trimmed_str_field(event_plan, "eventType")?.to_string(); let mut materialized = event_plan.clone(); if materialized.get("streamDelta").is_none() { let stream_delta = materialize_tree_stream_delta(plan, result).or_else(|| { event_plan .get("streamDeltaHint") .and_then(|hint| materialize_tree_stream_delta_from_hint(hint, result)) }); if let Some(stream_delta) = stream_delta { if let Some(map) = materialized.as_object_mut() { map.insert("streamDelta".into(), stream_delta); } } } Some((event_type, materialized)) } fn tree_artifact_payload( context: &RuntimeBridgeContextWire, command: &RuntimeCommandEnvelopeWire, event_type: &str, aggregate_type: &str, aggregate_id: &str, domain_event_plan: &Value, ) -> Value { let mut payload = json!({ "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": event_type, "aggregate": { "type": aggregate_type, "id": aggregate_id, }, "trace": { "requestId": context.request_id, "traceId": context.trace_id, }, "command": { "id": command.command_id, "name": command.name, "idempotencyKey": command.idempotency_key, }, "error": Value::Null, "request_id": context.request_id, "trace_id": context.trace_id, "command_id": command.command_id, "command_name": command.name, "idempotency_key": command.idempotency_key, }); if let Some(stream_delta) = domain_event_plan.get("streamDelta") { if let Some(map) = payload.as_object_mut() { map.insert("streamDelta".into(), stream_delta.clone()); } } if let Some(formal_payload) = domain_event_plan.get("payload").and_then(Value::as_object) { if let Some(map) = payload.as_object_mut() { for (key, value) in formal_payload { map.insert(key.clone(), value.clone()); } } } payload } pub fn build_runtime_command_artifact_plan( context: &RuntimeBridgeContextWire, command: &RuntimeCommandEnvelopeWire, plan: &RuntimeCommandExecutionPlan, result: &Value, now: &str, ) -> Option { let workspace_id = plan .workspace_id .as_deref() .or(context.workspace_id.as_deref()) .or_else(|| { command .target .as_ref() .and_then(|target| target.workspace_id.as_deref()) })? .trim(); if workspace_id.is_empty() { return None; } let command_log_id = format!("clog_{}", command.command_id); let target_page_id = command .target .as_ref() .and_then(|target| target.page_id.as_ref()) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()); let target_block_id = command .target .as_ref() .and_then(|target| target.block_id.as_ref()) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()); let materialized_event_plans = materialize_tree_domain_event_plans(plan, result); let command_payload = if let Some((_, domain_event_plan)) = materialized_event_plans.first() { if let Some(stream_delta) = domain_event_plan.get("streamDelta") { let mut payload = command.payload.clone(); if let Some(map) = payload.as_object_mut() { map.insert("streamDelta".into(), stream_delta.clone()); payload } else { json!({ "payload": command.payload, "streamDelta": stream_delta, }) } } else { command.payload.clone() } } else { command.payload.clone() }; let aggregate_type = if target_block_id.is_some() { "block" } else if target_page_id.is_some() { "page" } else { "workspace" }; let aggregate_id = target_block_id .as_deref() .or(target_page_id.as_deref()) .unwrap_or(workspace_id); let command_log = RuntimeCommandLogArtifactPlan { workspace_id: workspace_id.into(), id: command_log_id.clone(), request_id: context.request_id.clone(), trace_id: context.trace_id.clone(), command_id: command.command_id.clone(), command_name: command.name.clone(), actor_id: context.actor.actor_id.clone(), actor_type: context.actor.actor_type.clone(), source_channel: context.source.channel.clone(), source_client: context.source.client.clone(), status: "succeeded".into(), target_page_id: target_page_id.clone(), target_block_id: target_block_id.clone(), payload: command_payload, payload_summary: format!( "command={};request_id={};trace_id={}", command.name, context.request_id, context.trace_id ), refs: command.refs.clone(), idempotency_key: command.idempotency_key.clone(), error: None, created_at: now.into(), finished_at: Some(now.into()), }; let domain_events: Vec = materialized_event_plans .iter() .enumerate() .map( |(index, (event_type, domain_event_plan))| RuntimeDomainEventArtifactPlan { workspace_id: workspace_id.into(), id: tree_domain_event_artifact_id( &command.command_id, event_type, index, materialized_event_plans.len(), ), request_id: context.request_id.clone(), trace_id: context.trace_id.clone(), command_id: command.command_id.clone(), command_log_id: command_log_id.clone(), event_type: event_type.clone(), aggregate_type: aggregate_type.into(), aggregate_id: aggregate_id.into(), event_version: 1, status: "committed".into(), actor_type: context.actor.actor_type.clone(), payload: tree_artifact_payload( context, command, &event_type, aggregate_type, aggregate_id, &domain_event_plan, ), created_at: now.into(), }, ) .collect(); let domain_event = domain_events.first().cloned(); Some(RuntimeCommandArtifactPlan { command_log, domain_event, domain_events, }) } fn tree_domain_event_artifact_id( command_id: &str, event_type: &str, index: usize, total: usize, ) -> String { if total <= 1 || index == 0 { return format!("evt_{command_id}"); } let suffix: String = event_type .chars() .map(|ch| { if ch.is_ascii_alphanumeric() { ch.to_ascii_lowercase() } else { '_' } }) .collect(); format!("evt_{command_id}_{:02}_{suffix}", index + 1) } fn workspace_source_value(source: &RuntimeSourceWire) -> Value { let mut value = json!({ "channel": source.channel, "client": source.client, }); if let Some(map) = value.as_object_mut() { if let Some(source_kind) = source .source_kind .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) { map.insert("sourceKind".into(), json!(source_kind)); } if let Some(root_uri) = source .root_uri .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) { map.insert("rootUri".into(), json!(root_uri)); } if let Some(workspace_id) = source .workspace_id .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) { map.insert("workspaceId".into(), json!(workspace_id)); } if !source.capabilities.is_empty() { map.insert("capabilities".into(), json!(source.capabilities)); } } value } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum WorkspaceCommandExecutor { LegacyConvex, ConvexWorkspace, LocalFolder, } fn resolve_workspace_command_executor( source: &RuntimeSourceWire, ) -> Result { let Some(source_kind) = source .source_kind .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) else { return Ok(WorkspaceCommandExecutor::LegacyConvex); }; let executor = match source_kind { "convex_workspace" => WorkspaceCommandExecutor::ConvexWorkspace, "local_folder" => WorkspaceCommandExecutor::LocalFolder, other => { return Err(BridgeError::validation(format!( "unknown workspace source kind: {other}" ))) } }; if !source .capabilities .iter() .any(|capability| capability.trim() == "execute-command") { return Err(BridgeError::validation(format!( "workspace source {source_kind} missing capability: execute-command" ))); } Ok(executor) } fn read_preflight_field<'a>( command: &'a RuntimeCommandEnvelopeWire, field: &str, ) -> Option<&'a Value> { command .preflight_data .as_ref() .and_then(|preflight| preflight.get(field)) } fn compose_content_with_blocks(content: &Value, blocks: Vec) -> Value { if content.is_array() { return Value::Array(blocks); } if let Some(map) = content.as_object() { let mut next = map.clone(); next.insert("blocks".into(), Value::Array(blocks)); return Value::Object(next); } json!({ "blocks": blocks }) } fn build_page_aggregate_embed_plan( command_wire: &RuntimeCommandEnvelopeWire, payload: &DocumentEmbedCommandPayload, ) -> Result, BridgeError> { let Some(input) = read_preflight_field(command_wire, "pageAggregateEmbed") else { return Ok(None); }; let target_content = input .get("targetContent") .ok_or_else(|| BridgeError::validation("pageAggregateEmbed 缺少 targetContent"))?; let source_title = read_trimmed_str_field(input, "sourceTitle").unwrap_or("无标题"); let anchor_block_id = payload .anchor_block_id .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .or_else(|| read_trimmed_str_field(input, "anchorBlockId")); let block_id = read_trimmed_str_field(input, "blockId") .map(ToOwned::to_owned) .unwrap_or_else(|| format!("page_ref_{}", payload.source_document_id)); let current_blocks = normalize_blocks_from_value(target_content); let insert_index = anchor_block_id .and_then(|anchor_id| { current_blocks.iter().position(|block| { read_trimmed_str_field(block, "id") .map(|block_id| block_id == anchor_id) .unwrap_or(false) }) }) .map(|index| index + 1) .unwrap_or(current_blocks.len()); let page_reference_block = json!({ "id": block_id, "type": "pageReference", "props": { "pageId": payload.source_document_id, "title": source_title, }, }); let mut next_blocks = Vec::with_capacity(current_blocks.len() + 1); next_blocks.extend(current_blocks.iter().take(insert_index).cloned()); next_blocks.push(page_reference_block.clone()); next_blocks.extend(current_blocks.iter().skip(insert_index).cloned()); let next_content = compose_content_with_blocks(target_content, next_blocks); Ok(Some(json!({ "schema": "mnote.page_aggregate.embed_plan", "schemaVersion": 1, "sourceDocumentId": payload.source_document_id, "targetDocumentId": payload.target_document_id, "anchorBlockId": anchor_block_id, "insertIndex": insert_index, "block": page_reference_block, "content": next_content, "blockCount": normalize_blocks_from_value(&next_content).len(), }))) } #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] struct RuntimeBlockSummary { id: String, #[serde(rename = "type")] block_type: String, text: String, depth: usize, child_count: usize, } #[derive(Debug, Clone)] struct RuntimeInsertSpec { block_type: String, text: String, level: u64, } #[derive(Debug)] struct InsertBlocksResult { inserted: Vec, blocks: Vec, editor_commands: Vec, } #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] struct RuntimeMindmapSummary { uid: String, text: String, parent_uid: Option, depth: usize, child_count: usize, } #[derive(Debug, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] struct RuntimeMindmapSubtreeSummary { uid: String, text: String, depth: usize, child_count: usize, } pub fn execute_runtime_input(input: RuntimeInput) -> Result { match input { RuntimeInput::Query { context, query, .. } => execute_query(context, query), RuntimeInput::Command { context, command } => execute_command(context, command), RuntimeInput::CommandArtifact { .. } => Err(BridgeError::validation( "execute_runtime_input 不支持 command artifact 输入", )), RuntimeInput::Tool { context, tool, .. } => execute_tool_plan(context, tool), } } pub fn execute_runtime_query(input: RuntimeInput) -> Result { match input { RuntimeInput::Query { context, query, data, } => execute_query_result(context, query, data.unwrap_or(Value::Null)), RuntimeInput::Tool { context, tool, data, } => execute_tool_result(context, tool, data.unwrap_or(Value::Null)), RuntimeInput::Command { .. } | RuntimeInput::CommandArtifact { .. } => Err( BridgeError::validation("execute_runtime_query 仅支持 query 输入"), ), } } pub fn build_success_response(plan: RuntimeExecutionPlan) -> RuntimeSuccess { RuntimeSuccess { ok: true, plan } } pub fn build_artifact_success_response( artifacts: Option, ) -> RuntimeArtifactSuccess { RuntimeArtifactSuccess { ok: true, artifacts, } } pub fn execute_runtime_command_artifact( input: RuntimeInput, ) -> Result, BridgeError> { match input { RuntimeInput::CommandArtifact { context, command, plan, result, now, } => Ok(build_runtime_command_artifact_plan( &context, &command, &plan, &result, &now, )), _ => Err(BridgeError::validation( "execute_runtime_command_artifact 仅支持 command artifact 输入", )), } } pub fn build_failure_response(error: BridgeError) -> RuntimeFailure { RuntimeFailure { ok: false, error: RuntimeErrorPayload { kind: bridge_error_kind_to_wire(&error.kind).into(), message: error.message, }, } } pub fn runtime_input_requests_result(input: &RuntimeInput) -> bool { match input { RuntimeInput::Query { data: Some(_), .. } => true, RuntimeInput::Tool { tool, .. } => { parse_tool_mode(tool.mode.as_deref()).unwrap_or(ToolExecutionMode::Plan) == ToolExecutionMode::Result } RuntimeInput::CommandArtifact { .. } => false, RuntimeInput::Command { .. } | RuntimeInput::Query { data: None, .. } => false, } } fn execute_query( context_wire: RuntimeBridgeContextWire, query_wire: RuntimeQueryEnvelopeWire, ) -> Result { let context = to_bridge_context(context_wire); match query_wire.name.as_str() { "kernel.node.get" => { let payload: KernelGetNodeQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "kernel.node.get".into(), payload: KernelGetNode { node_id: payload.node_id.clone(), workspace_id: payload.workspace_id.clone(), }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "nodeId": payload.node_id, "workspaceId": payload.workspace_id, }), })) } "kernel.subtree.get" => { let payload: KernelGetSubtreeQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "kernel.subtree.get".into(), payload: KernelGetSubtree { subtree: KernelSubtreeRef { root_node_id: payload.root_node_id.clone(), path: vec![payload.root_node_id.clone()], depth: payload.depth, }, workspace_id: payload.workspace_id.clone(), include_edges: payload.include_edges.unwrap_or(true), filters: KernelProjectionFilter { node_types: payload.node_types.clone().unwrap_or_default(), edge_types: payload.edge_types.clone().unwrap_or_default(), query: None, max_results: None, include_deleted: false, }, }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "rootNodeId": payload.root_node_id, "workspaceId": payload.workspace_id, "depth": payload.depth, "includeEdges": payload.include_edges.unwrap_or(true), "nodeTypes": payload.node_types.unwrap_or_default(), "edgeTypes": payload.edge_types.unwrap_or_default(), }), })) } "kernel.children.list" => { let payload: KernelListChildrenQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "kernel.children.list".into(), payload: KernelListChildren { parent_node_id: payload.parent_node_id.clone(), workspace_id: payload.workspace_id.clone(), node_types: payload.node_types.clone().unwrap_or_default(), }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "parentNodeId": payload.parent_node_id, "workspaceId": payload.workspace_id, "nodeTypes": payload.node_types.unwrap_or_default(), }), })) } "kernel.edges.list" => { let payload: KernelListEdgesQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "kernel.edges.list".into(), payload: KernelListEdges { node_id: payload.node_id.clone(), workspace_id: payload.workspace_id.clone(), edge_types: payload.edge_types.clone().unwrap_or_default(), direction: payload.direction.clone(), }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "nodeId": payload.node_id, "workspaceId": payload.workspace_id, "edgeTypes": payload.edge_types.unwrap_or_default(), "direction": payload.direction.unwrap_or(KernelGraphDirection::Both), }), })) } "kernel.graph.traverse" => { let payload: KernelTraverseGraphQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "kernel.graph.traverse".into(), payload: KernelTraverseGraph { start_node_id: payload.start_node_id.clone(), workspace_id: payload.workspace_id.clone(), edge_types: payload.edge_types.clone().unwrap_or_default(), max_depth: payload.max_depth.unwrap_or(2), }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "startNodeId": payload.start_node_id, "workspaceId": payload.workspace_id, "edgeTypes": payload.edge_types.unwrap_or_default(), "maxDepth": payload.max_depth.unwrap_or(2), }), })) } "kernel.project_view" => { let payload: KernelProjectViewQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "kernel.project_view".into(), payload: KernelProjectionRequest { projection: payload.projection.clone(), workspace_id: payload.workspace_id.clone(), root_node_id: payload.root_node_id.clone(), subtree: payload .root_node_id .as_ref() .map(|root_node_id| KernelSubtreeRef { root_node_id: root_node_id.clone(), path: vec![root_node_id.clone()], depth: payload.depth, }), filters: KernelProjectionFilter { node_types: payload.node_types.clone().unwrap_or_default(), edge_types: payload.edge_types.clone().unwrap_or_default(), query: payload.query.clone(), max_results: payload.max_results, include_deleted: false, }, include_content: payload.include_content.unwrap_or(false), include_edges: payload.include_edges.unwrap_or(true), }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "projection": payload.projection, "workspaceId": payload.workspace_id, "rootNodeId": payload.root_node_id, "depth": payload.depth, "includeContent": payload.include_content.unwrap_or(false), "includeEdges": payload.include_edges.unwrap_or(true), "nodeTypes": payload.node_types.unwrap_or_default(), "edgeTypes": payload.edge_types.unwrap_or_default(), }), })) } "documents.content.get" => { let payload: DocumentContentQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "documents.content.get".into(), payload: core_protocol::GetPageContent { page_id: payload.document_id.clone(), workspace_id: payload.workspace_id, }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, }), })) } "page.aggregate.get" => { let payload: PageAggregateQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "page.aggregate.get".into(), payload: core_protocol::GetPageContent { page_id: payload.document_id.clone(), workspace_id: payload.workspace_id.clone(), }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "documentId": payload.document_id, "workspaceId": payload.workspace_id, }), })) } "mindmaps.get" | "mindmap.projection.get" => { let payload: MindmapGetQueryPayload = parse_payload(query_wire.payload)?; let query_name = if query_wire.name == "mindmap.projection.get" { "mindmap.projection.get" } else { "mindmaps.get" }; let query = QueryEnvelope { name: query_name.into(), payload: GetMindmap { document_id: payload.document_id.clone(), mindmap_id: payload.mindmap_id.clone(), workspace_id: payload.workspace_id.clone(), }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "docId": payload.document_id, "mindmapId": payload.mindmap_id, }), })) } "blocks.get" => { let payload: BlockGetQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "blocks.get".into(), payload: GetBlock { block_id: payload.block_id.clone(), }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "id": payload.block_id, "workspaceId": payload.workspace_id, }), })) } "sidebar.dataset.list" => { let payload: SidebarDatasetQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "sidebar.dataset.list".into(), payload: core_protocol::ListSidebarDataset { workspace_id: payload.workspace_id.clone(), }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "workspaceId": payload.workspace_id, }), })) } "search.documents" | "search.documents.query" => { let payload: SearchDocumentsQueryPayload = parse_payload(query_wire.payload)?; let query_name = if query_wire.name == "search.documents.query" { "search.documents.query" } else { "search.documents" }; let time_range = payload.time_range.clone().unwrap_or_else(|| "any".into()); let time_field = payload .time_field .clone() .unwrap_or_else(|| "updated".into()); let query = QueryEnvelope { name: query_name.into(), payload: SearchDocuments { query: payload.query.clone(), workspace_id: payload.workspace_id.clone(), page_id: payload.page_id.clone(), pagination: core_protocol::query::Pagination { limit: payload.limit.unwrap_or(30), cursor: payload.cursor.clone(), }, title_only: payload.title_only.unwrap_or(false), exact: payload.exact.unwrap_or(false), include_ocr: payload.include_ocr.unwrap_or(false), time_range: time_range.clone(), time_field: time_field.clone(), custom_range_from: payload.custom_range_from.clone(), custom_range_to: payload.custom_range_to.clone(), }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "query": payload.query, "workspaceId": payload.workspace_id, "pageId": payload.page_id, "limit": payload.limit.unwrap_or(30), "cursor": payload.cursor, "titleOnly": payload.title_only.unwrap_or(false), "exact": payload.exact.unwrap_or(false), "includeOcr": payload.include_ocr.unwrap_or(false), "timeRange": time_range, "timeField": time_field, "customRangeFrom": payload.custom_range_from, "customRangeTo": payload.custom_range_to, }), })) } "search.recent" => { let payload: SearchRecentQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "search.recent".into(), payload: SearchRecent { workspace_id: payload.workspace_id.clone(), pagination: core_protocol::query::Pagination { limit: payload.limit.unwrap_or(10), cursor: payload.cursor.clone(), }, }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "workspaceId": payload.workspace_id, "limit": payload.limit.unwrap_or(10), "cursor": payload.cursor, }), })) } "bridge.request.get" => { let payload: BridgeRequestQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "bridge.request.get".into(), payload: GetBridgeRequest { workspace_id: payload.workspace_id.clone(), request_id: payload.request_id.clone(), command_id: payload.command_id.clone(), }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "workspaceId": payload.workspace_id, "requestId": payload.request_id, "commandId": payload.command_id, }), })) } "bridge.trace.get" => { let payload: BridgeTraceQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "bridge.trace.get".into(), payload: GetBridgeTrace { workspace_id: payload.workspace_id.clone(), trace_id: payload.trace_id.clone(), command_id: payload.command_id.clone(), }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "workspaceId": payload.workspace_id, "traceId": payload.trace_id, "commandId": payload.command_id, }), })) } "bridge.command.get" => { let payload: BridgeCommandQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "bridge.command.get".into(), payload: GetBridgeCommand { workspace_id: payload.workspace_id.clone(), command_id: payload.command_id.clone(), }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "workspaceId": payload.workspace_id, "commandId": payload.command_id, }), })) } "bridge.workspace.overview" => { let payload: BridgeWorkspaceOverviewQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "bridge.workspace.overview".into(), payload: ListBridgeWorkspaceOverview { workspace_id: payload.workspace_id.clone(), pagination: core_protocol::query::Pagination { limit: payload.limit.unwrap_or(50), cursor: payload.cursor.clone(), }, command_status: payload.command_status.clone(), event_status: payload.event_status.clone(), target_page_id: payload.target_page_id.clone(), target_block_id: payload.target_block_id.clone(), aggregate_type: payload.aggregate_type.clone(), aggregate_id: payload.aggregate_id.clone(), }, }; let request = build_query_request(&context, &query)?; Ok(RuntimeExecutionPlan::Query(RuntimeQueryExecutionPlan { query_name: query.name, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, payload_json: request.payload_json, args_json: json!({ "workspaceId": payload.workspace_id, "limit": payload.limit.unwrap_or(50), "cursor": payload.cursor, "commandStatus": payload.command_status, "eventStatus": payload.event_status, "targetPageId": payload.target_page_id, "targetBlockId": payload.target_block_id, "aggregateType": payload.aggregate_type, "aggregateId": payload.aggregate_id, }), })) } other => Err(BridgeError::validation(format!( "bridge runtime 暂不支持 query: {other}" ))), } } fn execute_tool_plan( context_wire: RuntimeBridgeContextWire, tool_wire: RuntimeToolInvocationWire, ) -> Result { let context = to_bridge_context(context_wire.clone()); let invocation = to_tool_invocation(&tool_wire)?; let spec = default_tool_registry() .tool(invocation.tool.as_str()) .ok_or_else(|| { BridgeError::validation(format!("bridge runtime 暂不支持 tool: {}", invocation.tool)) })?; let payload_json = build_tool_payload_json(&context_wire, &invocation); let steps = build_tool_plan_steps(&context, &tool_wire, &invocation)?; Ok(RuntimeExecutionPlan::Tool(RuntimeToolExecutionPlan { tool_name: invocation.tool, invocation_kind: invocation_kind_label(&invocation.kind).into(), execution_mode: core_protocol::tool_mode_label(&invocation.mode).into(), effect: tool_effect_label(&spec.effect).into(), toolset_id: spec.toolset_id.into(), requires_confirmation: spec.requires_confirmation, request_id: context.request_id, trace_id: context.trace_id, actor_id: context.actor_id, validate_only: context.validate_only, dry_run: context.dry_run, payload_json, args_json: tool_wire.args_json, target: tool_wire.target.as_ref().map(target_to_json), steps, })) } fn execute_tool_result( _context_wire: RuntimeBridgeContextWire, tool_wire: RuntimeToolInvocationWire, data: Value, ) -> Result { let invocation = to_tool_invocation(&tool_wire)?; let spec = default_tool_registry() .tool(invocation.tool.as_str()) .ok_or_else(|| { BridgeError::validation(format!("bridge runtime 暂不支持 tool: {}", invocation.tool)) })?; let args = tool_wire.args_json; match spec.name { "search_web" => { let query = read_required_string_field(&args, "query")?; let count = read_u64_field(&args, "count").unwrap_or(6).clamp(1, 10); let base = env::var("SEARXNG_BASE_URL") .unwrap_or_else(|_| "http://127.0.0.1:8889".into()) .trim() .trim_end_matches('/') .to_string(); let token = env::var("SEARXNG_API_TOKEN") .unwrap_or_default() .trim() .to_string(); let url = format!( "{base}/search?q={}&format=json&language=zhh-CN&categories=general&safesearch=1", urlencoding::encode(&query) ); let payload = fetch_searxng_json(&url, &token)?; let results = payload .get("results") .and_then(Value::as_array) .cloned() .unwrap_or_default() .into_iter() .filter_map(|row| { let title = read_string_value(&row, "title")?; let url = read_string_value(&row, "url")?; if !(url.starts_with("http://") || url.starts_with("https://")) { return None; } Some(json!({ "title": title, "url": url, "snippet": read_string_value(&row, "content").or_else(|| read_string_value(&row, "snippet")), "engine": read_string_value(&row, "engine"), })) }) .take(count as usize) .collect::>(); return Ok(json!({ "ok": true, "source": infer_tool_source(&data), "query": query, "results": results, })); } "image_read" => { let input = merge_tool_input(&args, &data); return Ok(json!({ "ok": true, "source": infer_tool_source(&data), "found": input.get("asset").is_some(), "asset": input.get("asset").cloned().unwrap_or(Value::Null), "ocrStatus": input.get("ocrStatus").cloned().unwrap_or(Value::Null), "ocrText": input.get("ocrText").cloned().unwrap_or(Value::Null), "hasOcrText": input.get("hasOcrText").cloned().unwrap_or(Value::Null), "note": input.get("note").cloned().unwrap_or(Value::Null), })); } "slash_run" => { let input = merge_tool_input(&args, &data); let parsed = parse_slash_command_payload(&input)?; return Ok(json!({ "ok": true, "source": infer_tool_source(&data), "parsed": parsed, })); } "onlyoffice_session_resolve" => { let input: OnlyOfficeSessionResolveInput = parse_tool_input(&args, &data)?; let result = resolve_session(input); Ok(json!({ "ok": true, "source": infer_tool_source(&data), "locator": result.locator, "session": result.session, })) } "onlyoffice_sign" => { let input = merge_tool_input(&args, &data); let config = input .get("config") .cloned() .ok_or_else(|| BridgeError::validation("onlyoffice_sign 缺少 config"))?; let secret = input.get("secret").and_then(Value::as_str).unwrap_or(""); let tokens = sign_config(&config, secret).map_err(BridgeError::validation)?; Ok(json!({ "ok": true, "source": infer_tool_source(&data), "token": tokens.token, "documentToken": tokens.document_token, "editorConfigToken": tokens.editor_config_token, })) } "onlyoffice_prepare_proxy" => { let input: OnlyOfficeProxyPreparationInput = parse_tool_input(&args, &data)?; let result = prepare_proxy_request(input).map_err(BridgeError::validation)?; Ok(json!({ "ok": true, "source": infer_tool_source(&data), "targetUrl": result.target_url, "forwardHeaders": result.forward_headers, })) } "onlyoffice_prepare_callback" => { let input: OnlyOfficeCallbackPreparationInput = parse_tool_input(&args, &data)?; let result = prepare_callback(input).map_err(BridgeError::validation)?; Ok(json!({ "ok": true, "source": infer_tool_source(&data), "shouldWrite": result.should_write, "downloadUrl": result.download_url, "idempotencyKey": result.idempotency_key, "locator": result.locator, "session": result.session, })) } "onlyoffice_prepare_forcesave" => { let input: OnlyOfficeForcesavePreparationInput = parse_tool_input(&args, &data)?; let requests = prepare_forcesave(input).map_err(BridgeError::validation)?; Ok(json!({ "ok": true, "source": infer_tool_source(&data), "requests": requests, })) } "bridge_request_get" => normalize_bridge_observability_result(&data, "bridge.request.get"), "bridge_trace_get" => normalize_bridge_observability_result(&data, "bridge.trace.get"), "bridge_command_get" => normalize_bridge_observability_result(&data, "bridge.command.get"), "event_replay" => execute_event_replay(&args, &data), "index_rebuild" => execute_index_rebuild(&args, &data), "doc_get" => { let blocks = normalize_blocks_from_value(&data); let max_blocks = read_u64_field(&args, "maxBlocks").unwrap_or(80); let summaries = walk_block_summaries(&blocks, max_blocks as usize); Ok(json!({ "ok": true, "source": infer_tool_source(&data), "totalTopLevelBlocks": blocks.len(), "blocks": summaries, })) } "doc_find" => { let query = read_required_string_field(&args, "query")?; let max_results = read_u64_field(&args, "maxResults").unwrap_or(8); let blocks = normalize_blocks_from_value(&data); let summaries = walk_block_summaries(&blocks, 400); let needle = query.to_lowercase(); let results = summaries .into_iter() .filter(|summary| summary.text.to_lowercase().contains(&needle)) .take(max_results as usize) .collect::>(); Ok(json!({ "ok": true, "source": infer_tool_source(&data), "query": query, "results": results, })) } "docs_search" => { let payload: DocsSearchToolPayload = parse_tool_input(&args, &data)?; let normalized_query = payload.query.trim().to_string(); if normalized_query.is_empty() { return Ok(json!({ "ok": true, "source": infer_tool_source(&data), "query": normalized_query, "results": [], "enqueueAssetIds": [], })); } let limit = payload.limit.unwrap_or(12).clamp(1, 30) as usize; let requested_workspace = payload .workspace_id .as_ref() .map(|value| value.trim()) .filter(|value| !value.is_empty()) .map(str::to_string); let mut enqueue_asset_ids = Vec::::new(); let mut results = Vec::::new(); for dataset in payload.datasets { if let Some(workspace_id) = requested_workspace.as_ref() { if dataset.workspace_id != *workspace_id { continue; } } let evaluation = evaluate_search_documents( &SearchDocumentsRequest { query: normalized_query.clone(), workspace_id: dataset.workspace_id.clone(), page_id: payload.page_id.clone(), limit, title_only: payload.title_only.unwrap_or(false), exact: payload.exact.unwrap_or(false), include_ocr: payload.include_ocr.unwrap_or(false), time_range: payload.time_range.clone().unwrap_or_else(|| "any".into()), time_field: payload .time_field .clone() .unwrap_or_else(|| "updated".into()), custom_range_from: payload.custom_range_from.clone(), custom_range_to: payload.custom_range_to.clone(), }, &SearchDocumentsDataset { documents: dataset.documents, mindmaps: dataset.mindmaps, tables: dataset.tables, table_rows: dataset.table_rows, assets: dataset.assets, }, ); enqueue_asset_ids.extend(evaluation.enqueue_asset_ids); for item in evaluation.results { results.push(json!({ "id": item.id, "title": item.title, "snippet": item.snippet, "updatedAt": item.updated_at, "createdAt": item.created_at, "matchField": item.match_field, "hasOcr": item.has_ocr, "publicPath": item.public_path, "score": item.score, "workspaceId": dataset.workspace_id, })); } } results.sort_by(|left, right| { let left_score = left.get("score").and_then(Value::as_f64).unwrap_or(0.0); let right_score = right.get("score").and_then(Value::as_f64).unwrap_or(0.0); right_score .partial_cmp(&left_score) .unwrap_or(std::cmp::Ordering::Equal) .then_with(|| { let left_updated = left.get("updatedAt").and_then(Value::as_str).unwrap_or(""); let right_updated = right.get("updatedAt").and_then(Value::as_str).unwrap_or(""); right_updated.cmp(left_updated) }) }); results.truncate(limit); enqueue_asset_ids.sort(); enqueue_asset_ids.dedup(); Ok(json!({ "ok": true, "source": infer_tool_source(&data), "query": normalized_query, "results": results, "enqueueAssetIds": enqueue_asset_ids, "includeDeleted": payload.include_deleted.unwrap_or(false), })) } "docs_read" => { let payload: DocsReadToolPayload = parse_tool_input(&args, &data)?; let max_chars = payload.max_chars.unwrap_or(2500).clamp(200, 20_000) as usize; let raw_text = payload.raw_text.unwrap_or_default(); let raw_text_length = payload .raw_text_length .unwrap_or_else(|| raw_text.chars().count() as u64); let trimmed = trim_text_for_docs_read(&raw_text, max_chars); let mut result = json!({ "ok": true, "source": infer_tool_source(&data), "documentId": payload.document_id, "title": payload.title.unwrap_or_else(|| "".into()), "workspaceId": payload.workspace_id.unwrap_or_else(|| "".into()), "parentId": payload.parent_id, "updatedAt": payload.updated_at, "rawTextLength": raw_text_length, "rawText": trimmed, }); if payload.include_content.unwrap_or(false) { if let Some(map) = result.as_object_mut() { map.insert("content".into(), payload.content.unwrap_or(Value::Null)); } } Ok(result) } "doc_insert_blocks" => { let blocks = normalize_blocks_from_value(&data); let specs = parse_insert_specs(&args)?; let after_block_id = read_optional_string_field(&args, "afterBlockId"); let before_block_id = read_optional_string_field(&args, "beforeBlockId"); let result = apply_insert_blocks(blocks, after_block_id, before_block_id, specs)?; let editor_command_source = if result.editor_commands.is_empty() { "legacy_snapshot_fallback" } else { "rust_editor_command" }; Ok(json!({ "ok": true, "source": infer_tool_source(&data), "inserted": result.inserted, "editorCommandSource": editor_command_source, "editorCommands": result.editor_commands, "data": result.blocks, })) } "doc_replace_range" => { let blocks = normalize_blocks_from_value(&data); let block_id = read_required_string_field(&args, "blockId")?; let text = read_required_string_field(&args, "text")?; let mode = read_optional_string_field(&args, "mode").unwrap_or_else(|| "replace".into()); let editor_commands = build_replace_editor_commands(&blocks, &block_id, &text, &mode); let result = apply_replace_range(blocks, &block_id, &text, &mode)?; let editor_command_source = if editor_commands.is_empty() { "legacy_snapshot_fallback" } else { "rust_editor_command" }; Ok(json!({ "ok": true, "source": infer_tool_source(&data), "blockId": block_id, "mode": mode, "editorCommandSource": editor_command_source, "editorCommands": editor_commands, "data": result, })) } "mindmap_get" => { let tree = normalize_mindmap_from_value(&data)?; let max_nodes = read_u64_field(&args, "maxNodes").unwrap_or(120); let nodes = walk_mindmap_summaries(&tree, max_nodes as usize); Ok(json!({ "ok": true, "source": infer_tool_source(&data), "documentId": resolve_document_id(&args, tool_wire.target.as_ref())?, "mindmapId": resolve_mindmap_id(&args, tool_wire.target.as_ref())?, "nodes": nodes, })) } "mindmap_get_subtree" => { let tree = normalize_mindmap_from_value(&data)?; let uid = read_required_string_field(&args, "uid")?; let depth = read_u64_field(&args, "depth").unwrap_or(2) as usize; let max_nodes = read_u64_field(&args, "maxNodes").unwrap_or(60) as usize; let node = find_mindmap_node_by_uid(&tree, &uid) .ok_or_else(|| BridgeError::validation(format!("未找到 uid={uid}")))?; Ok(json!({ "ok": true, "source": infer_tool_source(&data), "uid": uid, "nodes": summarize_mindmap_subtree(node, depth, max_nodes), })) } "mindmap_put" => { let tree = parse_mindmap_tree_from_args(&args, "data")?; Ok(json!({ "ok": true, "source": infer_tool_source(&data), "data": tree, })) } "mindmap_apply_ops" => { let mut tree = normalize_mindmap_from_value(&data)?; let ops = parse_mindmap_ops(&args)?; let result = apply_mindmap_ops(&mut tree, &ops)?; Ok(json!({ "ok": true, "source": infer_tool_source(&data), "applied": result.applied, "errors": result.errors, "data": tree, "meta": { "reason": read_optional_string_field(&args, "reason"), }, })) } "mindmap_expand_node" => { let merged = merge_tool_input(&args, &data); let mut tree = normalize_mindmap_from_value(&data)?; let target_uid = read_required_string_field(&merged, "targetUid")?; let instruction = read_optional_string_field(&merged, "instruction").unwrap_or_default(); let mut ops = if let Some(ops_value) = merged.get("ops").cloned() { serde_json::from_value::>(ops_value).map_err(|error| { BridgeError::validation(format!( "mindmap_expand_node ops 反序列化失败: {error}" )) })? } else { Vec::new() }; if ops.is_empty() { let search_results = merged .get("searchResults") .and_then(Value::as_array) .cloned() .unwrap_or_default(); for item in search_results { let title = item .as_object() .and_then(|map| map.get("title")) .and_then(Value::as_str) .unwrap_or("") .trim() .to_string(); let url = item .as_object() .and_then(|map| map.get("url")) .and_then(Value::as_str) .unwrap_or("") .trim() .to_string(); let snippet = item .as_object() .and_then(|map| map.get("snippet")) .and_then(Value::as_str) .unwrap_or("") .trim() .to_string(); if title.is_empty() || url.is_empty() { continue; } ops.push(MindmapOp::AddChild { parent_uid: target_uid.clone(), node: MindmapNodeInput { uid: None, text: title, hyperlink: Some(url.clone()), note: None, refs: Some(vec![MindmapNodeRef { kind: "url".into(), asset_id: None, file_url: Some(url), page: None, slide: None, title: None, snippet: if snippet.is_empty() { None } else { Some(snippet) }, }]), }, }); } } if ops.is_empty() { let base = instruction.chars().take(12).collect::(); let label = if base.trim().is_empty() { "补完".to_string() } else { base.trim().to_string() }; for index in 1..=3 { ops.push(MindmapOp::AddChild { parent_uid: target_uid.clone(), node: MindmapNodeInput { uid: None, text: format!("{label}(待核验){index}"), hyperlink: None, note: None, refs: None, }, }); } } let result = apply_mindmap_ops(&mut tree, &ops)?; Ok(json!({ "ok": true, "source": infer_tool_source(&data), "applied": result.applied, "errors": result.errors, "ops": ops, "data": tree, })) } "mindmap_empty_trash" => { let payload: MindmapEmptyTrashToolPayload = parse_tool_input(&args, &data)?; Ok(json!({ "ok": true, "source": infer_tool_source(&data), "workspaceId": payload.workspace_id, "removed": 0, })) } "mindmap_outline_to_mindmap" => { let payload: MindmapOutlineToolPayload = parse_tool_input(&args, &data)?; let outline = parse_mindmap_outline_items(&json!({ "outline": payload.outline, }))?; let tree = build_mindmap_outline_tree( &payload.root_title, &outline, &payload.page_link_pattern, )?; Ok(json!({ "ok": true, "source": infer_tool_source(&data), "documentId": payload.document_id, "mindmapId": payload.mindmap_id, "data": tree, "meta": { "title": payload.root_title, "outlineCount": outline.len(), }, })) } other => Err(BridgeError::validation(format!( "bridge runtime 暂不支持 tool 执行: {other}" ))), } } fn fetch_searxng_json(url: &str, token: &str) -> Result { let client = reqwest::blocking::Client::builder() .timeout(std::time::Duration::from_secs(20)) .build() .map_err(|error| BridgeError::transport(format!("SearxNG 客户端创建失败: {error}")))?; let mut request = client.get(url); if !token.is_empty() { request = request.header("Authorization", format!("Bearer {token}")); } let response = request .send() .map_err(|error| BridgeError::transport(format!("SearxNG 请求失败: {error}")))?; let status = response.status(); let text = response .text() .map_err(|error| BridgeError::transport(format!("SearxNG 响应读取失败: {error}")))?; if !status.is_success() { return Err(BridgeError::transport(format!( "SearxNG 请求失败:{} {}", status.as_u16(), status.canonical_reason().unwrap_or("unknown") ))); } serde_json::from_str(&text) .map_err(|error| BridgeError::transport(format!("SearxNG JSON 解析失败: {error}"))) } fn to_tool_invocation( tool_wire: &RuntimeToolInvocationWire, ) -> Result { Ok(ToolInvocation { tool: tool_wire.tool.clone(), kind: parse_invocation_kind(tool_wire.kind.as_str())?, mode: parse_tool_mode(tool_wire.mode.as_deref()).unwrap_or(ToolExecutionMode::Plan), args_json: serde_json::to_string(&tool_wire.args_json).map_err(|error| { BridgeError::validation(format!("tool args_json 序列化失败: {error}")) })?, }) } fn parse_invocation_kind(value: &str) -> Result { match value { "command" => Ok(InvocationKind::Command), "query" => Ok(InvocationKind::Query), "job" => Ok(InvocationKind::Job), other => Err(BridgeError::validation(format!( "未知 tool invocation kind: {other}" ))), } } fn parse_tool_mode(value: Option<&str>) -> Result { match value.unwrap_or("plan") { "plan" => Ok(ToolExecutionMode::Plan), "result" => Ok(ToolExecutionMode::Result), "explain-plan" => Ok(ToolExecutionMode::ExplainPlan), other => Err(BridgeError::validation(format!( "未知 tool execution mode: {other}" ))), } } fn build_tool_payload_json( context_wire: &RuntimeBridgeContextWire, invocation: &ToolInvocation, ) -> String { json!({ "kind": "tool", "tool": invocation.tool, "invocation_kind": invocation_kind_label(&invocation.kind), "mode": core_protocol::tool_mode_label(&invocation.mode), "request_id": context_wire.request_id, "trace_id": context_wire.trace_id, "workspace_id": context_wire.workspace_id, "actor": { "type": context_wire.actor.actor_type, "id": context_wire.actor.actor_id, "session_id": context_wire.actor.session_id, }, "source": { "channel": context_wire.source.channel, "client": context_wire.source.client, }, "validate_only": context_wire.validate_only, "dry_run": context_wire.dry_run, }) .to_string() } fn build_tool_plan_steps( context: &BridgeContext, tool_wire: &RuntimeToolInvocationWire, invocation: &ToolInvocation, ) -> Result, BridgeError> { if invocation.tool == "search_web" { return Ok(vec![RuntimeToolPlanStep { kind: "external".into(), name: "search_web".into(), function_name: None, description: "在 Rust runtime 内直接请求 SearxNG 并归一化搜索结果".into(), args_json: tool_wire.args_json.clone(), }]); } if invocation.tool == "image_read" { return Ok(vec![RuntimeToolPlanStep { kind: "transport".into(), name: "media_assets.resolve".into(), function_name: None, description: "通过 transport 读取附件/媒体行,再由 Rust runtime 统一归一化 OCR 结果" .into(), args_json: tool_wire.args_json.clone(), }]); } if invocation.tool == "slash_run" { return Ok(vec![RuntimeToolPlanStep { kind: "transform".into(), name: "slash_run".into(), function_name: None, description: "在 Rust runtime 内解析斜杠命令并生成统一执行语义,TS 只保留最小 transport 写壳" .into(), args_json: tool_wire.args_json.clone(), }]); } if invocation.tool == "docs_search" { return Ok(vec![ RuntimeToolPlanStep { kind: "transport".into(), name: "docs_search.dataset".into(), function_name: None, description: "通过 transport 拉取文档、导图、表格与 OCR 搜索数据集,再交由 Rust runtime 统一排序。".into(), args_json: tool_wire.args_json.clone(), }, RuntimeToolPlanStep { kind: "transform".into(), name: "docs_search".into(), function_name: None, description: "在 Rust runtime 内复用统一搜索评估器,输出跨页文档搜索结果。".into(), args_json: tool_wire.args_json.clone(), }, ]); } if invocation.tool == "docs_read" { return Ok(vec![ RuntimeToolPlanStep { kind: "transport".into(), name: "docs_read.document".into(), function_name: None, description: "通过 transport 读取目标文档 meta/content,再交由 Rust runtime 统一裁剪正文与返回结构。".into(), args_json: tool_wire.args_json.clone(), }, RuntimeToolPlanStep { kind: "transform".into(), name: "docs_read".into(), function_name: None, description: "在 Rust runtime 内标准化跨页文档读取结果。".into(), args_json: tool_wire.args_json.clone(), }, ]); } if invocation.tool.starts_with("onlyoffice_") { let description = match invocation.tool.as_str() { "onlyoffice_session_resolve" => { "解析 OnlyOffice 的附件、页面与用户上下文,生成稳定 session/asset 边界" } "onlyoffice_sign" => { "在 Rust adapter 内为 OnlyOffice config/document/editorConfig 生成 JWT 签名" } "onlyoffice_prepare_proxy" => { "在 Rust adapter 内校验 proxy 目标、重写回源地址并生成上游请求头" } "onlyoffice_prepare_callback" => { "在 Rust adapter 内解析 callback 状态、下载地址和 session 边界" } "onlyoffice_prepare_forcesave" => { "在 Rust adapter 内生成 forcesave 请求序列与 token 负载" } _ => "执行 OnlyOffice 对象边界适配", }; let mut steps = vec![RuntimeToolPlanStep { kind: "transform".into(), name: invocation.tool.clone(), function_name: None, description: description.into(), args_json: tool_wire.args_json.clone(), }]; if invocation.tool == "onlyoffice_prepare_callback" { steps.push(RuntimeToolPlanStep { kind: "write".into(), name: "media.assets.replace_storage".into(), function_name: Some("mediaAssets:replaceStorageFromUpload".into()), description: "callback 下载并上传文件后,会继续通过统一 bridge 命令写回附件 storage 绑定" .into(), args_json: json!({ "assetId": read_string_value(&tool_wire.args_json, "assetId"), "documentId": read_string_value(&tool_wire.args_json, "documentId"), }), }); } return Ok(steps); } if invocation.tool.starts_with("bridge_") { let (query_name, description) = match invocation.tool.as_str() { "bridge_request_get" => ( "bridge.request.get", "按 request_id 回查统一 command log 与 domain event 视图", ), "bridge_trace_get" => ( "bridge.trace.get", "按 trace_id 回查统一 command log 与 domain event 视图", ), "bridge_command_get" => ( "bridge.command.get", "按 command_id 回查统一 command log 与 domain event 视图", ), other => { return Err(BridgeError::validation(format!("未知观测工具: {other}"))); } }; let query = QueryEnvelope { name: query_name.into(), payload: tool_wire.args_json.clone(), }; let request = build_query_request(context, &query)?; return Ok(vec![RuntimeToolPlanStep { kind: "query".into(), name: query_name.into(), function_name: Some(request.function_name), description: description.into(), args_json: tool_wire.args_json.clone(), }]); } if matches!(invocation.tool.as_str(), "event_replay" | "index_rebuild") { let description = if invocation.tool == "event_replay" { "在 Rust runtime 内按事件流回放统一观测结果,并返回回放后的 cursor 摘要" } else { "在 Rust runtime 内基于事件流重建全文索引批次与 cursor 摘要" }; return Ok(vec![RuntimeToolPlanStep { kind: "job".into(), name: invocation.tool.clone(), function_name: None, description: description.into(), args_json: tool_wire.args_json.clone(), }]); } if invocation.tool.starts_with("mindmap_") { if invocation.tool == "mindmap_empty_trash" { return Ok(vec![RuntimeToolPlanStep { kind: "validate".into(), name: "mindmap_empty_trash".into(), function_name: None, description: "校验清空导图回收站所需的工作区对象边界".into(), args_json: tool_wire.args_json.clone(), }]); } if invocation.tool == "mindmap_outline_to_mindmap" { return Ok(vec![RuntimeToolPlanStep { kind: "transform".into(), name: "mindmap_outline_to_mindmap".into(), function_name: None, description: "在 Rust runtime 内把结构化大纲转换成思维导图树".into(), args_json: tool_wire.args_json.clone(), }]); } if invocation.tool == "mindmap_expand_node" { return Ok(vec![RuntimeToolPlanStep { kind: "transform".into(), name: "mindmap_expand_node".into(), function_name: None, description: "在 Rust runtime 内归一化补完候选并应用到当前导图".into(), args_json: tool_wire.args_json.clone(), }]); } let document_id = resolve_document_id(&tool_wire.args_json, tool_wire.target.as_ref())?; let workspace_id = resolve_workspace_id(context, &tool_wire.args_json, tool_wire.target.as_ref()); let mindmap_id = resolve_mindmap_id(&tool_wire.args_json, tool_wire.target.as_ref())?; let mindmap_query = QueryEnvelope { name: "mindmaps.get".into(), payload: GetMindmap { document_id: document_id.clone(), mindmap_id: mindmap_id.clone(), workspace_id, }, }; let mindmap_request = build_query_request(context, &mindmap_query)?; let mut steps = vec![RuntimeToolPlanStep { kind: "query".into(), name: "mindmaps.get".into(), function_name: Some(mindmap_request.function_name), description: "读取当前思维导图树,供工具执行使用".into(), args_json: json!({ "docId": document_id, "mindmapId": mindmap_id, }), }]; if invocation.tool == "mindmap_put" { steps.push(RuntimeToolPlanStep { kind: "write".into(), name: "mindmaps.put".into(), function_name: Some("mindmaps:put".into()), description: "用完整思维导图树覆盖当前导图".into(), args_json: tool_wire.args_json.clone(), }); } else if invocation.tool == "mindmap_apply_ops" { steps.push(RuntimeToolPlanStep { kind: "transform".into(), name: "mindmap_apply_ops".into(), function_name: None, description: "在 Rust runtime 内应用 MindmapOp 列表并返回新树".into(), args_json: tool_wire.args_json.clone(), }); } else if invocation.tool == "mindmap_get_subtree" { steps.push(RuntimeToolPlanStep { kind: "inspect".into(), name: "mindmap_get_subtree".into(), function_name: None, description: "在 Rust runtime 内裁剪指定 uid 的子树摘要".into(), args_json: tool_wire.args_json.clone(), }); } else if invocation.mode == ToolExecutionMode::ExplainPlan { steps.push(RuntimeToolPlanStep { kind: "inspect".into(), name: invocation.tool.clone(), function_name: None, description: "解释该导图工具将如何读取并整理当前树结构".into(), args_json: tool_wire.args_json.clone(), }); } return Ok(steps); } let document_id = resolve_document_id(&tool_wire.args_json, tool_wire.target.as_ref())?; let workspace_id = resolve_workspace_id(context, &tool_wire.args_json, tool_wire.target.as_ref()); let content_query = QueryEnvelope { name: "documents.content.get".into(), payload: core_protocol::GetPageContent { page_id: document_id.clone(), workspace_id: workspace_id.clone(), }, }; let content_request = build_query_request(context, &content_query)?; let mut steps = vec![RuntimeToolPlanStep { kind: "query".into(), name: "documents.content.get".into(), function_name: Some(content_request.function_name), description: "读取当前文档快照,供工具执行使用".into(), args_json: json!({ "id": document_id, }), }]; if invocation.tool == "doc_insert_blocks" { steps.push(RuntimeToolPlanStep { kind: "transform".into(), name: "doc_insert_blocks".into(), function_name: None, description: "在 Rust runtime 内按 before/after block 位置插入块快照".into(), args_json: tool_wire.args_json.clone(), }); } else if invocation.tool == "doc_replace_range" { steps.push(RuntimeToolPlanStep { kind: "transform".into(), name: "doc_replace_range".into(), function_name: None, description: "在 Rust runtime 内替换目标块文本并返回新快照".into(), args_json: tool_wire.args_json.clone(), }); } else if invocation.mode == ToolExecutionMode::ExplainPlan { steps.push(RuntimeToolPlanStep { kind: "inspect".into(), name: invocation.tool.clone(), function_name: None, description: "解释该工具将如何读取并整理当前文档快照".into(), args_json: tool_wire.args_json.clone(), }); } Ok(steps) } fn target_to_json(target: &RuntimeTargetWire) -> Value { json!({ "workspaceId": target.workspace_id, "pageId": target.page_id, "blockId": target.block_id, }) } fn resolve_document_id( args: &Value, target: Option<&RuntimeTargetWire>, ) -> Result { if let Some(value) = read_string_value(args, "documentId") { return Ok(value); } if let Some(value) = read_string_value(args, "pageId") { return Ok(value); } if let Some(target) = target { if let Some(value) = target.page_id.clone() { if !value.trim().is_empty() { return Ok(value); } } } Err(BridgeError::validation( "tool 缺少 documentId/pageId 或 target.pageId", )) } fn resolve_mindmap_id( args: &Value, target: Option<&RuntimeTargetWire>, ) -> Result { if let Some(value) = read_string_value(args, "mindmapId") { return Ok(value); } if let Some(value) = read_string_value(args, "attachmentId") { return Ok(value); } if let Some(target) = target { if let Some(value) = target.block_id.clone() { if !value.trim().is_empty() { return Ok(value); } } } Err(BridgeError::validation( "tool 缺少 mindmapId/attachmentId 或 target.blockId", )) } fn resolve_workspace_id( context: &BridgeContext, args: &Value, target: Option<&RuntimeTargetWire>, ) -> Option { read_string_value(args, "workspaceId") .or_else(|| target.and_then(|value| value.workspace_id.clone())) .or_else(|| context.workspace_id.clone()) } fn infer_tool_source(data: &Value) -> String { data.as_object() .and_then(|map| map.get("source")) .and_then(Value::as_str) .filter(|value| !value.trim().is_empty()) .unwrap_or("runtime") .to_string() } fn merge_tool_input(args: &Value, data: &Value) -> Value { let mut merged = serde_json::Map::new(); if let Some(map) = data.as_object() { for (key, value) in map { merged.insert(key.clone(), value.clone()); } } if let Some(map) = args.as_object() { for (key, value) in map { merged.insert(key.clone(), value.clone()); } } Value::Object(merged) } fn parse_slash_command_payload(args: &Value) -> Result { let raw_text = read_string_value(args, "text").unwrap_or_default(); let command = read_string_value(args, "command").unwrap_or_default(); let params = args .as_object() .and_then(|map| map.get("params")) .cloned() .unwrap_or(Value::Null); if raw_text.starts_with('/') { let segments = raw_text .split_whitespace() .map(str::trim) .filter(|value| !value.is_empty()) .collect::>(); let head = segments.first().copied().unwrap_or_default(); let rest = raw_text[head.len()..].trim(); if matches!(head, "/new" | "/new-doc" | "/newdoc") { if rest.is_empty() { return Err(BridgeError::validation("用法:/new <标题>")); } return Ok(json!({ "ok": true, "command": "new_doc", "params": { "title": rest, "parentId": Value::Null, "workspaceId": Value::Null, } })); } if matches!(head, "/rename" | "/rename-doc" | "/renamedoc") { let document_id = segments.get(1).copied().unwrap_or_default(); let title = segments .iter() .skip(2) .copied() .collect::>() .join(" ") .trim() .to_string(); if document_id.is_empty() || title.is_empty() { return Err(BridgeError::validation( "用法:/rename <新标题>", )); } return Ok(json!({ "ok": true, "command": "rename_doc", "params": { "documentId": document_id, "title": title, } })); } return Err(BridgeError::validation(format!("未知命令:{head}"))); } if command == "new_doc" { let title = params .get("title") .and_then(Value::as_str) .map(str::trim) .unwrap_or_default(); if title.is_empty() { return Err(BridgeError::validation("缺少标题")); } let parent_id = params .get("parentId") .and_then(Value::as_str) .filter(|value| !value.trim().is_empty()); let workspace_id = params .get("workspaceId") .and_then(Value::as_str) .filter(|value| !value.trim().is_empty()); return Ok(json!({ "ok": true, "command": "new_doc", "params": { "title": title, "parentId": parent_id, "workspaceId": workspace_id, } })); } if command == "rename_doc" { let document_id = params .get("documentId") .and_then(Value::as_str) .map(str::trim) .unwrap_or_default(); let title = params .get("title") .and_then(Value::as_str) .map(str::trim) .unwrap_or_default(); if document_id.is_empty() || title.is_empty() { return Err(BridgeError::validation("缺少 documentId 或 title")); } return Ok(json!({ "ok": true, "command": "rename_doc", "params": { "documentId": document_id, "title": title, } })); } Err(BridgeError::validation("缺少 text(以 / 开头)或 command")) } fn normalize_bridge_observability_result( data: &Value, query_name: &str, ) -> Result { let scope_key = match query_name { "bridge.request.get" => "request_id", "bridge.trace.get" => "trace_id", "bridge.command.get" => "command_id", "bridge.workspace.overview" => "workspace_id", other => { return Err(BridgeError::validation(format!( "未知观测查询类型: {other}" ))); } }; let scope_value = read_string_value(data, scope_key) .or_else(|| { data.as_object() .and_then(|map| { map.get(match scope_key { "request_id" => "requestId", "trace_id" => "traceId", "workspace_id" => "workspaceId", _ => "commandId", }) }) .and_then(Value::as_str) .map(str::to_string) }) .unwrap_or_default(); let command_logs = sort_bridge_entries_by_time( data.as_object() .and_then(|map| map.get("command_logs").or_else(|| map.get("commandLogs"))) .and_then(Value::as_array) .cloned() .unwrap_or_default(), ); let domain_events = sort_bridge_entries_by_time( data.as_object() .and_then(|map| map.get("domain_events").or_else(|| map.get("domainEvents"))) .and_then(Value::as_array) .cloned() .unwrap_or_default(), ); let mut payload = serde_json::Map::new(); payload.insert(scope_key.to_string(), Value::String(scope_value)); payload.insert("command_logs".into(), Value::Array(command_logs.clone())); payload.insert("domain_events".into(), Value::Array(domain_events.clone())); payload.insert( "counts".into(), json!({ "command_logs": command_logs.len(), "domain_events": domain_events.len(), }), ); payload.insert( "generated_at".into(), data.as_object() .and_then(|map| map.get("generated_at").or_else(|| map.get("generatedAt"))) .cloned() .unwrap_or(Value::Null), ); if query_name == "bridge.workspace.overview" { let filters = data .as_object() .and_then(|map| map.get("filters")) .cloned() .unwrap_or(Value::Null); let next_cursor = data .as_object() .and_then(|map| map.get("next_cursor").or_else(|| map.get("nextCursor"))) .cloned() .unwrap_or(Value::Null); let has_more = data .as_object() .and_then(|map| map.get("has_more").or_else(|| map.get("hasMore"))) .cloned() .unwrap_or(Value::Bool(false)); payload.insert("filters".into(), filters); payload.insert("next_cursor".into(), next_cursor); payload.insert("has_more".into(), has_more); } Ok(Value::Object(payload)) } fn sort_bridge_entries_by_time(mut entries: Vec) -> Vec { entries.sort_by(|left, right| bridge_entry_time(right).cmp(&bridge_entry_time(left))); entries } fn bridge_entry_time(entry: &Value) -> String { read_string_value(entry, "created_at") .or_else(|| read_string_value(entry, "createdAt")) .or_else(|| read_string_value(entry, "finished_at")) .or_else(|| read_string_value(entry, "finishedAt")) .unwrap_or_default() } fn execute_event_replay(args: &Value, data: &Value) -> Result { let workspace_id = read_required_string_field(args, "workspaceId")?; let cursor = build_replay_cursor(args, &workspace_id); let events = parse_domain_events_from_value(data)?; let projector = MinimalWorkspaceProjector; let result = rebuild_from_events(&projector, &events, &cursor); Ok(json!({ "ok": true, "workspaceId": workspace_id, "canReplay": can_rebuild_from_events(&cursor), "replayedEventIds": result.batches.iter().map(|batch| batch.source_event_id.clone()).collect::>(), "batchCount": result.batches.len(), "cursor": index_cursor_to_json(&result.cursor), })) } fn execute_index_rebuild(args: &Value, data: &Value) -> Result { let workspace_id = read_required_string_field(args, "workspaceId")?; let cursor = build_replay_cursor(args, &workspace_id); let events = parse_domain_events_from_value(data)?; let projector = MinimalWorkspaceProjector; let result = rebuild_from_events(&projector, &events, &cursor); Ok(json!({ "ok": true, "workspaceId": workspace_id, "indexedObjectKinds": index_fts::supported_index_objects().into_iter().map(|kind| format!("{kind:?}")).collect::>(), "cursor": index_cursor_to_json(&result.cursor), "batchCount": result.batches.len(), "documentCount": result.batches.iter().map(|batch| batch.documents.len()).sum::(), "batches": result.batches.iter().map(|batch| { json!({ "workspaceId": batch.workspace_id, "sourceEventId": batch.source_event_id, "documents": batch.documents.iter().map(|doc| { json!({ "workspaceId": doc.workspace_id, "entityKind": format!("{:?}", doc.entity_kind), "entityId": doc.entity_id, "parentId": doc.parent_id, "title": doc.title, "sourceEventId": doc.source_event_id, "revision": doc.revision, }) }).collect::>(), }) }).collect::>(), })) } fn build_replay_cursor(args: &Value, workspace_id: &str) -> IndexCursor { IndexCursor { workspace_id: workspace_id.to_string(), last_processed_event_id: read_optional_string_field(args, "lastProcessedEventId") .unwrap_or_else(|| "evt_bootstrap".into()), last_processed_at: read_optional_string_field(args, "lastProcessedAt") .unwrap_or_else(|| "1970-01-01T00:00:00Z".into()), } } fn parse_domain_events_from_value(data: &Value) -> Result, BridgeError> { let rows = if let Some(events) = data .as_object() .and_then(|map| map.get("events")) .and_then(Value::as_array) { events.clone() } else if let Some(events) = data.as_array() { events.clone() } else if let Some(events) = data .as_object() .and_then(|map| map.get("domain_events").or_else(|| map.get("domainEvents"))) .and_then(Value::as_array) { events.clone() } else { Vec::new() }; rows.into_iter() .map(|row| parse_domain_event(&row)) .collect() } fn parse_domain_event(row: &Value) -> Result { let event_id = read_required_string_field(row, "event_id") .or_else(|_| read_required_string_field(row, "id"))?; let workspace_id = read_required_string_field(row, "workspace_id") .or_else(|_| read_required_string_field(row, "workspaceId"))?; let aggregate_type = read_required_string_field(row, "aggregate_type") .or_else(|_| read_required_string_field(row, "aggregateType"))?; let aggregate_id = read_required_string_field(row, "aggregate_id") .or_else(|_| read_required_string_field(row, "aggregateId"))?; let event_type = read_required_string_field(row, "event_type") .or_else(|_| read_required_string_field(row, "eventType"))?; let event_version = read_u64_field(row, "event_version") .or_else(|| read_u64_field(row, "eventVersion")) .unwrap_or(1) as u32; let payload = row .as_object() .and_then(|map| map.get("payload")) .cloned() .unwrap_or_else(|| row.get("payload_json").cloned().unwrap_or(Value::Null)); let payload_json = if payload.is_string() { payload.as_str().unwrap_or("").to_string() } else { serde_json::to_string(&payload).map_err(|error| { BridgeError::transport(format!("领域事件 payload 序列化失败: {error}")) })? }; let created_at = read_required_string_field(row, "created_at") .or_else(|_| read_required_string_field(row, "createdAt"))?; let command_log_id = read_required_string_field(row, "command_log_id") .or_else(|_| read_required_string_field(row, "commandLogId"))?; let actor_type = read_required_string_field(row, "actor_type") .or_else(|_| read_required_string_field(row, "actorType"))?; let trace_id = read_required_string_field(row, "trace_id") .or_else(|_| read_required_string_field(row, "traceId"))?; let request_id = read_required_string_field(row, "request_id") .or_else(|_| read_required_string_field(row, "requestId"))?; let command_id = read_required_string_field(row, "command_id") .or_else(|_| read_required_string_field(row, "commandId"))?; Ok(DomainEventRecord { event_id, workspace_id, aggregate_type, aggregate_id, event_type, event_version, payload_json, command_log_id, actor_type, created_at: Timestamp::new(created_at), status: event_log::EventStatus::Committed, trace_id, request_id, command_id, }) } fn index_cursor_to_json(cursor: &IndexCursor) -> Value { json!({ "workspaceId": cursor.workspace_id, "lastProcessedEventId": cursor.last_processed_event_id, "lastProcessedAt": cursor.last_processed_at, }) } fn parse_tool_input(args: &Value, data: &Value) -> Result where T: DeserializeOwned, { serde_json::from_value(merge_tool_input(args, data)) .map_err(|error| BridgeError::validation(format!("tool 输入反序列化失败: {error}"))) } fn normalize_blocks_from_value(data: &Value) -> Vec { if let Some(array) = data.as_array() { return array.clone(); } if let Some(map) = data.as_object() { if let Some(array) = map.get("blocks").and_then(Value::as_array) { return array.clone(); } if let Some(content) = map.get("content") { return normalize_blocks_from_value(content); } } vec![] } fn read_trimmed_string_from_map( map: &serde_json::Map, keys: &[&str], ) -> Option { for key in keys { let value = map .get(*key) .and_then(Value::as_str) .map(str::trim) .unwrap_or(""); if !value.is_empty() { return Some(value.to_string()); } } None } fn read_trimmed_string_field(value: &Value, keys: &[&str]) -> Option { value .as_object() .and_then(|map| read_trimmed_string_from_map(map, keys)) } fn normalize_page_subtree_projection_id(document_id: &str) -> String { format!("kernel_projection:page_tree:{document_id}") } fn pick_first_text(values: &[Option]) -> String { for value in values { if let Some(value) = value .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) { return value.to_string(); } } String::new() } fn normalize_snippet(value: &str) -> String { value .split_whitespace() .collect::>() .join(" ") .chars() .take(220) .collect() } fn clamp_heading_level(value: Option) -> u32 { let level = value.unwrap_or(1); level.clamp(1, 5) as u32 } fn get_page_block_children(value: Option<&Value>) -> Vec { value.and_then(Value::as_array).cloned().unwrap_or_default() } fn get_inline_text(value: &Value) -> String { if let Some(text) = value.as_str() { return text.to_string(); } let Some(nodes) = value.as_array() else { return String::new(); }; let mut text = String::new(); for node in nodes { if let Some(fragment) = node.as_str() { text.push_str(fragment); continue; } let Some(map) = node.as_object() else { continue; }; if map.get("type").and_then(Value::as_str) == Some("link") { if let Some(content) = map.get("content") { text.push_str(&get_inline_text(content)); } continue; } if let Some(fragment) = map.get("text").and_then(Value::as_str) { text.push_str(fragment); } } text } fn get_block_snippet(block: &Value) -> String { let inline_text = block .as_object() .and_then(|map| map.get("content")) .map(get_inline_text) .map(|value| normalize_snippet(&value)) .unwrap_or_default(); if !inline_text.is_empty() { return inline_text; } let props = block .as_object() .and_then(|map| map.get("props")) .and_then(Value::as_object); let fallback = props.map(|props| { pick_first_text(&[ read_trimmed_string_from_map(props, &["title"]), read_trimmed_string_from_map(props, &["caption"]), read_trimmed_string_from_map(props, &["summary"]), read_trimmed_string_from_map(props, &["fileName", "file_name"]), read_trimmed_string_from_map(props, &["name"]), read_trimmed_string_from_map(props, &["alt"]), read_trimmed_string_from_map(props, &["status"]), ]) }); normalize_snippet(fallback.as_deref().unwrap_or_default()) } fn get_block_display_title(block: &Value, snippet: &str) -> Option { let block_type = read_trimmed_string_field(block, &["type"]); let props = block .as_object() .and_then(|map| map.get("props")) .and_then(Value::as_object); let title = props.and_then(|props| read_trimmed_string_from_map(props, &["title"])); let caption = props.and_then(|props| read_trimmed_string_from_map(props, &["caption"])); let file_name = props.and_then(|props| read_trimmed_string_from_map(props, &["fileName", "file_name"])); match block_type.as_deref() { Some("heading") => Some(if snippet.is_empty() { "未命名标题".into() } else { snippet.into() }), Some("pageReference") => Some(pick_first_text(&[ title, Some(snippet.into()), Some("页面引用".into()), ])), Some("blockReference") => Some(pick_first_text(&[ title, Some(snippet.into()), Some("块引用".into()), ])), Some("onlineTable") => Some(pick_first_text(&[ title, Some(snippet.into()), Some("在线表格".into()), ])), Some("mindmap") => Some(pick_first_text(&[ title, Some(snippet.into()), Some("思维导图".into()), ])), Some("media") => Some(pick_first_text(&[ caption, file_name, Some(snippet.into()), Some("附件".into()), ])), Some("codeBlock") => Some(if snippet.is_empty() { "代码块".into() } else { snippet.into() }), Some("advancedTodo") => Some(if snippet.is_empty() { "任务".into() } else { snippet.into() }), Some("quote") => Some(if snippet.is_empty() { "引用".into() } else { snippet.into() }), _ => { if snippet.is_empty() { None } else { Some(snippet.into()) } } } } fn get_document_read_node_type(block_type: Option<&str>) -> DocumentReadNodeType { match block_type { Some("heading") => DocumentReadNodeType::Section, Some("blockReference") | Some("pageReference") => DocumentReadNodeType::ReferenceAnchor, Some("mindmap") => DocumentReadNodeType::Mindmap, _ => DocumentReadNodeType::ContentNode, } } fn get_document_read_evidence_kind(block_type: Option<&str>) -> DocumentReadEvidenceKind { match block_type { Some("heading") => DocumentReadEvidenceKind::Heading, Some("paragraph") => DocumentReadEvidenceKind::Paragraph, Some("bulletListItem") | Some("numberedListItem") | Some("checkListItem") => { DocumentReadEvidenceKind::List } Some("advancedTodo") => DocumentReadEvidenceKind::Todo, Some("quote") => DocumentReadEvidenceKind::Quote, Some("codeBlock") => DocumentReadEvidenceKind::Code, Some("media") => DocumentReadEvidenceKind::Media, Some("pageReference") | Some("blockReference") => DocumentReadEvidenceKind::Reference, Some("onlineTable") => DocumentReadEvidenceKind::Table, Some("mindmap") => DocumentReadEvidenceKind::Mindmap, _ => DocumentReadEvidenceKind::Text, } } fn build_document_page_subtree( document_id: &str, title: Option<&str>, content: &Value, ) -> DocumentReadPageSubtree { let root_node_id = document_id.trim().to_string(); let blocks = normalize_blocks_from_value(content); let root_title = title .map(str::trim) .filter(|value| !value.is_empty()) .unwrap_or("无标题") .to_string(); let root_node = DocumentReadNode { id: root_node_id.clone(), parent_node_id: None, node_type: DocumentReadNodeType::Page, block_id: None, anchor_block_id: None, depth: 0, metadata: DocumentReadNodeMeta { title: Some(root_title.clone()), text_snippet: None, block_type: Some("page".into()), heading_level: None, numbering: None, child_count: blocks.len() as u32, order: 0, path: vec![root_node_id.clone()], }, }; let mut nodes = vec![root_node.clone()]; let mut outline = Vec::::new(); let mut evidence = Vec::::new(); let mut heading_counters = [0u32; 5]; let mut heading_stack = Vec::<(u32, String)>::new(); let mut order = 0u32; let mut max_depth = 0u32; fn walk_blocks( items: &[Value], parent_block_node_id: Option<&str>, depth: u32, path: &[usize], root_node_id: &str, nodes: &mut Vec, outline: &mut Vec, evidence: &mut Vec, heading_counters: &mut [u32; 5], heading_stack: &mut Vec<(u32, String)>, order: &mut u32, max_depth: &mut u32, ) { for (index, block) in items.iter().enumerate() { let block_id = read_trimmed_string_field(block, &["id"]); let mut auto_path = path .iter() .map(|value| value.to_string()) .collect::>(); auto_path.push(index.to_string()); let node_id = block_id .as_ref() .map(|value| format!("block:{value}")) .unwrap_or_else(|| format!("block:auto:{}", auto_path.join("."))); let snippet = get_block_snippet(block); let block_type = read_trimmed_string_field(block, &["type"]); let heading_level = if block_type.as_deref() == Some("heading") { let level = block .as_object() .and_then(|map| map.get("props")) .and_then(Value::as_object) .and_then(|props| props.get("level")) .and_then(Value::as_u64); Some(clamp_heading_level(level)) } else { None }; let mut parent_node_id = parent_block_node_id .map(str::to_string) .or_else(|| heading_stack.last().map(|(_, node_id)| node_id.clone())) .unwrap_or_else(|| root_node_id.to_string()); let mut numbering = None::; if let Some(level) = heading_level { while heading_stack .last() .map(|(current_level, _)| *current_level >= level) .unwrap_or(false) { heading_stack.pop(); } parent_node_id = heading_stack .last() .map(|(_, node_id)| node_id.clone()) .or_else(|| parent_block_node_id.map(str::to_string)) .unwrap_or_else(|| root_node_id.to_string()); let index = (level - 1) as usize; heading_counters[index] += 1; for counter in heading_counters.iter_mut().skip(index + 1) { *counter = 0; } numbering = Some( heading_counters .iter() .take(level as usize) .copied() .filter(|value| *value > 0) .map(|value| value.to_string()) .collect::>() .join("."), ); } *order += 1; let children = get_page_block_children(block.as_object().and_then(|map| map.get("children"))); let node = DocumentReadNode { id: node_id.clone(), parent_node_id: Some(parent_node_id), node_type: get_document_read_node_type(block_type.as_deref()), block_id: block_id.clone(), anchor_block_id: block_id.clone(), depth: depth + 1, metadata: DocumentReadNodeMeta { title: get_block_display_title(block, &snippet), text_snippet: if snippet.is_empty() { None } else { Some(snippet.clone()) }, block_type: block_type.clone(), heading_level, numbering: numbering.clone(), child_count: children.len() as u32, order: *order, path: std::iter::once(root_node_id.to_string()) .chain(path.iter().map(|value| value.to_string())) .chain(std::iter::once(index.to_string())) .collect(), }, }; nodes.push(node.clone()); *max_depth = (*max_depth).max(node.depth); if let Some(level) = heading_level { outline.push(DocumentReadOutlineEntry { id: block_id.clone().unwrap_or_else(|| node.id.clone()), node_id: node.id.clone(), anchor_block_id: block_id.clone(), title: node .metadata .title .clone() .unwrap_or_else(|| "未命名标题".into()), level, numbering: numbering.unwrap_or_default(), }); heading_stack.push((level, node.id.clone())); } if !snippet.is_empty() { evidence.push(DocumentReadEvidenceItem { id: format!("evidence:{}", node.id), node_id: node.id.clone(), block_id: block_id.clone(), kind: get_document_read_evidence_kind(block_type.as_deref()), snippet, }); } if !children.is_empty() { let mut next_path = path.to_vec(); next_path.push(index); walk_blocks( &children, Some(node.id.as_str()), depth + 1, &next_path, root_node_id, nodes, outline, evidence, heading_counters, heading_stack, order, max_depth, ); } } } walk_blocks( &blocks, None, 0, &[], &root_node_id, &mut nodes, &mut outline, &mut evidence, &mut heading_counters, &mut heading_stack, &mut order, &mut max_depth, ); if !root_title.is_empty() { evidence.insert( 0, DocumentReadEvidenceItem { id: format!("evidence:{root_node_id}"), node_id: root_node_id.clone(), block_id: None, kind: DocumentReadEvidenceKind::Page, snippet: root_title, }, ); } let block_count = nodes.len().saturating_sub(1) as u32; let heading_count = outline.len() as u32; let evidence_count = evidence.len() as u32; DocumentReadPageSubtree { projection_id: normalize_page_subtree_projection_id(&root_node_id), projection: "page_tree".into(), root_node_id: root_node_id.clone(), root_node, subtree: DocumentReadSubtree { root_node_id: root_node_id.clone(), nodes, }, outline, evidence: evidence.clone(), stats: DocumentReadStats { block_count, heading_count, evidence_count, max_depth, }, } } fn build_document_content_result( data: &Value, document_id: &str, _workspace_id: Option<&str>, ) -> Result { if data.is_null() { return Err(BridgeError::not_found( "documents.content.get 未返回文档内容", )); } let content = data .as_object() .and_then(|map| map.get("content")) .cloned() .unwrap_or(Value::Null); let revision = data .as_object() .and_then(|map| map.get("revision")) .and_then(Value::as_u64) .unwrap_or(0); let conflict_detection_key = read_trimmed_string_field(data, &["conflict_detection_key", "conflictDetectionKey"]) .unwrap_or_else(|| format!("{document_id}:{revision}")); let title = read_trimmed_string_field(data, &["title"]); let page_subtree = build_document_page_subtree(document_id, title.as_deref(), &content); Ok(DocumentContentResult { content, revision, conflict_detection_key, title, page_subtree, }) } fn build_page_aggregate_projection_result( data: &Value, document_id: &str, workspace_id: Option<&str>, source: PageAggregateSource, ) -> Result { if data.is_null() { return Err(BridgeError::not_found( "page.aggregate.get 未返回页面聚合数据", )); } let meta = data.get("meta").unwrap_or(data); let content_result = data .get("content") .filter(|value| { value.get("content").is_some() || value.get("revision").is_some() || value.get("pageSubtree").is_some() || value.get("page_subtree").is_some() }) .unwrap_or(data); let resolved_document_id = read_trimmed_string_field(meta, &["id", "documentId"]) .unwrap_or_else(|| document_id.to_string()); let resolved_workspace_id = read_trimmed_string_field(meta, &["workspace_id", "workspaceId"]) .or_else(|| workspace_id.map(ToOwned::to_owned)) .unwrap_or_else(|| "default".into()); let title = read_trimmed_string_field(meta, &["title"]).unwrap_or_else(|| "无标题".into()); let updated_at = read_trimmed_string_field(meta, &["updated_at", "updatedAt"]); let parent_id = read_trimmed_string_field(meta, &["parent_id", "parentId"]); let content = content_result .get("content") .cloned() .unwrap_or_else(|| Value::Array(vec![])); let revision = content_result .get("revision") .cloned() .unwrap_or(Value::Null); let conflict_detection_key = content_result .get("conflictDetectionKey") .or_else(|| content_result.get("conflict_detection_key")) .cloned() .unwrap_or(Value::Null); let page_subtree = content_result .get("pageSubtree") .or_else(|| content_result.get("page_subtree")) .cloned() .unwrap_or_else(|| json!({ "rootNodeId": resolved_document_id })); let page_options = PageOptions { wide_layout: bool_field(meta, "wide_layout") .or_else(|| bool_field(meta, "wideLayout")) .unwrap_or(false), small_text: bool_field(meta, "use_small_text") .or_else(|| bool_field(meta, "smallText")) .unwrap_or(false), layout_density: read_trimmed_string_field(meta, &["layout_density", "layoutDensity"]) .unwrap_or_else(|| "normal".into()), show_heading_numbers: bool_field(meta, "show_heading_numbers") .or_else(|| bool_field(meta, "showHeadingNumbers")) .unwrap_or(true), show_toc: bool_field(meta, "show_toc") .or_else(|| bool_field(meta, "showToc")) .unwrap_or(false), show_structure: bool_field(meta, "show_structure") .or_else(|| bool_field(meta, "showStructure")) .unwrap_or(false), protect_editing: bool_field(meta, "protect_editing") .or_else(|| bool_field(meta, "protectEditing")) .unwrap_or(false), show_word_count: bool_field(meta, "show_word_count") .or_else(|| bool_field(meta, "showWordCount")) .unwrap_or(true), collapse_backlinks: bool_field(meta, "collapse_backlinks") .or_else(|| bool_field(meta, "collapseBacklinks")) .unwrap_or(false), page_font: read_trimmed_string_field(meta, &["page_font", "pageFont"]) .unwrap_or_else(|| "default".into()), hide_child_pages: bool_field(meta, "hide_child_pages") .or_else(|| bool_field(meta, "hideChildPages")) .unwrap_or(false), show_block_ref_count: bool_field(meta, "show_block_ref_count") .or_else(|| bool_field(meta, "showBlockRefCount")) .unwrap_or(false), embed_default_block_id: meta .get("embed_default_block_id") .or_else(|| meta.get("embedDefaultBlockId")) .cloned() .unwrap_or(Value::Null), }; let layout_options = serde_json::to_value(&page_options) .map_err(|error| BridgeError::transport(format!("PageOptions 序列化失败: {error}")))?; let revision_ref = revision .as_u64() .map(|value| format!("{resolved_document_id}:{value}")); Ok(PageAggregateProjection { schema: PageAggregateProjection::SCHEMA.into(), projection_version: PageAggregateProjection::VERSION, source, page_id: resolved_document_id.clone(), parent_id, title: title.clone(), path: vec![resolved_document_id.clone()], sidebar_tree_membership: vec!["page-tree".into(), "file-tree".into()], body_ref: revision_ref, layout_options, updated_at: updated_at.clone(), identity: PageIdentity { document_id: resolved_document_id, workspace_id: resolved_workspace_id, }, head: PageHead { title, updated_at: updated_at.map(Value::String).unwrap_or(Value::Null), permissions: PagePermissions { read_only: bool_field(meta, "can_edit") .map(|can_edit| !can_edit) .unwrap_or(false), disable_download: bool_field(meta, "disable_download") .or_else(|| bool_field(meta, "disableDownload")) .unwrap_or(false), disable_copy: bool_field(meta, "disable_copy") .or_else(|| bool_field(meta, "disableCopy")) .unwrap_or(false), }, }, layout: PageLayout { page_options }, body: PageBody { content, revision, conflict_detection_key, }, tree: PageTree { page_subtree }, stats: PageStats { word_count: meta.get("word_count").and_then(Value::as_u64).unwrap_or(0), character_count: meta .get("character_count") .and_then(Value::as_u64) .unwrap_or(0), block_count: meta.get("block_count").and_then(Value::as_u64).unwrap_or(0), todo_total: meta .get("todo_total") .or_else(|| meta.get("todo_total_count")) .and_then(Value::as_u64) .unwrap_or(0), todo_done: meta .get("todo_done") .or_else(|| meta.get("todo_done_count")) .and_then(Value::as_u64) .unwrap_or(0), }, }) } fn normalize_mindmap_from_value(data: &Value) -> Result { if data.is_null() { return Ok(default_mindmap_tree()); } let candidate = if let Some(map) = data.as_object() { if (map.contains_key("ok") || map.contains_key("meta") || map.contains_key("source")) && map.get("data").is_some() { map.get("data").cloned().unwrap_or(Value::Null) } else if map.get("data").is_some() && map.get("children").is_some() { data.clone() } else if let Some(nested) = map.get("mindmap") { nested.clone() } else if let Some(nested) = map.get("result") { nested.clone() } else { data.clone() } } else { data.clone() }; let mut tree: MindmapTreeNode = serde_json::from_value(candidate) .map_err(|error| BridgeError::validation(format!("mindmap 数据反序列化失败: {error}")))?; ensure_mindmap_uids(&mut tree); Ok(tree) } fn build_mindmap_projection_result( data: &Value, mindmap_id: &str, ) -> Result { let revision = data .get("revision") .or_else(|| data.get("meta").and_then(|meta| meta.get("revision"))) .and_then(Value::as_u64) .unwrap_or(1); let tree_input = data .get("data") .filter(|value| value.get("data").is_some() || value.get("children").is_some()) .unwrap_or(data); let tree = normalize_mindmap_from_value(tree_input)?; let root_node = tree.data.uid.clone().unwrap_or_else(|| "root".into()); let mut nodes = Vec::new(); let mut edges = Vec::new(); collect_mindmap_projection_rows(&tree, None, &mut nodes, &mut edges); Ok(MindmapProjection { schema: "mnote.mindmap_projection.v1".into(), map_id: mindmap_id.into(), root_node, nodes, edges, layout_hints: data .get("layoutHints") .or_else(|| data.get("layout_hints")) .cloned() .unwrap_or_else(|| json!({ "layout": "right" })), revision, owner: MindmapProjectionOwner::RustKernel, }) } fn collect_mindmap_projection_rows( node: &MindmapTreeNode, parent_id: Option<&str>, nodes: &mut Vec, edges: &mut Vec, ) { let node_id = node .data .uid .clone() .unwrap_or_else(|| format!("node_{}", nodes.len() + 1)); let title = node .data .text .clone() .filter(|value| !value.trim().is_empty()) .unwrap_or_else(|| "未命名节点".into()); if let Some(parent_id) = parent_id { edges.push(MindmapProjectionEdge { edge_id: format!("{parent_id}->{node_id}"), from_node_id: parent_id.into(), to_node_id: node_id.clone(), edge_kind: "mindmap_child".into(), }); } nodes.push(MindmapProjectionNode { node_id: node_id.clone(), title, parent_id: parent_id.map(ToOwned::to_owned), }); for child in &node.children { collect_mindmap_projection_rows(child, Some(&node_id), nodes, edges); } } fn search_documents_canonical_projection(evaluation: SearchDocumentsEvaluation) -> Value { let mut payload = serde_json::to_value(evaluation).unwrap_or_else(|_| { json!({ "enqueueAssetIds": [], "results": [] }) }); if let Some(map) = payload.as_object_mut() { map.insert("projectionOwner".into(), json!("rust-kernel")); if let Some(results) = map.get_mut("results").and_then(Value::as_array_mut) { for result in results { if let Some(result_map) = result.as_object_mut() { result_map.insert("projectionOwner".into(), json!("rust-kernel")); } } } } payload } fn walk_block_summaries(root_blocks: &[Value], max_nodes: usize) -> Vec { let mut queue = VecDeque::new(); for block in root_blocks { queue.push_back((block.clone(), 0usize)); } let mut summaries = Vec::new(); while let Some((block, depth)) = queue.pop_front() { if summaries.len() >= max_nodes { break; } let id = block .as_object() .and_then(|map| map.get("id")) .and_then(Value::as_str) .unwrap_or("") .trim() .to_string(); let block_type = block .as_object() .and_then(|map| map.get("type")) .and_then(Value::as_str) .unwrap_or("unknown") .trim() .to_string(); let children = block .as_object() .and_then(|map| map.get("children")) .and_then(Value::as_array) .cloned() .unwrap_or_default(); if !id.is_empty() { summaries.push(RuntimeBlockSummary { id, block_type, text: extract_inline_text(&block), depth, child_count: children.len(), }); } for child in children { queue.push_back((child, depth + 1)); } } summaries } fn extract_inline_text(block: &Value) -> String { block .as_object() .and_then(|map| map.get("content")) .and_then(Value::as_array) .map(|nodes| { nodes .iter() .filter_map(|node| { node.as_object() .and_then(|map| map.get("text")) .and_then(Value::as_str) }) .collect::() .trim() .to_string() }) .unwrap_or_default() } fn parse_insert_specs(args: &Value) -> Result, BridgeError> { let specs_raw = args .as_object() .and_then(|map| map.get("blocks")) .and_then(Value::as_array) .ok_or_else(|| BridgeError::validation("doc_insert_blocks 缺少 blocks"))?; if specs_raw.is_empty() { return Err(BridgeError::validation("doc_insert_blocks 缺少 blocks")); } if specs_raw.len() > 20 { return Err(BridgeError::validation( "doc_insert_blocks blocks 过多(最多 20)", )); } Ok(specs_raw .iter() .map(|spec| RuntimeInsertSpec { block_type: read_string_value(spec, "type") .filter(|value| value == "heading") .unwrap_or_else(|| "paragraph".into()), text: read_string_value(spec, "text").unwrap_or_default(), level: read_u64_field(spec, "level").unwrap_or(2), }) .collect()) } fn apply_insert_blocks( mut blocks: Vec, after_block_id: Option, before_block_id: Option, specs: Vec, ) -> Result { let created = specs .iter() .map(build_block_from_spec) .collect::>(); let editor_commands = build_insert_editor_commands( &blocks, after_block_id.as_deref(), before_block_id.as_deref(), &created, &specs, ); let inserted = created .iter() .filter_map(|block| read_string_value(block, "id")) .collect::>(); let target_id = before_block_id.clone().or(after_block_id.clone()); match target_id { Some(target_id) => { let inserted_ok = insert_blocks_at_target( &mut blocks, &target_id, before_block_id.is_some(), &created, ); if !inserted_ok { return Err(BridgeError::validation(format!( "未找到 blockId:{target_id}" ))); } } None => blocks.extend(created), } Ok(InsertBlocksResult { inserted, blocks, editor_commands, }) } fn insert_blocks_at_target( blocks: &mut Vec, target_id: &str, before: bool, created: &[Value], ) -> bool { for index in 0..blocks.len() { if read_string_value(&blocks[index], "id").as_deref() == Some(target_id) { let insert_at = if before { index } else { index + 1 }; blocks.splice(insert_at..insert_at, created.iter().cloned()); return true; } if let Some(children) = blocks[index] .as_object_mut() .and_then(|map| map.get_mut("children")) .and_then(Value::as_array_mut) { if insert_blocks_at_target(children, target_id, before, created) { return true; } } } false } fn build_block_from_spec(spec: &RuntimeInsertSpec) -> Value { let id = format!( "rust_tool_block_{}", TOOL_BLOCK_COUNTER.fetch_add(1, Ordering::Relaxed) ); let level = spec.level.clamp(1, 5); let props = if spec.block_type == "heading" { json!({ "level": level }) } else { json!({}) }; json!({ "id": id, "type": spec.block_type, "props": props, "content": [{"type":"text","text": spec.text}], "children": [], }) } fn build_text_content_nodes(text: &str) -> Vec { if text.trim().is_empty() { return vec![]; } vec![ContentNode { payload: ContentNodePayload::Text { text: text.to_string(), marks: vec![], }, attrs: BTreeMap::new(), }] } fn editor_block_type_from_spec(spec: &RuntimeInsertSpec) -> EditorBlockType { match spec.block_type.as_str() { "heading" => EditorBlockType::Heading, _ => EditorBlockType::Paragraph, } } fn build_editor_block_from_spec(spec: &RuntimeInsertSpec, block_id: &str) -> EditorBlock { let mut props = BlockProps::default(); if matches!(editor_block_type_from_spec(spec), EditorBlockType::Heading) { props.heading_level = Some(spec.level.clamp(1, 5) as u8); } EditorBlock { block_id: block_id.to_string(), block_type: editor_block_type_from_spec(spec), props, content_nodes: build_text_content_nodes(&spec.text), child_block_ids: vec![], } } fn collect_block_order(blocks: &[Value], order: &mut Vec) { for block in blocks { if let Some(block_id) = read_string_value(block, "id") { order.push(block_id); } if let Some(children) = block .as_object() .and_then(|map| map.get("children")) .and_then(Value::as_array) { collect_block_order(children, order); } } } fn resolve_insert_anchor( blocks: &[Value], after_block_id: Option<&str>, before_block_id: Option<&str>, ) -> Option { let mut order = Vec::new(); collect_block_order(blocks, &mut order); if let Some(after_block_id) = after_block_id { if order.iter().any(|item| item == after_block_id) { return Some(after_block_id.to_string()); } return None; } if let Some(before_block_id) = before_block_id { let index = order.iter().position(|item| item == before_block_id)?; if index == 0 { return None; } return order.get(index - 1).cloned(); } order.last().cloned() } fn build_insert_editor_commands( blocks: &[Value], after_block_id: Option<&str>, before_block_id: Option<&str>, created: &[Value], specs: &[RuntimeInsertSpec], ) -> Vec { let mut anchor = resolve_insert_anchor(blocks, after_block_id, before_block_id); if anchor.is_none() { return vec![]; } let mut commands = Vec::new(); for (created_block, spec) in created.iter().zip(specs.iter()) { let Some(block_id) = read_string_value(created_block, "id") else { continue; }; let Some(after_block_id) = anchor.clone() else { break; }; commands.push(EditorCommand::InsertBlockAfter(EditorInsertBlockAfter { after_block_id, block: build_editor_block_from_spec(spec, &block_id), })); anchor = Some(block_id); } commands } fn find_block_text(blocks: &[Value], block_id: &str) -> Option { for block in blocks { if read_string_value(block, "id").as_deref() == Some(block_id) { return Some(extract_inline_text(block)); } if let Some(children) = block .as_object() .and_then(|map| map.get("children")) .and_then(Value::as_array) { if let Some(found) = find_block_text(children, block_id) { return Some(found); } } } None } fn build_replace_editor_commands( blocks: &[Value], block_id: &str, text: &str, mode: &str, ) -> Vec { let Some(previous) = find_block_text(blocks, block_id) else { return vec![]; }; let next_text = match mode { "append" => format!("{previous}{text}"), "prepend" => format!("{text}{previous}"), _ => text.to_string(), }; vec![EditorCommand::ReplaceBlock(EditorReplaceBlock { block_id: block_id.to_string(), block_type: None, props: None, content_nodes: Some(build_text_content_nodes(&next_text)), })] } fn apply_replace_range( mut blocks: Vec, block_id: &str, text: &str, mode: &str, ) -> Result, BridgeError> { if !replace_block_text(&mut blocks, block_id, text, mode)? { return Err(BridgeError::validation(format!( "未找到 blockId:{block_id}" ))); } Ok(blocks) } fn replace_block_text( blocks: &mut Vec, block_id: &str, text: &str, mode: &str, ) -> Result { for block in blocks.iter_mut() { if read_string_value(block, "id").as_deref() == Some(block_id) { let previous = extract_inline_text(block); let next_text = match mode { "append" => format!("{previous}{text}"), "prepend" => format!("{text}{previous}"), _ => text.to_string(), }; let object = block .as_object_mut() .ok_or_else(|| BridgeError::validation(format!("block 数据异常:{block_id}")))?; object.insert("content".into(), json!([{"type":"text","text": next_text}])); return Ok(true); } if let Some(children) = block .as_object_mut() .and_then(|map| map.get_mut("children")) .and_then(Value::as_array_mut) { if replace_block_text(children, block_id, text, mode)? { return Ok(true); } } } Ok(false) } fn read_required_string_field(value: &Value, field: &str) -> Result { read_string_value(value, field) .ok_or_else(|| BridgeError::validation(format!("tool 缺少 {field}"))) } fn read_optional_string_field(value: &Value, field: &str) -> Option { read_string_value(value, field) } fn read_string_value(value: &Value, field: &str) -> Option { value .as_object() .and_then(|map| map.get(field)) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string) } fn read_u64_field(value: &Value, field: &str) -> Option { value .as_object() .and_then(|map| map.get(field)) .and_then(Value::as_u64) } fn next_mindmap_uid() -> String { format!( "rust_mindmap_uid_{}", MINDMAP_UID_COUNTER.fetch_add(1, Ordering::Relaxed) ) } fn default_mindmap_tree() -> MindmapTreeNode { let mut root = MindmapTreeNode { data: MindmapNodeData { uid: None, text: Some("中心主题".into()), hyperlink: None, note: None, refs: None, extra: BTreeMap::new(), }, children: vec![], extra: BTreeMap::new(), }; ensure_mindmap_uids(&mut root); root } fn ensure_mindmap_uids(node: &mut MindmapTreeNode) { if node .data .uid .as_ref() .map(|value| value.trim().is_empty()) .unwrap_or(true) { node.data.uid = Some(next_mindmap_uid()); } let text = node.data.text.clone().unwrap_or_else(|| "新节点".into()); node.data.text = Some(text); for child in &mut node.children { ensure_mindmap_uids(child); } } fn strip_html_tags(value: &str) -> String { let mut result = String::with_capacity(value.len()); let mut inside_tag = false; for ch in value.chars() { match ch { '<' => inside_tag = true, '>' => inside_tag = false, _ if !inside_tag => result.push(ch), _ => {} } } result.trim().to_string() } fn trim_text_for_docs_read(value: &str, max_chars: usize) -> String { let taken = value.chars().take(max_chars).collect::(); if value.chars().count() > max_chars { format!("{taken}…") } else { taken } } fn walk_mindmap_summaries(root: &MindmapTreeNode, max_nodes: usize) -> Vec { let mut list = Vec::new(); let mut queue = VecDeque::from([(root, None::, 0usize)]); while let Some((node, parent_uid, depth)) = queue.pop_front() { if list.len() >= max_nodes { break; } let uid = node.data.uid.clone().unwrap_or_default(); let text = strip_html_tags(node.data.text.as_deref().unwrap_or("")); list.push(RuntimeMindmapSummary { uid: uid.clone(), text, parent_uid, depth, child_count: node.children.len(), }); for child in &node.children { queue.push_back((child, Some(uid.clone()), depth + 1)); } } list } fn summarize_mindmap_subtree( root: &MindmapTreeNode, depth_limit: usize, max_nodes: usize, ) -> Vec { let mut list = Vec::new(); let mut queue = VecDeque::from([(root, 0usize)]); while let Some((node, depth)) = queue.pop_front() { if list.len() >= max_nodes { break; } list.push(RuntimeMindmapSubtreeSummary { uid: node.data.uid.clone().unwrap_or_default(), text: strip_html_tags(node.data.text.as_deref().unwrap_or("")), depth, child_count: node.children.len(), }); if depth < depth_limit { for child in &node.children { queue.push_back((child, depth + 1)); } } } list } fn find_mindmap_node_by_uid<'a>( node: &'a MindmapTreeNode, uid: &str, ) -> Option<&'a MindmapTreeNode> { if node.data.uid.as_deref() == Some(uid) { return Some(node); } for child in &node.children { if let Some(found) = find_mindmap_node_by_uid(child, uid) { return Some(found); } } None } fn parse_mindmap_tree_from_args(args: &Value, field: &str) -> Result { let value = args .as_object() .and_then(|map| map.get(field)) .cloned() .ok_or_else(|| BridgeError::validation(format!("tool 缺少 {field}")))?; let mut tree: MindmapTreeNode = serde_json::from_value(value).map_err(|error| { BridgeError::validation(format!("{field} 不是合法的思维导图树: {error}")) })?; ensure_mindmap_uids(&mut tree); Ok(tree) } fn parse_mindmap_ops(args: &Value) -> Result, BridgeError> { let ops_value = args .as_object() .and_then(|map| map.get("ops")) .cloned() .ok_or_else(|| BridgeError::validation("mindmap_apply_ops 缺少 ops"))?; let ops: Vec = serde_json::from_value(ops_value) .map_err(|error| BridgeError::validation(format!("ops 反序列化失败: {error}")))?; if ops.is_empty() { return Err(BridgeError::validation("mindmap_apply_ops 缺少 ops")); } if ops.len() > 80 { return Err(BridgeError::validation( "mindmap_apply_ops ops 过多(最多 80)", )); } Ok(ops) } fn parse_mindmap_outline_items( args: &Value, ) -> Result, BridgeError> { let outline_value = args .as_object() .and_then(|map| map.get("outline")) .cloned() .ok_or_else(|| BridgeError::validation("mindmap_outline_to_mindmap 缺少 outline"))?; let outline: Vec = serde_json::from_value(outline_value) .map_err(|error| BridgeError::validation(format!("outline 反序列化失败: {error}")))?; if outline.is_empty() { return Err(BridgeError::validation( "mindmap_outline_to_mindmap 缺少 outline", )); } if outline.len() > 600 { return Err(BridgeError::validation( "mindmap_outline_to_mindmap outline 过多(最多 600)", )); } Ok(outline) } #[derive(Debug)] struct MindmapOpsResult { applied: usize, errors: Vec, } fn apply_mindmap_ops( root: &mut MindmapTreeNode, ops: &[MindmapOp], ) -> Result { ensure_mindmap_uids(root); let mut applied = 0usize; let mut errors = Vec::new(); for op in ops { let result = match op { MindmapOp::AddChild { parent_uid, node } => add_mindmap_child(root, parent_uid, node), MindmapOp::AddSiblingAfter { target_uid, node } => { add_mindmap_sibling_after(root, target_uid, node) } MindmapOp::UpdateText { uid, text } => update_mindmap_text(root, uid, text), MindmapOp::SetHyperlink { uid, hyperlink } => { set_mindmap_hyperlink(root, uid, hyperlink.clone()) } MindmapOp::SetRefs { uid, refs } => set_mindmap_refs(root, uid, refs.clone()), MindmapOp::AppendNote { uid, markdown } => append_mindmap_note(root, uid, markdown), MindmapOp::DeleteNode { uid } => delete_mindmap_node(root, uid), }; match result { Ok(true) => applied += 1, Ok(false) => errors.push(format!( "未找到可操作节点: {}", describe_mindmap_op_target(op) )), Err(error) => errors.push(error), } } ensure_mindmap_uids(root); Ok(MindmapOpsResult { applied, errors }) } fn describe_mindmap_op_target(op: &MindmapOp) -> String { match op { MindmapOp::AddChild { parent_uid, .. } => format!("parentUid={parent_uid}"), MindmapOp::AddSiblingAfter { target_uid, .. } => format!("targetUid={target_uid}"), MindmapOp::UpdateText { uid, .. } | MindmapOp::SetHyperlink { uid, .. } | MindmapOp::SetRefs { uid, .. } | MindmapOp::AppendNote { uid, .. } | MindmapOp::DeleteNode { uid } => format!("uid={uid}"), } } fn mindmap_node_mut_by_path<'a>( node: &'a mut MindmapTreeNode, path: &[usize], ) -> Option<&'a mut MindmapTreeNode> { if path.is_empty() { return Some(node); } let (first, rest) = path.split_first()?; let child = node.children.get_mut(*first)?; mindmap_node_mut_by_path(child, rest) } fn build_mindmap_outline_tree( root_title: &str, outline: &[MindmapOutlineItemPayload], page_link_pattern: &str, ) -> Result { if !page_link_pattern.contains("{page}") { return Err(BridgeError::validation( "mindmap_outline_to_mindmap 的 pageLinkPattern 必须包含 {page}", )); } let mut root = default_mindmap_tree(); root.data.text = Some(root_title.trim().to_string()); let mut latest_paths: Vec> = vec![vec![]]; for item in outline { let level = item.level.clamp(1, 6) as usize; let title = item.title.trim(); if title.is_empty() { continue; } let page = item.page.max(1); let hyperlink = page_link_pattern.replace("{page}", &page.to_string()); let node = MindmapTreeNode { data: MindmapNodeData { uid: Some(next_mindmap_uid()), text: Some(title.to_string()), hyperlink: Some(hyperlink.clone()), note: None, refs: Some(vec![MindmapNodeRef { kind: "pdf".into(), asset_id: None, file_url: Some(hyperlink), page: Some(page), slide: None, title: Some(title.to_string()), snippet: None, }]), extra: BTreeMap::new(), }, children: vec![], extra: BTreeMap::new(), }; if latest_paths.len() > level + 1 { latest_paths.truncate(level + 1); } while latest_paths.len() <= level { latest_paths.push(vec![]); } let parent_path = if level == 1 { vec![] } else { latest_paths.get(level - 1).cloned().unwrap_or_default() }; let parent = mindmap_node_mut_by_path(&mut root, &parent_path) .ok_or_else(|| BridgeError::validation("mindmap_outline_to_mindmap 树路径异常"))?; parent.children.push(node); let next_index = parent.children.len().saturating_sub(1); let mut node_path = parent_path; node_path.push(next_index); latest_paths[level] = node_path; } ensure_mindmap_uids(&mut root); Ok(root) } fn build_mindmap_child(node: &MindmapNodeInput) -> MindmapTreeNode { let mut child = MindmapTreeNode { data: MindmapNodeData { uid: node.uid.clone(), text: Some(node.text.clone()), hyperlink: node.hyperlink.clone(), note: node.note.clone(), refs: node.refs.clone(), extra: BTreeMap::new(), }, children: vec![], extra: BTreeMap::new(), }; ensure_mindmap_uids(&mut child); child } fn add_mindmap_child( root: &mut MindmapTreeNode, parent_uid: &str, node: &MindmapNodeInput, ) -> Result { if let Some(parent) = find_mindmap_node_mut(root, parent_uid) { parent.children.push(build_mindmap_child(node)); return Ok(true); } Ok(false) } fn add_mindmap_sibling_after( root: &mut MindmapTreeNode, target_uid: &str, node: &MindmapNodeInput, ) -> Result { insert_mindmap_sibling_after(&mut root.children, target_uid, node) } fn insert_mindmap_sibling_after( nodes: &mut Vec, target_uid: &str, node: &MindmapNodeInput, ) -> Result { for index in 0..nodes.len() { if nodes[index].data.uid.as_deref() == Some(target_uid) { nodes.insert(index + 1, build_mindmap_child(node)); return Ok(true); } if insert_mindmap_sibling_after(&mut nodes[index].children, target_uid, node)? { return Ok(true); } } Ok(false) } fn update_mindmap_text(root: &mut MindmapTreeNode, uid: &str, text: &str) -> Result { if let Some(node) = find_mindmap_node_mut(root, uid) { node.data.text = Some(text.to_string()); return Ok(true); } Ok(false) } fn set_mindmap_hyperlink( root: &mut MindmapTreeNode, uid: &str, hyperlink: Option, ) -> Result { if let Some(node) = find_mindmap_node_mut(root, uid) { node.data.hyperlink = hyperlink; return Ok(true); } Ok(false) } fn set_mindmap_refs( root: &mut MindmapTreeNode, uid: &str, refs: Vec, ) -> Result { if let Some(node) = find_mindmap_node_mut(root, uid) { node.data.refs = Some(refs); return Ok(true); } Ok(false) } fn append_mindmap_note( root: &mut MindmapTreeNode, uid: &str, markdown: &str, ) -> Result { if let Some(node) = find_mindmap_node_mut(root, uid) { let next = if let Some(current) = node.data.note.as_ref() { if current.trim().is_empty() { markdown.to_string() } else { format!("{current}\n\n{markdown}") } } else { markdown.to_string() }; node.data.note = Some(next); return Ok(true); } Ok(false) } fn delete_mindmap_node(root: &mut MindmapTreeNode, uid: &str) -> Result { if root.data.uid.as_deref() == Some(uid) { return Err("deleteNode: 不能删除根节点".into()); } Ok(delete_mindmap_node_in_children(&mut root.children, uid)) } fn delete_mindmap_node_in_children(nodes: &mut Vec, uid: &str) -> bool { if let Some(index) = nodes .iter() .position(|node| node.data.uid.as_deref() == Some(uid)) { nodes.remove(index); return true; } for child in nodes.iter_mut() { if delete_mindmap_node_in_children(&mut child.children, uid) { return true; } } false } fn record_field<'a>(value: &'a Value, key: &str) -> Option<&'a Value> { value.as_object().and_then(|map| map.get(key)) } fn string_field(value: &Value, key: &str) -> Option { record_field(value, key) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) } fn bool_field(value: &Value, key: &str) -> Option { record_field(value, key).and_then(Value::as_bool) } fn integer_field(value: &Value, key: &str) -> Option { record_field(value, key).and_then(|field| { field .as_i64() .or_else(|| field.as_f64().map(|number| number as i64)) }) } fn is_pdf_asset(file_name: &str, mime_type: &str) -> bool { let lowered_mime = mime_type.trim().to_ascii_lowercase(); let lowered_name = file_name.trim().to_ascii_lowercase(); lowered_mime.contains("pdf") || lowered_name.ends_with(".pdf") } fn is_book_asset(file_name: &str, mime_type: &str) -> bool { let lowered_mime = mime_type.trim().to_ascii_lowercase(); let lowered_name = file_name.trim().to_ascii_lowercase(); lowered_mime.contains("epub") || lowered_mime.contains("ebook") || [".epub", ".mobi", ".azw3", ".azw", ".fb2"] .iter() .any(|suffix| lowered_name.ends_with(suffix)) } fn classify_asset_kind( asset_type: &str, file_name: &str, mime_type: &str, ) -> KernelProjectionAssetKind { let lowered_type = asset_type.trim().to_ascii_lowercase(); let lowered_mime = mime_type.trim().to_ascii_lowercase(); if lowered_type == "mindmap" { return KernelProjectionAssetKind::Mindmap; } if lowered_type == "luckysheet" { return KernelProjectionAssetKind::Table; } if is_pdf_asset(file_name, mime_type) { return KernelProjectionAssetKind::Pdf; } if is_book_asset(file_name, mime_type) { return KernelProjectionAssetKind::Book; } if lowered_mime.starts_with("image/") { return KernelProjectionAssetKind::Image; } if lowered_mime.starts_with("video/") { return KernelProjectionAssetKind::Video; } if lowered_mime.starts_with("audio/") { return KernelProjectionAssetKind::Audio; } if lowered_type == "file" || lowered_type.is_empty() { return KernelProjectionAssetKind::File; } KernelProjectionAssetKind::Unknown } fn classify_asset_resource_kind( asset_kind: &KernelProjectionAssetKind, ) -> KernelProjectionResourceKind { match asset_kind { KernelProjectionAssetKind::Mindmap => KernelProjectionResourceKind::Mindmap, KernelProjectionAssetKind::Table => KernelProjectionResourceKind::Table, KernelProjectionAssetKind::Book => KernelProjectionResourceKind::Book, KernelProjectionAssetKind::Pdf => KernelProjectionResourceKind::Pdf, _ => KernelProjectionResourceKind::Asset, } } fn classify_asset_node_type(asset_kind: &KernelProjectionAssetKind) -> KernelNodeType { match asset_kind { KernelProjectionAssetKind::Mindmap => KernelNodeType::Mindmap, KernelProjectionAssetKind::Table => KernelNodeType::Table, KernelProjectionAssetKind::Book => KernelNodeType::Book, KernelProjectionAssetKind::Pdf => KernelNodeType::Pdf, _ => KernelNodeType::Asset, } } fn classify_asset_icon_hint(asset_kind: &KernelProjectionAssetKind) -> &'static str { match asset_kind { KernelProjectionAssetKind::Mindmap => "mindmap", KernelProjectionAssetKind::Table => "table", KernelProjectionAssetKind::Book => "book", KernelProjectionAssetKind::Pdf => "pdf", KernelProjectionAssetKind::Image => "image", KernelProjectionAssetKind::Video => "video", KernelProjectionAssetKind::Audio => "audio", _ => "file", } } fn build_projection_row_id( projection: &KernelProjectionKind, row_kind: &KernelProjectionRowKind, node_id: &str, ) -> String { match projection { KernelProjectionKind::FileTree => match row_kind { KernelProjectionRowKind::Document => format!("doc:{node_id}"), KernelProjectionRowKind::Index => format!("index:{node_id}"), KernelProjectionRowKind::Asset => format!("asset:{node_id}"), KernelProjectionRowKind::AssetFolder => format!("asset-folder:{node_id}"), }, _ => format!("page:{node_id}"), } } fn build_document_projection_capabilities(child_count: u32) -> Vec { let mut capabilities = vec![ KernelProjectionCapability::Open, KernelProjectionCapability::Drag, KernelProjectionCapability::Drop, KernelProjectionCapability::Select, KernelProjectionCapability::CreateChild, KernelProjectionCapability::Rename, KernelProjectionCapability::Archive, KernelProjectionCapability::Restore, KernelProjectionCapability::ContextMenu, KernelProjectionCapability::Reorder, ]; if child_count > 0 { capabilities.insert(0, KernelProjectionCapability::Expand); } capabilities } fn build_asset_folder_projection_capabilities(child_count: u32) -> Vec { let mut capabilities = vec![ KernelProjectionCapability::OpenAsset, KernelProjectionCapability::Select, KernelProjectionCapability::ContextMenu, ]; if child_count > 0 { capabilities.insert(0, KernelProjectionCapability::Expand); } capabilities } fn normalize_kernel_node_from_record( value: &Value, workspace_id: Option<&str>, ) -> Result { let id = string_field(value, "id") .or_else(|| string_field(value, "documentId")) .ok_or_else(|| BridgeError::validation("kernel node 缺少 id"))?; let title = string_field(value, "title").or_else(|| string_field(value, "text")); let parent_id = string_field(value, "parent_id") .or_else(|| string_field(value, "parentId")) .or_else(|| string_field(value, "document_id")); let updated_at = string_field(value, "updated_at").or_else(|| string_field(value, "updatedAt")); let created_at = string_field(value, "created_at").or_else(|| string_field(value, "createdAt")); let access_scope = string_field(value, "access_scope").or_else(|| string_field(value, "accessScope")); let is_mindmap = string_field(value, "asset_type") .map(|asset_type| asset_type == "mindmap") .unwrap_or(false); let node_type = if is_mindmap { KernelNodeType::Mindmap } else if bool_field(value, "is_template").unwrap_or(false) { KernelNodeType::Page } else if value.get("content").is_some() || value.get("rawText").is_some() { KernelNodeType::ContentNode } else { KernelNodeType::Page }; let mut extra = BTreeMap::new(); if let Some(access_scope) = access_scope { extra.insert("accessScope".into(), json!(access_scope)); } if let Some(is_starred) = bool_field(value, "is_starred").or_else(|| bool_field(value, "isStarred")) { extra.insert("isStarred".into(), json!(is_starred)); } if let Some(sort_order) = integer_field(value, "sort_order").or_else(|| integer_field(value, "sortOrder")) { extra.insert("sortOrder".into(), json!(sort_order)); } let content = value .get("content") .cloned() .or_else(|| value.get("rawText").cloned()) .map(|body| KernelContentPayload { format: if body.is_string() { "text".into() } else { "json".into() }, body, }); Ok(KernelNode { id: id.clone(), node_type, workspace_id: workspace_id .map(ToOwned::to_owned) .or_else(|| string_field(value, "workspace_id")) .or_else(|| string_field(value, "workspaceId")), parent_id, subtree: Some(KernelSubtreeRef { root_node_id: id.clone(), path: vec![id.clone()], depth: Some(0), }), metadata: KernelNodeMetadata { title, icon: None, tags: Vec::new(), created_at, updated_at, extra, }, content, refs: None, audit: KernelAuditStamp::default(), }) } fn build_sidebar_kernel_nodes( data: &Value, workspace_id: Option<&str>, ) -> Result, BridgeError> { let documents = data .get("documents") .and_then(Value::as_array) .cloned() .unwrap_or_default(); documents .iter() .map(|item| normalize_kernel_node_from_record(item, workspace_id)) .collect::, _>>() } fn build_sidebar_kernel_edges(nodes: &[KernelNode]) -> Vec { nodes .iter() .filter_map(|node| { node.parent_id.as_ref().map(|parent_id| KernelEdge { id: format!("edge_parent_of_{}_{}", parent_id, node.id), edge_type: KernelEdgeType::ParentOf, workspace_id: node.workspace_id.clone(), from_node_id: parent_id.clone(), to_node_id: node.id.clone(), metadata: BTreeMap::new(), audit: KernelAuditStamp::default(), }) }) .collect() } fn kernel_node_sort_order(node: &KernelNode) -> i64 { node.metadata .extra .get("sortOrder") .and_then(Value::as_i64) .unwrap_or(0) } fn file_tree_leaf_capabilities(open_asset: bool) -> Vec { let mut capabilities = vec![ KernelProjectionCapability::Select, KernelProjectionCapability::ContextMenu, ]; if open_asset { capabilities.insert(0, KernelProjectionCapability::OpenAsset); } else { capabilities.insert(0, KernelProjectionCapability::Open); } capabilities } fn make_projection_edge( parent_node_id: &str, child_node_id: &str, workspace_id: Option<&str>, ) -> KernelEdge { KernelEdge { id: format!("edge_parent_of_{}_{}", parent_node_id, child_node_id), edge_type: KernelEdgeType::ParentOf, workspace_id: workspace_id.map(ToOwned::to_owned), from_node_id: parent_node_id.to_string(), to_node_id: child_node_id.to_string(), metadata: BTreeMap::new(), audit: KernelAuditStamp::default(), } } fn convex_projection_source_metadata( document_id: Option<&str>, asset_id: Option<&str>, workspace_id: Option<&str>, row_kind: &str, ) -> Option { let workspace_id = workspace_id?.trim(); if workspace_id.is_empty() { return None; } match row_kind { "document" => { let document_id = document_id?.trim(); if document_id.is_empty() { return None; } Some(json!({ "sourceKind": "convex_workspace", "sourceUri": format!("convex://workspace/{workspace_id}/documents/{document_id}"), "relativePath": format!("documents/{document_id}"), "storageIdentity": document_id, "operationProfile": "convex_workspace", })) } "index" => { let document_id = document_id?.trim(); if document_id.is_empty() { return None; } Some(json!({ "sourceKind": "convex_workspace", "sourceUri": format!("convex://workspace/{workspace_id}/documents/{document_id}/index"), "relativePath": format!("documents/{document_id}/index"), "storageIdentity": format!("{document_id}:index"), "operationProfile": "convex_workspace", })) } "asset" | "asset_folder" => { let asset_id = asset_id?.trim(); if asset_id.is_empty() { return None; } Some(json!({ "sourceKind": "convex_workspace", "sourceUri": format!("convex://workspace/{workspace_id}/assets/{asset_id}"), "relativePath": format!("assets/{asset_id}"), "storageIdentity": asset_id, "operationProfile": "convex_workspace", })) } _ => None, } } fn make_projection_resource_meta( resource_kind: KernelProjectionResourceKind, document_id: Option, asset_id: Option, workspace_id: Option, asset_kind: Option, icon_hint: &str, row_kind: &str, ) -> KernelProjectionResourceMeta { let mut extra = BTreeMap::new(); extra.insert("rowKind".into(), json!(row_kind)); if let Some(source) = convex_projection_source_metadata( document_id.as_deref(), asset_id.as_deref(), workspace_id.as_deref(), row_kind, ) { extra.insert("source".into(), source); } KernelProjectionResourceMeta { resource_kind: Some(resource_kind), document_id, asset_id, workspace_id, asset_kind, icon_hint: Some(icon_hint.into()), extra, } } #[derive(Debug, Clone)] struct NormalizedFileTreeAsset { id: String, document_id: String, workspace_id: Option, title: String, resource_kind: KernelProjectionResourceKind, asset_kind: KernelProjectionAssetKind, icon_hint: &'static str, } fn infer_file_tree_asset_shape( asset_type: &str, file_name: &str, mime_type: &str, ) -> ( KernelProjectionResourceKind, KernelProjectionAssetKind, &'static str, ) { let asset_kind = classify_asset_kind(asset_type, file_name, mime_type); ( classify_asset_resource_kind(&asset_kind), asset_kind.clone(), classify_asset_icon_hint(&asset_kind), ) } fn normalize_file_tree_asset(value: &Value) -> Option { let id = string_field(value, "id")?; let document_id = string_field(value, "document_id").or_else(|| string_field(value, "documentId"))?; let file_name = string_field(value, "file_name") .or_else(|| string_field(value, "title")) .unwrap_or_else(|| "附件".into()); let mime_type = string_field(value, "mime_type").unwrap_or_default(); let asset_type = string_field(value, "asset_type").unwrap_or_else(|| "file".into()); let (resource_kind, asset_kind, icon_hint) = infer_file_tree_asset_shape(&asset_type, &file_name, &mime_type); Some(NormalizedFileTreeAsset { id, document_id, workspace_id: string_field(value, "workspace_id") .or_else(|| string_field(value, "workspaceId")), title: file_name, resource_kind, asset_kind, icon_hint, }) } fn build_file_tree_assets( data: &Value, ) -> ( BTreeMap>, BTreeMap, BTreeMap>, ) { let mut assets_by_doc = BTreeMap::>::new(); let mut asset_by_id = BTreeMap::::new(); let mut seen = BTreeMap::::new(); for key in ["media_assets", "mindmap_assets", "table_assets"] { for value in data .get(key) .and_then(Value::as_array) .cloned() .unwrap_or_default() { let Some(asset) = normalize_file_tree_asset(&value) else { continue; }; if seen.insert(asset.id.clone(), true).is_some() { continue; } assets_by_doc .entry(asset.document_id.clone()) .or_default() .push(asset.clone()); asset_by_id.insert(asset.id.clone(), asset); } } let child_map = data .get("mindmap_asset_children") .and_then(Value::as_object) .map(|object| { object .iter() .map(|(asset_id, children)| { let child_ids = children .as_array() .cloned() .unwrap_or_default() .into_iter() .filter_map(|value| value.as_str().map(ToOwned::to_owned)) .collect::>(); (asset_id.clone(), child_ids) }) .collect::>() }) .unwrap_or_default(); (assets_by_doc, asset_by_id, child_map) } fn build_file_tree_projection_result( subtree: &KernelSubtreeResult, _workspace_id: Option<&str>, root_node_id: Option<&str>, data: &Value, filters: &KernelProjectionFilter, ) -> KernelProjectionResult { let (assets_by_doc, asset_by_id, child_assets_by_parent) = build_file_tree_assets(data); let requested_query = normalize_projection_query(filters.query.as_deref()); let requested_max_results = filters.max_results; let page_ids = subtree .nodes .iter() .map(|node| (node.id.clone(), node.clone())) .collect::>(); let mut child_pages = BTreeMap::>::new(); for node in &subtree.nodes { if let Some(parent_id) = node.parent_id.as_ref() { child_pages .entry(parent_id.clone()) .or_default() .push(node.id.clone()); } } for ids in child_pages.values_mut() { ids.sort_by_key(|node_id| { page_ids .get(node_id) .map(kernel_node_sort_order) .unwrap_or_default() }); } let mut roots = if let Some(root_node_id) = root_node_id { vec![root_node_id.to_string()] } else { subtree .nodes .iter() .filter(|node| { node.parent_id .as_ref() .map(|parent_id| !page_ids.contains_key(parent_id)) .unwrap_or(true) }) .map(|node| node.id.clone()) .collect::>() }; roots.sort_by_key(|node_id| { page_ids .get(node_id) .map(kernel_node_sort_order) .unwrap_or_default() }); let mut items = Vec::::new(); let mut edges = subtree.edges.clone(); fn walk_page( page_id: &str, depth: u32, page_ids: &BTreeMap, child_pages: &BTreeMap>, assets_by_doc: &BTreeMap>, asset_by_id: &BTreeMap, child_assets_by_parent: &BTreeMap>, items: &mut Vec, edges: &mut Vec, ) { let Some(node) = page_ids.get(page_id) else { return; }; let direct_child_pages = child_pages.get(page_id).cloned().unwrap_or_default(); let doc_assets = assets_by_doc.get(page_id).cloned().unwrap_or_default(); let mut nested_child_asset_ids = BTreeMap::::new(); for asset in &doc_assets { if let Some(child_ids) = child_assets_by_parent.get(&asset.id) { for child_id in child_ids { nested_child_asset_ids.insert(child_id.clone(), true); } } } let direct_asset_count = doc_assets .iter() .filter(|asset| !nested_child_asset_ids.contains_key(&asset.id)) .count() as u32; let doc_child_count = 1 + direct_asset_count + direct_child_pages.len() as u32; let parent_node_id = if depth == 0 { None } else { node.parent_id.clone() }; let workspace_id = node.workspace_id.clone(); items.push(KernelProjectionItem { row_id: build_projection_row_id( &KernelProjectionKind::FileTree, &KernelProjectionRowKind::Document, &node.id, ), row_kind: KernelProjectionRowKind::Document, node_id: node.id.clone(), parent_node_id, node_type: KernelNodeType::Page, projection_kind: KernelProjectionKind::FileTree, title: node.metadata.title.clone(), depth, position: Some(kernel_node_sort_order(node)), child_count: doc_child_count, expandable: doc_child_count > 0, expanded_by_default: true, capabilities: build_document_projection_capabilities(doc_child_count), resource_meta: Some(make_projection_resource_meta( KernelProjectionResourceKind::Document, Some(node.id.clone()), None, workspace_id.clone(), None, "page", "document", )), icon_hint: Some("page".into()), }); let index_node_id = format!("index:{}", node.id); items.push(KernelProjectionItem { row_id: build_projection_row_id( &KernelProjectionKind::FileTree, &KernelProjectionRowKind::Index, &node.id, ), row_kind: KernelProjectionRowKind::Index, node_id: index_node_id.clone(), parent_node_id: Some(node.id.clone()), node_type: KernelNodeType::IndexNode, projection_kind: KernelProjectionKind::FileTree, title: Some("index.md".into()), depth: depth + 1, position: Some(0), child_count: 0, expandable: false, expanded_by_default: false, capabilities: file_tree_leaf_capabilities(false), resource_meta: Some(make_projection_resource_meta( KernelProjectionResourceKind::Index, Some(node.id.clone()), None, workspace_id.clone(), None, "index", "index", )), icon_hint: Some("index".into()), }); edges.push(make_projection_edge( &node.id, &index_node_id, workspace_id.as_deref(), )); let mut asset_position = 1i64; for asset in doc_assets { if nested_child_asset_ids.contains_key(&asset.id) { continue; } if let Some(child_ids) = child_assets_by_parent .get(&asset.id) .filter(|ids| !ids.is_empty()) { let folder_node_id = format!("asset-folder:{}", asset.id); items.push(KernelProjectionItem { row_id: build_projection_row_id( &KernelProjectionKind::FileTree, &KernelProjectionRowKind::AssetFolder, &asset.id, ), row_kind: KernelProjectionRowKind::AssetFolder, node_id: folder_node_id.clone(), parent_node_id: Some(node.id.clone()), node_type: KernelNodeType::Mindmap, projection_kind: KernelProjectionKind::FileTree, title: Some(asset.title.trim_end_matches(".json").to_string()), depth: depth + 1, position: Some(asset_position), child_count: child_ids.len() as u32, expandable: true, expanded_by_default: false, capabilities: build_asset_folder_projection_capabilities(child_ids.len() as u32), resource_meta: Some(make_projection_resource_meta( asset.resource_kind.clone(), Some(asset.document_id.clone()), Some(asset.id.clone()), asset.workspace_id.clone(), Some(KernelProjectionAssetKind::Mindmap), "mindmap", "asset_folder", )), icon_hint: Some("mindmap".into()), }); edges.push(make_projection_edge( &node.id, &folder_node_id, workspace_id.as_deref(), )); for (child_index, child_id) in child_ids.iter().enumerate() { let Some(child_asset) = asset_by_id.get(child_id) else { continue; }; let child_node_id = format!("asset:{}", child_asset.id); items.push(KernelProjectionItem { row_id: build_projection_row_id( &KernelProjectionKind::FileTree, &KernelProjectionRowKind::Asset, &child_asset.id, ), row_kind: KernelProjectionRowKind::Asset, node_id: child_node_id.clone(), parent_node_id: Some(folder_node_id.clone()), node_type: KernelNodeType::Asset, projection_kind: KernelProjectionKind::FileTree, title: Some(child_asset.title.clone()), depth: depth + 2, position: Some(child_index as i64), child_count: 0, expandable: false, expanded_by_default: false, capabilities: file_tree_leaf_capabilities(true), resource_meta: Some(make_projection_resource_meta( child_asset.resource_kind.clone(), Some(child_asset.document_id.clone()), Some(child_asset.id.clone()), child_asset.workspace_id.clone(), Some(child_asset.asset_kind.clone()), child_asset.icon_hint, "asset", )), icon_hint: Some(child_asset.icon_hint.into()), }); edges.push(make_projection_edge( &folder_node_id, &child_node_id, workspace_id.as_deref(), )); } asset_position += 1; continue; } let asset_node_id = format!("asset:{}", asset.id); items.push(KernelProjectionItem { row_id: build_projection_row_id( &KernelProjectionKind::FileTree, &KernelProjectionRowKind::Asset, &asset.id, ), row_kind: KernelProjectionRowKind::Asset, node_id: asset_node_id.clone(), parent_node_id: Some(node.id.clone()), node_type: classify_asset_node_type(&asset.asset_kind), projection_kind: KernelProjectionKind::FileTree, title: Some(asset.title.clone()), depth: depth + 1, position: Some(asset_position), child_count: 0, expandable: false, expanded_by_default: false, capabilities: file_tree_leaf_capabilities(true), resource_meta: Some(make_projection_resource_meta( asset.resource_kind.clone(), Some(asset.document_id.clone()), Some(asset.id.clone()), asset.workspace_id.clone(), Some(asset.asset_kind.clone()), asset.icon_hint, "asset", )), icon_hint: Some(asset.icon_hint.into()), }); edges.push(make_projection_edge( &node.id, &asset_node_id, workspace_id.as_deref(), )); asset_position += 1; } for child_page_id in direct_child_pages { walk_page( &child_page_id, depth + 1, page_ids, child_pages, assets_by_doc, asset_by_id, child_assets_by_parent, items, edges, ); } } for root_id in roots { walk_page( &root_id, 0, &page_ids, &child_pages, &assets_by_doc, &asset_by_id, &child_assets_by_parent, &mut items, &mut edges, ); } apply_file_tree_projection_search_filter(&mut items, &mut edges, filters); let visible_rows = items.len(); let visible_edges = edges.len(); KernelProjectionResult { projection_id: format!( "kernel_projection:file_tree:{}", root_node_id.unwrap_or("root") ), projection: KernelProjectionKind::FileTree, root_node_id: root_node_id.map(ToOwned::to_owned), items, edges, meta: BTreeMap::from([( "search".into(), json!({ "query": requested_query.clone(), "maxResults": requested_max_results, "maxResultsRule": "matches_only_before_ancestor_completion", "ancestorCompletion": "include_all_ancestors_after_match_truncation", "ordering": "kernel_file_tree_preorder", "indexingVisibility": { "schema": "mnote.file_tree.indexing_visibility", "schemaVersion": 1, "source": "kernel.project_view", "status": "visible", "requestKey": requested_query .as_ref() .map(|query| format!("{}:{query}", root_node_id.unwrap_or("root"))), "indexedResourceKinds": ["document", "index", "asset", "asset_folder", "mindmap", "book", "pdf"], "visibleResourceKinds": ["document", "index", "asset", "asset_folder", "mindmap", "book", "pdf"], "metrics": { "visibleRows": visible_rows, "visibleEdges": visible_edges, }, }, }), )]), } } fn normalize_projection_query(raw: Option<&str>) -> Option { let query = raw?.trim().to_lowercase(); if query.is_empty() { None } else { Some(query) } } fn file_tree_item_matches_query(item: &KernelProjectionItem, query: &str) -> bool { let title = item.title.as_deref().unwrap_or("").to_lowercase(); if title.contains(query) { return true; } item.resource_meta.as_ref().is_some_and(|meta| { meta.document_id .as_deref() .map(|value| value.to_lowercase().contains(query)) .unwrap_or(false) || meta .asset_id .as_deref() .map(|value| value.to_lowercase().contains(query)) .unwrap_or(false) }) } fn apply_file_tree_projection_search_filter( items: &mut Vec, edges: &mut Vec, filters: &KernelProjectionFilter, ) { let Some(query) = normalize_projection_query(filters.query.as_deref()) else { return; }; let parent_by_node_id = items .iter() .filter_map(|item| { item.parent_node_id .as_ref() .map(|parent_id| (item.node_id.clone(), parent_id.clone())) }) .collect::>(); let mut include = BTreeMap::::new(); let mut matched_count = 0usize; for item in items.iter() { if !file_tree_item_matches_query(item, &query) { continue; } if let Some(max_results) = filters.max_results { if matched_count >= max_results { continue; } } matched_count += 1; include.insert(item.node_id.clone(), true); let mut current = item.parent_node_id.clone(); while let Some(parent_id) = current { include.insert(parent_id.clone(), true); current = parent_by_node_id.get(&parent_id).cloned(); } } items.retain(|item| include.contains_key(&item.node_id)); for item in items.iter_mut() { if item.expandable { item.expanded_by_default = true; } } edges.retain(|edge| { include.contains_key(&edge.from_node_id) && include.contains_key(&edge.to_node_id) }); } fn build_kernel_subtree_result( data: &Value, root_node_id: &str, workspace_id: Option<&str>, depth: Option, ) -> Result { let mut all_nodes = build_sidebar_kernel_nodes(data, workspace_id)?; let max_depth = depth.unwrap_or(u32::MAX); let mut by_parent = BTreeMap::, Vec>::new(); for node in &all_nodes { by_parent .entry(node.parent_id.clone()) .or_default() .push(node.id.clone()); } let mut queue = VecDeque::from([(root_node_id.to_string(), 0u32)]); let mut visited = BTreeMap::::new(); while let Some((node_id, current_depth)) = queue.pop_front() { if visited.contains_key(&node_id) || current_depth > max_depth { continue; } visited.insert(node_id.clone(), current_depth); if let Some(children) = by_parent.get(&Some(node_id.clone())) { for child in children { queue.push_back((child.clone(), current_depth + 1)); } } } let nodes = all_nodes .iter_mut() .filter_map(|node| { visited.get(&node.id).copied().map(|node_depth| { node.subtree = Some(KernelSubtreeRef { root_node_id: root_node_id.to_string(), path: vec![root_node_id.to_string(), node.id.clone()], depth: Some(node_depth), }); node.clone() }) }) .collect::>(); let edges = build_sidebar_kernel_edges(&nodes); Ok(KernelSubtreeResult { root_node_id: root_node_id.to_string(), nodes, edges, }) } fn build_kernel_edge_list_result( data: &Value, node_id: &str, workspace_id: Option<&str>, ) -> Result { let nodes = build_sidebar_kernel_nodes(data, workspace_id)?; let edges = build_sidebar_kernel_edges(&nodes) .into_iter() .filter(|edge| edge.from_node_id == node_id || edge.to_node_id == node_id) .collect::>(); Ok(KernelEdgeListResult { node_id: node_id.to_string(), edges, }) } fn build_kernel_graph_traversal_result( data: &Value, start_node_id: &str, workspace_id: Option<&str>, max_depth: u32, ) -> Result { let subtree = build_kernel_subtree_result(data, start_node_id, workspace_id, Some(max_depth))?; let mut visits = Vec::new(); let mut queue = VecDeque::from([(start_node_id.to_string(), 0u32)]); let parent_map = subtree .nodes .iter() .map(|node| (node.id.clone(), node.parent_id.clone())) .collect::>(); let mut seen = BTreeMap::::new(); while let Some((node_id, depth)) = queue.pop_front() { if seen.insert(node_id.clone(), true).is_some() || depth > max_depth { continue; } visits.push(KernelGraphVisit { node_id: node_id.clone(), depth, }); for (candidate_id, parent_id) in &parent_map { if parent_id.as_deref() == Some(node_id.as_str()) { queue.push_back((candidate_id.clone(), depth + 1)); } } } Ok(KernelGraphTraversalResult { start_node_id: start_node_id.to_string(), visited: visits, edges: subtree.edges, }) } fn build_kernel_projection_result( data: &Value, projection: KernelProjectionKind, root_node_id: Option<&str>, workspace_id: Option<&str>, depth: Option, filters: KernelProjectionFilter, ) -> Result { let subtree = if let Some(root_node_id) = root_node_id { build_kernel_subtree_result(data, root_node_id, workspace_id, depth)? } else { let mut nodes = build_sidebar_kernel_nodes(data, workspace_id)?; for node in &mut nodes { node.subtree = Some(KernelSubtreeRef { root_node_id: node.id.clone(), path: vec![node.id.clone()], depth: Some(0), }); } let edges = build_sidebar_kernel_edges(&nodes); KernelSubtreeResult { root_node_id: "workspace_root".into(), nodes, edges, } }; if projection == KernelProjectionKind::FileTree { return Ok(build_file_tree_projection_result( &subtree, workspace_id, root_node_id, data, &filters, )); } let items = subtree .nodes .iter() .map(|node| { let child_count = subtree .edges .iter() .filter(|edge| { edge.from_node_id == node.id && edge.edge_type == KernelEdgeType::ParentOf }) .count() as u32; KernelProjectionItem { row_id: build_projection_row_id( &projection, &KernelProjectionRowKind::Document, &node.id, ), row_kind: KernelProjectionRowKind::Document, node_id: node.id.clone(), parent_node_id: node.parent_id.clone(), node_type: node.node_type.clone(), projection_kind: projection.clone(), title: node.metadata.title.clone(), depth: node .subtree .as_ref() .and_then(|subtree| subtree.depth) .unwrap_or(0), position: node.metadata.extra.get("sortOrder").and_then(Value::as_i64), child_count, expandable: child_count > 0, expanded_by_default: true, capabilities: build_document_projection_capabilities(child_count), resource_meta: Some(make_projection_resource_meta( KernelProjectionResourceKind::Document, Some(node.id.clone()), None, node.workspace_id.clone(), None, "page", "document", )), icon_hint: Some("page".into()), } }) .collect::>(); Ok(KernelProjectionResult { projection_id: format!( "kernel_projection:{}:{}", match projection { KernelProjectionKind::SidebarTree => "sidebar_tree", KernelProjectionKind::PageTree => "page_tree", KernelProjectionKind::FileTree => "file_tree", KernelProjectionKind::Mindmap => "mindmap", KernelProjectionKind::ReadView => "read_view", KernelProjectionKind::SearchResults => "search_results", KernelProjectionKind::RagIndex => "rag_index", }, root_node_id.unwrap_or("root") ), projection, root_node_id: root_node_id.map(ToOwned::to_owned), items, edges: subtree.edges, meta: BTreeMap::new(), }) } fn find_mindmap_node_mut<'a>( node: &'a mut MindmapTreeNode, uid: &str, ) -> Option<&'a mut MindmapTreeNode> { if node.data.uid.as_deref() == Some(uid) { return Some(node); } for child in &mut node.children { if let Some(found) = find_mindmap_node_mut(child, uid) { return Some(found); } } None } fn execute_query_result( _context_wire: RuntimeBridgeContextWire, query_wire: RuntimeQueryEnvelopeWire, data: Value, ) -> Result { match query_wire.name.as_str() { "documents.content.get" => { let payload: DocumentContentQueryPayload = parse_payload(query_wire.payload)?; let result = build_document_content_result( &data, &payload.document_id, payload.workspace_id.as_deref(), )?; serde_json::to_value(result).map_err(|error| { BridgeError::transport(format!("documents.content.get result 序列化失败: {error}")) }) } "page.aggregate.get" => { let payload: PageAggregateQueryPayload = parse_payload(query_wire.payload)?; let result = build_page_aggregate_projection_result( &data, &payload.document_id, payload.workspace_id.as_deref(), PageAggregateSource::KernelProjection, )?; serde_json::to_value(result).map_err(|error| { BridgeError::transport(format!("page.aggregate.get result 序列化失败: {error}")) }) } "kernel.node.get" => { let payload: KernelGetNodeQueryPayload = parse_payload(query_wire.payload)?; let node = normalize_kernel_node_from_record(&data, payload.workspace_id.as_deref())?; serde_json::to_value(node).map_err(|error| { BridgeError::transport(format!("kernel.node.get result 序列化失败: {error}")) }) } "kernel.subtree.get" => { let payload: KernelGetSubtreeQueryPayload = parse_payload(query_wire.payload)?; let result = build_kernel_subtree_result( &data, &payload.root_node_id, payload.workspace_id.as_deref(), payload.depth, )?; serde_json::to_value(result).map_err(|error| { BridgeError::transport(format!("kernel.subtree.get result 序列化失败: {error}")) }) } "kernel.children.list" => { let payload: KernelListChildrenQueryPayload = parse_payload(query_wire.payload)?; let subtree = build_kernel_subtree_result( &data, &payload.parent_node_id, payload.workspace_id.as_deref(), Some(1), )?; let nodes = subtree .nodes .into_iter() .filter(|node| node.parent_id.as_deref() == Some(payload.parent_node_id.as_str())) .collect::>(); serde_json::to_value(nodes).map_err(|error| { BridgeError::transport(format!("kernel.children.list result 序列化失败: {error}")) }) } "kernel.edges.list" => { let payload: KernelListEdgesQueryPayload = parse_payload(query_wire.payload)?; let result = build_kernel_edge_list_result( &data, &payload.node_id, payload.workspace_id.as_deref(), )?; serde_json::to_value(result).map_err(|error| { BridgeError::transport(format!("kernel.edges.list result 序列化失败: {error}")) }) } "kernel.graph.traverse" => { let payload: KernelTraverseGraphQueryPayload = parse_payload(query_wire.payload)?; let result = build_kernel_graph_traversal_result( &data, &payload.start_node_id, payload.workspace_id.as_deref(), payload.max_depth.unwrap_or(2), )?; serde_json::to_value(result).map_err(|error| { BridgeError::transport(format!("kernel.graph.traverse result 序列化失败: {error}")) }) } "kernel.project_view" => { let payload: KernelProjectViewQueryPayload = parse_payload(query_wire.payload)?; let result = build_kernel_projection_result( &data, payload.projection, payload.root_node_id.as_deref(), payload.workspace_id.as_deref(), payload.depth, KernelProjectionFilter { node_types: payload.node_types.unwrap_or_default(), edge_types: payload.edge_types.unwrap_or_default(), query: payload.query, max_results: payload.max_results, include_deleted: false, }, )?; serde_json::to_value(result).map_err(|error| { BridgeError::transport(format!("kernel.project_view result 序列化失败: {error}")) }) } "mindmaps.get" => { let tree = normalize_mindmap_from_value(&data)?; serde_json::to_value(tree).map_err(|error| { BridgeError::transport(format!("mindmaps.get result 序列化失败: {error}")) }) } "mindmap.projection.get" => { let payload: MindmapGetQueryPayload = parse_payload(query_wire.payload)?; let result = build_mindmap_projection_result(&data, &payload.mindmap_id)?; serde_json::to_value(result).map_err(|error| { BridgeError::transport(format!("mindmap.projection.get result 序列化失败: {error}")) }) } "search.documents" | "search.documents.query" => { let payload: SearchDocumentsQueryPayload = parse_payload(query_wire.payload)?; let dataset: SearchDocumentsDataset = serde_json::from_value(data).map_err(|error| { BridgeError::validation(format!( "search.documents dataset 反序列化失败: {error}" )) })?; let result = evaluate_search_documents( &SearchDocumentsRequest { query: payload.query, workspace_id: payload.workspace_id, page_id: payload.page_id, limit: payload.limit.unwrap_or(30) as usize, title_only: payload.title_only.unwrap_or(false), exact: payload.exact.unwrap_or(false), include_ocr: payload.include_ocr.unwrap_or(false), time_range: payload.time_range.unwrap_or_else(|| "any".into()), time_field: payload.time_field.unwrap_or_else(|| "updated".into()), custom_range_from: payload.custom_range_from, custom_range_to: payload.custom_range_to, }, &dataset, ); let legacy_result = SearchDocumentsEvaluation { enqueue_asset_ids: result.enqueue_asset_ids, results: result.results, }; if query_wire.name == "search.documents.query" { serde_json::to_value(search_documents_canonical_projection(legacy_result)) } else { serde_json::to_value(legacy_result) } .map_err(|error| { BridgeError::transport(format!("search.documents result 序列化失败: {error}")) }) } "search.recent" => { let rows = data .as_object() .and_then(|map| map.get("recents")) .and_then(Value::as_array) .cloned() .unwrap_or_default(); let items = rows .into_iter() .filter_map(|row| { let document_id = row.get("documentId")?.as_str()?.trim(); if document_id.is_empty() { return None; } Some(json!({ "documentId": document_id })) }) .collect::>(); Ok(json!({ "items": items })) } "bridge.request.get" | "bridge.trace.get" | "bridge.command.get" | "bridge.workspace.overview" => { normalize_bridge_observability_result(&data, query_wire.name.as_str()) } other => Err(BridgeError::validation(format!( "bridge runtime 暂不支持 query 执行: {other}" ))), } } fn execute_command( context_wire: RuntimeBridgeContextWire, command_wire: RuntimeCommandEnvelopeWire, ) -> Result { let context = to_bridge_context(context_wire); let source_executor = resolve_workspace_command_executor(&command_wire.source)?; if source_executor == WorkspaceCommandExecutor::LocalFolder { return Err(BridgeError::validation( "local_folder command executor 尚未接入 runtime command router", )); } match command_wire.name.as_str() { "kernel.node.create" => { let payload: KernelCreateNodeCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "kernel.node.create".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: KernelCreateNode { node: payload.node.clone(), position: payload.position, }, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: serde_json::to_value(&payload).map_err(|error| { BridgeError::transport(format!("kernel.node.create 参数序列化失败: {error}")) })?, })) } "kernel.node.update" => { let payload: KernelUpdateNodeCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "kernel.node.update".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: KernelUpdateNode { node_id: payload.node_id.clone(), metadata: payload.metadata.clone(), content: payload.content.clone(), refs: payload.refs.clone(), audit: payload.audit.clone(), }, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: serde_json::to_value(&payload).map_err(|error| { BridgeError::transport(format!("kernel.node.update 参数序列化失败: {error}")) })?, })) } "kernel.subtree.move" => { let payload: KernelMoveSubtreeCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "kernel.subtree.move".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: KernelMoveSubtree { subtree: payload.subtree.clone(), new_parent_node_id: payload.new_parent_node_id.clone(), sort_order: payload.sort_order, }, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: serde_json::to_value(&payload).map_err(|error| { BridgeError::transport(format!("kernel.subtree.move 参数序列化失败: {error}")) })?, })) } "kernel.edge.attach" => { let payload: KernelAttachEdgeCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "kernel.edge.attach".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: KernelAttachEdge { edge: payload.edge.clone(), }, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: serde_json::to_value(&payload).map_err(|error| { BridgeError::transport(format!("kernel.edge.attach 参数序列化失败: {error}")) })?, })) } "kernel.edge.detach" => { let payload: KernelDetachEdgeCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "kernel.edge.detach".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: KernelDetachEdge { edge_id: payload.edge_id.clone(), from_node_id: payload.from_node_id.clone(), to_node_id: payload.to_node_id.clone(), edge_type: payload.edge_type.clone(), }, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: serde_json::to_value(&payload).map_err(|error| { BridgeError::transport(format!("kernel.edge.detach 参数序列化失败: {error}")) })?, })) } "blocks.patch" => { let payload: BlockPatchCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "blocks.patch".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: PatchBlock { page_id: payload.document_id.clone(), block_id: payload.block_id.clone(), workspace_id: payload.workspace_id.clone(), revision: None, block_snapshot_json: serde_json::to_string(&payload.next_block).map_err( |error| { BridgeError::validation(format!( "blocks.patch block snapshot 序列化失败: {error}" )) }, )?, conflict_detection_key: None, }, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: { let stream_delta_hint = tree_resync_required_hint( "blocks.patch", json!({ "documentId": payload.document_id, "blockId": payload.block_id, }), ); let domain_event_payload = block_patch_domain_event_payload(&payload); json!({ "id": payload.document_id, "blockId": payload.block_id, "nextBlock": payload.next_block, "streamDeltaHint": stream_delta_hint, "domainEventHint": tree_domain_event_hint("block.patched"), "domainEventPlan": tree_domain_event_plan_with_payload( "block.patched", domain_event_payload, stream_delta_hint, ), }) }, })) } "blocks.move" => { let payload: BlockMoveCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "blocks.move".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: MoveBlock { block_id: payload.block_id.clone(), new_parent_block_id: Some(payload.target_document_id.clone()), new_page_id: Some(payload.target_document_id.clone()), prev_block_id: None, }, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; let stream_delta_hint = tree_resync_required_hint( "blocks.move", json!({ "documentId": payload.target_document_id, "blockId": payload.block_id, }), ); let domain_event_payload = block_move_domain_event_payload(&payload); Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "id": payload.block_id, "sourceDocumentId": payload.source_document_id, "targetDocumentId": payload.target_document_id, "streamDeltaHint": stream_delta_hint, "domainEventHint": tree_domain_event_hint("block.moved"), "domainEventPlan": tree_domain_event_plan_with_payload( "block.moved", domain_event_payload, stream_delta_hint, ), }), })) } "blocks.embed" => { let payload: BlockEmbedCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "blocks.embed".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: EmbedBlock { source_document_id: payload.source_document_id.clone(), source_block_id: payload.block_id.clone(), target_document_id: payload.target_document_id.clone(), target_block_id: payload.target_block_id.clone(), }, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; let stream_delta_hint = tree_resync_required_hint( "blocks.embed", json!({ "documentId": payload.target_document_id, "blockId": payload.block_id, }), ); let domain_event_payload = block_embed_domain_event_payload(&payload); Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "sourceDocumentId": payload.source_document_id, "blockId": payload.block_id, "targetDocumentId": payload.target_document_id, "targetBlockId": payload.target_block_id, "streamDeltaHint": stream_delta_hint, "domainEventHint": tree_domain_event_hint("block.embedded"), "domainEventPlan": tree_domain_event_plan_with_payload( "block.embedded", domain_event_payload, stream_delta_hint, ), }), })) } "documents.title.update" | "tree.node.rename" | "page.head.updateTitle" => { let payload: DocumentTitleCommandPayload = parse_payload(command_wire.payload.clone())?; let command_name = match command_wire.name.as_str() { "tree.node.rename" => "tree.node.rename", "page.head.updateTitle" => "page.head.updateTitle", _ => "documents.title.update", }; let command = CommandEnvelope { name: command_name.into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: core_protocol::UpdatePageTitle { page_id: payload.document_id.clone(), title: payload.title.clone(), }, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, "title": payload.title, "streamDeltaHint": tree_stream_delta_hint("upsert_document_patch", json!({ "documentId": payload.document_id, "patch": { "title": payload.title, }, })), "domainEventHint": tree_domain_event_hint("tree.node.renamed"), "domainEventPlan": tree_domain_event_plan( "tree.node.renamed", tree_stream_delta_hint("upsert_document_patch", json!({ "documentId": payload.document_id, "patch": { "title": payload.title, }, })), ), }), })) } "documents.options.update" | "page.layout.updateOptions" => { let payload: DocumentOptionsCommandPayload = parse_payload(command_wire.payload.clone())?; let command_name = match command_wire.name.as_str() { "page.layout.updateOptions" => "page.layout.updateOptions", _ => "documents.options.update", }; let options = payload.options; let mut options_json = serde_json::Map::new(); if let Some(value) = options.wide_layout { options_json.insert("wideLayout".into(), json!(value)); } if let Some(value) = options.small_text { options_json.insert("smallText".into(), json!(value)); } if let Some(value) = options.show_heading_numbers { options_json.insert("showHeadingNumbers".into(), json!(value)); } if let Some(value) = options.show_toc { options_json.insert("showToc".into(), json!(value)); } if let Some(value) = options.show_structure { options_json.insert("showStructure".into(), json!(value)); } if let Some(value) = options.protect_editing { options_json.insert("protectEditing".into(), json!(value)); } if let Some(value) = options.show_word_count { options_json.insert("showWordCount".into(), json!(value)); } if let Some(value) = options.collapse_backlinks { options_json.insert("collapseBacklinks".into(), json!(value)); } if let Some(value) = options.page_font.clone() { options_json.insert("pageFont".into(), json!(value)); } if let Some(value) = options.layout_density.clone() { options_json.insert("layoutDensity".into(), json!(value)); } if let Some(value) = options.hide_child_pages { options_json.insert("hideChildPages".into(), json!(value)); } if let Some(value) = options.show_block_ref_count { options_json.insert("showBlockRefCount".into(), json!(value)); } if let Some(value) = options.embed_default_block_id.clone() { options_json.insert("embedDefaultBlockId".into(), json!(value)); } let command = CommandEnvelope { name: command_name.into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: UpdatePageOptions { page_id: payload.document_id.clone(), wide_layout: options.wide_layout, small_text: options.small_text, show_heading_numbers: options.show_heading_numbers, show_toc: options.show_toc, show_structure: options.show_structure, protect_editing: options.protect_editing, show_word_count: options.show_word_count, collapse_backlinks: options.collapse_backlinks, page_font: options.page_font.clone(), layout_density: options.layout_density.clone(), hide_child_pages: options.hide_child_pages, show_block_ref_count: options.show_block_ref_count, embed_default_block_id: options.embed_default_block_id.clone(), }, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, "options": Value::Object(options_json), }), })) } "documents.stats.update" => { let payload: DocumentStatsCommandPayload = parse_payload(command_wire.payload.clone())?; let stats = payload.stats; let command = CommandEnvelope { name: "documents.stats.update".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: UpdatePageStats { page_id: payload.document_id.clone(), word_count: stats.word_count, character_count: stats.character_count, block_count: stats.block_count, todo_total: stats.todo_total, todo_done: stats.todo_done, }, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, "workspaceId": payload.workspace_id, "wordCount": stats.word_count, "characterCount": stats.character_count, "blockCount": stats.block_count, "todoTotal": stats.todo_total, "todoDone": stats.todo_done, }), })) } "media.assets.replace_storage" => { let payload: MediaAssetReplaceStorageCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "media.assets.replace_storage".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: core_protocol::ReplaceMediaAssetStorage { asset_id: payload.asset_id.clone(), storage_id: payload.storage_id.clone(), }, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "userId": payload.user_id, "id": payload.asset_id, "documentId": payload.document_id, "workspaceId": payload.workspace_id, "storageId": payload.storage_id, }), })) } "documents.save" | "page.body.save" => { let payload: DocumentSaveCommandPayload = parse_payload(command_wire.payload.clone())?; let editor_document = normalize_save_editor_document(&payload)?; let canonical_content = legacy_content_from_editor_document(&editor_document); let command = CommandEnvelope { name: command_wire.name.clone(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: core_protocol::SavePageContent { page_id: payload.document_id.clone(), workspace_id: payload.workspace_id.clone(), revision: payload.revision, content_json: serde_json::to_string(&canonical_content).map_err(|error| { BridgeError::validation(format!( "{} content 序列化失败: {error}", command_wire.name )) })?, conflict_detection_key: payload.conflict_detection_key.clone(), }, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: { let stream_delta_hint = tree_resync_required_hint( "page_body_saved", json!({ "pageId": payload.document_id, "documentId": payload.document_id, }), ); let page_body_event_payload = document_save_page_body_domain_event_payload(&payload, &editor_document); let snapshot_event_payload = document_save_snapshot_domain_event_payload(&payload, &canonical_content)?; let page_body_event_plan = tree_domain_event_plan_with_payload( "page.body.saved", page_body_event_payload, stream_delta_hint.clone(), ); let snapshot_event_plan = tree_domain_event_plan_with_payload( "document.snapshot.saved", snapshot_event_payload, stream_delta_hint.clone(), ); json!({ "id": payload.document_id, "content": canonical_content, "editorDocument": editor_document, "tiptapDocument": payload.tiptap_document, "expectedRevision": payload.revision, "conflictDetectionKey": payload.conflict_detection_key, "streamDeltaHint": stream_delta_hint, "domainEventHint": tree_domain_event_hint("page.body.saved"), "domainEventPlan": page_body_event_plan.clone(), "domainEventPlans": [page_body_event_plan, snapshot_event_plan], }) }, })) } "documents.embed" | "tree.node.embed" => { let payload: DocumentEmbedCommandPayload = parse_payload(command_wire.payload.clone())?; let command_name = if command_wire.name == "tree.node.embed" { "tree.node.embed" } else { "documents.embed" }; let page_aggregate_embed_plan = build_page_aggregate_embed_plan(&command_wire, &payload)?; let embed_content = page_aggregate_embed_plan .as_ref() .and_then(|plan| plan.get("content")) .cloned() .or_else(|| payload.content.clone()) .ok_or_else(|| { BridgeError::validation(format!( "{command_name} 缺少 content 或 pageAggregateEmbed preflight" )) })?; let command = CommandEnvelope { name: command_name.into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: core_protocol::SavePageContent { page_id: payload.document_id.clone(), workspace_id: payload.workspace_id.clone(), revision: payload.revision, content_json: serde_json::to_string(&embed_content).map_err(|error| { BridgeError::validation(format!( "{command_name} content 序列化失败: {error}" )) })?, conflict_detection_key: payload.conflict_detection_key.clone(), }, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, "content": embed_content, "expectedRevision": payload.revision, "conflictDetectionKey": payload.conflict_detection_key, "sourceDocumentId": payload.source_document_id, "targetDocumentId": payload.target_document_id, "anchorBlockId": payload.anchor_block_id, "pageAggregateEmbedPlan": page_aggregate_embed_plan, "streamDeltaHint": tree_stream_delta_hint("noop", json!({})), "domainEventHint": tree_domain_event_hint("tree.node.embedded"), "domainEventPlan": tree_domain_event_plan( "tree.node.embedded", tree_stream_delta_hint("noop", json!({})), ), }), })) } "mindmaps.put" => { let payload: MindmapPutCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "mindmaps.put".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: PutMindmap { document_id: payload.document_id.clone(), mindmap_id: payload.mindmap_id.clone(), data_json: serde_json::to_string(&payload.data).map_err(|error| { BridgeError::validation(format!("mindmaps.put data 序列化失败: {error}")) })?, create_only: payload.create_only.unwrap_or(false), }, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "docId": payload.document_id, "mindmapId": payload.mindmap_id, "data": payload.data, "createOnly": payload.create_only.unwrap_or(false), }), })) } "mindmap.command.apply" => { let payload: MindmapCommandApplyPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "mindmap.command.apply".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()).or(Some(TargetRef { workspace_id: payload.workspace_id.clone(), page_id: Some(payload.document_id.clone()), block_id: None, })), payload: payload.commands.clone(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "documentId": payload.document_id, "mindmapId": payload.mindmap_id, "commands": payload.commands, "projectionRevision": payload.projection_revision, "canonicalCommand": "mindmap.command.apply", }), })) } "mindmaps.delete" => { let payload: MindmapDeleteCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "mindmaps.delete".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: payload.mindmap_id.clone(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "docId": payload.document_id, "mindmapId": payload.mindmap_id, }), })) } "mindmaps.restore" => { let payload: MindmapRestoreCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "mindmaps.restore".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: payload.mindmap_id.clone(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "docId": payload.document_id, "mindmapId": payload.mindmap_id, }), })) } "mindmaps.purge" => { let payload: MindmapPurgeCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "mindmaps.purge".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: payload.mindmap_id.clone(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "docId": payload.document_id, "mindmapId": payload.mindmap_id, }), })) } "mindmaps.emptyTrashByWorkspace" => { let payload: MindmapEmptyTrashCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "mindmaps.emptyTrashByWorkspace".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: payload.workspace_id.clone(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "workspaceId": payload.workspace_id, }), })) } "documents.create" | "tree.node.create" => { let payload: DocumentCreateCommandPayload = parse_payload(command_wire.payload.clone())?; let command_name = if command_wire.name == "tree.node.create" { "tree.node.create" } else { "documents.create" }; let command = CommandEnvelope { name: command_name.into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: payload.content.clone(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, "workspaceId": payload.workspace_id, "parentId": payload.parent_id, "title": payload.title, "accessScope": payload.access_scope, "content": payload.content, "streamDeltaHint": tree_document_result_hint("document"), "domainEventHint": tree_domain_event_hint("tree.node.created"), "domainEventPlan": tree_domain_event_plan( "tree.node.created", tree_document_result_hint("document"), ), }), })) } "documents.move" | "tree.subtree.move" => { let payload: DocumentMoveCommandPayload = parse_payload(command_wire.payload.clone())?; validate_document_move_legality(&payload, command_wire.preflight_data.as_ref())?; let move_order_plan = resolve_document_move_order_plan(&payload, command_wire.preflight_data.as_ref())?; let tree_write_operation = document_move_write_operation( context.workspace_id.as_deref(), move_order_plan.as_ref(), ); let command_name = if command_wire.name == "tree.subtree.move" { "tree.subtree.move" } else { "documents.move" }; let command = CommandEnvelope { name: command_name.into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: payload.sort_order, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, "parentId": payload.parent_id, "sortOrder": payload.sort_order, "treeWriteOperation": tree_write_operation, "streamDeltaHint": tree_stream_delta_hint("move_document", json!({ "documentId": payload.document_id, "parentId": payload.parent_id, "sortOrder": payload.sort_order, })), "domainEventHint": tree_domain_event_hint("tree.subtree.moved"), "domainEventPlan": tree_domain_event_plan( "tree.subtree.moved", tree_stream_delta_hint("move_document", json!({ "documentId": payload.document_id, "parentId": payload.parent_id, "sortOrder": payload.sort_order, })), ), }), })) } "documents.delete" | "tree.node.archive" => { let payload: DocumentDeleteCommandPayload = parse_payload(command_wire.payload.clone())?; let command_name = if command_wire.name == "tree.node.archive" { "tree.node.archive" } else { "documents.delete" }; let command = CommandEnvelope { name: command_name.into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: payload.document_id.clone(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, "streamDeltaHint": tree_stream_delta_hint("remove_document", json!({ "documentId": payload.document_id, })), "domainEventHint": tree_domain_event_hint("tree.node.archived"), "domainEventPlan": tree_domain_event_plan( "tree.node.archived", tree_stream_delta_hint("remove_document", json!({ "documentId": payload.document_id, })), ), }), })) } "documents.restore" | "tree.node.restore" => { let payload: DocumentRestoreCommandPayload = parse_payload(command_wire.payload.clone())?; let command_name = if command_wire.name == "tree.node.restore" { "tree.node.restore" } else { "documents.restore" }; let command = CommandEnvelope { name: command_name.into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: payload.document_id.clone(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, "streamDeltaHint": tree_document_result_hint("document"), "domainEventHint": tree_domain_event_hint("tree.node.restored"), "domainEventPlan": tree_domain_event_plan( "tree.node.restored", tree_document_result_hint("document"), ), }), })) } "documents.duplicate" => { let payload: DocumentDuplicateCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "documents.duplicate".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: payload.source_document_id.clone(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "sourceId": payload.source_document_id, "newId": payload.new_document_id, "title": payload.title, "streamDeltaHint": tree_result_document_hint(), "domainEventHint": tree_domain_event_hint("tree.node.duplicated"), "domainEventPlan": tree_domain_event_plan( "tree.node.duplicated", tree_result_document_hint(), ), }), })) } "documents.template" => { let payload: DocumentTemplateCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "documents.template".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: payload.is_template, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, "isTemplate": payload.is_template, }), })) } "documents.emptyTrashByWorkspace" => { let payload: DocumentEmptyTrashCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "documents.emptyTrashByWorkspace".into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: payload.workspace_id.clone(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "workspaceId": payload.workspace_id, }), })) } "documents.purge" | "tree.node.purge" => { let payload: DocumentPurgeCommandPayload = parse_payload(command_wire.payload.clone())?; let command_name = if command_wire.name == "tree.node.purge" { "tree.node.purge" } else { "documents.purge" }; let command = CommandEnvelope { name: command_name.into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: payload.document_id.clone(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, }), })) } "documents.copy_tree" | "tree.subtree.copy" => { let payload: DocumentCopyTreeCommandPayload = parse_payload(command_wire.payload.clone())?; let command_name = if command_wire.name == "tree.subtree.copy" { "tree.subtree.copy" } else { "documents.copy_tree" }; let command = CommandEnvelope { name: command_name.into(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: payload.items.len(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "items": payload.items.iter().map(|item| json!({ "documentId": item.document_id, "recursive": item.recursive, })).collect::>(), "targetParentId": payload.target_parent_id, "streamDeltaHint": tree_stream_delta_hint("copy_result", json!({ "itemsField": "items", "documentField": "document", })), "domainEventHint": tree_domain_event_hint("tree.subtree.copied"), "domainEventPlan": tree_domain_event_plan( "tree.subtree.copied", tree_stream_delta_hint("copy_result", json!({ "itemsField": "items", "documentField": "document", })), ), }), })) } "tree.filetree.drop.preflight" => { let payload: FileTreeDropPreflightCommandPayload = parse_payload(command_wire.payload.clone())?; let drop_plan = build_filetree_drop_plan(&payload)?; let command = CommandEnvelope { name: command_wire.name.clone(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: drop_plan.row_ids.len(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "fileTreeDropPlan": drop_plan, }), })) } "tree.filetree.delete.preflight" => { let payload: FileTreeDeletePreflightCommandPayload = parse_payload(command_wire.payload.clone())?; let delete_plan = build_filetree_delete_plan(&payload)?; let command = CommandEnvelope { name: command_wire.name.clone(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: delete_plan.row_ids.len(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "fileTreeDeletePlan": delete_plan, }), })) } "tree.filetree.paste.preflight" => { let payload: FileTreePastePreflightCommandPayload = parse_payload(command_wire.payload.clone())?; let paste_plan = build_filetree_paste_plan(&payload)?; let command = CommandEnvelope { name: command_wire.name.clone(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: paste_plan.row_ids.len(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "fileTreePastePlan": paste_plan, }), })) } "tree.filetree.upload-target.preflight" => { let payload: FileTreeUploadTargetPreflightCommandPayload = parse_payload(command_wire.payload.clone())?; let upload_target_plan = build_filetree_upload_target_plan(&payload)?; let command = CommandEnvelope { name: command_wire.name.clone(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: 1, reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "fileTreeUploadTargetPlan": upload_target_plan, }), })) } "tree.resource.copy" | "tree.resource.move" => { let payload: ResourceTransferCommandPayload = parse_payload(command_wire.payload.clone())?; let action = if command_wire.name == "tree.resource.copy" { "copy" } else { "move" }; let transfer_plan = build_resource_transfer_plan(action, &payload)?; let command = CommandEnvelope { name: command_wire.name.clone(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: transfer_plan.asset_ids.len(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "assetIds": transfer_plan.asset_ids, "targetDocumentId": transfer_plan.target_document_id, "targetSubPath": transfer_plan.target_sub_path, "resourceTransferPlan": transfer_plan, "streamDeltaHint": tree_stream_delta_hint("asset_result", json!({ "itemsField": "items", })), "domainEventHint": tree_domain_event_hint(if action == "copy" { "tree.resource.copied" } else { "tree.resource.moved" }), "domainEventPlan": tree_domain_event_plan( if action == "copy" { "tree.resource.copied" } else { "tree.resource.moved" }, tree_stream_delta_hint("asset_result", json!({ "itemsField": "items", })), ), }), })) } "tree.resource.upload" => { let payload: ResourceUploadCommandPayload = parse_payload(command_wire.payload.clone())?; let upload_plan = build_resource_upload_plan(&payload)?; let command = CommandEnvelope { name: command_wire.name.clone(), command_id: command_wire.command_id.clone(), idempotency_key: command_wire.idempotency_key.clone(), actor: to_actor_payload(&command_wire.actor), source: to_source_payload(&command_wire.source), target: to_target_ref(command_wire.target.as_ref()), payload: upload_plan.asset_id.clone(), reason: command_wire.reason, refs: command_wire.refs, dry_run: command_wire.dry_run, validate_only: command_wire.validate_only, }; let request = build_write_request(&context, &command)?; Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan { command_name: command.name, command_id: command.command_id, function_name: request.function_name, workspace_id: request.workspace_id, request_id: request.request_id, trace_id: request.trace_id, actor_id: request.actor_id, idempotency_key: request.idempotency_key, source: workspace_source_value(&command_wire.source), payload_json: request.payload_json, args_json: json!({ "assetId": upload_plan.asset_id, "workspaceId": upload_plan.workspace_id, "targetDocumentId": upload_plan.target_document_id, "targetSubPath": upload_plan.target_sub_path, "fileName": upload_plan.file_name, "fileSize": upload_plan.file_size, "mimeType": upload_plan.mime_type, "assetType": upload_plan.asset_type, "resourceUploadPlan": upload_plan, "streamDeltaHint": tree_stream_delta_hint("asset_result", json!({ "itemsField": "items", })), "domainEventHint": tree_domain_event_hint("tree.resource.uploaded"), "domainEventPlan": tree_domain_event_plan( "tree.resource.uploaded", tree_stream_delta_hint("asset_result", json!({ "itemsField": "items", })), ), }), })) } other => Err(BridgeError::validation(format!( "bridge runtime 暂不支持 command: {other}" ))), } } fn parse_payload(value: Value) -> Result where T: DeserializeOwned, { serde_json::from_value(value).map_err(|error| { BridgeError::validation(format!("bridge runtime payload 反序列化失败: {error}")) }) } fn normalize_editor_block_type_for_save(raw_type: &str) -> EditorBlockType { match raw_type { "heading" => EditorBlockType::Heading, "bullet_list_item" | "bullet_list" | "bullet-list" => EditorBlockType::BulletListItem, "numbered_list_item" | "ordered_list" | "ordered-list" => EditorBlockType::NumberedListItem, "todo" | "task" => EditorBlockType::Todo, "quote" | "blockquote" => EditorBlockType::Quote, "code" | "code_block" | "code-block" => EditorBlockType::CodeBlock, "divider" | "horizontal_rule" | "horizontalrule" => EditorBlockType::Divider, "table" => EditorBlockType::Table, "image" | "picture" => EditorBlockType::Image, "toc" | "toc_node" | "tocnode" => EditorBlockType::Toc, "page_reference" => EditorBlockType::PageReference, "block_reference" => EditorBlockType::BlockReference, _ => EditorBlockType::Paragraph, } } fn normalize_editor_block_from_legacy(block: &Value, index: usize) -> EditorBlock { let block_id = read_trimmed_string_field(block, &["blockId", "id"]) .unwrap_or_else(|| format!("block-{}", index + 1)); let raw_type = read_trimmed_string_field(block, &["blockType", "type"]) .unwrap_or_else(|| "paragraph".into()) .to_lowercase(); let mut props = BlockProps::default(); if matches!( normalize_editor_block_type_for_save(&raw_type), EditorBlockType::Heading ) { props.heading_level = block .get("props") .and_then(Value::as_object) .and_then(|map| map.get("level").or_else(|| map.get("headingLevel"))) .and_then(Value::as_u64) .and_then(|value| u8::try_from(value).ok()) .map(|value| value.clamp(1, 6)); props.collapsed = block .get("props") .and_then(Value::as_object) .and_then(|map| map.get("collapsed")) .and_then(Value::as_bool); } if matches!( normalize_editor_block_type_for_save(&raw_type), EditorBlockType::Todo ) { props.checked = block .get("props") .and_then(Value::as_object) .and_then(|map| map.get("checked")) .and_then(Value::as_bool); } if matches!( normalize_editor_block_type_for_save(&raw_type), EditorBlockType::CodeBlock ) { props.language = block .get("props") .and_then(Value::as_object) .and_then(|map| map.get("language")) .and_then(Value::as_str) .map(ToOwned::to_owned); } if let Some(tiptap_table) = block .get("props") .and_then(Value::as_object) .and_then(|map| map.get("tiptapTable")) .cloned() { props.extra.insert("tiptapTable".into(), tiptap_table); } if let Some(tiptap_image) = block .get("props") .and_then(Value::as_object) .and_then(|map| map.get("tiptapImage")) .cloned() { props.extra.insert("tiptapImage".into(), tiptap_image); } if let Some(tiptap_toc) = block .get("props") .and_then(Value::as_object) .and_then(|map| map.get("tiptapTocNode").or_else(|| map.get("tiptapToc"))) .cloned() { props.extra.insert("tiptapTocNode".into(), tiptap_toc); } let text = read_trimmed_string_field(block, &["content"]) .filter(|value| !value.is_empty()) .unwrap_or_else(|| extract_inline_text(block)); EditorBlock { block_id, block_type: normalize_editor_block_type_for_save(&raw_type), props, content_nodes: build_text_content_nodes(&text), child_block_ids: vec![], } } fn editor_document_from_legacy_content(document_id: &str, content: &Value) -> EditorBlockDocument { let blocks = normalize_blocks_from_value(content) .iter() .enumerate() .map(|(index, block)| normalize_editor_block_from_legacy(block, index)) .collect::>(); let root_block_ids = blocks .iter() .map(|block| block.block_id.clone()) .collect::>(); EditorBlockDocument { document_id: document_id.to_string(), root_block_ids, blocks, } } fn normalize_save_editor_document( payload: &DocumentSaveCommandPayload, ) -> Result { if let Some(editor_document) = payload.editor_document.clone() { if let Ok(mut parsed) = serde_json::from_value::(editor_document) { if parsed.document_id.trim().is_empty() { parsed.document_id = payload.document_id.clone(); } if parsed.root_block_ids.is_empty() { parsed.root_block_ids = parsed .blocks .iter() .map(|block| block.block_id.clone()) .collect(); } return Ok(parsed); } } if let Some(tiptap_document) = payload.tiptap_document.clone() { let parsed = serde_json::from_value::(tiptap_document).map_err(|error| { BridgeError::validation(format!("documents.save tiptapDocument 非法: {error}")) })?; return EditorBlockDocumentTiptapBridge::from_tiptap_doc( payload.document_id.clone(), &parsed, ) .map_err(|error| { BridgeError::validation(format!( "documents.save tiptap -> editorDocument 失败: {error:?}" )) }); } Ok(editor_document_from_legacy_content( &payload.document_id, &payload.content, )) } fn legacy_props_from_editor_block(block: &EditorBlock) -> Option { let mut props = serde_json::Map::new(); match block.block_type { EditorBlockType::Heading => { props.insert( "level".into(), json!(block.props.heading_level.unwrap_or(1)), ); if let Some(collapsed) = block.props.collapsed { props.insert("collapsed".into(), json!(collapsed)); } } EditorBlockType::Todo => { props.insert( "checked".into(), json!(block.props.checked.unwrap_or(false)), ); } EditorBlockType::CodeBlock => { props.insert("language".into(), json!(block.props.language)); } EditorBlockType::Table => { if let Some(tiptap_table) = block.props.extra.get("tiptapTable") { props.insert("tiptapTable".into(), tiptap_table.clone()); } } EditorBlockType::Image => { if let Some(tiptap_image) = block.props.extra.get("tiptapImage") { props.insert("tiptapImage".into(), tiptap_image.clone()); if let Some(attrs) = tiptap_image.get("attrs").and_then(Value::as_object) { for key in ["src", "alt", "title"] { if let Some(value) = attrs.get(key) { props.insert(key.into(), value.clone()); } } } } } EditorBlockType::Toc => { if let Some(tiptap_toc) = block.props.extra.get("tiptapTocNode") { props.insert("tiptapTocNode".into(), tiptap_toc.clone()); } } _ => {} } if let Some(text_align) = block .props .extra .get("textAlign") .or_else(|| block.props.extra.get("text_align")) .and_then(Value::as_str) .map(str::trim) .filter(|value| matches!(*value, "left" | "center" | "right" | "justify")) { props.insert("textAlign".into(), json!(text_align)); } if props.is_empty() { None } else { Some(Value::Object(props)) } } fn legacy_type_from_editor_block(block: &EditorBlock) -> &'static str { match block.block_type { EditorBlockType::Paragraph => "paragraph", EditorBlockType::Heading => "heading", EditorBlockType::BulletListItem => "bullet_list_item", EditorBlockType::NumberedListItem => "numbered_list_item", EditorBlockType::Quote => "blockquote", EditorBlockType::Todo => "todo", EditorBlockType::CodeBlock => "code_block", EditorBlockType::Divider => "divider", EditorBlockType::Table => "table", EditorBlockType::Image => "image", EditorBlockType::Toc => "toc", EditorBlockType::PageReference => "page_reference", EditorBlockType::BlockReference => "block_reference", } } fn legacy_text_from_editor_block(block: &EditorBlock) -> String { block .content_nodes .iter() .filter_map(|node| match &node.payload { ContentNodePayload::Text { text, .. } => Some(text.as_str()), ContentNodePayload::HardBreak => Some("\n"), ContentNodePayload::ReferenceToken { token } => token.label.as_deref(), }) .collect::() } fn legacy_content_from_editor_document(document: &EditorBlockDocument) -> Value { fn block_to_legacy_value(block: &EditorBlock, document: &EditorBlockDocument) -> Value { let children = block .child_block_ids .iter() .filter_map(|child_id| { document .blocks .iter() .find(|candidate| &candidate.block_id == child_id) }) .map(|child| block_to_legacy_value(child, document)) .collect::>(); let mut value = json!({ "id": block.block_id, "type": legacy_type_from_editor_block(block), "props": legacy_props_from_editor_block(block), "content": if matches!(block.block_type, EditorBlockType::Divider) { Value::Array(Vec::new()) } else { Value::String(legacy_text_from_editor_block(block)) }, }); if !children.is_empty() { if let Value::Object(map) = &mut value { map.insert("children".into(), Value::Array(children)); } } value } fn mark_block_tree_seen( block: &EditorBlock, document: &EditorBlockDocument, seen: &mut std::collections::BTreeSet, ) { seen.insert(block.block_id.clone()); for child_id in &block.child_block_ids { if let Some(child) = document .blocks .iter() .find(|candidate| &candidate.block_id == child_id) { mark_block_tree_seen(child, document, seen); } } } let mut ordered = Vec::<&EditorBlock>::new(); let mut seen = std::collections::BTreeSet::::new(); for root_block_id in &document.root_block_ids { if let Some(block) = document .blocks .iter() .find(|block| &block.block_id == root_block_id) { mark_block_tree_seen(block, document, &mut seen); ordered.push(block); } } for block in &document.blocks { if seen.insert(block.block_id.clone()) { ordered.push(block); } } Value::Array( ordered .into_iter() .map(|block| block_to_legacy_value(block, document)) .collect(), ) } fn stable_json_content_hash(value: &Value) -> Result { let serialized = serde_json::to_string(value).map_err(|error| { BridgeError::validation(format!("复合命令 payload content hash 序列化失败: {error}")) })?; let mut hash = 0xcbf29ce484222325u64; for byte in serialized.as_bytes() { hash ^= u64::from(*byte); hash = hash.wrapping_mul(0x100000001b3); } Ok(format!("fnv1a64:{hash:016x}")) } fn editor_document_block_ids(document: &EditorBlockDocument) -> Vec { let mut ids = document.root_block_ids.clone(); for block in &document.blocks { if !ids.iter().any(|id| id == &block.block_id) { ids.push(block.block_id.clone()); } } ids } fn document_save_page_body_domain_event_payload( payload: &DocumentSaveCommandPayload, editor_document: &EditorBlockDocument, ) -> Value { json!({ "page": { "id": payload.document_id.clone(), "workspaceId": payload.workspace_id.clone(), }, "blocks": { "ids": editor_document_block_ids(editor_document), "count": editor_document.blocks.len(), }, }) } fn document_save_snapshot_domain_event_payload( payload: &DocumentSaveCommandPayload, canonical_content: &Value, ) -> Result { Ok(json!({ "page": { "id": payload.document_id.clone(), "workspaceId": payload.workspace_id.clone(), }, "snapshot": { "version": payload.revision, "contentHash": stable_json_content_hash(canonical_content)?, "updatedAt": Value::Null, }, })) } fn block_patch_domain_event_payload(payload: &BlockPatchCommandPayload) -> Value { let next_type = read_trimmed_str_field(&payload.next_block, "type") .or_else(|| read_trimmed_str_field(&payload.next_block, "blockType")); json!({ "document": { "id": payload.document_id.clone(), "workspaceId": payload.workspace_id.clone(), }, "block": { "id": payload.block_id.clone(), }, "patch": { "summary": "replace_block", "nextType": next_type, }, }) } fn block_move_domain_event_payload(payload: &BlockMoveCommandPayload) -> Value { json!({ "block": { "id": payload.block_id.clone(), }, "move": { "sourceDocumentId": payload.source_document_id.clone(), "targetDocumentId": payload.target_document_id.clone(), }, }) } fn block_embed_domain_event_payload(payload: &BlockEmbedCommandPayload) -> Value { json!({ "block": { "id": payload.block_id.clone(), }, "embed": { "sourceDocumentId": payload.source_document_id.clone(), "targetDocumentId": payload.target_document_id.clone(), "targetBlockId": payload.target_block_id.clone(), }, }) } fn to_bridge_context(context: RuntimeBridgeContextWire) -> BridgeContext { BridgeContext { deployment_id: context.deployment_id, project_id: context.project_id, workspace_id: context.workspace_id, request_id: context.request_id, trace_id: context.trace_id, actor_type: context.actor.actor_type, actor_id: context.actor.actor_id, session_id: context.actor.session_id, tenant_id: context.tenant_id, auth_token: context.auth_token, source_channel: context.source.channel, source_client: context.source.client, idempotency_key: context.idempotency_key, validate_only: context.validate_only, dry_run: context.dry_run, } } fn to_actor_payload(actor: &RuntimeActorWire) -> ActorPayload { ActorPayload { actor_type: actor.actor_type.clone(), actor_id: actor.actor_id.clone(), session_id: actor.session_id.clone(), } } fn to_source_payload(source: &RuntimeSourceWire) -> SourcePayload { SourcePayload { channel: source.channel.clone(), client: source.client.clone(), } } fn to_target_ref(target: Option<&RuntimeTargetWire>) -> Option { target.map(|target| TargetRef { workspace_id: target.workspace_id.clone(), page_id: target.page_id.clone(), block_id: target.block_id.clone(), }) } fn bridge_error_kind_to_wire(kind: &BridgeErrorKind) -> &'static str { match kind { BridgeErrorKind::Validation => "validation", BridgeErrorKind::Unauthorized => "unauthorized", BridgeErrorKind::Conflict => "conflict", BridgeErrorKind::NotFound => "not_found", BridgeErrorKind::Transport => "transport", BridgeErrorKind::Rejected => "rejected", } } #[cfg(test)] mod tests { use super::*; fn demo_context() -> RuntimeBridgeContextWire { RuntimeBridgeContextWire { deployment_id: Some("dep_1".into()), project_id: Some("proj_1".into()), workspace_id: Some("ws_1".into()), request_id: "req_1".into(), trace_id: "trace_1".into(), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, tenant_id: None, auth_token: None, idempotency_key: Some("idem_1".into()), validate_only: false, dry_run: false, } } fn demo_tree_rename_command(command_id: &str) -> RuntimeCommandEnvelopeWire { RuntimeCommandEnvelopeWire { name: "tree.node.rename".into(), command_id: command_id.into(), idempotency_key: Some(format!("idem_{command_id}")), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "mnote-tree-shell-v1".into(), client: "mnote-web".into(), source_kind: Some("local_folder".into()), root_uri: Some("file:///tmp/mnote-vault".into()), workspace_id: Some("local_ws".into()), capabilities: vec!["execute-command".into()], }, target: Some(RuntimeTargetWire { workspace_id: Some("local_ws".into()), page_id: Some("local_page".into()), block_id: None, }), payload: json!({ "documentId": "local_page", "title": "本地页面", }), preflight_data: None, reason: Some("mock workspace source".into()), refs: vec!["workspace-source-test".into()], dry_run: false, validate_only: false, } } #[test] fn blocks_get_query_plan_maps_to_blocks_get_by_id() { let plan = execute_runtime_input(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "blocks.get".into(), payload: json!({ "blockId": "block_1", "workspaceId": "ws_1", }), }, data: None, }) .expect("query plan should build"); match plan { RuntimeExecutionPlan::Query(plan) => { assert_eq!(plan.function_name, "blocks:getById"); assert_eq!( plan.args_json, json!({ "id": "block_1", "workspaceId": "ws_1", }) ); } RuntimeExecutionPlan::Command(_) => panic!("expected query plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected query plan"), } } #[test] fn blocks_patch_command_plan_maps_to_documents_update_content() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "blocks.patch".into(), command_id: "cmd_patch_1".into(), idempotency_key: Some("idem_patch".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: Some("block_1".into()), }), payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", "blockId": "block_1", "nextBlock": { "id": "block_1", "type": "paragraph", }, }), preflight_data: None, reason: Some("替换块快照".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("command plan should build"); match plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.function_name, "documents:updateContent"); assert_eq!(plan.command_name, "blocks.patch"); assert_eq!( plan.args_json, json!({ "id": "doc_1", "blockId": "block_1", "nextBlock": { "id": "block_1", "type": "paragraph", }, "streamDeltaHint": { "family": "tree", "kind": "resync_required", "args": { "reason": "blocks.patch", "documentId": "doc_1", "blockId": "block_1" } }, "domainEventHint": { "family": "tree", "eventType": "block.patched" }, "domainEventPlan": { "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "block.patched", "payload": { "document": { "id": "doc_1", "workspaceId": "ws_1" }, "block": { "id": "block_1" }, "patch": { "summary": "replace_block", "nextType": "paragraph" } }, "streamDeltaHint": { "family": "tree", "kind": "resync_required", "args": { "reason": "blocks.patch", "documentId": "doc_1", "blockId": "block_1" } } } }) ); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } } #[test] fn tree_command_plan_keeps_workspace_source_contract_in_envelope() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.node.rename".into(), command_id: "cmd_tree_source_1".into(), idempotency_key: Some("idem_tree_source".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "mnote-tree-shell-v1".into(), client: "mnote-web".into(), source_kind: Some("convex_workspace".into()), root_uri: Some("convex://workspace/ws_1".into()), workspace_id: Some("ws_1".into()), capabilities: vec![ "load-snapshot".into(), "preflight-command".into(), "execute-command".into(), ], }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "title": "新标题", }), preflight_data: None, reason: Some("tree-shell rename".into()), refs: vec!["mnote-web-tree".into()], dry_run: false, validate_only: false, }, }) .expect("command plan should build"); match plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.command_name, "tree.node.rename"); assert_eq!(plan.source["sourceKind"], json!("convex_workspace")); assert_eq!(plan.source["rootUri"], json!("convex://workspace/ws_1")); assert_eq!(plan.source["workspaceId"], json!("ws_1")); assert_eq!( plan.source["capabilities"], json!(["load-snapshot", "preflight-command", "execute-command"]) ); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } } #[test] fn tree_command_rejects_unknown_workspace_source_kind() { let error = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.node.rename".into(), command_id: "cmd_unknown_source".into(), idempotency_key: Some("idem_unknown_source".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "mnote-tree-shell-v1".into(), client: "mnote-web".into(), source_kind: Some("unknown_source".into()), root_uri: Some("unknown://workspace/ws_1".into()), workspace_id: Some("ws_1".into()), capabilities: vec!["execute-command".into()], }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "title": "新标题", }), preflight_data: None, reason: Some("tree-shell rename".into()), refs: vec!["mnote-web-tree".into()], dry_run: false, validate_only: false, }, }) .expect_err("unknown source kind should be rejected"); assert_eq!(error.kind, BridgeErrorKind::Validation); assert!(error.message.contains("unknown_source")); } #[test] fn tree_command_rejects_workspace_source_without_execute_capability() { let error = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.node.rename".into(), command_id: "cmd_missing_execute".into(), idempotency_key: Some("idem_missing_execute".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "mnote-tree-shell-v1".into(), client: "mnote-web".into(), source_kind: Some("convex_workspace".into()), root_uri: Some("convex://workspace/ws_1".into()), workspace_id: Some("ws_1".into()), capabilities: vec!["load-snapshot".into(), "preflight-command".into()], }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "title": "新标题", }), preflight_data: None, reason: Some("tree-shell rename".into()), refs: vec!["mnote-web-tree".into()], dry_run: false, validate_only: false, }, }) .expect_err("source without execute-command should be rejected"); assert_eq!(error.kind, BridgeErrorKind::Validation); assert!(error.message.contains("execute-command")); } struct TestWorkspaceSourceAdapter { source: core_protocol::WorkspaceSource, } impl WorkspaceSourceAdapter for TestWorkspaceSourceAdapter { fn source(&self) -> &core_protocol::WorkspaceSource { &self.source } fn load_snapshot( &self, projection: KernelProjectionKind, ) -> Result { Ok(KernelProjectionResult { projection_id: format!("mock:{projection:?}"), projection, root_node_id: Some("mock_root".into()), items: Vec::new(), edges: Vec::new(), meta: BTreeMap::new(), }) } fn watch(&self) -> Result { Ok(WorkspaceSourceWatch { source_kind: "local_folder".into(), active: true, }) } fn preflight_command( &self, _command: &RuntimeCommandEnvelopeWire, ) -> Result { Ok(json!({ "ok": true, "executor": "mock" })) } fn execute_command( &self, _command: &RuntimeCommandEnvelopeWire, ) -> Result { Ok(json!({ "ok": true, "executor": "mock" })) } fn resolve_page_aggregate( &self, page_id: &str, ) -> Result { build_page_aggregate_projection_result( &json!({ "id": page_id, "workspaceId": self.source.workspace_id, "title": "Mock Page", "content": [], }), page_id, Some(self.source.workspace_id.as_str()), PageAggregateSource::Fixture, ) } } #[test] fn workspace_source_adapter_mock_outputs_file_and_page_tree_snapshots() { let adapter = TestWorkspaceSourceAdapter { source: core_protocol::WorkspaceSource { source_kind: core_protocol::WorkspaceSourceKind::LocalFolder, root_uri: "file:///tmp/mnote-vault".into(), workspace_id: "local_ws".into(), capabilities: vec![ core_protocol::WorkspaceSourceCapability::LoadSnapshot, core_protocol::WorkspaceSourceCapability::Watch, core_protocol::WorkspaceSourceCapability::PreflightCommand, core_protocol::WorkspaceSourceCapability::ExecuteCommand, core_protocol::WorkspaceSourceCapability::ResolvePageAggregate, ], }, }; assert_eq!(adapter.source().root_uri, "file:///tmp/mnote-vault"); assert_eq!( adapter .load_snapshot(KernelProjectionKind::FileTree) .expect("file_tree snapshot") .projection, KernelProjectionKind::FileTree ); assert_eq!( adapter .load_snapshot(KernelProjectionKind::PageTree) .expect("page_tree snapshot") .projection, KernelProjectionKind::PageTree ); assert!(adapter.watch().expect("watch").active); assert_eq!( adapter .preflight_command(&demo_tree_rename_command("cmd_preflight")) .expect("preflight")["executor"], json!("mock") ); assert_eq!( adapter .execute_command(&demo_tree_rename_command("cmd_execute")) .expect("execute")["executor"], json!("mock") ); assert_eq!( adapter .resolve_page_aggregate("local_page") .expect("page aggregate") .page_id, "local_page" ); } #[test] fn sidebar_dataset_query_plan_maps_to_sidebar_dataset_list() { let plan = execute_runtime_input(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "sidebar.dataset.list".into(), payload: json!({ "workspaceId": "ws_1", }), }, data: None, }) .expect("query plan should build"); match plan { RuntimeExecutionPlan::Query(plan) => { assert_eq!(plan.function_name, "sidebar:datasetList"); assert_eq!(plan.args_json, json!({ "workspaceId": "ws_1" })); } RuntimeExecutionPlan::Command(_) => panic!("expected query plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected query plan"), } } #[test] fn search_documents_query_plan_maps_to_search_documents() { let plan = execute_runtime_input(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "search.documents".into(), payload: json!({ "query": "rust", "workspaceId": "ws_1", "pageId": "page_1", "limit": 20, "titleOnly": false, "exact": false, "includeOcr": true, "timeRange": "any", "timeField": "updated", "customRangeFrom": null, "customRangeTo": null, }), }, data: None, }) .expect("query plan should build"); match plan { RuntimeExecutionPlan::Query(plan) => { assert_eq!(plan.function_name, "search:documents"); assert_eq!( plan.args_json, json!({ "query": "rust", "workspaceId": "ws_1", "pageId": "page_1", "limit": 20, "cursor": null, "titleOnly": false, "exact": false, "includeOcr": true, "timeRange": "any", "timeField": "updated", "customRangeFrom": null, "customRangeTo": null, }) ); } RuntimeExecutionPlan::Command(_) => panic!("expected query plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected query plan"), } } #[test] fn page_aggregate_get_query_executes_into_core_projection() { let result = execute_runtime_query(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "page.aggregate.get".into(), payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", }), }, data: Some(json!({ "meta": { "id": "doc_1", "workspace_id": "ws_1", "title": "聚合页面", "parent_id": "root", "updated_at": "2026-04-29T00:00:00Z" }, "content": { "content": [], "revision": 7, "conflict_detection_key": "doc_1:7", "pageSubtree": {"rootNodeId": "doc_1"} } })), }) .expect("page aggregate query should build"); assert_eq!(result["schema"], json!("mnote.page_aggregate.v1")); assert_eq!(result["projectionVersion"], json!(1)); assert_eq!(result["source"], json!("KernelProjection")); assert_eq!(result["pageId"], json!("doc_1")); assert_eq!(result["identity"]["documentId"], json!("doc_1")); assert_eq!(result["body"]["revision"], json!(7)); } #[test] fn search_documents_query_canonical_facade_keeps_projection_owner() { let result = execute_runtime_query(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "search.documents.query".into(), payload: json!({ "query": "rust", "workspaceId": "ws_1", "pageId": null, "limit": 10, "titleOnly": false, "exact": false, "includeOcr": true, "timeRange": "any", "timeField": "updated", "customRangeFrom": null, "customRangeTo": null, }), }, data: Some(json!({ "documents": [ { "id": "page_1", "workspaceId": "ws_1", "title": "Rust Notes", "rawText": "这里有 rust 搜索内容", "createdAt": "2026-04-15T00:00:00Z", "updatedAt": "2026-04-15T01:00:00Z" } ], "mindmaps": [], "tables": [], "tableRows": [], "assets": [] })), }) .expect("canonical search query result should build"); assert_eq!(result["projectionOwner"], json!("rust-kernel")); assert_eq!( result["results"][0]["projectionOwner"], json!("rust-kernel") ); assert_eq!(result["results"][0]["id"], json!("page_1")); } #[test] fn mindmap_projection_get_query_returns_kernel_projection() { let result = execute_runtime_query(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "mindmap.projection.get".into(), payload: json!({ "documentId": "doc_1", "mindmapId": "mind_1", "workspaceId": "ws_1", }), }, data: Some(json!({ "data": { "data": {"uid": "root", "text": "中心主题"}, "children": [ {"data": {"uid": "child_1", "text": "子节点"}, "children": []} ] }, "revision": 3 })), }) .expect("mindmap projection should build"); assert_eq!(result["schema"], json!("mnote.mindmap_projection.v1")); assert_eq!(result["owner"], json!("rust-kernel")); assert_eq!(result["mapId"], json!("mind_1")); assert_eq!(result["revision"], json!(3)); } #[test] fn mindmap_command_apply_plan_uses_kernel_command_facade() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "mindmap.command.apply".into(), command_id: "cmd_mindmap_apply".into(), idempotency_key: None, actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: None, }, source: RuntimeSourceWire { channel: "rust-web".into(), client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: None, payload: json!({ "documentId": "doc_1", "mindmapId": "mind_1", "commands": [ {"type": "renameNode", "mapId": "mind_1", "nodeId": "root", "title": "新标题"} ], "projectionRevision": 3 }), preflight_data: None, reason: Some("mindmap command facade".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("mindmap command facade should plan"); match plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.command_name, "mindmap.command.apply"); assert_eq!(plan.function_name, "mindmaps:applyCommand"); assert_eq!( plan.args_json["canonicalCommand"], json!("mindmap.command.apply") ); } RuntimeExecutionPlan::Query(_) | RuntimeExecutionPlan::Tool(_) => { panic!("expected command plan") } } } #[test] fn search_documents_query_executes_in_rust_runtime() { let result = execute_runtime_query(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "search.documents".into(), payload: json!({ "query": "rust", "workspaceId": "ws_1", "pageId": null, "limit": 10, "titleOnly": false, "exact": false, "includeOcr": true, "timeRange": "any", "timeField": "updated", "customRangeFrom": null, "customRangeTo": null, }), }, data: Some(json!({ "documents": [ { "id": "page_1", "workspaceId": "ws_1", "title": "Rust Notes", "rawText": "这里有 rust 搜索内容", "createdAt": "2026-04-15T00:00:00Z", "updatedAt": "2026-04-15T01:00:00Z" } ], "mindmaps": [], "tables": [], "tableRows": [], "assets": [] })), }) .expect("query result should build"); assert_eq!( result, json!({ "enqueueAssetIds": [], "results": [ { "id": "page_1", "title": "Rust Notes", "snippet": "Rust Notes", "updatedAt": "2026-04-15T01:00:00Z", "createdAt": "2026-04-15T00:00:00Z", "matchField": "title", "hasOcr": false, "publicPath": "/documents/page_1", "nodeId": "page_1", "subtreeRootId": "page_1", "evidence": [ { "kind": "title", "nodeId": "page_1", "snippet": "Rust Notes" } ], "score": 3.0 } ] }) ); } #[test] fn documents_content_query_executes_into_page_subtree_projection() { let result = execute_runtime_query(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "documents.content.get".into(), payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", }), }, data: Some(json!({ "title": "服务端标题", "content": [ { "id": "heading_1", "type": "heading", "props": { "level": 1 }, "content": [{ "type": "text", "text": "章节一" }] } ], "revision": 5, "conflict_detection_key": "doc_1:5" })), }) .expect("query result should build"); assert_eq!(result["revision"], json!(5)); assert_eq!(result["pageSubtree"]["rootNodeId"], json!("doc_1")); assert_eq!( result["pageSubtree"]["outline"][0]["title"], json!("章节一") ); assert_eq!(result["pageSubtree"]["stats"]["headingCount"], json!(1)); } #[test] fn search_recent_query_executes_in_rust_runtime() { let result = execute_runtime_query(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "search.recent".into(), payload: json!({ "workspaceId": "ws_1", "limit": 10, "cursor": null, }), }, data: Some(json!({ "recents": [ { "documentId": "page_1", "workspaceId": "ws_1", "lastAccessedAt": "2026-04-15T02:00:00Z" } ] })), }) .expect("query result should build"); assert_eq!( result, json!({ "items": [ { "documentId": "page_1" } ] }) ); } #[test] fn mindmaps_get_query_plan_maps_to_mindmaps_get() { let plan = execute_runtime_input(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "mindmaps.get".into(), payload: json!({ "documentId": "doc_1", "mindmapId": "mind_1", "workspaceId": "ws_1", }), }, data: None, }) .expect("mindmap query plan should build"); match plan { RuntimeExecutionPlan::Query(plan) => { assert_eq!(plan.function_name, "mindmaps:get"); assert_eq!( plan.args_json, json!({ "docId": "doc_1", "mindmapId": "mind_1", }) ); } _ => panic!("expected query plan"), } } #[test] fn mindmaps_put_command_plan_maps_to_mindmaps_put() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "mindmaps.put".into(), command_id: "cmd_mindmap_put_1".into(), idempotency_key: Some("idem_mindmap_put".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: Some("mind_1".into()), }), payload: json!({ "documentId": "doc_1", "mindmapId": "mind_1", "data": { "data": {"text": "中心主题"}, "children": [], }, "createOnly": true, }), preflight_data: None, reason: Some("保存导图".into()), refs: vec!["task-032".into()], dry_run: false, validate_only: false, }, }) .expect("mindmap command plan should build"); match plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.function_name, "mindmaps:put"); assert_eq!( plan.args_json, json!({ "docId": "doc_1", "mindmapId": "mind_1", "data": { "data": {"text": "中心主题"}, "children": [], }, "createOnly": true, }) ); } _ => panic!("expected command plan"), } } #[test] fn docs_search_tool_plan_uses_transport_and_transform_steps() { let plan = execute_runtime_input(RuntimeInput::Tool { context: demo_context(), tool: RuntimeToolInvocationWire { tool: "docs_search".into(), kind: "query".into(), mode: Some("plan".into()), args_json: json!({ "query": "rust", }), target: None, reason: Some("跨页搜索".into()), refs: vec!["task-050".into()], }, data: None, }) .expect("docs_search plan should build"); match plan { RuntimeExecutionPlan::Tool(plan) => { assert_eq!(plan.tool_name, "docs_search"); assert_eq!(plan.toolset_id, "toolset.docs_read"); assert_eq!(plan.steps.len(), 2); assert_eq!(plan.steps[0].name, "docs_search.dataset"); assert_eq!(plan.steps[1].name, "docs_search"); } _ => panic!("expected tool plan"), } } #[test] fn docs_search_tool_result_uses_rust_search_evaluation() { let result = execute_runtime_query(RuntimeInput::Tool { context: demo_context(), tool: RuntimeToolInvocationWire { tool: "docs_search".into(), kind: "query".into(), mode: Some("result".into()), args_json: json!({ "query": "rust", "limit": 5, }), target: None, reason: Some("跨页搜索".into()), refs: vec!["task-050".into()], }, data: Some(json!({ "source": "convex", "datasets": [ { "workspaceId": "ws_1", "documents": [ { "id": "page_1", "workspaceId": "ws_1", "title": "Rust 文档", "rawText": "这里记录 rust runtime 收口", "createdAt": "2026-04-15T00:00:00Z", "updatedAt": "2026-04-16T00:00:00Z" } ], "mindmaps": [], "tables": [], "tableRows": [], "assets": [] } ] })), }) .expect("docs_search result should build"); assert_eq!(result.get("query").and_then(Value::as_str), Some("rust")); assert_eq!( result .get("results") .and_then(Value::as_array) .map(Vec::len), Some(1) ); assert_eq!(result["results"][0]["id"], json!("page_1")); } #[test] fn docs_read_tool_result_trims_text_and_keeps_content_optional() { let long_text = "a".repeat(260); let result = execute_runtime_query(RuntimeInput::Tool { context: demo_context(), tool: RuntimeToolInvocationWire { tool: "docs_read".into(), kind: "query".into(), mode: Some("result".into()), args_json: json!({ "documentId": "page_1", "maxChars": 200, "includeContent": true, }), target: None, reason: Some("读取文档".into()), refs: vec!["task-050".into()], }, data: Some(json!({ "source": "convex", "title": "示例页面", "workspaceId": "ws_1", "parentId": "parent_1", "updatedAt": "2026-04-16T00:00:00Z", "rawText": long_text, "rawTextLength": 260, "content": {"blocks": [{"id": "b1"}]} })), }) .expect("docs_read result should build"); assert_eq!(result["documentId"], json!("page_1")); let trimmed = result["rawText"] .as_str() .expect("rawText should be string"); assert_eq!(trimmed.chars().count(), 201); assert!(trimmed.ends_with('…')); assert_eq!(result["rawTextLength"], json!(260)); assert_eq!(result["content"]["blocks"][0]["id"], json!("b1")); } #[test] fn doc_get_tool_plan_uses_documents_content_query() { let plan = execute_runtime_input(RuntimeInput::Tool { context: demo_context(), tool: RuntimeToolInvocationWire { tool: "doc_get".into(), kind: "query".into(), mode: Some("plan".into()), args_json: json!({ "maxBlocks": 20, }), target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), reason: Some("查看页面结构".into()), refs: vec!["task-031".into()], }, data: None, }) .expect("tool plan should build"); match plan { RuntimeExecutionPlan::Tool(plan) => { assert_eq!(plan.tool_name, "doc_get"); assert_eq!(plan.invocation_kind, "query"); assert_eq!(plan.execution_mode, "plan"); assert_eq!(plan.toolset_id, "toolset.doc_read"); assert_eq!(plan.steps[0].name, "documents.content.get"); } _ => panic!("expected tool plan"), } } #[test] fn search_web_tool_plan_executes_inside_rust_runtime() { let plan = execute_runtime_input(RuntimeInput::Tool { context: demo_context(), tool: RuntimeToolInvocationWire { tool: "search_web".into(), kind: "query".into(), mode: Some("plan".into()), args_json: json!({ "query": "onlyoffice rust", "count": 6, }), target: None, reason: Some("查询资料".into()), refs: vec!["task-040".into()], }, data: None, }) .expect("search_web plan should build"); match plan { RuntimeExecutionPlan::Tool(plan) => { assert_eq!(plan.tool_name, "search_web"); assert_eq!(plan.toolset_id, "toolset.readonly"); assert_eq!(plan.steps.len(), 1); assert_eq!(plan.steps[0].kind, "external"); assert_eq!(plan.steps[0].name, "search_web"); } _ => panic!("expected tool plan"), } } #[test] fn image_read_tool_plan_uses_transport_step() { let plan = execute_runtime_input(RuntimeInput::Tool { context: demo_context(), tool: RuntimeToolInvocationWire { tool: "image_read".into(), kind: "query".into(), mode: Some("plan".into()), args_json: json!({ "assetId": "asset_1", }), target: None, reason: Some("读取 OCR".into()), refs: vec!["task-040".into()], }, data: None, }) .expect("image_read plan should build"); match plan { RuntimeExecutionPlan::Tool(plan) => { assert_eq!(plan.tool_name, "image_read"); assert_eq!(plan.toolset_id, "toolset.media_read"); assert_eq!(plan.steps.len(), 1); assert_eq!(plan.steps[0].kind, "transport"); assert_eq!(plan.steps[0].name, "media_assets.resolve"); } _ => panic!("expected tool plan"), } } #[test] fn slash_run_tool_plan_builds_without_ts_fallback() { let plan = execute_runtime_input(RuntimeInput::Tool { context: demo_context(), tool: RuntimeToolInvocationWire { tool: "slash_run".into(), kind: "command".into(), mode: Some("plan".into()), args_json: json!({ "text": "/new 新页面", }), target: None, reason: Some("执行斜杠命令".into()), refs: vec!["task-040".into()], }, data: None, }) .expect("slash_run plan should build"); match plan { RuntimeExecutionPlan::Tool(plan) => { assert_eq!(plan.tool_name, "slash_run"); assert_eq!(plan.toolset_id, "toolset.slash_write"); assert_eq!(plan.steps.len(), 1); assert_eq!(plan.steps[0].kind, "transform"); assert_eq!(plan.steps[0].name, "slash_run"); } _ => panic!("expected tool plan"), } } #[test] fn slash_run_tool_result_parses_new_command_inside_rust() { let result = execute_runtime_query(RuntimeInput::Tool { context: demo_context(), tool: RuntimeToolInvocationWire { tool: "slash_run".into(), kind: "command".into(), mode: Some("result".into()), args_json: json!({ "text": "/new 新页面", }), target: None, reason: Some("执行斜杠命令".into()), refs: vec!["task-040".into()], }, data: Some(json!({ "source": "ai-agent-route" })), }) .expect("slash_run result should build"); assert_eq!(result.get("ok"), Some(&json!(true))); assert_eq!(result.get("source"), Some(&json!("ai-agent-route"))); assert_eq!(result.pointer("/parsed/command"), Some(&json!("new_doc"))); assert_eq!( result.pointer("/parsed/params/title"), Some(&json!("新页面")) ); } #[test] fn doc_replace_range_tool_executes_in_rust_runtime() { let result = execute_runtime_query(RuntimeInput::Tool { context: demo_context(), tool: RuntimeToolInvocationWire { tool: "doc_replace_range".into(), kind: "command".into(), mode: Some("result".into()), args_json: json!({ "blockId": "block_1", "text": "新的正文", "mode": "replace", }), target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: Some("block_1".into()), }), reason: Some("改写段落".into()), refs: vec!["task-031".into()], }, data: Some(json!({ "source": "client", "blocks": [ { "id": "block_1", "type": "paragraph", "content": [{"type":"text","text":"旧内容"}], "children": [] } ] })), }) .expect("tool result should build"); assert_eq!( result, json!({ "ok": true, "source": "client", "blockId": "block_1", "mode": "replace", "editorCommandSource": "rust_editor_command", "editorCommands": [ { "kind": "replace_block", "blockId": "block_1", "blockType": null, "props": null, "contentNodes": [ { "payload": { "type": "text", "text": "新的正文", "marks": [] }, "attrs": {} } ] } ], "data": [ { "id": "block_1", "type": "paragraph", "content": [{"type":"text","text":"新的正文"}], "children": [] } ] }) ); } #[test] fn doc_insert_blocks_tool_emits_editor_commands() { let result = execute_runtime_query(RuntimeInput::Tool { context: demo_context(), tool: RuntimeToolInvocationWire { tool: "doc_insert_blocks".into(), kind: "command".into(), mode: Some("result".into()), args_json: json!({ "afterBlockId": "block_1", "blocks": [ { "type": "heading", "level": 2, "text": "Rust 命令插入标题" } ] }), target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: Some("block_1".into()), }), reason: Some("在块后插入标题".into()), refs: vec!["task-054".into()], }, data: Some(json!({ "source": "client", "blocks": [ { "id": "block_1", "type": "paragraph", "content": [{"type":"text","text":"旧内容"}], "children": [] } ] })), }) .expect("tool result should build"); assert_eq!(result.get("ok"), Some(&json!(true))); assert_eq!( result.get("editorCommandSource"), Some(&json!("rust_editor_command")) ); assert_eq!( result.pointer("/editorCommands/0/kind"), Some(&json!("insert_block_after")) ); assert_eq!( result.pointer("/editorCommands/0/afterBlockId"), Some(&json!("block_1")) ); assert_eq!( result.pointer("/editorCommands/0/block/blockType"), Some(&json!("heading")) ); assert_eq!( result.pointer("/editorCommands/0/block/props/headingLevel"), Some(&json!(2)) ); assert_eq!( result.pointer("/editorCommands/0/block/contentNodes/0/payload/text"), Some(&json!("Rust 命令插入标题")) ); } #[test] fn documents_save_command_plan_normalizes_tiptap_into_rust_truth() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "documents.save".into(), command_id: "cmd_save_1".into(), idempotency_key: Some("idem_save".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", "revision": 4, "content": [], "tiptapDocument": { "type": "doc", "content": [ { "type": "paragraph", "attrs": { "blockId": "p_1" }, "content": [ { "type": "text", "text": "来自 Rust tiptap 归一化" } ] } ] }, "conflictDetectionKey": "doc_1:4" }), preflight_data: None, reason: Some("保存正文".into()), refs: vec!["task-055".into()], dry_run: false, validate_only: false, }, }) .expect("documents.save plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!(plan.function_name, "documents:updateContent"); assert_eq!(plan.args_json.get("id"), Some(&json!("doc_1"))); assert_eq!( plan.args_json.pointer("/editorDocument/rootBlockIds/0"), Some(&json!("p_1")) ); assert_eq!( plan.args_json.pointer("/editorDocument/blocks/0/blockType"), Some(&json!("paragraph")) ); assert_eq!(plan.args_json.pointer("/content/0/id"), Some(&json!("p_1"))); assert_eq!( plan.args_json.pointer("/content/0/content"), Some(&json!("来自 Rust tiptap 归一化")) ); } #[test] fn documents_save_command_plan_preserves_horizontal_rule_as_divider() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "documents.save".into(), command_id: "cmd_save_divider".into(), idempotency_key: Some("idem_save_divider".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "rust-web".into(), client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", "revision": 5, "content": [], "tiptapDocument": { "type": "doc", "content": [ { "type": "horizontalRule", "attrs": { "blockId": "divider_1" } } ] }, "conflictDetectionKey": "doc_1:5" }), preflight_data: None, reason: Some("保存分割线".into()), refs: vec!["phase-e-divider".into()], dry_run: false, validate_only: false, }, }) .expect("documents.save horizontalRule plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!( plan.args_json.pointer("/editorDocument/blocks/0/blockType"), Some(&json!("divider")) ); assert_eq!( plan.args_json.pointer("/content/0/type"), Some(&json!("divider")) ); assert_eq!( plan.args_json.pointer("/content/0/content"), Some(&json!([])) ); } #[test] fn documents_save_command_plan_preserves_tiptap_image() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "documents.save".into(), command_id: "cmd_save_image".into(), idempotency_key: Some("idem_save_image".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "rust-web".into(), client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", "revision": 5, "content": [], "tiptapDocument": { "type": "doc", "content": [{ "type": "image", "attrs": { "src": "/api/editor/image-placeholder.svg", "alt": "E24 图片占位", "title": "E24 图片" } }] }, "conflictDetectionKey": "doc_1:5" }), preflight_data: None, reason: Some("保存图片".into()), refs: vec!["phase-e-image".into()], dry_run: false, validate_only: false, }, }) .expect("documents.save image plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!( plan.args_json.pointer("/editorDocument/blocks/0/blockType"), Some(&json!("image")) ); assert_eq!( plan.args_json.pointer("/content/0/type"), Some(&json!("image")) ); assert_eq!( plan.args_json.pointer("/content/0/props/tiptapImage/type"), Some(&json!("image")) ); assert_eq!( plan.args_json.pointer("/content/0/props/src"), Some(&json!("/api/editor/image-placeholder.svg")) ); } #[test] fn documents_save_command_plan_preserves_tiptap_table() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "documents.save".into(), command_id: "cmd_save_table".into(), idempotency_key: Some("idem_save_table".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "rust-web".into(), client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", "revision": 5, "content": [], "tiptapDocument": { "type": "doc", "content": [{ "type": "table", "attrs": { "blockId": "table_1" }, "content": [{ "type": "tableRow", "content": [{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": null }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "A1" }] }] }] }] }] }, "conflictDetectionKey": "doc_1:5" }), preflight_data: None, reason: Some("保存简单表格".into()), refs: vec!["phase-e-table".into()], dry_run: false, validate_only: false, }, }) .expect("documents.save table plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!( plan.args_json.pointer("/editorDocument/blocks/0/blockType"), Some(&json!("table")) ); assert_eq!( plan.args_json.pointer("/content/0/type"), Some(&json!("table")) ); assert_eq!( plan.args_json.pointer("/content/0/props/tiptapTable/type"), Some(&json!("table")) ); assert_eq!( plan.args_json.pointer("/content/0/content"), Some(&json!("A1")) ); } #[test] fn documents_save_command_plan_preserves_nested_list_children() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "documents.save".into(), command_id: "cmd_save_nested_list".into(), idempotency_key: Some("idem_save_nested_list".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "rust-web".into(), client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", "revision": 5, "content": [], "tiptapDocument": { "type": "doc", "content": [{ "type": "bulletList", "content": [{ "type": "listItem", "attrs": { "blockId": "parent" }, "content": [ { "type": "paragraph", "content": [{ "type": "text", "text": "E19 parent" }] }, { "type": "bulletList", "content": [{ "type": "listItem", "attrs": { "blockId": "child" }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "E19 child" }] }] }]} ] }] }] }, "conflictDetectionKey": "doc_1:5" }), preflight_data: None, reason: Some("保存嵌套列表".into()), refs: vec!["phase-e-indent".into()], dry_run: false, validate_only: false, }, }) .expect("documents.save nested list plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!( plan.args_json.pointer("/editorDocument/rootBlockIds"), Some(&json!(["parent"])) ); assert_eq!( plan.args_json .pointer("/editorDocument/blocks/0/childBlockIds"), Some(&json!(["child"])) ); assert_eq!( plan.args_json.pointer("/content/0/children/0/type"), Some(&json!("bullet_list_item")) ); assert_eq!( plan.args_json.pointer("/content/0/children/0/content"), Some(&json!("E19 child")) ); assert_eq!(plan.args_json.pointer("/content/1"), None); } #[test] fn page_body_save_command_plan_uses_canonical_page_command() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "page.body.save".into(), command_id: "cmd_page_body_save".into(), idempotency_key: None, actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: None, }, source: RuntimeSourceWire { channel: "rust-web".into(), client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", "revision": 1, "content": [], "conflictDetectionKey": "doc_1:1" }), preflight_data: None, reason: Some("canonical body save".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("page.body.save plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!(plan.command_name, "page.body.save"); assert_eq!(plan.function_name, "documents:updateContent"); } #[test] fn page_head_update_title_command_plan_uses_canonical_page_command() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "page.head.updateTitle".into(), command_id: "cmd_page_head_title".into(), idempotency_key: None, actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: None, }, source: RuntimeSourceWire { channel: "rust-web".into(), client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: None, payload: json!({ "documentId": "doc_1", "title": "新标题" }), preflight_data: None, reason: Some("canonical title update".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("page.head.updateTitle plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!(plan.command_name, "page.head.updateTitle"); assert_eq!(plan.function_name, "documents:updateTitle"); } #[test] fn page_layout_update_options_command_plan_uses_canonical_page_command() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "page.layout.updateOptions".into(), command_id: "cmd_page_layout_options".into(), idempotency_key: None, actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: None, }, source: RuntimeSourceWire { channel: "rust-web".into(), client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: None, payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", "options": {"showToc": true} }), preflight_data: None, reason: Some("canonical layout update".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("page.layout.updateOptions plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!(plan.command_name, "page.layout.updateOptions"); assert_eq!(plan.function_name, "documents:updateOptions"); } #[test] fn documents_save_command_plan_falls_back_from_invalid_editor_document_to_tiptap() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "documents.save".into(), command_id: "cmd_save_2".into(), idempotency_key: Some("idem_save_2".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", "revision": 6, "editorDocument": { "documentId": "doc_1", "rootBlockIds": ["legacy_block_1"], "blocks": [ { "blockId": "legacy_block_1", "blockType": "paragraph", "contentNodes": [ { "attrs": {} } ], "childBlockIds": [] } ] }, "tiptapDocument": { "type": "doc", "content": [ { "type": "paragraph", "attrs": { "blockId": "p_1" }, "content": [ { "type": "text", "text": "来自 tiptap 回退" } ] } ] }, "content": [ { "id": "content_block_1", "type": "paragraph", "content": "来自 content 回退" } ], "conflictDetectionKey": "doc_1:6" }), preflight_data: None, reason: Some("保存正文".into()), refs: vec!["task-save-fallback".into()], dry_run: false, validate_only: false, }, }) .expect("documents.save plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!(plan.function_name, "documents:updateContent"); assert_eq!(plan.args_json.pointer("/content/0/id"), Some(&json!("p_1"))); assert_eq!( plan.args_json.pointer("/content/0/content"), Some(&json!("来自 tiptap 回退")) ); assert_eq!( plan.args_json.pointer("/editorDocument/rootBlockIds/0"), Some(&json!("p_1")) ); assert_eq!( plan.args_json.pointer("/editorDocument/blocks/0/blockId"), Some(&json!("p_1")) ); } #[test] fn documents_save_command_plan_prefers_valid_editor_document_over_other_sources() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "documents.save".into(), command_id: "cmd_save_3".into(), idempotency_key: Some("idem_save_3".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", "revision": 7, "editorDocument": { "documentId": "doc_1", "rootBlockIds": ["editor_block_1"], "blocks": [ { "blockId": "editor_block_1", "blockType": "paragraph", "contentNodes": [ { "payload": { "type": "text", "text": "来自 editorDocument", "marks": [] }, "attrs": {} } ], "childBlockIds": [] } ] }, "tiptapDocument": { "type": "doc", "content": [ { "type": "paragraph", "attrs": { "blockId": "p_1" }, "content": [ { "type": "text", "text": "来自 tiptap" } ] } ] }, "content": [ { "id": "content_block_1", "type": "paragraph", "content": "来自 content" } ], "conflictDetectionKey": "doc_1:7" }), preflight_data: None, reason: Some("保存正文".into()), refs: vec!["task-save-prefer-editor".into()], dry_run: false, validate_only: false, }, }) .expect("documents.save plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!(plan.function_name, "documents:updateContent"); assert_eq!( plan.args_json.pointer("/content/0/content"), Some(&json!("来自 editorDocument")) ); assert_eq!( plan.args_json .pointer("/editorDocument/blocks/0/contentNodes/0/payload/text"), Some(&json!("来自 editorDocument")) ); assert_eq!( plan.args_json.pointer("/content/0/id"), Some(&json!("editor_block_1")) ); } #[test] fn documents_save_command_plan_supports_legacy_content_only_payloads() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "page.body.save".into(), command_id: "cmd_save_4".into(), idempotency_key: Some("idem_save_4".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", "revision": 8, "content": [ { "id": "legacy_content_1", "type": "paragraph", "content": "来自旧式 content-only 保存" } ], "conflictDetectionKey": "doc_1:8" }), preflight_data: None, reason: Some("保存正文".into()), refs: vec!["task-save-content-only".into()], dry_run: false, validate_only: false, }, }) .expect("page.body.save plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!(plan.function_name, "documents:updateContent"); assert_eq!( plan.args_json.pointer("/content/0/id"), Some(&json!("legacy_content_1")) ); assert_eq!( plan.args_json.pointer("/content/0/content"), Some(&json!("来自旧式 content-only 保存")) ); assert_eq!( plan.args_json.pointer("/editorDocument/rootBlockIds/0"), Some(&json!("legacy_content_1")) ); } #[test] fn documents_save_command_plans_include_formal_domain_event_contract() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "page.body.save".into(), command_id: "cmd_save_contract_1".into(), idempotency_key: Some("idem_save_contract_1".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", "revision": 8, "content": [ { "id": "legacy_content_1", "type": "paragraph", "content": "来自旧式 content-only 保存" } ], "conflictDetectionKey": "doc_1:8" }), preflight_data: None, reason: Some("保存正文".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("page.body.save plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!( plan.args_json["streamDeltaHint"], json!({ "family": "tree", "kind": "resync_required", "args": { "reason": "page_body_saved", "pageId": "doc_1", "documentId": "doc_1" } }) ); assert_eq!( plan.args_json["domainEventHint"], json!({ "family": "tree", "eventType": "page.body.saved" }) ); assert_eq!( plan.args_json["domainEventPlan"], json!({ "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "page.body.saved", "payload": { "page": { "id": "doc_1", "workspaceId": "ws_1" }, "blocks": { "ids": ["legacy_content_1"], "count": 1 } }, "streamDeltaHint": { "family": "tree", "kind": "resync_required", "args": { "reason": "page_body_saved", "pageId": "doc_1", "documentId": "doc_1" } } }) ); assert_eq!( plan.args_json["domainEventPlans"], json!([ { "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "page.body.saved", "payload": { "page": { "id": "doc_1", "workspaceId": "ws_1" }, "blocks": { "ids": ["legacy_content_1"], "count": 1 } }, "streamDeltaHint": { "family": "tree", "kind": "resync_required", "args": { "reason": "page_body_saved", "pageId": "doc_1", "documentId": "doc_1" } } }, { "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "document.snapshot.saved", "payload": { "page": { "id": "doc_1", "workspaceId": "ws_1" }, "snapshot": { "version": 8, "contentHash": "fnv1a64:d039f8f3496411e8", "updatedAt": null } }, "streamDeltaHint": { "family": "tree", "kind": "resync_required", "args": { "reason": "page_body_saved", "pageId": "doc_1", "documentId": "doc_1" } } } ]) ); } #[test] fn blocks_move_command_plan_maps_to_blocks_move() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "blocks.move".into(), command_id: "cmd_move_1".into(), idempotency_key: Some("idem_move".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: Some("block_1".into()), }), payload: json!({ "sourceDocumentId": "doc_1", "blockId": "block_1", "targetDocumentId": "doc_2", }), preflight_data: None, reason: Some("移动块".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("command plan should build"); match plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.function_name, "blocks:move"); assert_eq!(plan.command_name, "blocks.move"); assert_eq!( plan.args_json, json!({ "id": "block_1", "sourceDocumentId": "doc_1", "targetDocumentId": "doc_2", "streamDeltaHint": { "family": "tree", "kind": "resync_required", "args": { "reason": "blocks.move", "documentId": "doc_2", "blockId": "block_1" } }, "domainEventHint": { "family": "tree", "eventType": "block.moved" }, "domainEventPlan": { "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "block.moved", "payload": { "block": { "id": "block_1" }, "move": { "sourceDocumentId": "doc_1", "targetDocumentId": "doc_2" } }, "streamDeltaHint": { "family": "tree", "kind": "resync_required", "args": { "reason": "blocks.move", "documentId": "doc_2", "blockId": "block_1" } } } }) ); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } } #[test] fn blocks_embed_command_plan_maps_to_blocks_embed() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "blocks.embed".into(), command_id: "cmd_embed_1".into(), idempotency_key: Some("idem_embed".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_2".into()), block_id: Some("block_2".into()), }), payload: json!({ "sourceDocumentId": "doc_1", "blockId": "block_1", "targetDocumentId": "doc_2", "targetBlockId": "anchor_1", }), preflight_data: None, reason: Some("嵌入块".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("command plan should build"); match plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.function_name, "blocks:insert"); assert_eq!(plan.command_name, "blocks.embed"); assert_eq!( plan.args_json, json!({ "sourceDocumentId": "doc_1", "blockId": "block_1", "targetDocumentId": "doc_2", "targetBlockId": "anchor_1", "streamDeltaHint": { "family": "tree", "kind": "resync_required", "args": { "reason": "blocks.embed", "documentId": "doc_2", "blockId": "block_1" } }, "domainEventHint": { "family": "tree", "eventType": "block.embedded" }, "domainEventPlan": { "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "block.embedded", "payload": { "block": { "id": "block_1" }, "embed": { "sourceDocumentId": "doc_1", "targetDocumentId": "doc_2", "targetBlockId": "anchor_1" } }, "streamDeltaHint": { "family": "tree", "kind": "resync_required", "args": { "reason": "blocks.embed", "documentId": "doc_2", "blockId": "block_1" } } } }) ); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } } #[test] fn block_commands_include_formal_domain_event_contract() { let cases = [ ( "blocks.patch", json!({ "documentId": "doc_1", "workspaceId": "ws_1", "blockId": "block_1", "nextBlock": { "id": "block_1", "type": "paragraph" } }), "block.patched", json!({ "document": { "id": "doc_1", "workspaceId": "ws_1" }, "block": { "id": "block_1" }, "patch": { "summary": "replace_block", "nextType": "paragraph" } }), ), ( "blocks.move", json!({ "sourceDocumentId": "doc_1", "blockId": "block_1", "targetDocumentId": "doc_2" }), "block.moved", json!({ "block": { "id": "block_1" }, "move": { "sourceDocumentId": "doc_1", "targetDocumentId": "doc_2" } }), ), ( "blocks.embed", json!({ "sourceDocumentId": "doc_1", "blockId": "block_1", "targetDocumentId": "doc_2", "targetBlockId": "anchor_1" }), "block.embedded", json!({ "block": { "id": "block_1" }, "embed": { "sourceDocumentId": "doc_1", "targetDocumentId": "doc_2", "targetBlockId": "anchor_1" } }), ), ]; for (command_name, payload, event_type, expected_payload) in cases { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: command_name.into(), command_id: format!("cmd_{command_name}_contract"), idempotency_key: Some(format!("idem_{command_name}_contract")), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_2".into()), block_id: Some("block_1".into()), }), payload, preflight_data: None, reason: Some("块命令 formal contract".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("block command plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!( plan.args_json["streamDeltaHint"], json!({ "family": "tree", "kind": "resync_required", "args": { "reason": command_name, "documentId": if command_name == "blocks.patch" { "doc_1" } else { "doc_2" }, "blockId": "block_1" } }) ); assert_eq!( plan.args_json["domainEventHint"], json!({ "family": "tree", "eventType": event_type }) ); assert_eq!( plan.args_json["domainEventPlan"], json!({ "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": event_type, "payload": expected_payload, "streamDeltaHint": { "family": "tree", "kind": "resync_required", "args": { "reason": command_name, "documentId": if command_name == "blocks.patch" { "doc_1" } else { "doc_2" }, "blockId": "block_1" } } }) ); } } #[test] fn documents_embed_command_plan_maps_to_documents_update_content() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "documents.embed".into(), command_id: "cmd_doc_embed_1".into(), idempotency_key: Some("idem_doc_embed".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_2".into()), block_id: None, }), payload: json!({ "documentId": "doc_2", "workspaceId": "ws_1", "revision": 5, "content": [{ "id": "block_1", "type": "pageReference" }], "conflictDetectionKey": "conflict_5", "sourceDocumentId": "doc_1", "targetDocumentId": "doc_2", "anchorBlockId": "anchor_1", }), preflight_data: None, reason: Some("嵌入页面".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("command plan should build"); match plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.function_name, "documents:updateContent"); assert_eq!(plan.command_name, "documents.embed"); assert_eq!( plan.args_json, json!({ "id": "doc_2", "content": [{ "id": "block_1", "type": "pageReference" }], "expectedRevision": 5, "conflictDetectionKey": "conflict_5", "sourceDocumentId": "doc_1", "targetDocumentId": "doc_2", "anchorBlockId": "anchor_1", "pageAggregateEmbedPlan": null, "streamDeltaHint": { "family": "tree", "kind": "noop", "args": {} }, "domainEventHint": { "family": "tree", "eventType": "tree.node.embedded" }, "domainEventPlan": { "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "tree.node.embedded", "streamDeltaHint": { "family": "tree", "kind": "noop", "args": {} } } }) ); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } } #[test] fn tree_lifecycle_command_aliases_keep_tree_command_names() { let cases = [ ( "tree.node.archive", "documents:softDelete", json!({ "documentId": "doc_1", "workspaceId": "ws_1", }), json!({ "id": "doc_1", "streamDeltaHint": { "family": "tree", "kind": "remove_document", "args": { "documentId": "doc_1" } }, "domainEventHint": { "family": "tree", "eventType": "tree.node.archived" }, "domainEventPlan": { "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "tree.node.archived", "streamDeltaHint": { "family": "tree", "kind": "remove_document", "args": { "documentId": "doc_1" } } } }), ), ( "tree.node.restore", "documents:restore", json!({ "documentId": "doc_1", "workspaceId": "ws_1", }), json!({ "id": "doc_1", "streamDeltaHint": { "family": "tree", "kind": "document_result", "args": { "documentField": "document" } }, "domainEventHint": { "family": "tree", "eventType": "tree.node.restored" }, "domainEventPlan": { "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "tree.node.restored", "streamDeltaHint": { "family": "tree", "kind": "document_result", "args": { "documentField": "document" } } } }), ), ( "tree.node.purge", "documents:purge", json!({ "documentId": "doc_1", }), json!({ "id": "doc_1" }), ), ]; for (command_name, function_name, payload, args_json) in cases { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: command_name.into(), command_id: format!("cmd_{command_name}"), idempotency_key: Some(format!("idem_{command_name}")), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload, preflight_data: None, reason: Some("树命令切流".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("command plan should build"); match plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.function_name, function_name); assert_eq!(plan.command_name, command_name); assert_eq!(plan.args_json, args_json); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } } } #[test] fn tree_subtree_move_command_rejects_self_target_via_preflight() { let error = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.subtree.move".into(), command_id: "cmd_tree_move_self".into(), idempotency_key: Some("idem_tree_move_self".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "parentId": "doc_1", "sortOrder": 0, "movePreflight": { "sourceDocument": { "id": "doc_1", "workspaceId": "ws_1", "parentId": null }, "targetParentDocument": { "id": "doc_1", "workspaceId": "ws_1", "parentId": null }, "targetAncestorIds": [] } }), preflight_data: Some(json!({ "sourceDocument": { "id": "doc_1", "workspaceId": "ws_1", "parentId": null }, "targetParentDocument": { "id": "doc_1", "workspaceId": "ws_1", "parentId": null }, "targetAncestorIds": [] })), reason: Some("树命令切流".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect_err("self move should be rejected"); assert_eq!(error.kind, BridgeErrorKind::Validation); assert_eq!(error.message, "不能把页面移动到自身下面"); } #[test] fn tree_subtree_move_command_rejects_descendant_target_via_preflight() { let error = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.subtree.move".into(), command_id: "cmd_tree_move_descendant".into(), idempotency_key: Some("idem_tree_move_descendant".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "parentId": "child_1", "sortOrder": 0, "movePreflight": { "sourceDocument": { "id": "doc_1", "workspaceId": "ws_1", "parentId": null }, "targetParentDocument": { "id": "child_1", "workspaceId": "ws_1", "parentId": "doc_1" }, "targetAncestorIds": ["doc_1"] } }), preflight_data: Some(json!({ "sourceDocument": { "id": "doc_1", "workspaceId": "ws_1", "parentId": null }, "targetParentDocument": { "id": "child_1", "workspaceId": "ws_1", "parentId": "doc_1" }, "targetAncestorIds": ["doc_1"] })), reason: Some("树命令切流".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect_err("descendant move should be rejected"); assert_eq!(error.kind, BridgeErrorKind::Validation); assert_eq!(error.message, "不能把页面移动到自己的后代下面"); } #[test] fn tree_subtree_move_command_rejects_cross_workspace_target_via_preflight() { let error = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.subtree.move".into(), command_id: "cmd_tree_move_cross_workspace".into(), idempotency_key: Some("idem_tree_move_cross_workspace".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "parentId": "parent_remote", "sortOrder": 0, "movePreflight": { "sourceDocument": { "id": "doc_1", "workspaceId": "ws_1", "parentId": null }, "targetParentDocument": { "id": "parent_remote", "workspaceId": "ws_2", "parentId": null }, "targetAncestorIds": [] } }), preflight_data: Some(json!({ "sourceDocument": { "id": "doc_1", "workspaceId": "ws_1", "parentId": null }, "targetParentDocument": { "id": "parent_remote", "workspaceId": "ws_2", "parentId": null }, "targetAncestorIds": [] })), reason: Some("树命令切流".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect_err("cross-workspace move should be rejected"); assert_eq!(error.kind, BridgeErrorKind::Validation); assert_eq!(error.message, "暂不支持跨工作空间移动页面"); } #[test] fn tree_subtree_move_command_rejects_descendant_target_via_snapshot_documents() { let error = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.subtree.move".into(), command_id: "cmd_tree_move_desc_snapshot".into(), idempotency_key: Some("idem_tree_move_desc_snapshot".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "parentId": "child_1", "sortOrder": 0 }), preflight_data: Some(json!({ "documents": [ { "id": "doc_1", "workspace_id": "ws_1", "parent_id": null }, { "id": "child_1", "workspace_id": "ws_1", "parent_id": "doc_1" } ] })), reason: Some("树命令切流".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect_err("descendant move should be rejected from snapshot documents"); assert_eq!(error.kind, BridgeErrorKind::Validation); assert_eq!(error.message, "不能把页面移动到自己的后代下面"); } #[test] fn tree_subtree_move_command_rejects_missing_target_parent_via_snapshot_documents() { let error = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.subtree.move".into(), command_id: "cmd_tree_move_missing_parent_snapshot".into(), idempotency_key: Some("idem_tree_move_missing_parent_snapshot".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "parentId": "missing_parent", "sortOrder": 0 }), preflight_data: Some(json!({ "documents": [ { "id": "doc_1", "workspace_id": "ws_1", "parent_id": null } ] })), reason: Some("树命令切流".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect_err("missing parent should be rejected from snapshot documents"); assert_eq!(error.kind, BridgeErrorKind::Validation); assert_eq!(error.message, "目标父页面不存在或无权限"); } #[test] fn document_move_order_plan_normalizes_same_parent_order() { let payload = DocumentMoveCommandPayload { document_id: "doc_b".into(), parent_id: Some("root".into()), sort_order: 2, }; let snapshot = serde_json::from_value::(json!({ "documents": [ { "id": "doc_b", "workspace_id": "ws_1", "parent_id": "root", "sort_order": 0, "created_at": "2026-04-25T00:00:02Z" }, { "id": "doc_a", "workspace_id": "ws_1", "parent_id": "root", "sort_order": 0, "created_at": "2026-04-25T00:00:01Z" }, { "id": "doc_c", "workspace_id": "ws_1", "parent_id": "root", "sort_order": null, "created_at": "2026-04-25T00:00:03Z" } ] })) .expect("snapshot"); let plan = build_document_move_order_plan_from_snapshot(&payload, &snapshot) .expect("move order plan"); assert_eq!(plan.normalized_sort_order, 2); assert_eq!( plan.patches, vec![ DocumentMoveOrderPatch { document_id: "doc_c".into(), parent_id: Some("root".into()), sort_order: 1, moved: false, }, DocumentMoveOrderPatch { document_id: "doc_b".into(), parent_id: Some("root".into()), sort_order: 2, moved: true, }, ] ); } #[test] fn document_move_order_plan_accepts_convex_float_sort_order_snapshot() { let payload = DocumentMoveCommandPayload { document_id: "doc_b".into(), parent_id: None, sort_order: 0, }; let snapshot = serde_json::from_value::(json!({ "documents": [ { "id": "doc_a", "workspace_id": "ws_1", "parent_id": null, "sort_order": 0.0, "created_at": "2026-04-25T00:00:01Z" }, { "id": "doc_b", "workspace_id": "ws_1", "parent_id": null, "sort_order": 1.0, "created_at": "2026-04-25T00:00:02Z" } ] })) .expect("snapshot with float sort_order"); let plan = build_document_move_order_plan_from_snapshot(&payload, &snapshot) .expect("move order plan"); assert_eq!(plan.normalized_sort_order, 0); assert!(plan .patches .iter() .any(|patch| patch.document_id == "doc_b" && patch.sort_order == 0 && patch.moved)); } #[test] fn document_move_order_plan_normalizes_cross_parent_and_clamps_index() { let payload = DocumentMoveCommandPayload { document_id: "doc_b".into(), parent_id: Some("target".into()), sort_order: 99, }; let snapshot = serde_json::from_value::(json!({ "documents": [ { "id": "doc_a", "workspace_id": "ws_1", "parent_id": "source", "sort_order": 0, "created_at": "2026-04-25T00:00:01Z" }, { "id": "doc_b", "workspace_id": "ws_1", "parent_id": "source", "sort_order": 1, "created_at": "2026-04-25T00:00:02Z" }, { "id": "doc_c", "workspace_id": "ws_1", "parent_id": "source", "sort_order": 2, "created_at": "2026-04-25T00:00:03Z" }, { "id": "doc_d", "workspace_id": "ws_1", "parent_id": "target", "sort_order": 7, "created_at": "2026-04-25T00:00:04Z" } ] })) .expect("snapshot"); let plan = build_document_move_order_plan_from_snapshot(&payload, &snapshot) .expect("move order plan"); assert_eq!(plan.from_parent_id, Some("source".into())); assert_eq!(plan.to_parent_id, Some("target".into())); assert_eq!(plan.normalized_sort_order, 1); assert_eq!( plan.patches, vec![ DocumentMoveOrderPatch { document_id: "doc_c".into(), parent_id: Some("source".into()), sort_order: 1, moved: false, }, DocumentMoveOrderPatch { document_id: "doc_d".into(), parent_id: Some("target".into()), sort_order: 0, moved: false, }, DocumentMoveOrderPatch { document_id: "doc_b".into(), parent_id: Some("target".into()), sort_order: 1, moved: true, }, ] ); } #[test] fn tree_subtree_move_command_does_not_emit_legacy_normalized_move_fallback() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.subtree.move".into(), command_id: "cmd_tree_move_plan".into(), idempotency_key: Some("idem_tree_move_plan".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_b".into()), block_id: None, }), payload: json!({ "documentId": "doc_b", "parentId": "target", "sortOrder": -2 }), preflight_data: Some(json!({ "documents": [ { "id": "target", "workspace_id": "ws_1", "parent_id": null, "sort_order": 0, "created_at": "2026-04-25T00:00:00Z" }, { "id": "doc_a", "workspace_id": "ws_1", "parent_id": "source", "sort_order": 0, "created_at": "2026-04-25T00:00:01Z" }, { "id": "doc_b", "workspace_id": "ws_1", "parent_id": "source", "sort_order": 1, "created_at": "2026-04-25T00:00:02Z" }, { "id": "doc_c", "workspace_id": "ws_1", "parent_id": "target", "sort_order": 0, "created_at": "2026-04-25T00:00:03Z" } ] })), reason: Some("树命令切流".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("command plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!(plan.command_name, "tree.subtree.move"); assert_eq!(plan.function_name, "documents:move"); assert_eq!(plan.args_json["sortOrder"], json!(-2)); assert!(plan.args_json.get("normalizedMove").is_none()); assert_eq!( plan.args_json["treeWriteOperation"], json!({ "family": "tree", "schema": "mnote.tree.write_operation", "schemaVersion": 1, "operation": "tree.subtree.move.write", "workspaceId": "ws_1", "documentId": "doc_b", "fromParentId": "source", "toParentId": "target", "requestedSortOrder": -2, "normalizedSortOrder": 0, "patches": [ { "documentId": "doc_b", "parentId": "target", "sortOrder": 0, "moved": true }, { "documentId": "doc_c", "parentId": "target", "sortOrder": 1, "moved": false } ] }) ); assert_eq!( plan.args_json["domainEventHint"], json!({ "family": "tree", "eventType": "tree.subtree.moved" }) ); assert_eq!( plan.args_json["domainEventPlan"], json!({ "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "tree.subtree.moved", "streamDeltaHint": { "family": "tree", "kind": "move_document", "args": { "documentId": "doc_b", "parentId": "target", "sortOrder": -2 } } }) ); } #[test] fn tree_subtree_move_command_includes_tree_write_operation_from_snapshot() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.subtree.move".into(), command_id: "cmd_tree_move_write".into(), idempotency_key: Some("idem_tree_move_write".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_b".into()), block_id: None, }), payload: json!({ "documentId": "doc_b", "parentId": "target", "sortOrder": 99 }), preflight_data: Some(json!({ "documents": [ { "id": "target", "workspace_id": "ws_1", "parent_id": null, "sort_order": 0, "created_at": "2026-04-25T00:00:00Z" }, { "id": "doc_a", "workspace_id": "ws_1", "parent_id": "source", "sort_order": 0, "created_at": "2026-04-25T00:00:01Z" }, { "id": "doc_b", "workspace_id": "ws_1", "parent_id": "source", "sort_order": 1, "created_at": "2026-04-25T00:00:02Z" }, { "id": "doc_c", "workspace_id": "ws_1", "parent_id": "target", "sort_order": 0, "created_at": "2026-04-25T00:00:03Z" } ] })), reason: Some("树命令切流".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("command plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!( plan.args_json["treeWriteOperation"], json!({ "family": "tree", "schema": "mnote.tree.write_operation", "schemaVersion": 1, "operation": "tree.subtree.move.write", "workspaceId": "ws_1", "documentId": "doc_b", "fromParentId": "source", "toParentId": "target", "requestedSortOrder": 99, "normalizedSortOrder": 1, "patches": [ { "documentId": "doc_b", "parentId": "target", "sortOrder": 1, "moved": true } ] }) ); } #[test] fn tree_command_artifact_plan_materializes_domain_event_payload_from_rust_plan() { let context = demo_context(); let command = RuntimeCommandEnvelopeWire { name: "tree.subtree.move".into(), command_id: "cmd_tree_move_artifact".into(), idempotency_key: Some("idem_tree_move_artifact".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_b".into()), block_id: None, }), payload: json!({ "documentId": "doc_b", "parentId": "target", "sortOrder": 3 }), preflight_data: Some(json!({ "documents": [ { "id": "target", "workspace_id": "ws_1", "parent_id": null, "sort_order": 0, "created_at": "2026-04-25T00:00:00Z" }, { "id": "doc_b", "workspace_id": "ws_1", "parent_id": null, "sort_order": 0, "created_at": "2026-04-25T00:00:01Z" } ] })), reason: Some("树命令移动".into()), refs: vec!["next-tree-route".into()], dry_run: false, validate_only: false, }; let RuntimeExecutionPlan::Command(plan) = execute_runtime_input(RuntimeInput::Command { context: context.clone(), command: command.clone(), }) .expect("command plan should build") else { panic!("expected command plan"); }; let artifacts = build_runtime_command_artifact_plan( &context, &command, &plan, &json!({ "ok": true, "parent_id": "target", "sort_order": 1, "workspace_id": "ws_1", "updated_at": "2026-04-26T10:00:00Z" }), "2026-04-26T10:00:01Z", ) .expect("artifact plan should build"); assert_eq!(artifacts.command_log.id, "clog_cmd_tree_move_artifact"); assert_eq!(artifacts.command_log.workspace_id, "ws_1"); assert_eq!(artifacts.command_log.command_name, "tree.subtree.move"); assert_eq!( artifacts.command_log.payload["streamDelta"], json!({ "op": "move_document", "documentId": "doc_b", "parentId": "target", "sortOrder": 1, "updatedAt": "2026-04-26T10:00:00Z" }) ); let event = artifacts .domain_event .expect("tree command should have domain event"); assert_eq!(event.id, "evt_cmd_tree_move_artifact"); assert_eq!(event.event_type, "tree.subtree.moved"); assert_eq!(event.aggregate_type, "page"); assert_eq!(event.aggregate_id, "doc_b"); assert_eq!(event.status, "committed"); assert_eq!( event.payload["streamDelta"], json!({ "op": "move_document", "documentId": "doc_b", "parentId": "target", "sortOrder": 1, "updatedAt": "2026-04-26T10:00:00Z" }) ); assert_eq!(event.payload["schema"], json!("mnote.tree.domain_event")); assert_eq!(event.payload["schemaVersion"], json!(1)); } #[test] fn runtime_command_artifact_input_returns_rust_artifact_plan() { let context = demo_context(); let command = RuntimeCommandEnvelopeWire { name: "tree.subtree.move".into(), command_id: "cmd_tree_move_artifact_input".into(), idempotency_key: Some("idem_tree_move_artifact_input".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_b".into()), block_id: None, }), payload: json!({ "documentId": "doc_b", "parentId": "target", "sortOrder": 3 }), preflight_data: Some(json!({ "documents": [ { "id": "target", "workspace_id": "ws_1", "parent_id": null, "sort_order": 0, "created_at": "2026-04-25T00:00:00Z" }, { "id": "doc_b", "workspace_id": "ws_1", "parent_id": null, "sort_order": 0, "created_at": "2026-04-25T00:00:01Z" } ] })), reason: Some("树命令移动".into()), refs: vec!["next-tree-route".into()], dry_run: false, validate_only: false, }; let RuntimeExecutionPlan::Command(plan) = execute_runtime_input(RuntimeInput::Command { context: context.clone(), command: command.clone(), }) .expect("command plan should build") else { panic!("expected command plan"); }; let artifacts = execute_runtime_command_artifact(RuntimeInput::CommandArtifact { context, command, plan, result: json!({ "ok": true, "parent_id": "target", "sort_order": 1, "workspace_id": "ws_1", "updated_at": "2026-04-26T10:00:00Z" }), now: "2026-04-26T10:00:01Z".into(), }) .expect("artifact input should execute") .expect("artifact plan should build"); assert_eq!( artifacts.command_log.payload["streamDelta"], json!({ "op": "move_document", "documentId": "doc_b", "parentId": "target", "sortOrder": 1, "updatedAt": "2026-04-26T10:00:00Z" }) ); assert_eq!( artifacts .domain_event .as_ref() .expect("domain event") .event_type, "tree.subtree.moved" ); } #[test] fn tree_create_and_rename_command_plans_include_stream_delta_hints() { let create_plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.node.create".into(), command_id: "cmd_tree_create_1".into(), idempotency_key: Some("idem_tree_create_1".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_new".into()), block_id: None, }), payload: json!({ "documentId": "doc_new", "workspaceId": "ws_1", "parentId": null, "title": "新页面", "accessScope": "private", "content": [] }), preflight_data: None, reason: Some("树命令创建页面".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("create plan should build"); match create_plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.command_name, "tree.node.create"); assert_eq!(plan.function_name, "documents:createWithParentReference"); assert_eq!( plan.args_json["streamDeltaHint"], json!({ "family": "tree", "kind": "document_result", "args": { "documentField": "document" } }) ); assert_eq!( plan.args_json["domainEventHint"], json!({ "family": "tree", "eventType": "tree.node.created" }) ); assert_eq!( plan.args_json["domainEventPlan"], json!({ "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "tree.node.created", "streamDeltaHint": { "family": "tree", "kind": "document_result", "args": { "documentField": "document" } } }) ); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } let rename_plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.node.rename".into(), command_id: "cmd_tree_rename_1".into(), idempotency_key: Some("idem_tree_rename_1".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "title": "改名" }), preflight_data: None, reason: Some("树命令重命名".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("rename plan should build"); match rename_plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.command_name, "tree.node.rename"); assert_eq!(plan.function_name, "documents:updateTitle"); assert_eq!( plan.args_json["streamDeltaHint"], json!({ "family": "tree", "kind": "upsert_document_patch", "args": { "documentId": "doc_1", "patch": { "title": "改名" } } }) ); assert_eq!( plan.args_json["domainEventHint"], json!({ "family": "tree", "eventType": "tree.node.renamed" }) ); assert_eq!( plan.args_json["domainEventPlan"], json!({ "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "tree.node.renamed", "streamDeltaHint": { "family": "tree", "kind": "upsert_document_patch", "args": { "documentId": "doc_1", "patch": { "title": "改名" } } } }) ); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } } #[test] fn tree_embed_and_copy_aliases_keep_tree_command_names() { let embed_plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.node.embed".into(), command_id: "cmd_tree_embed_1".into(), idempotency_key: Some("idem_tree_embed".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_2".into()), block_id: None, }), payload: json!({ "documentId": "doc_2", "workspaceId": "ws_1", "revision": 5, "conflictDetectionKey": "conflict_5", "sourceDocumentId": "doc_1", "targetDocumentId": "doc_2", "anchorBlockId": "anchor_1", }), preflight_data: Some(json!({ "pageAggregateEmbed": { "sourceDocumentId": "doc_1", "sourceTitle": "来源页面", "targetDocumentId": "doc_2", "targetContent": { "blocks": [ { "id": "anchor_1", "type": "paragraph" } ], "format": "editor" }, "anchorBlockId": "anchor_1", "blockId": "page_ref_doc_1" } })), reason: Some("树命令嵌入页面".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("embed plan should build"); match embed_plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.function_name, "documents:updateContent"); assert_eq!(plan.command_name, "tree.node.embed"); assert_eq!( plan.args_json, json!({ "id": "doc_2", "content": { "blocks": [ { "id": "anchor_1", "type": "paragraph" }, { "id": "page_ref_doc_1", "type": "pageReference", "props": { "pageId": "doc_1", "title": "来源页面" } } ], "format": "editor" }, "expectedRevision": 5, "conflictDetectionKey": "conflict_5", "sourceDocumentId": "doc_1", "targetDocumentId": "doc_2", "anchorBlockId": "anchor_1", "pageAggregateEmbedPlan": { "schema": "mnote.page_aggregate.embed_plan", "schemaVersion": 1, "sourceDocumentId": "doc_1", "targetDocumentId": "doc_2", "anchorBlockId": "anchor_1", "insertIndex": 1, "block": { "id": "page_ref_doc_1", "type": "pageReference", "props": { "pageId": "doc_1", "title": "来源页面" } }, "content": { "blocks": [ { "id": "anchor_1", "type": "paragraph" }, { "id": "page_ref_doc_1", "type": "pageReference", "props": { "pageId": "doc_1", "title": "来源页面" } } ], "format": "editor" }, "blockCount": 2 }, "streamDeltaHint": { "family": "tree", "kind": "noop", "args": {} }, "domainEventHint": { "family": "tree", "eventType": "tree.node.embedded" }, "domainEventPlan": { "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "tree.node.embedded", "streamDeltaHint": { "family": "tree", "kind": "noop", "args": {} } } }) ); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } let copy_plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.subtree.copy".into(), command_id: "cmd_tree_copy_1".into(), idempotency_key: Some("idem_tree_copy".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("parent_1".into()), block_id: None, }), payload: json!({ "items": [ { "documentId": "doc_1", "recursive": true, } ], "targetParentId": "parent_1", }), preflight_data: None, reason: Some("树命令复制子树".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("copy plan should build"); match copy_plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.function_name, "documents:copyTree"); assert_eq!(plan.command_name, "tree.subtree.copy"); assert_eq!( plan.args_json, json!({ "items": [ { "documentId": "doc_1", "recursive": true, } ], "targetParentId": "parent_1", "streamDeltaHint": { "family": "tree", "kind": "copy_result", "args": { "itemsField": "items", "documentField": "document" } }, "domainEventHint": { "family": "tree", "eventType": "tree.subtree.copied" }, "domainEventPlan": { "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "tree.subtree.copied", "streamDeltaHint": { "family": "tree", "kind": "copy_result", "args": { "itemsField": "items", "documentField": "document" } } } }) ); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } } #[test] fn documents_duplicate_plan_includes_result_document_artifact_contract() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "documents.duplicate".into(), command_id: "cmd_duplicate_1".into(), idempotency_key: Some("idem_duplicate_1".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_copy".into()), block_id: None, }), payload: json!({ "sourceDocumentId": "doc_1", "newDocumentId": "doc_copy", "title": "页面副本" }), preflight_data: None, reason: Some("复制页面".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("duplicate plan should build"); match plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.function_name, "documents:duplicateWithMindmaps"); assert_eq!(plan.command_name, "documents.duplicate"); assert_eq!( plan.args_json["streamDeltaHint"], json!({ "family": "tree", "kind": "result_document", "args": {} }) ); assert_eq!( plan.args_json["domainEventPlan"], json!({ "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "tree.node.duplicated", "streamDeltaHint": { "family": "tree", "kind": "result_document", "args": {} } }) ); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } } #[test] fn document_options_command_plan_maps_to_documents_update_options() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "documents.options.update".into(), command_id: "cmd_options_1".into(), idempotency_key: Some("idem_options_1".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", "options": { "showToc": true, "layoutDensity": "compact", "embedDefaultBlockId": null } }), preflight_data: None, reason: Some("更新页面选项".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("options plan should build"); match plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.command_name, "documents.options.update"); assert_eq!(plan.function_name, "documents:updateOptions"); assert_eq!( plan.args_json, json!({ "id": "doc_1", "options": { "showToc": true, "layoutDensity": "compact", }, }) ); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } } #[test] fn document_stats_command_plan_maps_to_documents_update_stats() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "documents.stats.update".into(), command_id: "cmd_stats_1".into(), idempotency_key: Some("idem_stats_1".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "documentId": "doc_1", "workspaceId": "ws_1", "stats": { "wordCount": 12, "characterCount": 34, "blockCount": 5, "todoTotal": 6, "todoDone": 2 } }), preflight_data: None, reason: Some("更新页面统计".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("stats plan should build"); match plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.command_name, "documents.stats.update"); assert_eq!(plan.function_name, "documents:updateStats"); assert_eq!( plan.args_json, json!({ "id": "doc_1", "workspaceId": "ws_1", "wordCount": 12, "characterCount": 34, "blockCount": 5, "todoTotal": 6, "todoDone": 2, }) ); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } } #[test] fn media_asset_replace_storage_command_plan_maps_to_transport_write() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "media.assets.replace_storage".into(), command_id: "cmd_asset_writeback_1".into(), idempotency_key: Some("idem_asset_writeback_1".into()), actor: RuntimeActorWire { actor_type: "service".into(), actor_id: "onlyoffice-callback".into(), session_id: Some("onlyoffice".into()), }, source: RuntimeSourceWire { channel: "onlyoffice-callback".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: None, }), payload: json!({ "assetId": "asset_1", "documentId": "doc_1", "workspaceId": "ws_1", "storageId": "storage_1", "userId": "user_1" }), preflight_data: None, reason: Some("OnlyOffice callback 写回".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect("media asset writeback plan should build"); match plan { RuntimeExecutionPlan::Command(plan) => { assert_eq!(plan.command_name, "media.assets.replace_storage"); assert_eq!(plan.function_name, "mediaAssets:replaceStorageFromUpload"); assert_eq!( plan.args_json, json!({ "userId": "user_1", "id": "asset_1", "documentId": "doc_1", "workspaceId": "ws_1", "storageId": "storage_1", }) ); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } } #[test] fn tree_resource_copy_and_move_plans_include_transfer_contract() { let cases = [ ( "tree.resource.copy", "mediaAssets:batchCopy", "copy", "tree.resource.copied", ), ( "tree.resource.move", "mediaAssets:batchMove", "move", "tree.resource.moved", ), ]; for (command_name, function_name, action, event_type) in cases { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: command_name.into(), command_id: format!("cmd_{action}_asset"), idempotency_key: Some(format!("idem_{action}_asset")), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("target_doc".into()), block_id: None, }), payload: json!({ "assetIds": [" asset_1 ", "asset_1", "asset_2"], "targetDocumentId": "target_doc", "targetSubPath": "mindmaps/mind_1/../assets", }), preflight_data: None, reason: Some("文件树资源投放".into()), refs: vec!["file-tree-shell".into()], dry_run: false, validate_only: false, }, }) .expect("resource transfer plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!(plan.command_name, command_name); assert_eq!(plan.function_name, function_name); assert_eq!( plan.args_json, json!({ "assetIds": ["asset_1", "asset_2"], "targetDocumentId": "target_doc", "targetSubPath": "mindmaps/mind_1/assets", "resourceTransferPlan": { "action": action, "assetIds": ["asset_1", "asset_2"], "targetDocumentId": "target_doc", "targetSubPath": "mindmaps/mind_1/assets", }, "streamDeltaHint": { "family": "tree", "kind": "asset_result", "args": { "itemsField": "items" } }, "domainEventHint": { "family": "tree", "eventType": event_type }, "domainEventPlan": { "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": event_type, "streamDeltaHint": { "family": "tree", "kind": "asset_result", "args": { "itemsField": "items" } } } }) ); } } #[test] fn tree_resource_transfer_rejects_empty_assets_or_target() { let empty_assets = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.resource.move".into(), command_id: "cmd_move_empty_assets".into(), idempotency_key: Some("idem_move_empty_assets".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("target_doc".into()), block_id: None, }), payload: json!({ "assetIds": [], "targetDocumentId": "target_doc", }), preflight_data: None, reason: Some("文件树资源投放".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect_err("empty assets should be rejected"); assert_eq!(empty_assets.kind, BridgeErrorKind::Validation); assert_eq!(empty_assets.message, "resource command 缺少 assetIds"); let missing_target = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.resource.copy".into(), command_id: "cmd_copy_missing_target".into(), idempotency_key: Some("idem_copy_missing_target".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: None, block_id: None, }), payload: json!({ "assetIds": ["asset_1"], "targetDocumentId": " ", }), preflight_data: None, reason: Some("文件树资源投放".into()), refs: vec![], dry_run: false, validate_only: false, }, }) .expect_err("missing target should be rejected"); assert_eq!(missing_target.kind, BridgeErrorKind::Validation); assert_eq!( missing_target.message, "resource command 缺少 targetDocumentId" ); } #[test] fn tree_filetree_drop_preflight_plans_mixed_doc_and_asset_drop() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.filetree.drop.preflight".into(), command_id: "cmd_filetree_drop_preflight_1".into(), idempotency_key: Some("idem_filetree_drop_preflight_1".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_target".into()), block_id: None, }), payload: json!({ "copy": false, "targetDocumentId": null, "targetRowId": "asset-folder:mind_1", "focusedRowId": null, "activeDocumentId": null, "rowIds": ["asset:asset_child_1", "doc:doc_child", "asset:pdf_1", "asset:pdf_1"], "rows": [ { "rowId": "doc:doc_target", "rowKind": "doc", "documentId": "doc_target" }, { "rowId": "doc:doc_child", "rowKind": "doc", "documentId": "doc_child" }, { "rowId": "asset-folder:mind_1", "rowKind": "asset-folder", "documentId": "doc_target", "assetId": "mind_1", "assetType": "mindmap", "storagePath": "mindmaps/mind_1/mindmap.json" }, { "rowId": "asset:asset_child_1", "rowKind": "asset", "documentId": "doc_source", "assetId": "asset_child_1", "assetDocumentId": "doc_source", "assetType": "file", "storagePath": "mindmaps/mind_1/assets/node.png" }, { "rowId": "asset:pdf_1", "rowKind": "asset", "documentId": "doc_source", "assetId": "pdf_1", "assetDocumentId": "doc_source", "assetType": "file", "storagePath": "uploads/guide.pdf" } ], "documentParents": [ { "documentId": "doc_target", "parentId": null }, { "documentId": "doc_child", "parentId": null } ] }), preflight_data: None, reason: Some("文件树内部拖放预检".into()), refs: vec!["file-tree-shell".into()], dry_run: false, validate_only: true, }, }) .expect("filetree drop preflight plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!(plan.command_name, "tree.filetree.drop.preflight"); assert_eq!(plan.function_name, "tree:fileTreeDropPreflight"); assert_eq!( plan.args_json["fileTreeDropPlan"], json!({ "copy": false, "targetDocumentId": "doc_target", "targetMindmapId": "mind_1", "targetSubPath": "mindmaps/mind_1", "rowIds": ["asset:asset_child_1", "doc:doc_child", "asset:pdf_1"], "docIds": ["doc_child"], "topLevelDocIds": ["doc_child"], "copyableAssetIds": ["asset_child_1", "pdf_1"], "sourceAssetDocumentIds": ["doc_source"], "documentTransferPlan": { "action": "move", "targetParentId": "doc_target", "documentIds": ["doc_child"], "topLevelDocumentIds": ["doc_child"], "copyItems": [ { "documentId": "doc_child", "recursive": true } ] }, "resourceTransferPlan": { "action": "move", "assetIds": ["asset_child_1", "pdf_1"], "targetDocumentId": "doc_target", "targetSubPath": "mindmaps/mind_1" } }) ); } #[test] fn tree_filetree_drop_preflight_rejects_illegal_doc_move() { let err = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.filetree.drop.preflight".into(), command_id: "cmd_filetree_drop_preflight_illegal".into(), idempotency_key: Some("idem_filetree_drop_preflight_illegal".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_child".into()), block_id: None, }), payload: json!({ "copy": false, "targetDocumentId": "doc_child", "targetRowId": null, "focusedRowId": null, "activeDocumentId": null, "rowIds": ["doc:doc_parent"], "rows": [ { "rowId": "doc:doc_parent", "rowKind": "doc", "documentId": "doc_parent" }, { "rowId": "doc:doc_child", "rowKind": "doc", "documentId": "doc_child" } ], "documentParents": [ { "documentId": "doc_parent", "parentId": null }, { "documentId": "doc_child", "parentId": "doc_parent" } ] }), preflight_data: None, reason: Some("文件树内部拖放预检".into()), refs: vec!["file-tree-shell".into()], dry_run: false, validate_only: true, }, }) .expect_err("illegal descendant move should be rejected"); assert_eq!(err.kind, BridgeErrorKind::Validation); assert_eq!(err.message, "不能把页面移动到自身或后代下面"); } #[test] fn tree_filetree_delete_preflight_plans_doc_and_asset_targets() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.filetree.delete.preflight".into(), command_id: "cmd_filetree_delete_preflight_1".into(), idempotency_key: Some("idem_filetree_delete_preflight_1".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: None, block_id: None, }), payload: json!({ "rowIds": ["asset:pdf_1", "doc:doc_parent", "index:doc_child", "asset:asset_child_1", "missing"], "rows": [ { "rowId": "doc:doc_parent", "rowKind": "doc", "documentId": "doc_parent" }, { "rowId": "index:doc_child", "rowKind": "index", "documentId": "doc_child" }, { "rowId": "asset:pdf_1", "rowKind": "asset", "documentId": "doc_parent", "assetId": "pdf_1", "assetDocumentId": "doc_parent", "assetType": "file", "storagePath": "uploads/guide.pdf" }, { "rowId": "asset:asset_child_1", "rowKind": "asset", "documentId": "doc_child", "assetId": "asset_child_1", "assetDocumentId": "doc_child", "assetType": "file", "storagePath": "uploads/child.png" } ], "documentParents": [ { "documentId": "doc_parent", "parentId": null }, { "documentId": "doc_child", "parentId": "doc_parent" } ] }), preflight_data: None, reason: Some("文件树删除预检".into()), refs: vec!["file-tree-shell".into()], dry_run: false, validate_only: true, }, }) .expect("filetree delete preflight plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!(plan.command_name, "tree.filetree.delete.preflight"); assert_eq!(plan.function_name, "tree:fileTreeDeletePreflight"); assert_eq!( plan.args_json["fileTreeDeletePlan"], json!({ "rowIds": ["asset:pdf_1", "doc:doc_parent", "index:doc_child", "asset:asset_child_1"], "docIds": ["doc_parent"], "assetIds": [], "assetDocumentIds": [] }) ); } #[test] fn tree_filetree_paste_preflight_plans_doc_index_and_asset_copy() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.filetree.paste.preflight".into(), command_id: "cmd_filetree_paste_preflight_1".into(), idempotency_key: Some("idem_filetree_paste_preflight_1".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_target".into()), block_id: None, }), payload: json!({ "targetDocumentId": null, "focusedRowId": "asset-folder:mind_1", "activeDocumentId": null, "rowIds": ["index:doc_index", "doc:doc_full", "asset:pdf_1", "asset-folder:mind_1", "asset:virtual_1", "asset:pdf_1"], "rows": [ { "rowId": "asset-folder:mind_1", "rowKind": "asset-folder", "documentId": "doc_target", "assetId": "mind_1", "assetType": "mindmap", "storagePath": "mindmaps/mind_1/mindmap.json" }, { "rowId": "doc:doc_full", "rowKind": "doc", "documentId": "doc_full" }, { "rowId": "index:doc_index", "rowKind": "index", "documentId": "doc_index" }, { "rowId": "asset:pdf_1", "rowKind": "asset", "documentId": "doc_source", "assetId": "pdf_1", "assetDocumentId": "doc_source", "assetType": "file", "storagePath": "uploads/guide.pdf" }, { "rowId": "asset:virtual_1", "rowKind": "asset", "documentId": "doc_source", "assetId": "virtual_1", "assetDocumentId": "doc_source", "assetType": "file", "storagePath": null } ] }), preflight_data: None, reason: Some("文件树粘贴预检".into()), refs: vec!["file-tree-shell".into()], dry_run: false, validate_only: true, }, }) .expect("filetree paste preflight plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!(plan.command_name, "tree.filetree.paste.preflight"); assert_eq!(plan.function_name, "tree:fileTreePastePreflight"); assert_eq!( plan.args_json["fileTreePastePlan"], json!({ "targetDocumentId": "doc_target", "targetMindmapId": "mind_1", "targetSubPath": "mindmaps/mind_1", "rowIds": ["index:doc_index", "doc:doc_full", "asset:pdf_1", "asset-folder:mind_1", "asset:virtual_1"], "docItems": [ { "documentId": "doc_index", "recursive": false }, { "documentId": "doc_full", "recursive": true } ], "copyableAssetIds": ["pdf_1"], "resourceTransferPlan": { "action": "copy", "assetIds": ["pdf_1"], "targetDocumentId": "doc_target", "targetSubPath": "mindmaps/mind_1" } }) ); } #[test] fn tree_filetree_upload_target_preflight_resolves_workspace_doc_and_mindmap_target() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.filetree.upload-target.preflight".into(), command_id: "cmd_filetree_upload_target_preflight_1".into(), idempotency_key: Some("idem_filetree_upload_target_preflight_1".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_target".into()), block_id: None, }), payload: json!({ "workspaceId": "ws_fallback", "targetDocumentId": null, "targetRowId": "asset:asset_child_1", "focusedRowId": null, "activeDocumentId": "doc_active", "rows": [ { "rowId": "asset:asset_child_1", "rowKind": "asset", "documentId": "doc_target", "assetId": "asset_child_1", "assetDocumentId": "doc_target", "assetType": "file", "storagePath": "mindmaps/mind_1/assets/node.png" } ], "documentWorkspaces": [ { "documentId": "doc_target", "workspaceId": "ws_1" }, { "documentId": "doc_active", "workspaceId": "ws_active" } ] }), preflight_data: None, reason: Some("文件树上传目标预检".into()), refs: vec!["file-tree-shell".into()], dry_run: false, validate_only: true, }, }) .expect("filetree upload target preflight plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!(plan.command_name, "tree.filetree.upload-target.preflight"); assert_eq!(plan.function_name, "tree:fileTreeUploadTargetPreflight"); assert_eq!( plan.args_json["fileTreeUploadTargetPlan"], json!({ "workspaceId": "ws_1", "targetDocumentId": "doc_target", "targetMindmapId": "mind_1", "targetSubPath": "mindmaps/mind_1" }) ); } #[test] fn tree_resource_upload_plan_includes_upload_contract_and_delta_hint() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { name: "tree.resource.upload".into(), command_id: "cmd_upload_asset".into(), idempotency_key: Some("idem_upload_asset".into()), actor: RuntimeActorWire { actor_type: "user".into(), actor_id: "user_1".into(), session_id: Some("sess_1".into()), }, source: RuntimeSourceWire { channel: "next-route".into(), client: "wolai-frontend".into(), source_kind: None, root_uri: None, workspace_id: None, capabilities: Vec::new(), }, target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("target_doc".into()), block_id: None, }), payload: json!({ "assetId": " asset_upload_1 ", "workspaceId": " ws_1 ", "targetDocumentId": " target_doc ", "targetSubPath": "mindmaps/mind_1/../assets", "fileName": "demo/file.pdf", "fileSize": 1024, "mimeType": "application/pdf", "assetType": "file", }), preflight_data: None, reason: Some("文件树资源上传".into()), refs: vec!["file-tree-upload".into()], dry_run: false, validate_only: false, }, }) .expect("resource upload plan should build"); let RuntimeExecutionPlan::Command(plan) = plan else { panic!("expected command plan"); }; assert_eq!(plan.command_name, "tree.resource.upload"); assert_eq!(plan.function_name, "mediaAssets:createWithStorage"); assert_eq!( plan.args_json, json!({ "assetId": "asset_upload_1", "workspaceId": "ws_1", "targetDocumentId": "target_doc", "targetSubPath": "mindmaps/mind_1/assets", "fileName": "demo_file.pdf", "fileSize": 1024, "mimeType": "application/pdf", "assetType": "file", "resourceUploadPlan": { "action": "upload", "assetId": "asset_upload_1", "workspaceId": "ws_1", "targetDocumentId": "target_doc", "targetSubPath": "mindmaps/mind_1/assets", "fileName": "demo_file.pdf", "fileSize": 1024, "mimeType": "application/pdf", "assetType": "file", }, "streamDeltaHint": { "family": "tree", "kind": "asset_result", "args": { "itemsField": "items" } }, "domainEventHint": { "family": "tree", "eventType": "tree.resource.uploaded" }, "domainEventPlan": { "family": "tree", "schema": "mnote.tree.domain_event", "schemaVersion": 1, "eventType": "tree.resource.uploaded", "streamDeltaHint": { "family": "tree", "kind": "asset_result", "args": { "itemsField": "items" } } } }) ); } #[test] fn mindmap_get_tool_plan_uses_mindmaps_get_query() { let plan = execute_runtime_input(RuntimeInput::Tool { context: demo_context(), tool: RuntimeToolInvocationWire { tool: "mindmap_get".into(), kind: "query".into(), mode: Some("plan".into()), args_json: json!({ "documentId": "doc_1", "mindmapId": "mind_1", "maxNodes": 20, }), target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: Some("mind_1".into()), }), reason: Some("查看导图摘要".into()), refs: vec!["task-032".into()], }, data: None, }) .expect("tool plan should build"); match plan { RuntimeExecutionPlan::Tool(plan) => { assert_eq!(plan.tool_name, "mindmap_get"); assert_eq!(plan.toolset_id, "toolset.mindmap_read"); assert_eq!(plan.steps[0].name, "mindmaps.get"); } _ => panic!("expected tool plan"), } } #[test] fn mindmap_apply_ops_executes_in_rust_runtime() { let result = execute_runtime_query(RuntimeInput::Tool { context: demo_context(), tool: RuntimeToolInvocationWire { tool: "mindmap_apply_ops".into(), kind: "command".into(), mode: Some("result".into()), args_json: json!({ "documentId": "doc_1", "mindmapId": "mind_1", "ops": [ { "op": "addChild", "parentUid": "root_1", "node": { "text": "新分支" } } ], }), target: Some(RuntimeTargetWire { workspace_id: Some("ws_1".into()), page_id: Some("doc_1".into()), block_id: Some("mind_1".into()), }), reason: Some("补充分支".into()), refs: vec!["task-032".into()], }, data: Some(json!({ "source": "convex", "data": { "data": { "uid": "root_1", "text": "中心主题" }, "children": [] } })), }) .expect("mindmap tool result should build"); assert_eq!(result.get("ok"), Some(&json!(true))); assert_eq!(result.get("source"), Some(&json!("convex"))); assert_eq!(result.get("applied"), Some(&json!(1))); assert_eq!(result.get("errors"), Some(&json!([]))); assert_eq!(result.pointer("/data/data/uid"), Some(&json!("root_1"))); assert_eq!(result.pointer("/data/data/text"), Some(&json!("中心主题"))); assert_eq!( result.pointer("/data/children/0/data/text"), Some(&json!("新分支")) ); let generated_uid = result .pointer("/data/children/0/data/uid") .and_then(Value::as_str) .expect("mindmap child uid should exist"); assert!( generated_uid.starts_with("rust_mindmap_uid_"), "unexpected generated uid: {generated_uid}" ); assert_eq!(result.pointer("/meta/reason"), Some(&Value::Null)); } #[test] fn bridge_request_query_plan_maps_to_bridge_logs() { let plan = execute_runtime_input(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "bridge.request.get".into(), payload: json!({ "workspaceId": "ws_1", "requestId": "req_lookup_1", "commandId": "cmd_lookup_1", }), }, data: None, }) .expect("bridge request query plan should build"); match plan { RuntimeExecutionPlan::Query(plan) => { assert_eq!(plan.function_name, "bridgeLogs:listByRequest"); assert_eq!(plan.args_json["workspaceId"], json!("ws_1")); assert_eq!(plan.args_json["requestId"], json!("req_lookup_1")); assert_eq!(plan.args_json["commandId"], json!("cmd_lookup_1")); } _ => panic!("expected query plan"), } } #[test] fn bridge_trace_tool_plan_uses_observe_toolset() { let plan = execute_runtime_input(RuntimeInput::Tool { context: demo_context(), tool: RuntimeToolInvocationWire { tool: "bridge_trace_get".into(), kind: "query".into(), mode: Some("plan".into()), args_json: json!({ "workspaceId": "ws_1", "traceId": "trace_lookup_1", }), target: None, reason: Some("排查链路".into()), refs: vec!["task-034".into()], }, data: None, }) .expect("bridge trace tool plan should build"); match plan { RuntimeExecutionPlan::Tool(plan) => { assert_eq!(plan.tool_name, "bridge_trace_get"); assert_eq!(plan.toolset_id, "toolset.observe_read"); assert_eq!(plan.steps[0].name, "bridge.trace.get"); assert_eq!( plan.steps[0].function_name.as_deref(), Some("bridgeLogs:listByTrace") ); } _ => panic!("expected tool plan"), } } #[test] fn kernel_project_view_query_executes_into_sidebar_projection() { let result = execute_runtime_query(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "kernel.project_view".into(), payload: json!({ "projection": "sidebar_tree", "workspaceId": "ws_1", "rootNodeId": "page_root", "depth": 2, "includeEdges": true, "nodeTypes": ["page"], }), }, data: Some(json!({ "documents": [ { "id": "page_root", "workspace_id": "ws_1", "title": "根页面", "parent_id": null, "sort_order": 0, "is_starred": true, "is_template": false, "created_at": "2026-04-16T00:00:00Z", "updated_at": "2026-04-16T00:00:00Z" }, { "id": "page_child", "workspace_id": "ws_1", "title": "子页面", "parent_id": "page_root", "sort_order": 1, "is_starred": false, "is_template": false, "created_at": "2026-04-16T00:00:00Z", "updated_at": "2026-04-16T00:00:00Z" } ] })), }) .expect("kernel projection result should build"); assert_eq!(result["projection"], json!("sidebar_tree")); assert_eq!(result["rootNodeId"], json!("page_root")); assert_eq!(result["items"][0]["rowId"], json!("page:page_root")); assert_eq!(result["items"][0]["rowKind"], json!("document")); assert_eq!(result["items"][0]["nodeId"], json!("page_root")); assert_eq!(result["items"][0]["position"], json!(0)); assert_eq!(result["items"][0]["projectionKind"], json!("sidebar_tree")); assert_eq!( result["items"][0]["resourceMeta"]["resourceKind"], json!("document") ); assert!(result["items"][0]["capabilities"] .as_array() .map(|caps| caps.contains(&json!("create-child"))) .unwrap_or(false)); assert_eq!(result["items"][1]["parentNodeId"], json!("page_root")); assert_eq!(result["items"][1]["position"], json!(1)); } #[test] fn kernel_project_view_query_executes_into_file_tree_projection_contract() { let result = execute_runtime_query(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "kernel.project_view".into(), payload: json!({ "projection": "file_tree", "workspaceId": "ws_1", "rootNodeId": "page_root", "depth": 2, "includeEdges": true, "nodeTypes": ["page"], }), }, data: Some(json!({ "documents": [ { "id": "page_root", "workspace_id": "ws_1", "title": "根页面", "parent_id": null, "sort_order": 0, "is_starred": true, "is_template": false, "created_at": "2026-04-16T00:00:00Z", "updated_at": "2026-04-16T00:00:00Z" }, { "id": "page_child", "workspace_id": "ws_1", "title": "子页面", "parent_id": "page_root", "sort_order": 2, "is_starred": false, "is_template": false, "created_at": "2026-04-16T00:00:00Z", "updated_at": "2026-04-16T00:00:00Z" } ], "media_assets": [ { "id": "asset_img_1", "workspace_id": "ws_1", "document_id": "page_root", "asset_type": "file", "file_name": "cover.png", "mime_type": "image/png", "storage_path": "cover.png" }, { "id": "asset_pdf_1", "workspace_id": "ws_1", "document_id": "page_child", "asset_type": "file", "file_name": "notes.pdf", "mime_type": "application/pdf", "storage_path": "notes.pdf" }, { "id": "asset_book_1", "workspace_id": "ws_1", "document_id": "page_child", "asset_type": "file", "file_name": "guide.epub", "mime_type": "application/epub+zip", "storage_path": "guide.epub" }, { "id": "asset_ref_1", "workspace_id": "ws_1", "document_id": "page_root", "asset_type": "file", "file_name": "diagram-source.png", "mime_type": "image/png", "storage_path": "diagram-source.png" } ], "mindmap_assets": [ { "id": "mind_1", "workspace_id": "ws_1", "document_id": "page_root", "asset_type": "mindmap", "file_name": "roadmap.json", "mime_type": "application/json", "storage_path": "roadmap.json" } ], "table_assets": [ { "id": "table_1", "workspace_id": "ws_1", "document_id": "page_root", "asset_type": "luckysheet", "file_name": "budget.luckysheet", "mime_type": "application/json", "storage_path": "budget.luckysheet" } ], "mindmap_asset_children": { "mind_1": ["asset_ref_1"] } })), }) .expect("file_tree projection result should build"); let items = result["items"].as_array().expect("items should be array"); let item_by_row_id = items .iter() .filter_map(|item| { item.get("rowId") .and_then(Value::as_str) .map(|row_id| (row_id.to_string(), item)) }) .collect::>(); assert_eq!(result["projection"], json!("file_tree")); assert_eq!( result["projectionId"], json!("kernel_projection:file_tree:page_root") ); assert_eq!( result["meta"]["search"]["indexingVisibility"]["schema"], json!("mnote.file_tree.indexing_visibility") ); assert_eq!( result["meta"]["search"]["indexingVisibility"]["status"], json!("visible") ); assert_eq!( result["meta"]["search"]["indexingVisibility"]["metrics"]["visibleRows"], json!(items.len()) ); assert_eq!( item_by_row_id["doc:page_root"]["rowKind"], json!("document") ); assert_eq!( item_by_row_id["doc:page_root"]["resourceMeta"]["resourceKind"], json!("document") ); assert_eq!( item_by_row_id["doc:page_root"]["resourceMeta"]["extra"]["source"], json!({ "sourceKind": "convex_workspace", "sourceUri": "convex://workspace/ws_1/documents/page_root", "relativePath": "documents/page_root", "storageIdentity": "page_root", "operationProfile": "convex_workspace" }) ); assert_eq!(item_by_row_id["index:page_root"]["rowKind"], json!("index")); assert_eq!( item_by_row_id["index:page_root"]["resourceMeta"]["resourceKind"], json!("index") ); assert_eq!( item_by_row_id["index:page_root"]["resourceMeta"]["extra"]["source"], json!({ "sourceKind": "convex_workspace", "sourceUri": "convex://workspace/ws_1/documents/page_root/index", "relativePath": "documents/page_root/index", "storageIdentity": "page_root:index", "operationProfile": "convex_workspace" }) ); assert_eq!( item_by_row_id["asset:asset_img_1"]["resourceMeta"]["assetKind"], json!("image") ); assert_eq!( item_by_row_id["asset:asset_img_1"]["resourceMeta"]["resourceKind"], json!("asset") ); assert_eq!( item_by_row_id["asset:asset_img_1"]["resourceMeta"]["extra"]["source"], json!({ "sourceKind": "convex_workspace", "sourceUri": "convex://workspace/ws_1/assets/asset_img_1", "relativePath": "assets/asset_img_1", "storageIdentity": "asset_img_1", "operationProfile": "convex_workspace" }) ); assert_eq!( item_by_row_id["asset-folder:mind_1"]["rowKind"], json!("asset_folder") ); assert_eq!( item_by_row_id["asset-folder:mind_1"]["resourceMeta"]["resourceKind"], json!("mindmap") ); assert_eq!( item_by_row_id["asset-folder:mind_1"]["resourceMeta"]["assetKind"], json!("mindmap") ); assert_eq!( item_by_row_id["asset:asset_ref_1"]["parentNodeId"], json!("asset-folder:mind_1") ); assert_eq!( item_by_row_id["asset:asset_ref_1"]["capabilities"][0], json!("open-asset") ); assert_eq!( item_by_row_id["asset:table_1"]["resourceMeta"]["resourceKind"], json!("table") ); assert_eq!( item_by_row_id["asset:asset_pdf_1"]["resourceMeta"]["resourceKind"], json!("pdf") ); assert_eq!( item_by_row_id["asset:asset_book_1"]["resourceMeta"]["resourceKind"], json!("book") ); } #[test] fn kernel_file_tree_projection_query_keeps_matches_and_ancestors() { let result = execute_runtime_query(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "kernel.project_view".into(), payload: json!({ "projection": "file_tree", "workspaceId": "ws_1", "rootNodeId": "page_root", "depth": 3, "includeEdges": true, "nodeTypes": ["page"], "query": "roadmap" }), }, data: Some(json!({ "documents": [ { "id": "page_root", "workspace_id": "ws_1", "title": "根页面", "parent_id": null, "sort_order": 0, "is_starred": true, "is_template": false, "created_at": "2026-04-16T00:00:00Z", "updated_at": "2026-04-16T00:00:00Z" }, { "id": "page_child", "workspace_id": "ws_1", "title": "子页面", "parent_id": "page_root", "sort_order": 1, "is_starred": false, "is_template": false, "created_at": "2026-04-16T00:00:00Z", "updated_at": "2026-04-16T00:00:00Z" } ], "mindmap_assets": [ { "id": "mind_roadmap", "workspace_id": "ws_1", "document_id": "page_child", "asset_type": "mindmap", "file_name": "roadmap.json", "mime_type": "application/json" } ], "media_assets": [ { "id": "asset_other", "workspace_id": "ws_1", "document_id": "page_root", "asset_type": "file", "file_name": "cover.png", "mime_type": "image/png" } ], "table_assets": [], "mindmap_asset_children": {} })), }) .expect("file_tree search projection should build"); let row_ids = result["items"] .as_array() .expect("items should be array") .iter() .filter_map(|item| item.get("rowId").and_then(Value::as_str)) .collect::>(); assert_eq!( row_ids, vec!["doc:page_root", "doc:page_child", "asset:mind_roadmap"] ); assert_eq!(result["items"][0]["expandedByDefault"], json!(true)); assert_eq!(result["items"][1]["expandedByDefault"], json!(true)); assert_eq!( result["edges"] .as_array() .expect("edges should be array") .iter() .filter_map(|edge| edge.get("toNodeId").and_then(Value::as_str)) .collect::>(), vec!["page_child", "asset:mind_roadmap"] ); } #[test] fn kernel_file_tree_projection_query_supports_extended_resources_and_max_results() { let result = execute_runtime_query(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "kernel.project_view".into(), payload: json!({ "projection": "file_tree", "workspaceId": "ws_1", "rootNodeId": "page_root", "depth": 3, "includeEdges": true, "nodeTypes": ["page"], "query": "rust", "maxResults": 4 }), }, data: Some(json!({ "documents": [ { "id": "page_root", "workspace_id": "ws_1", "title": "根页面", "parent_id": null, "sort_order": 0, "is_starred": true, "is_template": false, "created_at": "2026-04-16T00:00:00Z", "updated_at": "2026-04-16T00:00:00Z" }, { "id": "page_child", "workspace_id": "ws_1", "title": "子页面", "parent_id": "page_root", "sort_order": 1, "is_starred": false, "is_template": false, "created_at": "2026-04-16T00:00:00Z", "updated_at": "2026-04-16T00:00:00Z" } ], "media_assets": [ { "id": "asset_rust_pdf", "workspace_id": "ws_1", "document_id": "page_child", "asset_type": "file", "file_name": "rust-guide.pdf", "mime_type": "application/pdf" }, { "id": "asset_rust_book", "workspace_id": "ws_1", "document_id": "page_child", "asset_type": "file", "file_name": "rust-book.epub", "mime_type": "application/epub+zip" }, { "id": "asset_rust_child", "workspace_id": "ws_1", "document_id": "page_child", "asset_type": "file", "file_name": "rust-node.png", "mime_type": "image/png" }, { "id": "asset_extra", "workspace_id": "ws_1", "document_id": "page_child", "asset_type": "file", "file_name": "rust-extra.png", "mime_type": "image/png" } ], "mindmap_assets": [ { "id": "mind_rust", "workspace_id": "ws_1", "document_id": "page_child", "asset_type": "mindmap", "file_name": "rust-map.json", "mime_type": "application/json" } ], "table_assets": [], "mindmap_asset_children": { "mind_rust": ["asset_rust_child"] } })), }) .expect("file_tree search projection should build"); let items = result["items"].as_array().expect("items should be array"); let row_ids = items .iter() .filter_map(|item| item.get("rowId").and_then(Value::as_str)) .collect::>(); assert_eq!( row_ids, vec![ "doc:page_root", "doc:page_child", "asset:asset_rust_pdf", "asset:asset_rust_book", "asset:asset_extra", "asset-folder:mind_rust" ] ); assert_eq!(items[0]["expandedByDefault"], json!(true)); assert_eq!(items[1]["expandedByDefault"], json!(true)); assert_eq!(items[2]["resourceMeta"]["resourceKind"], json!("pdf")); assert_eq!(items[3]["resourceMeta"]["resourceKind"], json!("book")); assert_eq!(items[4]["resourceMeta"]["resourceKind"], json!("asset")); assert_eq!(items[5]["resourceMeta"]["resourceKind"], json!("mindmap")); assert!(!row_ids.contains(&"asset:asset_rust_child")); } #[test] fn kernel_file_tree_projection_query_matches_index_resource() { let result = execute_runtime_query(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "kernel.project_view".into(), payload: json!({ "projection": "file_tree", "workspaceId": "ws_1", "rootNodeId": "page_root", "depth": 2, "includeEdges": true, "nodeTypes": ["page"], "query": "index.md" }), }, data: Some(json!({ "documents": [ { "id": "page_root", "workspace_id": "ws_1", "title": "根页面", "parent_id": null, "sort_order": 0, "is_starred": true, "is_template": false, "created_at": "2026-04-16T00:00:00Z", "updated_at": "2026-04-16T00:00:00Z" } ], "media_assets": [], "mindmap_assets": [], "table_assets": [], "mindmap_asset_children": {} })), }) .expect("file_tree index search projection should build"); let row_ids = result["items"] .as_array() .expect("items should be array") .iter() .filter_map(|item| item.get("rowId").and_then(Value::as_str)) .collect::>(); assert_eq!(row_ids, vec!["doc:page_root", "index:page_root"]); assert_eq!( result["items"][1]["resourceMeta"]["resourceKind"], json!("index") ); } #[test] fn kernel_subtree_query_executes_into_unified_subtree() { let result = execute_runtime_query(RuntimeInput::Query { context: demo_context(), query: RuntimeQueryEnvelopeWire { name: "kernel.subtree.get".into(), payload: json!({ "workspaceId": "ws_1", "rootNodeId": "page_root", "depth": 2, "includeEdges": true, "nodeTypes": ["page"], }), }, data: Some(json!({ "documents": [ { "id": "page_root", "workspace_id": "ws_1", "title": "根页面", "parent_id": null, "sort_order": 0, "is_starred": true, "is_template": false, "created_at": "2026-04-16T00:00:00Z", "updated_at": "2026-04-16T00:00:00Z" }, { "id": "page_child", "workspace_id": "ws_1", "title": "子页面", "parent_id": "page_root", "sort_order": 1, "is_starred": false, "is_template": false, "created_at": "2026-04-16T00:00:00Z", "updated_at": "2026-04-16T00:00:00Z" } ] })), }) .expect("kernel subtree result should build"); assert_eq!(result["rootNodeId"], json!("page_root")); assert_eq!(result["nodes"].as_array().map(Vec::len), Some(2)); assert_eq!(result["edges"].as_array().map(Vec::len), Some(1)); } #[test] fn index_rebuild_tool_executes_in_rust_runtime() { let result = execute_runtime_query(RuntimeInput::Tool { context: demo_context(), tool: RuntimeToolInvocationWire { tool: "index_rebuild".into(), kind: "job".into(), mode: Some("result".into()), args_json: json!({ "workspaceId": "ws_1", "lastProcessedEventId": "evt_0", "lastProcessedAt": "2026-04-15T00:00:00Z", }), target: None, reason: Some("重建搜索索引".into()), refs: vec!["task-034".into()], }, data: Some(json!({ "events": [ { "id": "evt_1", "workspace_id": "ws_1", "command_id": "cmd_1", "command_log_id": "clog_1", "aggregate_type": "page", "aggregate_id": "page_1", "event_type": "page.created", "event_version": 1, "actor_type": "user", "payload": {"title": "第一篇"}, "request_id": "req_1", "trace_id": "trace_1", "created_at": "2026-04-15T01:00:00Z" } ] })), }) .expect("index rebuild tool result should build"); assert_eq!(result.get("ok"), Some(&json!(true))); assert_eq!(result.get("workspaceId"), Some(&json!("ws_1"))); assert_eq!(result.get("batchCount"), Some(&json!(1))); assert_eq!(result.get("documentCount"), Some(&json!(1))); assert_eq!( result.pointer("/cursor/lastProcessedEventId"), Some(&json!("evt_1")) ); } }