Files
WeChatDataAnalysis/frontend/composables/useDesktopUpdate.js
2977094657 a14f8de6d0 feat(app-shell): 桌面端集成自动更新(electron-updater)
- 集成 electron-updater:检查更新/下载/安装/忽略此版本,并推送下载进度到前端

- 打包版启动后自动检查更新;托盘菜单支持手动检查

- preload 暴露 updater IPC + __brand 标记;前端新增更新弹窗与设置页版本/检查更新入口

- 补全发布配置:artifactName/publish;release workflow 增加上传 latest.yml
2026-02-18 16:53:50 +08:00

237 lines
6.8 KiB
JavaScript

let listenersInitialized = false;
let removeListeners = [];
const getDesktopApi = () => {
if (!process.client) return null;
if (typeof window === "undefined") return null;
return window?.wechatDesktop || null;
};
const isDesktopShell = () => !!getDesktopApi();
const isUpdaterSupported = () => {
const api = getDesktopApi();
if (!api) return false;
// If the bridge exposes a brand marker, ensure it's our Electron shell.
if (api.__brand && api.__brand !== "WeChatDataAnalysisDesktop") return false;
// Require updater IPC to avoid showing update UI in the pure web build.
return (
typeof api.getVersion === "function" &&
typeof api.checkForUpdates === "function" &&
typeof api.downloadAndInstall === "function"
);
};
export const useDesktopUpdate = () => {
const info = useState("desktopUpdate.info", () => null);
const open = useState("desktopUpdate.open", () => false);
const isDownloading = useState("desktopUpdate.isDownloading", () => false);
const readyToInstall = useState("desktopUpdate.readyToInstall", () => false);
const progress = useState("desktopUpdate.progress", () => ({ percent: 0 }));
const error = useState("desktopUpdate.error", () => "");
const currentVersion = useState("desktopUpdate.currentVersion", () => "");
const manualCheckLoading = useState("desktopUpdate.manualCheckLoading", () => false);
const lastCheckMessage = useState("desktopUpdate.lastCheckMessage", () => "");
const lastCheckAt = useState("desktopUpdate.lastCheckAt", () => 0);
const setUpdateInfo = (payload) => {
if (!payload) return;
const version = String(payload?.version || "").trim();
const releaseNotes = String(payload?.releaseNotes || "");
if (!version) return;
info.value = { version, releaseNotes };
readyToInstall.value = false;
};
const dismiss = () => {
open.value = false;
};
const refreshVersion = async () => {
if (!isUpdaterSupported()) return "";
try {
const v = await getDesktopApi()?.getVersion?.();
currentVersion.value = String(v || "");
return currentVersion.value;
} catch {
return currentVersion.value || "";
}
};
const initListeners = async () => {
if (!isUpdaterSupported()) return;
if (listenersInitialized) return;
listenersInitialized = true;
await refreshVersion();
const unsubs = [];
const unUpdate = window.wechatDesktop?.onUpdateAvailable?.((payload) => {
error.value = "";
isDownloading.value = false;
readyToInstall.value = false;
progress.value = { percent: 0 };
setUpdateInfo(payload);
open.value = true;
});
if (typeof unUpdate === "function") unsubs.push(unUpdate);
const unProgress = window.wechatDesktop?.onDownloadProgress?.((p) => {
progress.value = p || { percent: 0 };
const percent = Number(progress.value?.percent || 0);
if (Number.isFinite(percent) && percent > 0) {
isDownloading.value = true;
}
});
if (typeof unProgress === "function") unsubs.push(unProgress);
const unDownloaded = window.wechatDesktop?.onUpdateDownloaded?.((payload) => {
// Download finished. Keep the dialog open and let the user decide when to install.
setUpdateInfo(payload || info.value || {});
isDownloading.value = false;
readyToInstall.value = true;
progress.value = { ...(progress.value || {}), percent: 100 };
open.value = true;
});
if (typeof unDownloaded === "function") unsubs.push(unDownloaded);
const unError = window.wechatDesktop?.onUpdateError?.((payload) => {
const msg = String(payload?.message || "");
if (msg) error.value = msg;
isDownloading.value = false;
readyToInstall.value = false;
});
if (typeof unError === "function") unsubs.push(unError);
removeListeners = unsubs;
};
const startUpdate = async () => {
if (!isUpdaterSupported()) return;
error.value = "";
isDownloading.value = true;
readyToInstall.value = false;
progress.value = { percent: 0 };
try {
await getDesktopApi()?.downloadAndInstall?.();
} catch (e) {
const msg = e?.message || String(e);
error.value = msg;
isDownloading.value = false;
}
};
const installUpdate = async () => {
if (!isUpdaterSupported()) return;
if (!getDesktopApi()?.installUpdate) return;
error.value = "";
try {
await getDesktopApi()?.installUpdate?.();
} catch (e) {
const msg = e?.message || String(e);
error.value = msg;
}
};
const ignore = async () => {
if (!isUpdaterSupported()) return;
const version = String(info.value?.version || "").trim();
if (!version) return;
try {
await getDesktopApi()?.ignoreUpdate?.(version);
} catch (e) {
const msg = e?.message || String(e);
error.value = msg;
} finally {
// Hide the dialog locally; startup auto-check will also respect the ignore.
open.value = false;
info.value = null;
}
};
const manualCheck = async () => {
if (!isDesktopShell()) {
lastCheckMessage.value = "仅桌面端可用。";
return { hasUpdate: false };
}
if (!isUpdaterSupported()) {
lastCheckMessage.value = "当前桌面端版本不支持自动更新。";
return { hasUpdate: false };
}
manualCheckLoading.value = true;
error.value = "";
lastCheckMessage.value = "";
try {
await refreshVersion();
const res = await getDesktopApi()?.checkForUpdates?.();
lastCheckAt.value = Date.now();
if (res?.enabled === false) {
lastCheckMessage.value = "自动更新已禁用(仅打包版本可用)。";
return res;
}
if (res?.error) {
lastCheckMessage.value = `检查更新失败:${String(res.error)}`;
return res;
}
if (res?.hasUpdate && res?.version) {
setUpdateInfo({ version: res.version, releaseNotes: res.releaseNotes || "" });
open.value = true;
lastCheckMessage.value = `发现新版本:${String(res.version)}`;
return res;
}
lastCheckMessage.value = "当前已是最新版本。";
return res;
} catch (e) {
const msg = e?.message || String(e);
lastCheckMessage.value = `检查更新失败:${msg}`;
return { hasUpdate: false, error: msg };
} finally {
manualCheckLoading.value = false;
}
};
const cleanup = () => {
try {
for (const fn of removeListeners) fn?.();
} catch {}
removeListeners = [];
listenersInitialized = false;
};
return {
info,
open,
isDownloading,
readyToInstall,
progress,
error,
currentVersion,
manualCheckLoading,
lastCheckMessage,
lastCheckAt,
initListeners,
refreshVersion,
manualCheck,
startUpdate,
installUpdate,
ignore,
dismiss,
cleanup,
};
};