feat(agent): pluggable database drivers via JDBC agent subprocess

Migrate domestic database support (DaMeng, KingBase, Vastbase, GoldenDB) from
built-in ODBC/protocol drivers to standalone Java agent processes communicating
via stdin/stdout JSON-RPC 2.0. Agents are distributed as fat JARs with embedded
JDBC drivers, downloaded on-demand from GitHub Releases with proxy fallback.

- Add AgentDriverClient for subprocess lifecycle + JSON-RPC communication
- Add AgentManager for JRE/JAR install state management
- Add Tauri commands for driver install/uninstall/list
- Add DriverManager UI panel in settings dialog
- Extend DatabaseType with Kingbase, Vastbase, Goldendb variants
- Migrate Dameng from ODBC (dm_driver.rs) to JDBC agent
- Delete dm_driver.rs (343 lines)
- Add GitHub proxy fallback for downloads (update.hwdns.net, gh-proxy.org)

Agent code lives at https://github.com/t8y2/dbx-agents
This commit is contained in:
t8y2
2026-05-13 04:44:52 +08:00
parent 8ef1999ec9
commit e85f9df1fe
20 changed files with 769 additions and 470 deletions
+94
View File
@@ -0,0 +1,94 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { Button } from "@/components/ui/button";
interface AgentDriverInfo {
db_type: string;
label: string;
version: string;
size: number;
installed: boolean;
installed_version: string | null;
update_available: boolean;
}
const drivers = ref<AgentDriverInfo[]>([]);
const jreInstalled = ref(false);
const installing = ref<string | null>(null);
onMounted(async () => {
await refresh();
});
async function refresh() {
jreInstalled.value = await invoke<boolean>("check_jre_installed");
drivers.value = await invoke<AgentDriverInfo[]>("list_installed_agents");
}
async function installDriver(dbType: string) {
installing.value = dbType;
try {
await invoke("install_agent", { dbType });
await refresh();
} catch (e: any) {
alert(e);
} finally {
installing.value = null;
}
}
async function uninstallDriver(dbType: string) {
try {
await invoke("uninstall_agent", { dbType });
await refresh();
} catch (e: any) {
alert(e);
}
}
function formatSize(bytes: number): string {
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
</script>
<template>
<div class="space-y-4 p-4">
<h3 class="text-lg font-medium">驱动管理</h3>
<div class="text-sm text-muted-foreground">
JRE: {{ jreInstalled ? "✓ 已安装" : "未安装" }}
<span v-if="!jreInstalled" class="ml-2 text-xs">(首次安装驱动时自动下载)</span>
</div>
<div class="space-y-2">
<div
v-for="driver in drivers"
:key="driver.db_type"
class="flex items-center justify-between rounded-md border p-3"
>
<div>
<span class="font-medium">{{ driver.label }}</span>
<span v-if="driver.installed" class="ml-2 text-xs text-muted-foreground">
v{{ driver.installed_version }}
</span>
<span class="ml-2 text-xs text-muted-foreground">{{ formatSize(driver.size) }}</span>
</div>
<div>
<Button
v-if="!driver.installed"
size="sm"
:disabled="installing !== null"
@click="installDriver(driver.db_type)"
>
{{ installing === driver.db_type ? "安装中..." : "安装" }}
</Button>
<div v-else class="flex items-center gap-2">
<span class="text-sm text-green-600">✓ 已安装</span>
<Button size="sm" variant="ghost" @click="uninstallDriver(driver.db_type)">卸载</Button>
</div>
</div>
</div>
</div>
</div>
</template>
+13 -5
View File
@@ -146,7 +146,7 @@ const driverProfiles: Record<
mariadb: { type: "mysql", port: 3306, user: "root", label: "MariaDB", icon: "mariadb" },
tidb: { type: "mysql", port: 4000, user: "root", label: "TiDB", icon: "tidb" },
oceanbase: { type: "mysql", port: 2881, user: "root", label: "OceanBase", icon: "oceanbase" },
goldendb: { type: "mysql", port: 3306, user: "root", label: "GoldenDB", icon: "goldendb" },
goldendb: { type: "goldendb", port: 3306, user: "root", label: "GoldenDB", icon: "goldendb" },
opengauss: {
type: "gaussdb",
port: 5432,
@@ -155,8 +155,8 @@ const driverProfiles: Record<
icon: "opengauss",
},
gaussdb: { type: "gaussdb", port: 5432, user: "gaussdb", label: "GaussDB", icon: "gaussdb" },
kingbase: { type: "postgres", port: 54321, user: "system", label: "KingBase", icon: "kingbase" },
vastbase: { type: "postgres", port: 5432, user: "vastbase", label: "Vastbase", icon: "vastbase" },
kingbase: { type: "kingbase", port: 54321, user: "system", label: "KingBase", icon: "kingbase" },
vastbase: { type: "vastbase", port: 5432, user: "vastbase", label: "Vastbase", icon: "vastbase" },
doris: { type: "mysql", port: 9030, user: "root", label: "Doris", icon: "doris", urlParams: "" },
selectdb: {
type: "mysql",
@@ -322,7 +322,8 @@ function defaultDatabaseForProfile() {
if (form.value.db_type === "redshift") return "dev";
if (form.value.db_type === "gaussdb") return "postgres";
if (selectedType.value === "cockroachdb") return "defaultdb";
if (form.value.db_type === "postgres") return "postgres";
if (form.value.db_type === "postgres" || form.value.db_type === "kingbase" || form.value.db_type === "vastbase")
return "postgres";
if (form.value.db_type === "sqlserver") return "master";
if (form.value.db_type === "oracle") return "ORCL";
return "";
@@ -1116,7 +1117,14 @@ function openExternalUrl(url: string) {
</div>
<div
v-if="form.db_type === 'mysql' || form.db_type === 'postgres' || form.db_type === 'redshift'"
v-if="
form.db_type === 'mysql' ||
form.db_type === 'postgres' ||
form.db_type === 'redshift' ||
form.db_type === 'kingbase' ||
form.db_type === 'vastbase' ||
form.db_type === 'goldendb'
"
class="grid grid-cols-4 items-center gap-4"
>
<Label class="text-right">{{ t("connection.urlParams") }}</Label>
@@ -25,6 +25,7 @@ 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();
@@ -489,6 +490,7 @@ watch(
<TabsTrigger value="appearance" class="flex-1">{{ t("settings.appearanceTab") }}</TabsTrigger>
<TabsTrigger value="ai" class="flex-1">{{ t("settings.aiTab") }}</TabsTrigger>
<TabsTrigger v-if="!isWeb" value="jdbc" class="flex-1">{{ t("settings.jdbcTab") }}</TabsTrigger>
<TabsTrigger v-if="!isWeb" value="drivers" class="flex-1">驱动管理</TabsTrigger>
<TabsTrigger v-if="isWeb" value="security" class="flex-1">{{ t("settings.securityTab") }}</TabsTrigger>
<TabsTrigger value="about" class="flex-1">{{ t("settings.aboutTab") }}</TabsTrigger>
</TabsList>
@@ -928,6 +930,10 @@ watch(
</DialogFooter>
</TabsContent>
<TabsContent v-if="!isWeb" value="drivers" class="py-2">
<DriverManager />
</TabsContent>
<TabsContent value="about" class="space-y-5 py-2">
<div class="rounded-lg border bg-muted/20 p-4">
<div class="flex items-start justify-between gap-4">
+13
View File
@@ -7,6 +7,8 @@ export const SCHEMA_AWARE_TYPES = new Set<DatabaseType>([
"redshift",
"dameng",
"gaussdb",
"kingbase",
"vastbase",
"jdbc",
]);
@@ -21,6 +23,9 @@ export const DIAGRAM_SUPPORTED_TYPES = new Set<DatabaseType>([
"redshift",
"dameng",
"gaussdb",
"kingbase",
"vastbase",
"goldendb",
]);
export const DATABASE_SEARCH_SUPPORTED_TYPES = new Set<DatabaseType>([
@@ -34,6 +39,9 @@ export const DATABASE_SEARCH_SUPPORTED_TYPES = new Set<DatabaseType>([
"clickhouse",
"dameng",
"gaussdb",
"kingbase",
"vastbase",
"goldendb",
]);
export const TABLE_IMPORT_SUPPORTED_TYPES = new Set<DatabaseType>([
@@ -49,6 +57,9 @@ export const TABLE_IMPORT_SUPPORTED_TYPES = new Set<DatabaseType>([
"redshift",
"dameng",
"gaussdb",
"kingbase",
"vastbase",
"goldendb",
]);
export const TABLE_STRUCTURE_SUPPORTED_TYPES = new Set<DatabaseType>(["mysql", "postgres", "sqlite", "sqlserver"]);
@@ -85,6 +96,8 @@ export const TREE_SCHEMA_TYPES = new Set<DatabaseType>(["postgres", "redshift",
export const PG_LIKE_STRUCTURE_TYPES = new Set<DatabaseType>(["postgres", "redshift", "gaussdb"]);
export const AGENT_DRIVER_TYPES = new Set<DatabaseType>(["dameng", "kingbase", "vastbase", "goldendb"]);
export const TRANSFER_SQL_TYPES = new Set<DatabaseType>([
"mysql",
"postgres",
+9
View File
@@ -159,6 +159,9 @@ export const useConnectionStore = defineStore("connection", () => {
redshift: "Redshift",
dameng: "DM (Dameng)",
gaussdb: "GaussDB",
kingbase: "KingBase",
vastbase: "Vastbase",
goldendb: "GoldenDB",
};
const profile = config.driver_profile || config.db_type;
@@ -167,6 +170,12 @@ export const useConnectionStore = defineStore("connection", () => {
dbType = "gaussdb" as ConnectionConfig["db_type"];
} else if (profile === "redshift" && dbType === "postgres") {
dbType = "redshift" as ConnectionConfig["db_type"];
} else if (profile === "kingbase" && dbType === "postgres") {
dbType = "kingbase" as ConnectionConfig["db_type"];
} else if (profile === "vastbase" && dbType === "postgres") {
dbType = "vastbase" as ConnectionConfig["db_type"];
} else if (profile === "goldendb" && dbType === "mysql") {
dbType = "goldendb" as ConnectionConfig["db_type"];
}
return {
+3
View File
@@ -14,6 +14,9 @@ export type DatabaseType =
| "redshift"
| "dameng"
| "gaussdb"
| "kingbase"
| "vastbase"
| "goldendb"
| "jdbc";
export interface ConnectionConfig {