feat: consolidate local-first mnote web runtime

This commit is contained in:
lix-2026
2026-05-28 22:01:44 +08:00
parent 7354807ee9
commit 39b9a0183a
154 changed files with 13591 additions and 12728 deletions
+117 -77
View File
@@ -2,11 +2,11 @@ use super::hermes_client;
use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::hermes_tools::{artifact, block, doc, manifest, page, resource, ToolCallInput};
use crate::hermes_tools::{ToolCallInput, artifact, block, doc, manifest, page, resource};
use axum::Json;
use axum::extract::{Extension, Query, State};
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
use axum::Json;
use serde_json::{json, Value};
use serde_json::{Value, json};
use std::collections::HashMap;
use std::env;
use std::fs::{self, File, OpenOptions};
@@ -827,10 +827,10 @@ fn stamp_tool_headers() -> HeaderMap {
#[cfg(test)]
mod tests {
use crate::app::{build_app, AppConfig, AppState};
use axum::body::{to_bytes, Body};
use crate::app::{AppConfig, AppState, build_app};
use axum::body::{Body, to_bytes};
use axum::http::{Request, StatusCode};
use serde_json::{json, Value};
use serde_json::{Value, json};
use std::collections::BTreeMap;
use std::fs;
use std::sync::Mutex;
@@ -1112,36 +1112,56 @@ mod tests {
assert!(tools.iter().any(|tool| tool["name"] == "mnote.doc.fetch"));
assert!(tools.iter().any(|tool| tool["name"] == "mnote.doc.find"));
assert!(tools.iter().any(|tool| tool["name"] == "mnote.block.fetch"));
assert!(tools
.iter()
.any(|tool| tool["name"] == "mnote.doc.plan_update"));
assert!(tools
.iter()
.any(|tool| tool["name"] == "mnote.block.replace"));
assert!(tools
.iter()
.any(|tool| tool["name"] == "mnote.block.insert_after"));
assert!(tools
.iter()
.any(|tool| tool["name"] == "mnote.block.delete"));
assert!(tools
.iter()
.any(|tool| tool["name"] == "mnote.block.move_after"));
assert!(tools
.iter()
.any(|tool| tool["name"] == "mnote.doc.apply_block_ops"));
assert!(tools
.iter()
.any(|tool| tool["name"] == "mnote.mindmap.fetch"));
assert!(tools
.iter()
.any(|tool| tool["name"] == "mnote.mindmap.apply_ops"));
assert!(tools
.iter()
.any(|tool| tool["name"] == "mnote.office.fetch_summary"));
assert!(tools
.iter()
.any(|tool| tool["name"] == "mnote.office.propose_changes"));
assert!(
tools
.iter()
.any(|tool| tool["name"] == "mnote.doc.plan_update")
);
assert!(
tools
.iter()
.any(|tool| tool["name"] == "mnote.block.replace")
);
assert!(
tools
.iter()
.any(|tool| tool["name"] == "mnote.block.insert_after")
);
assert!(
tools
.iter()
.any(|tool| tool["name"] == "mnote.block.delete")
);
assert!(
tools
.iter()
.any(|tool| tool["name"] == "mnote.block.move_after")
);
assert!(
tools
.iter()
.any(|tool| tool["name"] == "mnote.doc.apply_block_ops")
);
assert!(
tools
.iter()
.any(|tool| tool["name"] == "mnote.mindmap.fetch")
);
assert!(
tools
.iter()
.any(|tool| tool["name"] == "mnote.mindmap.apply_ops")
);
assert!(
tools
.iter()
.any(|tool| tool["name"] == "mnote.office.fetch_summary")
);
assert!(
tools
.iter()
.any(|tool| tool["name"] == "mnote.office.propose_changes")
);
let page_save = tools
.iter()
.find(|tool| tool["name"] == "mnote.page.save")
@@ -1189,16 +1209,20 @@ mod tests {
}
assert!(markdown_edit["inputSchema"]["properties"]["operations"].is_object());
assert!(markdown_edit["inputSchema"]["properties"]["full_content"].is_object());
assert!(markdown_edit["inputSchema"]["anyOf"]
.as_array()
.expect("anyOf")
.iter()
.any(|rule| rule["required"] == json!(["operations"])));
assert!(markdown_edit["inputSchema"]["anyOf"]
.as_array()
.expect("anyOf")
.iter()
.any(|rule| rule["required"] == json!(["full_content"])));
assert!(
markdown_edit["inputSchema"]["anyOf"]
.as_array()
.expect("anyOf")
.iter()
.any(|rule| rule["required"] == json!(["operations"]))
);
assert!(
markdown_edit["inputSchema"]["anyOf"]
.as_array()
.expect("anyOf")
.iter()
.any(|rule| rule["required"] == json!(["full_content"]))
);
}
#[tokio::test]
@@ -1228,14 +1252,18 @@ mod tests {
.find(|tool| tool["name"] == "mnote.page.save")
.expect("page save tool");
assert!(markdown_edit["description"]
.as_str()
.expect("description")
.contains("兼容"));
assert!(markdown_edit["description"]
.as_str()
.expect("description")
.contains("agent 原生 patch/diff"));
assert!(
markdown_edit["description"]
.as_str()
.expect("description")
.contains("兼容")
);
assert!(
markdown_edit["description"]
.as_str()
.expect("description")
.contains("agent 原生 patch/diff")
);
assert_eq!(
page_save["annotations"]["requiresWritePermission"],
Value::Bool(true)
@@ -1426,10 +1454,12 @@ mod tests {
payload["result"]["objectIdentity"],
"resource:mindmap:local-md:README.md:mind_allowed"
);
assert!(payload["result"]["markdownSummary"]
.as_str()
.unwrap_or_default()
.contains("中心主题"));
assert!(
payload["result"]["markdownSummary"]
.as_str()
.unwrap_or_default()
.contains("中心主题")
);
assert_eq!(payload["result"]["nodes"][1]["text"], "分支一");
let _ = fs::remove_dir_all(&root);
}
@@ -1765,10 +1795,12 @@ mod tests {
assert_eq!(payload["toolName"], "mnote.page.get");
assert_eq!(payload["toolCallId"], "call_1");
assert_eq!(payload["result"]["title"], "服务端页面");
assert!(payload["result"]["bodySummary"]
.as_str()
.unwrap_or_default()
.contains("章节一"));
assert!(
payload["result"]["bodySummary"]
.as_str()
.unwrap_or_default()
.contains("章节一")
);
assert_eq!(payload["audit"]["effect"], "read");
}
@@ -1814,10 +1846,12 @@ mod tests {
json!("heading_1")
);
assert_eq!(payload["result"]["blocks"][0]["text"], json!("章节一"));
assert!(payload["result"]["blocks"][0]["revisionRef"]
.as_str()
.unwrap_or_default()
.starts_with("pageRev:7:block:heading_1:hash:"));
assert!(
payload["result"]["blocks"][0]["revisionRef"]
.as_str()
.unwrap_or_default()
.starts_with("pageRev:7:block:heading_1:hash:")
);
}
#[tokio::test]
@@ -1944,10 +1978,12 @@ mod tests {
assert_eq!(payload["result"]["scope"], "selection");
assert_eq!(payload["result"]["format"], "page_xml");
assert_eq!(payload["result"]["blocks"].as_array().unwrap().len(), 1);
assert!(payload["result"]["content"]
.as_str()
.unwrap_or_default()
.contains("<block id=\"heading_1\""));
assert!(
payload["result"]["content"]
.as_str()
.unwrap_or_default()
.contains("<block id=\"heading_1\"")
);
assert_eq!(payload["result"]["allowedTargetBlockIds"][0], "heading_1");
}
@@ -2210,10 +2246,12 @@ mod tests {
insert_payload["result"]["changedBlocks"][0]["op"],
json!("insert_after")
);
assert!(insert_payload["result"]["changedBlocks"][0]["blockId"]
.as_str()
.unwrap_or_default()
.starts_with("ai_block_"));
assert!(
insert_payload["result"]["changedBlocks"][0]["blockId"]
.as_str()
.unwrap_or_default()
.starts_with("ai_block_")
);
let delete_response = app()
.oneshot(
@@ -2565,9 +2603,11 @@ mod tests {
.as_array()
.expect("insertedBlockIds");
assert_eq!(inserted_ids.len(), 2);
assert!(inserted_ids
.iter()
.all(|id| { id.as_str().unwrap_or_default().starts_with("ai_block_") }));
assert!(
inserted_ids
.iter()
.all(|id| { id.as_str().unwrap_or_default().starts_with("ai_block_") })
);
assert_eq!(
payload["result"]["changedBlocks"]
.as_array()