281 lines
11 KiB
Rust
281 lines
11 KiB
Rust
mod commands;
|
|
mod knowledge;
|
|
mod mail;
|
|
mod mcp_client;
|
|
mod tools;
|
|
mod workflow;
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use tokio::sync::RwLock;
|
|
use tauri::{Emitter, Manager};
|
|
use tauri_plugin_autostart::MacosLauncher;
|
|
use xianren_core::CoreApp;
|
|
use xianren_core::models as models_db;
|
|
use xianren_core::settings as settings_db;
|
|
use xianren_engine::EngineManager;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
pub struct App {
|
|
pub core: Arc<CoreApp>,
|
|
pub engine: EngineManager,
|
|
pub engine_base: Arc<RwLock<Option<String>>>,
|
|
pub server: tokio::sync::Mutex<Option<xianren_api::ApiServer>>,
|
|
/// 会话 -> 生成取消信号(watch 发送端)
|
|
pub cancel_flags:
|
|
Arc<tokio::sync::Mutex<HashMap<String, tokio::sync::watch::Sender<bool>>>>,
|
|
}
|
|
|
|
pub fn run() {
|
|
let data_dir = dirs::data_dir()
|
|
.unwrap_or_else(|| PathBuf::from("."))
|
|
.join("XianrenStudio");
|
|
let logs_dir = data_dir.join("logs");
|
|
let _ = std::fs::create_dir_all(&logs_dir);
|
|
// 全局 panic 钩子:任何 panic 都会记录到日志目录,便于定位崩溃原因
|
|
let panic_log = logs_dir.join("panic.log");
|
|
std::panic::set_hook(Box::new(move |info| {
|
|
let msg = format!(
|
|
"[pid={}] thread '{}' panicked: {}\n{}",
|
|
std::process::id(),
|
|
std::thread::current().name().unwrap_or("<unnamed>"),
|
|
info,
|
|
std::backtrace::Backtrace::force_capture(),
|
|
);
|
|
let _ = std::fs::OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(&panic_log)
|
|
.and_then(|mut f| {
|
|
use std::io::Write;
|
|
f.write_all(msg.as_bytes())
|
|
});
|
|
eprintln!("{msg}");
|
|
}));
|
|
let file_appender = tracing_appender::rolling::daily(&logs_dir, "app.log");
|
|
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
|
|
tracing_subscriber::fmt()
|
|
.with_env_filter(
|
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| "info".into()),
|
|
)
|
|
.with_writer(non_blocking)
|
|
.with_ansi(false)
|
|
.init();
|
|
|
|
tauri::Builder::default()
|
|
.plugin(tauri_plugin_opener::init())
|
|
.plugin(tauri_plugin_autostart::init(
|
|
MacosLauncher::LaunchAgent,
|
|
None,
|
|
))
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.setup(|app| {
|
|
let core = CoreApp::init(None)
|
|
.map_err(|e| -> Box<dyn std::error::Error> { e.to_string().into() })?;
|
|
tauri::Manager::manage(
|
|
app,
|
|
App {
|
|
core: Arc::new(core),
|
|
engine: EngineManager::new(),
|
|
engine_base: Arc::new(RwLock::new(None)),
|
|
server: tokio::sync::Mutex::new(None),
|
|
cancel_flags: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
|
|
},
|
|
);
|
|
|
|
// 启动时自动扫描模型目录
|
|
let app_state = app.state::<App>();
|
|
let db = app_state.core.db.lock().unwrap();
|
|
// 校验引擎路径:旧版本可能残留指向无效 exe 的配置(缺 DLL),自动修复
|
|
let configured = settings_db::get(&db, "engine_bin").ok().flatten();
|
|
let configured_ok = configured
|
|
.as_ref()
|
|
.map(|p| engine_binary_usable(PathBuf::from(p).as_path()))
|
|
.unwrap_or(false);
|
|
if !configured_ok {
|
|
let mut found: Option<PathBuf> = None;
|
|
for dir in ["cpu", "vulkan", "cuda"] {
|
|
let candidate = app_state.core.engines_dir.join(dir).join("llama-server.exe");
|
|
if engine_binary_usable(&candidate) {
|
|
found = Some(candidate);
|
|
break;
|
|
}
|
|
}
|
|
if found.is_none() {
|
|
let candidate = app_state.core.engines_dir.join("llama-server-cpu.exe");
|
|
if engine_binary_usable(&candidate) {
|
|
found = Some(candidate);
|
|
}
|
|
}
|
|
if let Some(path) = found {
|
|
let _ = settings_db::set(&db, "engine_bin", &path.to_string_lossy());
|
|
tracing::info!(path = %path.display(), "engine binary auto-fixed");
|
|
} else {
|
|
tracing::warn!("no usable llama-server binary found");
|
|
}
|
|
}
|
|
let model_dir = settings_db::get(&db, "model_dir")
|
|
.ok()
|
|
.flatten()
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| app_state.core.models_dir.clone());
|
|
match models_db::scan_directory(&db, &model_dir) {
|
|
Ok(result) => {
|
|
tracing::info!(
|
|
added = result.added,
|
|
updated = result.updated,
|
|
missing = result.missing,
|
|
"auto scan complete"
|
|
);
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "auto scan failed");
|
|
}
|
|
}
|
|
let seeded_agents = commands::seed_preset_agents(&db);
|
|
if seeded_agents > 0 {
|
|
tracing::info!(seeded = seeded_agents, "preset agents seeded");
|
|
}
|
|
let seeded_workflows = commands::seed_preset_workflows(&db);
|
|
if seeded_workflows > 0 {
|
|
tracing::info!(seeded = seeded_workflows, "preset workflows seeded");
|
|
}
|
|
let _ = app.emit("models://updated", ());
|
|
|
|
// 自动加载模型:按设置列表顺序逐个部署,最后一个保持运行
|
|
let auto_load_app = app.handle().clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
commands::auto_load_models(&auto_load_app).await;
|
|
});
|
|
|
|
// 定时计划后台调度:每 30 秒检查一次到点的计划并执行
|
|
let scheduler_app = app.handle().clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
let mut ticker = tokio::time::interval(std::time::Duration::from_secs(30));
|
|
loop {
|
|
ticker.tick().await;
|
|
if let Err(e) = commands::run_due_scheduled_tasks(&scheduler_app).await {
|
|
tracing::warn!(error = %e, "scheduled tasks tick failed");
|
|
}
|
|
}
|
|
});
|
|
Ok(())
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
commands::app_info,
|
|
commands::list_agents,
|
|
commands::add_agent,
|
|
commands::update_agent,
|
|
commands::remove_agent,
|
|
commands::set_agent_enabled,
|
|
commands::restore_preset_agents,
|
|
commands::list_workflows,
|
|
commands::add_workflow,
|
|
commands::update_workflow,
|
|
commands::remove_workflow,
|
|
commands::set_workflow_enabled,
|
|
commands::restore_preset_workflows,
|
|
commands::run_workflow,
|
|
commands::list_scheduled_tasks,
|
|
commands::add_scheduled_task,
|
|
commands::update_scheduled_task,
|
|
commands::remove_scheduled_task,
|
|
commands::set_scheduled_task_enabled,
|
|
commands::run_scheduled_task_now,
|
|
commands::autostart_status,
|
|
commands::autostart_set,
|
|
commands::mail_test,
|
|
commands::list_knowledge_bases,
|
|
commands::add_knowledge_base,
|
|
commands::update_knowledge_base,
|
|
commands::remove_knowledge_base,
|
|
commands::list_kb_documents,
|
|
commands::get_kb_document,
|
|
commands::kb_import_documents,
|
|
commands::remove_kb_document,
|
|
commands::kb_rechunk_document,
|
|
commands::kb_search,
|
|
commands::kb_list_sources,
|
|
commands::kb_add_source,
|
|
commands::kb_scan_sources,
|
|
commands::kb_remove_source,
|
|
commands::list_models,
|
|
commands::import_model,
|
|
commands::remove_model,
|
|
commands::set_model_enabled,
|
|
commands::scan_models,
|
|
commands::add_remote_model,
|
|
commands::list_skills,
|
|
commands::add_skill,
|
|
commands::update_skill,
|
|
commands::remove_skill,
|
|
commands::set_skill_enabled,
|
|
commands::test_skill,
|
|
commands::list_mcp_servers,
|
|
commands::add_mcp_server,
|
|
commands::update_mcp_server,
|
|
commands::remove_mcp_server,
|
|
commands::set_mcp_server_enabled,
|
|
commands::mcp_test_server,
|
|
commands::mcp_list_tools,
|
|
commands::mcp_call_tool,
|
|
commands::search_models,
|
|
commands::list_model_files,
|
|
commands::list_recommended_models,
|
|
commands::import_recommendations,
|
|
commands::fetch_model_page,
|
|
commands::settings_get,
|
|
commands::settings_set,
|
|
commands::list_conversations,
|
|
commands::create_conversation,
|
|
commands::rename_conversation,
|
|
commands::set_conversation_pinned,
|
|
commands::set_conversation_favorite,
|
|
commands::import_conversation,
|
|
commands::set_conversation_tools,
|
|
commands::web_search,
|
|
commands::delete_conversation,
|
|
commands::get_messages,
|
|
commands::engine_start,
|
|
commands::engine_stop,
|
|
commands::engine_status,
|
|
commands::deploy_model,
|
|
commands::chat_send,
|
|
commands::chat_stop,
|
|
commands::regenerate_message,
|
|
commands::edit_message,
|
|
commands::list_message_versions,
|
|
commands::apply_message_version,
|
|
commands::report_error,
|
|
commands::download_enqueue,
|
|
commands::server_start,
|
|
commands::server_stop,
|
|
commands::server_status,
|
|
commands::open_path,
|
|
])
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running tauri application");
|
|
}
|
|
|
|
/// 判断引擎二进制是否可用:文件存在,且同目录包含 DLL(llama.cpp 发布版依赖同目录 DLL)。
|
|
fn engine_binary_usable(path: &Path) -> bool {
|
|
if !path.is_file() {
|
|
return false;
|
|
}
|
|
let Some(parent) = path.parent() else {
|
|
return false;
|
|
};
|
|
std::fs::read_dir(parent)
|
|
.map(|entries| {
|
|
entries.flatten().any(|entry| {
|
|
entry
|
|
.path()
|
|
.extension()
|
|
.map(|ext| ext.eq_ignore_ascii_case("dll"))
|
|
.unwrap_or(false)
|
|
})
|
|
})
|
|
.unwrap_or(false)
|
|
}
|