feat(ai): switch page ai to hermes panel

This commit is contained in:
lix-2026
2026-05-14 15:10:33 +08:00
parent e9188716e6
commit 9816035491
48 changed files with 6353 additions and 412 deletions
+218 -31
View File
@@ -7376,10 +7376,10 @@ fn build_sidebar_kernel_nodes(
}
fn build_sidebar_kernel_edges(nodes: &[KernelNode]) -> Vec<KernelEdge> {
nodes
.iter()
.filter_map(|node| {
node.parent_id.as_ref().map(|parent_id| KernelEdge {
let mut edges = Vec::new();
for node in nodes {
if let Some(parent_id) = node.parent_id.as_ref() {
edges.push(KernelEdge {
id: format!("edge_parent_of_{}_{}", parent_id, node.id),
edge_type: KernelEdgeType::ParentOf,
workspace_id: node.workspace_id.clone(),
@@ -7387,9 +7387,58 @@ fn build_sidebar_kernel_edges(nodes: &[KernelNode]) -> Vec<KernelEdge> {
to_node_id: node.id.clone(),
metadata: BTreeMap::new(),
audit: KernelAuditStamp::default(),
})
});
if let Some(artifact_type) = ai_artifact_type_for_node(node) {
edges.push(KernelEdge {
id: format!("edge_ai_artifact_reference_{}_{}", parent_id, node.id),
edge_type: KernelEdgeType::SourceOf,
workspace_id: node.workspace_id.clone(),
from_node_id: parent_id.clone(),
to_node_id: node.id.clone(),
metadata: BTreeMap::from([
("kind".into(), json!("ai_artifact_reference")),
("artifactType".into(), json!(artifact_type)),
("projectionOnlyGroup".into(), json!("AI Artifacts")),
]),
audit: KernelAuditStamp::default(),
});
}
}
}
edges
}
fn ai_artifact_type_for_node(node: &KernelNode) -> Option<&'static str> {
let parent_id = node.parent_id.as_ref()?;
if node.id == format!("summary_{parent_id}") {
return Some("summary");
}
if node.id.starts_with(&format!("ai_note_{parent_id}_")) {
return Some("ai_note");
}
None
}
fn ai_artifacts_projection_group_meta(edges: &[KernelEdge]) -> Value {
let artifact_document_ids = edges
.iter()
.filter(|edge| {
edge.metadata
.get("kind")
.and_then(Value::as_str)
.map(|kind| kind == "ai_artifact_reference")
.unwrap_or(false)
})
.collect()
.map(|edge| edge.to_node_id.clone())
.collect::<Vec<_>>();
json!({
"title": "AI Artifacts",
"projectionOnly": true,
"source": "kernel.project_view.synthetic_group",
"kernelNodeId": Value::Null,
"edgeKind": "ai_artifact_reference",
"artifactDocumentIds": artifact_document_ids,
})
}
fn kernel_node_sort_order(node: &KernelNode) -> i64 {
@@ -7979,6 +8028,36 @@ fn build_file_tree_projection_result(
let visible_rows = items.len();
let visible_edges = edges.len();
let mut meta = BTreeMap::from([(
"search".into(),
json!({
"query": requested_query.clone(),
"maxResults": requested_max_results,
"maxResultsRule": "matches_only_before_ancestor_completion",
"ancestorCompletion": "include_all_ancestors_after_match_truncation",
"ordering": "kernel_file_tree_preorder",
"indexingVisibility": {
"schema": "mnote.file_tree.indexing_visibility",
"schemaVersion": 1,
"source": "kernel.project_view",
"status": "visible",
"requestKey": requested_query
.as_ref()
.map(|query| format!("{}:{query}", root_node_id.unwrap_or("root"))),
"indexedResourceKinds": ["document", "index", "asset", "asset_folder", "mindmap", "book", "pdf"],
"visibleResourceKinds": ["document", "index", "asset", "asset_folder", "mindmap", "book", "pdf"],
"metrics": {
"visibleRows": visible_rows,
"visibleEdges": visible_edges,
},
},
}),
)]);
meta.insert(
"aiArtifacts".into(),
ai_artifacts_projection_group_meta(&edges),
);
KernelProjectionResult {
projection_id: format!(
"kernel_projection:file_tree:{}",
@@ -7988,31 +8067,7 @@ fn build_file_tree_projection_result(
root_node_id: root_node_id.map(ToOwned::to_owned),
items,
edges,
meta: BTreeMap::from([(
"search".into(),
json!({
"query": requested_query.clone(),
"maxResults": requested_max_results,
"maxResultsRule": "matches_only_before_ancestor_completion",
"ancestorCompletion": "include_all_ancestors_after_match_truncation",
"ordering": "kernel_file_tree_preorder",
"indexingVisibility": {
"schema": "mnote.file_tree.indexing_visibility",
"schemaVersion": 1,
"source": "kernel.project_view",
"status": "visible",
"requestKey": requested_query
.as_ref()
.map(|query| format!("{}:{query}", root_node_id.unwrap_or("root"))),
"indexedResourceKinds": ["document", "index", "asset", "asset_folder", "mindmap", "book", "pdf"],
"visibleResourceKinds": ["document", "index", "asset", "asset_folder", "mindmap", "book", "pdf"],
"metrics": {
"visibleRows": visible_rows,
"visibleEdges": visible_edges,
},
},
}),
)]),
meta,
}
}
@@ -17443,6 +17498,138 @@ mod tests {
assert_eq!(result["edges"].as_array().map(Vec::len), Some(1));
}
#[test]
fn kernel_edges_list_exposes_ai_artifact_reference_edges() {
let result = execute_runtime_query(RuntimeInput::Query {
context: demo_context(),
query: RuntimeQueryEnvelopeWire {
name: "kernel.edges.list".into(),
payload: json!({
"workspaceId": "ws_1",
"nodeId": "page_root",
"direction": "both",
}),
},
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": "summary_page_root",
"workspace_id": "ws_1",
"title": "AI Summary",
"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"
},
{
"id": "ai_note_page_root_req_1",
"workspace_id": "ws_1",
"title": "AI Note",
"parent_id": "page_root",
"sort_order": 2,
"is_starred": false,
"is_template": false,
"created_at": "2026-04-16T00:00:00Z",
"updated_at": "2026-04-16T00:00:00Z"
}
]
})),
})
.expect("kernel edge list should build");
let edges = result["edges"].as_array().expect("edges should be array");
let artifact_edges = edges
.iter()
.filter(|edge| edge["metadata"]["kind"] == json!("ai_artifact_reference"))
.collect::<Vec<_>>();
assert_eq!(artifact_edges.len(), 2);
assert_eq!(artifact_edges[0]["fromNodeId"], json!("page_root"));
assert_eq!(artifact_edges[0]["toNodeId"], json!("summary_page_root"));
assert_eq!(artifact_edges[0]["edgeType"], json!("source_of"));
assert_eq!(artifact_edges[0]["metadata"]["artifactType"], json!("summary"));
assert_eq!(artifact_edges[1]["toNodeId"], json!("ai_note_page_root_req_1"));
assert_eq!(artifact_edges[1]["metadata"]["artifactType"], json!("ai_note"));
}
#[test]
fn file_tree_projection_marks_ai_artifacts_group_as_projection_only() {
let result = execute_runtime_query(RuntimeInput::Query {
context: demo_context(),
query: RuntimeQueryEnvelopeWire {
name: "kernel.project_view".into(),
payload: json!({
"projection": "file_tree",
"workspaceId": "ws_1",
"rootNodeId": "page_root",
"depth": 2,
"includeEdges": true,
"nodeTypes": ["page"],
}),
},
data: Some(json!({
"documents": [
{
"id": "page_root",
"workspace_id": "ws_1",
"title": "根页面",
"parent_id": null,
"sort_order": 0,
"is_starred": true,
"is_template": false,
"created_at": "2026-04-16T00:00:00Z",
"updated_at": "2026-04-16T00:00:00Z"
},
{
"id": "summary_page_root",
"workspace_id": "ws_1",
"title": "AI Summary",
"parent_id": "page_root",
"sort_order": 1,
"is_starred": false,
"is_template": false,
"created_at": "2026-04-16T00:00:00Z",
"updated_at": "2026-04-16T00:00:00Z"
}
],
"media_assets": [],
"mindmap_assets": [],
"table_assets": [],
"mindmap_asset_children": {}
})),
})
.expect("file tree projection should build");
let items = result["items"].as_array().expect("items should be array");
assert!(!items
.iter()
.any(|item| item["nodeId"] == json!("AI Artifacts")));
assert_eq!(result["meta"]["aiArtifacts"]["title"], json!("AI Artifacts"));
assert_eq!(result["meta"]["aiArtifacts"]["projectionOnly"], json!(true));
assert_eq!(
result["meta"]["aiArtifacts"]["source"],
json!("kernel.project_view.synthetic_group")
);
assert_eq!(result["meta"]["aiArtifacts"]["kernelNodeId"], Value::Null);
assert_eq!(
result["meta"]["aiArtifacts"]["artifactDocumentIds"],
json!(["summary_page_root"])
);
}
#[test]
fn index_rebuild_tool_executes_in_rust_runtime() {
let result = execute_runtime_query(RuntimeInput::Tool {