chore: scaffold Phase 0 - Tauri2 + Rust + React + llama.cpp integration
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "xianren-desktop"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "xianren_desktop_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
tracing-subscriber.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
futures.workspace = true
|
||||
uuid.workspace = true
|
||||
xianren-core = { path = "../../crates/core" }
|
||||
xianren-engine = { path = "../../crates/engine" }
|
||||
xianren-download = { path = "../../crates/download" }
|
||||
xianren-api = { path = "../../crates/api" }
|
||||
@@ -0,0 +1,4 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "主窗口默认能力",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default", "opener:default"]
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "xianren-desktop",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
"build": "tauri build",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,583 @@
|
||||
use crate::App;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{AppHandle, Emitter, State};
|
||||
use xianren_api::ApiState;
|
||||
use xianren_core::models as models_db;
|
||||
use xianren_core::sessions as sessions_db;
|
||||
use xianren_core::settings as settings_db;
|
||||
use xianren_core::{ModelInfo, Message};
|
||||
use xianren_engine::{ChatMessage, ChatRequest, EngineConfig};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AppInfo {
|
||||
pub version: String,
|
||||
pub platform: String,
|
||||
pub models_dir: String,
|
||||
pub engine_bin: Option<String>,
|
||||
pub engine_exists: bool,
|
||||
pub db_path: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct ChatParams {
|
||||
pub temperature: f32,
|
||||
pub top_p: f32,
|
||||
pub max_tokens: u32,
|
||||
pub ctx_size: usize,
|
||||
pub ngl: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChatSendPayload {
|
||||
pub conversation_id: String,
|
||||
pub model_id: String,
|
||||
pub content: String,
|
||||
pub params: ChatParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct ChatTokenEvent {
|
||||
pub conversation_id: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct ChatDoneEvent {
|
||||
pub conversation_id: String,
|
||||
pub message_id: String,
|
||||
pub content: String,
|
||||
pub tokens_in: Option<i64>,
|
||||
pub tokens_out: Option<i64>,
|
||||
pub elapsed_ms: u128,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct ChatErrorEvent {
|
||||
pub conversation_id: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct DownloadPayload {
|
||||
pub url: String,
|
||||
pub file_name: String,
|
||||
pub sha256: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct DownloadProgressEvent {
|
||||
pub id: String,
|
||||
pub url: String,
|
||||
pub file_name: String,
|
||||
pub downloaded: u64,
|
||||
pub total: Option<u64>,
|
||||
pub percent: Option<f64>,
|
||||
pub speed_bps: u64,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn app_info(state: State<'_, App>) -> AppInfo {
|
||||
let db = state.core.db.lock().unwrap();
|
||||
let engine_bin = settings_db::get(&db, "engine_bin").ok().flatten();
|
||||
AppInfo {
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
platform: std::env::consts::OS.to_string(),
|
||||
models_dir: state.core.models_dir.to_string_lossy().to_string(),
|
||||
engine_exists: engine_bin
|
||||
.as_ref()
|
||||
.is_some_and(|p| PathBuf::from(p).exists()),
|
||||
engine_bin,
|
||||
db_path: state
|
||||
.core
|
||||
.data_dir
|
||||
.join("xianren.db")
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_models(state: State<'_, App>) -> Result<Vec<ModelInfo>, String> {
|
||||
let db = state.core.db.lock().unwrap();
|
||||
models_db::list(&db).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn import_model(state: State<'_, App>, path: String) -> Result<ModelInfo, String> {
|
||||
let p = PathBuf::from(&path);
|
||||
if !p.exists() {
|
||||
return Err(format!("file not found: {path}"));
|
||||
}
|
||||
let name = p
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
let size = std::fs::metadata(&p).map(|m| m.len() as i64).unwrap_or(0);
|
||||
let db = state.core.db.lock().unwrap();
|
||||
let id = models_db::insert(
|
||||
&db,
|
||||
&name,
|
||||
"local",
|
||||
&name,
|
||||
&path,
|
||||
size,
|
||||
guess_quant(&name).as_deref(),
|
||||
None,
|
||||
None,
|
||||
serde_json::json!({}),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
models_db::get(&db, &id)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "model not found after insert".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_model(state: State<'_, App>, id: String) -> Result<(), String> {
|
||||
let db = state.core.db.lock().unwrap();
|
||||
models_db::remove(&db, &id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn settings_get(state: State<'_, App>) -> Result<HashMap<String, String>, String> {
|
||||
let db = state.core.db.lock().unwrap();
|
||||
let pairs = settings_db::all(&db).map_err(|e| e.to_string())?;
|
||||
Ok(pairs.into_iter().collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn settings_set(state: State<'_, App>, key: String, value: String) -> Result<(), String> {
|
||||
let db = state.core.db.lock().unwrap();
|
||||
settings_db::set(&db, &key, &value).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_conversations(state: State<'_, App>) -> Result<Vec<xianren_core::Conversation>, String> {
|
||||
let db = state.core.db.lock().unwrap();
|
||||
sessions_db::list_conversations(&db).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_conversation(
|
||||
state: State<'_, App>,
|
||||
title: String,
|
||||
) -> Result<xianren_core::Conversation, String> {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let db = state.core.db.lock().unwrap();
|
||||
sessions_db::create_conversation(&db, &id, &title, None, None).map_err(|e| e.to_string())?;
|
||||
sessions_db::get_conversation(&db, &id)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "conversation not found".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_conversation(state: State<'_, App>, id: String) -> Result<(), String> {
|
||||
let db = state.core.db.lock().unwrap();
|
||||
sessions_db::delete_conversation(&db, &id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_messages(state: State<'_, App>, conversation_id: String) -> Result<Vec<Message>, String> {
|
||||
let db = state.core.db.lock().unwrap();
|
||||
sessions_db::list_messages(&db, &conversation_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn engine_start(
|
||||
state: State<'_, App>,
|
||||
model_id: String,
|
||||
params: ChatParams,
|
||||
) -> Result<xianren_engine::EngineStatus, String> {
|
||||
let core = state.core.clone();
|
||||
let engine = state.engine.clone();
|
||||
let base = state.engine_base.clone();
|
||||
|
||||
let (model, engine_bin) = {
|
||||
let db = core.db.lock().unwrap();
|
||||
let model = models_db::get(&db, &model_id)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "model not found".to_string())?;
|
||||
let bin = settings_db::get(&db, "engine_bin")
|
||||
.map_err(|e| e.to_string())?
|
||||
.unwrap_or_default();
|
||||
(model, bin)
|
||||
};
|
||||
|
||||
let bin = PathBuf::from(engine_bin);
|
||||
if !bin.exists() {
|
||||
return Err(format!(
|
||||
"engine binary not found: {},请先在设置页配置 llama-server 路径",
|
||||
bin.display()
|
||||
));
|
||||
}
|
||||
if engine.status().await.running {
|
||||
return Err("engine already running".into());
|
||||
}
|
||||
|
||||
let cfg = EngineConfig {
|
||||
binary_path: bin,
|
||||
model_path: PathBuf::from(model.file_path),
|
||||
host: "127.0.0.1".into(),
|
||||
ctx_size: params.ctx_size,
|
||||
ngl: params.ngl,
|
||||
threads: None,
|
||||
log_file: core.logs_dir.join(format!("engine-{}.log", model.file_name)),
|
||||
};
|
||||
engine.start(cfg).await.map_err(|e| e.to_string())?;
|
||||
*base.write().await = engine.base_url().await;
|
||||
Ok(engine.status().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn engine_stop(state: State<'_, App>) -> Result<(), String> {
|
||||
state.engine.stop().await.map_err(|e| e.to_string())?;
|
||||
*state.engine_base.write().await = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn engine_status(
|
||||
state: State<'_, App>,
|
||||
) -> Result<xianren_engine::EngineStatus, String> {
|
||||
Ok(state.engine.status().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn chat_send(
|
||||
app: AppHandle,
|
||||
state: State<'_, App>,
|
||||
payload: ChatSendPayload,
|
||||
) -> Result<(), String> {
|
||||
let core = state.core.clone();
|
||||
let engine = state.engine.clone();
|
||||
let base = state.engine_base.clone();
|
||||
|
||||
let (history, model, engine_bin) = {
|
||||
let db = core.db.lock().unwrap();
|
||||
if sessions_db::get_conversation(&db, &payload.conversation_id)
|
||||
.map_err(|e| e.to_string())?
|
||||
.is_none()
|
||||
{
|
||||
let title: String = payload.content.chars().take(30).collect();
|
||||
sessions_db::create_conversation(
|
||||
&db,
|
||||
&payload.conversation_id,
|
||||
&title,
|
||||
Some(&payload.model_id),
|
||||
None,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
sessions_db::add_message(
|
||||
&db,
|
||||
&payload.conversation_id,
|
||||
"user",
|
||||
&payload.content,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let history = sessions_db::list_messages(&db, &payload.conversation_id)
|
||||
.map_err(|e| e.to_string())?
|
||||
.into_iter()
|
||||
.map(|m| ChatMessage {
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let model = models_db::get(&db, &payload.model_id)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "model not found".to_string())?;
|
||||
let bin = settings_db::get(&db, "engine_bin")
|
||||
.map_err(|e| e.to_string())?
|
||||
.unwrap_or_default();
|
||||
(history, model, bin)
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
if !engine.status().await.running {
|
||||
let bin = PathBuf::from(&engine_bin);
|
||||
if !bin.exists() {
|
||||
let _ = app.emit(
|
||||
"chat://error",
|
||||
ChatErrorEvent {
|
||||
conversation_id: payload.conversation_id.clone(),
|
||||
message: format!(
|
||||
"engine binary not found: {},请先在设置页配置",
|
||||
bin.display()
|
||||
),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
let cfg = EngineConfig {
|
||||
binary_path: bin,
|
||||
model_path: PathBuf::from(model.file_path),
|
||||
host: "127.0.0.1".into(),
|
||||
ctx_size: payload.params.ctx_size,
|
||||
ngl: payload.params.ngl,
|
||||
threads: None,
|
||||
log_file: core.logs_dir.join(format!("engine-{}.log", model.file_name)),
|
||||
};
|
||||
if let Err(e) = engine.start(cfg).await {
|
||||
let _ = app.emit(
|
||||
"chat://error",
|
||||
ChatErrorEvent {
|
||||
conversation_id: payload.conversation_id.clone(),
|
||||
message: e.to_string(),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
*base.write().await = engine.base_url().await;
|
||||
}
|
||||
|
||||
let started = std::time::Instant::now();
|
||||
let req = ChatRequest {
|
||||
model: model.repo_id,
|
||||
messages: history,
|
||||
temperature: Some(payload.params.temperature),
|
||||
top_p: Some(payload.params.top_p),
|
||||
max_tokens: Some(payload.params.max_tokens),
|
||||
stream: true,
|
||||
};
|
||||
|
||||
let mut full = String::new();
|
||||
match engine.stream_chat(req).await {
|
||||
Ok(mut stream) => {
|
||||
use futures::StreamExt;
|
||||
while let Some(item) = stream.next().await {
|
||||
match item {
|
||||
Ok(text) => {
|
||||
full.push_str(&text);
|
||||
let _ = app.emit(
|
||||
"chat://token",
|
||||
ChatTokenEvent {
|
||||
conversation_id: payload.conversation_id.clone(),
|
||||
text,
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = app.emit(
|
||||
"chat://error",
|
||||
ChatErrorEvent {
|
||||
conversation_id: payload.conversation_id.clone(),
|
||||
message: e.to_string(),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
let elapsed_ms = started.elapsed().as_millis();
|
||||
let db = core.db.lock().unwrap();
|
||||
let message_id = sessions_db::add_message(
|
||||
&db,
|
||||
&payload.conversation_id,
|
||||
"assistant",
|
||||
&full,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
let _ = sessions_db::touch_conversation(&db, &payload.conversation_id);
|
||||
let _ = app.emit(
|
||||
"chat://done",
|
||||
ChatDoneEvent {
|
||||
conversation_id: payload.conversation_id.clone(),
|
||||
message_id,
|
||||
content: full,
|
||||
tokens_in: None,
|
||||
tokens_out: None,
|
||||
elapsed_ms,
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = app.emit(
|
||||
"chat://error",
|
||||
ChatErrorEvent {
|
||||
conversation_id: payload.conversation_id.clone(),
|
||||
message: e.to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn download_enqueue(
|
||||
app: AppHandle,
|
||||
state: State<'_, App>,
|
||||
payload: DownloadPayload,
|
||||
) -> Result<String, String> {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let core = state.core.clone();
|
||||
let dest = core.models_dir.join(&payload.file_name);
|
||||
let opts = xianren_download::DownloadOptions {
|
||||
url: payload.url.clone(),
|
||||
dest,
|
||||
expected_sha256: payload.sha256.clone(),
|
||||
chunk_size: 8 * 1024 * 1024,
|
||||
};
|
||||
let pid = payload;
|
||||
let app2 = app.clone();
|
||||
let task_id = id.clone();
|
||||
let progress_url = pid.url.clone();
|
||||
let progress_name = pid.file_name.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let tid = task_id.clone();
|
||||
let progress = move |pr: xianren_download::DownloadProgress| {
|
||||
let _ = app2.emit(
|
||||
"download://progress",
|
||||
DownloadProgressEvent {
|
||||
id: tid.clone(),
|
||||
url: progress_url.clone(),
|
||||
file_name: progress_name.clone(),
|
||||
downloaded: pr.downloaded,
|
||||
total: pr.total,
|
||||
percent: pr.percent,
|
||||
speed_bps: pr.speed_bps,
|
||||
status: "downloading".into(),
|
||||
},
|
||||
);
|
||||
};
|
||||
match xianren_download::download(opts, progress).await {
|
||||
Ok(path) => {
|
||||
let size = std::fs::metadata(&path)
|
||||
.map(|m| m.len() as i64)
|
||||
.unwrap_or(0);
|
||||
let db = core.db.lock().unwrap();
|
||||
let _ = models_db::insert(
|
||||
&db,
|
||||
&pid.url,
|
||||
"download",
|
||||
&pid.file_name,
|
||||
&path.to_string_lossy(),
|
||||
size,
|
||||
guess_quant(&pid.file_name).as_deref(),
|
||||
None,
|
||||
pid.sha256.as_deref(),
|
||||
serde_json::json!({ "url": pid.url }),
|
||||
);
|
||||
let _ = app.emit(
|
||||
"download://done",
|
||||
DownloadProgressEvent {
|
||||
id: task_id.clone(),
|
||||
url: pid.url,
|
||||
file_name: pid.file_name,
|
||||
downloaded: size as u64,
|
||||
total: Some(size as u64),
|
||||
percent: Some(100.0),
|
||||
speed_bps: 0,
|
||||
status: "done".into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = app.emit(
|
||||
"download://error",
|
||||
DownloadProgressEvent {
|
||||
id: task_id.clone(),
|
||||
url: pid.url,
|
||||
file_name: pid.file_name,
|
||||
downloaded: 0,
|
||||
total: None,
|
||||
percent: None,
|
||||
speed_bps: 0,
|
||||
status: format!("error: {e}"),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn server_start(
|
||||
state: State<'_, App>,
|
||||
port: u16,
|
||||
api_key: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let mut guard = state.server.lock().await;
|
||||
if guard.is_some() {
|
||||
return Err("server already running".into());
|
||||
}
|
||||
let api_state = ApiState::new(
|
||||
state.engine_base.clone(),
|
||||
{
|
||||
let core = state.core.clone();
|
||||
move || {
|
||||
let db = core.db.lock().unwrap();
|
||||
models_db::list(&db)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
serde_json::json!({
|
||||
"id": m.repo_id,
|
||||
"object": "model",
|
||||
"created": 0,
|
||||
"owned_by": m.source,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
},
|
||||
if api_key.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(api_key)
|
||||
},
|
||||
);
|
||||
let server = xianren_api::start(port, api_state)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let addr = server.addr;
|
||||
*guard = Some(server);
|
||||
Ok(serde_json::json!({ "running": true, "port": addr.port() }))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn server_stop(state: State<'_, App>) -> Result<(), String> {
|
||||
let mut guard = state.server.lock().await;
|
||||
if let Some(server) = guard.take() {
|
||||
server.shutdown();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn server_status(state: State<'_, App>) -> Result<serde_json::Value, String> {
|
||||
let guard = state.server.lock().await;
|
||||
Ok(match guard.as_ref() {
|
||||
Some(s) => serde_json::json!({ "running": true, "port": s.addr.port() }),
|
||||
None => serde_json::json!({ "running": false, "port": null }),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_path(path: String) -> Result<(), String> {
|
||||
tauri_plugin_opener::open_path(path, None::<&str>).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn guess_quant(file_name: &str) -> Option<String> {
|
||||
const QUANTS: &[&str] = &[
|
||||
"q4_k_m", "q5_k_m", "q6_k", "q8_0", "q4_0", "q4_1", "q5_0", "q5_1", "q2_k", "q3_k_m",
|
||||
"f16", "f32",
|
||||
];
|
||||
let lower = file_name.to_lowercase();
|
||||
QUANTS
|
||||
.iter()
|
||||
.find(|q| lower.contains(**q))
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
mod commands;
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use xianren_core::CoreApp;
|
||||
use xianren_engine::EngineManager;
|
||||
|
||||
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>>,
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "info".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::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),
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::app_info,
|
||||
commands::list_models,
|
||||
commands::import_model,
|
||||
commands::remove_model,
|
||||
commands::settings_get,
|
||||
commands::settings_set,
|
||||
commands::list_conversations,
|
||||
commands::create_conversation,
|
||||
commands::delete_conversation,
|
||||
commands::get_messages,
|
||||
commands::engine_start,
|
||||
commands::engine_stop,
|
||||
commands::engine_status,
|
||||
commands::chat_send,
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
xianren_desktop_lib::run()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Xianren Studio",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.xianren.studio",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev --prefix ../../ui",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeBuildCommand": "npm run build --prefix ../../ui",
|
||||
"frontendDist": "../../ui/dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "仙人工作室",
|
||||
"width": 1280,
|
||||
"height": 820,
|
||||
"minWidth": 960,
|
||||
"minHeight": 640,
|
||||
"center": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["nsis"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user