feat(desktop): implement startup and scheduled auto update flow
This commit is contained in:
@@ -132,6 +132,12 @@ const updateState = reactive({
|
||||
lastCheckedAt: "",
|
||||
message: "",
|
||||
});
|
||||
const AUTO_UPDATE_CHECK_INTERVAL_MS = 30 * 60 * 1000;
|
||||
const updateRuntime = reactive({
|
||||
downloading: false,
|
||||
promptedVersion: "",
|
||||
mandatoryInstalledVersion: "",
|
||||
});
|
||||
const contextMenu = reactive({
|
||||
visible: false,
|
||||
x: 0,
|
||||
@@ -147,6 +153,7 @@ const dropState = reactive({
|
||||
});
|
||||
let unlistenDragDrop: UnlistenFn | null = null;
|
||||
let syncTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let updateCheckTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const toast = reactive({
|
||||
visible: false,
|
||||
@@ -557,6 +564,13 @@ function clearSyncScheduler() {
|
||||
syncState.nextRunAt = "";
|
||||
}
|
||||
|
||||
function clearUpdateScheduler() {
|
||||
if (updateCheckTimer) {
|
||||
clearInterval(updateCheckTimer);
|
||||
updateCheckTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function syncFingerprint(item: LocalSyncFileItem) {
|
||||
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.message = "";
|
||||
const response = await invokeBridge("api_check_client_update", {
|
||||
@@ -626,22 +644,34 @@ async function checkClientUpdate(showResultToast = true) {
|
||||
showToast("当前已是最新版本", "info");
|
||||
}
|
||||
}
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
updateState.available = false;
|
||||
updateState.downloadUrl = "";
|
||||
updateState.message = String(response.data?.message || "检查更新失败");
|
||||
if (showResultToast) {
|
||||
showToast(updateState.message, "error");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function installLatestUpdate() {
|
||||
if (!updateState.downloadUrl) {
|
||||
showToast("当前没有可用的更新下载地址", "info");
|
||||
return;
|
||||
async function installLatestUpdate(trigger: "manual" | "auto" = "manual"): Promise<boolean> {
|
||||
if (updateRuntime.downloading) {
|
||||
if (trigger === "manual") {
|
||||
showToast("更新包正在下载,请稍候", "info");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!updateState.downloadUrl) {
|
||||
if (trigger === "manual") {
|
||||
showToast("当前没有可用的更新下载地址", "info");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
updateRuntime.downloading = true;
|
||||
const taskId = `UPD-${Date.now()}`;
|
||||
const installerName = `wanwan-cloud-desktop_v${updateState.latestVersion || updateState.currentVersion}.exe`;
|
||||
prependTransferTask({
|
||||
@@ -661,33 +691,78 @@ async function installLatestUpdate() {
|
||||
fileName: installerName,
|
||||
});
|
||||
|
||||
if (response.ok && response.data?.success) {
|
||||
try {
|
||||
if (response.ok && response.data?.success) {
|
||||
updateTransferTask(taskId, {
|
||||
speed: "-",
|
||||
progress: 100,
|
||||
status: "done",
|
||||
note: "更新包已下载,准备启动安装",
|
||||
});
|
||||
const savePath = String(response.data?.savePath || "").trim();
|
||||
if (savePath) {
|
||||
try {
|
||||
await openPath(savePath);
|
||||
} catch (error) {
|
||||
console.error("open installer failed", error);
|
||||
}
|
||||
}
|
||||
showToast("更新包已下载,已尝试启动安装程序", "success");
|
||||
return true;
|
||||
}
|
||||
|
||||
const message = String(response.data?.message || "下载更新包失败");
|
||||
updateTransferTask(taskId, {
|
||||
speed: "-",
|
||||
progress: 100,
|
||||
status: "done",
|
||||
note: "更新包已下载,准备启动安装",
|
||||
progress: 0,
|
||||
status: "failed",
|
||||
note: message,
|
||||
});
|
||||
const savePath = String(response.data?.savePath || "").trim();
|
||||
if (savePath) {
|
||||
try {
|
||||
await openPath(savePath);
|
||||
} catch (error) {
|
||||
console.error("open installer failed", error);
|
||||
}
|
||||
}
|
||||
showToast("更新包已下载,已尝试启动安装程序", "success");
|
||||
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 message = String(response.data?.message || "下载更新包失败");
|
||||
updateTransferTask(taskId, {
|
||||
speed: "-",
|
||||
progress: 0,
|
||||
status: "failed",
|
||||
note: message,
|
||||
});
|
||||
showToast(message, "error");
|
||||
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() {
|
||||
@@ -747,6 +822,16 @@ function rebuildSyncScheduler() {
|
||||
}, intervalMinutes * 60 * 1000);
|
||||
}
|
||||
|
||||
function rebuildUpdateScheduler() {
|
||||
clearUpdateScheduler();
|
||||
if (!authenticated.value) {
|
||||
return;
|
||||
}
|
||||
updateCheckTimer = setInterval(() => {
|
||||
void runAutoUpdateCycle("timer");
|
||||
}, AUTO_UPDATE_CHECK_INTERVAL_MS);
|
||||
}
|
||||
|
||||
async function clearSyncSnapshot() {
|
||||
if (!syncState.localDir.trim()) {
|
||||
showToast("请先配置本地同步目录", "info");
|
||||
@@ -1034,9 +1119,10 @@ async function restoreSession() {
|
||||
authenticated.value = true;
|
||||
loadSyncConfig();
|
||||
rebuildSyncScheduler();
|
||||
rebuildUpdateScheduler();
|
||||
await loadFiles("/");
|
||||
await loadShares(true);
|
||||
await checkClientUpdate(false);
|
||||
await runAutoUpdateCycle("startup");
|
||||
}
|
||||
|
||||
function buildItemPath(item: FileItem) {
|
||||
@@ -1101,11 +1187,12 @@ async function handleLogin() {
|
||||
showToast("登录成功,正在同步文件目录", "success");
|
||||
loadSyncConfig();
|
||||
rebuildSyncScheduler();
|
||||
rebuildUpdateScheduler();
|
||||
await loadFiles("/");
|
||||
if (!user.value) {
|
||||
await loadProfile();
|
||||
}
|
||||
await checkClientUpdate(false);
|
||||
await runAutoUpdateCycle("login");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1117,6 +1204,7 @@ async function handleLogin() {
|
||||
async function handleLogout() {
|
||||
await invokeBridge("api_logout", { baseUrl: appConfig.baseUrl });
|
||||
clearSyncScheduler();
|
||||
clearUpdateScheduler();
|
||||
authenticated.value = false;
|
||||
user.value = null;
|
||||
files.value = [];
|
||||
@@ -1137,6 +1225,9 @@ async function handleLogout() {
|
||||
syncState.lastRunAt = "";
|
||||
syncState.lastSummary = "";
|
||||
syncState.nextRunAt = "";
|
||||
updateRuntime.downloading = false;
|
||||
updateRuntime.promptedVersion = "";
|
||||
updateRuntime.mandatoryInstalledVersion = "";
|
||||
showToast("已退出客户端", "info");
|
||||
}
|
||||
|
||||
@@ -1611,6 +1702,7 @@ onBeforeUnmount(() => {
|
||||
window.removeEventListener("click", handleGlobalClick);
|
||||
window.removeEventListener("keydown", handleGlobalKey);
|
||||
clearSyncScheduler();
|
||||
clearUpdateScheduler();
|
||||
if (unlistenDragDrop) {
|
||||
unlistenDragDrop();
|
||||
unlistenDragDrop = null;
|
||||
@@ -1753,10 +1845,12 @@ onBeforeUnmount(() => {
|
||||
<button class="action-btn" @click="clearSyncSnapshot">重建索引</button>
|
||||
</template>
|
||||
<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 ? "检查中..." : "检查更新" }}
|
||||
</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>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1959,6 +2053,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<div class="update-meta">
|
||||
<span>自动检查:已开启(每 30 分钟)</span>
|
||||
<span>上次检查:{{ updateState.lastCheckedAt ? formatDate(updateState.lastCheckedAt) : "-" }}</span>
|
||||
<span>提示:{{ updateState.message || "可手动点击“检查更新”获取最新信息" }}</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user