feat: 添加 Tauri 桌面应用

- 创建 packages/desktop 模块
- 实现 Tauri 2.0 + React 桌面应用
- 复用 Web 前端代码
- 添加系统托盘功能
- 实现本地文件访问命令
- 配置 Vite + Tauri 集成
- 更新 .gitignore 添加 Rust/Tauri 相关规则
This commit is contained in:
2025-12-12 13:19:00 +08:00
parent b5d3b7df57
commit 6ef9d95172
86 changed files with 14495 additions and 1 deletions
+30
View File
@@ -0,0 +1,30 @@
[package]
name = "ai-assistant-desktop"
version = "1.0.0"
description = "AI Terminal Assistant Desktop Application"
authors = ["AI Assistant Team"]
edition = "2021"
rust-version = "1.70"
[lib]
name = "ai_assistant_desktop_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-shell = "2"
tauri-plugin-fs = "2"
tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
[profile.release]
panic = "abort"
codegen-units = 1
lto = true
opt-level = "s"
strip = true
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+118
View File
@@ -0,0 +1,118 @@
use serde::Serialize;
use std::fs;
use std::path::PathBuf;
use std::sync::mpsc;
use tauri_plugin_dialog::DialogExt;
#[derive(Serialize)]
pub struct AppInfo {
pub name: String,
pub version: String,
pub tauri_version: String,
}
#[tauri::command]
pub fn get_app_info() -> AppInfo {
AppInfo {
name: "AI Assistant".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
tauri_version: tauri::VERSION.to_string(),
}
}
#[tauri::command]
pub async fn open_directory_dialog(app: tauri::AppHandle) -> Result<Option<String>, String> {
let (tx, rx) = mpsc::channel();
app.dialog()
.file()
.set_title("Select Working Directory")
.pick_folder(move |path| {
let _ = tx.send(path);
});
match rx.recv() {
Ok(Some(path)) => Ok(Some(path.to_string())),
Ok(None) => Ok(None),
Err(e) => Err(e.to_string()),
}
}
#[derive(Serialize)]
pub struct FileContent {
pub path: String,
pub content: String,
pub size: u64,
}
#[tauri::command]
pub async fn read_local_file(path: String) -> Result<FileContent, String> {
let path_buf = PathBuf::from(&path);
if !path_buf.exists() {
return Err(format!("File not found: {}", path));
}
let metadata = fs::metadata(&path_buf).map_err(|e| e.to_string())?;
if metadata.len() > 10 * 1024 * 1024 {
return Err("File too large (max 10MB)".to_string());
}
let content = fs::read_to_string(&path_buf).map_err(|e| e.to_string())?;
Ok(FileContent {
path,
content,
size: metadata.len(),
})
}
#[derive(Serialize)]
pub struct DirectoryEntry {
pub name: String,
pub path: String,
pub is_dir: bool,
pub size: u64,
}
#[tauri::command]
pub async fn list_directory(path: String) -> Result<Vec<DirectoryEntry>, String> {
let path_buf = PathBuf::from(&path);
if !path_buf.exists() {
return Err(format!("Directory not found: {}", path));
}
if !path_buf.is_dir() {
return Err(format!("Not a directory: {}", path));
}
let mut entries = Vec::new();
let read_dir = fs::read_dir(&path_buf).map_err(|e| e.to_string())?;
for entry in read_dir {
let entry = entry.map_err(|e| e.to_string())?;
let metadata = entry.metadata().map_err(|e| e.to_string())?;
entries.push(DirectoryEntry {
name: entry.file_name().to_string_lossy().to_string(),
path: entry.path().to_string_lossy().to_string(),
is_dir: metadata.is_dir(),
size: metadata.len(),
});
}
entries.sort_by(|a, b| {
if a.is_dir == b.is_dir {
a.name.to_lowercase().cmp(&b.name.to_lowercase())
} else if a.is_dir {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
}
});
Ok(entries)
}
+66
View File
@@ -0,0 +1,66 @@
use tauri::{
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Manager, Runtime,
};
mod commands;
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.setup(|app| {
setup_tray(app)?;
Ok(())
})
.invoke_handler(tauri::generate_handler![
commands::get_app_info,
commands::open_directory_dialog,
commands::read_local_file,
commands::list_directory,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
fn setup_tray<R: Runtime>(app: &tauri::App<R>) -> Result<(), Box<dyn std::error::Error>> {
let show_item = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show_item, &quit_item])?;
let _tray = TrayIconBuilder::new()
.menu(&menu)
.show_menu_on_left_click(false)
.on_menu_event(|app, event| match event.id.as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"quit" => {
app.exit(0);
}
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)?;
Ok(())
}
+5
View File
@@ -0,0 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
ai_assistant_desktop_lib::run()
}
@@ -0,0 +1,55 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "AI Assistant",
"version": "1.0.0",
"identifier": "com.ai-assistant.desktop",
"build": {
"beforeDevCommand": "pnpm dev:vite",
"devUrl": "http://localhost:5199",
"beforeBuildCommand": "pnpm build:vite",
"frontendDist": "../dist"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "AI Assistant",
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 600,
"resizable": true,
"fullscreen": false,
"center": true
}
],
"security": {
"csp": null
},
"trayIcon": {
"iconPath": "icons/icon.png",
"iconAsTemplate": true
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"macOS": {
"minimumSystemVersion": "10.15"
},
"windows": {
"wix": null,
"nsis": {
"installMode": "currentUser"
}
}
},
"plugins": {}
}