2026-05-17 12:58:27 +08:00
|
|
|
|
use crate::acp_runtime::AcpRuntimeManager;
|
2026-05-20 10:43:38 +08:00
|
|
|
|
use crate::document_buffer_store::BufferStore;
|
2026-05-16 22:03:14 +08:00
|
|
|
|
use crate::editor_actor::EditorRuntimeActor;
|
2026-05-11 13:16:34 +08:00
|
|
|
|
use crate::local_folder_watcher_registry::LocalFolderWatcherRegistry;
|
2026-04-16 22:01:51 +08:00
|
|
|
|
use crate::middleware::request_context::inject_request_context;
|
|
|
|
|
|
use crate::routes::build_router;
|
2026-05-29 11:13:05 +08:00
|
|
|
|
use axum::extract::Request;
|
|
|
|
|
|
use axum::middleware::Next;
|
|
|
|
|
|
use axum::response::Response;
|
2026-04-16 22:01:51 +08:00
|
|
|
|
use axum::Router;
|
2026-07-03 23:20:16 +08:00
|
|
|
|
use control_plane::ControlPlaneStore;
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
use control_plane::SqliteControlPlaneStore;
|
2026-07-03 13:24:20 +08:00
|
|
|
|
#[cfg(not(test))]
|
|
|
|
|
|
use control_plane::{TursoControlPlaneConfig, TursoControlPlaneMode, TursoControlPlaneStore};
|
2026-04-16 22:01:51 +08:00
|
|
|
|
use std::env;
|
2026-04-17 23:36:24 +08:00
|
|
|
|
use std::fs;
|
2026-06-07 10:35:21 +08:00
|
|
|
|
use std::sync::Arc;
|
2026-07-03 13:24:20 +08:00
|
|
|
|
#[cfg(not(test))]
|
|
|
|
|
|
use std::time::Duration;
|
2026-04-16 22:01:51 +08:00
|
|
|
|
use tower_http::trace::TraceLayer;
|
2026-05-29 11:13:05 +08:00
|
|
|
|
use tracing::{error, warn};
|
2026-04-16 22:01:51 +08:00
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub struct AppConfig {
|
|
|
|
|
|
pub service_name: String,
|
|
|
|
|
|
pub service_version: String,
|
|
|
|
|
|
pub bind_addr: String,
|
2026-04-29 12:24:44 +08:00
|
|
|
|
pub public_bind_addr: String,
|
|
|
|
|
|
pub legacy_next_base_url: Option<String>,
|
|
|
|
|
|
pub enable_legacy_next_compat: bool,
|
2026-04-23 07:38:34 +08:00
|
|
|
|
pub enable_debug_shell_routes: bool,
|
2026-05-16 22:03:14 +08:00
|
|
|
|
pub enable_editor_actor: bool,
|
2026-04-16 22:01:51 +08:00
|
|
|
|
pub hermes_base_path: String,
|
|
|
|
|
|
pub compat_next_base_path: String,
|
|
|
|
|
|
pub convex_url: Option<String>,
|
|
|
|
|
|
pub convex_admin_key: Option<String>,
|
2026-04-17 00:25:28 +08:00
|
|
|
|
pub allow_dev_fixtures: bool,
|
2026-04-17 23:36:24 +08:00
|
|
|
|
pub query_fixtures_json: Option<String>,
|
|
|
|
|
|
pub mutation_fixtures_json: Option<String>,
|
2026-04-16 22:01:51 +08:00
|
|
|
|
pub dev_user_id: String,
|
|
|
|
|
|
pub dev_user_name: String,
|
|
|
|
|
|
pub dev_user_email: String,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl AppConfig {
|
|
|
|
|
|
pub fn from_env() -> Self {
|
|
|
|
|
|
Self {
|
2026-04-17 00:25:28 +08:00
|
|
|
|
service_name: env::var("MNOTE_WEB_SERVICE_NAME").unwrap_or_else(|_| "mnote-web".into()),
|
2026-04-16 22:01:51 +08:00
|
|
|
|
service_version: env::var("MNOTE_WEB_SERVICE_VERSION")
|
|
|
|
|
|
.unwrap_or_else(|_| env!("CARGO_PKG_VERSION").into()),
|
2026-04-29 12:24:44 +08:00
|
|
|
|
bind_addr: env::var("MNOTE_WEB_BIND")
|
|
|
|
|
|
.or_else(|_| env::var("MNOTE_WEB_PUBLIC_BIND"))
|
|
|
|
|
|
.unwrap_or_else(|_| "127.0.0.1:0".into()),
|
|
|
|
|
|
public_bind_addr: env::var("MNOTE_WEB_PUBLIC_BIND")
|
|
|
|
|
|
.unwrap_or_else(|_| "127.0.0.1:3000".into()),
|
|
|
|
|
|
legacy_next_base_url: env::var("MNOTE_WEB_LEGACY_NEXT_BASE_URL")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| value.trim().trim_end_matches('/').to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty()),
|
2026-04-30 05:46:36 +08:00
|
|
|
|
enable_legacy_next_compat: env_bool("MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT", false),
|
2026-04-23 07:38:34 +08:00
|
|
|
|
enable_debug_shell_routes: env::var("MNOTE_WEB_ENABLE_DEBUG_SHELL_ROUTES")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| matches!(value.trim(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
|
|
|
|
|
.unwrap_or(false),
|
2026-05-16 22:03:14 +08:00
|
|
|
|
enable_editor_actor: env_bool("MNOTE_WEB_ENABLE_EDITOR_ACTOR", true),
|
2026-04-16 22:01:51 +08:00
|
|
|
|
hermes_base_path: env::var("MNOTE_WEB_HERMES_BASE_PATH")
|
|
|
|
|
|
.unwrap_or_else(|_| "/api/hermes".into()),
|
|
|
|
|
|
compat_next_base_path: env::var("MNOTE_WEB_COMPAT_NEXT_BASE_PATH")
|
|
|
|
|
|
.unwrap_or_else(|_| "/api/compat/next".into()),
|
2026-05-28 22:01:44 +08:00
|
|
|
|
convex_url: None,
|
|
|
|
|
|
convex_admin_key: None,
|
2026-04-17 00:25:28 +08:00
|
|
|
|
allow_dev_fixtures: env::var("MNOTE_WEB_ALLOW_DEV_FIXTURES")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| matches!(value.trim(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
|
|
|
|
|
.unwrap_or(false),
|
2026-04-17 23:36:24 +08:00
|
|
|
|
query_fixtures_json: env::var("MNOTE_WEB_QUERY_FIXTURES_JSON")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty()),
|
|
|
|
|
|
mutation_fixtures_json: env::var("MNOTE_WEB_MUTATION_FIXTURES_JSON")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty()),
|
|
|
|
|
|
dev_user_id: env::var("DEV_USER_ID")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.or_else(|| read_env_or_dotenv("DEV_USER_ID"))
|
|
|
|
|
|
.unwrap_or_else(|| "dev-user".into()),
|
|
|
|
|
|
dev_user_name: env::var("DEV_USER_NAME")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.or_else(|| read_env_or_dotenv("DEV_USER_NAME"))
|
|
|
|
|
|
.unwrap_or_else(|| "开发用户".into()),
|
|
|
|
|
|
dev_user_email: env::var("DEV_USER_EMAIL")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.or_else(|| read_env_or_dotenv("DEV_USER_EMAIL"))
|
|
|
|
|
|
.unwrap_or_else(|| "dev@mnote.local".into()),
|
2026-04-16 22:01:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 12:24:44 +08:00
|
|
|
|
fn env_bool(key: &str, default: bool) -> bool {
|
|
|
|
|
|
env::var(key)
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| matches!(value.trim(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
|
|
|
|
|
.unwrap_or(default)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 23:36:24 +08:00
|
|
|
|
fn read_env_or_dotenv(key: &str) -> Option<String> {
|
|
|
|
|
|
if let Ok(value) = env::var(key) {
|
|
|
|
|
|
let trimmed = value.trim().trim_matches('"').to_string();
|
|
|
|
|
|
if !trimmed.is_empty() {
|
|
|
|
|
|
return Some(trimmed);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
|
|
|
|
.join("../../..")
|
|
|
|
|
|
.join(".env.all");
|
|
|
|
|
|
let content = fs::read_to_string(root).ok()?;
|
|
|
|
|
|
for line in content.lines() {
|
|
|
|
|
|
let line = line.trim_end_matches('\r');
|
|
|
|
|
|
if line.starts_with('#') || line.trim().is_empty() {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
let Some((k, v)) = line.split_once('=') else {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
};
|
|
|
|
|
|
if k.trim() != key {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
let trimmed = v.trim().trim_matches('"').to_string();
|
|
|
|
|
|
if !trimmed.is_empty() {
|
|
|
|
|
|
return Some(trimmed);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 13:24:20 +08:00
|
|
|
|
#[cfg(not(test))]
|
|
|
|
|
|
fn parse_turso_sync_interval_ms() -> Option<Duration> {
|
|
|
|
|
|
let value = env::var("MNOTE_TURSO_SYNC_INTERVAL_MS")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|v| v.trim().to_string())
|
|
|
|
|
|
.filter(|v| !v.is_empty())?;
|
|
|
|
|
|
let ms: u64 = value
|
|
|
|
|
|
.parse()
|
|
|
|
|
|
.expect("MNOTE_TURSO_SYNC_INTERVAL_MS 必须为正整数(毫秒),例如 5000");
|
|
|
|
|
|
if ms == 0 {
|
|
|
|
|
|
panic!("MNOTE_TURSO_SYNC_INTERVAL_MS 必须为正整数,当前值: {ms}");
|
|
|
|
|
|
}
|
|
|
|
|
|
Some(Duration::from_millis(ms))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 22:03:14 +08:00
|
|
|
|
use tokio::sync::broadcast;
|
|
|
|
|
|
|
2026-05-22 17:45:22 +08:00
|
|
|
|
#[derive(Clone)]
|
2026-04-16 22:01:51 +08:00
|
|
|
|
pub struct AppState {
|
|
|
|
|
|
config: Arc<AppConfig>,
|
2026-05-08 23:15:00 +08:00
|
|
|
|
local_folder_watcher_registry: LocalFolderWatcherRegistry,
|
2026-05-16 22:03:14 +08:00
|
|
|
|
pub editor_actor: EditorRuntimeActor,
|
|
|
|
|
|
pub block_delta_tx: broadcast::Sender<serde_json::Value>,
|
2026-05-17 12:58:27 +08:00
|
|
|
|
pub stream_delta_tx: broadcast::Sender<serde_json::Value>,
|
|
|
|
|
|
pub acp_runtime: Arc<AcpRuntimeManager>,
|
2026-05-20 10:43:38 +08:00
|
|
|
|
pub buffer_store: BufferStore,
|
2026-07-03 13:24:20 +08:00
|
|
|
|
control_plane: Arc<dyn ControlPlaneStore>,
|
2026-04-16 22:01:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl AppState {
|
|
|
|
|
|
pub fn new(config: AppConfig) -> Self {
|
2026-05-16 22:03:14 +08:00
|
|
|
|
let (block_delta_tx, _) = broadcast::channel(256);
|
2026-05-17 12:58:27 +08:00
|
|
|
|
let (stream_delta_tx, _) = broadcast::channel(256);
|
2026-05-16 22:03:14 +08:00
|
|
|
|
let actor = EditorRuntimeActor::new();
|
|
|
|
|
|
actor.set_block_delta_tx(block_delta_tx.clone());
|
2026-05-20 10:43:38 +08:00
|
|
|
|
let buffer_store = BufferStore::new();
|
2026-07-03 13:24:20 +08:00
|
|
|
|
let control_plane = open_control_plane_store();
|
2026-04-16 22:01:51 +08:00
|
|
|
|
Self {
|
|
|
|
|
|
config: Arc::new(config),
|
2026-06-04 18:51:16 +08:00
|
|
|
|
local_folder_watcher_registry: LocalFolderWatcherRegistry::new(
|
|
|
|
|
|
buffer_store.clone(),
|
|
|
|
|
|
control_plane.clone(),
|
|
|
|
|
|
),
|
2026-05-16 22:03:14 +08:00
|
|
|
|
editor_actor: actor,
|
|
|
|
|
|
block_delta_tx,
|
2026-05-17 12:58:27 +08:00
|
|
|
|
stream_delta_tx,
|
|
|
|
|
|
acp_runtime: Arc::new(AcpRuntimeManager::from_env()),
|
2026-05-20 10:43:38 +08:00
|
|
|
|
buffer_store,
|
2026-05-22 17:45:22 +08:00
|
|
|
|
control_plane,
|
2026-04-16 22:01:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn config(&self) -> &AppConfig {
|
|
|
|
|
|
self.config.as_ref()
|
|
|
|
|
|
}
|
2026-05-08 23:15:00 +08:00
|
|
|
|
|
|
|
|
|
|
pub fn local_folder_watcher_registry(&self) -> &LocalFolderWatcherRegistry {
|
|
|
|
|
|
&self.local_folder_watcher_registry
|
|
|
|
|
|
}
|
2026-05-22 17:45:22 +08:00
|
|
|
|
|
|
|
|
|
|
pub fn control_plane(&self) -> &dyn ControlPlaneStore {
|
|
|
|
|
|
self.control_plane.as_ref()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
2026-07-03 13:24:20 +08:00
|
|
|
|
pub(crate) fn open_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
2026-07-03 23:20:16 +08:00
|
|
|
|
Arc::new(SqliteControlPlaneStore::in_memory().expect("初始化测试 control-plane"))
|
2026-05-22 17:45:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(test))]
|
2026-07-03 13:24:20 +08:00
|
|
|
|
pub(crate) fn open_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
|
|
|
|
|
let backend = env::var("MNOTE_CONTROL_PLANE_BACKEND")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
2026-07-03 23:20:16 +08:00
|
|
|
|
.unwrap_or_else(|| "libsql-local".to_string());
|
2026-07-03 13:24:20 +08:00
|
|
|
|
match backend.as_str() {
|
|
|
|
|
|
"libsql-local" | "turso-local" | "turso" => open_turso_local_control_plane_store(),
|
|
|
|
|
|
"turso-remote" => open_turso_remote_control_plane_store(),
|
|
|
|
|
|
"turso-local-replica" | "turso-remote-replica" => {
|
|
|
|
|
|
open_turso_remote_replica_control_plane_store()
|
|
|
|
|
|
}
|
|
|
|
|
|
"turso-synced" => open_turso_synced_control_plane_store(),
|
|
|
|
|
|
other => panic!(
|
2026-07-03 23:20:16 +08:00
|
|
|
|
"不支持的控制面后端 {other},mnote-web 运行时只支持 libsql-local/turso-remote/turso-local-replica/turso-synced;SQLite 仅保留给 control-plane-admin 迁移/导出和测试"
|
2026-07-03 13:24:20 +08:00
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(test))]
|
|
|
|
|
|
fn open_turso_local_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
|
|
|
|
|
let db_path = env::var("MNOTE_TURSO_LOCAL_PATH")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.filter(|value| !value.trim().is_empty())
|
|
|
|
|
|
.unwrap_or_else(|| {
|
|
|
|
|
|
"/mnt/Data1T/Mnote_data/control-plane/control-plane-libsql.db".to_string()
|
|
|
|
|
|
});
|
|
|
|
|
|
if let Some(parent) = std::path::Path::new(&db_path).parent() {
|
|
|
|
|
|
fs::create_dir_all(parent).expect("创建 libSQL local 控制面目录");
|
|
|
|
|
|
}
|
|
|
|
|
|
Arc::new(TursoControlPlaneStore::open_local(&db_path).expect("初始化 libSQL local 控制面"))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(test))]
|
|
|
|
|
|
fn open_turso_remote_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
|
|
|
|
|
let url = env::var("MNOTE_TURSO_DATABASE_URL")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
.expect("MNOTE_CONTROL_PLANE_BACKEND=turso-remote 需要 MNOTE_TURSO_DATABASE_URL");
|
|
|
|
|
|
let token = env::var("MNOTE_TURSO_AUTH_TOKEN")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
.expect("MNOTE_CONTROL_PLANE_BACKEND=turso-remote 需要 MNOTE_TURSO_AUTH_TOKEN");
|
|
|
|
|
|
Arc::new(TursoControlPlaneStore::open_remote(url, token).expect("初始化 Turso remote 控制面"))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(test))]
|
|
|
|
|
|
fn open_turso_remote_replica_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
|
|
|
|
|
let replica_path = env::var("MNOTE_TURSO_LOCAL_REPLICA_PATH")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
.unwrap_or_else(|| {
|
|
|
|
|
|
"/mnt/Data1T/Mnote_data/control-plane/control-plane-replica.db".to_string()
|
|
|
|
|
|
});
|
|
|
|
|
|
if let Some(parent) = std::path::Path::new(&replica_path).parent() {
|
|
|
|
|
|
fs::create_dir_all(parent).expect("创建 Turso local replica 控制面目录");
|
|
|
|
|
|
}
|
|
|
|
|
|
let url = env::var("MNOTE_TURSO_DATABASE_URL")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
.expect("MNOTE_CONTROL_PLANE_BACKEND=turso-local-replica 需要 MNOTE_TURSO_DATABASE_URL");
|
|
|
|
|
|
let token = env::var("MNOTE_TURSO_AUTH_TOKEN")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
.expect("MNOTE_CONTROL_PLANE_BACKEND=turso-local-replica 需要 MNOTE_TURSO_AUTH_TOKEN");
|
|
|
|
|
|
Arc::new(
|
|
|
|
|
|
TursoControlPlaneStore::open_remote_replica(
|
|
|
|
|
|
replica_path,
|
|
|
|
|
|
url,
|
|
|
|
|
|
token,
|
|
|
|
|
|
parse_turso_sync_interval_ms(),
|
|
|
|
|
|
)
|
|
|
|
|
|
.expect("初始化 Turso local replica 控制面"),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(test))]
|
|
|
|
|
|
fn open_turso_synced_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
|
|
|
|
|
let local_path = env::var("MNOTE_TURSO_SYNCED_PATH")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
|
env::var("MNOTE_TURSO_LOCAL_REPLICA_PATH")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or_else(|| {
|
|
|
|
|
|
"/mnt/Data1T/Mnote_data/control-plane/control-plane-synced.db".to_string()
|
|
|
|
|
|
});
|
|
|
|
|
|
if let Some(parent) = std::path::Path::new(&local_path).parent() {
|
|
|
|
|
|
fs::create_dir_all(parent).expect("创建 Turso synced 控制面目录");
|
|
|
|
|
|
}
|
|
|
|
|
|
let url = env::var("MNOTE_TURSO_DATABASE_URL")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
.expect("MNOTE_CONTROL_PLANE_BACKEND=turso-synced 需要 MNOTE_TURSO_DATABASE_URL");
|
|
|
|
|
|
let token = env::var("MNOTE_TURSO_AUTH_TOKEN")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
.expect("MNOTE_CONTROL_PLANE_BACKEND=turso-synced 需要 MNOTE_TURSO_AUTH_TOKEN");
|
|
|
|
|
|
Arc::new(
|
|
|
|
|
|
TursoControlPlaneStore::open_with_config(TursoControlPlaneConfig {
|
|
|
|
|
|
mode: TursoControlPlaneMode::Synced,
|
|
|
|
|
|
path: Some(local_path.into()),
|
|
|
|
|
|
remote_url: Some(url),
|
|
|
|
|
|
auth_token: Some(token),
|
|
|
|
|
|
sync_interval: parse_turso_sync_interval_ms(),
|
|
|
|
|
|
})
|
|
|
|
|
|
.expect("初始化 Turso synced 控制面"),
|
|
|
|
|
|
)
|
2026-04-16 22:01:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn build_app(state: AppState) -> Router {
|
|
|
|
|
|
build_router(state)
|
2026-05-29 11:13:05 +08:00
|
|
|
|
.layer(TraceLayer::new_for_http().on_failure(()))
|
|
|
|
|
|
.layer(axum::middleware::from_fn(log_failed_response))
|
2026-04-16 22:01:51 +08:00
|
|
|
|
.layer(axum::middleware::from_fn(inject_request_context))
|
|
|
|
|
|
}
|
2026-05-22 17:45:22 +08:00
|
|
|
|
|
2026-05-29 11:13:05 +08:00
|
|
|
|
async fn log_failed_response(request: Request, next: Next) -> Response {
|
|
|
|
|
|
let method = request.method().clone();
|
|
|
|
|
|
let uri = request.uri().clone();
|
|
|
|
|
|
let response = next.run(request).await;
|
|
|
|
|
|
let status = response.status();
|
|
|
|
|
|
if status.is_server_error() {
|
|
|
|
|
|
let error_code = response
|
|
|
|
|
|
.headers()
|
|
|
|
|
|
.get("x-error-code")
|
|
|
|
|
|
.and_then(|value| value.to_str().ok())
|
|
|
|
|
|
.unwrap_or("");
|
|
|
|
|
|
if error_code == "convex_retired" {
|
|
|
|
|
|
warn!(
|
|
|
|
|
|
method = %method,
|
|
|
|
|
|
uri = %uri,
|
|
|
|
|
|
status = %status,
|
|
|
|
|
|
error_code = %error_code,
|
2026-06-09 22:12:09 +08:00
|
|
|
|
"退役 legacy cloud/Convex 兼容路径被请求"
|
2026-05-29 11:13:05 +08:00
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
error!(
|
|
|
|
|
|
method = %method,
|
|
|
|
|
|
uri = %uri,
|
|
|
|
|
|
status = %status,
|
|
|
|
|
|
error_code = %error_code,
|
|
|
|
|
|
"mnote-web 请求返回服务端错误"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
response
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 17:45:22 +08:00
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod tests {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
use control_plane::UpsertUserInput;
|
|
|
|
|
|
|
|
|
|
|
|
fn test_config() -> AppConfig {
|
|
|
|
|
|
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: None,
|
|
|
|
|
|
enable_legacy_next_compat: false,
|
|
|
|
|
|
enable_debug_shell_routes: false,
|
|
|
|
|
|
enable_editor_actor: true,
|
|
|
|
|
|
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(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-07-03 23:20:16 +08:00
|
|
|
|
fn app_state_initializes_control_plane_store_for_tests() {
|
2026-05-22 17:45:22 +08:00
|
|
|
|
let state = AppState::new(test_config());
|
|
|
|
|
|
|
|
|
|
|
|
state
|
|
|
|
|
|
.control_plane()
|
|
|
|
|
|
.upsert_user(UpsertUserInput {
|
|
|
|
|
|
id: Some("shujuan".into()),
|
|
|
|
|
|
email: Some("shujuan@163.com".into()),
|
|
|
|
|
|
username: "shujuan".into(),
|
|
|
|
|
|
display_name: "shujuan".into(),
|
|
|
|
|
|
role: None,
|
|
|
|
|
|
password_hash: None,
|
|
|
|
|
|
})
|
|
|
|
|
|
.expect("upsert user");
|
|
|
|
|
|
|
|
|
|
|
|
let workspace = state
|
|
|
|
|
|
.control_plane()
|
|
|
|
|
|
.ensure_default_workspace("shujuan")
|
|
|
|
|
|
.expect("default workspace");
|
|
|
|
|
|
assert_eq!(workspace.name, "shujuan 的空间");
|
|
|
|
|
|
|
|
|
|
|
|
let access = state
|
|
|
|
|
|
.control_plane()
|
|
|
|
|
|
.resolve_access("shujuan", &workspace.root_uri)
|
|
|
|
|
|
.expect("owner access");
|
|
|
|
|
|
assert_eq!(access.permission, "write");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|