1
This commit is contained in:
180
src-tauri/src/app_config.rs
Normal file
180
src-tauri/src/app_config.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
//! 全局应用配置:登录后从 `GET /api/v1/appConfig` 拉取 `desktop_configs` 键值表,
|
||||
//! 原样缓存;启动 Node 时写入环境变量 `AICLIENT_CFG_<规范化键名>`。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// 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 {
|
||||
entries: HashMap<String, String>,
|
||||
last_message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AppConfig {
|
||||
inner: Arc<RwLock<Inner>>,
|
||||
}
|
||||
|
||||
pub fn api_base() -> String {
|
||||
std::env::var("AICLIENT_API_BASE").unwrap_or_else(|_| "http://127.0.0.1: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))
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(Inner::default())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn entries(&self) -> HashMap<String, String> {
|
||||
self.inner.read().await.entries.clone()
|
||||
}
|
||||
|
||||
pub async fn get(&self, name: &str) -> Option<String> {
|
||||
self.inner.read().await.entries.get(name).cloned()
|
||||
}
|
||||
|
||||
pub async fn last_message(&self) -> String {
|
||||
self.inner.read().await.last_message.clone()
|
||||
}
|
||||
|
||||
pub async fn clear(&self) {
|
||||
let mut g = self.inner.write().await;
|
||||
g.entries.clear();
|
||||
g.last_message.clear();
|
||||
}
|
||||
|
||||
/// 供 Node 子进程 `Command::env` 使用。
|
||||
pub async fn env_for_node(&self) -> HashMap<String, String> {
|
||||
let g = self.inner.read().await;
|
||||
g.entries
|
||||
.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().await;
|
||||
return Ok(());
|
||||
}
|
||||
match fetch_from_server(token).await {
|
||||
Ok(map) => {
|
||||
let mut g = self.inner.write().await;
|
||||
g.entries = map;
|
||||
g.last_message.clear();
|
||||
log::info!(
|
||||
target: "app_config",
|
||||
"应用配置已更新,共 {} 项",
|
||||
g.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 base = api_base().trim_end_matches('/').to_string();
|
||||
let url = format!("{base}/api/v1/appConfig");
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.map_err(|e| AppConfigError::Fetch(e.to_string()))?;
|
||||
|
||||
let res = client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", token.trim()))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| AppConfigError::Fetch(e.to_string()))?;
|
||||
|
||||
let status = res.status();
|
||||
let body = res
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| AppConfigError::Fetch(e.to_string()))?;
|
||||
|
||||
if status == reqwest::StatusCode::UNAUTHORIZED {
|
||||
return Err(AppConfigError::Fetch("未认证或令牌已失效".into()));
|
||||
}
|
||||
|
||||
let v: Value =
|
||||
serde_json::from_str(&body).map_err(|e| AppConfigError::Fetch(format!("响应非 JSON: {e}")))?;
|
||||
|
||||
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())),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user