Files
mnote/rust/crates/mnote-web/src/routes/mod.rs
T

579 lines
21 KiB
Rust
Raw Normal View History

mod bridge;
2026-05-14 15:10:33 +08:00
pub(crate) mod command_support;
mod compat;
mod dev_hot;
mod documents;
mod editor;
2026-04-29 12:24:44 +08:00
mod gateway;
mod health;
mod hermes;
2026-05-14 15:10:33 +08:00
mod hermes_client;
mod hermes_tools;
mod kernel;
2026-05-08 11:23:08 +08:00
mod local_folder_events;
2026-05-11 13:16:34 +08:00
mod local_folder_source;
mod local_markdown_parser;
mod local_search_index;
mod media;
mod mindmap_api;
2026-04-29 12:24:44 +08:00
mod mindmap_shell;
mod onlyoffice;
mod page_ai_workflow;
mod query_support;
2026-05-15 23:20:25 +08:00
mod resource_trash;
2026-04-29 12:24:44 +08:00
mod search;
mod session;
mod snapshot_support;
mod sse;
mod stream_support;
mod tree;
2026-05-14 15:10:33 +08:00
pub(crate) mod web_shell;
mod ws;
pub(crate) use local_folder_source::{
ensure_local_path_read_access, ensure_local_workspace_access, local_workspace_id_from_root_uri,
update_local_markdown_title, update_local_page_options, write_local_markdown_page_body,
};
pub(crate) use local_search_index::refresh_local_search_index_for_path;
use crate::app::AppState;
2026-05-15 23:20:25 +08:00
use axum::routing::{any, delete, get, post, put};
2026-04-29 12:24:44 +08:00
use axum::Router;
pub fn build_router(state: AppState) -> Router {
let hermes_base_path = state.config().hermes_base_path.clone();
let enable_debug_shell_routes = state.config().enable_debug_shell_routes;
let mut router = Router::new()
.route("/health", get(health::health))
2026-04-29 12:24:44 +08:00
.route("/", get(gateway::root_entry))
2026-05-15 23:20:25 +08:00
.route("/trash", get(gateway::trash_entry))
2026-05-06 21:44:20 +08:00
.route("/favicon.ico", get(gateway::favicon))
.route(
"/admin/access-policy",
get(gateway::admin_access_policy_entry),
)
.route(
"/user/access-policy",
get(gateway::user_access_policy_entry),
)
2026-04-29 12:24:44 +08:00
.route("/auth", get(gateway::auth_entry).post(gateway::auth_entry))
.route("/search", get(search::shell))
.route(
"/mindmap/{doc_id}/{mindmap_id}",
get(mindmap_shell::mindmap_object_shell),
)
.route(
"/api/mindmap/{doc_id}/{mindmap_id}",
2026-05-15 23:20:25 +08:00
get(mindmap_api::get_mindmap)
.post(mindmap_api::apply_mindmap_command)
.delete(resource_trash::mindmap_delete)
.patch(resource_trash::mindmap_trash_action),
)
.route(
"/api/mindmap-trash/empty",
post(resource_trash::mindmap_empty_trash),
)
2026-04-29 12:24:44 +08:00
.route(
"/documents/{document_id}",
get(web_shell::document_page_shell),
)
.route(
"/api/page-aggregate/{document_id}",
get(web_shell::page_aggregate),
)
2026-04-29 14:36:24 +08:00
.route(
"/api/leptos-tiptap-runtime/manifest.json",
get(web_shell::leptos_tiptap_manifest),
)
.route(
"/api/leptos-tiptap-runtime/{*asset_path}",
get(web_shell::leptos_tiptap_asset),
)
2026-05-02 06:25:26 +08:00
.route(
"/api/editor/image-placeholder.svg",
get(web_shell::editor_image_placeholder_asset),
)
2026-05-24 21:03:33 +08:00
.route(
"/api/mnote-browser-runtime/resource-open-runtime.js",
get(web_shell::resource_open_runtime_asset),
)
2026-05-24 22:42:53 +08:00
.route(
"/api/mnote-browser-runtime/local-upload-runtime.js",
get(web_shell::local_upload_runtime_asset),
)
.route(
"/api/mnote-browser-runtime/filetree-runtime.js",
get(web_shell::filetree_runtime_asset),
)
.route(
"/api/mnote-browser-runtime/filetree-selection-runtime.js",
get(web_shell::filetree_selection_runtime_asset),
)
.route(
"/api/mnote-browser-runtime/filetree-context-menu-runtime.js",
get(web_shell::filetree_context_menu_runtime_asset),
)
2026-05-24 21:35:21 +08:00
.route(
"/api/mnote-browser-runtime/tree-live-controller.js",
get(web_shell::tree_live_controller_runtime_asset),
)
2026-05-25 00:03:12 +08:00
.route(
"/api/mnote-browser-runtime/tree-shell-runtime.js",
get(web_shell::tree_shell_runtime_asset),
)
.route(
"/api/mnote-browser-runtime/document-conflict-panel-runtime.js",
get(web_shell::document_conflict_panel_runtime_asset),
)
2026-04-29 12:24:44 +08:00
.route("/api/search/documents", post(search::documents))
.route(
"/api/search/local-index/refresh",
post(search::refresh_local_index),
)
.route(
"/api/search/local-index/backlinks",
get(search::local_index_backlinks),
)
.route(
"/api/search/local-index/tags",
get(search::local_index_tags),
)
2026-04-29 12:24:44 +08:00
.route("/api/gateway/health", get(gateway::gateway_health))
.route("/api/dev/hot-reload", get(dev_hot::hot_reload))
2026-04-29 12:24:44 +08:00
.route("/api/runtime/config", get(session::runtime_config))
2026-05-06 21:44:20 +08:00
.route("/api/auth", post(gateway::auth_api))
2026-04-29 12:24:44 +08:00
.route("/api/auth/session", get(session::session))
2026-05-06 21:44:20 +08:00
.route("/api/auth/whoami", get(session::session))
.route("/api/auth/mnote-web-token", get(session::session))
2026-04-29 12:24:44 +08:00
.route("/api/auth/session/refresh", post(session::refresh_session))
.route(
"/api/admin/access-policy",
get(local_folder_source::get_local_access_policy),
)
.route(
"/api/admin/access-policy/validate-root",
post(local_folder_source::validate_local_access_root),
)
.route(
"/api/admin/access-policy/grants",
post(local_folder_source::create_local_access_grant),
)
.route(
"/api/admin/access-policy/grants/{grant_id}",
delete(local_folder_source::delete_local_access_grant),
)
.route(
"/api/user/access-policy",
get(local_folder_source::get_user_access_policy),
)
.route(
"/api/user/access-policy/grants",
post(local_folder_source::create_user_access_grant),
)
.route(
"/api/user/access-policy/grants/{grant_id}",
delete(local_folder_source::delete_user_access_grant),
)
.route(
"/api/admin/share-links",
get(local_folder_source::get_share_links).post(local_folder_source::create_share_link),
)
.route(
"/api/admin/share-links/{link_id}",
delete(local_folder_source::delete_share_link),
)
2026-05-19 11:11:08 +08:00
.route(
"/api/admin/share-grants",
get(local_folder_source::get_share_grants)
.post(local_folder_source::create_share_grant),
)
.route(
"/api/admin/share-grants/{share_id}",
delete(local_folder_source::delete_share_grant),
)
.route(
"/api/user/share-grants",
get(local_folder_source::get_user_share_grants)
.post(local_folder_source::create_user_share_grant),
)
.route(
"/api/user/share-grants/{share_id}",
delete(local_folder_source::delete_user_share_grant),
)
2026-05-19 11:11:08 +08:00
.route(
"/api/local-folder/shared-cache/record",
post(local_folder_source::record_shared_cache),
)
.route(
"/api/local-folder/sync/pending-change",
post(local_folder_source::record_sync_pending_change),
)
.route(
"/api/local-folder/sync/conflict-report",
post(local_folder_source::write_sync_conflict_report),
)
2026-05-06 21:44:20 +08:00
.route("/api/ai-agent/run", post(compat::next_ai_agent_run))
.route(
"/api/page-ai/block-edit-workflow",
post(page_ai_workflow::block_edit_workflow),
)
.route("/onlyoffice", get(onlyoffice::page))
2026-05-19 11:39:04 +08:00
.route(
"/office/{document_id}/{asset_id}",
get(onlyoffice::object_shell),
)
.route("/onlyoffice-server/{*path}", any(onlyoffice::server_proxy))
.route("/cache/{*path}", any(onlyoffice::cache_proxy))
.route("/api/onlyoffice/sign", post(onlyoffice::sign))
.route("/api/onlyoffice/proxy", get(onlyoffice::proxy))
.route("/api/onlyoffice/callback", post(onlyoffice::callback))
.route("/api/onlyoffice/forcesave", post(onlyoffice::forcesave))
.route("/api/media/upload", post(media::upload))
.route("/api/media/sign", get(media::sign))
2026-05-15 23:20:25 +08:00
.route("/api/media/batch", post(resource_trash::media_batch))
.route("/api/media/purge", post(resource_trash::media_purge))
.route(
"/api/media/empty-trash",
post(resource_trash::media_empty_trash),
)
.route("/api/tables/restore", post(resource_trash::table_restore))
.route("/api/tables/create", post(resource_trash::table_create))
.route(
"/api/tables/{table_id}",
delete(resource_trash::table_delete),
)
.route("/api/tables/purge", post(resource_trash::table_purge))
.route(
"/api/tables/empty-trash",
post(resource_trash::table_empty_trash),
)
.route(
"/api/tree/filetree/upload-target-preflight",
post(media::filetree_upload_target_preflight),
)
2026-05-15 23:20:25 +08:00
.route(
"/api/tree/filetree/drop-preflight",
post(tree::filetree_drop_preflight),
)
.route("/api/documents/meta", get(documents::meta))
.route("/api/documents/content", get(documents::content))
.route("/api/documents/buffer-state", get(documents::buffer_state))
2026-05-06 21:44:20 +08:00
.route("/api/documents/purge", post(documents::purge))
2026-05-15 23:20:25 +08:00
.route("/api/documents/empty-trash", post(documents::empty_trash))
.route("/api/documents/title", post(documents::title))
.route("/api/documents/options", post(documents::options))
.route("/api/documents/save", post(documents::save))
.route("/api/page-body/write", post(documents::page_body_write))
.route(
"/api/documents/runtime/transform",
post(editor::transform_runtime_snapshot),
)
.route(
"/api/tree/projections/sidebar",
get(kernel::project_tree_sidebar),
)
.route("/api/tree/projections/page", get(kernel::project_tree_page))
.route("/api/tree/projections/file", get(kernel::project_tree_file))
.route(
"/api/kernel/projections/sidebar",
get(kernel::project_sidebar),
)
.route("/api/kernel/subtree", get(kernel::subtree))
.route("/api/kernel/edges", get(kernel::edges))
.route("/api/kernel/graph", get(kernel::graph))
.route("/api/tree/commands", post(tree::tree_command))
2026-05-08 00:41:03 +08:00
.route(
"/api/tree/local-folder-watch",
get(tree::local_folder_watch),
)
2026-05-08 11:23:08 +08:00
.route(
"/api/local-folder/events",
get(local_folder_events::local_folder_events),
)
.route(
"/api/local-folder/workspaces/default",
post(local_folder_source::create_default_local_workspace),
)
.route(
"/api/local-folder/assets/upload",
post(local_folder_source::upload_local_markdown_asset),
)
.route(
"/api/local-folder/files/open",
get(local_folder_source::open_local_file),
)
.route(
"/api/local-folder/files/stat",
get(local_folder_source::stat_local_file),
)
2026-05-20 14:20:48 +08:00
.route(
"/api/local-folder/resource/read",
get(local_folder_source::read_local_resource),
)
.route(
"/api/local-folder/resource/write",
post(local_folder_source::write_local_resource),
)
2026-04-28 16:30:51 +08:00
.route(
"/api/tree/runtime/reduce",
post(tree::reduce_tree_shell_runtime),
)
.route("/api/bridge/workspace", get(bridge::workspace))
.route("/api/bridge/request", get(bridge::request))
.route("/api/bridge/trace", get(bridge::trace))
2026-04-29 12:24:44 +08:00
.route("/api/tree/events", get(sse::tree_events))
.route("/api/stream/events", get(sse::events))
.route("/api/realtime/ws", get(ws::socket))
.nest(
&hermes_base_path,
Router::new()
.route("/health", get(hermes::health))
2026-05-14 15:10:33 +08:00
.route("/bridge", post(hermes::bridge_runtime))
.route(
"/client/sessions",
get(hermes_client::list_sessions).post(hermes_client::create_session),
)
.route(
"/client/sessions/search",
get(hermes_client::search_sessions),
)
2026-05-14 15:10:33 +08:00
.route(
"/client/sessions/{session_id}",
get(hermes_client::get_session).delete(hermes_client::delete_session),
2026-05-14 15:10:33 +08:00
)
2026-05-15 23:20:25 +08:00
.route(
"/client/sessions/{session_id}/resume",
post(hermes_client::resume_session),
)
.route(
"/client/sessions/{session_id}/rename",
post(hermes_client::rename_session),
)
.route(
"/client/sessions/{session_id}/auto-title",
post(hermes_client::auto_title_session),
)
2026-05-15 23:20:25 +08:00
.route("/client/gateway/health", get(hermes_client::gateway_health))
2026-05-14 17:04:17 +08:00
.route("/client/profiles", get(hermes_client::list_profiles))
.route(
"/client/profiles/active",
put(hermes_client::switch_active_profile),
)
.route(
"/client/profiles/{profile_name}",
get(hermes_client::get_profile),
)
.route(
"/client/profile-memory",
2026-05-15 23:20:25 +08:00
get(hermes_client::get_profile_memory).post(hermes_client::save_profile_memory),
2026-05-14 17:04:17 +08:00
)
2026-05-15 23:20:25 +08:00
.route("/client/skills", get(hermes_client::list_skills))
.route("/client/skills/toggle", put(hermes_client::toggle_skill))
.route("/client/tools/toggle", put(hermes_client::toggle_tool))
2026-05-14 15:10:33 +08:00
.route("/client/runs", post(hermes_client::create_run))
2026-05-15 23:20:25 +08:00
.route(
"/client/sessions/{session_id}/queue/{queue_id}",
delete(hermes_client::cancel_queued_run),
)
2026-05-14 15:10:33 +08:00
.route("/client/events/{run_id}", get(hermes_client::stream_events))
.route(
"/client/runs/{run_id}/abort",
post(hermes_client::abort_run),
)
2026-05-21 05:40:06 +08:00
.route(
"/client/runs/{run_id}/resolve-permission",
post(hermes_client::resolve_permission),
)
2026-05-14 15:10:33 +08:00
.route("/client/models", get(hermes_client::list_models))
.route("/client/tools", get(hermes_client::list_tools)),
)
.nest(
"/api/hermes/tools",
Router::new()
.route("/mnote/manifest", get(hermes_tools::mnote_manifest))
.route("/mnote/call", post(hermes_tools::mnote_call))
.route("/mnote/audit", get(hermes_tools::mnote_audit)),
2026-05-11 13:16:34 +08:00
);
if enable_debug_shell_routes {
router = router
.route("/tree", get(tree::tree_shell))
2026-05-11 13:16:34 +08:00
.route("/document-debug", get(editor::document_editor_shell));
}
router.with_state(state)
}
2026-05-11 13:16:34 +08:00
#[cfg(test)]
mod tests {
use super::build_router;
use crate::app::{AppConfig, AppState};
use axum::body::Body;
use axum::http::{Request, StatusCode};
use axum::Router;
use tower::ServiceExt;
fn app(enable_debug_shell_routes: bool) -> Router {
build_router(AppState::new(AppConfig {
service_name: "mnote-web".into(),
service_version: "0.1.0".into(),
bind_addr: "127.0.0.1:0".into(),
public_bind_addr: "127.0.0.1:3000".into(),
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
enable_legacy_next_compat: false,
enable_debug_shell_routes,
enable_editor_actor: true,
2026-05-11 13:16:34 +08:00
hermes_base_path: "/api/hermes".into(),
compat_next_base_path: "/api/compat/next".into(),
convex_url: None,
convex_admin_key: None,
allow_dev_fixtures: false,
query_fixtures_json: None,
mutation_fixtures_json: None,
dev_user_id: "dev-user".into(),
dev_user_name: "开发用户".into(),
dev_user_email: "dev@mnote.local".into(),
}))
}
#[tokio::test]
async fn debug_shell_routes_are_not_mounted_by_default() {
for path in ["/tree", "/document-debug"] {
let response = app(false)
.oneshot(
Request::builder()
.uri(path)
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(
response.status(),
StatusCode::NOT_FOUND,
"{path} 默认不应暴露"
);
}
}
#[tokio::test]
async fn debug_shell_routes_are_only_available_when_explicitly_enabled() {
let tree_response = app(true)
.oneshot(
Request::builder()
.uri("/tree")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_ne!(
tree_response.status(),
StatusCode::NOT_FOUND,
"/tree 显式启用 debug gate 后不应继续返回 404",
);
let document_debug_response = app(true)
.oneshot(
Request::builder()
.uri("/document-debug")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_ne!(
document_debug_response.status(),
StatusCode::NOT_FOUND,
"/document-debug 显式启用 debug gate 后不应继续返回 404",
);
}
#[tokio::test]
async fn onlyoffice_and_media_transport_routes_are_explicitly_mounted() {
for (method, path) in [
("GET", "/onlyoffice"),
("POST", "/api/onlyoffice/sign"),
("GET", "/api/onlyoffice/proxy?u=x"),
("POST", "/api/onlyoffice/callback"),
("POST", "/api/onlyoffice/forcesave"),
("POST", "/api/media/upload"),
("GET", "/api/media/sign?assetId=x"),
("POST", "/api/tree/filetree/upload-target-preflight"),
(
"GET",
"/onlyoffice-server/web-apps/apps/api/documents/api.js",
),
] {
let response = app(false)
.oneshot(
Request::builder()
.method(method)
.uri(path)
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_ne!(
response.status(),
StatusCode::NOT_FOUND,
"{method} {path} 应显式挂到 Rust 3000 单入口边界",
);
}
}
#[tokio::test]
async fn mnote_browser_runtime_assets_are_explicitly_mounted() {
for path in [
"/api/mnote-browser-runtime/resource-open-runtime.js",
"/api/mnote-browser-runtime/local-upload-runtime.js",
"/api/mnote-browser-runtime/filetree-runtime.js",
"/api/mnote-browser-runtime/filetree-selection-runtime.js",
"/api/mnote-browser-runtime/filetree-context-menu-runtime.js",
"/api/mnote-browser-runtime/tree-live-controller.js",
"/api/mnote-browser-runtime/tree-shell-runtime.js",
"/api/mnote-browser-runtime/document-conflict-panel-runtime.js",
] {
let response = app(false)
.oneshot(
Request::builder()
.uri(path)
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(
response.status(),
StatusCode::OK,
"{path} 应返回 runtime asset"
);
let content_type = response
.headers()
.get(axum::http::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap_or("");
assert!(
content_type.starts_with("application/javascript"),
"{path} 应声明 JavaScript content-type"
);
}
}
#[tokio::test]
async fn dev_hot_reload_endpoint_is_mounted() {
let response = app(false)
.oneshot(
Request::builder()
.uri("/api/dev/hot-reload")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
}
2026-05-11 13:16:34 +08:00
}