fix(web): support offline driver zip import

This commit is contained in:
t8y2 2026-05-21 11:40:05 +08:00
parent 01572ebad7
commit 73117255c6
6 changed files with 96 additions and 14 deletions

View File

@ -243,15 +243,31 @@ async function uninstallDriver(dbType: string) {
const importingZip = ref(false);
async function importOfflineZip() {
if (isWeb || importingZip.value) return;
const { open } = await import("@tauri-apps/plugin-dialog");
const selected = await open({
title: "选择离线驱动包",
multiple: false,
filters: [{ name: "ZIP", extensions: ["zip"] }],
function chooseWebOfflineZip(): Promise<File | null> {
return new Promise((resolve) => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".zip";
input.onchange = () => resolve(input.files?.[0] ?? null);
input.click();
});
if (typeof selected !== "string") return;
}
async function importOfflineZip() {
if (importingZip.value) return;
let selected: string | File | null = null;
if (isWeb) {
selected = await chooseWebOfflineZip();
} else {
const { open } = await import("@tauri-apps/plugin-dialog");
const path = await open({
title: "选择离线驱动包",
multiple: false,
filters: [{ name: "ZIP", extensions: ["zip"] }],
});
selected = typeof path === "string" ? path : null;
}
if (!selected) return;
importingZip.value = true;
progress.value = null;
try {

View File

@ -179,8 +179,16 @@ export async function invalidateAgentRegistryCache(): Promise<void> {
await post("/api/agents/invalidate-registry-cache", {});
}
export async function importAgentsFromZip(_path: string): Promise<number> {
throw new Error("Offline ZIP import is only available in the desktop app");
export async function importAgentsFromZip(fileOrPath: string | File): Promise<number> {
if (typeof fileOrPath === "string") {
throw new Error("Offline ZIP import in web mode requires a File object, not a file path");
}
const formData = new FormData();
formData.append("file", fileOrPath);
const res = await fetch("/api/agents/import-offline", { method: "POST", body: formData });
if (!res.ok) throw new Error(await res.text());
const result: { count: number } = await res.json();
return result.count;
}
export async function importAgentJar(_dbType: string, _path: string): Promise<void> {

View File

@ -389,7 +389,10 @@ export async function invalidateAgentRegistryCache(): Promise<void> {
return invoke("invalidate_agent_registry_cache");
}
export async function importAgentsFromZip(path: string): Promise<number> {
export async function importAgentsFromZip(path: string | File): Promise<number> {
if (typeof path !== "string") {
throw new Error("Desktop offline ZIP import requires a local file path");
}
return invoke("import_agents_from_zip", { path });
}

View File

@ -92,6 +92,7 @@ async fn main() {
.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/import-offline", post(routes::agents::import_agents_from_zip))
.route(
"/agents/java-runtime",
get(routes::agents::get_agent_java_runtime_config).post(routes::agents::set_agent_java_runtime_config),
@ -199,7 +200,7 @@ async fn main() {
// Build app
let mut app = Router::new()
.nest("/api", api)
.layer(DefaultBodyLimit::max(50 * 1024 * 1024))
.layer(DefaultBodyLimit::max(300 * 1024 * 1024))
.layer(tower_http::trace::TraceLayer::new_for_http())
.layer(cors);

View File

@ -1,6 +1,6 @@
use std::sync::Arc;
use axum::extract::{Path, State};
use axum::extract::{Multipart, Path, State};
use axum::response::sse::{Event, Sse};
use axum::Json;
use dbx_core::agent_manager::{
@ -9,7 +9,7 @@ use dbx_core::agent_manager::{
};
use dbx_core::agent_service::{
build_agent_list, download_temp_path, fetch_registry, find_local_agent_jar, github_url_to_r2_path,
install_local_agent, invalidate_registry_cache, replace_download,
import_offline_zip, install_local_agent, invalidate_registry_cache, replace_download, OfflineImportProgress,
};
use futures::Stream;
use serde::Deserialize;
@ -127,6 +127,48 @@ pub async fn invalidate_agent_registry_cache() -> Result<Json<serde_json::Value>
Ok(Json(serde_json::json!({ "ok": true })))
}
pub async fn import_agents_from_zip(
State(state): State<Arc<WebState>>,
mut multipart: Multipart,
) -> Result<Json<serde_json::Value>, AppError> {
let tmp_dir = state.data_dir.join("tmp");
std::fs::create_dir_all(&tmp_dir).map_err(|err| AppError(err.to_string()))?;
while let Some(field) = multipart.next_field().await.map_err(|err| AppError(err.to_string()))? {
let file_name = field.file_name().unwrap_or("offline-drivers.zip").to_string();
if !file_name.to_ascii_lowercase().ends_with(".zip") {
return Err(AppError("Offline driver package must be a .zip file".to_string()));
}
let data = field.bytes().await.map_err(|err| AppError(err.to_string()))?;
let zip_path = tmp_dir.join(format!("agent-offline-{}.zip", uuid::Uuid::new_v4()));
std::fs::write(&zip_path, &data).map_err(|err| AppError(err.to_string()))?;
let tx = progress_sender(&state, "global").await;
let result = import_offline_zip(&state.app.agent_manager, &zip_path, |p: OfflineImportProgress| {
send_progress(
&tx,
serde_json::json!({
"step": p.step,
"downloaded": p.current as u64,
"total": p.total as u64,
"db_type": p.label,
"current": p.current,
"total_drivers": p.total,
}),
);
})
.map_err(AppError);
let _ = std::fs::remove_file(&zip_path);
let result = result?;
send_progress(&tx, serde_json::json!({ "step": "done" }));
return Ok(Json(serde_json::json!({ "count": result.drivers_installed.len() as u32 })));
}
Err(AppError("No file uploaded".to_string()))
}
pub async fn reinstall_jre(
State(state): State<Arc<WebState>>,
Json(req): Json<JreRequest>,

View File

@ -5,6 +5,7 @@ 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 driverStoreSource = readFileSync("apps/desktop/src/components/config/DriverStoreDialog.vue", "utf8");
const webMainSource = readFileSync("crates/dbx-web/src/main.rs", "utf8");
const webRoutesSource = readFileSync("crates/dbx-web/src/routes/mod.rs", "utf8");
@ -41,3 +42,14 @@ test("web backend exposes agent driver management routes", () => {
assert.match(webMainSource, /\/agents\/java-runtime/);
assert.match(webMainSource, /\/ai\/models/);
});
test("web runtime supports importing an offline agent driver zip", () => {
assert.match(httpSource, /export async function importAgentsFromZip\(fileOrPath: string \| File\)/);
assert.match(httpSource, /FormData/);
assert.match(httpSource, /\/api\/agents\/import-offline/);
assert.doesNotMatch(httpSource, /Offline ZIP import is only available in the desktop app/);
assert.match(webMainSource, /\/agents\/import-offline/);
assert.match(driverStoreSource, /chooseWebOfflineZip/);
assert.match(driverStoreSource, /accept = "\.zip"/);
assert.doesNotMatch(driverStoreSource, /if \(isWeb \|\| importingZip\.value\) return;/);
});