Files
yaoayanui/src-tauri/src/app_config.rs
949036910@qq.com 50ab92d8f5 11
2026-05-27 22:31:34 +08:00

297 lines
9.3 KiB
Rust

//! 应用配置:服务端 `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<String, String>,
local_entries: HashMap<String, String>,
last_message: String,
local_db: Option<Arc<LocalConfigDb>>,
}
#[derive(Debug, Default)]
pub struct AppConfig {
inner: Arc<RwLock<Inner>>,
}
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<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 {
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<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> {
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> {
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<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;
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<HashMap<String, String>, 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())),
}
}