use crate::context::RequestContext; use crate::error::WebError; use crate::hermes_tools::ToolCallInput; use axum::http::StatusCode; use serde_json::{json, Value}; use std::collections::HashSet; use std::fs; use std::path::{Path, PathBuf}; pub async fn mindmap_fetch( context: &RequestContext, input: &ToolCallInput, ) -> Result { let target = ResourceToolTarget::from_input(context, input, "mindmap", "mindmapId")?; ensure_resource_scope_allowed(context, input, &target)?; let path = target.resolve_read_path(context, input)?; let content = fs::read_to_string(&path).map_err(|error| { WebError::bad_request_code( "mnote_resource_read_failed", format!("无法读取 mindmap resource: {error}"), ) .with_context(context) })?; let data = serde_json::from_str::(&content).unwrap_or_else(|_| json!({ "raw": content })); let scope = input .arg_string("scope") .unwrap_or_else(|| "tree".into()) .to_ascii_lowercase(); let root = mindmap_root_value(&data).clone(); let nodes = collect_mindmap_nodes(&root); let envelope = if scope == "full_envelope" { data } else { Value::Null }; Ok(json!({ "objectIdentity": target.object_identity, "resourceKind": "mindmap", "documentId": target.document_id, "mindmapId": target.resource_id, "resourcePath": target.resource_path, "scope": scope, "root": root, "envelope": envelope, "nodes": nodes, "edges": [], "markdownSummary": mindmap_markdown_summary(&nodes), "revision": file_revision(&path), "source": "local_folder" })) } pub async fn mindmap_apply_ops( context: &RequestContext, input: &ToolCallInput, ) -> Result { ensure_resource_write_contract(context, input)?; let target = ResourceToolTarget::from_input(context, input, "mindmap", "mindmapId")?; ensure_resource_scope_allowed(context, input, &target)?; let path = target.resolve_write_path(context, input)?; let ops = input.arg_value("ops").ok_or_else(|| { WebError::bad_request_code("mnote_resource_ops_required", "mindmap 写工具缺少 ops") .with_context(context) })?; if input.dry_run.unwrap_or(false) { return Ok(json!({ "dryRun": true, "commandName": "mnote.mindmap.apply_ops", "objectIdentity": target.object_identity, "resourceKind": "mindmap", "documentId": target.document_id, "mindmapId": target.resource_id, "resourcePath": target.resource_path, "diff": [{"op": "mindmap.apply_ops", "ops": ops}] })); } ensure_mindmap_revision_precondition(context, input, &path)?; let content = fs::read_to_string(&path).map_err(|error| { WebError::bad_request_code( "mnote_resource_read_failed", format!("无法读取 mindmap resource: {error}"), ) .with_context(context) })?; let mut envelope = serde_json::from_str::(&content).map_err(|error| { WebError::bad_request_code( "mnote_resource_json_invalid", format!("mindmap resource 不是有效 JSON: {error}"), ) .with_context(context) })?; apply_mindmap_ops(context, &mut envelope, &ops)?; let serialized = serde_json::to_string_pretty(&envelope).map_err(|error| { WebError::bad_request_code( "mnote_resource_serialize_failed", format!("无法序列化 mindmap JSON: {error}"), ) .with_context(context) })?; fs::write(&path, serialized).map_err(|error| { WebError::bad_request_code( "mnote_resource_write_failed", format!("无法写入 mindmap resource: {error}"), ) .with_context(context) })?; let root = mindmap_root_value(&envelope).clone(); let nodes = collect_mindmap_nodes(&root); Ok(json!({ "dryRun": false, "commandName": "mnote.mindmap.apply_ops", "objectIdentity": target.object_identity, "resourceKind": "mindmap", "documentId": target.document_id, "mindmapId": target.resource_id, "resourcePath": target.resource_path, "root": root, "nodes": nodes, "edges": [], "markdownSummary": mindmap_markdown_summary(&nodes), "revision": file_revision(&path), "changedFiles": [target.relative_path()], "source": "local_folder" })) } pub async fn mindmap_create_from_outline( context: &RequestContext, input: &ToolCallInput, ) -> Result { ensure_resource_write_contract(context, input)?; let target = ResourceToolTarget::from_input(context, input, "mindmap", "mindmapId")?; ensure_resource_scope_allowed(context, input, &target)?; let path = target.resolve_create_path(context, input)?; let title = input .arg_string("title") .unwrap_or_else(|| "KMIND".to_string()); let outline = input.arg_value("outline").ok_or_else(|| { WebError::bad_request_code("mnote_mindmap_outline_required", "创建思维导图缺少 outline") .with_context(context) })?; let source_refs = input.arg_value("sourceRefs").unwrap_or_else(|| json!([])); let envelope = mindmap_envelope_from_outline(&title, &outline, source_refs, context)?; let root = mindmap_root_value(&envelope).clone(); let nodes = collect_mindmap_nodes(&root); let resource_relative_path = target.relative_path(); let mut changed_files = if input.dry_run.unwrap_or(false) { Vec::new() } else { vec![resource_relative_path.clone()] }; let mut embed_result = Value::Null; if !input.dry_run.unwrap_or(false) { let parent = path.parent().ok_or_else(|| { WebError::bad_request_code("mnote_resource_bad_path", "无法解析 mindmap 父目录") .with_context(context) })?; fs::create_dir_all(parent).map_err(|error| { WebError::bad_request_code( "mnote_resource_create_dir_failed", format!("无法创建 mindmap 目录: {error}"), ) .with_context(context) })?; let content = serde_json::to_string_pretty(&envelope).map_err(|error| { WebError::bad_request_code( "mnote_resource_serialize_failed", format!("无法序列化 mindmap JSON: {error}"), ) .with_context(context) })?; fs::write(&path, content).map_err(|error| { WebError::bad_request_code( "mnote_resource_write_failed", format!("无法写入 mindmap resource: {error}"), ) .with_context(context) })?; if input .arg_value("embedIntoPage") .and_then(|value| value.as_bool()) .unwrap_or(true) { embed_result = embed_mindmap_into_local_markdown_page( context, input, &target.document_id, &resource_relative_path, &title, )?; if let Some(changed_file) = embed_result .get("changedFile") .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) { changed_files.push(changed_file.to_string()); } } } Ok(json!({ "dryRun": input.dry_run.unwrap_or(false), "commandName": "mnote.mindmap.create_from_outline", "objectIdentity": target.object_identity, "resourceKind": "mindmap", "documentId": target.document_id, "mindmapId": target.resource_id, "resourcePath": target.resource_path, "envelope": envelope, "root": root, "nodes": nodes, "edges": [], "markdownSummary": mindmap_markdown_summary(&nodes), "revision": if input.dry_run.unwrap_or(false) { Value::Null } else { file_revision(&path) }, "changedFiles": changed_files, "embedResult": embed_result, "source": "local_folder" })) } pub async fn office_fetch_summary( context: &RequestContext, input: &ToolCallInput, ) -> Result { let target = ResourceToolTarget::from_input(context, input, "onlyoffice", "assetId")?; ensure_resource_scope_allowed(context, input, &target)?; let path = target.resolve_read_path(context, input)?; let metadata = fs::metadata(&path).map_err(|error| { WebError::bad_request_code( "mnote_resource_metadata_failed", format!("无法读取 Office resource 元数据: {error}"), ) .with_context(context) })?; let file_name = path .file_name() .and_then(|value| value.to_str()) .unwrap_or(target.resource_id.as_str()); Ok(json!({ "objectIdentity": target.object_identity, "resourceKind": "only_office", "documentId": target.document_id, "assetId": target.resource_id, "resourcePath": target.resource_path, "fileName": file_name, "mimeType": office_mime_type(&path), "textSummary": office_text_preview(&path), "outline": [], "revision": file_revision(&path), "size": metadata.len(), "source": "local_folder" })) } pub async fn office_propose_changes( context: &RequestContext, input: &ToolCallInput, ) -> Result { let target = ResourceToolTarget::from_input(context, input, "onlyoffice", "assetId")?; ensure_resource_scope_allowed(context, input, &target)?; let path = target.resolve_write_path(context, input)?; let instructions = input.arg_string("instructions").ok_or_else(|| { WebError::bad_request_code( "mnote_resource_instructions_required", "office 建议工具缺少 instructions", ) .with_context(context) })?; Ok(json!({ "dryRun": input.dry_run.unwrap_or(false), "commandName": "mnote.office.propose_changes", "objectIdentity": target.object_identity, "resourceKind": "only_office", "documentId": target.document_id, "assetId": target.resource_id, "resourcePath": target.resource_path, "basisRevision": input.arg_value("basisRevision"), "revision": file_revision(&path), "changeSummary": instructions, "suggestedEdits": [{ "kind": "instruction", "text": instructions }], "requiresOnlyOffice": true, "requiresOfficeCli": true, "writesBinary": false })) } struct ResourceToolTarget { document_id: String, resource_id: String, object_identity: String, resource_path: Option, } impl ResourceToolTarget { fn from_input( context: &RequestContext, input: &ToolCallInput, resource_kind: &str, resource_id_arg: &str, ) -> Result { let document_id = input.effective_document_id().ok_or_else(|| { WebError::bad_request_code( "mnote_resource_document_required", "资源工具缺少 documentId", ) .with_context(context) })?; let resource_id = input.arg_string(resource_id_arg).ok_or_else(|| { WebError::bad_request_code( "mnote_resource_id_required", format!("资源工具缺少 {resource_id_arg}"), ) .with_context(context) })?; let object_identity = format!("resource:{resource_kind}:{document_id}:{resource_id}"); Ok(Self { document_id, resource_id, object_identity, resource_path: input.arg_string("resourcePath"), }) } fn resolve_read_path( &self, context: &RequestContext, input: &ToolCallInput, ) -> Result { let root_uri = local_root_uri_for_resource(input).ok_or_else(|| { WebError::new( StatusCode::FORBIDDEN, "mnote_resource_root_uri_required", "资源工具需要授权 rootUri", ) .with_context(context) })?; let relative = self.relative_path(); crate::routes::ensure_local_path_read_access(context, &root_uri, &relative) .map_err(|error| error.with_context(context)) } fn resolve_write_path( &self, context: &RequestContext, input: &ToolCallInput, ) -> Result { let root_uri = local_root_uri_for_resource(input).ok_or_else(|| { WebError::new( StatusCode::FORBIDDEN, "mnote_resource_root_uri_required", "资源工具需要授权 rootUri", ) .with_context(context) })?; let root = crate::routes::ensure_local_workspace_access(context, &root_uri) .map_err(|error| error.with_context(context))?; let relative = self.relative_path(); let path = root.join(relative); let canonical = path.canonicalize().map_err(|error| { WebError::bad_request_code( "mnote_resource_unavailable", format!("无法访问资源文件: {error}"), ) .with_context(context) })?; if !canonical.starts_with(root) { return Err(WebError::bad_request_code( "mnote_resource_root_escape", "资源工具不能越过授权目录", ) .with_context(context)); } Ok(canonical) } fn resolve_create_path( &self, context: &RequestContext, input: &ToolCallInput, ) -> Result { let root_uri = local_root_uri_for_resource(input).ok_or_else(|| { WebError::new( StatusCode::FORBIDDEN, "mnote_resource_root_uri_required", "资源工具需要授权 rootUri", ) .with_context(context) })?; let root = crate::routes::ensure_local_workspace_access(context, &root_uri) .map_err(|error| error.with_context(context))?; let relative = self.relative_path(); let relative_path = Path::new(&relative); if relative_path.is_absolute() || relative_path .components() .any(|component| matches!(component, std::path::Component::ParentDir)) { return Err(WebError::bad_request_code( "mnote_resource_root_escape", "资源工具不能越过授权目录", ) .with_context(context)); } let path = root.join(relative_path); if !path.starts_with(&root) { return Err(WebError::bad_request_code( "mnote_resource_root_escape", "资源工具不能越过授权目录", ) .with_context(context)); } Ok(path) } fn relative_path(&self) -> String { self.resource_path .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) .unwrap_or_else(|| self.resource_id.clone()) } } fn ensure_resource_scope_allowed( context: &RequestContext, input: &ToolCallInput, target: &ResourceToolTarget, ) -> Result<(), WebError> { let Some(scope) = input.arg_value("aiAccessScope") else { return Ok(()); }; let allowed = scope .get("allowedResourceIds") .or_else(|| scope.get("allowed_resource_ids")) .and_then(Value::as_array) .map(|values| { values .iter() .filter_map(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) .collect::>() }) .unwrap_or_default(); if allowed.is_empty() || allowed.contains(&target.resource_id) || allowed.contains(&target.object_identity) { return Ok(()); } Err(WebError::new( StatusCode::FORBIDDEN, "mnote_resource_ai_scope_forbidden", "当前 AI scope 不允许访问该资源", ) .with_context(context)) } fn ensure_resource_write_contract( context: &RequestContext, input: &ToolCallInput, ) -> Result<(), WebError> { crate::hermes_tools::ensure_write_authorized(context, input) } fn local_root_uri_for_resource(input: &ToolCallInput) -> Option { input.effective_root_uri().or_else(|| { input .arg_value("aiAccessScope") .and_then(|scope| { scope .get("allowedRoots") .or_else(|| scope.get("allowed_roots")) .cloned() }) .and_then(|allowed_roots| { allowed_roots.as_array().and_then(|roots| { roots .iter() .filter_map(|root| { root.get("rootUri") .or_else(|| root.get("root_uri")) .and_then(Value::as_str) }) .map(str::trim) .find(|root_uri| !root_uri.is_empty()) .map(ToOwned::to_owned) }) }) }) } fn embed_mindmap_into_local_markdown_page( context: &RequestContext, input: &ToolCallInput, document_id: &str, resource_relative_path: &str, title: &str, ) -> Result { let root_uri = local_root_uri_for_resource(input).ok_or_else(|| { WebError::new( StatusCode::FORBIDDEN, "mnote_resource_root_uri_required", "资源工具需要授权 rootUri", ) .with_context(context) })?; let root = crate::routes::ensure_local_workspace_access(context, &root_uri) .map_err(|error| error.with_context(context))?; let page_relative_path = local_markdown_relative_path_from_document_id(context, document_id)?; let page_path = root.join(&page_relative_path); if !page_path.starts_with(&root) { return Err(WebError::bad_request_code( "mnote_resource_root_escape", "资源工具不能越过授权目录", ) .with_context(context)); } if !page_path.is_file() { return Err(WebError::bad_request_code( "mnote_resource_page_not_found", "找不到要绑定 mindmap 的本地 Markdown 页面", ) .with_context(context)); } let href = markdown_href_from_page(&root, &page_path, resource_relative_path); let mut markdown = fs::read_to_string(&page_path).map_err(|error| { WebError::bad_request_code( "mnote_resource_page_read_failed", format!("无法读取本地 Markdown 页面: {error}"), ) .with_context(context) })?; let link = format!("[{}]({href})", markdown_link_label(title)); if markdown.contains(&link) || markdown.contains(&format!("]({href})")) { return Ok(json!({ "status": "already_present", "documentId": document_id, "changedFile": page_relative_path, "href": href })); } if !markdown.ends_with('\n') { markdown.push('\n'); } if !markdown.ends_with("\n\n") { markdown.push('\n'); } markdown.push_str(&link); markdown.push('\n'); fs::write(&page_path, markdown).map_err(|error| { WebError::bad_request_code( "mnote_resource_page_write_failed", format!("无法写入本地 Markdown 页面: {error}"), ) .with_context(context) })?; Ok(json!({ "status": "embedded", "documentId": document_id, "changedFile": page_relative_path, "href": href })) } fn local_markdown_relative_path_from_document_id( context: &RequestContext, document_id: &str, ) -> Result { let encoded = document_id .trim() .strip_prefix("local-md:") .ok_or_else(|| { WebError::bad_request_code( "mnote_resource_local_markdown_required", "绑定 mindmap 需要 local-md 页面", ) .with_context(context) })?; let relative = crate::routes::decode_local_id_segment(encoded) .map_err(|error| error.with_context(context))?; let path = Path::new(&relative); if path.is_absolute() || path .components() .any(|component| matches!(component, std::path::Component::ParentDir)) { return Err(WebError::bad_request_code( "mnote_resource_root_escape", "资源工具不能越过授权目录", ) .with_context(context)); } Ok(relative) } fn markdown_href_from_page(root: &Path, page_path: &Path, resource_relative_path: &str) -> String { let page_dir = page_path.parent().unwrap_or(root); let target = root.join(resource_relative_path); if let Ok(relative) = target.strip_prefix(page_dir) { return path_to_markdown_href(relative); } let page_dir_relative = page_dir.strip_prefix(root).unwrap_or(Path::new("")); let depth = page_dir_relative .components() .filter(|component| matches!(component, std::path::Component::Normal(_))) .count(); let mut href = String::new(); for _ in 0..depth { href.push_str("../"); } href.push_str(&resource_relative_path.replace('\\', "/")); href } fn path_to_markdown_href(path: &Path) -> String { path.to_string_lossy().replace('\\', "/") } fn markdown_link_label(value: &str) -> String { value .trim() .replace('\\', "\\\\") .replace('[', "\\[") .replace(']', "\\]") .replace('\n', " ") } fn default_mindmap_view() -> Value { json!({ "state": { "scale": 1, "sx": 0, "sy": 0, "x": -44.99991989135742_f64, "y": -15.500006675720217_f64 }, "transform": { "a": 1, "b": 0, "c": 0, "d": 1, "e": -44.99991989135742_f64, "f": -15.500006675720217_f64, "originX": 0, "originY": 0, "rotate": 0, "scaleX": 1, "scaleY": 1, "shear": 0, "translateX": -44.99991989135742_f64, "translateY": -15.500006675720217_f64 } }) } fn mindmap_envelope_from_outline( title: &str, outline: &Value, source_refs: Value, context: &RequestContext, ) -> Result { let outline_items = outline.as_array().ok_or_else(|| { WebError::bad_request_code( "mnote_mindmap_outline_invalid", "mindmap outline 必须是数组", ) .with_context(context) })?; let title = title.trim(); let root_title = if title.is_empty() { "KMIND" } else { title }; Ok(json!({ "data": { "children": mindmap_outline_items_to_children(outline_items, "node"), "data": { "expand": true, "isActive": false, "text": root_title, "uid": "root" } }, "view": default_mindmap_view(), "metadata": { "sourceRefs": source_refs } })) } fn mindmap_outline_items_to_children(items: &[Value], prefix: &str) -> Vec { items .iter() .enumerate() .map(|(index, item)| { let ordinal = index + 1; let uid = format!("{prefix}_{ordinal}"); let text = item .get("text") .or_else(|| item.get("title")) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .unwrap_or("未命名节点"); let children = item .get("children") .and_then(Value::as_array) .map(|children| mindmap_outline_items_to_children(children, &uid)) .unwrap_or_default(); json!({ "data": { "expand": true, "isActive": false, "text": text, "uid": uid }, "children": children }) }) .collect() } fn mindmap_root_value(value: &Value) -> &Value { value .get("data") .filter(|data| data.get("data").is_some() || data.get("children").is_some()) .unwrap_or(value) } fn mindmap_root_value_mut(value: &mut Value) -> Option<&mut Value> { let has_envelope_root = value .get("data") .map(|data| data.get("data").is_some() || data.get("children").is_some()) .unwrap_or(false); if has_envelope_root { return value.get_mut("data"); } Some(value) } fn apply_mindmap_ops( context: &RequestContext, envelope: &mut Value, ops: &Value, ) -> Result<(), WebError> { let ops = ops.as_array().ok_or_else(|| { WebError::bad_request_code("mnote_resource_ops_invalid", "mindmap ops 必须是数组") .with_context(context) })?; let root = mindmap_root_value_mut(envelope).ok_or_else(|| { WebError::bad_request_code("mnote_resource_json_invalid", "mindmap 缺少 root") .with_context(context) })?; for op in ops { apply_mindmap_op(context, root, op)?; } Ok(()) } fn apply_mindmap_op( context: &RequestContext, root: &mut Value, op: &Value, ) -> Result<(), WebError> { let op_name = op .get("op") .or_else(|| op.get("type")) .or_else(|| op.get("action")) .and_then(Value::as_str) .map(normalize_mindmap_op_name) .ok_or_else(|| { WebError::bad_request_code("mnote_resource_op_required", "mindmap op 缺少 op") .with_context(context) })?; match op_name.as_str() { "updatetext" | "updatenode" => { let node_id = mindmap_op_string(op, &["nodeId", "node_id", "id"]).ok_or_else(|| { WebError::bad_request_code("mnote_resource_node_required", "更新节点缺少 nodeId") .with_context(context) })?; let text = mindmap_op_string(op, &["text", "title"]).ok_or_else(|| { WebError::bad_request_code("mnote_resource_text_required", "更新节点缺少 text") .with_context(context) })?; let node = find_mindmap_node_mut(root, &node_id).ok_or_else(|| { WebError::bad_request_code("mnote_resource_node_not_found", "找不到要更新的节点") .with_context(context) })?; set_mindmap_node_text(node, &text); } "insertchild" | "addchild" => { let parent_id = mindmap_op_string(op, &["parentId", "parent_id", "nodeId"]) .ok_or_else(|| { WebError::bad_request_code( "mnote_resource_parent_required", "插入子节点缺少 parentId", ) .with_context(context) })?; let parent = find_mindmap_node_mut(root, &parent_id).ok_or_else(|| { WebError::bad_request_code("mnote_resource_node_not_found", "找不到父节点") .with_context(context) })?; let child_input = op.get("node").unwrap_or(op); let child = mindmap_node_from_input(context, child_input)?; ensure_mindmap_children_array(context, parent)?.push(child); } "deletenode" => { let node_id = mindmap_op_string(op, &["nodeId", "node_id", "id"]).ok_or_else(|| { WebError::bad_request_code("mnote_resource_node_required", "删除节点缺少 nodeId") .with_context(context) })?; if mindmap_node_matches(root, &node_id) { return Err(WebError::bad_request_code( "mnote_resource_root_delete_forbidden", "不能删除 mindmap 根节点", ) .with_context(context)); } remove_mindmap_node(root, &node_id).ok_or_else(|| { WebError::bad_request_code("mnote_resource_node_not_found", "找不到要删除的节点") .with_context(context) })?; } _ => { return Err(WebError::bad_request_code( "mnote_resource_op_unsupported", format!("暂不支持 mindmap op: {op_name}"), ) .with_context(context)); } } Ok(()) } fn normalize_mindmap_op_name(value: &str) -> String { value .chars() .filter(|ch| *ch != '_' && *ch != '-' && !ch.is_whitespace()) .flat_map(char::to_lowercase) .collect() } fn mindmap_op_string(op: &Value, keys: &[&str]) -> Option { keys.iter().find_map(|key| { op.get(*key) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) }) } fn find_mindmap_node_mut<'a>(node: &'a mut Value, node_id: &str) -> Option<&'a mut Value> { if mindmap_node_matches(node, node_id) { return Some(node); } let children = node.get_mut("children").and_then(Value::as_array_mut)?; for child in children { if let Some(found) = find_mindmap_node_mut(child, node_id) { return Some(found); } } None } fn mindmap_node_matches(node: &Value, node_id: &str) -> bool { mindmap_node_id(node) .as_deref() .map(|value| value == node_id) .unwrap_or(false) } fn mindmap_node_id(node: &Value) -> Option { node.pointer("/data/uid") .or_else(|| node.pointer("/data/id")) .or_else(|| node.get("uid")) .or_else(|| node.get("id")) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) } fn set_mindmap_node_text(node: &mut Value, text: &str) { if let Some(data) = node.get_mut("data").and_then(Value::as_object_mut) { data.insert("text".into(), json!(text)); return; } if let Some(object) = node.as_object_mut() { object.insert("text".into(), json!(text)); } } fn ensure_mindmap_children_array<'a>( context: &RequestContext, node: &'a mut Value, ) -> Result<&'a mut Vec, WebError> { let object = node.as_object_mut().ok_or_else(|| { WebError::bad_request_code("mnote_resource_node_invalid", "mindmap 节点必须是对象") .with_context(context) })?; let children = object .entry("children") .or_insert_with(|| Value::Array(Vec::new())); if !children.is_array() { *children = Value::Array(Vec::new()); } Ok(children.as_array_mut().expect("children 已归一为数组")) } fn mindmap_node_from_input(context: &RequestContext, input: &Value) -> Result { if input.get("data").is_some() || input.get("children").is_some() { let mut node = input.clone(); ensure_mindmap_children_array(context, &mut node)?; return Ok(node); } let text = input .get("text") .or_else(|| input.get("title")) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .unwrap_or("未命名节点"); let uid = input .get("uid") .or_else(|| input.get("id")) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .unwrap_or(text); let mut node = json!({ "data": { "expand": true, "isActive": false, "text": text, "uid": uid }, "children": [] }); if let Some(object) = node.as_object_mut() { for key in ["metadata", "sourceRefs", "refs", "note", "hyperlink"] { if let Some(value) = input.get(key) { object.insert(key.into(), value.clone()); } } } Ok(node) } fn remove_mindmap_node(parent: &mut Value, node_id: &str) -> Option { let children = parent.get_mut("children").and_then(Value::as_array_mut)?; if let Some(index) = children .iter() .position(|child| mindmap_node_matches(child, node_id)) { return Some(children.remove(index)); } for child in children { if let Some(removed) = remove_mindmap_node(child, node_id) { return Some(removed); } } None } fn ensure_mindmap_revision_precondition( context: &RequestContext, input: &ToolCallInput, path: &Path, ) -> Result<(), WebError> { let Some(expected) = input.arg_value("expectedRevision") else { return Ok(()); }; let current = file_revision(path); if revision_label(&expected).as_deref() != revision_label(¤t).as_deref() { return Err(WebError::bad_request_code( "mnote_resource_revision_conflict", "mindmap resource revision 已变化,请重新读取后再写入", ) .with_context(context)); } Ok(()) } fn revision_label(value: &Value) -> Option { match value { Value::String(value) => Some(value.trim().to_string()).filter(|value| !value.is_empty()), Value::Number(value) => Some(value.to_string()), _ => None, } } fn collect_mindmap_nodes(value: &Value) -> Vec { let mut nodes = Vec::new(); collect_mindmap_nodes_inner(mindmap_root_value(value), &mut nodes); nodes } fn collect_mindmap_nodes_inner(value: &Value, nodes: &mut Vec) { let text = value .pointer("/data/text") .or_else(|| value.get("text")) .and_then(Value::as_str) .unwrap_or(""); let id = value .pointer("/data/uid") .or_else(|| value.pointer("/data/id")) .or_else(|| value.get("uid")) .or_else(|| value.get("id")) .and_then(Value::as_str) .unwrap_or("root"); if !text.is_empty() { nodes.push(json!({ "id": id, "text": text })); } if let Some(children) = value.get("children").and_then(Value::as_array) { for child in children { collect_mindmap_nodes_inner(child, nodes); } } } fn mindmap_markdown_summary(nodes: &[Value]) -> String { nodes .iter() .filter_map(|node| node.get("text").and_then(Value::as_str)) .take(20) .map(|text| format!("- {text}")) .collect::>() .join("\n") } fn file_revision(path: &Path) -> Value { let Ok(metadata) = fs::metadata(path) else { return Value::Null; }; let modified_ms = metadata .modified() .ok() .and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok()) .map(|duration| duration.as_millis()) .unwrap_or_default(); json!(format!("{}:{}", metadata.len(), modified_ms)) } fn office_mime_type(path: &Path) -> &'static str { match path .extension() .and_then(|value| value.to_str()) .unwrap_or("") { "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation", _ => "application/octet-stream", } } fn office_text_preview(path: &Path) -> Value { match fs::read_to_string(path) { Ok(text) => json!(text.chars().take(4000).collect::()), Err(_) => Value::Null, } }