Files
mnote/rust/crates/bridge-runtime/src/lib.rs
T

5815 lines
219 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"))
);
}
}