chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,872 @@
/**
* 自动更新服务 - 基于 electron-updater + latest.json
*
* 更新检查流程:
* 1. 从阿里云 OSS 拉取 latest.json 获取最新版本号
* 2. 比较版本号,如有更新则将 electron-updater 指向版本化 OSS 路径
* 3. electron-updater 从版本化路径读取 latest-*.yml 完成下载/安装
*
* - autoDownload = false: 用户控制下载时机
* - autoInstallOnAppQuit = true: 下载完成后退出时自动安装
* - Windows: NSIS 安装支持自动更新MSI 安装引导到官网下载安装页
*/
import { app, BrowserWindow, shell, dialog, net } from "electron";
import * as path from "path";
import * as fs from "fs";
import log from "electron-log";
import type {
UpdateState,
UpdateInfo,
UpdateProgress,
} from "@shared/types/updateTypes";
import { readSetting } from "../db";
import { t } from "./i18n";
import {
getWindowsDownloadUrl,
getMacosDownloadUrl,
getLinuxDownloadUrl,
type Platforms,
} from "./updatePlatformUtils";
// ==================== OSS latest.json ====================
const OSS_BASE =
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron";
const OSS_STABLE_LATEST_JSON_URL = `${OSS_BASE}/latest/latest.json`;
const OSS_BETA_LATEST_JSON_URL = `${OSS_BASE}/beta/latest.json`;
const OFFICIAL_DOWNLOAD_PAGE_URL = "https://qiming.com/qimingclaw.html";
type UpdateChannel = "stable" | "beta";
const UPDATE_CHANNEL_SETTING_KEY = "update_channel";
/** Squirrel.Mac 在只读卷(如从「下载」直接打开)上无法就地更新时的错误信息特征 */
const READ_ONLY_VOLUME_ERROR_SUBSTR = "read-only volume";
function isReadOnlyVolumeError(err: Error): boolean {
return err?.message?.includes(READ_ONLY_VOLUME_ERROR_SUBSTR) ?? false;
}
interface LatestJson {
version: string;
notes?: string;
pub_date?: string;
platforms?: Record<
string,
{ url: string; signature?: string; size?: number }
>;
/** yml 文件的完整 OSS URL新增字段CI 生成;旧版 CI 无此字段则降级到老逻辑) */
yml?: Record<string, string>;
}
function getUpdateChannel(): UpdateChannel {
const raw = readSetting(UPDATE_CHANNEL_SETTING_KEY);
return raw === "beta" ? "beta" : "stable";
}
function getLatestJsonUrlByChannel(channel: UpdateChannel): string {
return channel === "beta"
? OSS_BETA_LATEST_JSON_URL
: OSS_STABLE_LATEST_JSON_URL;
}
/**
* 从 OSS 拉取 latest.json
*/
function fetchLatestJson(url: string, timeoutMs = 15_000): Promise<LatestJson> {
return new Promise((resolve, reject) => {
// 添加时间戳参数绕过 CDN/浏览器缓存,确保每次都获取最新版本信息
const cacheBustUrl = url.includes("?")
? `${url}&_t=${Date.now()}`
: `${url}?_t=${Date.now()}`;
const request = net.request(cacheBustUrl);
let body = "";
let settled = false;
const timer = setTimeout(() => {
if (!settled) {
settled = true;
request.abort();
reject(new Error(`Timeout after ${timeoutMs}ms fetching ${url}`));
}
}, timeoutMs);
request.on("response", (response) => {
if (response.statusCode !== 200) {
clearTimeout(timer);
settled = true;
reject(new Error(`HTTP ${response.statusCode} fetching ${url}`));
return;
}
response.on("data", (chunk) => {
body += chunk.toString();
});
response.on("end", () => {
clearTimeout(timer);
if (settled) return;
settled = true;
try {
resolve(JSON.parse(body));
} catch (e) {
reject(new Error(`Invalid JSON from ${url}`));
}
});
});
request.on("error", (err) => {
clearTimeout(timer);
if (!settled) {
settled = true;
reject(err);
}
});
request.end();
});
}
// ==================== 安装类型检测 ====================
type InstallerType = "nsis" | "msi" | "mac" | "linux" | "dev";
/**
* 检测 Windows 安装类型NSIS vs MSI
*
* NSIS 安装会在应用目录下创建卸载程序文件,按优先级检测:
* 1. 标准命名Uninstall {productName}.exe
* 2. NSIS 通用命名unins000.exe, unins001.exe 等
* 3. 匹配 Uninstall*.exe 或 unins*.exe避免误判其他文件
*
* MSI 安装由 Windows Installer 管理,通常不含卸载程序文件。
*/
function detectInstallerType(): InstallerType {
if (!app.isPackaged) return "dev";
if (process.platform === "darwin") return "mac";
if (process.platform === "linux") return "linux";
if (process.platform === "win32") {
const appDir = path.dirname(app.getPath("exe"));
// 方式1: 标准的 electron-builder NSIS 卸载程序
// 文件名格式: "Uninstall {productName}.exe"
const productName = app.getName();
const standardNsisUninstaller = path.join(
appDir,
`Uninstall ${productName}.exe`,
);
if (fs.existsSync(standardNsisUninstaller)) {
log.info(
`[AutoUpdater] Windows installer type: NSIS (found standard uninstaller: ${standardNsisUninstaller})`,
);
return "nsis";
}
// 方式2和3: 读取目录一次,检查多种 NSIS 卸载程序模式
// 避免重复调用 readdirSync提高性能
let appFiles: string[] | undefined;
try {
appFiles = fs.readdirSync(appDir);
} catch (e) {
log.warn("[AutoUpdater] Failed to read app directory:", e);
}
if (appFiles && appFiles.length > 0) {
// 方式2: NSIS 通用卸载程序模式 (unins000.exe, unins001.exe 等)
const genericNsisUninstaller = appFiles.find((f) =>
/^unins\d{3}\.exe$/i.test(f),
);
if (genericNsisUninstaller) {
log.info(
`[AutoUpdater] Windows installer type: NSIS (found generic NSIS uninstaller: ${genericNsisUninstaller})`,
);
return "nsis";
}
// 方式3: 匹配 Uninstall*.exe 或 unins*.exe 开头的文件
// 严格模式:避免误判如 "uninstaller_helper.exe" 等非卸载程序文件
const anyUninstaller = appFiles.find((f) => {
const lowerName = f.toLowerCase();
return (
// Uninstall 开头 + .exe 结尾(如 Uninstall.exe, Uninstall-1.0.0.exe
(lowerName.startsWith("uninstall") && lowerName.endsWith(".exe")) ||
// unins 开头但不是 uninsNNN.exe 模式的(兼容其他 NSIS 变体)
(lowerName.startsWith("unins") &&
!/^unins\d{3}\.exe$/i.test(f) &&
lowerName.endsWith(".exe"))
);
});
if (anyUninstaller) {
log.info(
`[AutoUpdater] Windows installer type: NSIS (found uninstaller: ${anyUninstaller})`,
);
return "nsis";
}
}
// Fallback: 找不到任何卸载程序文件,判定为 MSI
log.info(
"[AutoUpdater] Windows installer type: MSI (no uninstaller found in app directory)",
);
return "msi";
}
return "nsis"; // 非预期平台 fallback实际 win32/mac/linux 已覆盖)
}
let cachedInstallerType: InstallerType | undefined;
export function getInstallerType(): InstallerType {
if (!cachedInstallerType) {
cachedInstallerType = detectInstallerType();
}
return cachedInstallerType;
}
/**
* 当前安装方式是否支持自动更新
* - NSIS / mac / linux / dev: electron-updater 原生支持dev 模式下载有单独 guard
* - MSI: 不支持,引导到官网下载安装页
*/
export function canAutoUpdate(): boolean {
const type = getInstallerType();
return type !== "msi";
}
// ==================== 更新状态管理 ====================
/**
* 语义化版本比较: a > b 返回 1, a < b 返回 -1, 相等返回 0
*/
function compareVersions(a: string, b: string): number {
const pa = a.replace(/^v/, "").split(".").map(Number);
const pb = b.replace(/^v/, "").split(".").map(Number);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const na = pa[i] || 0;
const nb = pb[i] || 0;
if (na > nb) return 1;
if (na < nb) return -1;
}
return 0;
}
/**
* MVP 仅支持 x.y.z 纯数字版本,避免 compareVersions 对 prerelease 得到 NaN
*/
function isNumericSemver(version: string): boolean {
return /^\d+\.\d+\.\d+$/.test(version.trim());
}
let currentState: UpdateState = { status: "idle" };
let getMainWindow: (() => BrowserWindow | null) | null = null;
let cleanupBeforeInstall: (() => void) | null = null;
/**
* 在 quitAndInstall 前调用,通知主进程:
* 1. 设置 isQuitting = true防止窗口 close 事件被拦截到托盘
* 2. 设置 isInstallingUpdate = true让 before-quit 跳过 e.preventDefault()
* 允许 Squirrel.Mac 正常接管退出流程完成安装
*/
let markQuitting: (() => void) | null = null;
function sendStatusToRenderer(): void {
const win = getMainWindow?.();
if (win && !win.isDestroyed()) {
win.webContents.send("update:status", currentState);
}
}
/**
* 显示模态对话框(挂载到主窗口,避免 Linux 标题栏图标显示异常)
*/
function showModal(
options: Electron.MessageBoxOptions,
): Promise<Electron.MessageBoxReturnValue> {
const win = getMainWindow?.();
if (win && !win.isDestroyed()) {
return dialog.showMessageBox(win, options);
}
return dialog.showMessageBox(options);
}
function setState(patch: Partial<UpdateState>): void {
currentState = { ...currentState, ...patch };
sendStatusToRenderer();
}
// ==================== latest.json 更新检查 ====================
let checkInProgress = false;
/**
* 通过 OSS latest.json 检查更新
*
* 流程:
* 1. 拉取 latest.json 获取最新版本号和 signature
* 2. 与本地版本比较
* 3. 如果有更新,设置 electron-updater feedURL 指向版本化 OSS 路径,
* 并调用 autoUpdater.checkForUpdates() 初始化 electron-updater 下载状态
* 4. OSS 不可达时直接报错,不 fallback 到 GitHub
*/
async function checkForUpdatesViaLatestJson(): Promise<UpdateInfo> {
if (checkInProgress) {
log.info("[AutoUpdater] Check already in progress, skipping");
// 返回 alreadyChecking: true让调用方知道检查正在进行避免误报"当前已是最新版本"
return { hasUpdate: false, alreadyChecking: true };
}
checkInProgress = true;
try {
return await doCheckViaLatestJson();
} finally {
checkInProgress = false;
}
}
async function doCheckViaLatestJson(): Promise<UpdateInfo> {
const { autoUpdater } = require("electron-updater");
const updateChannel = getUpdateChannel();
const latestJsonUrl = getLatestJsonUrlByChannel(updateChannel);
log.info(
`[AutoUpdater] Check updates via channel=${updateChannel}, url=${latestJsonUrl}`,
);
setState({
status: "checking",
error: undefined,
isReadOnlyVolumeError: undefined,
canAutoUpdate: canAutoUpdate(),
});
let latestJson: LatestJson;
try {
latestJson = await fetchLatestJson(latestJsonUrl);
} catch (e: any) {
// 日志保留完整错误信息(含 URL用户提示仅显示 HTTP 状态码
log.error(
`[AutoUpdater] Failed to fetch latest.json from OSS(channel=${updateChannel}): ${e.message}`,
);
const statusMatch = e.message.match(/^HTTP (\d+)/);
const userMsg = statusMatch
? `HTTP ${statusMatch[1]}`
: t("Claw.AutoUpdater.networkFailed");
setState({
status: "error",
error: `${t("Claw.AutoUpdater.getUpdateInfoFailed", updateChannel)}: ${userMsg}`,
canAutoUpdate: canAutoUpdate(),
});
return {
hasUpdate: false,
error: `${t("Claw.AutoUpdater.getUpdateInfoFailed", updateChannel)}: ${userMsg}`,
};
}
if (!isNumericSemver(latestJson.version)) {
const msg = t(
"Claw.AutoUpdater.invalidMetadataVersion",
latestJson.version,
);
log.error(
`[AutoUpdater] Invalid latest.json version for channel=${updateChannel}: ${latestJson.version}`,
);
setState({
status: "error",
error: msg,
canAutoUpdate: canAutoUpdate(),
});
return { hasUpdate: false, error: msg };
}
const hasUpdate = compareVersions(latestJson.version, app.getVersion()) > 0;
if (hasUpdate) {
// 优先从 latest.json 的 yml 字段读取完整 yml URLCI 生成,支持任意 OSS 路径结构)
// 降级老逻辑:客户端自己拼接 electron-v{version} 或 beta-build/prerelease-v{version} 路径
// 注意ymlUrl 是文件 URL.../latest.yml但 setFeedURL 期望目录路径,它会自动拼接 {channel}.yml
const platformKey = process.platform === "win32" ? "win" : process.platform;
const ymlUrl = latestJson.yml?.[platformKey];
// 去掉文件名得到目录路径,供 electron-updater generic provider 自动拼接 {channel}.yml
const ymlDir = ymlUrl ? ymlUrl.replace(/\/[^/]+\.yml$/, "/") : null;
const versionedUrl = ymlDir
? ymlDir
: updateChannel === "beta"
? `${OSS_BASE}/beta-build/prerelease-v${latestJson.version}`
: `${OSS_BASE}/electron-v${latestJson.version}`;
log.info(
`[AutoUpdater] New version ${latestJson.version} found via channel=${updateChannel}, ymlUrl=${ymlUrl ?? "none (using fallback)"}, feedUrl=${versionedUrl}`,
);
autoUpdater.setFeedURL({ provider: "generic", url: versionedUrl });
// 初始化 electron-updater 内部状态,为后续 downloadUpdate() 做准备
await autoUpdater.checkForUpdates();
} else {
setState({
status: "not-available",
canAutoUpdate: canAutoUpdate(),
});
}
return {
hasUpdate,
version: latestJson.version,
releaseNotes: latestJson.notes,
};
}
// ==================== 初始化 ====================
/**
* 初始化自动更新(应在 app.whenReady 后调用)
* @param getWindow 获取主窗口
* @param cleanup 安装更新前的清理回调(停止服务、关闭数据库等)
* @param onMarkQuitting 在调用 quitAndInstall 前调用,用于设置主进程的 isQuitting/isInstallingUpdate 标志,
* 防止窗口 close 被拦截,并让 before-quit 不阻止退出
*/
export function initAutoUpdater(
getWindow: () => BrowserWindow | null,
cleanup?: () => void,
onMarkQuitting?: () => void,
): void {
getMainWindow = getWindow;
cleanupBeforeInstall = cleanup || null;
markQuitting = onMarkQuitting || null;
const installerType = getInstallerType();
log.info(
`[AutoUpdater] Installer type: ${installerType}, canAutoUpdate: ${canAutoUpdate()}`,
);
// MSI 安装只支持检查更新,不支持自动下载/安装
if (installerType === "msi") {
log.info(
"[AutoUpdater] MSI installation detected: auto-download disabled, will redirect to official download page",
);
}
// CJS 兼容导入 electron-updater
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { autoUpdater } = require("electron-updater");
autoUpdater.logger = log;
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = canAutoUpdate();
// 开发模式:使用 dev-app-update.yml 配置禁用自动安装Squirrel.Mac 无法匹配 dev bundle ID
if (!app.isPackaged) {
autoUpdater.forceDevUpdateConfig = true;
autoUpdater.autoInstallOnAppQuit = false;
log.info(
"[AutoUpdater] Dev mode: using dev-app-update.yml (autoInstall disabled)",
);
}
// 自定义更新源覆盖(本地测试),直接走 electron-updater 的 generic provider
const customServer = process.env.QIMING_UPDATE_SERVER;
if (customServer) {
log.info(`[AutoUpdater] Using custom update server: ${customServer}`);
autoUpdater.setFeedURL({ provider: "generic", url: customServer });
}
// -------- 事件监听 --------
autoUpdater.on("checking-for-update", () => {
log.info("[AutoUpdater] Checking for update...");
setState({
status: "checking",
isReadOnlyVolumeError: undefined,
canAutoUpdate: canAutoUpdate(),
});
});
autoUpdater.on("update-available", (info: any) => {
log.info("[AutoUpdater] Update available:", info.version);
setState({
status: "available",
version: info.version,
isReadOnlyVolumeError: undefined,
canAutoUpdate: canAutoUpdate(),
});
});
autoUpdater.on("update-not-available", (_info: any) => {
log.info("[AutoUpdater] Already up to date");
setState({
status: "not-available",
isReadOnlyVolumeError: undefined,
canAutoUpdate: canAutoUpdate(),
});
});
autoUpdater.on("download-progress", (progress: UpdateProgress) => {
log.info(
`[AutoUpdater] Download progress: ${progress.percent.toFixed(1)}%`,
);
setState({
status: "downloading",
progress,
canAutoUpdate: true,
});
});
autoUpdater.on("update-downloaded", (info: any) => {
log.info("[AutoUpdater] Update downloaded:", info.version);
setState({
status: "downloaded",
version: info.version,
progress: undefined,
canAutoUpdate: true,
});
});
autoUpdater.on("error", (err: Error) => {
log.error("[AutoUpdater] Error:", err.message);
setState({
status: "error",
error: err.message,
progress: undefined,
canAutoUpdate: canAutoUpdate(),
isReadOnlyVolumeError: isReadOnlyVolumeError(err),
});
});
function isUpdateInProgress(): boolean {
return (
currentState.status === "checking" ||
currentState.status === "available" ||
currentState.status === "downloading" ||
currentState.status === "downloaded"
);
}
// 延迟 10s 启动时检查一次,发现新版本弹窗提示;退出时清除避免在已退出状态下弹窗
const STARTUP_CHECK_DELAY_MS = 10_000;
const startupCheckTimerId = setTimeout(async () => {
log.info("[AutoUpdater] Initial startup check");
try {
if (isUpdateInProgress()) {
log.info(
"[AutoUpdater] Startup: update already in progress (status=%s), skipping",
currentState.status,
);
return;
}
const result = await checkForUpdatesViaLatestJson();
if (result.hasUpdate && result.version) {
log.info(`[AutoUpdater] Startup: found new version v${result.version}`);
showStartupUpdateDialog(result.version);
}
} catch (e: any) {
log.warn("[AutoUpdater] Startup check failed:", e.message);
}
}, STARTUP_CHECK_DELAY_MS);
app.once("before-quit", () => {
clearTimeout(startupCheckTimerId);
});
}
/**
* 启动时发现新版本,仅推送状态到渲染进程,由 header tag 展示更新入口
*/
async function showStartupUpdateDialog(version: string): Promise<void> {
log.info(`[AutoUpdater] Startup: v${version} available, notifying renderer`);
setState({
status: "available",
version,
isReadOnlyVolumeError: undefined,
canAutoUpdate: canAutoUpdate(),
});
}
/**
* 通用更新对话框流程(供托盘菜单等外部调用)
* 检查更新 → 有更新则弹窗 → 下载 → 二次确认 → 安装
*/
export async function showUpdateDialogFlow(): Promise<void> {
try {
const result = await checkForUpdates();
if (!result.hasUpdate) {
showModal({
type: "info",
title: t("Claw.AutoUpdater.checking"),
message: t("Claw.AutoUpdater.alreadyLatest"),
});
return;
}
const version = result.version ?? "unknown";
const state = getUpdateState();
if (state.canAutoUpdate === false) {
const { response } = await showModal({
type: "info",
title: t("Claw.AutoUpdater.newVersionFound"),
message: t("Claw.AutoUpdater.versionFound", version),
detail: t("Claw.AutoUpdater.unsupportedInstall"),
buttons: [
t("Claw.AutoUpdater.downloadPage"),
t("Claw.AutoUpdater.later"),
],
defaultId: 0,
cancelId: 1,
});
if (response === 0) {
openReleasesPage();
}
return;
}
const { response } = await showModal({
type: "info",
title: t("Claw.AutoUpdater.newVersionFound"),
message: t("Claw.AutoUpdater.versionFound", version),
detail: t("Claw.AutoUpdater.downloadInstallNow"),
buttons: [t("Claw.AutoUpdater.downloadNow"), t("Claw.AutoUpdater.later")],
defaultId: 0,
cancelId: 1,
});
if (response !== 0) return;
const dlResult = await downloadUpdate();
if (!dlResult.success) {
if (dlResult.error)
showModal({
type: "error",
title: t("Claw.AutoUpdater.downloadFailed"),
message: dlResult.error,
});
return;
}
const { response: installResponse } = await showModal({
type: "info",
title: t("Claw.AutoUpdater.updateDownloaded"),
message: t("Claw.AutoUpdater.updateReady"),
detail: t("Claw.AutoUpdater.installNow"),
buttons: [
t("Claw.AutoUpdater.installNow"),
t("Claw.AutoUpdater.installOnExit"),
],
defaultId: 0,
cancelId: 1,
});
if (installResponse === 0) {
installUpdate();
}
} catch (e: any) {
log.error("[AutoUpdater] Update dialog flow error:", e.message);
showModal({
type: "error",
title: t("Claw.AutoUpdater.checkFailed"),
message: e.message || t("Claw.AutoUpdater.later"),
});
}
}
// ==================== 公开 API ====================
/**
* 手动检查更新(通过 latest.json
*/
export async function checkForUpdates(): Promise<UpdateInfo> {
// 自定义更新源(本地测试用)直接走 electron-updater适用于 stable 通道
// beta 通道始终走 doCheckViaLatestJson确保 feedURL 指向 beta-build 路径
if (process.env.QIMING_UPDATE_SERVER && getUpdateChannel() === "stable") {
try {
const { autoUpdater } = require("electron-updater");
setState({
status: "checking",
error: undefined,
isReadOnlyVolumeError: undefined,
canAutoUpdate: canAutoUpdate(),
});
const result = await autoUpdater.checkForUpdates();
if (result?.updateInfo) {
const hasUpdate =
compareVersions(result.updateInfo.version, app.getVersion()) > 0;
return {
hasUpdate,
version: result.updateInfo.version,
releaseDate: result.updateInfo.releaseDate,
releaseNotes:
typeof result.updateInfo.releaseNotes === "string"
? result.updateInfo.releaseNotes
: undefined,
};
}
return { hasUpdate: false };
} catch (err: any) {
log.error("[AutoUpdater] checkForUpdates error:", err.message);
setState({
status: "error",
error: err.message,
canAutoUpdate: canAutoUpdate(),
});
return { hasUpdate: false, error: err.message };
}
}
return checkForUpdatesViaLatestJson();
}
/**
* 下载更新
*/
export async function downloadUpdate(): Promise<{
success: boolean;
error?: string;
}> {
// Dev 模式下 Squirrel.Mac 无法处理更新包bundle ID 不匹配),只允许检查更新
if (!app.isPackaged) {
const errMsg = t("Claw.AutoUpdater.devModeUnsupported");
log.info(
"[AutoUpdater] downloadUpdate skipped: dev/unpackaged build cannot apply auto-update",
);
return {
success: false,
error: errMsg,
};
}
// MSI 安装不支持自动更新,引导到官网下载安装页
if (getInstallerType() === "msi") {
log.info(
"[AutoUpdater] MSI installation: redirecting to official download page for manual download",
);
openReleasesPage();
return {
success: false,
error: t("Claw.AutoUpdater.unsupportedInstall"),
};
}
try {
// 立即设置 downloading 状态,让渲染进程马上显示 loading
setState({
status: "downloading",
progress: undefined,
canAutoUpdate: true,
});
const { autoUpdater } = require("electron-updater");
await autoUpdater.downloadUpdate();
return { success: true };
} catch (err: any) {
log.error("[AutoUpdater] downloadUpdate error:", err.message);
setState({
status: "error",
error: err.message,
canAutoUpdate: canAutoUpdate(),
isReadOnlyVolumeError: isReadOnlyVolumeError(err),
});
return { success: false, error: err.message };
}
}
/**
* 退出并安装更新
*/
export function installUpdate(): { success: boolean; error?: string } {
if (!app.isPackaged) {
const errMsg = t("Claw.AutoUpdater.installDevUnsupported");
log.info(
"[AutoUpdater] installUpdate skipped: dev/unpackaged build cannot install update",
);
return {
success: false,
error: errMsg,
};
}
if (getInstallerType() === "msi") {
openReleasesPage();
return {
success: false,
error: t("Claw.AutoUpdater.unsupportedInstall"),
};
}
try {
// 关键:在 quitAndInstall 前设置退出标志(所有平台通用)
// 原因1quitAndInstall 会触发 app.quit(),进而触发窗口 close 事件;
// 若 isQuitting 未设置close 会被拦截到托盘app 无法正常退出
// 原因2通知 before-quit handler 跳过 e.preventDefault()
// 让各平台的安装器macOS Squirrel.Mac / Windows NSIS / Linux AppImage
// 正常接管退出流程完成安装;否则 e.preventDefault() 会阻止安装器触发
if (markQuitting) {
log.info("[AutoUpdater] Marking app as quitting for update install...");
markQuitting();
}
// 先停止所有服务避免残留进程cleanup 是同步触发的异步操作)
if (cleanupBeforeInstall) {
log.info("[AutoUpdater] Running cleanup before install...");
cleanupBeforeInstall();
}
const { autoUpdater } = require("electron-updater");
autoUpdater.quitAndInstall(false, true);
return { success: true };
} catch (err: any) {
log.error("[AutoUpdater] installUpdate error:", err.message);
return { success: false, error: err.message };
}
}
/**
* 获取当前更新状态
*/
export function getUpdateState(): UpdateState {
return { ...currentState, canAutoUpdate: canAutoUpdate() };
}
/**
* 打开下载页:
* - Windows: 统一打开官网下载安装页(避免 MSI/EXE 安装路径不一致带来的升级问题)
* - macOS/Linux: 从 OSS latest.json 获取当前平台对应下载链接
* - macOS: 根据架构选择 arm64/x64 .zip
* - Linux: 根据架构选择 arm64/x64 AppImage
* OSS 不可达时弹窗提示错误,不 fallback 到 GitHub
*/
export async function openReleasesPage(): Promise<void> {
if (process.platform === "win32") {
log.info(
`[AutoUpdater] Opening official download page on Windows: ${OFFICIAL_DOWNLOAD_PAGE_URL}`,
);
await shell.openExternal(OFFICIAL_DOWNLOAD_PAGE_URL);
return;
}
let url: string;
let platformName: string;
const updateChannel = getUpdateChannel();
const latestJsonUrl = getLatestJsonUrlByChannel(updateChannel);
try {
log.info(
`[AutoUpdater] Resolve download URL via channel=${updateChannel}, url=${latestJsonUrl}`,
);
const latest = await fetchLatestJson(latestJsonUrl);
const platforms: Platforms | undefined = latest.platforms;
if (process.platform === "darwin") {
url = getMacosDownloadUrl(platforms);
platformName = `macOS (${process.arch})`;
} else {
url = getLinuxDownloadUrl(platforms);
platformName = `Linux (${process.arch})`;
}
if (!url) {
throw new Error(`No download package found for ${platformName}`);
}
log.info(
`[AutoUpdater] Opening ${platformName} download URL from OSS(channel=${updateChannel}): ${url}`,
);
await shell.openExternal(url);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
log.error(
`[AutoUpdater] Failed to get download URL from OSS(channel=${updateChannel}): ${msg}`,
);
// 弹窗提示用户,不 fallback 到 GitHub
await showModal({
type: "error",
title: t("Claw.AutoUpdater.getDownloadLinkFailed"),
message: t("Claw.AutoUpdater.getDownloadLinkFailedDetail", msg),
});
}
}