From b161a3e3e7c53d10bd2c60bab0edb3c3f80afeff Mon Sep 17 00:00:00 2001 From: yuyx <237899745@qq.com> Date: Fri, 20 Feb 2026 23:44:26 +0800 Subject: [PATCH] feat: improve sync workspace and updater stability in 0.1.27 --- backend/server.js | 2 +- desktop-client/package.json | 2 +- desktop-client/src-tauri/Cargo.lock | 2 +- desktop-client/src-tauri/Cargo.toml | 2 +- desktop-client/src-tauri/src/lib.rs | 87 +++++++++++++++++- desktop-client/src-tauri/tauri.conf.json | 2 +- desktop-client/src/App.vue | 108 +++++++++++++++++++++-- 7 files changed, 192 insertions(+), 13 deletions(-) diff --git a/backend/server.js b/backend/server.js index 6c1af8b..9136e16 100644 --- a/backend/server.js +++ b/backend/server.js @@ -112,7 +112,7 @@ const DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS = Math.max( 10, Math.min(3600, Number(process.env.DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS || 30)) ); -const DEFAULT_DESKTOP_VERSION = process.env.DESKTOP_LATEST_VERSION || '0.1.26'; +const DEFAULT_DESKTOP_VERSION = process.env.DESKTOP_LATEST_VERSION || '0.1.27'; const DEFAULT_DESKTOP_INSTALLER_URL = process.env.DESKTOP_INSTALLER_URL || ''; const DEFAULT_DESKTOP_INSTALLER_SHA256 = String(process.env.DESKTOP_INSTALLER_SHA256 || '').trim().toLowerCase(); const DEFAULT_DESKTOP_INSTALLER_SIZE = Math.max(0, Number(process.env.DESKTOP_INSTALLER_SIZE || 0)); diff --git a/desktop-client/package.json b/desktop-client/package.json index c5fa99a..f414664 100644 --- a/desktop-client/package.json +++ b/desktop-client/package.json @@ -1,7 +1,7 @@ { "name": "desktop-client", "private": true, - "version": "0.1.26", + "version": "0.1.27", "type": "module", "scripts": { "dev": "vite", diff --git a/desktop-client/src-tauri/Cargo.lock b/desktop-client/src-tauri/Cargo.lock index 0a01079..849c184 100644 --- a/desktop-client/src-tauri/Cargo.lock +++ b/desktop-client/src-tauri/Cargo.lock @@ -693,7 +693,7 @@ dependencies = [ [[package]] name = "desktop-client" -version = "0.1.26" +version = "0.1.27" dependencies = [ "reqwest 0.12.28", "rusqlite", diff --git a/desktop-client/src-tauri/Cargo.toml b/desktop-client/src-tauri/Cargo.toml index e3ca8f0..ee07a61 100644 --- a/desktop-client/src-tauri/Cargo.toml +++ b/desktop-client/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "desktop-client" -version = "0.1.26" +version = "0.1.27" description = "A Tauri App" authors = ["you"] edition = "2021" diff --git a/desktop-client/src-tauri/src/lib.rs b/desktop-client/src-tauri/src/lib.rs index 4213289..3ee1981 100644 --- a/desktop-client/src-tauri/src/lib.rs +++ b/desktop-client/src-tauri/src/lib.rs @@ -12,6 +12,8 @@ use std::io::{Read, Seek, SeekFrom}; use std::os::windows::process::CommandExt; use std::path::{Path, PathBuf}; use std::process::Command; +#[cfg(target_os = "windows")] +use std::process::Stdio; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use tauri::Emitter; use tokio::time::sleep; @@ -25,6 +27,10 @@ use windows_sys::Win32::Foundation::LocalFree; #[cfg(target_os = "windows")] const CREATE_NO_WINDOW: u32 = 0x08000000; +#[cfg(target_os = "windows")] +const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; +#[cfg(target_os = "windows")] +const DETACHED_PROCESS: u32 = 0x00000008; const RESUMABLE_CHUNK_MAX_RETRIES: u32 = 3; const RESUMABLE_CHUNK_RETRY_BASE_DELAY_MS: u64 = 900; @@ -440,6 +446,61 @@ fn build_download_resume_temp_path(download_dir: &Path, preferred_name: &str, ur download_dir.join(temp_name) } +fn is_update_installer_file_name(file_name: &str) -> bool { + let lower = file_name.trim().to_ascii_lowercase(); + if !lower.ends_with(".exe") { + return false; + } + lower.starts_with("wanwan-cloud-desktop_v") || file_name.trim().starts_with("玩玩云_v") +} + +fn cleanup_old_update_installers( + download_dir: &Path, + keep_file_name: &str, + keep_latest: usize, +) -> Result<(), String> { + let mut entries: Vec<(PathBuf, SystemTime)> = Vec::new(); + let normalized_keep = keep_file_name.trim(); + for entry in fs::read_dir(download_dir).map_err(|err| format!("扫描下载目录失败: {}", err))? { + let path = match entry { + Ok(item) => item.path(), + Err(_) => continue, + }; + if !path.is_file() { + continue; + } + let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else { + continue; + }; + if !is_update_installer_file_name(file_name) { + continue; + } + let modified = fs::metadata(&path) + .ok() + .and_then(|meta| meta.modified().ok()) + .unwrap_or(SystemTime::UNIX_EPOCH); + entries.push((path, modified)); + } + + entries.sort_by(|a, b| b.1.cmp(&a.1)); + let retain_count = keep_latest.max(1); + let mut retained = 0usize; + for (path, _) in entries { + let file_name = path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default() + .to_string(); + let should_keep = file_name == normalized_keep || retained < retain_count; + if should_keep { + retained += 1; + continue; + } + let _ = fs::remove_file(&path); + } + Ok(()) +} + fn resolve_local_state_dir() -> PathBuf { if let Some(appdata) = env::var_os("APPDATA") { return PathBuf::from(appdata).join("wanwan-cloud-desktop"); @@ -502,10 +563,17 @@ fn load_login_state_record() -> Result, String> record.get::<_, String>(2)?, )) }); + drop(stmt); match row { Ok((base_url, username, password)) => { - let decoded_password = decode_login_password(&password)?; - Ok(Some((base_url, username, decoded_password))) + match decode_login_password(&password) { + Ok(decoded_password) => Ok(Some((base_url, username, decoded_password))), + Err(err) => { + eprintln!("decode login state failed, clearing invalid state: {}", err); + let _ = conn.execute("DELETE FROM login_state WHERE id = 1", []); + Ok(None) + } + } } Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(err) => Err(format!("读取登录状态失败: {}", err)), @@ -1124,6 +1192,11 @@ async fn api_native_download( let save_path = alloc_download_path(&download_dir, preferred_name); fs::rename(&resume_temp_path, &save_path) .map_err(|err| format!("完成断点下载失败: {}", err))?; + if let Some(saved_name) = save_path.file_name().and_then(|name| name.to_str()) { + if is_update_installer_file_name(saved_name) { + let _ = cleanup_old_update_installers(&download_dir, saved_name, 3); + } + } if let Some(ref id) = task_id { emit_native_download_progress( &window, @@ -1232,6 +1305,11 @@ async fn api_native_download( let save_path = alloc_download_path(&download_dir, preferred_name); fs::rename(&resume_temp_path, &save_path) .map_err(|err| format!("保存下载文件失败: {}", err))?; + if let Some(saved_name) = save_path.file_name().and_then(|name| name.to_str()) { + if is_update_installer_file_name(saved_name) { + let _ = cleanup_old_update_installers(&download_dir, saved_name, 3); + } + } if let Some(ref id) = task_id { emit_native_download_progress( @@ -1419,7 +1497,10 @@ del \"%~f0\" >nul 2>nul\r\n", .arg("call") .arg(&script_path) .current_dir(&temp_dir) - .creation_flags(CREATE_NO_WINDOW) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS) .spawn(); if let Err(err) = spawn_result { let _ = fs::OpenOptions::new() diff --git a/desktop-client/src-tauri/tauri.conf.json b/desktop-client/src-tauri/tauri.conf.json index d1fde5d..0611245 100644 --- a/desktop-client/src-tauri/tauri.conf.json +++ b/desktop-client/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "玩玩云", - "version": "0.1.26", + "version": "0.1.27", "identifier": "cn.workyai.wanwancloud.desktop", "build": { "beforeDevCommand": "npm run dev", diff --git a/desktop-client/src/App.vue b/desktop-client/src/App.vue index a1f963d..5efb72d 100644 --- a/desktop-client/src/App.vue +++ b/desktop-client/src/App.vue @@ -153,7 +153,7 @@ const syncState = reactive({ nextRunAt: "", }); const updateState = reactive({ - currentVersion: "0.1.26", + currentVersion: "0.1.27", latestVersion: "", available: false, mandatory: false, @@ -725,6 +725,30 @@ function normalizeRelativePath(rawPath: string) { return String(rawPath || "").replace(/\\/g, "/").replace(/^\/+/, ""); } +function sanitizeSyncFolderSegment(raw: string, fallback = "默认同步盘") { + const normalized = String(raw || "") + .trim() + .replace(/[\/\\:*?"<>|]/g, "_") + .replace(/\.\./g, "_") + .replace(/\s+/g, " "); + const compact = normalized.replace(/^_+|_+$/g, "").trim(); + if (!compact) return fallback; + return compact.slice(0, 60); +} + +function deriveDefaultSyncRemoteBasePath() { + const localDirName = sanitizeSyncFolderSegment(extractFileNameFromPath(syncState.localDir || ""), ""); + const owner = sanitizeSyncFolderSegment(String(user.value?.username || user.value?.id || "desktop"), "desktop"); + const folderName = localDirName || `同步盘_${owner}`; + return normalizePath(`/同步盘/${folderName}`); +} + +function applyDefaultSyncRemoteBasePath(force = false) { + const normalizedCurrent = normalizePath(syncState.remoteBasePath || "/"); + if (!force && normalizedCurrent !== "/") return; + syncState.remoteBasePath = deriveDefaultSyncRemoteBasePath(); +} + function getSyncConfigStorageKey() { const userId = String(user.value?.id || "guest"); return `wanwan_desktop_sync_config_v2_${userId}`; @@ -748,7 +772,10 @@ function safeParseObject(raw: string | null) { function loadSyncConfig() { const key = getSyncConfigStorageKey(); const raw = localStorage.getItem(key); - if (!raw) return; + if (!raw) { + applyDefaultSyncRemoteBasePath(true); + return; + } try { const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object") return; @@ -760,6 +787,7 @@ function loadSyncConfig() { } catch { // ignore invalid cache } + applyDefaultSyncRemoteBasePath(false); } function saveSyncConfig() { @@ -1052,8 +1080,12 @@ async function installLatestUpdate(): Promise { const launchResponse = await invokeBridge("api_silent_install_and_restart", { installerPath: savePath, }); + const logFilePath = String(launchResponse.data?.logFilePath || "").trim(); if (launchResponse.ok && launchResponse.data?.success) { - showToast("静默安装已启动,完成后会自动重启客户端", "success"); + const launchTip = logFilePath + ? `静默安装已启动(日志:${logFilePath})` + : "静默安装已启动,完成后会自动重启客户端"; + showToast(launchTip, "success"); setTimeout(() => { const win = getCurrentWindow(); void (async () => { @@ -1070,6 +1102,9 @@ async function installLatestUpdate(): Promise { }, 400); return true; } + if (logFilePath) { + console.warn("silent updater log path:", logFilePath); + } try { await openPath(savePath); @@ -1077,7 +1112,7 @@ async function installLatestUpdate(): Promise { console.error("open installer fallback failed", error); } } - showToast("更新包已下载,请手动运行安装程序", "info"); + showToast("更新包已下载,静默安装未启动,请手动运行安装程序", "info"); setTimeout(() => { resetUpdateRuntime(); }, 1200); @@ -1197,7 +1232,11 @@ async function chooseSyncDirectory() { title: "选择本地同步文件夹", }); if (typeof result === "string" && result.trim()) { + const hadCustomRemoteBase = normalizePath(syncState.remoteBasePath || "/") !== "/"; syncState.localDir = result.trim(); + if (!hadCustomRemoteBase) { + applyDefaultSyncRemoteBasePath(true); + } showToast("同步目录已更新", "success"); } } catch { @@ -1302,6 +1341,36 @@ async function clearSyncSnapshot() { showToast("同步索引已清理,下次会全量扫描", "success"); } +async function ensureRemoteFolderPath(targetPath: string) { + const normalized = normalizePath(targetPath || "/"); + if (normalized === "/") { + return { ok: true, path: "/" }; + } + + const segments = normalized.split("/").filter(Boolean); + let current = "/"; + for (const segment of segments) { + const response = await invokeBridge("api_mkdir", { + baseUrl: appConfig.baseUrl, + path: current, + folderName: segment, + }); + if (!(response.ok && response.data?.success)) { + const message = String(response.data?.message || ""); + if (!message.includes("已存在")) { + return { + ok: false, + path: normalizePath(`${current}/${segment}`), + message: message || "创建远程同步目录失败", + }; + } + } + current = normalizePath(`${current}/${segment}`); + } + + return { ok: true, path: normalized }; +} + async function runSyncOnce(trigger: "manual" | "auto" = "manual") { if (!authenticated.value) return; const localDir = syncState.localDir.trim(); @@ -1355,6 +1424,21 @@ async function runSyncOnce(trigger: "manual" | "auto" = "manual") { return; } + applyDefaultSyncRemoteBasePath(false); + const remoteBase = normalizePath(syncState.remoteBasePath || "/"); + const ensuredRemotePaths = new Set(); + const baseEnsureResult = await ensureRemoteFolderPath(remoteBase); + if (!baseEnsureResult.ok) { + syncState.syncing = false; + syncState.lastRunAt = new Date().toISOString(); + syncState.lastSummary = String(baseEnsureResult.message || "创建远程同步目录失败"); + if (trigger === "manual") { + showToast(syncState.lastSummary, "error"); + } + return; + } + ensuredRemotePaths.add(remoteBase); + const successPaths = new Set(); for (let index = 0; index < changedItems.length; index += 1) { await waitForTransferQueue(); @@ -1362,7 +1446,6 @@ async function runSyncOnce(trigger: "manual" | "auto" = "manual") { const relPath = normalizeRelativePath(item.relativePath); const fileName = extractFileNameFromPath(relPath) || `同步文件${index + 1}`; const parent = getRelativeParentPath(relPath); - const remoteBase = normalizePath(syncState.remoteBasePath || "/"); const targetPath = parent ? normalizePath(`${remoteBase}/${parent}`) : remoteBase; const taskId = `S-${Date.now()}-${index}`; @@ -1380,6 +1463,21 @@ async function runSyncOnce(trigger: "manual" | "auto" = "manual") { }); updateTransferTask(taskId, { status: "uploading", speed: "上传中", progress: 10 }); + if (!ensuredRemotePaths.has(targetPath)) { + const ensureResult = await ensureRemoteFolderPath(targetPath); + if (!ensureResult.ok) { + syncState.failedCount += 1; + updateTransferTask(taskId, { + speed: "-", + progress: 0, + status: "failed", + note: String(ensureResult.message || "创建远程同步目录失败"), + }); + continue; + } + ensuredRemotePaths.add(targetPath); + } + const resp = await uploadFileWithResume(item.path, targetPath, taskId); if (resp.ok && resp.data?.success) {