//! 应用配置:服务端 `desktop_configs` + SQLite 本地配置,同名键**本地优先**。 //! 合并结果供 `get_app_config` 与 Node 环境变量 `AICLIENT_CFG_*`。 use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use serde_json::Value; use thiserror::Error; use tokio::sync::RwLock; use crate::local_config::LocalConfigDb; /// Node 子进程环境变量前缀,与 `scripts/nodejs/desktop_config.js` 一致。 pub const NODE_ENV_PREFIX: &str = "AICLIENT_CFG_"; #[derive(Debug, Error)] pub enum AppConfigError { #[error("拉取配置失败: {0}")] Fetch(String), } #[derive(Debug, Default)] struct Inner { server_entries: HashMap, local_entries: HashMap, last_message: String, local_db: Option>, } #[derive(Debug, Default)] pub struct AppConfig { inner: Arc>, } fn is_dev() -> bool { cfg!(debug_assertions) } pub fn api_base() -> String { std::env::var("AICLIENT_API_BASE").unwrap_or_else(|_| { if is_dev() { "http://127.0.0.1:8001".to_string() } else { "http://81.71.163.140:8001".to_string() } }) } /// 将库表 `name` 转为 Node 环境变量名(不含前缀),如 `llm.api_key` → `LLM_API_KEY`。 pub fn normalize_config_key(name: &str) -> String { let mut out = String::new(); let mut prev_underscore = false; for c in name.trim().chars() { if c.is_ascii_alphanumeric() { out.push(c.to_ascii_uppercase()); prev_underscore = false; } else if !prev_underscore { out.push('_'); prev_underscore = true; } } while out.starts_with('_') { out.remove(0); } while out.ends_with('_') { out.pop(); } out } /// 完整环境变量名:`AICLIENT_CFG_LLM_API_KEY` pub fn node_env_var_name(db_name: &str) -> String { format!("{}{}", NODE_ENV_PREFIX, normalize_config_key(db_name)) } fn merged_entries(server: &HashMap, local: &HashMap) -> HashMap { let mut map = server.clone(); for (k, v) in local { map.insert(k.clone(), v.clone()); } map } impl AppConfig { pub fn new() -> Self { Self { inner: Arc::new(RwLock::new(Inner::default())), } } /// 应用启动时初始化 SQLite 并加载本地配置。 pub async fn init_local_store(&self, db_path: PathBuf) -> Result<(), String> { let entries = tokio::task::spawn_blocking(move || -> Result<(Arc, HashMap), String> { let db = LocalConfigDb::open(db_path).map_err(|e| e.to_string())?; let map = db.list_all_map().map_err(|e| e.to_string())?; Ok((Arc::new(db), map)) }) .await .map_err(|e| e.to_string())? .map_err(|e| e)?; let (db, map) = entries; let mut g = self.inner.write().await; g.local_db = Some(db); g.local_entries = map; log::info!( target: "app_config", "本地配置已加载,共 {} 项", g.local_entries.len() ); Ok(()) } async fn with_db(&self, f: F) -> Result where F: FnOnce(Arc) -> Result + Send + 'static, T: Send + 'static, { let db = self .inner .read() .await .local_db .clone() .ok_or_else(|| "本地配置数据库未初始化".to_string())?; tokio::task::spawn_blocking(move || f(db)) .await .map_err(|e| e.to_string())? } /// 合并后的有效配置(本地覆盖服务端同名键)。 pub async fn entries(&self) -> HashMap { let g = self.inner.read().await; merged_entries(&g.server_entries, &g.local_entries) } pub async fn server_entries(&self) -> HashMap { self.inner.read().await.server_entries.clone() } pub async fn local_entries(&self) -> HashMap { self.inner.read().await.local_entries.clone() } pub async fn get(&self, name: &str) -> Option { let g = self.inner.read().await; if let Some(v) = g.local_entries.get(name) { return Some(v.clone()); } g.server_entries.get(name).cloned() } pub async fn last_message(&self) -> String { self.inner.read().await.last_message.clone() } pub async fn clear_server(&self) { let mut g = self.inner.write().await; g.server_entries.clear(); g.last_message.clear(); } pub async fn reload_local_from_db(&self) -> Result<(), String> { let map = self .with_db(|db| db.list_all_map().map_err(|e| e.to_string())) .await?; self.inner.write().await.local_entries = map; Ok(()) } pub async fn set_local(&self, name: String, value: String, mark: Option) -> Result<(), String> { let name_trim = name.trim().to_string(); if name_trim.is_empty() { return Err("配置键名不能为空".into()); } let value_owned = value; let mark_owned = mark.clone(); self.with_db({ let name = name_trim.clone(); let value = value_owned.clone(); move |db| db.upsert(&name, &value, mark_owned.as_deref()).map_err(|e| e.to_string()) }) .await?; let mut g = self.inner.write().await; g.local_entries.insert(name_trim, value_owned); Ok(()) } pub async fn delete_local(&self, name: &str) -> Result<(), String> { let key = name.trim().to_string(); if key.is_empty() { return Err("配置键名不能为空".into()); } self.with_db({ let key = key.clone(); move |db| db.delete(&key).map_err(|e| e.to_string()) }) .await?; self.inner.write().await.local_entries.remove(name.trim()); Ok(()) } pub async fn list_local_items(&self) -> Result, String> { self.with_db(|db| db.list_items().map_err(|e| e.to_string())) .await } /// 供 Node 子进程 `Command::env` 使用(已合并,本地优先)。 pub async fn env_for_node(&self) -> HashMap { let g = self.inner.read().await; let merged = merged_entries(&g.server_entries, &g.local_entries); merged .iter() .map(|(name, value)| (node_env_var_name(name), value.clone())) .collect() } pub async fn refresh_after_auth(&self, token: &str) -> Result<(), AppConfigError> { if token.trim().is_empty() { self.clear_server().await; return Ok(()); } match fetch_from_server(token).await { Ok(map) => { let mut g = self.inner.write().await; g.server_entries = map; g.last_message.clear(); let merged_len = merged_entries(&g.server_entries, &g.local_entries).len(); log::info!( target: "app_config", "服务端配置已更新;合并后共 {} 项(本地 {} 项可覆盖同名键)", merged_len, g.local_entries.len() ); Ok(()) } Err(e) => { let mut g = self.inner.write().await; g.last_message = e.to_string(); log::warn!(target: "app_config", "拉取应用配置失败: {}", e); Err(e) } } } } async fn fetch_from_server(token: &str) -> Result, AppConfigError> { let mut headers = HashMap::new(); headers.insert( "Authorization".to_string(), format!("Bearer {}", token.trim()), ); let (status, v) = match crate::api_client::request_json("GET", "/appConfig", None, headers).await { Ok(pair) => pair, Err(e) => { log::warn!(target: "app_config", "GET /appConfig 请求失败(未拿到响应体): {e}"); println!("GET /appConfig 请求失败(未拿到响应体): {e}"); return Err(AppConfigError::Fetch(e)); } }; if status == 401 { return Err(AppConfigError::Fetch("未认证或令牌已失效".into())); } let ok = v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false); if !ok { let msg = v .get("message") .and_then(|x| x.as_str()) .unwrap_or("未知错误"); return Err(AppConfigError::Fetch(msg.to_string())); } let data = v .get("data") .ok_or_else(|| AppConfigError::Fetch("响应缺少 data".into()))?; match data { Value::Object(obj) => { let mut map = HashMap::new(); for (k, val) in obj { let s = match val { Value::String(s) => s.clone(), Value::Null => String::new(), other => other.to_string(), }; map.insert(k.clone(), s); } Ok(map) } _ => Err(AppConfigError::Fetch("data 应为 JSON 对象".into())), } }