mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-04 14:52:19 +00:00
Upgrade to Tauri v2 (#693)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
c3276df727
commit
6ee5a1945b
18 files changed
+2599
-1729
No files matched your search
Generated
+1862
-1181
File diff suppressed because it is too large.
Load diff
+13
-10
@@ -1,16 +1,19 @@
|
||||
[package]
|
||||
name = "mindwork-ai-studio"
|
||||
version = "26.5.1"
|
||||
edition = "2021"
|
||||
version = "26.5.2"
|
||||
edition = "2024"
|
||||
description = "MindWork AI Studio"
|
||||
authors = ["Thorsten Sommer"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.5.6", features = [] }
|
||||
tauri-build = { version = "2.6.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.8.3", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog", "global-shortcut"] }
|
||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tauri = { version = "2.11.1", features = [] }
|
||||
tauri-plugin-window-state = { version = "2.4.1" }
|
||||
tauri-plugin-shell = "2.3.5"
|
||||
tauri-plugin-dialog = "2.7.1"
|
||||
tauri-plugin-opener = "2.5.4"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
keyring = { version = "3.6.2", features = ["apple-native", "windows-native", "sync-secret-service"] }
|
||||
@@ -32,7 +35,7 @@ pbkdf2 = "0.12.2"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.8"
|
||||
rcgen = { version = "0.14.7", features = ["pem"] }
|
||||
file-format = "0.28.0"
|
||||
file-format = "0.29.0"
|
||||
calamine = "0.34.0"
|
||||
pdfium-render = "0.8.37"
|
||||
sys-locale = "0.3.2"
|
||||
@@ -45,17 +48,17 @@ sysinfo = "0.38.4"
|
||||
# Fixes security vulnerability downstream, where the upstream is not fixed yet:
|
||||
time = "0.3.47" # -> Rocket
|
||||
bytes = "1.11.1" # -> almost every dependency
|
||||
tar = "0.4.45" # -> Tauri v1
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# See issue https://github.com/tauri-apps/tauri/issues/4470
|
||||
reqwest = { version = "0.13.2", features = ["native-tls-vendored"] }
|
||||
|
||||
# Fixes security vulnerability downstream, where the upstream is not fixed yet:
|
||||
openssl = "0.10.76" # -> reqwest, Tauri v1
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows-registry = "0.6.1"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
tauri-plugin-updater = "2.10.0"
|
||||
|
||||
[features]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
+26
-1
@@ -53,6 +53,18 @@ fn update_cargo_toml(cargo_path: &str, version: &str) {
|
||||
let cargo_toml_lines = cargo_toml.lines();
|
||||
let mut new_cargo_toml = String::new();
|
||||
|
||||
// Return early when the version already matches to avoid unnecessary rewrites.
|
||||
let current_version = cargo_toml.lines().find_map(|line| {
|
||||
let trimmed = line.trim_start();
|
||||
let rest = trimmed.strip_prefix("\"version\": ")?;
|
||||
let quoted = rest.strip_prefix('"')?;
|
||||
let end_idx = quoted.find('"')?;
|
||||
Some("ed[..end_idx])
|
||||
});
|
||||
if current_version == Some(version) {
|
||||
return;
|
||||
}
|
||||
|
||||
for line in cargo_toml_lines {
|
||||
if line.starts_with("version = ") {
|
||||
new_cargo_toml.push_str(&format!("version = \"{version}\""));
|
||||
@@ -67,6 +79,19 @@ fn update_cargo_toml(cargo_path: &str, version: &str) {
|
||||
|
||||
fn update_tauri_conf(tauri_conf_path: &str, version: &str) {
|
||||
let tauri_conf = std::fs::read_to_string(tauri_conf_path).unwrap();
|
||||
|
||||
// Return early when the version already matches to avoid unnecessary rewrites.
|
||||
let current_version = tauri_conf.lines().find_map(|line| {
|
||||
let trimmed = line.trim_start();
|
||||
let rest = trimmed.strip_prefix("\"version\": ")?;
|
||||
let quoted = rest.strip_prefix('"')?;
|
||||
let end_idx = quoted.find('"')?;
|
||||
Some("ed[..end_idx])
|
||||
});
|
||||
if current_version == Some(version) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tauri_conf_lines = tauri_conf.lines();
|
||||
let mut new_tauri_conf = String::new();
|
||||
|
||||
@@ -75,7 +100,7 @@ fn update_tauri_conf(tauri_conf_path: &str, version: &str) {
|
||||
// "version": "0.1.0-alpha.0"
|
||||
// Please notice, that the version number line might have a leading tab, etc.
|
||||
if line.contains("\"version\": ") {
|
||||
new_tauri_conf.push_str(&format!("\t\"version\": \"{version}\""));
|
||||
new_tauri_conf.push_str(&format!(" \"version\": \"{version}\","));
|
||||
} else {
|
||||
new_tauri_conf.push_str(line);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default capability for MindWork AI Studio",
|
||||
"remote": {
|
||||
"urls": [
|
||||
"http://localhost:*"
|
||||
]
|
||||
},
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"updater:default",
|
||||
"opener:default",
|
||||
"shell:allow-open",
|
||||
{
|
||||
"identifier": "shell:allow-spawn",
|
||||
"allow": [
|
||||
{
|
||||
"name": "mindworkAIStudioServer",
|
||||
"sidecar": true,
|
||||
"args": true
|
||||
},
|
||||
{
|
||||
"name": "qdrant",
|
||||
"sidecar": true,
|
||||
"args": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+243
-404
@@ -9,9 +9,12 @@ use rocket::serde::json::Json;
|
||||
use rocket::serde::Serialize;
|
||||
use serde::Deserialize;
|
||||
use strum_macros::Display;
|
||||
use tauri::updater::UpdateResponse;
|
||||
use tauri::{FileDropEvent, GlobalShortcutManager, UpdaterEvent, RunEvent, Manager, PathResolver, Window, WindowEvent, generate_context};
|
||||
use tauri::api::dialog::blocking::FileDialogBuilder;
|
||||
use tauri::{DragDropEvent,RunEvent, Manager, WindowEvent, generate_context};
|
||||
use tauri::path::PathResolver;
|
||||
use tauri::WebviewWindow;
|
||||
use tauri_plugin_updater::{UpdaterExt, Update};
|
||||
use tauri_plugin_global_shortcut::GlobalShortcutExt;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::time;
|
||||
use crate::api_token::APIToken;
|
||||
@@ -24,10 +27,10 @@ use crate::qdrant::{cleanup_qdrant, start_qdrant_server, stop_qdrant_server};
|
||||
use crate::dotnet::create_startup_env_file;
|
||||
|
||||
/// The Tauri main window.
|
||||
static MAIN_WINDOW: Lazy<Mutex<Option<Window>>> = Lazy::new(|| Mutex::new(None));
|
||||
pub static MAIN_WINDOW: Lazy<Mutex<Option<WebviewWindow>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
/// The update response coming from the Tauri updater.
|
||||
static CHECK_UPDATE_RESPONSE: Lazy<Mutex<Option<UpdateResponse<tauri::Wry>>>> = Lazy::new(|| Mutex::new(None));
|
||||
static CHECK_UPDATE_RESPONSE: Lazy<Mutex<Option<Update>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
/// The event broadcast sender for Tauri events.
|
||||
static EVENT_BROADCAST: Lazy<Mutex<Option<broadcast::Sender<Event>>>> = Lazy::new(|| Mutex::new(None));
|
||||
@@ -35,6 +38,9 @@ static EVENT_BROADCAST: Lazy<Mutex<Option<broadcast::Sender<Event>>>> = Lazy::ne
|
||||
/// Stores the currently registered global shortcuts (name -> shortcut string).
|
||||
static REGISTERED_SHORTCUTS: Lazy<Mutex<HashMap<Shortcut, String>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
/// Stores the localhost origin of the Blazor app after the .NET server is ready.
|
||||
static APPROVED_APP_URL: Lazy<Mutex<Option<tauri::Url>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
/// Enum identifying global keyboard shortcuts.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display)]
|
||||
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
|
||||
@@ -76,10 +82,34 @@ pub fn start_tauri() {
|
||||
});
|
||||
|
||||
let app = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(
|
||||
tauri::plugin::Builder::<tauri::Wry, ()>::new("external-link-handler")
|
||||
.on_navigation(|webview, url| {
|
||||
if !should_open_in_system_browser(webview, url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
match webview.app_handle().opener().open_url(url.as_str(), None::<&str>) {
|
||||
Ok(_) => {
|
||||
info!(Source = "Tauri"; "Opening external URL in system browser: {url}");
|
||||
},
|
||||
Err(error) => {
|
||||
error!(Source = "Tauri"; "Failed to open external URL '{url}' in system browser: {error}");
|
||||
},
|
||||
}
|
||||
false
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.setup(move |app| {
|
||||
|
||||
// Get the main window:
|
||||
let window = app.get_window("main").expect("Failed to get main window.");
|
||||
let window = app.get_webview_window("main").expect("Failed to get main window.");
|
||||
|
||||
// Register a callback for window events, such as file drops. We have to use
|
||||
// this handler in addition to the app event handler, because file drop events
|
||||
@@ -100,27 +130,27 @@ pub fn start_tauri() {
|
||||
*MAIN_WINDOW.lock().unwrap() = Some(window);
|
||||
|
||||
info!(Source = "Bootloader Tauri"; "Setup is running.");
|
||||
let data_path = app.path_resolver().app_local_data_dir().unwrap();
|
||||
let data_path = app.path().app_local_data_dir().unwrap();
|
||||
let data_path = data_path.join("data");
|
||||
|
||||
// Get and store the data and config directories:
|
||||
DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the data directory.")).unwrap();
|
||||
CONFIG_DIRECTORY.set(app.path_resolver().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap();
|
||||
CONFIG_DIRECTORY.set(app.path().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap();
|
||||
|
||||
if is_dev() {
|
||||
#[cfg(debug_assertions)]
|
||||
create_startup_env_file();
|
||||
} else {
|
||||
cleanup_dotnet_server();
|
||||
start_dotnet_server();
|
||||
start_dotnet_server(app.handle().clone());
|
||||
}
|
||||
|
||||
cleanup_qdrant();
|
||||
start_qdrant_server(app.path_resolver());
|
||||
start_qdrant_server(app.handle().clone());
|
||||
|
||||
info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {data_path:?}");
|
||||
switch_to_file_logging(data_path).map_err(|e| error!("Failed to switch logging to file: {e}")).unwrap();
|
||||
set_pdfium_path(app.path_resolver());
|
||||
set_pdfium_path(app.path());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -129,7 +159,7 @@ pub fn start_tauri() {
|
||||
.expect("Error while running Tauri application");
|
||||
|
||||
// The app event handler:
|
||||
app.run(|app_handle, event| {
|
||||
app.run(|_app_handle, event| {
|
||||
if !matches!(event, RunEvent::MainEventsCleared) {
|
||||
debug!(Source = "Tauri"; "Tauri event received: location=app event handler , event={event:?}");
|
||||
}
|
||||
@@ -149,54 +179,6 @@ pub fn start_tauri() {
|
||||
}
|
||||
}
|
||||
|
||||
RunEvent::Updater(updater_event) => {
|
||||
match updater_event {
|
||||
UpdaterEvent::UpdateAvailable { body, date, version } => {
|
||||
let body_len = body.len();
|
||||
info!(Source = "Tauri"; "Updater: update available: body size={body_len} time={date:?} version={version}");
|
||||
}
|
||||
|
||||
UpdaterEvent::Pending => {
|
||||
info!(Source = "Tauri"; "Updater: update is pending!");
|
||||
}
|
||||
|
||||
UpdaterEvent::DownloadProgress { chunk_length, content_length: _ } => {
|
||||
trace!(Source = "Tauri"; "Updater: downloading chunk of {chunk_length} bytes");
|
||||
}
|
||||
|
||||
UpdaterEvent::Downloaded => {
|
||||
info!(Source = "Tauri"; "Updater: update has been downloaded!");
|
||||
warn!(Source = "Tauri"; "Try to stop the .NET server now...");
|
||||
|
||||
if is_prod() {
|
||||
stop_dotnet_server();
|
||||
stop_qdrant_server();
|
||||
} else {
|
||||
warn!(Source = "Tauri"; "Development environment detected; do not stop the .NET server.");
|
||||
}
|
||||
}
|
||||
|
||||
UpdaterEvent::Updated => {
|
||||
info!(Source = "Tauri"; "Updater: app has been updated");
|
||||
warn!(Source = "Tauri"; "Try to restart the app now...");
|
||||
|
||||
if is_prod() {
|
||||
app_handle.restart();
|
||||
} else {
|
||||
warn!(Source = "Tauri"; "Development environment detected; do not restart the app.");
|
||||
}
|
||||
}
|
||||
|
||||
UpdaterEvent::AlreadyUpToDate => {
|
||||
info!(Source = "Tauri"; "Updater: app is already up to date");
|
||||
}
|
||||
|
||||
UpdaterEvent::Error(error) => {
|
||||
warn!(Source = "Tauri"; "Updater: failed to update: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RunEvent::ExitRequested { .. } => {
|
||||
warn!(Source = "Tauri"; "Run event: exit was requested.");
|
||||
stop_qdrant_server();
|
||||
@@ -217,6 +199,46 @@ pub fn start_tauri() {
|
||||
warn!(Source = "Tauri"; "Tauri app was stopped.");
|
||||
}
|
||||
|
||||
fn is_local_host(host: Option<&str>) -> bool {
|
||||
matches!(host, Some("localhost") | Some("127.0.0.1") | Some("::1") | Some("[::1]"))
|
||||
}
|
||||
|
||||
fn is_local_http_url(url: &tauri::Url) -> bool {
|
||||
matches!(url.scheme(), "http" | "https") && is_local_host(url.host_str())
|
||||
}
|
||||
|
||||
fn same_origin(left: &tauri::Url, right: &tauri::Url) -> bool {
|
||||
left.scheme() == right.scheme()
|
||||
&& left.host_str() == right.host_str()
|
||||
&& left.port_or_known_default() == right.port_or_known_default()
|
||||
}
|
||||
|
||||
fn should_open_in_system_browser<R: tauri::Runtime>(webview: &tauri::Webview<R>, url: &tauri::Url) -> bool {
|
||||
match url.scheme() {
|
||||
"mailto" | "tel" => return true,
|
||||
"http" | "https" => {},
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
if let Some(approved_app_url) = APPROVED_APP_URL.lock().unwrap().as_ref() {
|
||||
if same_origin(approved_app_url, url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if is_local_http_url(url) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(current_url) = webview.url() {
|
||||
if same_origin(¤t_url, url) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
!is_local_host(url.host_str())
|
||||
}
|
||||
|
||||
/// Our event API endpoint for Tauri events. We try to send an endless stream of events to the client.
|
||||
/// If no events are available for a certain time, we send a ping event to keep the connection alive.
|
||||
/// When the client disconnects, the stream is closed. But we try to not lose events in between.
|
||||
@@ -303,23 +325,21 @@ impl Event {
|
||||
/// Creates an Event instance from a Tauri WindowEvent.
|
||||
pub fn from_window_event(window_event: &WindowEvent) -> Self {
|
||||
match window_event {
|
||||
WindowEvent::FileDrop(drop_event) => {
|
||||
WindowEvent::DragDrop(drop_event) => {
|
||||
match drop_event {
|
||||
FileDropEvent::Hovered(files) => Event::new(TauriEventType::FileDropHovered,
|
||||
files.iter().map(|f| f.to_string_lossy().to_string()).collect(),
|
||||
DragDropEvent::Enter { paths, .. } => Event::new(
|
||||
TauriEventType::FileDropHovered,
|
||||
paths.iter().map(|p| p.display().to_string()).collect(),
|
||||
),
|
||||
|
||||
FileDropEvent::Dropped(files) => Event::new(TauriEventType::FileDropDropped,
|
||||
files.iter().map(|f| f.to_string_lossy().to_string()).collect(),
|
||||
DragDropEvent::Drop { paths, .. } => Event::new(
|
||||
TauriEventType::FileDropDropped,
|
||||
paths.iter().map(|p| p.display().to_string()).collect(),
|
||||
),
|
||||
|
||||
FileDropEvent::Cancelled => Event::new(TauriEventType::FileDropCanceled,
|
||||
Vec::new(),
|
||||
),
|
||||
DragDropEvent::Leave => Event::new(TauriEventType::FileDropCanceled, Vec::new()),
|
||||
|
||||
_ => Event::new(TauriEventType::Unknown,
|
||||
Vec::new(),
|
||||
),
|
||||
_ => Event::new(TauriEventType::Unknown, Vec::new()),
|
||||
}
|
||||
},
|
||||
|
||||
@@ -380,6 +400,12 @@ pub async fn change_location_to(url: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(parsed_url) = tauri::Url::parse(url) {
|
||||
if is_local_http_url(&parsed_url) {
|
||||
*APPROVED_APP_URL.lock().unwrap() = Some(parsed_url);
|
||||
}
|
||||
}
|
||||
|
||||
let js_location_change = format!("window.location = '{url}';");
|
||||
let main_window = main_window_spawn_clone.lock().unwrap();
|
||||
let location_change_result = main_window.as_ref().unwrap().eval(js_location_change.as_str());
|
||||
@@ -402,46 +428,67 @@ pub async fn check_for_update(_token: APIToken) -> Json<CheckUpdateResponse> {
|
||||
});
|
||||
}
|
||||
|
||||
let app_handle = MAIN_WINDOW.lock().unwrap().as_ref().unwrap().app_handle();
|
||||
let response = app_handle.updater().check().await;
|
||||
match response {
|
||||
Ok(update_response) => match update_response.is_update_available() {
|
||||
true => {
|
||||
*CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update_response.clone());
|
||||
let new_version = update_response.latest_version();
|
||||
info!(Source = "Updater"; "An update to version '{new_version}' is available.");
|
||||
let changelog = update_response.body();
|
||||
Json(CheckUpdateResponse {
|
||||
update_is_available: true,
|
||||
error: false,
|
||||
new_version: new_version.to_string(),
|
||||
changelog: match changelog {
|
||||
Some(c) => c.to_string(),
|
||||
None => String::from(""),
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
false => {
|
||||
info!(Source = "Updater"; "No updates are available.");
|
||||
Json(CheckUpdateResponse {
|
||||
let app_handle = {
|
||||
let main_window = MAIN_WINDOW.lock().unwrap();
|
||||
match main_window.as_ref() {
|
||||
Some(window) => window.app_handle().clone(),
|
||||
None => {
|
||||
error!(Source = "Updater"; "Cannot check updates: main window not available.");
|
||||
return Json(CheckUpdateResponse {
|
||||
update_is_available: false,
|
||||
error: false,
|
||||
error: true,
|
||||
new_version: String::from(""),
|
||||
changelog: String::from(""),
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
let response = match app_handle.updater() {
|
||||
Ok(updater) => updater.check().await,
|
||||
Err(e) => {
|
||||
warn!(Source = "Updater"; "Failed to check for updates: {e}.");
|
||||
warn!(Source = "Updater"; "Failed to get updater instance: {e}");
|
||||
return Json(CheckUpdateResponse {
|
||||
update_is_available: false,
|
||||
error: true,
|
||||
new_version: String::from(""),
|
||||
changelog: String::from(""),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
match response {
|
||||
Ok(Some(update)) => {
|
||||
let body_len = update.body.as_ref().map_or(0, |body| body.len());
|
||||
let date = update.date;
|
||||
let new_version = update.version.clone();
|
||||
info!(Source = "Tauri"; "Updater: update available: body size={body_len} time={date:?} version={new_version}");
|
||||
let changelog = update.body.clone().unwrap_or_default();
|
||||
*CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update);
|
||||
Json(CheckUpdateResponse {
|
||||
update_is_available: true,
|
||||
error: false,
|
||||
new_version,
|
||||
changelog,
|
||||
})
|
||||
}
|
||||
Ok(None) => {
|
||||
info!(Source = "Tauri"; "Updater: app is already up to date");
|
||||
Json(CheckUpdateResponse {
|
||||
update_is_available: false,
|
||||
error: false,
|
||||
new_version: String::from(""),
|
||||
changelog: String::from(""),
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(Source = "Tauri"; "Updater: failed to update: {e}");
|
||||
Json(CheckUpdateResponse {
|
||||
update_is_available: false,
|
||||
error: true,
|
||||
new_version: String::from(""),
|
||||
changelog: String::from(""),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,9 +510,51 @@ pub async fn install_update(_token: APIToken) {
|
||||
}
|
||||
|
||||
let cloned_response_option = CHECK_UPDATE_RESPONSE.lock().unwrap().clone();
|
||||
let app_handle = MAIN_WINDOW
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|window| window.app_handle().clone());
|
||||
|
||||
match cloned_response_option {
|
||||
Some(update_response) => {
|
||||
update_response.download_and_install().await.unwrap();
|
||||
info!(Source = "Tauri"; "Updater: update is pending!");
|
||||
let result = update_response.download_and_install(
|
||||
|chunk_length, _content_length| {
|
||||
trace!(Source = "Tauri"; "Updater: downloading chunk of {chunk_length} bytes");
|
||||
},
|
||||
|| {
|
||||
info!(Source = "Tauri"; "Updater: update has been downloaded!");
|
||||
warn!(Source = "Tauri"; "Try to stop the .NET server now...");
|
||||
|
||||
if is_prod() {
|
||||
stop_dotnet_server();
|
||||
stop_qdrant_server();
|
||||
} else {
|
||||
warn!(Source = "Tauri"; "Development environment detected; do not stop the .NET server.");
|
||||
}
|
||||
},
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
info!(Source = "Tauri"; "Updater: app has been updated");
|
||||
warn!(Source = "Tauri"; "Try to restart the app now...");
|
||||
|
||||
if is_prod() {
|
||||
if let Some(handle) = app_handle {
|
||||
handle.restart();
|
||||
} else {
|
||||
warn!(Source = "Tauri"; "Cannot restart after update: main window not available.");
|
||||
}
|
||||
} else {
|
||||
warn!(Source = "Tauri"; "Development environment detected; do not restart the app.");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(Source = "Tauri"; "Updater: failed to update: {e}");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
None => {
|
||||
@@ -474,269 +563,6 @@ pub async fn install_update(_token: APIToken) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Let the user select a directory.
|
||||
#[post("/select/directory?<title>", data = "<previous_directory>")]
|
||||
pub fn select_directory(
|
||||
_token: APIToken,
|
||||
title: &str,
|
||||
previous_directory: Option<Json<PreviousDirectory>>,
|
||||
) -> Json<DirectorySelectionResponse> {
|
||||
let folder_path = match previous_directory {
|
||||
Some(previous) => {
|
||||
let previous_path = previous.path.as_str();
|
||||
create_file_dialog()
|
||||
.set_title(title)
|
||||
.set_directory(previous_path)
|
||||
.pick_folder()
|
||||
},
|
||||
|
||||
None => create_file_dialog().set_title(title).pick_folder(),
|
||||
};
|
||||
|
||||
match folder_path {
|
||||
Some(path) => {
|
||||
info!("User selected directory: {path:?}");
|
||||
Json(DirectorySelectionResponse {
|
||||
user_cancelled: false,
|
||||
selected_directory: path.to_str().unwrap().to_string(),
|
||||
})
|
||||
},
|
||||
|
||||
None => {
|
||||
info!("User cancelled directory selection.");
|
||||
Json(DirectorySelectionResponse {
|
||||
user_cancelled: true,
|
||||
selected_directory: String::from(""),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct PreviousDirectory {
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct FileTypeFilter {
|
||||
filter_name: String,
|
||||
filter_extensions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct SelectFileOptions {
|
||||
title: String,
|
||||
previous_file: Option<PreviousFile>,
|
||||
filter: Option<FileTypeFilter>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct SaveFileOptions {
|
||||
title: String,
|
||||
name_file: Option<PreviousFile>,
|
||||
filter: Option<FileTypeFilter>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DirectorySelectionResponse {
|
||||
user_cancelled: bool,
|
||||
selected_directory: String,
|
||||
}
|
||||
|
||||
/// Let the user select a file.
|
||||
#[post("/select/file", data = "<payload>")]
|
||||
pub fn select_file(
|
||||
_token: APIToken,
|
||||
payload: Json<SelectFileOptions>,
|
||||
) -> Json<FileSelectionResponse> {
|
||||
// Create a new file dialog builder:
|
||||
let file_dialog = create_file_dialog();
|
||||
|
||||
// Set the title of the file dialog:
|
||||
let file_dialog = file_dialog.set_title(&payload.title);
|
||||
|
||||
// Set the file type filter if provided:
|
||||
let file_dialog = apply_filter(file_dialog, &payload.filter);
|
||||
|
||||
// Set the previous file path if provided:
|
||||
let file_dialog = match &payload.previous_file {
|
||||
Some(previous) => {
|
||||
let previous_path = previous.file_path.as_str();
|
||||
file_dialog.set_directory(previous_path)
|
||||
},
|
||||
|
||||
None => file_dialog,
|
||||
};
|
||||
|
||||
// Show the file dialog and get the selected file path:
|
||||
let file_path = file_dialog.pick_file();
|
||||
match file_path {
|
||||
Some(path) => {
|
||||
info!("User selected file: {path:?}");
|
||||
Json(FileSelectionResponse {
|
||||
user_cancelled: false,
|
||||
selected_file_path: path.to_str().unwrap().to_string(),
|
||||
})
|
||||
},
|
||||
|
||||
None => {
|
||||
info!("User cancelled file selection.");
|
||||
Json(FileSelectionResponse {
|
||||
user_cancelled: true,
|
||||
selected_file_path: String::from(""),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Let the user select some files.
|
||||
#[post("/select/files", data = "<payload>")]
|
||||
pub fn select_files(
|
||||
_token: APIToken,
|
||||
payload: Json<SelectFileOptions>,
|
||||
) -> Json<FilesSelectionResponse> {
|
||||
// Create a new file dialog builder:
|
||||
let file_dialog = create_file_dialog();
|
||||
|
||||
// Set the title of the file dialog:
|
||||
let file_dialog = file_dialog.set_title(&payload.title);
|
||||
|
||||
// Set the file type filter if provided:
|
||||
let file_dialog = apply_filter(file_dialog, &payload.filter);
|
||||
|
||||
// Set the previous file path if provided:
|
||||
let file_dialog = match &payload.previous_file {
|
||||
Some(previous) => {
|
||||
let previous_path = previous.file_path.as_str();
|
||||
file_dialog.set_directory(previous_path)
|
||||
},
|
||||
|
||||
None => file_dialog,
|
||||
};
|
||||
|
||||
// Show the file dialog and get the selected file path:
|
||||
let file_paths = file_dialog.pick_files();
|
||||
match file_paths {
|
||||
Some(paths) => {
|
||||
info!("User selected {} files.", paths.len());
|
||||
Json(FilesSelectionResponse {
|
||||
user_cancelled: false,
|
||||
selected_file_paths: paths
|
||||
.iter()
|
||||
.map(|p| p.to_str().unwrap().to_string())
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
None => {
|
||||
info!("User cancelled file selection.");
|
||||
Json(FilesSelectionResponse {
|
||||
user_cancelled: true,
|
||||
selected_file_paths: Vec::new(),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/save/file", data = "<payload>")]
|
||||
pub fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json<FileSaveResponse> {
|
||||
// Create a new file dialog builder:
|
||||
let file_dialog = create_file_dialog();
|
||||
|
||||
// Set the title of the file dialog:
|
||||
let file_dialog = file_dialog.set_title(&payload.title);
|
||||
|
||||
// Set the file type filter if provided:
|
||||
let file_dialog = apply_filter(file_dialog, &payload.filter);
|
||||
|
||||
// Set the previous file path if provided:
|
||||
let file_dialog = match &payload.name_file {
|
||||
Some(previous) => {
|
||||
let previous_path = previous.file_path.as_str();
|
||||
file_dialog.set_directory(previous_path)
|
||||
},
|
||||
|
||||
None => file_dialog,
|
||||
};
|
||||
|
||||
// Displays the file dialogue box and select the file:
|
||||
let file_path = file_dialog.save_file();
|
||||
match file_path {
|
||||
Some(path) => {
|
||||
info!("User selected file for writing operation: {path:?}");
|
||||
Json(FileSaveResponse {
|
||||
user_cancelled: false,
|
||||
save_file_path: path.to_str().unwrap().to_string(),
|
||||
})
|
||||
},
|
||||
|
||||
None => {
|
||||
info!("User cancelled file selection.");
|
||||
Json(FileSaveResponse {
|
||||
user_cancelled: true,
|
||||
save_file_path: String::from(""),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct PreviousFile {
|
||||
file_path: String,
|
||||
}
|
||||
|
||||
/// Creates a file dialog builder and assigns the main window as parent where supported.
|
||||
fn create_file_dialog() -> FileDialogBuilder {
|
||||
let file_dialog = FileDialogBuilder::new();
|
||||
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
{
|
||||
let main_window_lock = MAIN_WINDOW.lock().unwrap();
|
||||
match main_window_lock.as_ref() {
|
||||
Some(window) => file_dialog.set_parent(window),
|
||||
None => {
|
||||
warn!(Source = "Tauri"; "Cannot assign parent window to file dialog: main window not available.");
|
||||
file_dialog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(windows, target_os = "macos")))]
|
||||
{
|
||||
file_dialog
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies an optional file type filter to a FileDialogBuilder.
|
||||
fn apply_filter(file_dialog: FileDialogBuilder, filter: &Option<FileTypeFilter>) -> FileDialogBuilder {
|
||||
match filter {
|
||||
Some(f) => file_dialog.add_filter(
|
||||
&f.filter_name,
|
||||
&f.filter_extensions.iter().map(|s| s.as_str()).collect::<Vec<&str>>(),
|
||||
),
|
||||
|
||||
None => file_dialog,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct FileSelectionResponse {
|
||||
user_cancelled: bool,
|
||||
selected_file_path: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct FilesSelectionResponse {
|
||||
user_cancelled: bool,
|
||||
selected_file_paths: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct FileSaveResponse {
|
||||
user_cancelled: bool,
|
||||
save_file_path: String,
|
||||
}
|
||||
|
||||
/// Request payload for registering a global shortcut.
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct RegisterShortcutRequest {
|
||||
@@ -765,47 +591,42 @@ pub struct AppExitResponse {
|
||||
/// Internal helper function to register a shortcut with its callback.
|
||||
/// This is used by both `register_shortcut` and `resume_shortcuts` to
|
||||
/// avoid code duplication.
|
||||
fn register_shortcut_with_callback(
|
||||
shortcut_manager: &mut impl GlobalShortcutManager,
|
||||
fn register_shortcut_with_callback<R: tauri::Runtime>(
|
||||
app_handle: &tauri::AppHandle<R>,
|
||||
shortcut: &str,
|
||||
shortcut_id: Shortcut,
|
||||
event_sender: broadcast::Sender<Event>,
|
||||
) -> Result<(), tauri::Error> {
|
||||
//
|
||||
// Match the shortcut registration to transform the Tauri result into the Rust result:
|
||||
//
|
||||
match shortcut_manager.register(shortcut, move || {
|
||||
) -> Result<(), tauri_plugin_global_shortcut::Error> {
|
||||
let shortcut_manager = app_handle.global_shortcut();
|
||||
shortcut_manager.on_shortcut(shortcut, move |_app, _shortcut, _event| {
|
||||
info!(Source = "Tauri"; "Global shortcut triggered for '{}'.", shortcut_id);
|
||||
let event = Event::new(TauriEventType::GlobalShortcutPressed, vec![shortcut_id.to_string()]);
|
||||
let sender = event_sender.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match sender.send(event) {
|
||||
Ok(_) => {}
|
||||
Err(error) => error!(Source = "Tauri"; "Failed to send global shortcut event: {error}"),
|
||||
if let Err(error) = sender.send(event) {
|
||||
error!(Source = "Tauri"; "Failed to send global shortcut event: {error}");
|
||||
}
|
||||
});
|
||||
}) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Requests a controlled shutdown of the entire desktop application.
|
||||
#[post("/app/exit")]
|
||||
pub fn exit_app(_token: APIToken) -> Json<AppExitResponse> {
|
||||
let main_window_lock = MAIN_WINDOW.lock().unwrap();
|
||||
let main_window = match main_window_lock.as_ref() {
|
||||
Some(window) => window,
|
||||
None => {
|
||||
error!(Source = "Tauri"; "Cannot exit app: main window not available.");
|
||||
return Json(AppExitResponse {
|
||||
success: false,
|
||||
error_message: "Main window not available".to_string(),
|
||||
});
|
||||
let app_handle = {
|
||||
let main_window_lock = MAIN_WINDOW.lock().unwrap();
|
||||
match main_window_lock.as_ref() {
|
||||
Some(window) => window.app_handle().clone(),
|
||||
None => {
|
||||
error!(Source = "Tauri"; "Cannot exit app: main window not available.");
|
||||
return Json(AppExitResponse {
|
||||
success: false,
|
||||
error_message: "Main window not available".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let app_handle = main_window.app_handle();
|
||||
info!(Source = "Tauri"; "Controlled app exit was requested by the UI.");
|
||||
tauri::async_runtime::spawn(async move {
|
||||
time::sleep(Duration::from_millis(50)).await;
|
||||
@@ -848,7 +669,8 @@ pub fn register_shortcut(_token: APIToken, payload: Json<RegisterShortcutRequest
|
||||
}
|
||||
};
|
||||
|
||||
let mut shortcut_manager = main_window.app_handle().global_shortcut_manager();
|
||||
let app_handle = main_window.app_handle();
|
||||
let shortcut_manager = app_handle.global_shortcut();
|
||||
let mut registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap();
|
||||
|
||||
// Unregister the old shortcut if one exists for this name:
|
||||
@@ -887,7 +709,7 @@ pub fn register_shortcut(_token: APIToken, payload: Json<RegisterShortcutRequest
|
||||
drop(event_broadcast_lock);
|
||||
|
||||
// Register the new shortcut:
|
||||
match register_shortcut_with_callback(&mut shortcut_manager, &new_shortcut, id, event_sender) {
|
||||
match register_shortcut_with_callback(app_handle, &new_shortcut, id, event_sender) {
|
||||
Ok(_) => {
|
||||
info!(Source = "Tauri"; "Global shortcut '{new_shortcut}' registered successfully for '{}'.", id);
|
||||
registered_shortcuts.insert(id, new_shortcut);
|
||||
@@ -997,7 +819,8 @@ pub fn suspend_shortcuts(_token: APIToken) -> Json<ShortcutResponse> {
|
||||
}
|
||||
};
|
||||
|
||||
let mut shortcut_manager = main_window.app_handle().global_shortcut_manager();
|
||||
let app_handle = main_window.app_handle();
|
||||
let shortcut_manager = app_handle.global_shortcut();
|
||||
let registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap();
|
||||
|
||||
// Unregister all shortcuts from the OS (but keep them in our map):
|
||||
@@ -1033,7 +856,7 @@ pub fn resume_shortcuts(_token: APIToken) -> Json<ShortcutResponse> {
|
||||
}
|
||||
};
|
||||
|
||||
let mut shortcut_manager = main_window.app_handle().global_shortcut_manager();
|
||||
let app_handle = main_window.app_handle();
|
||||
let registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap();
|
||||
|
||||
// Get the event broadcast sender for the shortcut callbacks:
|
||||
@@ -1058,7 +881,7 @@ pub fn resume_shortcuts(_token: APIToken) -> Json<ShortcutResponse> {
|
||||
continue;
|
||||
}
|
||||
|
||||
match register_shortcut_with_callback(&mut shortcut_manager, shortcut, *shortcut_id, event_sender.clone()) {
|
||||
match register_shortcut_with_callback(&app_handle, shortcut, *shortcut_id, event_sender.clone()) {
|
||||
Ok(_) => {
|
||||
info!(Source = "Tauri"; "Re-registered shortcut '{shortcut}' for '{}'.", shortcut_id);
|
||||
success_count += 1;
|
||||
@@ -1119,15 +942,31 @@ fn validate_shortcut_syntax(shortcut: &str) -> bool {
|
||||
has_key
|
||||
}
|
||||
|
||||
fn set_pdfium_path(path_resolver: PathResolver) {
|
||||
let pdfium_relative_source_path = String::from("resources/libraries/");
|
||||
let pdfium_source_path = path_resolver.resolve_resource(pdfium_relative_source_path);
|
||||
if pdfium_source_path.is_none() {
|
||||
error!(Source = "Bootloader Tauri"; "Failed to set the PDFium library path.");
|
||||
return;
|
||||
}
|
||||
fn set_pdfium_path<R: tauri::Runtime>(path_resolver: &PathResolver<R>) {
|
||||
let resource_dir = match path_resolver.resource_dir() {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
error!(Source = "Bootloader Tauri"; "Failed to resolve resource dir: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let pdfium_source_path = pdfium_source_path.unwrap();
|
||||
let pdfium_source_path = pdfium_source_path.to_str().unwrap().to_string();
|
||||
*PDFIUM_LIB_PATH.lock().unwrap() = Some(pdfium_source_path.clone());
|
||||
let candidate_paths = [
|
||||
resource_dir.join("resources").join("libraries"),
|
||||
resource_dir.join("libraries"),
|
||||
];
|
||||
|
||||
let pdfium_source_path = candidate_paths
|
||||
.iter()
|
||||
.find(|path| path.exists())
|
||||
.map(|path| path.to_string_lossy().to_string());
|
||||
|
||||
match pdfium_source_path {
|
||||
Some(path) => {
|
||||
*PDFIUM_LIB_PATH.lock().unwrap() = Some(path);
|
||||
}
|
||||
None => {
|
||||
error!(Source = "Bootloader Tauri"; "Failed to set the PDFium library path.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-12
@@ -6,8 +6,9 @@ use base64::prelude::BASE64_STANDARD;
|
||||
use log::{error, info, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use rocket::get;
|
||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
||||
use tauri::Url;
|
||||
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use crate::api_token::APIToken;
|
||||
use crate::runtime_api_token::API_TOKEN;
|
||||
use crate::app_window::change_location_to;
|
||||
@@ -130,14 +131,14 @@ pub fn create_startup_env_file() {
|
||||
}
|
||||
|
||||
/// Starts the .NET server in a separate process.
|
||||
pub fn start_dotnet_server() {
|
||||
pub fn start_dotnet_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>) {
|
||||
|
||||
// Get the secret password & salt and convert it to a base64 string:
|
||||
let secret_password = BASE64_STANDARD.encode(ENCRYPTION.secret_password);
|
||||
let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt);
|
||||
let api_port = *API_SERVER_PORT;
|
||||
|
||||
let dotnet_server_environment = HashMap::from_iter([
|
||||
let dotnet_server_environment: HashMap<String, String> = HashMap::from_iter([
|
||||
(String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password),
|
||||
(String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt),
|
||||
(String::from("AI_STUDIO_CERTIFICATE_FINGERPRINT"), CERTIFICATE_FINGERPRINT.get().unwrap().to_string()),
|
||||
@@ -148,11 +149,13 @@ pub fn start_dotnet_server() {
|
||||
info!("Try to start the .NET server...");
|
||||
let server_spawn_clone = DOTNET_SERVER.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let (mut rx, child) = Command::new_sidecar("mindworkAIStudioServer")
|
||||
.expect("Failed to create sidecar")
|
||||
.envs(dotnet_server_environment)
|
||||
.spawn()
|
||||
.expect("Failed to spawn .NET server process.");
|
||||
let shell = app_handle.shell();
|
||||
let (mut rx, child) = shell
|
||||
.sidecar("mindworkAIStudioServer")
|
||||
.expect("Failed to create sidecar")
|
||||
.envs(dotnet_server_environment)
|
||||
.spawn()
|
||||
.expect("Failed to spawn .NET server process.");
|
||||
let server_pid = child.pid();
|
||||
info!(Source = "Bootloader .NET"; "The .NET server process started with PID={server_pid}.");
|
||||
log_potential_stale_process(Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME), server_pid, SIDECAR_TYPE);
|
||||
@@ -163,10 +166,13 @@ pub fn start_dotnet_server() {
|
||||
// Log the output of the .NET server:
|
||||
// NOTE: Log events are sent via structured HTTP API calls.
|
||||
// This loop serves for fundamental output (e.g., startup errors).
|
||||
while let Some(CommandEvent::Stdout(line)) = rx.recv().await {
|
||||
let line = sanitize_stdout_line(line.trim_end());
|
||||
if !line.trim().is_empty() {
|
||||
info!(Source = ".NET Server (stdout)"; "{line}");
|
||||
while let Some(event) = rx.recv().await {
|
||||
if let CommandEvent::Stdout(line) = event {
|
||||
let line_utf8 = String::from_utf8_lossy(&line).to_string();
|
||||
let line = sanitize_stdout_line(line_utf8.trim_end());
|
||||
if !line.trim().is_empty() {
|
||||
info!(Source = ".NET Server (stdout)"; "{line}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
use log::{error, info};
|
||||
use rocket::post;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::serde::json::Json;
|
||||
use tauri_plugin_dialog::{DialogExt, FileDialogBuilder};
|
||||
use crate::api_token::APIToken;
|
||||
use crate::app_window::MAIN_WINDOW;
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct PreviousDirectory {
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct FileTypeFilter {
|
||||
filter_name: String,
|
||||
filter_extensions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct SelectFileOptions {
|
||||
title: String,
|
||||
previous_file: Option<PreviousFile>,
|
||||
filter: Option<FileTypeFilter>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct SaveFileOptions {
|
||||
title: String,
|
||||
name_file: Option<PreviousFile>,
|
||||
filter: Option<FileTypeFilter>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DirectorySelectionResponse {
|
||||
user_cancelled: bool,
|
||||
selected_directory: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct FileSelectionResponse {
|
||||
user_cancelled: bool,
|
||||
selected_file_path: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct FilesSelectionResponse {
|
||||
user_cancelled: bool,
|
||||
selected_file_paths: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct FileSaveResponse {
|
||||
user_cancelled: bool,
|
||||
save_file_path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct PreviousFile {
|
||||
file_path: String,
|
||||
}
|
||||
|
||||
/// Let the user select a directory.
|
||||
#[post("/select/directory?<title>", data = "<previous_directory>")]
|
||||
pub fn select_directory(
|
||||
_token: APIToken,
|
||||
title: &str,
|
||||
previous_directory: Option<Json<PreviousDirectory>>,
|
||||
) -> Json<DirectorySelectionResponse> {
|
||||
let main_window_lock = MAIN_WINDOW.lock().unwrap();
|
||||
let main_window = match main_window_lock.as_ref() {
|
||||
Some(window) => window,
|
||||
None => {
|
||||
error!(Source = "Tauri"; "Cannot open directory dialog: main window not available.");
|
||||
return Json(DirectorySelectionResponse {
|
||||
user_cancelled: true,
|
||||
selected_directory: String::from(""),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let mut dialog = main_window.dialog().file().set_parent(main_window).set_title(title);
|
||||
if let Some(previous) = previous_directory {
|
||||
dialog = dialog.set_directory(previous.path.clone());
|
||||
}
|
||||
|
||||
drop(main_window_lock);
|
||||
|
||||
let folder_path = dialog.blocking_pick_folder();
|
||||
match folder_path {
|
||||
Some(path) => {
|
||||
match path.into_path() {
|
||||
Ok(pb) => {
|
||||
info!("User selected directory: {pb:?}");
|
||||
Json(DirectorySelectionResponse {
|
||||
user_cancelled: false,
|
||||
selected_directory: pb.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
error!(Source = "Tauri"; "Failed to convert directory path: {e}");
|
||||
Json(DirectorySelectionResponse {
|
||||
user_cancelled: true,
|
||||
selected_directory: String::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
None => {
|
||||
info!("User cancelled directory selection.");
|
||||
Json(DirectorySelectionResponse {
|
||||
user_cancelled: true,
|
||||
selected_directory: String::from(""),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Let the user select a file.
|
||||
#[post("/select/file", data = "<payload>")]
|
||||
pub fn select_file(
|
||||
_token: APIToken,
|
||||
payload: Json<SelectFileOptions>,
|
||||
) -> Json<FileSelectionResponse> {
|
||||
// Create a new file dialog builder:
|
||||
let file_dialog = MAIN_WINDOW
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|w| w.dialog().file().set_parent(w).set_title(&payload.title));
|
||||
|
||||
let Some(mut file_dialog) = file_dialog else {
|
||||
error!(Source = "Tauri"; "Cannot open file dialog: main window not available.");
|
||||
return Json(FileSelectionResponse {
|
||||
user_cancelled: true,
|
||||
selected_file_path: String::from(""),
|
||||
});
|
||||
};
|
||||
|
||||
// Set the file type filter if provided:
|
||||
file_dialog = apply_filter(file_dialog, &payload.filter);
|
||||
|
||||
// Set the previous file path if provided:
|
||||
if let Some(previous) = &payload.previous_file {
|
||||
let previous_path = previous.file_path.as_str();
|
||||
file_dialog = file_dialog.set_directory(previous_path);
|
||||
}
|
||||
|
||||
// Show the file dialog and get the selected file path:
|
||||
let file_path = file_dialog.blocking_pick_file();
|
||||
match file_path {
|
||||
Some(path) => match path.into_path() {
|
||||
Ok(pb) => {
|
||||
info!("User selected file: {pb:?}");
|
||||
Json(FileSelectionResponse {
|
||||
user_cancelled: false,
|
||||
selected_file_path: pb.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
error!(Source = "Tauri"; "Failed to convert file path: {e}");
|
||||
Json(FileSelectionResponse {
|
||||
user_cancelled: true,
|
||||
selected_file_path: String::new(),
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
None => {
|
||||
info!("User cancelled file selection.");
|
||||
Json(FileSelectionResponse {
|
||||
user_cancelled: true,
|
||||
selected_file_path: String::from(""),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Let the user select some files.
|
||||
#[post("/select/files", data = "<payload>")]
|
||||
pub fn select_files(
|
||||
_token: APIToken,
|
||||
payload: Json<SelectFileOptions>,
|
||||
) -> Json<FilesSelectionResponse> {
|
||||
// Create a new file dialog builder:
|
||||
let file_dialog = MAIN_WINDOW
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|w| w.dialog().file().set_parent(w).set_title(&payload.title));
|
||||
|
||||
let Some(mut file_dialog) = file_dialog else {
|
||||
error!(Source = "Tauri"; "Cannot open file dialog: main window not available.");
|
||||
return Json(FilesSelectionResponse {
|
||||
user_cancelled: true,
|
||||
selected_file_paths: Vec::new(),
|
||||
});
|
||||
};
|
||||
|
||||
// Set the file type filter if provided:
|
||||
file_dialog = apply_filter(file_dialog, &payload.filter);
|
||||
|
||||
// Set the previous file path if provided:
|
||||
if let Some(previous) = &payload.previous_file {
|
||||
let previous_path = previous.file_path.as_str();
|
||||
file_dialog = file_dialog.set_directory(previous_path);
|
||||
}
|
||||
|
||||
// Show the file dialog and get the selected file path:
|
||||
let file_paths = file_dialog.blocking_pick_files();
|
||||
match file_paths {
|
||||
Some(paths) => {
|
||||
let converted: Vec<String> = paths.into_iter().filter_map(|p| p.into_path().ok()).map(|pb| pb.to_string_lossy().to_string()).collect();
|
||||
info!("User selected {} files.", converted.len());
|
||||
Json(FilesSelectionResponse {
|
||||
user_cancelled: false,
|
||||
selected_file_paths: converted,
|
||||
})
|
||||
}
|
||||
|
||||
None => {
|
||||
info!("User cancelled file selection.");
|
||||
Json(FilesSelectionResponse {
|
||||
user_cancelled: true,
|
||||
selected_file_paths: Vec::new(),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/save/file", data = "<payload>")]
|
||||
pub fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json<FileSaveResponse> {
|
||||
// Create a new file dialog builder:
|
||||
let file_dialog = MAIN_WINDOW
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|w| w.dialog().file().set_parent(w).set_title(&payload.title));
|
||||
|
||||
let Some(mut file_dialog) = file_dialog else {
|
||||
error!(Source = "Tauri"; "Cannot open save dialog: main window not available.");
|
||||
return Json(FileSaveResponse {
|
||||
user_cancelled: true,
|
||||
save_file_path: String::from(""),
|
||||
});
|
||||
};
|
||||
|
||||
// Set the file type filter if provided:
|
||||
file_dialog = apply_filter(file_dialog, &payload.filter);
|
||||
|
||||
// Set the previous file path if provided:
|
||||
if let Some(previous) = &payload.name_file {
|
||||
let previous_path = previous.file_path.as_str();
|
||||
file_dialog = file_dialog.set_directory(previous_path);
|
||||
}
|
||||
|
||||
// Displays the file dialogue box and select the file:
|
||||
let file_path = file_dialog.blocking_save_file();
|
||||
match file_path {
|
||||
Some(path) => match path.into_path() {
|
||||
Ok(pb) => {
|
||||
info!("User selected file for writing operation: {pb:?}");
|
||||
Json(FileSaveResponse {
|
||||
user_cancelled: false,
|
||||
save_file_path: pb.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
error!(Source = "Tauri"; "Failed to convert save file path: {e}");
|
||||
Json(FileSaveResponse {
|
||||
user_cancelled: true,
|
||||
save_file_path: String::new(),
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
None => {
|
||||
info!("User cancelled file selection.");
|
||||
Json(FileSaveResponse {
|
||||
user_cancelled: true,
|
||||
save_file_path: String::from(""),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies an optional file type filter to a FileDialogBuilder.
|
||||
fn apply_filter<R: tauri::Runtime>(file_dialog: FileDialogBuilder<R>, filter: &Option<FileTypeFilter>) -> FileDialogBuilder<R> {
|
||||
match filter {
|
||||
Some(f) => file_dialog.add_filter(
|
||||
&f.filter_name,
|
||||
&f.filter_extensions.iter().map(|s| s.as_str()).collect::<Vec<&str>>(),
|
||||
),
|
||||
|
||||
None => file_dialog,
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -17,4 +17,5 @@ pub mod qdrant;
|
||||
pub mod certificate_factory;
|
||||
pub mod runtime_api_token;
|
||||
pub mod stale_process_cleanup;
|
||||
mod sidecar_types;
|
||||
mod sidecar_types;
|
||||
mod file_actions;
|
||||
+16
-10
@@ -10,15 +10,17 @@ use once_cell::sync::Lazy;
|
||||
use rocket::get;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::serde::Serialize;
|
||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
||||
use crate::api_token::{APIToken};
|
||||
use crate::environment::{is_dev, DATA_DIRECTORY};
|
||||
use crate::certificate_factory::generate_certificate;
|
||||
use std::path::PathBuf;
|
||||
use tauri::PathResolver;
|
||||
use tauri::Manager;
|
||||
use tauri::path::BaseDirectory;
|
||||
use tempfile::{TempDir, Builder};
|
||||
use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process};
|
||||
use crate::sidecar_types::SidecarType;
|
||||
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
// Qdrant server process started in a separate process and can communicate
|
||||
// via HTTP or gRPC with the .NET server and the runtime process
|
||||
@@ -98,7 +100,7 @@ pub fn qdrant_port(_token: APIToken) -> Json<ProvideQdrantInfo> {
|
||||
}
|
||||
|
||||
/// Starts the Qdrant server in a separate process.
|
||||
pub fn start_qdrant_server(path_resolver: PathResolver){
|
||||
pub fn start_qdrant_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>){
|
||||
let path = qdrant_base_path();
|
||||
if !path.exists() {
|
||||
if let Err(e) = fs::create_dir_all(&path){
|
||||
@@ -121,7 +123,7 @@ pub fn start_qdrant_server(path_resolver: PathResolver){
|
||||
let snapshot_path = path.join("snapshots").to_string_lossy().to_string();
|
||||
let init_path = path.join(".qdrant-initialized").to_string_lossy().to_string();
|
||||
|
||||
let qdrant_server_environment = HashMap::from_iter([
|
||||
let qdrant_server_environment: HashMap<String, String> = HashMap::from_iter([
|
||||
(String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()),
|
||||
(String::from("QDRANT__SERVICE__GRPC_PORT"), QDRANT_SERVER_PORT_GRPC.to_string()),
|
||||
(String::from("QDRANT_INIT_FILE_PATH"), init_path),
|
||||
@@ -135,9 +137,9 @@ pub fn start_qdrant_server(path_resolver: PathResolver){
|
||||
|
||||
let server_spawn_clone = QDRANT_SERVER.clone();
|
||||
let qdrant_relative_source_path = "resources/databases/qdrant/config.yaml";
|
||||
let qdrant_source_path = match path_resolver.resolve_resource(qdrant_relative_source_path) {
|
||||
Some(path) => path,
|
||||
None => {
|
||||
let qdrant_source_path = match app_handle.path().resolve(qdrant_relative_source_path, BaseDirectory::Resource) {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
let reason = format!("The Qdrant config resource '{qdrant_relative_source_path}' could not be resolved.");
|
||||
error!(Source = "Qdrant"; "{reason} Starting the app without Qdrant.");
|
||||
set_qdrant_unavailable(reason);
|
||||
@@ -147,7 +149,9 @@ pub fn start_qdrant_server(path_resolver: PathResolver){
|
||||
|
||||
let qdrant_source_path_display = qdrant_source_path.to_string_lossy().to_string();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let sidecar = match Command::new_sidecar("qdrant") {
|
||||
let shell = app_handle.shell();
|
||||
|
||||
let sidecar = match shell.sidecar("qdrant") {
|
||||
Ok(sidecar) => sidecar,
|
||||
Err(e) => {
|
||||
let reason = format!("Failed to create sidecar for Qdrant: {e}");
|
||||
@@ -183,7 +187,8 @@ pub fn start_qdrant_server(path_resolver: PathResolver){
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line) => {
|
||||
let line = line.trim_end();
|
||||
let line_utf8 = String::from_utf8_lossy(&line).to_string();
|
||||
let line = line_utf8.trim_end();
|
||||
if line.contains("INFO") || line.contains("info") {
|
||||
info!(Source = "Qdrant Server"; "{line}");
|
||||
} else if line.contains("WARN") || line.contains("warning") {
|
||||
@@ -196,7 +201,8 @@ pub fn start_qdrant_server(path_resolver: PathResolver){
|
||||
},
|
||||
|
||||
CommandEvent::Stderr(line) => {
|
||||
error!(Source = "Qdrant Server (stderr)"; "{line}");
|
||||
let line_utf8 = String::from_utf8_lossy(&line).to_string();
|
||||
error!(Source = "Qdrant Server (stderr)"; "{line_utf8}");
|
||||
},
|
||||
|
||||
_ => {}
|
||||
|
||||
@@ -72,11 +72,11 @@ pub fn start_runtime_api() {
|
||||
crate::app_window::get_event_stream,
|
||||
crate::app_window::check_for_update,
|
||||
crate::app_window::install_update,
|
||||
crate::app_window::select_directory,
|
||||
crate::app_window::select_file,
|
||||
crate::app_window::select_files,
|
||||
crate::app_window::save_file,
|
||||
crate::app_window::exit_app,
|
||||
crate::file_actions::select_directory,
|
||||
crate::file_actions::select_file,
|
||||
crate::file_actions::select_files,
|
||||
crate::file_actions::save_file,
|
||||
crate::secret::get_secret,
|
||||
crate::secret::store_secret,
|
||||
crate::secret::delete_secret,
|
||||
|
||||
+49
-79
@@ -1,44 +1,52 @@
|
||||
{
|
||||
"productName": "MindWork AI Studio",
|
||||
"mainBinaryName": "MindWork AI Studio",
|
||||
"version": "26.5.2",
|
||||
"identifier": "com.github.mindwork-ai.ai-studio",
|
||||
|
||||
"build": {
|
||||
"devPath": "ui/",
|
||||
"distDir": "ui/",
|
||||
"withGlobalTauri": false
|
||||
"frontendDist": "ui/"
|
||||
},
|
||||
"package": {
|
||||
"productName": "MindWork AI Studio",
|
||||
"version": "26.5.1"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"shell": {
|
||||
"sidecar": true,
|
||||
"all": false,
|
||||
"open": true,
|
||||
"scope": [
|
||||
{
|
||||
"name": "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer",
|
||||
"sidecar": true,
|
||||
"args": true
|
||||
},
|
||||
{
|
||||
"name": "target/databases/qdrant/qdrant",
|
||||
"sidecar": true,
|
||||
"args": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"http" : {
|
||||
"all": true,
|
||||
"request": true,
|
||||
"scope": [
|
||||
"http://localhost"
|
||||
]
|
||||
},
|
||||
"fs": {
|
||||
"scope": ["$RESOURCE/resources/*"]
|
||||
}
|
||||
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"externalBin": [
|
||||
"../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer",
|
||||
"target/databases/qdrant/qdrant"
|
||||
],
|
||||
"resources": [
|
||||
"resources/databases/qdrant/config.yaml",
|
||||
"resources/libraries/*"
|
||||
],
|
||||
"macOS": {
|
||||
"exceptionDomain": "localhost"
|
||||
},
|
||||
"createUpdaterArtifacts": "v1Compatible"
|
||||
},
|
||||
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"windows": {
|
||||
"installMode": "passive"
|
||||
},
|
||||
"endpoints": [
|
||||
"https://github.com/MindWorkAI/AI-Studio/releases/download/v26.5.3/latest.json"
|
||||
],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDM3MzE4MTM4RTNDMkM0NEQKUldSTnhNTGpPSUV4TjFkczFxRFJOZWgydzFQN1dmaFlKbXhJS1YyR1RKS1RnR09jYUpMaGsrWXYK"
|
||||
}
|
||||
},
|
||||
|
||||
"app": {
|
||||
"withGlobalTauri": false,
|
||||
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
@@ -46,51 +54,13 @@
|
||||
"title": "MindWork AI Studio",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"fileDropEnabled": true
|
||||
"dragDropEnabled": true,
|
||||
"useHttpsScheme": true
|
||||
}
|
||||
],
|
||||
|
||||
"security": {
|
||||
"csp": null,
|
||||
"dangerousRemoteDomainIpcAccess": [
|
||||
{
|
||||
"domain": "localhost",
|
||||
"windows": ["main"],
|
||||
"enableTauriAPI": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "com.github.mindwork-ai.ai-studio",
|
||||
"externalBin": [
|
||||
"../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer",
|
||||
"target/databases/qdrant/qdrant"
|
||||
],
|
||||
"resources": [
|
||||
"resources/**"
|
||||
],
|
||||
"macOS": {
|
||||
"exceptionDomain": "localhost"
|
||||
},
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"endpoints": [
|
||||
"https://github.com/MindWorkAI/AI-Studio/releases/download/v26.5.2/latest.json"
|
||||
],
|
||||
"dialog": false,
|
||||
"windows": {
|
||||
"installMode": "passive"
|
||||
},
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDM3MzE4MTM4RTNDMkM0NEQKUldSTnhNTGpPSUV4TjFkczFxRFJOZWgydzFQN1dmaFlKbXhJS1YyR1RKS1RnR09jYUpMaGsrWXYK"
|
||||
"csp": null
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user