feat(web): support driver management

This commit is contained in:
t8y2 2026-05-18 18:16:25 +08:00
parent af16ac219c
commit b01c17e709
13 changed files with 766 additions and 49 deletions

2
Cargo.lock generated
View File

@ -1766,9 +1766,11 @@ dependencies = [
"argon2",
"async-stream",
"axum",
"chrono",
"dbx-core",
"futures",
"log",
"reqwest 0.12.28",
"rustls 0.23.40",
"serde",
"serde_json",

View File

@ -623,14 +623,16 @@ function handleContextMenu(e: MouseEvent) {
e.preventDefault();
}
function openDriverStoreFromEvent() {
showDriverStore.value = true;
}
onMounted(async () => {
console.log("[STARTUP] onMounted begin");
const mountStart = performance.now();
applyTheme();
window.addEventListener("keydown", handleKeydown, true);
window.addEventListener("dbx-open-driver-store", () => {
showDriverStore.value = true;
});
window.addEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
if (isDesktop) {
document.addEventListener("contextmenu", handleContextMenu);
}
@ -684,6 +686,7 @@ onUnmounted(() => {
clearInterval(updateCheckTimer);
}
window.removeEventListener("keydown", handleKeydown, true);
window.removeEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
document.removeEventListener("contextmenu", handleContextMenu);
});
</script>

View File

@ -1,8 +1,5 @@
<script setup lang="ts">
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, RefreshCw, Check, Clock3 } from "lucide-vue-next";
import { Button } from "@/components/ui/button";
@ -17,6 +14,7 @@ import { isTauriRuntime } from "@/lib/tauriRuntime";
import { countAvailableAgentDriverUpdates } from "@/lib/agentDriverUpdateBadge";
import type { JdbcDriverInfo, JdbcPluginStatus } from "@/types/database";
import * as api from "@/lib/api";
import type { AgentDriverInfo, JavaRuntimeConfig } from "@/lib/api";
import {
addDriverInstallQueue,
driverInstallProgressPercent,
@ -36,25 +34,6 @@ const emit = defineEmits<{
// Agent drivers
interface AgentDriverInfo {
db_type: string;
label: string;
version: string;
size: number;
installed: boolean;
installed_version: string | null;
update_available: boolean;
jre: string;
jre_installed: boolean;
}
type JavaRuntimeMode = "managed" | "system" | "custom";
interface JavaRuntimeConfig {
mode: JavaRuntimeMode;
custom_java_path: string | null;
}
const drivers = ref<AgentDriverInfo[]>([]);
const installing = ref<string | null>(null);
const upgradingAll = ref(false);
@ -69,7 +48,7 @@ const javaRuntimeConfig = ref<JavaRuntimeConfig>({ mode: "managed", custom_java_
const customJavaPath = ref("");
const savingJavaRuntime = ref(false);
let unlisten: UnlistenFn | null = null;
let unlisten: (() => void) | null = null;
const installedJres = computed(() => {
const jreMap = new Map<string, boolean>();
@ -138,13 +117,13 @@ function removeQueuedDriverInstall(dbType: string) {
}
async function refreshAgents() {
updateAgentDrivers(await invoke<AgentDriverInfo[]>("list_installed_agents"));
updateAgentDrivers(await api.listInstalledAgents());
}
async function forceRefresh() {
refreshing.value = true;
try {
await invoke("invalidate_agent_registry_cache");
await api.invalidateAgentRegistryCache();
await refreshAgents();
} finally {
refreshing.value = false;
@ -152,7 +131,7 @@ async function forceRefresh() {
}
async function loadJavaRuntimeConfig() {
const config = await invoke<JavaRuntimeConfig>("get_agent_java_runtime_config");
const config = await api.getAgentJavaRuntimeConfig();
javaRuntimeConfig.value = config;
customJavaPath.value = config.custom_java_path ?? "";
}
@ -166,11 +145,9 @@ function setJavaRuntimeMode(value: any) {
async function saveJavaRuntimeConfig() {
savingJavaRuntime.value = true;
try {
const config = await invoke<JavaRuntimeConfig>("set_agent_java_runtime_config", {
config: {
mode: javaRuntimeConfig.value.mode,
custom_java_path: javaRuntimeConfig.value.mode === "custom" ? customJavaPath.value.trim() || null : null,
},
const config = await api.setAgentJavaRuntimeConfig({
mode: javaRuntimeConfig.value.mode,
custom_java_path: javaRuntimeConfig.value.mode === "custom" ? customJavaPath.value.trim() || null : null,
});
javaRuntimeConfig.value = config;
customJavaPath.value = config.custom_java_path ?? "";
@ -208,7 +185,7 @@ async function runDriverInstall(dbType: string) {
installing.value = dbType;
progress.value = null;
try {
await invoke("install_agent", { dbType });
await api.installAgent(dbType);
await refreshAgents();
toast(`${label} 驱动安装成功`);
} catch (e: any) {
@ -235,7 +212,7 @@ async function upgradeAll() {
queuedDriverInstalls.value = [];
progress.value = null;
try {
const count = await invoke<number>("upgrade_all_agents");
const count = await api.upgradeAllAgents();
await refreshAgents();
toast(`${count} 个驱动升级完成`);
} catch (e: any) {
@ -252,7 +229,7 @@ async function upgradeAll() {
async function uninstallDriver(dbType: string) {
const label = drivers.value.find((d) => d.db_type === dbType)?.label ?? dbType;
try {
await invoke("uninstall_agent", { dbType });
await api.uninstallAgent(dbType);
await refreshAgents();
toast(`${label} 驱动已卸载`);
} catch (e: any) {
@ -264,7 +241,7 @@ async function reinstallJre(jreKey: string) {
reinstallingJre.value = jreKey;
progress.value = null;
try {
await invoke("reinstall_jre", { jreKey });
await api.reinstallJre(jreKey);
await refreshAgents();
toast(`JRE ${jreKey} 重新安装成功`);
} catch (e: any) {
@ -277,7 +254,7 @@ async function reinstallJre(jreKey: string) {
async function uninstallJre(jreKey: string) {
try {
await invoke("uninstall_jre", { jreKey });
await api.uninstallJre(jreKey);
await refreshAgents();
toast(`JRE ${jreKey} 已卸载`);
} catch (e: any) {
@ -352,7 +329,7 @@ async function installJdbcPluginLocal() {
if (typeof selected !== "string") return;
isInstallingJdbcPlugin.value = true;
try {
jdbcPluginStatus.value = await invoke<JdbcPluginStatus>("install_jdbc_plugin_local", { path: selected });
jdbcPluginStatus.value = await api.installJdbcPluginLocal(selected);
toast(t("settings.jdbcPluginInstallSuccess"));
await loadJdbcDrivers();
} catch (e: any) {
@ -423,19 +400,18 @@ async function deleteJdbcDriver(path: string) {
// Lifecycle
onMounted(async () => {
updateAgentDrivers(await invoke<AgentDriverInfo[]>("list_installed_agents_local"));
updateAgentDrivers(await api.listInstalledAgentsLocal());
void loadJavaRuntimeConfig();
invoke<AgentDriverInfo[]>("list_installed_agents").then((result) => {
api.listInstalledAgents().then((result) => {
updateAgentDrivers(result);
});
unlisten = await listen<DriverInstallProgress>("agent-install-progress", (event) => {
const payload = event.payload as any;
unlisten = await api.listenAgentInstallProgress((payload) => {
if (payload.step === "done" || payload.step === "all-done") {
progress.value = null;
} else {
progress.value = payload;
progress.value = payload as DriverInstallProgress;
}
if (payload.db_type && payload.total_drivers) {
upgradingCurrent.value = drivers.value.find((d) => d.db_type === payload.db_type)?.label ?? payload.db_type;

View File

@ -1,6 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { uuid } from "@/lib/utils";
import { useI18n } from "vue-i18n";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
@ -736,10 +735,10 @@ async function loadJdbcDrivers() {
}
async function loadAgentDrivers() {
if (!isDesktop) return;
try {
agentDrivers.value = await invoke<AgentDriverInstallState[]>("list_installed_agents_local");
invoke<AgentDriverInstallState[]>("list_installed_agents")
agentDrivers.value = await api.listInstalledAgentsLocal();
api
.listInstalledAgents()
.then((drivers) => {
agentDrivers.value = drivers;
})

View File

@ -42,7 +42,19 @@ export const importJdbcDrivers = forward("importJdbcDrivers");
export const deleteJdbcDriver = forward("deleteJdbcDriver");
export const jdbcPluginStatus = forward("jdbcPluginStatus");
export const installJdbcPlugin = forward("installJdbcPlugin");
export const installJdbcPluginLocal = forward("installJdbcPluginLocal");
export const uninstallJdbcPlugin = forward("uninstallJdbcPlugin");
export const listInstalledAgentsLocal = forward("listInstalledAgentsLocal");
export const listInstalledAgents = forward("listInstalledAgents");
export const installAgent = forward("installAgent");
export const upgradeAllAgents = forward("upgradeAllAgents");
export const uninstallAgent = forward("uninstallAgent");
export const getAgentJavaRuntimeConfig = forward("getAgentJavaRuntimeConfig");
export const setAgentJavaRuntimeConfig = forward("setAgentJavaRuntimeConfig");
export const invalidateAgentRegistryCache = forward("invalidateAgentRegistryCache");
export const reinstallJre = forward("reinstallJre");
export const uninstallJre = forward("uninstallJre");
export const listenAgentInstallProgress = forward("listenAgentInstallProgress");
export const loadSavedSqlLibrary = forward("loadSavedSqlLibrary");
export const saveSavedSqlFolder = forward("saveSavedSqlFolder");
export const deleteSavedSqlFolder = forward("deleteSavedSqlFolder");
@ -157,6 +169,10 @@ export type {
AiStreamChunk,
AiChatMessage,
AiConversation,
AgentDriverInfo,
JavaRuntimeMode,
JavaRuntimeConfig,
DriverInstallProgress,
UpdateInfo,
RedisDatabaseInfo,
RedisKeyInfo,

View File

@ -20,9 +20,12 @@ import type {
} from "@/types/database";
import type { AiConfig } from "@/stores/settingsStore";
import type {
AgentDriverInfo,
AiCompletionRequest,
AiStreamChunk,
AiConversation,
DriverInstallProgress,
JavaRuntimeConfig,
UpdateInfo,
RedisDatabaseInfo,
RedisValue,
@ -125,10 +128,69 @@ export async function installJdbcPlugin(): Promise<JdbcPluginStatus> {
return { installed: false, version: null, protocol_version: null, compatible: true, path: "" };
}
export async function installJdbcPluginLocal(_path: string): Promise<JdbcPluginStatus> {
return { installed: false, version: null, protocol_version: null, compatible: true, path: "" };
}
export async function uninstallJdbcPlugin(): Promise<JdbcPluginStatus> {
return { installed: false, version: null, protocol_version: null, compatible: true, path: "" };
}
export async function listInstalledAgentsLocal(): Promise<AgentDriverInfo[]> {
return get("/api/agents/installed-local");
}
export async function listInstalledAgents(): Promise<AgentDriverInfo[]> {
return get("/api/agents/installed");
}
export async function installAgent(dbType: string): Promise<void> {
await post("/api/agents/install", { dbType });
}
export async function upgradeAllAgents(): Promise<number> {
const result: { count: number } = await post("/api/agents/upgrade-all", {});
return result.count;
}
export async function uninstallAgent(dbType: string): Promise<void> {
await post("/api/agents/uninstall", { dbType });
}
export async function getAgentJavaRuntimeConfig(): Promise<JavaRuntimeConfig> {
return get("/api/agents/java-runtime");
}
export async function setAgentJavaRuntimeConfig(config: JavaRuntimeConfig): Promise<JavaRuntimeConfig> {
return post("/api/agents/java-runtime", { config });
}
export async function invalidateAgentRegistryCache(): Promise<void> {
await post("/api/agents/invalidate-registry-cache", {});
}
export async function reinstallJre(jreKey?: string): Promise<void> {
await post("/api/agents/reinstall-jre", { jreKey });
}
export async function uninstallJre(jreKey: string): Promise<void> {
await post("/api/agents/uninstall-jre", { jreKey });
}
export async function listenAgentInstallProgress(
handler: (progress: DriverInstallProgress) => void,
): Promise<() => void> {
const es = new EventSource("/api/agents/progress/global");
es.onmessage = (event) => {
try {
handler(JSON.parse(event.data));
} catch {
/* ignore malformed progress events */
}
};
return () => es.close();
}
export async function loadSavedSqlLibrary(): Promise<SavedSqlLibrary> {
return get("/api/saved-sql");
}

View File

@ -21,6 +21,34 @@ import type {
} from "@/types/database";
import type { AiConfig } from "@/stores/settingsStore";
export interface AgentDriverInfo {
db_type: string;
label: string;
version: string;
size: number;
installed: boolean;
installed_version: string | null;
update_available: boolean;
jre: string;
jre_installed: boolean;
}
export type JavaRuntimeMode = "managed" | "system" | "custom";
export interface JavaRuntimeConfig {
mode: JavaRuntimeMode;
custom_java_path: string | null;
}
export interface DriverInstallProgress {
step: string;
downloaded?: number;
total?: number;
db_type?: string;
current?: number;
total_drivers?: number;
}
export interface AiMessage {
role: "user" | "assistant" | "system";
content: string;
@ -304,10 +332,60 @@ export async function installJdbcPlugin(): Promise<JdbcPluginStatus> {
return invoke("install_jdbc_plugin");
}
export async function installJdbcPluginLocal(path: string): Promise<JdbcPluginStatus> {
return invoke("install_jdbc_plugin_local", { path });
}
export async function uninstallJdbcPlugin(): Promise<JdbcPluginStatus> {
return invoke("uninstall_jdbc_plugin");
}
export async function listInstalledAgentsLocal(): Promise<AgentDriverInfo[]> {
return invoke("list_installed_agents_local");
}
export async function listInstalledAgents(): Promise<AgentDriverInfo[]> {
return invoke("list_installed_agents");
}
export async function installAgent(dbType: string): Promise<void> {
return invoke("install_agent", { dbType });
}
export async function upgradeAllAgents(): Promise<number> {
return invoke("upgrade_all_agents");
}
export async function uninstallAgent(dbType: string): Promise<void> {
return invoke("uninstall_agent", { dbType });
}
export async function getAgentJavaRuntimeConfig(): Promise<JavaRuntimeConfig> {
return invoke("get_agent_java_runtime_config");
}
export async function setAgentJavaRuntimeConfig(config: JavaRuntimeConfig): Promise<JavaRuntimeConfig> {
return invoke("set_agent_java_runtime_config", { config });
}
export async function invalidateAgentRegistryCache(): Promise<void> {
return invoke("invalidate_agent_registry_cache");
}
export async function reinstallJre(jreKey?: string): Promise<void> {
return invoke("reinstall_jre", { jreKey });
}
export async function uninstallJre(jreKey: string): Promise<void> {
return invoke("uninstall_jre", { jreKey });
}
export async function listenAgentInstallProgress(
handler: (progress: DriverInstallProgress) => void,
): Promise<UnlistenFn> {
return listen<DriverInstallProgress>("agent-install-progress", (event) => handler(event.payload));
}
export async function loadSavedSqlLibrary(): Promise<SavedSqlLibrary> {
return invoke("load_saved_sql_library");
}

View File

@ -23,3 +23,5 @@ async-stream = "0.3"
futures = "0.3"
rustls = { version = "0.23", features = ["aws-lc-rs"] }
tokio-util = "0.7"
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] }
chrono = "0.4"

View File

@ -82,6 +82,20 @@ async fn main() {
.route("/connection/save", post(routes::connection::save_connections))
.route("/connection/list", get(routes::connection::load_connections))
.route("/plugins", get(routes::plugins::list_plugins))
// Agent drivers
.route("/agents/installed-local", get(routes::agents::list_installed_agents_local))
.route("/agents/installed", get(routes::agents::list_installed_agents))
.route("/agents/install", post(routes::agents::install_agent))
.route("/agents/upgrade-all", post(routes::agents::upgrade_all_agents))
.route("/agents/uninstall", post(routes::agents::uninstall_agent))
.route(
"/agents/java-runtime",
get(routes::agents::get_agent_java_runtime_config).post(routes::agents::set_agent_java_runtime_config),
)
.route("/agents/invalidate-registry-cache", post(routes::agents::invalidate_agent_registry_cache))
.route("/agents/reinstall-jre", post(routes::agents::reinstall_jre))
.route("/agents/uninstall-jre", post(routes::agents::uninstall_jre))
.route("/agents/progress/{operationId}", get(routes::agents::agent_progress))
// Schema
.route("/schema/databases", get(routes::schema::list_databases))
.route("/schema/schemas", get(routes::schema::list_schemas))

View File

@ -0,0 +1,501 @@
use std::path::PathBuf;
use std::sync::Arc;
use axum::extract::{Path, State};
use axum::response::sse::{Event, Sse};
use axum::Json;
use dbx_core::agent_manager::{
AgentDriverInfo, AgentManager, AgentRegistry, AgentState, InstalledDriver, JavaRuntimeConfig, JavaRuntimeMode,
DEFAULT_JRE_KEY,
};
use futures::Stream;
use serde::Deserialize;
use tokio::sync::{broadcast, Mutex};
use crate::error::AppError;
use crate::state::WebState;
const REGISTRY_PATH: &str = "https://github.com/t8y2/dbx-agents/releases/latest/download/agent-registry.json";
const REGISTRY_R2_PATH: &str = "agents/agent-registry.json";
static REGISTRY_CACHE: std::sync::LazyLock<Mutex<Option<(std::time::Instant, AgentRegistry)>>> =
std::sync::LazyLock::new(|| Mutex::new(None));
const AGENT_TYPES: &[(&str, &str)] = &[
("dameng", "达梦 DM8"),
("kingbase", "人大金仓 KingbaseES"),
("highgo", "瀚高 HighGo"),
("vastbase", "Vastbase"),
("goldendb", "GoldenDB"),
("access", "Microsoft Access"),
("oracle", "Oracle"),
("oracle-10g", "Oracle 10g"),
("h2", "H2"),
("snowflake", "Snowflake"),
("trino", "Trino (Presto)"),
("hive", "Apache Hive"),
("db2", "IBM DB2"),
("informix", "IBM Informix"),
("neo4j", "Neo4j"),
("cassandra", "Apache Cassandra"),
("bigquery", "Google BigQuery"),
("kylin", "Apache Kylin"),
("sundb", "SunDB"),
("gaussdb", "GaussDB"),
("yashandb", "崖山 YashanDB"),
("tdengine", "TDengine"),
("mongodb", "MongoDB (Legacy)"),
];
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentTypeRequest {
pub db_type: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JreRequest {
pub jre_key: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JavaRuntimeRequest {
pub config: JavaRuntimeConfig,
}
fn build_agent_list(am: &AgentManager, registry: Option<&AgentRegistry>) -> Vec<AgentDriverInfo> {
let local_state = am.load_state();
AGENT_TYPES
.iter()
.map(|(key, label)| {
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(),
version: remote.map(|r| r.version.clone()).unwrap_or_default(),
size: remote.map(|r| r.jar.size).unwrap_or(0),
installed,
installed_version: local.map(|l| l.version.clone()),
update_available: match (local, remote) {
(Some(l), Some(r)) => l.version != r.version,
_ => false,
},
jre: jre_key.clone(),
jre_installed: am.is_jre_installed(&jre_key),
}
})
.collect()
}
pub async fn list_installed_agents_local(
State(state): State<Arc<WebState>>,
) -> Result<Json<Vec<AgentDriverInfo>>, AppError> {
Ok(Json(build_agent_list(&state.app.agent_manager, None)))
}
pub async fn list_installed_agents(State(state): State<Arc<WebState>>) -> Result<Json<Vec<AgentDriverInfo>>, AppError> {
let registry = fetch_registry().await.ok();
Ok(Json(build_agent_list(&state.app.agent_manager, registry.as_ref())))
}
pub async fn install_agent(
State(state): State<Arc<WebState>>,
Json(req): Json<AgentTypeRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
let tx = progress_sender(&state, "global").await;
install_agent_core(&state.app.agent_manager, &req.db_type, &tx, None, None).await.map_err(AppError)?;
Ok(Json(serde_json::json!({ "ok": true })))
}
pub async fn upgrade_all_agents(State(state): State<Arc<WebState>>) -> Result<Json<serde_json::Value>, AppError> {
let tx = progress_sender(&state, "global").await;
let am = &state.app.agent_manager;
let registry = fetch_registry().await.map_err(AppError)?;
let agents = build_agent_list(am, Some(&registry));
let updatable: Vec<&AgentDriverInfo> = agents.iter().filter(|agent| agent.update_available).collect();
let total = updatable.len() as u32;
for (index, agent) in updatable.iter().enumerate() {
install_agent_from_registry(am, &registry, &agent.db_type, &tx, Some((index + 1) as u32), Some(total)).await?;
}
send_progress(&tx, serde_json::json!({ "step": "all-done" }));
Ok(Json(serde_json::json!({ "count": total })))
}
pub async fn uninstall_agent(
State(state): State<Arc<WebState>>,
Json(req): Json<AgentTypeRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
let am = &state.app.agent_manager;
let jar_path = am.driver_jar_path(&req.db_type);
if jar_path.exists() {
std::fs::remove_file(&jar_path).map_err(|err| AppError(err.to_string()))?;
}
if let Some(driver_dir) = jar_path.parent() {
if driver_dir.exists() {
std::fs::remove_dir_all(driver_dir).map_err(|err| AppError(err.to_string()))?;
}
}
let mut local_state = am.load_state();
local_state.installed_drivers.remove(&req.db_type);
am.save_state(&local_state).map_err(AppError)?;
Ok(Json(serde_json::json!({ "ok": true })))
}
pub async fn get_agent_java_runtime_config(
State(state): State<Arc<WebState>>,
) -> Result<Json<JavaRuntimeConfig>, AppError> {
Ok(Json(state.app.agent_manager.load_state().java_runtime))
}
pub async fn set_agent_java_runtime_config(
State(state): State<Arc<WebState>>,
Json(req): Json<JavaRuntimeRequest>,
) -> Result<Json<JavaRuntimeConfig>, AppError> {
let am = &state.app.agent_manager;
let mut config = req.config;
if config.mode == JavaRuntimeMode::Custom || config.mode == JavaRuntimeMode::System {
let candidate_state = AgentState { java_runtime: config.clone(), ..am.load_state() };
let resolved = am.resolve_java_runtime(&candidate_state, DEFAULT_JRE_KEY).map_err(AppError)?;
if config.mode == JavaRuntimeMode::Custom {
config.custom_java_path = Some(resolved.to_string_lossy().to_string());
}
}
if config.mode != JavaRuntimeMode::Custom {
config.custom_java_path = None;
}
let mut local_state = am.load_state();
local_state.java_runtime = config.clone();
am.save_state(&local_state).map_err(AppError)?;
am.stop_daemons().await;
Ok(Json(config))
}
pub async fn invalidate_agent_registry_cache() -> Result<Json<serde_json::Value>, AppError> {
*REGISTRY_CACHE.lock().await = None;
Ok(Json(serde_json::json!({ "ok": true })))
}
pub async fn reinstall_jre(
State(state): State<Arc<WebState>>,
Json(req): Json<JreRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
let tx = progress_sender(&state, "global").await;
reinstall_jre_core(&state.app.agent_manager, req.jre_key.as_deref().unwrap_or(DEFAULT_JRE_KEY), &tx)
.await
.map_err(AppError)?;
Ok(Json(serde_json::json!({ "ok": true })))
}
pub async fn uninstall_jre(
State(state): State<Arc<WebState>>,
Json(req): Json<JreRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
let key = req.jre_key.as_deref().unwrap_or(DEFAULT_JRE_KEY);
let am = &state.app.agent_manager;
let local_state = am.load_state();
let dependents: Vec<&str> =
local_state.installed_drivers.iter().filter(|(_, driver)| driver.jre == key).map(|(k, _)| k.as_str()).collect();
if !dependents.is_empty() {
return Err(AppError(format!("JRE {} 正在被以下驱动使用: {},请先卸载这些驱动", key, dependents.join(", "))));
}
let jre_dir = am.jre_dir(key);
if jre_dir.exists() {
std::fs::remove_dir_all(&jre_dir).map_err(|err| AppError(format!("Failed to remove JRE: {err}")))?;
}
let mut local_state = am.load_state();
local_state.jre_versions.remove(key);
am.save_state(&local_state).map_err(AppError)?;
Ok(Json(serde_json::json!({ "ok": true })))
}
pub async fn agent_progress(
State(state): State<Arc<WebState>>,
Path(operation_id): Path<String>,
) -> Result<Sse<impl Stream<Item = Result<Event, std::convert::Infallible>>>, AppError> {
let tx = progress_sender(&state, &operation_id).await;
Ok(crate::sse::sse_from_channel(tx.subscribe()))
}
async fn progress_sender(state: &WebState, operation_id: &str) -> broadcast::Sender<String> {
let mut channels = state.sse_channels.write().await;
channels
.entry(format!("agent-install-progress:{operation_id}"))
.or_insert_with(|| {
let (tx, _) = broadcast::channel::<String>(256);
tx
})
.clone()
}
async fn install_agent_core(
am: &AgentManager,
db_type: &str,
tx: &broadcast::Sender<String>,
current: Option<u32>,
total_drivers: Option<u32>,
) -> Result<(), String> {
match fetch_registry().await {
Ok(registry) => install_agent_from_registry(am, &registry, db_type, tx, current, total_drivers).await,
Err(registry_err) => {
if let Some(local_jar) = find_local_agent_jar(db_type) {
install_local_agent(am, db_type, local_jar)?;
send_progress(tx, serde_json::json!({ "step": "done" }));
return Ok(());
}
Err(registry_err)
}
}
}
async fn install_agent_from_registry(
am: &AgentManager,
registry: &AgentRegistry,
db_type: &str,
tx: &broadcast::Sender<String>,
current: Option<u32>,
total_drivers: Option<u32>,
) -> Result<(), String> {
let Some(driver) = registry.drivers.get(db_type) else {
if let Some(local_jar) = find_local_agent_jar(db_type) {
install_local_agent(am, db_type, local_jar)?;
send_progress(tx, serde_json::json!({ "step": "done" }));
return Ok(());
}
return Err(format!("Unknown driver type: {db_type}"));
};
let jre_key = &driver.jre;
let needs_jre = am.load_state().java_runtime.mode == JavaRuntimeMode::Managed && !am.is_jre_installed(jre_key);
if needs_jre {
let jre_info =
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");
send_install_progress(tx, "jre", 0, platform_jre.size, Some(db_type), current, total_drivers);
download_with_progress(
tx,
"jre",
&platform_jre.url,
&github_url_to_r2_path(&platform_jre.url, "jre"),
&jre_archive,
platform_jre.size,
Some(db_type),
current,
total_drivers,
)
.await?;
send_install_progress(tx, "jre-extract", 0, 0, Some(db_type), current, total_drivers);
extract_archive(&jre_archive, &am.jre_dir(jre_key))?;
std::fs::remove_file(&jre_archive).ok();
}
let jar_path = am.driver_jar_path(db_type);
send_install_progress(tx, "driver", 0, driver.jar.size, Some(db_type), current, total_drivers);
download_with_progress(
tx,
"driver",
&driver.jar.url,
&github_url_to_r2_path(&driver.jar.url, "driver"),
&jar_path,
driver.jar.size,
Some(db_type),
current,
total_drivers,
)
.await?;
let mut local_state = am.load_state();
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.to_string(),
InstalledDriver {
version: driver.version.clone(),
installed_at: chrono::Utc::now().to_rfc3339(),
jre: jre_key.clone(),
},
);
am.save_state(&local_state)?;
send_progress(tx, serde_json::json!({ "step": "done" }));
Ok(())
}
async fn reinstall_jre_core(am: &AgentManager, jre_key: &str, tx: &broadcast::Sender<String>) -> Result<(), String> {
let jre_dir = am.jre_dir(jre_key);
if jre_dir.exists() {
std::fs::remove_dir_all(&jre_dir).map_err(|err| format!("Failed to remove old JRE: {err}"))?;
}
let registry = fetch_registry().await?;
let jre_info = 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");
download_with_progress(
tx,
"jre",
&platform_jre.url,
&github_url_to_r2_path(&platform_jre.url, "jre"),
&jre_archive,
platform_jre.size,
None,
None,
None,
)
.await?;
extract_archive(&jre_archive, &jre_dir)?;
std::fs::remove_file(&jre_archive).ok();
let mut local_state = am.load_state();
local_state.jre_versions.insert(jre_key.to_string(), jre_info.version.clone());
am.save_state(&local_state)?;
send_progress(tx, serde_json::json!({ "step": "done" }));
Ok(())
}
fn local_agent_jar_candidates(db_type: &str) -> Vec<PathBuf> {
let jar_name = format!("dbx-agent-{db_type}.jar");
let relative = PathBuf::from("..").join("dbx-agents").join(db_type).join("build").join("libs").join(&jar_name);
let nested = PathBuf::from("dbx-agents").join(db_type).join("build").join("libs").join(&jar_name);
vec![relative, nested]
}
fn find_local_agent_jar(db_type: &str) -> Option<PathBuf> {
local_agent_jar_candidates(db_type).into_iter().find(|path| path.exists())
}
fn install_local_agent(am: &AgentManager, db_type: &str, source: PathBuf) -> Result<(), String> {
let jar_path = am.driver_jar_path(db_type);
let parent = jar_path.parent().ok_or_else(|| format!("Invalid driver path: {}", jar_path.display()))?;
std::fs::create_dir_all(parent).map_err(|err| err.to_string())?;
std::fs::copy(&source, &jar_path).map_err(|err| format!("Failed to copy local agent jar: {err}"))?;
let mut local_state = am.load_state();
local_state.installed_drivers.insert(
db_type.to_string(),
InstalledDriver {
version: "0.1.0-local".to_string(),
installed_at: chrono::Utc::now().to_rfc3339(),
jre: DEFAULT_JRE_KEY.to_string(),
},
);
am.save_state(&local_state)
}
async fn fetch_registry() -> Result<AgentRegistry, String> {
{
let cache = REGISTRY_CACHE.lock().await;
if let Some((ts, registry)) = cache.as_ref() {
if ts.elapsed() < std::time::Duration::from_secs(300) {
return Ok(registry.clone());
}
}
}
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|err| format!("Failed to create HTTP client: {err}"))?;
let resp = dbx_core::race_download(&client, REGISTRY_PATH, REGISTRY_R2_PATH, "dbx-agent-manager")
.await
.map_err(|err| format!("Failed to fetch agent registry: {err}"))?;
let registry: AgentRegistry = resp.json().await.map_err(|err| format!("Failed to parse registry: {err}"))?;
*REGISTRY_CACHE.lock().await = Some((std::time::Instant::now(), registry.clone()));
Ok(registry)
}
fn github_url_to_r2_path(github_url: &str, category: &str) -> String {
let filename = github_url.rsplit('/').next().unwrap_or(github_url);
match category {
"jre" => format!("agents/jre/{filename}"),
"driver" => format!("agents/drivers/{filename}"),
_ => format!("agents/{filename}"),
}
}
async fn download_with_progress(
tx: &broadcast::Sender<String>,
step: &str,
url: &str,
r2_path: &str,
dest: &std::path::Path,
total_size: u64,
db_type: Option<&str>,
current: Option<u32>,
total_drivers: Option<u32>,
) -> Result<(), String> {
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent).map_err(|err| err.to_string())?;
}
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(300))
.build()
.map_err(|err| format!("Failed to create HTTP client: {err}"))?;
let mut resp = dbx_core::race_download(&client, url, r2_path, "dbx-agent-manager")
.await
.map_err(|err| format!("Failed to download {url}: {err}"))?;
let content_length = resp.content_length().unwrap_or(total_size);
let mut file = std::fs::File::create(dest).map_err(|err| format!("Failed to create file: {err}"))?;
let mut downloaded = 0;
while let Some(chunk) = resp.chunk().await.map_err(|err| format!("Download stream error: {err}"))? {
std::io::Write::write_all(&mut file, &chunk).map_err(|err| format!("Failed to write chunk: {err}"))?;
downloaded += chunk.len() as u64;
send_install_progress(tx, step, downloaded, content_length, db_type, current, total_drivers);
}
Ok(())
}
fn send_install_progress(
tx: &broadcast::Sender<String>,
step: &str,
downloaded: u64,
total: u64,
db_type: Option<&str>,
current: Option<u32>,
total_drivers: Option<u32>,
) {
let mut payload = serde_json::json!({ "step": step, "downloaded": downloaded, "total": total });
if let Some(value) = db_type {
payload["db_type"] = serde_json::json!(value);
}
if let Some(value) = current {
payload["current"] = serde_json::json!(value);
}
if let Some(value) = total_drivers {
payload["total_drivers"] = serde_json::json!(value);
}
send_progress(tx, payload);
}
fn send_progress(tx: &broadcast::Sender<String>, payload: serde_json::Value) {
let _ = tx.send(payload.to_string());
}
fn extract_archive(archive: &std::path::Path, dest: &std::path::Path) -> Result<(), String> {
use std::process::Command;
std::fs::create_dir_all(dest).map_err(|err| err.to_string())?;
let status = Command::new("tar")
.args(["xzf", &archive.to_string_lossy(), "-C", &dest.to_string_lossy(), "--strip-components=1"])
.status()
.map_err(|err| format!("Failed to extract archive: {err}"))?;
if !status.success() {
return Err("Failed to extract JRE archive".to_string());
}
Ok(())
}

View File

@ -1,3 +1,4 @@
pub mod agents;
pub mod ai;
pub mod connection;
pub mod database_export;

View File

@ -0,0 +1,39 @@
import { readFileSync } from "node:fs";
import { strict as assert } from "node:assert";
import test from "node:test";
const apiSource = readFileSync("apps/desktop/src/lib/api.ts", "utf8");
const httpSource = readFileSync("apps/desktop/src/lib/http.ts", "utf8");
const tauriSource = readFileSync("apps/desktop/src/lib/tauri.ts", "utf8");
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
const webRoutesSource = readFileSync("crates/dbx-web/src/routes/mod.rs", "utf8");
const agentFunctions = [
"listInstalledAgentsLocal",
"listInstalledAgents",
"installAgent",
"upgradeAllAgents",
"uninstallAgent",
"getAgentJavaRuntimeConfig",
"setAgentJavaRuntimeConfig",
"invalidateAgentRegistryCache",
"reinstallJre",
"uninstallJre",
"listenAgentInstallProgress",
];
test("shared frontend API exposes agent driver management functions", () => {
for (const name of agentFunctions) {
assert.match(apiSource, new RegExp(`export const ${name} = forward\\("${name}"\\)`));
assert.match(httpSource, new RegExp(`export async function ${name}\\b`));
assert.match(tauriSource, new RegExp(`export async function ${name}\\b`));
}
});
test("web backend exposes agent driver management routes", () => {
assert.match(webRoutesSource, /pub mod agents;/);
assert.match(webMainSource, /\/agents\/installed-local/);
assert.match(webMainSource, /\/agents\/install/);
assert.match(webMainSource, /\/agents\/progress\/\{operationId\}/);
assert.match(webMainSource, /\/agents\/java-runtime/);
});

View File

@ -0,0 +1,24 @@
import { readFileSync } from "node:fs";
import { strict as assert } from "node:assert";
import test from "node:test";
const appSource = readFileSync("apps/desktop/src/App.vue", "utf8");
const connectionDialogSource = readFileSync("apps/desktop/src/components/connection/ConnectionDialog.vue", "utf8");
const driverStoreSource = readFileSync("apps/desktop/src/components/config/DriverStoreDialog.vue", "utf8");
test("web runtime handles driver store open events", () => {
assert.match(appSource, /showDriverStore\.value = true;/);
assert.doesNotMatch(appSource, /if \(!isDesktop\) return;\s+showDriverStore\.value = true;/);
});
test("web runtime can show driver install hints", () => {
assert.match(connectionDialogSource, /showAgentDriverInstallHint\(form\.value\.db_type, agentDrivers\.value\)/);
assert.doesNotMatch(connectionDialogSource, /isDesktop &&\s+showAgentDriverInstallHint/);
});
test("driver store uses the shared API instead of direct Tauri calls", () => {
assert.doesNotMatch(driverStoreSource, /@tauri-apps\/api\/core/);
assert.doesNotMatch(driverStoreSource, /@tauri-apps\/api\/event/);
assert.match(driverStoreSource, /api\.listInstalledAgents/);
assert.match(driverStoreSource, /api\.listenAgentInstallProgress/);
});