11
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
//! 全局应用配置:登录后从 `GET /api/v1/appConfig` 拉取 `desktop_configs` 键值表,
|
||||
//! 原样缓存;启动 Node 时写入环境变量 `AICLIENT_CFG_<规范化键名>`。
|
||||
//! 应用配置:服务端 `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_";
|
||||
|
||||
@@ -19,8 +22,10 @@ pub enum AppConfigError {
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Inner {
|
||||
entries: HashMap<String, String>,
|
||||
server_entries: HashMap<String, String>,
|
||||
local_entries: HashMap<String, String>,
|
||||
last_message: String,
|
||||
local_db: Option<Arc<LocalConfigDb>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -59,6 +64,14 @@ pub fn node_env_var_name(db_name: &str) -> String {
|
||||
format!("{}{}", NODE_ENV_PREFIX, normalize_config_key(db_name))
|
||||
}
|
||||
|
||||
fn merged_entries(server: &HashMap<String, String>, local: &HashMap<String, String>) -> HashMap<String, String> {
|
||||
let mut map = server.clone();
|
||||
for (k, v) in local {
|
||||
map.insert(k.clone(), v.clone());
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -66,28 +79,128 @@ impl AppConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用启动时初始化 SQLite 并加载本地配置。
|
||||
pub async fn init_local_store(&self, db_path: PathBuf) -> Result<(), String> {
|
||||
let entries = tokio::task::spawn_blocking(move || -> Result<(Arc<LocalConfigDb>, HashMap<String, String>), 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<F, T>(&self, f: F) -> Result<T, String>
|
||||
where
|
||||
F: FnOnce(Arc<LocalConfigDb>) -> Result<T, String> + 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<String, String> {
|
||||
self.inner.read().await.entries.clone()
|
||||
let g = self.inner.read().await;
|
||||
merged_entries(&g.server_entries, &g.local_entries)
|
||||
}
|
||||
|
||||
pub async fn server_entries(&self) -> HashMap<String, String> {
|
||||
self.inner.read().await.server_entries.clone()
|
||||
}
|
||||
|
||||
pub async fn local_entries(&self) -> HashMap<String, String> {
|
||||
self.inner.read().await.local_entries.clone()
|
||||
}
|
||||
|
||||
pub async fn get(&self, name: &str) -> Option<String> {
|
||||
self.inner.read().await.entries.get(name).cloned()
|
||||
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(&self) {
|
||||
pub async fn clear_server(&self) {
|
||||
let mut g = self.inner.write().await;
|
||||
g.entries.clear();
|
||||
g.server_entries.clear();
|
||||
g.last_message.clear();
|
||||
}
|
||||
|
||||
/// 供 Node 子进程 `Command::env` 使用。
|
||||
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<String>) -> 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<Vec<crate::local_config::LocalConfigItem>, 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<String, String> {
|
||||
let g = self.inner.read().await;
|
||||
g.entries
|
||||
let merged = merged_entries(&g.server_entries, &g.local_entries);
|
||||
merged
|
||||
.iter()
|
||||
.map(|(name, value)| (node_env_var_name(name), value.clone()))
|
||||
.collect()
|
||||
@@ -95,18 +208,21 @@ impl AppConfig {
|
||||
|
||||
pub async fn refresh_after_auth(&self, token: &str) -> Result<(), AppConfigError> {
|
||||
if token.trim().is_empty() {
|
||||
self.clear().await;
|
||||
self.clear_server().await;
|
||||
return Ok(());
|
||||
}
|
||||
match fetch_from_server(token).await {
|
||||
Ok(map) => {
|
||||
let mut g = self.inner.write().await;
|
||||
g.entries = map;
|
||||
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",
|
||||
"应用配置已更新,共 {} 项",
|
||||
g.entries.len()
|
||||
"服务端配置已更新;合并后共 {} 项(本地 {} 项可覆盖同名键)",
|
||||
merged_len,
|
||||
g.local_entries.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user