diff --git a/src-tauri/src/commands/agents.rs b/src-tauri/src/commands/agents.rs index 1335fc1cd1..5133899bc1 100644 --- a/src-tauri/src/commands/agents.rs +++ b/src-tauri/src/commands/agents.rs @@ -1,12 +1,16 @@ use std::sync::Arc; use tauri::{Emitter, State}; +use tokio::sync::Mutex; use dbx_core::agent_manager::{AgentDriverInfo, AgentManager, AgentRegistry, InstalledDriver}; use dbx_core::connection::AppState; const REGISTRY_PATH: &str = "https://github.com/t8y2/dbx-agents/releases/latest/download/agent-registry.json"; +static REGISTRY_CACHE: std::sync::LazyLock>> = + std::sync::LazyLock::new(|| Mutex::new(None)); + #[tauri::command] pub async fn list_installed_agents(state: State<'_, Arc>) -> Result, String> { let am = &state.agent_manager; @@ -153,6 +157,14 @@ pub async fn reinstall_jre(app: tauri::AppHandle, state: State<'_, Arc } async fn fetch_registry() -> Result { + { + let cache = REGISTRY_CACHE.lock().await; + if let Some((ts, reg)) = cache.as_ref() { + if ts.elapsed() < std::time::Duration::from_secs(300) { + return Ok(reg.clone()); + } + } + } let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(10)) .build() @@ -168,7 +180,9 @@ async fn fetch_registry() -> Result { .and_then(|r| r.error_for_status()) { Ok(resp) => { - return resp.json().await.map_err(|e| format!("Failed to parse registry: {e}")); + let reg: AgentRegistry = resp.json().await.map_err(|e| format!("Failed to parse registry: {e}"))?; + *REGISTRY_CACHE.lock().await = Some((std::time::Instant::now(), reg.clone())); + return Ok(reg); } Err(e) => { last_err = format!("{e}"); diff --git a/src/App.vue b/src/App.vue index 66d4e083d6..a3183a0195 100644 --- a/src/App.vue +++ b/src/App.vue @@ -11,6 +11,7 @@ import EditorToolbar from "@/components/layout/EditorToolbar.vue"; import ContentArea from "@/components/layout/ContentArea.vue"; import AppDialogs from "@/components/layout/AppDialogs.vue"; import WelcomeScreen from "@/components/layout/WelcomeScreen.vue"; +import DriverStorePage from "@/components/config/DriverStoreDialog.vue"; import UpdateDialog from "@/components/layout/UpdateDialog.vue"; import LoginPage from "@/components/auth/LoginPage.vue"; import { useConnectionStore } from "@/stores/connectionStore"; @@ -73,6 +74,7 @@ const setupRequired = ref(false); const showConnectionDialog = ref(false); const showSettingsDialog = ref(false); +const showDriverStore = ref(false); const showHistory = ref(false); const showAiPanel = ref(localStorage.getItem("dbx-ai-panel-open") !== "false"); const { sidebarWidth, aiPanelWidth, historyWidth, startSidebarResize, startAiPanelResize, startHistoryResize } = @@ -159,6 +161,7 @@ watch( () => { selectedSql.value = ""; activeOutputView.value = "result"; + showDriverStore.value = false; }, ); @@ -540,6 +543,7 @@ onUnmounted(() => { :theme-mode="themeMode" :show-ai-panel="showAiPanel" :show-history="showHistory" + :show-driver-store="showDriverStore" :checking-updates="checkingUpdates" :has-connections="connectionStore.connections.length > 0" :has-sql-file-connections="hasSqlFileConnections" @@ -550,6 +554,7 @@ onUnmounted(() => { @toggle-history="showHistory = !showHistory" @open-github="openGitHub" @open-settings="showSettingsDialog = true" + @open-driver-store="showDriverStore = !showDriverStore" @check-updates="checkUpdates()" @open-transfer="dialogs.showTransferDialog.value = true" @open-sql-file="dialogs.showSqlFileDialog.value = true" @@ -580,8 +585,13 @@ onUnmounted(() => { " >
- -
+ + +
+import { ref, onMounted, onUnmounted, computed } from "vue"; +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import type { UnlistenFn } from "@tauri-apps/api/event"; +import { useI18n } from "vue-i18n"; +import { FolderOpen, Trash2, Download, RotateCcw, Loader2 } from "lucide-vue-next"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; +import { useToast } from "@/composables/useToast"; +import { isTauriRuntime } from "@/lib/tauriRuntime"; +import type { JdbcDriverInfo, JdbcPluginStatus } from "@/types/database"; +import * as api from "@/lib/api"; + +const { t } = useI18n(); +const { toast } = useToast(); +const isWeb = !isTauriRuntime(); + +// ──────────── Agent drivers ──────────── + +interface AgentDriverInfo { + db_type: string; + label: string; + version: string; + size: number; + installed: boolean; + installed_version: string | null; + update_available: boolean; +} + +interface InstallProgress { + step: string; + downloaded?: number; + total?: number; +} + +const drivers = ref([]); +const jreInstalled = ref(false); +const installing = ref(null); +const reinstallingJre = ref(false); +const progress = ref(null); + +let unlisten: UnlistenFn | null = null; + +const progressText = computed(() => { + const p = progress.value; + if (!p) return ""; + if (p.step === "jre-extract") return "解压 JRE..."; + const label = p.step === "jre" ? "下载 JRE" : "下载驱动"; + if (!p.total) return `${label}...`; + const pct = Math.round(((p.downloaded ?? 0) / p.total) * 100); + const dl = formatSize(p.downloaded ?? 0); + const total = formatSize(p.total); + return `${label} ${dl} / ${total} (${pct}%)`; +}); + +const progressPercent = computed(() => { + const p = progress.value; + if (!p || !p.total) return 0; + return Math.round(((p.downloaded ?? 0) / p.total) * 100); +}); + +async function refreshAgents() { + jreInstalled.value = await invoke("check_jre_installed"); + drivers.value = await invoke("list_installed_agents"); +} + +async function installDriver(dbType: string) { + const label = drivers.value.find((d) => d.db_type === dbType)?.label ?? dbType; + installing.value = dbType; + progress.value = null; + try { + await invoke("install_agent", { dbType }); + await refreshAgents(); + toast(`${label} 驱动安装成功`); + } catch (e: any) { + toast(`${label} 驱动安装失败: ${e}`); + } finally { + installing.value = null; + progress.value = null; + } +} + +async function uninstallDriver(dbType: string) { + const label = drivers.value.find((d) => d.db_type === dbType)?.label ?? dbType; + try { + await invoke("uninstall_agent", { dbType }); + await refreshAgents(); + toast(`${label} 驱动已卸载`); + } catch (e: any) { + toast(`${label} 驱动卸载失败: ${e}`); + } +} + +async function reinstallJre() { + reinstallingJre.value = true; + progress.value = null; + try { + await invoke("reinstall_jre"); + await refreshAgents(); + toast("JRE 重新安装成功"); + } catch (e: any) { + toast(`JRE 重新安装失败: ${e}`); + } finally { + reinstallingJre.value = false; + progress.value = null; + } +} + +function formatSize(bytes: number): string { + if (!bytes) return ""; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`; + return `${(bytes / 1024 / 1024).toFixed(1)} MB`; +} + +// ──────────── JDBC drivers ──────────── + +const jdbcDrivers = ref([]); +const isLoadingJdbcDrivers = ref(false); +const jdbcPluginStatus = ref(null); +const isInstallingJdbcPlugin = ref(false); +const isUninstallingJdbcPlugin = ref(false); +const jdbcDriverPathInput = ref(""); + +function formatBytes(bytes: number) { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / 1024 / 1024).toFixed(1)} MB`; +} + +async function loadJdbcDrivers() { + if (isWeb) return; + isLoadingJdbcDrivers.value = true; + try { + jdbcDrivers.value = await api.listJdbcDrivers(); + } catch (e: any) { + toast(String(e?.message || e), 5000); + } finally { + isLoadingJdbcDrivers.value = false; + } +} + +async function loadJdbcPluginStatus() { + if (isWeb) return; + try { + jdbcPluginStatus.value = await api.jdbcPluginStatus(); + } catch (e: any) { + toast(String(e?.message || e), 5000); + } +} + +async function installJdbcPlugin() { + if (isWeb || isInstallingJdbcPlugin.value) return; + isInstallingJdbcPlugin.value = true; + try { + jdbcPluginStatus.value = await api.installJdbcPlugin(); + toast(t("settings.jdbcPluginInstallSuccess")); + await loadJdbcDrivers(); + } catch (e: any) { + toast(String(e?.message || e), 5000); + } finally { + isInstallingJdbcPlugin.value = false; + } +} + +async function uninstallJdbcPlugin() { + if (isWeb || isUninstallingJdbcPlugin.value) return; + isUninstallingJdbcPlugin.value = true; + try { + jdbcPluginStatus.value = await api.uninstallJdbcPlugin(); + toast(t("settings.jdbcPluginUninstallSuccess")); + await loadJdbcDrivers(); + } catch (e: any) { + toast(String(e?.message || e), 5000); + } finally { + isUninstallingJdbcPlugin.value = false; + } +} + +async function importJdbcDriverPaths(paths: string[]) { + if (!paths.length) return; + try { + jdbcDrivers.value = await api.importJdbcDrivers(paths); + jdbcDriverPathInput.value = ""; + toast(t("settings.jdbcImportSuccess", { count: paths.length })); + } catch (e: any) { + toast(String(e?.message || e), 5000); + } +} + +async function importJdbcDrivers() { + if (isWeb) return; + const { open } = await import("@tauri-apps/plugin-dialog"); + const selected = await open({ + title: t("settings.jdbcImport"), + multiple: true, + filters: [{ name: "JDBC Driver", extensions: ["jar"] }], + }); + if (!selected) return; + + const paths = (Array.isArray(selected) ? selected : [selected]).filter( + (path): path is string => typeof path === "string", + ); + await importJdbcDriverPaths(paths); +} + +async function importJdbcDriverPathInput() { + const paths = jdbcDriverPathInput.value + .split(/\r?\n/) + .map((path) => path.trim()) + .filter(Boolean); + await importJdbcDriverPaths(paths); +} + +async function deleteJdbcDriver(path: string) { + try { + jdbcDrivers.value = await api.deleteJdbcDriver(path); + toast(t("settings.jdbcDeleteSuccess")); + } catch (e: any) { + toast(String(e?.message || e), 5000); + } +} + +// ──────────── Lifecycle ──────────── + +onMounted(async () => { + await refreshAgents(); + unlisten = await listen("agent-install-progress", (event) => { + if (event.payload.step === "done") { + progress.value = null; + } else { + progress.value = event.payload; + } + }); + void loadJdbcDrivers(); + void loadJdbcPluginStatus(); +}); + +onUnmounted(() => { + unlisten?.(); +}); + + + diff --git a/src/components/connection/ConnectionDialog.vue b/src/components/connection/ConnectionDialog.vue index ec7cb36fa5..02d04d5d14 100644 --- a/src/components/connection/ConnectionDialog.vue +++ b/src/components/connection/ConnectionDialog.vue @@ -1131,7 +1131,7 @@ function openExternalUrl(url: string) {

