Improve local evidence search and AI capabilities

This commit is contained in:
lix-2026
2026-06-05 23:00:53 +08:00
parent a1dcfc9f76
commit 4dbd9a978b
52 changed files with 6415 additions and 527 deletions
@@ -3,8 +3,8 @@ use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::hermes_tools::{
artifact, block, context_tools, doc, evidence, manifest, onlyoffice_live, page, resource,
skill, ToolCallInput,
artifact, block, context_tools, doc, evidence, index, manifest, onlyoffice_live, page,
resource, skill, ToolCallInput,
};
use axum::extract::{Extension, Query, State};
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
@@ -365,6 +365,11 @@ pub(crate) async fn execute_mnote_tool_call(
"mnote.evidence.search" => evidence::evidence_search(&state, &context, &input).await,
"mnote.evidence.read" => evidence::evidence_read(&state, &context, &input).await,
"mnote.evidence.open" => evidence::evidence_open(&state, &context, &input).await,
"mnote.index.status" => index::index_status(&state, &context, &input).await,
"mnote.index.refresh" => index::index_refresh(&state, &context, &input).await,
"mnote.index.update_settings" => {
index::index_update_settings(&state, &context, &input).await
}
"mnote.doc.plan_update" => doc::plan_update(&state, &context, &input).await,
"mnote.block.fetch" => block::block_fetch(&state, &context, &input).await,
"mnote.block.replace" => block::block_replace(&state, &context, &input).await,
@@ -704,6 +709,8 @@ fn is_read_tool(tool_name: &str) -> bool {
| "mnote.evidence.search"
| "mnote.evidence.read"
| "mnote.evidence.open"
| "mnote.index.status"
| "mnote.index.refresh"
| "mnote.block.fetch"
| "mnote.mindmap.fetch"
| "mnote.office.fetch_summary"
@@ -1578,6 +1585,70 @@ mod tests {
);
}
#[tokio::test]
async fn hermes_tools_manifest_exposes_mnote_capability_packs() {
let response = app()
.oneshot(
Request::builder()
.uri("/api/hermes/tools/mnote/manifest")
.header("x-mnote-actor-id", "user_1")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let payload: Value = serde_json::from_slice(&body).expect("json");
let manifest = &payload["manifest"];
let capabilities = manifest["capabilities"].as_array().expect("capabilities");
assert!(capabilities
.iter()
.any(|capability| capability["id"] == "mnote-local-index"));
assert!(capabilities
.iter()
.any(|capability| capability["id"] == "mnote-onlyoffice-live"));
let tools = manifest["tools"].as_array().expect("tools");
let index_status = tools
.iter()
.find(|tool| tool["name"] == "mnote.index.status")
.expect("index status tool");
assert!(index_status["capabilityIds"]
.as_array()
.expect("index capability ids")
.iter()
.any(|id| id == "mnote-local-index"));
let onlyoffice_batch_set = tools
.iter()
.find(|tool| tool["name"] == "mnote.onlyoffice.sheet.batch_set_values")
.expect("onlyoffice batch set tool");
assert_eq!(
onlyoffice_batch_set["capabilityId"],
"mnote-onlyoffice-live"
);
assert!(onlyoffice_batch_set["capabilityIds"]
.as_array()
.expect("onlyoffice capability ids")
.iter()
.any(|id| id == "mnote-onlyoffice-live"));
let context_snapshot = tools
.iter()
.find(|tool| tool["name"] == "mnote.context.snapshot")
.expect("context snapshot tool");
assert!(
context_snapshot["capabilityIds"]
.as_array()
.expect("context capability ids")
.len()
> 1
);
}
#[tokio::test]
async fn hermes_tools_onlyoffice_session_current_reads_registered_bridge_session() {
let app = app();
@@ -5386,6 +5457,21 @@ mod tests {
)
.expect("markdown");
let root_uri = format!("file://{}", root.display());
crate::routes::local_search_index::write_local_index_settings(
&root,
&[String::from(".")],
None,
None,
None,
None,
)
.expect("settings");
crate::routes::local_search_index::refresh_local_search_index(
&root,
&root_uri,
"local-ws-docs-search",
)
.expect("refresh");
let response = app()
.oneshot(
@@ -5437,6 +5523,12 @@ mod tests {
result["source"]["ownerDocumentPath"].as_str(),
Some("README.md")
);
assert!(result["citationMarkdown"]
.as_str()
.is_some_and(|value| value.contains("](/documents/")));
assert!(result["citationUrl"]
.as_str()
.is_some_and(|value| value.contains("resourceTab=")));
let evidence_id = result["evidenceId"].as_str().expect("evidence id");
assert_eq!(
payload["result"]["evidenceIds"][0].as_str(),
@@ -5480,6 +5572,112 @@ mod tests {
let _ = fs::remove_dir_all(&root);
}
#[tokio::test]
async fn hermes_tools_local_index_update_and_status_manage_scope() {
let root = std::env::temp_dir().join(format!("mnote-index-tool-{}", std::process::id()));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(root.join(".mnote")).expect("metadata");
fs::create_dir_all(root.join("docs")).expect("docs");
fs::write(
root.join(".mnote").join("workspace.json"),
r#"{"workspaceId":"local-ws-index-tool","ownerId":"user_1","createdAt":"2026-06-04T00:00:00Z","capabilities":["local_files","search"]}"#,
)
.expect("manifest");
fs::write(
root.join("docs").join("indexed.md"),
"# Indexed\n\nindex-tool-token\n",
)
.expect("markdown");
let root_uri = format!("file://{}", root.display());
let app = app();
let update_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/hermes/tools/mnote/call")
.header("content-type", "application/json")
.header("x-mnote-actor-id", "user_1")
.header("x-mnote-actor-type", "user")
.body(Body::from(
json!({
"toolName": "mnote.index.update_settings",
"workspaceId": "local-ws-index-tool",
"sourceKind": "local_folder",
"rootUri": root_uri,
"sessionId": "sess_index_tool",
"runId": "run_index_tool",
"toolCallId": "call_index_update",
"traceId": "trace_index_tool",
"dryRun": false,
"idempotencyKey": "idem_index_tool_update",
"args": {
"includePaths": ["docs"],
"scheduleMode": "manual",
"scheduleTime": "02:00",
"runOnChange": false
}
})
.to_string(),
))
.expect("update request"),
)
.await
.expect("update response");
assert_eq!(update_response.status(), StatusCode::OK);
let update_body = to_bytes(update_response.into_body(), usize::MAX)
.await
.expect("update body");
let update_payload: Value = serde_json::from_slice(&update_body).expect("update json");
assert_eq!(
update_payload["result"]["settings"]["includePaths"][0],
"docs"
);
assert_eq!(update_payload["result"]["index"]["documentCount"], 1);
assert!(root.join(".mnote/index/search-index.json").exists());
assert!(root.join(".mnote/index/evidence.sqlite").exists());
let status_response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/hermes/tools/mnote/call")
.header("content-type", "application/json")
.header("x-mnote-actor-id", "user_1")
.header("x-mnote-actor-type", "user")
.body(Body::from(
json!({
"toolName": "mnote.index.status",
"workspaceId": "local-ws-index-tool",
"sourceKind": "local_folder",
"rootUri": root_uri,
"sessionId": "sess_index_tool",
"runId": "run_index_tool",
"toolCallId": "call_index_status",
"traceId": "trace_index_tool",
"args": {}
})
.to_string(),
))
.expect("status request"),
)
.await
.expect("status response");
assert_eq!(status_response.status(), StatusCode::OK);
let status_body = to_bytes(status_response.into_body(), usize::MAX)
.await
.expect("status body");
let status_payload: Value = serde_json::from_slice(&status_body).expect("status json");
assert_eq!(
status_payload["result"]["result"]["settings"]["includePaths"][0],
"docs"
);
assert_eq!(status_payload["audit"]["effect"], "read");
let _ = fs::remove_dir_all(&root);
}
#[tokio::test]
async fn hermes_tools_legacy_docs_read_returns_document_with_locator() {
let root =