Files
xianren_studio/crates/core/src/app.rs
T

130 lines
4.6 KiB
Rust

use crate::error::Result;
use crate::settings;
use rusqlite::Connection;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
const APP_DIR_NAME: &str = "XianrenStudio";
/// 应用核心状态:数据库连接与运行时目录。
pub struct CoreApp {
pub db: Mutex<Connection>,
pub data_dir: PathBuf,
pub models_dir: PathBuf,
pub engines_dir: PathBuf,
pub logs_dir: PathBuf,
}
impl CoreApp {
pub fn init(data_dir: Option<PathBuf>) -> Result<Self> {
let data_dir = match data_dir {
Some(d) => d,
None => dirs::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(APP_DIR_NAME),
};
let models_dir = data_dir.join("models");
let engines_dir = data_dir.join("engines");
let logs_dir = data_dir.join("logs");
let recommend_dir = data_dir.join("recommendations");
std::fs::create_dir_all(&data_dir)?;
std::fs::create_dir_all(&models_dir)?;
std::fs::create_dir_all(&engines_dir)?;
std::fs::create_dir_all(&logs_dir)?;
std::fs::create_dir_all(&recommend_dir)?;
let db_path = data_dir.join("xianren.db");
let conn = open_db(&db_path)?;
let app = Self {
db: Mutex::new(conn),
data_dir,
models_dir,
engines_dir,
logs_dir,
};
app.seed_default_settings();
Ok(app)
}
fn seed_default_settings(&self) {
let db = self.db.lock().unwrap();
let defaults = [
("model_dir", self.models_dir.to_string_lossy().to_string()),
(
"engine_bin",
self.engines_dir
.join("cpu")
.join("llama-server.exe")
.to_string_lossy()
.to_string(),
),
("backend", "auto".to_string()),
("hf_endpoint", "https://hf-mirror.com".to_string()),
("api_port", "1234".to_string()),
("api_key", String::new()),
("api_enabled", "false".to_string()),
("upload_max_mb", "10".to_string()),
("auto_title", "true".to_string()),
("suggest_enabled", "true".to_string()),
("suggest_count", "3".to_string()),
(
"tavily_api_key",
"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".to_string(),
),
("tool_web_search_enabled", "true".to_string()),
("auto_load_models", "[]".to_string()),
(
"recommend_dir",
self.data_dir
.join("recommendations")
.to_string_lossy()
.to_string(),
),
];
for (key, value) in defaults {
// 仅补默认值,不覆盖用户已保存的设置
let _ = settings::insert_default(&db, key, &value);
}
}
}
fn open_db(path: &Path) -> Result<Connection> {
let conn = Connection::open(path)?;
conn.pragma_update(None, "journal_mode", "WAL")?;
conn.pragma_update(None, "foreign_keys", "ON")?;
conn.execute_batch(include_str!("schema.sql"))?;
// 旧库迁移:为已存在的 models 表补充新增列
ensure_column(&conn, "models", "kind", "TEXT NOT NULL DEFAULT 'local'")?;
ensure_column(&conn, "models", "base_url", "TEXT")?;
ensure_column(&conn, "models", "api_key", "TEXT")?;
ensure_column(&conn, "models", "api_model", "TEXT")?;
ensure_column(&conn, "models", "enabled", "INTEGER NOT NULL DEFAULT 0")?;
ensure_column(&conn, "messages", "elapsed_ms", "INTEGER")?;
ensure_column(&conn, "messages", "first_token_ms", "INTEGER")?;
ensure_column(&conn, "messages", "images_json", "TEXT NOT NULL DEFAULT '[]'")?;
ensure_column(&conn, "messages", "model_id", "TEXT")?;
ensure_column(&conn, "conversations", "pinned", "INTEGER NOT NULL DEFAULT 0")?;
ensure_column(&conn, "conversations", "favorite", "INTEGER NOT NULL DEFAULT 0")?;
ensure_column(&conn, "conversations", "tools_json", "TEXT NOT NULL DEFAULT '[]'")?;
ensure_column(&conn, "conversations", "agent_id", "TEXT")?;
Ok(conn)
}
fn ensure_column(conn: &Connection, table: &str, column: &str, decl: &str) -> rusqlite::Result<()> {
let mut stmt = conn.prepare(&format!("PRAGMA table_info({table})"))?;
let columns: Vec<String> = stmt
.query_map([], |row| row.get::<_, String>(1))?
.collect::<std::result::Result<_, _>>()?;
if !columns.iter().any(|c| c == column) {
conn.execute(
&format!("ALTER TABLE {table} ADD COLUMN {column} {decl}"),
[],
)?;
}
Ok(())
}