feat(desktop): implement startup and scheduled auto update flow

This commit is contained in:
2026-02-18 21:11:59 +08:00
parent 5f91fd925d
commit c81b5395ac

View File

@@ -132,6 +132,12 @@ const updateState = reactive({
lastCheckedAt: "", lastCheckedAt: "",
message: "", message: "",
}); });
const AUTO_UPDATE_CHECK_INTERVAL_MS = 30 * 60 * 1000;
const updateRuntime = reactive({
downloading: false,
promptedVersion: "",
mandatoryInstalledVersion: "",
});
const contextMenu = reactive({ const contextMenu = reactive({
visible: false, visible: false,
x: 0, x: 0,
@@ -147,6 +153,7 @@ const dropState = reactive({
}); });
let unlistenDragDrop: UnlistenFn | null = null; let unlistenDragDrop: UnlistenFn | null = null;
let syncTimer: ReturnType<typeof setInterval> | null = null; let syncTimer: ReturnType<typeof setInterval> | null = null;
let updateCheckTimer: ReturnType<typeof setInterval> | null = null;
const toast = reactive({ const toast = reactive({
visible: false, visible: false,
@@ -557,6 +564,13 @@ function clearSyncScheduler() {
syncState.nextRunAt = ""; syncState.nextRunAt = "";
} }
function clearUpdateScheduler() {
if (updateCheckTimer) {
clearInterval(updateCheckTimer);
updateCheckTimer = null;
}
}
function syncFingerprint(item: LocalSyncFileItem) { function syncFingerprint(item: LocalSyncFileItem) {
return `${Number(item.size || 0)}:${Number(item.modifiedMs || 0)}`; return `${Number(item.size || 0)}:${Number(item.modifiedMs || 0)}`;
} }
@@ -600,7 +614,11 @@ async function initClientVersion() {
} }
} }
async function checkClientUpdate(showResultToast = true) { async function checkClientUpdate(showResultToast = true): Promise<boolean> {
if (updateState.checking) {
return false;
}
updateState.checking = true; updateState.checking = true;
updateState.message = ""; updateState.message = "";
const response = await invokeBridge("api_check_client_update", { const response = await invokeBridge("api_check_client_update", {
@@ -626,22 +644,34 @@ async function checkClientUpdate(showResultToast = true) {
showToast("当前已是最新版本", "info"); showToast("当前已是最新版本", "info");
} }
} }
return; return true;
} }
updateState.available = false; updateState.available = false;
updateState.downloadUrl = "";
updateState.message = String(response.data?.message || "检查更新失败"); updateState.message = String(response.data?.message || "检查更新失败");
if (showResultToast) { if (showResultToast) {
showToast(updateState.message, "error"); showToast(updateState.message, "error");
} }
return false;
} }
async function installLatestUpdate() { async function installLatestUpdate(trigger: "manual" | "auto" = "manual"): Promise<boolean> {
if (!updateState.downloadUrl) { if (updateRuntime.downloading) {
showToast("当前没有可用的更新下载地址", "info"); if (trigger === "manual") {
return; showToast("更新包正在下载,请稍候", "info");
}
return false;
} }
if (!updateState.downloadUrl) {
if (trigger === "manual") {
showToast("当前没有可用的更新下载地址", "info");
}
return false;
}
updateRuntime.downloading = true;
const taskId = `UPD-${Date.now()}`; const taskId = `UPD-${Date.now()}`;
const installerName = `wanwan-cloud-desktop_v${updateState.latestVersion || updateState.currentVersion}.exe`; const installerName = `wanwan-cloud-desktop_v${updateState.latestVersion || updateState.currentVersion}.exe`;
prependTransferTask({ prependTransferTask({
@@ -661,6 +691,7 @@ async function installLatestUpdate() {
fileName: installerName, fileName: installerName,
}); });
try {
if (response.ok && response.data?.success) { if (response.ok && response.data?.success) {
updateTransferTask(taskId, { updateTransferTask(taskId, {
speed: "-", speed: "-",
@@ -677,7 +708,7 @@ async function installLatestUpdate() {
} }
} }
showToast("更新包已下载,已尝试启动安装程序", "success"); showToast("更新包已下载,已尝试启动安装程序", "success");
return; return true;
} }
const message = String(response.data?.message || "下载更新包失败"); const message = String(response.data?.message || "下载更新包失败");
@@ -688,6 +719,50 @@ async function installLatestUpdate() {
note: message, note: message,
}); });
showToast(message, "error"); showToast(message, "error");
return false;
} finally {
updateRuntime.downloading = false;
}
}
async function runAutoUpdateCycle(trigger: "startup" | "login" | "timer" = "startup") {
const checked = await checkClientUpdate(false);
if (!checked) return;
if (!updateState.available || !updateState.downloadUrl) {
return;
}
const latestVersion = String(updateState.latestVersion || "").trim();
if (!latestVersion) return;
if (updateState.mandatory) {
if (updateRuntime.mandatoryInstalledVersion === latestVersion) {
return;
}
showToast(`检测到强制更新 v${latestVersion},正在下载升级包`, "info");
const installed = await installLatestUpdate("auto");
if (installed) {
updateRuntime.mandatoryInstalledVersion = latestVersion;
}
return;
}
if (updateRuntime.promptedVersion === latestVersion) {
return;
}
updateRuntime.promptedVersion = latestVersion;
if (trigger === "timer") {
showToast(`发现新版本 v${latestVersion},可在“版本更新”中安装`, "info");
return;
}
const confirmed = window.confirm(`发现新版本 v${latestVersion},是否立即下载并安装?`);
if (confirmed) {
await installLatestUpdate("auto");
}
} }
async function chooseSyncDirectory() { async function chooseSyncDirectory() {
@@ -747,6 +822,16 @@ function rebuildSyncScheduler() {
}, intervalMinutes * 60 * 1000); }, intervalMinutes * 60 * 1000);
} }
function rebuildUpdateScheduler() {
clearUpdateScheduler();
if (!authenticated.value) {
return;
}
updateCheckTimer = setInterval(() => {
void runAutoUpdateCycle("timer");
}, AUTO_UPDATE_CHECK_INTERVAL_MS);
}
async function clearSyncSnapshot() { async function clearSyncSnapshot() {
if (!syncState.localDir.trim()) { if (!syncState.localDir.trim()) {
showToast("请先配置本地同步目录", "info"); showToast("请先配置本地同步目录", "info");
@@ -1034,9 +1119,10 @@ async function restoreSession() {
authenticated.value = true; authenticated.value = true;
loadSyncConfig(); loadSyncConfig();
rebuildSyncScheduler(); rebuildSyncScheduler();
rebuildUpdateScheduler();
await loadFiles("/"); await loadFiles("/");
await loadShares(true); await loadShares(true);
await checkClientUpdate(false); await runAutoUpdateCycle("startup");
} }
function buildItemPath(item: FileItem) { function buildItemPath(item: FileItem) {
@@ -1101,11 +1187,12 @@ async function handleLogin() {
showToast("登录成功,正在同步文件目录", "success"); showToast("登录成功,正在同步文件目录", "success");
loadSyncConfig(); loadSyncConfig();
rebuildSyncScheduler(); rebuildSyncScheduler();
rebuildUpdateScheduler();
await loadFiles("/"); await loadFiles("/");
if (!user.value) { if (!user.value) {
await loadProfile(); await loadProfile();
} }
await checkClientUpdate(false); await runAutoUpdateCycle("login");
return; return;
} }
@@ -1117,6 +1204,7 @@ async function handleLogin() {
async function handleLogout() { async function handleLogout() {
await invokeBridge("api_logout", { baseUrl: appConfig.baseUrl }); await invokeBridge("api_logout", { baseUrl: appConfig.baseUrl });
clearSyncScheduler(); clearSyncScheduler();
clearUpdateScheduler();
authenticated.value = false; authenticated.value = false;
user.value = null; user.value = null;
files.value = []; files.value = [];
@@ -1137,6 +1225,9 @@ async function handleLogout() {
syncState.lastRunAt = ""; syncState.lastRunAt = "";
syncState.lastSummary = ""; syncState.lastSummary = "";
syncState.nextRunAt = ""; syncState.nextRunAt = "";
updateRuntime.downloading = false;
updateRuntime.promptedVersion = "";
updateRuntime.mandatoryInstalledVersion = "";
showToast("已退出客户端", "info"); showToast("已退出客户端", "info");
} }
@@ -1611,6 +1702,7 @@ onBeforeUnmount(() => {
window.removeEventListener("click", handleGlobalClick); window.removeEventListener("click", handleGlobalClick);
window.removeEventListener("keydown", handleGlobalKey); window.removeEventListener("keydown", handleGlobalKey);
clearSyncScheduler(); clearSyncScheduler();
clearUpdateScheduler();
if (unlistenDragDrop) { if (unlistenDragDrop) {
unlistenDragDrop(); unlistenDragDrop();
unlistenDragDrop = null; unlistenDragDrop = null;
@@ -1753,10 +1845,12 @@ onBeforeUnmount(() => {
<button class="action-btn" @click="clearSyncSnapshot">重建索引</button> <button class="action-btn" @click="clearSyncSnapshot">重建索引</button>
</template> </template>
<template v-else-if="nav === 'updates'"> <template v-else-if="nav === 'updates'">
<button class="action-btn" :disabled="updateState.checking" @click="checkClientUpdate()"> <button class="action-btn" :disabled="updateState.checking || updateRuntime.downloading" @click="checkClientUpdate()">
{{ updateState.checking ? "检查中..." : "检查更新" }} {{ updateState.checking ? "检查中..." : "检查更新" }}
</button> </button>
<button class="action-btn" :disabled="!updateState.available || !updateState.downloadUrl" @click="installLatestUpdate">立即更新</button> <button class="action-btn" :disabled="updateRuntime.downloading || !updateState.available || !updateState.downloadUrl" @click="installLatestUpdate('manual')">
{{ updateRuntime.downloading ? "下载中..." : "立即更新" }}
</button>
</template> </template>
</div> </div>
</header> </header>
@@ -1959,6 +2053,7 @@ onBeforeUnmount(() => {
</div> </div>
<div class="update-meta"> <div class="update-meta">
<span>自动检查已开启 30 分钟</span>
<span>上次检查{{ updateState.lastCheckedAt ? formatDate(updateState.lastCheckedAt) : "-" }}</span> <span>上次检查{{ updateState.lastCheckedAt ? formatDate(updateState.lastCheckedAt) : "-" }}</span>
<span>提示{{ updateState.message || "可手动点击“检查更新”获取最新信息" }}</span> <span>提示{{ updateState.message || "可手动点击“检查更新”获取最新信息" }}</span>
</div> </div>