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::{ default_tool_registry, invocation_kind_label, tool_effect_label, ActorPayload, CommandEnvelope, 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, KernelProjectionFilter, KernelProjectionItem, KernelProjectionKind, KernelProjectionRequest, KernelProjectionResult, KernelRefsPayload, KernelSubtreeRef, KernelSubtreeResult, KernelTraverseGraph, KernelUpdateNode, ListBridgeWorkspaceOverview, MindmapNodeData, MindmapNodeInput, MindmapNodeRef, MindmapOp, MindmapTreeNode, MoveBlock, PatchBlock, PutMindmap, QueryEnvelope, SearchDocuments, SearchRecent, SourcePayload, TargetRef, ToolExecutionMode, ToolInvocation, }; 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 storage_convex_bridge::{ build_query_request, build_write_request, BridgeContext, BridgeError, BridgeErrorKind, }; use std::env; use std::collections::{BTreeMap, VecDeque}; use std::sync::atomic::{AtomicU64, Ordering}; 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, }, 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, } #[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, pub reason: Option, pub refs: Vec, pub dry_run: bool, pub validate_only: bool, } #[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 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, Serialize, 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 payload_json: String, pub args_json: Value, } #[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 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>, } #[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, content: Value, conflict_detection_key: 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 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 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, 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, } #[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::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 { .. } => Err(BridgeError::validation( "execute_runtime_query 仅支持 query 输入", )), } } pub fn build_success_response(plan: RuntimeExecutionPlan) -> RuntimeSuccess { RuntimeSuccess { ok: true, plan } } 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::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(), 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(), 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, }), })) } "mindmaps.get" => { let payload: MindmapGetQueryPayload = parse_payload(query_wire.payload)?; let query = QueryEnvelope { name: "mindmaps.get".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" => { let payload: SearchDocumentsQueryPayload = parse_payload(query_wire.payload)?; 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: "search.documents".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)?; Ok(json!({ "ok": true, "source": infer_tool_source(&data), "inserted": result.inserted, "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 result = apply_replace_range(blocks, &block_id, &text, &mode)?; Ok(json!({ "ok": true, "source": infer_tool_source(&data), "blockId": block_id, "mode": mode, "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 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 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 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 }) } 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 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 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)); } 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 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, ) -> 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, } }; let items = subtree .nodes .iter() .map(|node| KernelProjectionItem { node_id: node.id.clone(), parent_node_id: node.parent_id.clone(), node_type: node.node_type.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: subtree .edges .iter() .filter(|edge| edge.from_node_id == node.id && edge.edge_type == KernelEdgeType::ParentOf) .count() as u32, expanded_by_default: true, }) .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, }) } 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() { "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, )?; 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}")) }) } "search.documents" => { 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, ); serde_json::to_value(SearchDocumentsEvaluation { enqueue_asset_ids: result.enqueue_asset_ids, results: result.results, }) .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); 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, 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, 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, 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, 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, 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, payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, "blockId": payload.block_id, "nextBlock": payload.next_block, }), })) } "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)?; 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, payload_json: request.payload_json, args_json: json!({ "id": payload.block_id, "sourceDocumentId": payload.source_document_id, "targetDocumentId": payload.target_document_id, }), })) } "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)?; 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, 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, }), })) } "documents.title.update" => { let payload: DocumentTitleCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "documents.title.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: 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, payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, "title": payload.title, }), })) } "documents.save" => { let payload: DocumentSaveCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "documents.save".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(&payload.content).map_err(|error| { BridgeError::validation(format!( "documents.save 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, payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, "content": payload.content, "expectedRevision": payload.revision, "conflictDetectionKey": payload.conflict_detection_key, }), })) } "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, 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), }), })) } "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, 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, 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, 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, payload_json: request.payload_json, args_json: json!({ "workspaceId": payload.workspace_id, }), })) } "documents.create" => { let payload: DocumentCreateCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "documents.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: 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, 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, }), })) } "documents.move" => { let payload: DocumentMoveCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "documents.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: 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, payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, "parentId": payload.parent_id, "sortOrder": payload.sort_order, }), })) } "documents.delete" => { let payload: DocumentDeleteCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "documents.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.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, payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, }), })) } "documents.restore" => { let payload: DocumentRestoreCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "documents.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.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, payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, }), })) } "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, payload_json: request.payload_json, args_json: json!({ "sourceId": payload.source_document_id, "newId": payload.new_document_id, "title": payload.title, }), })) } "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, 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, payload_json: request.payload_json, args_json: json!({ "workspaceId": payload.workspace_id, }), })) } "documents.purge" => { let payload: DocumentPurgeCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "documents.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.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, payload_json: request.payload_json, args_json: json!({ "id": payload.document_id, }), })) } "documents.copy_tree" => { let payload: DocumentCopyTreeCommandPayload = parse_payload(command_wire.payload.clone())?; let command = CommandEnvelope { name: "documents.copy_tree".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, 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, }), })) } 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 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(), }, tenant_id: None, auth_token: None, idempotency_key: Some("idem_1".into()), validate_only: false, dry_run: 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(), }, 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", }, }), 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", }, }) ); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } } #[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 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", "score": 3.0 } ] }) ); } #[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(), }, 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, }), 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", "data": [ { "id": "block_1", "type": "paragraph", "content": [{"type":"text","text":"新的正文"}], "children": [] } ] }) ); } #[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(), }, 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", }), 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", }) ); } 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(), }, 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", }), 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", }) ); } RuntimeExecutionPlan::Query(_) => panic!("expected command plan"), RuntimeExecutionPlan::Tool(_) => panic!("expected command plan"), } } #[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]["nodeId"], json!("page_root")); assert_eq!(result["items"][1]["parentNodeId"], json!("page_root")); } #[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")) ); } }