5815 lines
219 KiB
Rust
5815 lines
219 KiB
Rust
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<Value>,
|
||
},
|
||
Command {
|
||
context: RuntimeBridgeContextWire,
|
||
command: RuntimeCommandEnvelopeWire,
|
||
},
|
||
Tool {
|
||
context: RuntimeBridgeContextWire,
|
||
tool: RuntimeToolInvocationWire,
|
||
#[serde(default)]
|
||
data: Option<Value>,
|
||
},
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct RuntimeBridgeContextWire {
|
||
pub deployment_id: Option<String>,
|
||
pub project_id: Option<String>,
|
||
pub workspace_id: Option<String>,
|
||
pub request_id: String,
|
||
pub trace_id: String,
|
||
pub actor: RuntimeActorWire,
|
||
pub source: RuntimeSourceWire,
|
||
pub tenant_id: Option<String>,
|
||
pub auth_token: Option<String>,
|
||
pub idempotency_key: Option<String>,
|
||
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<String>,
|
||
}
|
||
|
||
#[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<String>,
|
||
pub page_id: Option<String>,
|
||
pub block_id: Option<String>,
|
||
}
|
||
|
||
#[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<String>,
|
||
pub actor: RuntimeActorWire,
|
||
pub source: RuntimeSourceWire,
|
||
pub target: Option<RuntimeTargetWire>,
|
||
pub payload: Value,
|
||
pub reason: Option<String>,
|
||
pub refs: Vec<String>,
|
||
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<String>,
|
||
pub args_json: Value,
|
||
pub target: Option<RuntimeTargetWire>,
|
||
pub reason: Option<String>,
|
||
#[serde(default)]
|
||
pub refs: Vec<String>,
|
||
}
|
||
|
||
#[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<MindmapOutlineItemPayload>,
|
||
}
|
||
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct DocsSearchDatasetPayload {
|
||
workspace_id: String,
|
||
documents: Vec<index_fts::SearchDocumentRecord>,
|
||
mindmaps: Vec<index_fts::SearchMindmapRecord>,
|
||
tables: Vec<index_fts::SearchTableRecord>,
|
||
table_rows: Vec<index_fts::SearchTableRowRecord>,
|
||
assets: Vec<index_fts::SearchAssetRecord>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct DocsSearchToolPayload {
|
||
query: String,
|
||
workspace_id: Option<String>,
|
||
limit: Option<u32>,
|
||
include_deleted: Option<bool>,
|
||
page_id: Option<String>,
|
||
title_only: Option<bool>,
|
||
exact: Option<bool>,
|
||
include_ocr: Option<bool>,
|
||
time_range: Option<String>,
|
||
time_field: Option<String>,
|
||
custom_range_from: Option<String>,
|
||
custom_range_to: Option<String>,
|
||
#[serde(default)]
|
||
datasets: Vec<DocsSearchDatasetPayload>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct DocsReadToolPayload {
|
||
document_id: String,
|
||
max_chars: Option<u32>,
|
||
include_content: Option<bool>,
|
||
title: Option<String>,
|
||
workspace_id: Option<String>,
|
||
parent_id: Option<String>,
|
||
updated_at: Option<String>,
|
||
raw_text_length: Option<u64>,
|
||
raw_text: Option<String>,
|
||
content: Option<Value>,
|
||
}
|
||
|
||
#[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<String>,
|
||
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<String>,
|
||
pub request_id: String,
|
||
pub trace_id: String,
|
||
pub actor_id: String,
|
||
pub idempotency_key: Option<String>,
|
||
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<Value>,
|
||
pub steps: Vec<RuntimeToolPlanStep>,
|
||
}
|
||
|
||
#[derive(Debug, Serialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct RuntimeToolPlanStep {
|
||
pub kind: String,
|
||
pub name: String,
|
||
pub function_name: Option<String>,
|
||
pub description: String,
|
||
pub args_json: Value,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct BlockGetQueryPayload {
|
||
block_id: String,
|
||
workspace_id: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct BlockPatchCommandPayload {
|
||
document_id: String,
|
||
workspace_id: Option<String>,
|
||
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<String>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct DocumentContentQueryPayload {
|
||
document_id: String,
|
||
workspace_id: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct MindmapGetQueryPayload {
|
||
document_id: String,
|
||
mindmap_id: String,
|
||
workspace_id: Option<String>,
|
||
}
|
||
|
||
#[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<String>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct KernelGetSubtreeQueryPayload {
|
||
root_node_id: String,
|
||
workspace_id: Option<String>,
|
||
depth: Option<u32>,
|
||
include_edges: Option<bool>,
|
||
node_types: Option<Vec<KernelNodeType>>,
|
||
edge_types: Option<Vec<KernelEdgeType>>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct KernelListChildrenQueryPayload {
|
||
parent_node_id: String,
|
||
workspace_id: Option<String>,
|
||
node_types: Option<Vec<KernelNodeType>>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct KernelListEdgesQueryPayload {
|
||
node_id: String,
|
||
workspace_id: Option<String>,
|
||
edge_types: Option<Vec<KernelEdgeType>>,
|
||
direction: Option<KernelGraphDirection>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct KernelTraverseGraphQueryPayload {
|
||
start_node_id: String,
|
||
workspace_id: Option<String>,
|
||
edge_types: Option<Vec<KernelEdgeType>>,
|
||
max_depth: Option<u32>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct KernelProjectViewQueryPayload {
|
||
projection: KernelProjectionKind,
|
||
workspace_id: Option<String>,
|
||
root_node_id: Option<String>,
|
||
depth: Option<u32>,
|
||
include_content: Option<bool>,
|
||
include_edges: Option<bool>,
|
||
node_types: Option<Vec<KernelNodeType>>,
|
||
edge_types: Option<Vec<KernelEdgeType>>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct SearchDocumentsQueryPayload {
|
||
query: String,
|
||
workspace_id: String,
|
||
page_id: Option<String>,
|
||
limit: Option<u32>,
|
||
cursor: Option<String>,
|
||
title_only: Option<bool>,
|
||
exact: Option<bool>,
|
||
include_ocr: Option<bool>,
|
||
time_range: Option<String>,
|
||
time_field: Option<String>,
|
||
custom_range_from: Option<String>,
|
||
custom_range_to: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct SearchRecentQueryPayload {
|
||
workspace_id: String,
|
||
limit: Option<u32>,
|
||
cursor: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct BridgeRequestQueryPayload {
|
||
workspace_id: String,
|
||
request_id: String,
|
||
command_id: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct BridgeTraceQueryPayload {
|
||
workspace_id: String,
|
||
trace_id: String,
|
||
command_id: Option<String>,
|
||
}
|
||
|
||
#[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<u32>,
|
||
cursor: Option<String>,
|
||
command_status: Option<String>,
|
||
event_status: Option<String>,
|
||
target_page_id: Option<String>,
|
||
target_block_id: Option<String>,
|
||
aggregate_type: Option<String>,
|
||
aggregate_id: Option<String>,
|
||
}
|
||
|
||
#[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<String>,
|
||
revision: Option<u64>,
|
||
content: Value,
|
||
conflict_detection_key: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct KernelCreateNodeCommandPayload {
|
||
node: KernelNode,
|
||
position: Option<i64>,
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct KernelUpdateNodeCommandPayload {
|
||
node_id: String,
|
||
metadata: Option<KernelNodeMetadata>,
|
||
content: Option<KernelContentPayload>,
|
||
refs: Option<KernelRefsPayload>,
|
||
audit: Option<KernelAuditStamp>,
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct KernelMoveSubtreeCommandPayload {
|
||
subtree: KernelSubtreeRef,
|
||
new_parent_node_id: Option<String>,
|
||
sort_order: Option<i64>,
|
||
}
|
||
|
||
#[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<String>,
|
||
from_node_id: Option<String>,
|
||
to_node_id: Option<String>,
|
||
edge_type: Option<KernelEdgeType>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct MindmapPutCommandPayload {
|
||
document_id: String,
|
||
mindmap_id: String,
|
||
data: Value,
|
||
create_only: Option<bool>,
|
||
}
|
||
|
||
#[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<String>,
|
||
title: String,
|
||
access_scope: String,
|
||
content: Value,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct DocumentMoveCommandPayload {
|
||
document_id: String,
|
||
parent_id: Option<String>,
|
||
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<String>,
|
||
}
|
||
|
||
#[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<DocumentCopyTreeItemPayload>,
|
||
target_parent_id: Option<String>,
|
||
}
|
||
|
||
#[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<String>,
|
||
blocks: Vec<Value>,
|
||
}
|
||
|
||
#[derive(Debug, Serialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct RuntimeMindmapSummary {
|
||
uid: String,
|
||
text: String,
|
||
parent_uid: Option<String>,
|
||
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<RuntimeExecutionPlan, BridgeError> {
|
||
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<Value, BridgeError> {
|
||
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<RuntimeExecutionPlan, BridgeError> {
|
||
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<RuntimeExecutionPlan, BridgeError> {
|
||
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<Value, BridgeError> {
|
||
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::<Vec<Value>>();
|
||
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::<Vec<RuntimeBlockSummary>>();
|
||
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::<String>::new();
|
||
let mut results = Vec::<Value>::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::<Vec<MindmapOp>>(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::<String>();
|
||
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<Value, BridgeError> {
|
||
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<ToolInvocation, BridgeError> {
|
||
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<InvocationKind, BridgeError> {
|
||
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<ToolExecutionMode, BridgeError> {
|
||
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<Vec<RuntimeToolPlanStep>, 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<String, BridgeError> {
|
||
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<String, BridgeError> {
|
||
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<String> {
|
||
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<Value, BridgeError> {
|
||
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::<Vec<&str>>();
|
||
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::<Vec<&str>>()
|
||
.join(" ")
|
||
.trim()
|
||
.to_string();
|
||
if document_id.is_empty() || title.is_empty() {
|
||
return Err(BridgeError::validation("用法:/rename <documentId> <新标题>"));
|
||
}
|
||
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<Value, BridgeError> {
|
||
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<Value>) -> Vec<Value> {
|
||
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<Value, BridgeError> {
|
||
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::<Vec<String>>(),
|
||
"batchCount": result.batches.len(),
|
||
"cursor": index_cursor_to_json(&result.cursor),
|
||
}))
|
||
}
|
||
|
||
fn execute_index_rebuild(args: &Value, data: &Value) -> Result<Value, BridgeError> {
|
||
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::<Vec<String>>(),
|
||
"cursor": index_cursor_to_json(&result.cursor),
|
||
"batchCount": result.batches.len(),
|
||
"documentCount": result.batches.iter().map(|batch| batch.documents.len()).sum::<usize>(),
|
||
"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::<Vec<Value>>(),
|
||
})
|
||
}).collect::<Vec<Value>>(),
|
||
}))
|
||
}
|
||
|
||
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<Vec<DomainEventRecord>, 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<DomainEventRecord, BridgeError> {
|
||
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<T>(args: &Value, data: &Value) -> Result<T, BridgeError>
|
||
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<Value> {
|
||
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<MindmapTreeNode, BridgeError> {
|
||
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<RuntimeBlockSummary> {
|
||
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::<String>()
|
||
.trim()
|
||
.to_string()
|
||
})
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
fn parse_insert_specs(args: &Value) -> Result<Vec<RuntimeInsertSpec>, 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<Value>,
|
||
after_block_id: Option<String>,
|
||
before_block_id: Option<String>,
|
||
specs: Vec<RuntimeInsertSpec>,
|
||
) -> Result<InsertBlocksResult, BridgeError> {
|
||
let created = specs
|
||
.iter()
|
||
.map(build_block_from_spec)
|
||
.collect::<Vec<Value>>();
|
||
let inserted = created
|
||
.iter()
|
||
.filter_map(|block| read_string_value(block, "id"))
|
||
.collect::<Vec<String>>();
|
||
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<Value>,
|
||
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<Value>,
|
||
block_id: &str,
|
||
text: &str,
|
||
mode: &str,
|
||
) -> Result<Vec<Value>, 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<Value>,
|
||
block_id: &str,
|
||
text: &str,
|
||
mode: &str,
|
||
) -> Result<bool, BridgeError> {
|
||
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<String, BridgeError> {
|
||
read_string_value(value, field).ok_or_else(|| {
|
||
BridgeError::validation(format!("tool 缺少 {field}"))
|
||
})
|
||
}
|
||
|
||
fn read_optional_string_field(value: &Value, field: &str) -> Option<String> {
|
||
read_string_value(value, field)
|
||
}
|
||
|
||
fn read_string_value(value: &Value, field: &str) -> Option<String> {
|
||
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<u64> {
|
||
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::<String>();
|
||
if value.chars().count() > max_chars {
|
||
format!("{taken}…")
|
||
} else {
|
||
taken
|
||
}
|
||
}
|
||
|
||
fn walk_mindmap_summaries(root: &MindmapTreeNode, max_nodes: usize) -> Vec<RuntimeMindmapSummary> {
|
||
let mut list = Vec::new();
|
||
let mut queue = VecDeque::from([(root, None::<String>, 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<RuntimeMindmapSubtreeSummary> {
|
||
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<MindmapTreeNode, BridgeError> {
|
||
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<Vec<MindmapOp>, 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<MindmapOp> = 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<Vec<MindmapOutlineItemPayload>, 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<MindmapOutlineItemPayload> = 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<String>,
|
||
}
|
||
|
||
fn apply_mindmap_ops(
|
||
root: &mut MindmapTreeNode,
|
||
ops: &[MindmapOp],
|
||
) -> Result<MindmapOpsResult, BridgeError> {
|
||
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<MindmapTreeNode, BridgeError> {
|
||
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<usize>> = 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<bool, String> {
|
||
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<bool, String> {
|
||
insert_mindmap_sibling_after(&mut root.children, target_uid, node)
|
||
}
|
||
|
||
fn insert_mindmap_sibling_after(
|
||
nodes: &mut Vec<MindmapTreeNode>,
|
||
target_uid: &str,
|
||
node: &MindmapNodeInput,
|
||
) -> Result<bool, String> {
|
||
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<bool, String> {
|
||
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<String>,
|
||
) -> Result<bool, String> {
|
||
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<MindmapNodeRef>,
|
||
) -> Result<bool, String> {
|
||
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<bool, String> {
|
||
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<bool, String> {
|
||
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<MindmapTreeNode>, 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<String> {
|
||
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<bool> {
|
||
record_field(value, key).and_then(Value::as_bool)
|
||
}
|
||
|
||
fn normalize_kernel_node_from_record(
|
||
value: &Value,
|
||
workspace_id: Option<&str>,
|
||
) -> Result<KernelNode, BridgeError> {
|
||
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<Vec<KernelNode>, 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::<Result<Vec<_>, _>>()
|
||
}
|
||
|
||
fn build_sidebar_kernel_edges(nodes: &[KernelNode]) -> Vec<KernelEdge> {
|
||
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<u32>,
|
||
) -> Result<KernelSubtreeResult, BridgeError> {
|
||
let mut all_nodes = build_sidebar_kernel_nodes(data, workspace_id)?;
|
||
let max_depth = depth.unwrap_or(u32::MAX);
|
||
let mut by_parent = BTreeMap::<Option<String>, Vec<String>>::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::<String, u32>::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::<Vec<_>>();
|
||
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<KernelEdgeListResult, BridgeError> {
|
||
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::<Vec<_>>();
|
||
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<KernelGraphTraversalResult, BridgeError> {
|
||
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::<BTreeMap<_, _>>();
|
||
let mut seen = BTreeMap::<String, bool>::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<u32>,
|
||
) -> Result<KernelProjectionResult, BridgeError> {
|
||
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::<Vec<_>>();
|
||
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<Value, BridgeError> {
|
||
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::<Vec<KernelNode>>();
|
||
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::<Vec<Value>>();
|
||
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<RuntimeExecutionPlan, BridgeError> {
|
||
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::<Vec<Value>>(),
|
||
"targetParentId": payload.target_parent_id,
|
||
}),
|
||
}))
|
||
}
|
||
other => Err(BridgeError::validation(format!(
|
||
"bridge runtime 暂不支持 command: {other}"
|
||
))),
|
||
}
|
||
}
|
||
|
||
fn parse_payload<T>(value: Value) -> Result<T, BridgeError>
|
||
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<TargetRef> {
|
||
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": "<mark>Rust</mark> 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"))
|
||
);
|
||
}
|
||
}
|