- 需要在设置 → 驱动中安装对应的 Agent 驱动才能连接。 + 需要在顶部导航栏「驱动管理」中安装对应的驱动才能连接。

diff --git a/src/components/editor/EditorSettingsDialog.vue b/src/components/editor/EditorSettingsDialog.vue index aa26099c16..3f23375b9f 100644 --- a/src/components/editor/EditorSettingsDialog.vue +++ b/src/components/editor/EditorSettingsDialog.vue @@ -2,7 +2,7 @@ import { ref, watch, shallowRef, computed } from "vue"; import type { EditorView as EditorViewType } from "@codemirror/view"; import { useI18n } from "vue-i18n"; -import { CircleHelp, ExternalLink, FolderOpen, Loader2, Settings, Trash2 } from "lucide-vue-next"; +import { CircleHelp, ExternalLink, Loader2, Settings } from "lucide-vue-next"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; @@ -22,15 +22,10 @@ import { } from "@/stores/settingsStore"; import { loadEditorTheme, editorFontTheme } from "@/lib/editorThemes"; import { isTauriRuntime } from "@/lib/tauriRuntime"; -import type { JdbcDriverInfo, JdbcPluginStatus } from "@/types/database"; -import * as api from "@/lib/api"; import { aiTestConnection } from "@/lib/api"; -import DriverManager from "@/components/config/DriverManager.vue"; -import { useToast } from "@/composables/useToast"; const { t } = useI18n(); const settingsStore = useSettingsStore(); -const { toast } = useToast(); const props = defineProps<{ open: boolean; @@ -125,13 +120,6 @@ function openExternalUrl(url: string) { } } -const jdbcDrivers = ref([]); -const isLoadingJdbcDrivers = ref(false); -const jdbcPluginStatus = ref(null); -const isInstallingJdbcPlugin = ref(false); -const isUninstallingJdbcPlugin = ref(false); -const jdbcDriverPathInput = ref(""); - watch( () => props.open, async (open) => { @@ -143,8 +131,6 @@ watch( confirmNewPassword.value = ""; await settingsStore.initAiConfig(); syncAiEditState(); - void loadJdbcDrivers(); - void loadJdbcPluginStatus(); } }, ); @@ -190,105 +176,6 @@ async function changePassword() { } } -function formatBytes(bytes: number) { - if (bytes < 1024) return `${bytes} B`; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; - return `${(bytes / 1024 / 1024).toFixed(1)} MB`; -} - -async function loadJdbcDrivers() { - if (isWeb) return; - isLoadingJdbcDrivers.value = true; - try { - jdbcDrivers.value = await api.listJdbcDrivers(); - } catch (e: any) { - toast(String(e?.message || e), 5000); - } finally { - isLoadingJdbcDrivers.value = false; - } -} - -async function loadJdbcPluginStatus() { - if (isWeb) return; - try { - jdbcPluginStatus.value = await api.jdbcPluginStatus(); - } catch (e: any) { - toast(String(e?.message || e), 5000); - } -} - -async function installJdbcPlugin() { - if (isWeb || isInstallingJdbcPlugin.value) return; - isInstallingJdbcPlugin.value = true; - try { - jdbcPluginStatus.value = await api.installJdbcPlugin(); - toast(t("settings.jdbcPluginInstallSuccess")); - await loadJdbcDrivers(); - } catch (e: any) { - toast(String(e?.message || e), 5000); - } finally { - isInstallingJdbcPlugin.value = false; - } -} - -async function uninstallJdbcPlugin() { - if (isWeb || isUninstallingJdbcPlugin.value) return; - isUninstallingJdbcPlugin.value = true; - try { - jdbcPluginStatus.value = await api.uninstallJdbcPlugin(); - toast(t("settings.jdbcPluginUninstallSuccess")); - await loadJdbcDrivers(); - } catch (e: any) { - toast(String(e?.message || e), 5000); - } finally { - isUninstallingJdbcPlugin.value = false; - } -} - -async function importJdbcDriverPaths(paths: string[]) { - if (!paths.length) return; - try { - jdbcDrivers.value = await api.importJdbcDrivers(paths); - jdbcDriverPathInput.value = ""; - toast(t("settings.jdbcImportSuccess", { count: paths.length })); - } catch (e: any) { - toast(String(e?.message || e), 5000); - } -} - -async function importJdbcDrivers() { - if (isWeb) return; - const { open } = await import("@tauri-apps/plugin-dialog"); - const selected = await open({ - title: t("settings.jdbcImport"), - multiple: true, - filters: [{ name: "JDBC Driver", extensions: ["jar"] }], - }); - if (!selected) return; - - const paths = (Array.isArray(selected) ? selected : [selected]).filter( - (path): path is string => typeof path === "string", - ); - await importJdbcDriverPaths(paths); -} - -async function importJdbcDriverPathInput() { - const paths = jdbcDriverPathInput.value - .split(/\r?\n/) - .map((path) => path.trim()) - .filter(Boolean); - await importJdbcDriverPaths(paths); -} - -async function deleteJdbcDriver(path: string) { - try { - jdbcDrivers.value = await api.deleteJdbcDriver(path); - toast(t("settings.jdbcDeleteSuccess")); - } catch (e: any) { - toast(String(e?.message || e), 5000); - } -} - // ---------- AI Settings ---------- const aiProviderDefaults: Record = { claude: { endpoint: "https://api.anthropic.com/v1/messages", model: "claude-sonnet-4-20250514" }, @@ -489,7 +376,6 @@ watch( {{ t("settings.editorTab") }} {{ t("settings.appearanceTab") }} {{ t("settings.aiTab") }} - 驱动 {{ t("settings.securityTab") }} {{ t("settings.aboutTab") }} @@ -793,102 +679,6 @@ watch( - -
-
-
- -

- {{ t("settings.jdbcPluginNotInstalled") }} -

-
-
- - {{ - jdbcPluginStatus.compatible - ? t("settings.jdbcPluginInstalled", { - version: jdbcPluginStatus.version || "-", - }) - : t("settings.jdbcPluginIncompatible") - }} - - - -
-
-
- -
-
- -
-
- - - -
-
- -
-
- {{ t("common.loading") }} -
-
- {{ t("settings.jdbcNoDrivers") }} -
-
-
-
-
{{ driver.name }}
-
{{ driver.path }}
-
-
{{ formatBytes(driver.size) }}
- -
-
-
- - - - - - -
-
diff --git a/src/components/layout/AppTabBar.vue b/src/components/layout/AppTabBar.vue index 9fe02ce97d..8d4b9468c9 100644 --- a/src/components/layout/AppTabBar.vue +++ b/src/components/layout/AppTabBar.vue @@ -1,7 +1,7 @@