feat(kernel): complete tree-first graph tasks 074-080
This commit is contained in:
@@ -8,10 +8,16 @@ 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, ListBridgeWorkspaceOverview, MindmapNodeData, MindmapNodeInput,
|
||||
MindmapNodeRef, MindmapOp, MindmapTreeNode, MoveBlock, PatchBlock, PutMindmap,
|
||||
QueryEnvelope, SearchDocuments, SearchRecent, SourcePayload, TargetRef, ToolExecutionMode,
|
||||
ToolInvocation,
|
||||
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::{
|
||||
@@ -346,6 +352,63 @@ 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 {
|
||||
@@ -425,6 +488,46 @@ struct DocumentSaveCommandPayload {
|
||||
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 {
|
||||
@@ -635,6 +738,193 @@ fn execute_query(
|
||||
) -> 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 {
|
||||
@@ -3021,6 +3311,292 @@ fn delete_mindmap_node_in_children(nodes: &mut Vec<MindmapTreeNode>, uid: &str)
|
||||
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,
|
||||
@@ -3042,6 +3618,78 @@ fn execute_query_result(
|
||||
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| {
|
||||
@@ -3118,6 +3766,181 @@ fn execute_command(
|
||||
) -> 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 {
|
||||
@@ -4842,6 +5665,104 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
|
||||
Reference in New Issue
Block a user