diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 950cd93d5..1b7e5bd42 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -133,10 +133,16 @@ body: - type: textarea id: logs attributes: - label: 错误日志 / 截图 - description: 可粘贴终端、DevTools、Tauri/Rust 日志,或直接拖入截图。 + label: 错误日志 + description: 粘贴终端、DevTools、Tauri/Rust 日志。 render: shell - placeholder: "请粘贴错误堆栈、控制台输出或截图。" + placeholder: "请粘贴错误堆栈或控制台输出。" + + - type: textarea + id: screenshots + attributes: + label: 截图 + description: 可直接粘贴或拖入截图。 - type: input id: app-version diff --git a/.github/ISSUE_TEMPLATE/database_compatibility.yml b/.github/ISSUE_TEMPLATE/database_compatibility.yml index e1d7cdc8c..0f7042d52 100644 --- a/.github/ISSUE_TEMPLATE/database_compatibility.yml +++ b/.github/ISSUE_TEMPLATE/database_compatibility.yml @@ -89,6 +89,12 @@ body: validations: required: true + - type: textarea + id: screenshots + attributes: + label: 截图 + description: 可直接粘贴或拖入截图。 + - type: textarea id: expected attributes: diff --git a/crates/dbx-core/src/agent_manager.rs b/crates/dbx-core/src/agent_manager.rs index 809f6173c..e21903dc2 100644 --- a/crates/dbx-core/src/agent_manager.rs +++ b/crates/dbx-core/src/agent_manager.rs @@ -6,12 +6,34 @@ use tokio::sync::Mutex; use crate::db::agent_driver::AgentDriverClient; use crate::models::connection::DatabaseType; +pub const DEFAULT_JRE_KEY: &str = "17"; + +fn default_jre_key() -> String { + DEFAULT_JRE_KEY.to_string() +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentRegistry { - pub jre: JreInfo, + #[serde(default)] + pub jre: Option, + #[serde(default)] + pub jres: std::collections::HashMap, pub drivers: std::collections::HashMap, } +impl AgentRegistry { + pub fn resolve_jre(&self, key: &str) -> Option<&JreInfo> { + if !self.jres.is_empty() { + return self.jres.get(key); + } + if key == DEFAULT_JRE_KEY { + self.jre.as_ref() + } else { + None + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JreInfo { pub version: String, @@ -24,6 +46,8 @@ pub struct DriverInfo { pub label: String, pub min_app_version: String, pub jar: ArtifactInfo, + #[serde(default = "default_jre_key")] + pub jre: String, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -38,6 +62,8 @@ pub struct AgentState { #[serde(default)] pub jre_version: Option, #[serde(default)] + pub jre_versions: std::collections::HashMap, + #[serde(default)] pub installed_drivers: std::collections::HashMap, } @@ -45,6 +71,8 @@ pub struct AgentState { pub struct InstalledDriver { pub version: String, pub installed_at: String, + #[serde(default = "default_jre_key")] + pub jre: String, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -56,6 +84,8 @@ pub struct AgentDriverInfo { pub installed: bool, pub installed_version: Option, pub update_available: bool, + pub jre: String, + pub jre_installed: bool, } pub struct AgentManager { @@ -67,9 +97,19 @@ impl AgentManager { pub fn new() -> Self { let home = std::env::var(if cfg!(windows) { "USERPROFILE" } else { "HOME" }).unwrap_or_else(|_| ".".to_string()); - Self { + let mgr = Self { base_dir: PathBuf::from(home).join(".dbx").join("agents"), daemons: Mutex::new(std::collections::HashMap::new()), + }; + mgr.migrate_legacy_jre(); + mgr + } + + fn migrate_legacy_jre(&self) { + let legacy = self.base_dir.join("jre"); + let versioned = self.jre_dir(DEFAULT_JRE_KEY); + if legacy.exists() && !versioned.exists() { + let _ = std::fs::rename(&legacy, &versioned); } } @@ -77,11 +117,16 @@ impl AgentManager { &self.base_dir } - pub fn jre_java_path(&self) -> PathBuf { + pub fn jre_dir(&self, jre_key: &str) -> PathBuf { + self.base_dir.join(format!("jre-{jre_key}")) + } + + pub fn jre_java_path(&self, jre_key: &str) -> PathBuf { + let dir = self.jre_dir(jre_key); if cfg!(windows) { - self.base_dir.join("jre").join("bin").join("java.exe") + dir.join("bin").join("java.exe") } else { - self.base_dir.join("jre").join("bin").join("java") + dir.join("bin").join("java") } } @@ -94,10 +139,9 @@ impl AgentManager { } pub fn load_state(&self) -> AgentState { - std::fs::read_to_string(self.state_path()) - .ok() - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or(AgentState { jre_version: None, installed_drivers: Default::default() }) + std::fs::read_to_string(self.state_path()).ok().and_then(|s| serde_json::from_str(&s).ok()).unwrap_or( + AgentState { jre_version: None, jre_versions: Default::default(), installed_drivers: Default::default() }, + ) } pub fn save_state(&self, state: &AgentState) -> Result<(), String> { @@ -107,8 +151,8 @@ impl AgentManager { std::fs::write(self.state_path(), json).map_err(|e| e.to_string()) } - pub fn is_jre_installed(&self) -> bool { - self.jre_java_path().exists() + pub fn is_jre_installed(&self, jre_key: &str) -> bool { + self.jre_java_path(jre_key).exists() } pub fn is_driver_installed(&self, db_type: &str) -> bool { @@ -146,14 +190,17 @@ impl AgentManager { let key = Self::db_type_to_agent_key(db_type) .ok_or_else(|| format!("{:?} is not an agent-driven database type", db_type))?; - if !self.is_jre_installed() { - return Err("JRE runtime is not installed. Please install it from the Driver Manager.".to_string()); + let state = self.load_state(); + let jre_key = state.installed_drivers.get(key).map(|d| d.jre.as_str()).unwrap_or(DEFAULT_JRE_KEY); + + if !self.is_jre_installed(jre_key) { + return Err(format!("JRE {jre_key} runtime is not installed. Please install it from the Driver Manager.")); } if !self.is_driver_installed(key) { return Err(format!("{key} driver is not installed. Please install it from the Driver Manager.")); } - let java = self.jre_java_path().to_string_lossy().to_string(); + let java = self.jre_java_path(jre_key).to_string_lossy().to_string(); let jar = self.driver_jar_path(key).to_string_lossy().to_string(); AgentDriverClient::spawn(&java, &jar).await } @@ -171,13 +218,18 @@ impl AgentManager { let mut daemons = self.daemons.lock().await; if !daemons.contains_key(&key) { - if !self.is_jre_installed() { - return Err("JRE runtime is not installed. Please install it from the Driver Manager.".to_string()); + let state = self.load_state(); + let jre_key = state.installed_drivers.get(&key).map(|d| d.jre.as_str()).unwrap_or(DEFAULT_JRE_KEY); + + if !self.is_jre_installed(jre_key) { + return Err(format!( + "JRE {jre_key} runtime is not installed. Please install it from the Driver Manager." + )); } if !self.is_driver_installed(&key) { return Err(format!("{key} driver is not installed. Please install it from the Driver Manager.")); } - let java = self.jre_java_path().to_string_lossy().to_string(); + let java = self.jre_java_path(jre_key).to_string_lossy().to_string(); let jar = self.driver_jar_path(&key).to_string_lossy().to_string(); let client = AgentDriverClient::spawn(&java, &jar).await?; daemons.insert(key.clone(), client); @@ -189,7 +241,9 @@ impl AgentManager { Err(e) => { log::warn!("[agent] daemon call failed, respawning: {e}"); daemons.remove(&key); - let java = self.jre_java_path().to_string_lossy().to_string(); + let state = self.load_state(); + let jre_key = state.installed_drivers.get(&key).map(|d| d.jre.as_str()).unwrap_or(DEFAULT_JRE_KEY); + let java = self.jre_java_path(jre_key).to_string_lossy().to_string(); let jar = self.driver_jar_path(&key).to_string_lossy().to_string(); let mut new_client = AgentDriverClient::spawn(&java, &jar).await?; let result = new_client.call::(method, params).await?; diff --git a/src-tauri/src/commands/agents.rs b/src-tauri/src/commands/agents.rs index 56c11680d..b78a4b97e 100644 --- a/src-tauri/src/commands/agents.rs +++ b/src-tauri/src/commands/agents.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use tauri::{Emitter, State}; use tokio::sync::Mutex; -use dbx_core::agent_manager::{AgentDriverInfo, AgentManager, AgentRegistry, InstalledDriver}; +use dbx_core::agent_manager::{AgentDriverInfo, AgentManager, AgentRegistry, InstalledDriver, DEFAULT_JRE_KEY}; use dbx_core::connection::AppState; const REGISTRY_PATH: &str = "https://github.com/t8y2/dbx-agents/releases/latest/download/agent-registry.json"; @@ -39,6 +39,10 @@ fn build_agent_list(am: &AgentManager, registry: Option<&AgentRegistry>) -> Vec< let installed = am.is_driver_installed(key); let local = local_state.installed_drivers.get(*key); let remote = registry.and_then(|r| r.drivers.get(*key)); + let jre_key = remote + .map(|r| r.jre.clone()) + .or_else(|| local.map(|l| l.jre.clone())) + .unwrap_or_else(|| DEFAULT_JRE_KEY.to_string()); AgentDriverInfo { db_type: key.to_string(), label: label.to_string(), @@ -50,6 +54,8 @@ fn build_agent_list(am: &AgentManager, registry: Option<&AgentRegistry>) -> Vec< (Some(l), Some(r)) => l.version != r.version, _ => false, }, + jre: jre_key.clone(), + jre_installed: am.is_jre_installed(&jre_key), } }) .collect() @@ -74,31 +80,37 @@ pub async fn install_agent( ) -> Result<(), String> { let am = &state.agent_manager; let registry = fetch_registry().await?; - let needs_jre = !am.is_jre_installed(); + + let driver = registry.drivers.get(&db_type).ok_or_else(|| format!("Unknown driver type: {db_type}"))?; + let jre_key = &driver.jre; + let needs_jre = !am.is_jre_installed(jre_key); if needs_jre { - let platform = AgentManager::current_platform(); let jre_info = - registry.jre.platforms.get(platform).ok_or_else(|| format!("No JRE available for platform: {platform}"))?; + registry.resolve_jre(jre_key).ok_or_else(|| format!("No JRE definition for version: {jre_key}"))?; + let platform = AgentManager::current_platform(); + let platform_jre = jre_info + .platforms + .get(platform) + .ok_or_else(|| format!("No JRE {jre_key} available for platform: {platform}"))?; let jre_archive = am.base_dir().join("jre-download.tar.gz"); let _ = app.emit( "agent-install-progress", serde_json::json!({ - "step": "jre", "downloaded": 0u64, "total": jre_info.size, + "step": "jre", "downloaded": 0u64, "total": platform_jre.size, }), ); - download_with_progress(&app, "jre", &jre_info.url, &jre_archive, jre_info.size).await?; + download_with_progress(&app, "jre", &platform_jre.url, &jre_archive, platform_jre.size).await?; let _ = app.emit( "agent-install-progress", serde_json::json!({ "step": "jre-extract", "downloaded": 0u64, "total": 0u64, }), ); - extract_archive(&jre_archive, &am.base_dir().join("jre"))?; + extract_archive(&jre_archive, &am.jre_dir(jre_key))?; std::fs::remove_file(&jre_archive).ok(); } - let driver = registry.drivers.get(&db_type).ok_or_else(|| format!("Unknown driver type: {db_type}"))?; let jar_path = am.driver_jar_path(&db_type); let _ = app.emit( "agent-install-progress", @@ -109,10 +121,16 @@ pub async fn install_agent( download_with_progress(&app, "driver", &driver.jar.url, &jar_path, driver.jar.size).await?; let mut local_state = am.load_state(); - local_state.jre_version = Some(registry.jre.version.clone()); + if let Some(jre_info) = registry.resolve_jre(jre_key) { + local_state.jre_versions.insert(jre_key.clone(), jre_info.version.clone()); + } local_state.installed_drivers.insert( db_type, - InstalledDriver { version: driver.version.clone(), installed_at: chrono::Utc::now().to_rfc3339() }, + InstalledDriver { + version: driver.version.clone(), + installed_at: chrono::Utc::now().to_rfc3339(), + jre: jre_key.clone(), + }, ); am.save_state(&local_state)?; let _ = app.emit("agent-install-progress", serde_json::json!({ "step": "done" })); @@ -137,8 +155,9 @@ pub async fn uninstall_agent(state: State<'_, Arc>, db_type: String) - } #[tauri::command] -pub async fn check_jre_installed(state: State<'_, Arc>) -> Result { - Ok(state.agent_manager.is_jre_installed()) +pub async fn check_jre_installed(state: State<'_, Arc>, jre_key: Option) -> Result { + let key = jre_key.as_deref().unwrap_or(DEFAULT_JRE_KEY); + Ok(state.agent_manager.is_jre_installed(key)) } #[tauri::command] @@ -148,22 +167,28 @@ pub async fn invalidate_agent_registry_cache() -> Result<(), String> { } #[tauri::command] -pub async fn reinstall_jre(app: tauri::AppHandle, state: State<'_, Arc>) -> Result<(), String> { +pub async fn reinstall_jre( + app: tauri::AppHandle, + state: State<'_, Arc>, + jre_key: Option, +) -> Result<(), String> { let am = &state.agent_manager; - let jre_dir = am.base_dir().join("jre"); + let key = jre_key.as_deref().unwrap_or(DEFAULT_JRE_KEY); + let jre_dir = am.jre_dir(key); if jre_dir.exists() { std::fs::remove_dir_all(&jre_dir).map_err(|e| format!("Failed to remove old JRE: {e}"))?; } let registry = fetch_registry().await?; + let jre_info = registry.resolve_jre(key).ok_or_else(|| format!("No JRE definition for version: {key}"))?; let platform = AgentManager::current_platform(); - let jre_info = - registry.jre.platforms.get(platform).ok_or_else(|| format!("No JRE available for platform: {platform}"))?; + let platform_jre = + jre_info.platforms.get(platform).ok_or_else(|| format!("No JRE {key} available for platform: {platform}"))?; let jre_archive = am.base_dir().join("jre-download.tar.gz"); - download_with_progress(&app, "jre", &jre_info.url, &jre_archive, jre_info.size).await?; + download_with_progress(&app, "jre", &platform_jre.url, &jre_archive, platform_jre.size).await?; extract_archive(&jre_archive, &jre_dir)?; std::fs::remove_file(&jre_archive).ok(); let mut local_state = am.load_state(); - local_state.jre_version = Some(registry.jre.version.clone()); + local_state.jre_versions.insert(key.to_string(), jre_info.version.clone()); am.save_state(&local_state)?; let _ = app.emit("agent-install-progress", serde_json::json!({ "step": "done" })); Ok(())