diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 342e22c66..f03faf629 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -33,6 +33,7 @@ tauri-plugin-store = "2" hyper = { version = "0.14", features = ["server"] } reqwest = { version = "0.11", features = ["json", "blocking", "stream"] } tokio = { version = "1", features = ["full"] } +rocksdb = "0.21" rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", rev = "c1c4c9a0c9afbfbbf9eb42d6f8b00d8546fbdc2c", features = [ "client", "transport-sse-client", diff --git a/src-tauri/src/core/cmd.rs b/src-tauri/src/core/cmd.rs index 7c8470d7d..3af5deb62 100644 --- a/src-tauri/src/core/cmd.rs +++ b/src-tauri/src/core/cmd.rs @@ -395,39 +395,3 @@ pub async fn read_logs(app: AppHandle) -> Result { Err(format!("Log file not found")) } } - -#[tauri::command] -pub async fn handle_app_update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> { - if let Some(update) = app.updater()?.check().await? { - let mut downloaded = 0; - - // alternatively we could also call update.download() and update.install() separately - log::info!( - "Has update {} {} {}", - update.version, - update.current_version, - update.download_url - ); - update - .download_and_install( - |chunk_length, content_length| { - downloaded += chunk_length; - log::info!("downloaded {downloaded} from {content_length:?}"); - }, - || { - log::info!("download finished"); - }, - ) - .await?; - - log::info!("update installed"); - let client = Client::new(); - let url = "http://127.0.0.1:39291/processManager/destroy"; - let _ = client.delete(url).send(); - app.restart(); - } else { - log::info!("Cannot parse response or update is not available"); - } - - Ok(()) -} diff --git a/src-tauri/src/core/migration.rs b/src-tauri/src/core/migration.rs new file mode 100644 index 000000000..8867c1e52 --- /dev/null +++ b/src-tauri/src/core/migration.rs @@ -0,0 +1,122 @@ +use rocksdb::{IteratorMode, DB}; +use std::collections::HashMap; +use tauri::Manager; + +#[tauri::command] +pub fn get_legacy_browser_data(app: tauri::AppHandle) -> Result, String> { + let mut path = app.path().data_dir().unwrap(); + + let app_name = + std::env::var("APP_NAME").unwrap_or_else(|_| app.config().product_name.clone().unwrap()); + path.push(app_name); + path.push("Local Storage"); + path.push("leveldb"); + // Check if the path exists + if !path.exists() { + log::info!("Path {:?} does not exist, skipping migration.", path); + return Ok(HashMap::new()); + } + + let db = DB::open_default(path); + match db { + Ok(db) => { + let iter = db.iterator(IteratorMode::Start); + + let migration_kvs: HashMap = HashMap::from([ + // Api Server + ( + "_file://\0\u{1}apiServerHost".to_string(), + "serverHost".to_string(), + ), + ( + "_file://\0\u{1}apiServerPort".to_string(), + "serverPort".to_string(), + ), + ( + "_file://\0\u{1}apiServerCorsEnabled".to_string(), + "corsEnabled".to_string(), + ), + ( + "_file://\0\u{1}apiServerPrefix".to_string(), + "apiPrefix".to_string(), + ), + ( + "_file://\0\u{1}apiServerVerboseLogEnabled".to_string(), + "verboseLogs".to_string(), + ), + // Proxy + ( + "_file://\0\u{1}proxyFeatureEnabled".to_string(), + "proxyEnabled".to_string(), + ), + ( + "_file://\0\u{1}httpsProxyFeature".to_string(), + "proxyUrl".to_string(), + ), + ( + "_file://\0\u{1}proxyPassword".to_string(), + "proxyPassword".to_string(), + ), + ( + "_file://\0\u{1}proxyUsername".to_string(), + "proxyUsername".to_string(), + ), + ( + "_file://\0\u{1}ignoreSSLFeature".to_string(), + "proxyIgnoreSSL".to_string(), + ), + ( + "_file://\0\u{1}verifyProxySSL".to_string(), + "verifyProxySSL".to_string(), + ), + ( + "_file://\0\u{1}verifyProxyHostSSL".to_string(), + "verifyProxyHostSSL".to_string(), + ), + ( + "_file://\0\u{1}verifyPeerSSL".to_string(), + "verifyPeerSSL".to_string(), + ), + ( + "_file://\0\u{1}verifyHostSSL".to_string(), + "verifyHostSSL".to_string(), + ), + ("_file://\0\u{1}noProxy".to_string(), "noProxy".to_string()), + // Analytics + ( + "_file://\0\u{1}productAnalytic".to_string(), + "productAnalytic".to_string(), + ), + ( + "_file://\0\u{1}productAnalyticPrompt".to_string(), + "productAnalyticPrompt".to_string(), + ), + ]); + + let mut results = HashMap::new(); + + for item in iter { + match item { + Ok((key, value)) => { + let key_str = String::from_utf8_lossy(&key).to_string(); + let value_str = String::from_utf8_lossy(&value).to_string(); + // log::info!("Key: {:?} | Value: {:?}", key_str, value_str); + if let Some(new_key) = migration_kvs.get(&key_str) { + log::info!("Migrating key {:?} to new key {:?}", key_str, new_key); + + results.insert(new_key.to_string(), value_str.replace("\u{1}", "")); + } + } + Err(e) => { + eprintln!("Error reading from DB: {:?}", e); + } + } + } + log::info!("Migration results: {:?}", results); + Ok(results) + } + Err(e) => { + return Ok(HashMap::new()); + } + } +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index d18a8d6cc..65738dac2 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,9 +1,10 @@ pub mod cmd; pub mod fs; +pub mod hardware; pub mod mcp; +pub mod migration; pub mod server; pub mod setup; pub mod state; pub mod threads; pub mod utils; -pub mod hardware; diff --git a/src-tauri/src/core/setup.rs b/src-tauri/src/core/setup.rs index 017b8a0bb..e007fb87e 100644 --- a/src-tauri/src/core/setup.rs +++ b/src-tauri/src/core/setup.rs @@ -12,7 +12,6 @@ use tauri_plugin_shell::ShellExt; use tauri_plugin_store::StoreExt; use tokio::sync::Mutex; // Using tokio::sync::Mutex use tokio::time::{sleep, Duration}; - // MCP use super::{ cmd::{get_jan_data_folder_path, get_jan_extensions_path}, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e6f21a6c8..744fb383a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -48,9 +48,9 @@ pub fn run() { core::cmd::start_server, core::cmd::stop_server, core::cmd::read_logs, - core::cmd::handle_app_update, core::cmd::change_app_data_folder, core::cmd::reset_cortex_restart_count, + core::migration::get_legacy_browser_data, // MCP commands core::mcp::get_tools, core::mcp::call_tool, @@ -106,11 +106,6 @@ pub fn run() { setup_mcp(app); setup_sidecar(app).expect("Failed to setup sidecar"); setup_engine_binaries(app).expect("Failed to setup engine binaries"); - // TODO(any) need to wire up with frontend - // let handle = app.handle().clone(); - // tauri::async_runtime::spawn(async move { - // handle_app_update(handle).await.unwrap(); - // }); Ok(()) }) .on_window_event(|window, event| match event { diff --git a/web-app/src/constants/localStorage.ts b/web-app/src/constants/localStorage.ts index 56ce0cdf6..4fa95ca2c 100644 --- a/web-app/src/constants/localStorage.ts +++ b/web-app/src/constants/localStorage.ts @@ -10,6 +10,7 @@ export const localStorageKey = { settingCodeBlock: 'setting-code-block', settingMCPSevers: 'setting-mcp-servers', settingLocalApiServer: 'setting-local-api-server', + settingProxyConfig: 'setting-proxy-config', settingHardware: 'setting-hardware', productAnalyticPrompt: 'productAnalyticPrompt', productAnalytic: 'productAnalytic', diff --git a/web-app/src/hooks/useProxyConfig.ts b/web-app/src/hooks/useProxyConfig.ts index 8863bc9ef..39754dfa0 100644 --- a/web-app/src/hooks/useProxyConfig.ts +++ b/web-app/src/hooks/useProxyConfig.ts @@ -52,7 +52,7 @@ export const useProxyConfig = create()( setNoProxy: (noProxy) => set({ noProxy }), }), { - name: localStorageKey.settingLocalApiServer, + name: localStorageKey.settingProxyConfig, storage: createJSONStorage(() => localStorage), } ) diff --git a/web-app/src/providers/DataProvider.tsx b/web-app/src/providers/DataProvider.tsx index 7f0d5e1e3..5b9abd370 100644 --- a/web-app/src/providers/DataProvider.tsx +++ b/web-app/src/providers/DataProvider.tsx @@ -12,6 +12,7 @@ import { useMCPServers } from '@/hooks/useMCPServers' import { getMCPConfig } from '@/services/mcp' import { useAssistant } from '@/hooks/useAssistant' import { getAssistants } from '@/services/assistants' +import { migrateData } from '@/utils/migration' export function DataProvider() { const { setProviders } = useModelProvider() @@ -30,6 +31,7 @@ export function DataProvider() { getAssistants().then((data) => setAssistants((data as unknown as Assistant[]) ?? []) ) + migrateData() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) diff --git a/web-app/src/utils/migration.ts b/web-app/src/utils/migration.ts new file mode 100644 index 000000000..63ceaf6fc --- /dev/null +++ b/web-app/src/utils/migration.ts @@ -0,0 +1,78 @@ +import { useProductAnalytic } from '@/hooks/useAnalytic' +import { useLocalApiServer } from '@/hooks/useLocalApiServer' +import { useModelProvider } from '@/hooks/useModelProvider' +import { useProxyConfig } from '@/hooks/useProxyConfig' +import { ExtensionManager } from '@/lib/extension' +import { configurePullOptions } from '@/services/models' +import { EngineManagementExtension, ExtensionTypeEnum } from '@janhq/core' +import { invoke } from '@tauri-apps/api/core' + +/** + * Migrates legacy browser data to new browser session. + */ +export const migrateData = async () => { + if (!localStorage.getItem('migration_completed')) { + try { + // Migrate local storage data + const oldData = await invoke('get_legacy_browser_data') + for (const [key, value] of Object.entries( + oldData as unknown as Record + )) { + if (value !== null && value !== undefined) { + if (Object.keys(useLocalApiServer.getState()).includes(key)) { + useLocalApiServer.setState({ + ...useLocalApiServer.getState(), + [key]: value.replace(/"/g, ''), + }) + } else if (Object.keys(useProxyConfig.getState()).includes(key)) { + useProxyConfig.setState({ + ...useProxyConfig.getState(), + [key]: value.replace(/"/g, ''), + }) + } else if (Object.keys(useProductAnalytic.getState()).includes(key)) { + useProductAnalytic.setState({ + ...useProductAnalytic.getState(), + [key]: value.replace(/"/g, ''), + }) + } + } + } + // Migrate provider configurations + const engines = await ExtensionManager.getInstance() + .get(ExtensionTypeEnum.Engine) + ?.getEngines() + if (engines) { + for (const [key, value] of Object.entries(engines)) { + const providerName = key.replace('google_gemini', 'gemini') + const engine = value[0] as + | { + api_key?: string + url?: string + engine?: string + } + | undefined + if (engine && 'api_key' in engine) { + const provider = useModelProvider + .getState() + .getProviderByName(providerName) + const settings = provider?.settings.map((e) => { + if (e.key === 'api-key') + e.controller_props.value = (engine.api_key as string) ?? '' + return e + }) + if (provider) { + useModelProvider.getState().updateProvider(providerName, { + ...provider, + settings: settings ?? [], + }) + } + } + } + } + localStorage.setItem('migration_completed', 'true') + configurePullOptions(useProxyConfig.getState()) + } catch (error) { + console.error('Migration failed:', error) + } + } +}