Improve local evidence search and AI capabilities
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::hermes_tools::{ensure_write_authorized, ToolCallInput};
|
||||
use crate::routes;
|
||||
use axum::http::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct IndexSettingsArgs {
|
||||
#[serde(default)]
|
||||
include_paths: Vec<String>,
|
||||
#[serde(default)]
|
||||
schedule_mode: Option<String>,
|
||||
#[serde(default)]
|
||||
schedule_time: Option<String>,
|
||||
#[serde(default)]
|
||||
schedule_date: Option<String>,
|
||||
#[serde(default)]
|
||||
run_on_change: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn index_status(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let workspace_id = effective_workspace_id(context, input);
|
||||
let root_uri = required_root_uri(
|
||||
context,
|
||||
input,
|
||||
"mnote_index_root_required",
|
||||
"本地索引状态缺少 rootUri",
|
||||
)?;
|
||||
let root_path =
|
||||
routes::ensure_local_workspace_read_access_with_state(state, context, &root_uri)
|
||||
.map_err(|error| error.with_context(context))?;
|
||||
let user_settings = read_user_settings(state, context, &workspace_id, &root_path)?;
|
||||
let effective_settings = routes::effective_local_index_settings_for_root(
|
||||
state.control_plane(),
|
||||
&workspace_id,
|
||||
&root_path,
|
||||
)?;
|
||||
let status = routes::local_index_status_with_settings(
|
||||
&root_path,
|
||||
&root_uri,
|
||||
&workspace_id,
|
||||
&user_settings,
|
||||
&effective_settings,
|
||||
)?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"schema": "mnote.index.status_result.v1",
|
||||
"result": status
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn index_refresh(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let workspace_id = effective_workspace_id(context, input);
|
||||
let root_uri = required_root_uri(
|
||||
context,
|
||||
input,
|
||||
"mnote_index_root_required",
|
||||
"本地索引刷新缺少 rootUri",
|
||||
)?;
|
||||
let root_path =
|
||||
routes::ensure_local_workspace_read_access_with_state(state, context, &root_uri)
|
||||
.map_err(|error| error.with_context(context))?;
|
||||
let effective_settings = routes::effective_local_index_settings_for_root(
|
||||
state.control_plane(),
|
||||
&workspace_id,
|
||||
&root_path,
|
||||
)?;
|
||||
let refreshed = routes::refresh_local_search_index_with_settings(
|
||||
&root_path,
|
||||
&root_uri,
|
||||
&workspace_id,
|
||||
&effective_settings,
|
||||
)?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"schema": "mnote.index.refresh_result.v1",
|
||||
"index": refreshed
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn index_update_settings(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
ensure_write_authorized(context, input)?;
|
||||
let workspace_id = effective_workspace_id(context, input);
|
||||
let root_uri = required_root_uri(
|
||||
context,
|
||||
input,
|
||||
"mnote_index_root_required",
|
||||
"本地索引设置缺少 rootUri",
|
||||
)?;
|
||||
let args = parse_settings_args(context, input)?;
|
||||
let root_path =
|
||||
routes::ensure_local_workspace_write_access_with_state(state, context, &root_uri)
|
||||
.map_err(|error| error.with_context(context))?;
|
||||
let actor_id = routes::current_actor_id(state, context).ok_or_else(|| {
|
||||
WebError::new(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"local_index_settings_auth_required",
|
||||
"本地索引设置需要登录用户",
|
||||
)
|
||||
.with_context(context)
|
||||
})?;
|
||||
if input.dry_run == Some(true) {
|
||||
let planned_settings = routes::preview_user_local_index_settings(
|
||||
state.control_plane(),
|
||||
&actor_id,
|
||||
&workspace_id,
|
||||
&root_path,
|
||||
&args.include_paths,
|
||||
args.schedule_mode.as_deref(),
|
||||
args.schedule_time.as_deref(),
|
||||
args.schedule_date.as_deref(),
|
||||
args.run_on_change,
|
||||
)?;
|
||||
let current_status = index_status(state, context, input).await?;
|
||||
let would_clear_index_files = planned_settings.include_paths.is_empty();
|
||||
return Ok(json!({
|
||||
"ok": true,
|
||||
"schema": "mnote.index.update_settings_plan.v1",
|
||||
"dryRun": true,
|
||||
"plannedSettings": planned_settings,
|
||||
"currentStatus": current_status.get("result").cloned().unwrap_or(Value::Null),
|
||||
"wouldRefresh": true,
|
||||
"wouldClearIndexFiles": would_clear_index_files
|
||||
}));
|
||||
}
|
||||
let settings = routes::write_user_local_index_settings(
|
||||
state.control_plane(),
|
||||
&actor_id,
|
||||
&workspace_id,
|
||||
&root_path,
|
||||
&args.include_paths,
|
||||
args.schedule_mode.as_deref(),
|
||||
args.schedule_time.as_deref(),
|
||||
args.schedule_date.as_deref(),
|
||||
args.run_on_change,
|
||||
)?;
|
||||
let effective_settings = routes::effective_local_index_settings_for_root(
|
||||
state.control_plane(),
|
||||
&workspace_id,
|
||||
&root_path,
|
||||
)?;
|
||||
let refreshed = routes::refresh_local_search_index_with_settings(
|
||||
&root_path,
|
||||
&root_uri,
|
||||
&workspace_id,
|
||||
&effective_settings,
|
||||
)?;
|
||||
let status = routes::local_index_status_with_settings(
|
||||
&root_path,
|
||||
&root_uri,
|
||||
&workspace_id,
|
||||
&settings,
|
||||
&effective_settings,
|
||||
)?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"schema": "mnote.index.update_settings_result.v1",
|
||||
"settings": settings,
|
||||
"index": refreshed,
|
||||
"result": status
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_settings_args(
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<IndexSettingsArgs, WebError> {
|
||||
let args = input.args.clone().unwrap_or_else(|| json!({}));
|
||||
serde_json::from_value::<IndexSettingsArgs>(args).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"mnote_index_settings_payload_invalid",
|
||||
format!("本地索引设置参数无效: {error}"),
|
||||
)
|
||||
.with_context(context)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_user_settings(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
workspace_id: &str,
|
||||
root_path: &std::path::Path,
|
||||
) -> Result<routes::LocalIndexSettings, WebError> {
|
||||
if let Some(actor_id) = routes::current_actor_id(state, context) {
|
||||
return routes::read_user_local_index_settings(
|
||||
state.control_plane(),
|
||||
&actor_id,
|
||||
workspace_id,
|
||||
root_path,
|
||||
);
|
||||
}
|
||||
routes::read_local_index_settings_or_default(root_path)
|
||||
}
|
||||
|
||||
fn effective_workspace_id(context: &RequestContext, input: &ToolCallInput) -> String {
|
||||
input
|
||||
.effective_workspace_id()
|
||||
.or_else(|| context.workspace.workspace_id.clone())
|
||||
.unwrap_or_else(|| "default".into())
|
||||
}
|
||||
|
||||
fn required_root_uri(
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
code: &'static str,
|
||||
message: &'static str,
|
||||
) -> Result<String, WebError> {
|
||||
input
|
||||
.effective_root_uri()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| WebError::bad_request_code(code, message).with_context(context))
|
||||
}
|
||||
@@ -1,9 +1,94 @@
|
||||
use super::skill;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub const MANIFEST_SCHEMA_VERSION: &str = "mnote.hermes_tool_manifest.v1";
|
||||
pub const TOOL_SCHEMA_VERSION: &str = "mnote.hermes_tool.v1";
|
||||
|
||||
pub fn manifest() -> Value {
|
||||
let tools = annotate_tools_with_capabilities(vec![
|
||||
skill_read_tool(),
|
||||
context_snapshot_tool(),
|
||||
context_read_current_page_tool(),
|
||||
context_resolve_target_tool(),
|
||||
doc_fetch_tool(),
|
||||
doc_find_tool(),
|
||||
evidence_search_tool(),
|
||||
evidence_read_tool(),
|
||||
evidence_open_tool(),
|
||||
index_status_tool(),
|
||||
index_refresh_tool(),
|
||||
index_update_settings_tool(),
|
||||
block_fetch_tool(),
|
||||
doc_plan_update_tool(),
|
||||
block_replace_tool(),
|
||||
block_insert_after_tool(),
|
||||
block_delete_tool(),
|
||||
block_move_after_tool(),
|
||||
doc_apply_block_ops_tool(),
|
||||
doc_markdown_edit_tool(),
|
||||
page_get_tool(),
|
||||
page_save_tool(),
|
||||
available_tool(
|
||||
"mnote.page.update_title",
|
||||
"更新当前页面标题",
|
||||
["page.write"],
|
||||
),
|
||||
available_tool(
|
||||
"mnote.page.update_options",
|
||||
"更新当前页面设置",
|
||||
["page.write"],
|
||||
),
|
||||
mindmap_fetch_tool(),
|
||||
mindmap_apply_ops_tool(),
|
||||
mindmap_create_from_outline_tool(),
|
||||
office_fetch_summary_tool(),
|
||||
office_propose_changes_tool(),
|
||||
onlyoffice_session_current_tool(),
|
||||
onlyoffice_capabilities_tool(),
|
||||
onlyoffice_selection_get_tool(),
|
||||
onlyoffice_document_insert_text_tool(),
|
||||
onlyoffice_document_replace_selection_tool(),
|
||||
onlyoffice_document_insert_html_tool(),
|
||||
onlyoffice_document_export_tool(),
|
||||
onlyoffice_document_search_replace_tool(),
|
||||
onlyoffice_document_insert_table_tool(),
|
||||
onlyoffice_document_get_comments_tool(),
|
||||
onlyoffice_document_add_comment_tool(),
|
||||
onlyoffice_sheet_get_sheets_tool(),
|
||||
onlyoffice_sheet_add_sheet_tool(),
|
||||
onlyoffice_sheet_rename_sheet_tool(),
|
||||
onlyoffice_sheet_get_range_tool(),
|
||||
onlyoffice_sheet_get_range_values_tool(),
|
||||
onlyoffice_sheet_get_values_tool(),
|
||||
onlyoffice_sheet_set_value_tool(),
|
||||
onlyoffice_sheet_set_formula_tool(),
|
||||
onlyoffice_sheet_batch_set_values_tool(),
|
||||
onlyoffice_sheet_set_range_values_tool(),
|
||||
onlyoffice_sheet_format_range_tool(),
|
||||
onlyoffice_sheet_set_dimensions_tool(),
|
||||
onlyoffice_sheet_sort_range_tool(),
|
||||
onlyoffice_sheet_add_chart_tool(),
|
||||
onlyoffice_presentation_get_slides_tool(),
|
||||
onlyoffice_presentation_get_slide_texts_tool(),
|
||||
onlyoffice_presentation_get_shapes_tool(),
|
||||
onlyoffice_presentation_add_text_slide_tool(),
|
||||
onlyoffice_presentation_replace_text_tool(),
|
||||
onlyoffice_presentation_set_shape_text_tool(),
|
||||
onlyoffice_presentation_delete_slide_tool(),
|
||||
onlyoffice_presentation_add_table_tool(),
|
||||
onlyoffice_presentation_clear_slide_tool(),
|
||||
onlyoffice_presentation_add_shape_tool(),
|
||||
available_tool(
|
||||
"mnote.artifact.create_summary",
|
||||
"为当前页面创建或更新 AI Summary",
|
||||
["artifact.write"],
|
||||
),
|
||||
available_tool(
|
||||
"mnote.artifact.create_ai_note",
|
||||
"基于当前页面创建新的 AI Note",
|
||||
["artifact.write"],
|
||||
),
|
||||
]);
|
||||
json!({
|
||||
"schemaVersion": MANIFEST_SCHEMA_VERSION,
|
||||
"plugin": {
|
||||
@@ -12,74 +97,30 @@ pub fn manifest() -> Value {
|
||||
"runtimeOwner": "mnote-web",
|
||||
"writeOwner": "rust-runtime-kernel"
|
||||
},
|
||||
"tools": [
|
||||
skill_read_tool(),
|
||||
context_snapshot_tool(),
|
||||
context_read_current_page_tool(),
|
||||
context_resolve_target_tool(),
|
||||
doc_fetch_tool(),
|
||||
doc_find_tool(),
|
||||
evidence_search_tool(),
|
||||
evidence_read_tool(),
|
||||
evidence_open_tool(),
|
||||
block_fetch_tool(),
|
||||
doc_plan_update_tool(),
|
||||
block_replace_tool(),
|
||||
block_insert_after_tool(),
|
||||
block_delete_tool(),
|
||||
block_move_after_tool(),
|
||||
doc_apply_block_ops_tool(),
|
||||
doc_markdown_edit_tool(),
|
||||
page_get_tool(),
|
||||
page_save_tool(),
|
||||
available_tool("mnote.page.update_title", "更新当前页面标题", ["page.write"]),
|
||||
available_tool("mnote.page.update_options", "更新当前页面设置", ["page.write"]),
|
||||
mindmap_fetch_tool(),
|
||||
mindmap_apply_ops_tool(),
|
||||
mindmap_create_from_outline_tool(),
|
||||
office_fetch_summary_tool(),
|
||||
office_propose_changes_tool(),
|
||||
onlyoffice_session_current_tool(),
|
||||
onlyoffice_capabilities_tool(),
|
||||
onlyoffice_selection_get_tool(),
|
||||
onlyoffice_document_insert_text_tool(),
|
||||
onlyoffice_document_replace_selection_tool(),
|
||||
onlyoffice_document_insert_html_tool(),
|
||||
onlyoffice_document_export_tool(),
|
||||
onlyoffice_document_search_replace_tool(),
|
||||
onlyoffice_document_insert_table_tool(),
|
||||
onlyoffice_document_get_comments_tool(),
|
||||
onlyoffice_document_add_comment_tool(),
|
||||
onlyoffice_sheet_get_sheets_tool(),
|
||||
onlyoffice_sheet_add_sheet_tool(),
|
||||
onlyoffice_sheet_rename_sheet_tool(),
|
||||
onlyoffice_sheet_get_range_tool(),
|
||||
onlyoffice_sheet_get_range_values_tool(),
|
||||
onlyoffice_sheet_get_values_tool(),
|
||||
onlyoffice_sheet_set_value_tool(),
|
||||
onlyoffice_sheet_set_formula_tool(),
|
||||
onlyoffice_sheet_batch_set_values_tool(),
|
||||
onlyoffice_sheet_set_range_values_tool(),
|
||||
onlyoffice_sheet_format_range_tool(),
|
||||
onlyoffice_sheet_set_dimensions_tool(),
|
||||
onlyoffice_sheet_sort_range_tool(),
|
||||
onlyoffice_sheet_add_chart_tool(),
|
||||
onlyoffice_presentation_get_slides_tool(),
|
||||
onlyoffice_presentation_get_slide_texts_tool(),
|
||||
onlyoffice_presentation_get_shapes_tool(),
|
||||
onlyoffice_presentation_add_text_slide_tool(),
|
||||
onlyoffice_presentation_replace_text_tool(),
|
||||
onlyoffice_presentation_set_shape_text_tool(),
|
||||
onlyoffice_presentation_delete_slide_tool(),
|
||||
onlyoffice_presentation_add_table_tool(),
|
||||
onlyoffice_presentation_clear_slide_tool(),
|
||||
onlyoffice_presentation_add_shape_tool(),
|
||||
available_tool("mnote.artifact.create_summary", "为当前页面创建或更新 AI Summary", ["artifact.write"]),
|
||||
available_tool("mnote.artifact.create_ai_note", "基于当前页面创建新的 AI Note", ["artifact.write"])
|
||||
]
|
||||
"capabilities": skill::manifest_capabilities(),
|
||||
"tools": tools
|
||||
})
|
||||
}
|
||||
|
||||
fn annotate_tools_with_capabilities(tools: Vec<Value>) -> Vec<Value> {
|
||||
tools
|
||||
.into_iter()
|
||||
.map(|mut tool| {
|
||||
let name = tool.get("name").and_then(Value::as_str).unwrap_or_default();
|
||||
let capability_ids = skill::capability_ids_for_tool(name);
|
||||
if let Value::Object(map) = &mut tool {
|
||||
if let Some(first) = capability_ids.first() {
|
||||
map.insert("capabilityId".into(), json!(first));
|
||||
}
|
||||
if !capability_ids.is_empty() {
|
||||
map.insert("capabilityIds".into(), json!(capability_ids));
|
||||
}
|
||||
}
|
||||
tool
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn skill_read_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
@@ -286,7 +327,7 @@ fn evidence_search_tool() -> Value {
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.evidence.search",
|
||||
"description": "搜索可回跳原文的文档证据,返回 quote、EvidenceLocator 与 openAction。",
|
||||
"description": "搜索可回跳原文的文档证据,返回 quote、EvidenceLocator、openAction 与可直接放进最终回答的 citationMarkdown 链接。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["evidence.read", "page.read"],
|
||||
"status": "available",
|
||||
@@ -317,7 +358,7 @@ fn evidence_read_tool() -> Value {
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.evidence.read",
|
||||
"description": "按 EvidenceLocator 读取原文证据及周边上下文,供回答引用。",
|
||||
"description": "按 EvidenceLocator 读取原文证据及周边上下文,返回 quote/contextBlocks 与可点击引用链接供回答引用。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["evidence.read", "page.read"],
|
||||
"status": "available",
|
||||
@@ -337,7 +378,7 @@ fn evidence_open_tool() -> Value {
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.evidence.open",
|
||||
"description": "把 EvidenceLocator 归一化为 MNote 可执行的打开/定位动作。",
|
||||
"description": "把 EvidenceLocator 归一化为 MNote 可执行的打开/定位动作,并返回 citationUrl/citationMarkdown。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["evidence.read", "page.read"],
|
||||
"status": "available",
|
||||
@@ -350,6 +391,79 @@ fn evidence_open_tool() -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn index_status_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("rootUri".into(), json!({ "type": "string" }));
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.index.status",
|
||||
"description": "查看本地索引范围、缓存文件、构建时间、文档数和 evidence block 数。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["index.read", "evidence.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "rootUri"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn index_refresh_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("rootUri".into(), json!({ "type": "string" }));
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.index.refresh",
|
||||
"description": "按当前有效索引范围重建本地搜索/evidence 缓存,不修改 Markdown 正文。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["index.read", "evidence.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, false, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "rootUri"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn index_update_settings_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("rootUri".into(), json!({ "type": "string" }));
|
||||
map.insert(
|
||||
"includePaths".into(),
|
||||
json!({ "type": "array", "items": { "type": "string" } }),
|
||||
);
|
||||
map.insert(
|
||||
"scheduleMode".into(),
|
||||
json!({ "type": "string", "enum": ["manual", "daily", "weekly", "monthly"] }),
|
||||
);
|
||||
map.insert("scheduleTime".into(), json!({ "type": "string" }));
|
||||
map.insert("scheduleDate".into(), json!({ "type": "string" }));
|
||||
map.insert("runOnChange".into(), json!({ "type": "boolean" }));
|
||||
map.insert("dryRun".into(), json!({ "type": "boolean" }));
|
||||
map.insert("idempotencyKey".into(), json!({ "type": "string" }));
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.index.update_settings",
|
||||
"description": "新增或删除本地索引范围;includePaths 为空表示删除当前用户索引范围并在无有效范围时清空索引文件。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["index.write", "evidence.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(false, true, false, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "rootUri", "includePaths", "dryRun", "idempotencyKey"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn block_fetch_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod block;
|
||||
pub mod context_tools;
|
||||
pub mod doc;
|
||||
pub mod evidence;
|
||||
pub mod index;
|
||||
pub mod manifest;
|
||||
pub mod onlyoffice_live;
|
||||
pub mod page;
|
||||
|
||||
@@ -5,10 +5,11 @@ use axum::http::StatusCode;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct MnoteSkill {
|
||||
pub struct MnoteCapabilityPack {
|
||||
pub id: &'static str,
|
||||
pub title: &'static str,
|
||||
pub description: &'static str,
|
||||
pub category: &'static str,
|
||||
pub agent_ids: &'static [&'static str],
|
||||
pub read_only: bool,
|
||||
pub requires_context_refs: &'static [&'static str],
|
||||
@@ -16,11 +17,14 @@ pub struct MnoteSkill {
|
||||
pub content: &'static str,
|
||||
}
|
||||
|
||||
const SKILLS: &[MnoteSkill] = &[
|
||||
MnoteSkill {
|
||||
pub type MnoteSkill = MnoteCapabilityPack;
|
||||
|
||||
const CAPABILITY_PACKS: &[MnoteCapabilityPack] = &[
|
||||
MnoteCapabilityPack {
|
||||
id: "mnote-current-page",
|
||||
title: "MNote current page",
|
||||
description: "Read the current MNote Markdown page only when the task needs page content.",
|
||||
title: "当前页读取",
|
||||
description: "仅在任务需要当前 MNote Markdown 页面内容时读取当前页。",
|
||||
category: "mnote",
|
||||
agent_ids: &["hermes", "reasonix"],
|
||||
read_only: true,
|
||||
requires_context_refs: &["current_page"],
|
||||
@@ -31,12 +35,13 @@ const SKILLS: &[MnoteSkill] = &[
|
||||
],
|
||||
content: include_str!("../../../../../skills/mnote-current-page/SKILL.md"),
|
||||
},
|
||||
MnoteSkill {
|
||||
id: "mnote-document-evidence",
|
||||
title: "MNote document evidence",
|
||||
description: "Search local documents and resources with clickable evidence locators.",
|
||||
MnoteCapabilityPack {
|
||||
id: "mnote-local-index",
|
||||
title: "本地索引与证据检索",
|
||||
description: "检索本地文档证据,并管理本地索引范围、刷新和删除。",
|
||||
category: "knowledge",
|
||||
agent_ids: &["hermes", "reasonix"],
|
||||
read_only: true,
|
||||
read_only: false,
|
||||
requires_context_refs: &["folder"],
|
||||
tool_names: &[
|
||||
"mnote.context.snapshot",
|
||||
@@ -44,23 +49,28 @@ const SKILLS: &[MnoteSkill] = &[
|
||||
"mnote.evidence.search",
|
||||
"mnote.evidence.read",
|
||||
"mnote.evidence.open",
|
||||
"mnote.index.status",
|
||||
"mnote.index.refresh",
|
||||
"mnote.index.update_settings",
|
||||
],
|
||||
content: include_str!("../../../../../skills/mnote-document-evidence/SKILL.md"),
|
||||
content: include_str!("../../../../../skills/mnote-local-index/SKILL.md"),
|
||||
},
|
||||
MnoteSkill {
|
||||
MnoteCapabilityPack {
|
||||
id: "mnote-local-file",
|
||||
title: "MNote local file editing",
|
||||
description: "Read and patch local Markdown files inside MNote allowed roots.",
|
||||
title: "本地文件编辑",
|
||||
description: "在 MNote 授权目录内读取和修改本地 Markdown 文件。",
|
||||
category: "file",
|
||||
agent_ids: &["hermes", "reasonix"],
|
||||
read_only: false,
|
||||
requires_context_refs: &["current_page", "file", "folder"],
|
||||
tool_names: &["mnote.context.snapshot", "mnote.context.resolve_target"],
|
||||
content: include_str!("../../../../../skills/mnote-local-file/SKILL.md"),
|
||||
},
|
||||
MnoteSkill {
|
||||
MnoteCapabilityPack {
|
||||
id: "mnote-onlyoffice-live",
|
||||
title: "MNote ONLYOFFICE live bridge",
|
||||
description: "Operate the currently open ONLYOFFICE editor session for Word, Excel, and PPT.",
|
||||
title: "ONLYOFFICE 实时编辑",
|
||||
description: "操作当前已打开的 ONLYOFFICE Word、Excel、PPT 编辑会话。",
|
||||
category: "office",
|
||||
agent_ids: &["hermes", "reasonix"],
|
||||
read_only: false,
|
||||
requires_context_refs: &["onlyoffice"],
|
||||
@@ -103,10 +113,11 @@ const SKILLS: &[MnoteSkill] = &[
|
||||
],
|
||||
content: include_str!("../../../../../skills/mnote-onlyoffice-live/SKILL.md"),
|
||||
},
|
||||
MnoteSkill {
|
||||
MnoteCapabilityPack {
|
||||
id: "mnote-mindmap",
|
||||
title: "MNote mindmap editing",
|
||||
description: "Read, update, summarize, or create MNote mindmap resources, including generating a new mindmap from PDF or document outlines.",
|
||||
title: "思维导图",
|
||||
description: "读取、更新、总结或创建 MNote 思维导图资源。",
|
||||
category: "resource",
|
||||
agent_ids: &["hermes", "reasonix"],
|
||||
read_only: false,
|
||||
requires_context_refs: &["current_page", "file", "folder", "resource"],
|
||||
@@ -119,10 +130,11 @@ const SKILLS: &[MnoteSkill] = &[
|
||||
],
|
||||
content: include_str!("../../../../../skills/mnote-mindmap/SKILL.md"),
|
||||
},
|
||||
MnoteSkill {
|
||||
MnoteCapabilityPack {
|
||||
id: "mnote-chat-only",
|
||||
title: "MNote chat only",
|
||||
description: "Reply conversationally without reading or writing MNote page/file context.",
|
||||
title: "纯聊天",
|
||||
description: "只进行对话回复,不读取或写入 MNote 页面、文件上下文。",
|
||||
category: "chat",
|
||||
agent_ids: &["chat_only", "hermes", "reasonix"],
|
||||
read_only: true,
|
||||
requires_context_refs: &[],
|
||||
@@ -132,20 +144,75 @@ const SKILLS: &[MnoteSkill] = &[
|
||||
];
|
||||
|
||||
pub fn skill_summaries_for_agent(agent_id: Option<&str>) -> Vec<Value> {
|
||||
SKILLS
|
||||
capability_summaries_for_agent(agent_id)
|
||||
}
|
||||
|
||||
pub fn capability_summaries_for_agent(agent_id: Option<&str>) -> Vec<Value> {
|
||||
CAPABILITY_PACKS
|
||||
.iter()
|
||||
.filter(|skill| skill_matches_agent(skill, agent_id))
|
||||
.map(skill_summary)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn public_capability_packs() -> &'static [MnoteCapabilityPack] {
|
||||
CAPABILITY_PACKS
|
||||
}
|
||||
|
||||
pub fn find_skill(skill_id: &str, agent_id: Option<&str>) -> Option<&'static MnoteSkill> {
|
||||
let requested = skill_id.trim();
|
||||
SKILLS
|
||||
find_capability_pack(skill_id, agent_id)
|
||||
}
|
||||
|
||||
pub fn find_capability_pack(
|
||||
capability_id: &str,
|
||||
agent_id: Option<&str>,
|
||||
) -> Option<&'static MnoteCapabilityPack> {
|
||||
let requested = canonical_skill_id(capability_id.trim());
|
||||
CAPABILITY_PACKS
|
||||
.iter()
|
||||
.find(|skill| skill.id == requested && skill_matches_agent(skill, agent_id))
|
||||
}
|
||||
|
||||
pub fn capability_ids_for_tool(tool_name: &str) -> Vec<&'static str> {
|
||||
let name = tool_name.trim();
|
||||
if name.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
CAPABILITY_PACKS
|
||||
.iter()
|
||||
.filter(|pack| pack.tool_names.iter().any(|tool| *tool == name))
|
||||
.map(|pack| pack.id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn manifest_capabilities() -> Vec<Value> {
|
||||
CAPABILITY_PACKS
|
||||
.iter()
|
||||
.map(|pack| {
|
||||
json!({
|
||||
"id": pack.id,
|
||||
"title": pack.title,
|
||||
"description": pack.description,
|
||||
"category": pack.category,
|
||||
"agentIds": pack.agent_ids,
|
||||
"readOnly": pack.read_only,
|
||||
"requiresContextRefs": pack.requires_context_refs,
|
||||
"skillId": pack.id,
|
||||
"toolNames": pack.tool_names,
|
||||
"uiKind": if pack.category == "chat" { "chat" } else { "ai_capability" },
|
||||
"public": true
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn canonical_skill_id(skill_id: &str) -> &str {
|
||||
match skill_id {
|
||||
"mnote-document-evidence" => "mnote-local-index",
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn skill_read(
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
@@ -196,6 +263,7 @@ fn skill_summary(skill: &MnoteSkill) -> Value {
|
||||
"id": skill.id,
|
||||
"title": skill.title,
|
||||
"description": skill.description,
|
||||
"category": skill.category,
|
||||
"agentIds": skill.agent_ids,
|
||||
"readOnly": skill.read_only,
|
||||
"requiresContextRefs": skill.requires_context_refs,
|
||||
@@ -296,18 +364,33 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_registry_exposes_document_evidence_skill_to_agents() {
|
||||
fn skill_registry_exposes_local_index_skill_to_agents() {
|
||||
let hermes_skills = skill_summaries_for_agent(Some("hermes"));
|
||||
let skill = hermes_skills
|
||||
.iter()
|
||||
.find(|skill| skill["id"] == "mnote-document-evidence")
|
||||
.expect("hermes should see document evidence skill");
|
||||
assert_eq!(skill["readOnly"], true);
|
||||
.find(|skill| skill["id"] == "mnote-local-index")
|
||||
.expect("hermes should see local index skill");
|
||||
assert_eq!(skill["readOnly"], false);
|
||||
assert!(skill["toolNames"]
|
||||
.as_array()
|
||||
.expect("tool names")
|
||||
.iter()
|
||||
.any(|name| name == "mnote.evidence.search"));
|
||||
assert!(skill["toolNames"]
|
||||
.as_array()
|
||||
.expect("tool names")
|
||||
.iter()
|
||||
.any(|name| name == "mnote.index.update_settings"));
|
||||
assert!(!hermes_skills
|
||||
.iter()
|
||||
.any(|skill| skill["id"] == "mnote-document-evidence"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_read_keeps_document_evidence_compat_alias() {
|
||||
let skill = find_skill("mnote-document-evidence", Some("reasonix"))
|
||||
.expect("compat alias should resolve");
|
||||
assert_eq!(skill.id, "mnote-local-index");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user