diff --git a/README.md b/README.md index abf06f1e7c..83ec321808 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,7 @@ No. DBX does not collect telemetry. The auto-update feature checks GitHub Releas
Can I use DBX without an internet connection? -Yes. The desktop app works fully offline. AI features need network access to the model endpoint (or a local model via Ollama). +Yes. The desktop app works fully offline. For air-gapped driver installs, download offline driver packages from the [Offline Drivers page](https://dbxio.com/en/drivers) on an internet-connected machine, transfer them to the offline machine, then import them in DBX from Settings > Driver Manager. AI features need network access to the model endpoint (or a local model via Ollama).
diff --git a/README.zh-CN.md b/README.zh-CN.md index 557dd6c51a..a1ecba291a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -284,7 +284,7 @@ pnpm tauri build
可以离线使用吗? -可以。桌面端完全支持离线使用。AI 功能需要网络访问模型端点(或通过 Ollama 使用本地模型)。 +可以。桌面端完全支持离线使用。内网环境安装驱动时,可在有网机器打开[离线驱动下载页](https://dbxio.com/cn/drivers)下载离线驱动包,传输到内网机器后,在 DBX 的「设置 > 驱动管理」中导入。AI 功能需要网络访问模型端点(或通过 Ollama 使用本地模型)。
diff --git a/apps/desktop/src/components/config/DriverStoreDialog.vue b/apps/desktop/src/components/config/DriverStoreDialog.vue index 32f353aa92..5e7e7e8735 100644 --- a/apps/desktop/src/components/config/DriverStoreDialog.vue +++ b/apps/desktop/src/components/config/DriverStoreDialog.vue @@ -3,9 +3,11 @@ import { ref, onMounted, onUnmounted, computed, watch } from "vue"; import { useI18n } from "vue-i18n"; import { Activity, + ExternalLink, Cpu, FolderOpen, MemoryStick, + Search, Square, Trash2, Download, @@ -74,6 +76,7 @@ const driverStoreTab = ref("agent"); // ──────────── Agent drivers ──────────── const drivers = ref([]); +const agentDriverSearch = ref(""); const installing = ref(null); const upgradingAll = ref(false); const upgradingCurrent = ref(""); @@ -93,6 +96,7 @@ const runtimeError = ref(""); const runtimeBusy = ref(null); let runtimeTimer: ReturnType | null = null; const DRIVER_RUNTIME_POLL_MS = 5000; +const OFFLINE_DRIVER_DOWNLOAD_URL = "https://dbxio.com/cn/drivers"; let unlisten: (() => void) | null = null; @@ -189,6 +193,15 @@ function canInstallOrUpdateDriver(dbType: string): boolean { return Boolean(driver && (!driver.installed || driver.update_available)); } +async function openOfflineDriverDownload() { + if (isWeb) { + window.open(OFFLINE_DRIVER_DOWNLOAD_URL, "_blank", "noopener,noreferrer"); + return; + } + const { open } = await import("@tauri-apps/plugin-shell"); + await open(OFFLINE_DRIVER_DOWNLOAD_URL); +} + function queueDriverInstall(dbType: string) { queuedDriverInstalls.value = addDriverInstallQueue(queuedDriverInstalls.value, dbType, installing.value); } @@ -470,12 +483,33 @@ function formatSize(bytes: number): string { // ──────────── JDBC drivers ──────────── const jdbcDrivers = ref([]); +const jdbcDriverSearch = ref(""); const isLoadingJdbcDrivers = ref(false); const jdbcPluginStatus = ref(null); const isInstallingJdbcPlugin = ref(false); const isUninstallingJdbcPlugin = ref(false); const jdbcDriverPathInput = ref(""); +const filteredAgentDrivers = computed(() => { + const query = agentDriverSearch.value.trim().toLowerCase(); + if (!query) return drivers.value; + return drivers.value.filter((driver) => + [driver.label, driver.db_type, driver.version, driver.installed_version, driver.jre] + .filter(Boolean) + .join(" ") + .toLowerCase() + .includes(query), + ); +}); + +const filteredJdbcDrivers = computed(() => { + const query = jdbcDriverSearch.value.trim().toLowerCase(); + if (!query) return jdbcDrivers.value; + return jdbcDrivers.value.filter((driver) => + [driver.name, driver.path, String(driver.size)].join(" ").toLowerCase().includes(query), + ); +}); + function formatBytes(bytes: number) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; @@ -803,6 +837,22 @@ watch(driverStoreTab, (tab) => {
{{ formatBytes(item.bytes) }}
+
+
+
+ {{ t("driverStore.offlineDownloadHint") }} +
+ +
+
@@ -845,22 +895,43 @@ watch(driverStoreTab, (tab) => { - +
-
-
- - -
+
+ + + + + {{ + javaRuntimeConfig.mode === "system" + ? t("driverStore.systemJavaHint") + : t("driverStore.jreRuntimeAutoDownloadHint") + }} + +
-
- - -
-

- {{ t("driverStore.systemJavaHint") }} -

-
- -
-
-
-
{{ t("driverStore.jreRuntimeTitle", { jre: jre.key }) }}
-
-
- - {{ jreUsageLabel(jre.key) }} - - - {{ t("driverStore.notInstalled") }} - - - - +
+
+
+
{{ t("driverStore.jreRuntimeTitle", { jre: jre.key }) }}
+
+
+ + {{ jreUsageLabel(jre.key) }} + + + {{ t("driverStore.notInstalled") }} + + + + +
-
-
{{ t("driverStore.jreRuntime") }}
-

{{ t("driverStore.jreRuntimeAutoDownloadHint") }}

-
+
+ + +
{{ t("common.loading") }}
+
+ {{ t("driverStore.noMatchingDrivers") }} +
{{ @@ -974,7 +1042,7 @@ watch(driverStoreTab, (tab) => {
@@ -1180,6 +1248,14 @@ watch(driverStoreTab, (tab) => {
+
+ + +
{
{{ t("settings.jdbcNoDrivers") }}
+
+ {{ t("driverStore.noMatchingDrivers") }} +
-
+
{{ driver.name }}
{{ driver.path }}
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 7e730188e9..4f73946c51 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1976,6 +1976,12 @@ export default { usageAgentDrivers: "Built-in driver agents", usageJdbcPlugin: "JDBC plugin", usageJdbcDriverJars: "JDBC driver JARs", + offlineDownloadHint: + "For air-gapped environments, download offline driver packages on an internet-connected machine, then import them here.", + offlineDownloadLink: "Offline driver downloads", + searchDrivers: "Search driver name, type, version...", + searchJdbcDrivers: "Search JDBC driver name or path...", + noMatchingDrivers: "No matching drivers.", javaRuntimeSaved: "Java runtime settings saved", javaRuntimeSaveFailed: "Failed to save Java runtime settings: {error}", chooseJavaExecutable: "Choose Java executable", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index e87d6946c9..dd702dde84 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1929,6 +1929,11 @@ export default { usageAgentDrivers: "内置驱动 Agent", usageJdbcPlugin: "JDBC 插件", usageJdbcDriverJars: "JDBC 驱动 JAR", + offlineDownloadHint: "内网环境可先在有网机器下载离线驱动包,再回到这里导入。", + offlineDownloadLink: "离线驱动下载", + searchDrivers: "搜索驱动名称、类型、版本...", + searchJdbcDrivers: "搜索 JDBC 驱动名称或路径...", + noMatchingDrivers: "没有匹配的驱动。", javaRuntimeSaved: "Java 运行时设置已保存", javaRuntimeSaveFailed: "Java 运行时设置失败: {error}", chooseJavaExecutable: "选择 Java 可执行文件", diff --git a/docs/app/[lang]/drivers/DriversClient.tsx b/docs/app/[lang]/drivers/DriversClient.tsx new file mode 100644 index 0000000000..95fb74088c --- /dev/null +++ b/docs/app/[lang]/drivers/DriversClient.tsx @@ -0,0 +1,361 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useParams } from "next/navigation"; +import { LandingNav } from "@/components/landing/LandingNav"; +import { + buildDriverEntries, + buildJreEntries, + fetchAgentRegistry, + formatSize, + type AgentRegistry, + type JreDisplayEntry, +} from "@/lib/agentRegistry"; +import { AlertTriangle, Cpu, Database, Download, Loader2, Search, X } from "lucide-react"; + +const i18n = { + en: { + title: "Offline Driver Downloads", + subtitle: + "Download database drivers and JRE packages for offline use. Search for the exact resource your air-gapped environment needs.", + drivers: "Database Drivers", + driversDesc: "JDBC driver JAR files for each supported database type.", + jre: "Java Runtime (JRE)", + jreDesc: + "JRE packages used by agent-based database drivers. Required for Oracle, SQL Server, and other agent-managed connections.", + loading: "Loading driver catalog...", + error: "Unable to load driver catalog. Please check your network connection.", + retry: "Retry", + download: "Download", + version: "Version", + size: "Size", + requiresJre: "Requires JRE", + platform: "Platform", + search: "Search drivers, platforms, versions...", + noResults: "No matching downloads.", + showing: "Showing", + of: "of", + clearSearch: "Clear search", + downloadHint: + "For air-gapped environments: download these files on an internet-connected machine, then transfer them to the offline machine and import them in DBX from Settings > Driver Manager.", + }, + cn: { + title: "离线驱动下载", + subtitle: "下载数据库驱动和 JRE 离线包。搜索内网环境需要的资源,在有网机器下载后传输。", + drivers: "数据库驱动", + driversDesc: "每种支持的数据库类型对应的 JDBC 驱动 JAR 文件。", + jre: "Java 运行时 (JRE)", + jreDesc: "Agent 驱动所需的 JRE 环境,Oracle、SQL Server 等数据库通过 Agent 连接时需要。", + loading: "正在加载驱动列表...", + error: "加载驱动列表失败,请检查网络连接。", + retry: "重试", + download: "下载", + version: "版本", + size: "大小", + requiresJre: "依赖 JRE", + platform: "平台", + search: "搜索驱动、平台、版本...", + noResults: "没有匹配的下载项。", + showing: "显示", + of: "/", + clearSearch: "清空搜索", + downloadHint: "内网环境使用说明:在有网的电脑上下载这些文件,然后传输到内网机器,在 DBX 的“设置 > 驱动管理”中导入。", + }, +}; + +type ActiveTab = "drivers" | "jre"; + +function platformKey(j: JreDisplayEntry): string { + return `${j.jreKey}-${j.platformKey}`; +} + +function matchesSearch(values: Array, query: string): boolean { + if (!query) return true; + return values.filter(Boolean).join(" ").toLowerCase().includes(query); +} + +export function DriversClient() { + const params = useParams(); + const rawLang = params?.lang as string | undefined; + const lang: "en" | "cn" = rawLang === "cn" ? "cn" : "en"; + const t = i18n[lang]; + + const [registry, setRegistry] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [activeTab, setActiveTab] = useState("drivers"); + const [searchQuery, setSearchQuery] = useState(""); + + const loadRegistry = useCallback(async () => { + setLoading(true); + setError(null); + try { + const data = await fetchAgentRegistry(); + if (data) { + setRegistry(data); + } else { + setError("Unable to load driver catalog"); + } + } catch { + setError("Unable to load driver catalog"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadRegistry(); + }, [loadRegistry]); + + const drivers = useMemo(() => (registry ? buildDriverEntries(registry) : []), [registry]); + const jres = useMemo(() => (registry ? buildJreEntries(registry) : []), [registry]); + const normalizedSearch = searchQuery.trim().toLowerCase(); + + const filteredDrivers = useMemo( + () => + drivers.filter((d) => + matchesSearch([d.label, d.key, d.version, d.jre, formatSize(d.jar.size)], normalizedSearch), + ), + [drivers, normalizedSearch], + ); + + const filteredJres = useMemo( + () => + jres.filter((j) => + matchesSearch( + [j.platformLabel, j.platformKey, j.jreVersion, j.jreKey, formatSize(j.info.size)], + normalizedSearch, + ), + ), + [jres, normalizedSearch], + ); + + const activeCount = activeTab === "drivers" ? filteredDrivers.length : filteredJres.length; + const activeTotal = activeTab === "drivers" ? drivers.length : jres.length; + + return ( +
+ + +
+
+
+

+ {t.subtitle} +

+
+
+
+ +
+ {loading && ( +
+ + {t.loading} +
+ )} + + {error && !loading && ( +
+ + {t.error} + +
+ )} + + {registry && !loading && ( + <> +
+
+
+ + +
+ +
+
+ + setSearchQuery(event.target.value)} + placeholder={t.search} + className="h-9 w-full rounded-[8px] border border-landing-line bg-black/10 pl-9 pr-9 text-sm text-landing-ink outline-none transition-colors placeholder:text-landing-muted focus:border-landing-blue" + /> + {searchQuery && ( + + )} +
+ + {t.showing} {activeCount} {t.of} {activeTotal} + +
+
+ + {activeTab === "drivers" && ( + <> +

+ {t.driversDesc} +

+ + + + + + + + + + + + {filteredDrivers.map((d) => ( + + + + + + + + + ))} + +
DriverKey{t.version}{t.requiresJre}{t.size} +
+
+ {d.label} + + {d.key} + +
+
+ + {d.key} + + {d.version}{d.jre} + {formatSize(d.jar.size)} + + + + {t.download} + +
+ {filteredDrivers.length === 0 && ( +
{t.noResults}
+ )} + + )} + + {activeTab === "jre" && ( + <> +

+ {t.jreDesc} +

+ + + + + + + + + + + {filteredJres.map((j) => { + const key = platformKey(j); + return ( + + + + + + + + ); + })} + +
{t.platform}JRE{t.version}{t.size} +
+
+ {j.platformLabel} + + JRE {j.jreKey} + +
+
+ + JRE {j.jreKey} + + {j.jreVersion} + {formatSize(j.info.size)} + + + + {t.download} + +
+ {filteredJres.length === 0 && ( +
{t.noResults}
+ )} + + )} +
+ +
+ {lang === "cn" ? "离线使用说明" : "Offline Usage"} +

{t.downloadHint}

+
+ + )} +
+
+ ); +} diff --git a/docs/app/[lang]/drivers/page.tsx b/docs/app/[lang]/drivers/page.tsx new file mode 100644 index 0000000000..bcfa21b407 --- /dev/null +++ b/docs/app/[lang]/drivers/page.tsx @@ -0,0 +1,38 @@ +import { DriversClient } from "./DriversClient"; +import { buildMetadata } from "@/lib/metadata"; +import type { Metadata } from "next"; + +const pageMeta = { + en: { + title: "Offline Driver Downloads", + description: + "Download all database drivers and JRE packages for offline use. Supports 30+ database types across macOS, Linux, and Windows.", + }, + cn: { + title: "离线驱动下载", + description: + "下载所有数据库驱动和 JRE 离线包,支持 30+ 种数据库类型,覆盖 macOS、Linux、Windows 平台。", + }, +}; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ lang: string }>; +}): Promise { + const { lang } = await params; + const l = lang === "cn" ? "cn" : "en"; + const meta = pageMeta[l]; + + return buildMetadata({ + title: meta.title, + description: meta.description, + path: `/${l}/drivers`, + lang: l, + ogType: "website", + }); +} + +export default function DriversPage() { + return ; +} diff --git a/docs/components/landing/LandingNav.tsx b/docs/components/landing/LandingNav.tsx index 5f0432ad47..9d1ee83d8a 100644 --- a/docs/components/landing/LandingNav.tsx +++ b/docs/components/landing/LandingNav.tsx @@ -4,8 +4,15 @@ import Link from 'next/link'; import { useEffect, useRef } from 'react'; const i18n = { - en: { home: 'Home', docs: 'Docs', changelog: 'Changelog', community: 'Community', lang: '中文' }, - cn: { home: '首页', docs: '文档', changelog: '更新日志', community: '交流群', lang: 'English' }, + en: { + home: 'Home', + docs: 'Docs', + changelog: 'Changelog', + community: 'Community', + drivers: 'Offline Drivers', + lang: '中文', + }, + cn: { home: '首页', docs: '文档', changelog: '更新日志', community: '交流群', drivers: '离线驱动', lang: 'English' }, }; export function LandingNav({ @@ -13,7 +20,7 @@ export function LandingNav({ active, }: { lang: 'en' | 'cn'; - active?: 'home' | 'changelog' | 'community'; + active?: 'home' | 'changelog' | 'community' | 'drivers'; }) { const ref = useRef(null); const t = i18n[lang]; @@ -21,6 +28,7 @@ export function LandingNav({ const langHrefMap: Record = { changelog: `/${otherLang}/changelog`, community: `/${otherLang}/community`, + drivers: `/${otherLang}/drivers`, }; const langHref = langHrefMap[active ?? ''] ?? `/${otherLang}`; @@ -69,6 +77,12 @@ export function LandingNav({ > {t.community} + + {t.drivers} + ; +} + +export interface DriverInfo { + version: string; + label: string; + min_app_version: string; + jar: ArtifactInfo; + jre: string; +} + +export interface AgentRegistry { + jre?: JreInfo; + jres?: Record; + drivers: Record; +} + +const AGENT_REGISTRY_R2_URL = "https://dl.dbxio.com/agents/agent-registry.json"; +const AGENT_REGISTRY_GITHUB_URL = + "https://github.com/t8y2/dbx-agents/releases/latest/download/agent-registry.json"; + +export async function fetchAgentRegistry(): Promise { + const urls = [AGENT_REGISTRY_R2_URL, AGENT_REGISTRY_GITHUB_URL]; + for (const url of urls) { + try { + const res = await fetch(url, { + cache: "no-store", + headers: { Accept: "application/json" }, + }); + if (res.ok) { + return (await res.json()) as AgentRegistry; + } + } catch { + continue; + } + } + return null; +} + +export interface JreDisplayEntry { + platformKey: string; + platformLabel: string; + info: ArtifactInfo; + jreVersion: string; + jreKey: string; + r2Url: string; +} + +export interface DriverDisplayEntry { + key: string; + label: string; + version: string; + minAppVersion: string; + jar: ArtifactInfo; + jre: string; + r2Url: string; +} + +export function buildJreEntries(registry: AgentRegistry): JreDisplayEntry[] { + const entries: JreDisplayEntry[] = []; + const platformLabels: Record = { + "macos-aarch64": "macOS (Apple Silicon)", + "macos-x64": "macOS (Intel)", + "linux-aarch64": "Linux (ARM64)", + "linux-x64": "Linux (x64)", + "windows-aarch64": "Windows (ARM64)", + "windows-x64": "Windows (x64)", + }; + + const jres = registry.jres ?? {}; + for (const [key, jreInfo] of Object.entries(jres)) { + for (const [platformKey, artifact] of Object.entries(jreInfo.platforms)) { + entries.push({ + platformKey, + platformLabel: platformLabels[platformKey] ?? platformKey, + info: artifact, + jreVersion: jreInfo.version, + jreKey: key, + r2Url: githubUrlToR2Url(artifact.url, "jre"), + }); + } + } + + if (entries.length === 0 && registry.jre) { + for (const [platformKey, artifact] of Object.entries(registry.jre.platforms)) { + entries.push({ + platformKey, + platformLabel: platformLabels[platformKey] ?? platformKey, + info: artifact, + jreVersion: registry.jre.version, + jreKey: "21", + r2Url: githubUrlToR2Url(artifact.url, "jre"), + }); + } + } + + return entries; +} + +export function buildDriverEntries(registry: AgentRegistry): DriverDisplayEntry[] { + return Object.entries(registry.drivers).map(([key, info]) => ({ + key, + label: info.label, + version: info.version, + minAppVersion: info.min_app_version, + jar: info.jar, + jre: info.jre, + r2Url: githubUrlToR2Url(info.jar.url, "driver"), + })); +} + +export function formatSize(bytes: number): string { + if (bytes >= 1024 * 1024 * 1024) { + return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; + } + if (bytes >= 1024 * 1024) { + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + } + if (bytes >= 1024) { + return `${(bytes / 1024).toFixed(1)} KB`; + } + return `${bytes} B`; +}