Files
ele-HeiXiu/electron/main/updater.ts
YoungestSongMo 8b697e775d feat: 去除构建时版控变量 + 窗口标题动态版控 + 退出登录保留缓存 (0.0.24)
版控体系重构(13 文件):
- 移除 build.mjs --personal/--enterprise 参数和 EDITION 环境变量
- 统一安装包文件名:船长-HeiXiu-{os}-{版本号}-Setup.{ext}
- 简化 package.json 构建脚本,移除 :enterprise 变体
- scripts/*.mjs 不再接受 edition 参数
- 移除 shared/constants/app.ts 的 parseEdition()

窗口标题动态版控:
- 新格式:船长-HeiXiu-{os}-{版本号}(登录前)/ ·{版控} 后缀(登录后)
- 链路:AppProvider(account_type) → useEffect → appRuntime IPC → main process
- 移除 updater.ts 更新检查 API 的 edition 查询参数

本地存储生命周期修正:
- 退出登录不再自动清空缓存(user_id 隔离,重新登录命中缓存)
- clearUserStorage 添加 isOpen() 竞态守卫
- 移除 closeDatabase() 避免退出→重新登录后数据库不可用
- 手动「清理缓存」按钮保留

文档:
- CHANGELOG.md 新增 0.0.24 条目
- TODO.md 新增已完成项
2026-06-22 13:36:42 +08:00

435 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 自动更新 — HeiXiuProvider + electron-updater 增量下载
// 流程图:调用自有 API → 获取版本信息 → electron-updater 差分下载 → SHA512 校验 → 安装
import {BrowserWindow, ipcMain} from 'electron';
import {URL} from 'node:url';
import {
NsisUpdater,
MacUpdater,
AppImageUpdater,
Provider,
} from 'electron-updater';
import type {ProviderRuntimeOptions} from 'electron-updater/out/providers/Provider';
import type {UpdateInfo, UpdateFileInfo} from 'builder-util-runtime';
import type {ResolvedUpdateFileInfo} from 'electron-updater/out/types';
import type {UpdateCheckResult} from '../../shared/types';
import {APP_VERSION} from '../../shared/constants/version';
import {logger} from './logger';
import {netRequest} from './net-request';
// ---------- IPC 通道(对渲染进程暴露,与旧版兼容)----------
export const UPDATE_CHANNELS = {
UPDATE_AVAILABLE: 'update-available',
UPDATE_PROGRESS: 'update-progress',
UPDATE_DOWNLOADED: 'update-downloaded',
UPDATE_ERROR: 'update-error',
UPDATE_NOT_AVAILABLE: 'update-not-available',
INSTALL_UPDATE: 'install-update',
CHECK_FOR_UPDATE: 'check-for-update',
DOWNLOAD_UPDATE: 'download-update',
} as const;
// ---------- 配置 ----------
function getApiBaseUrl(): string {
const url = (process.env.UPDATE_API_URL || 'https://www.heixiu.com').trim();
console.log(`[updater] UPDATE_API_URL=${process.env.UPDATE_API_URL} → 使用: ${url}`);
return url;
}
// ============================================================
// 自定义 Provider
// ============================================================
class HeiXiuProvider extends Provider<UpdateInfo> {
private apiBaseUrl: string;
/** 短时缓存:避免 checkForUpdates 预检 + electron-updater 内部调用导致两次 API 请求 */
private _cachedResult: { data: UpdateInfo; ts: number } | null = null;
constructor(runtimeOptions: ProviderRuntimeOptions) {
super(runtimeOptions);
this.apiBaseUrl = getApiBaseUrl();
}
/**
* 调用自有版本检查 API返回 electron-updater 格式的 UpdateInfo。
* blockMapSize > 0 时 electron-updater 自动启用差分下载。
* 无更新时返回当前版本号APP_VERSION
*/
async getLatestVersion(): Promise<UpdateInfo> {
// 短时缓存500ms预检和 electron-updater 内部调用共享同一结果
if (this._cachedResult && Date.now() - this._cachedResult.ts < 500) {
return this._cachedResult.data;
}
const data = await this._doGetLatestVersion();
this._cachedResult = { data, ts: Date.now() };
return data;
}
private async _doGetLatestVersion(): Promise<UpdateInfo> {
const {data} = await netRequest.get<{ code: number; data?: UpdateCheckResult } & UpdateCheckResult>(
'/api/v1/update/check',
{
baseURL: this.apiBaseUrl,
params: {
platform: process.platform,
version: APP_VERSION,
},
},
);
// 兼容两种 API 响应格式
const result: UpdateCheckResult =
(data as { data?: UpdateCheckResult }).data ?? (data as UpdateCheckResult);
// 无更新 → 返回当前版本号
if (!result.hasUpdate) {
return {
version: APP_VERSION,
files: [],
path: '',
sha512: '',
releaseDate: new Date().toISOString(),
};
}
const file: UpdateFileInfo = {
url: result.downloadUrl,
sha512: result.sha512,
size: result.fileSize,
};
// 后端 API 返回 blockMapSize 时启用增量更新
if (result.blockMapSize) {
file.blockMapSize = result.blockMapSize;
}
logger.info('updater', 'Provider 获取到新版本', {
latestVersion: result.latestVersion,
hasBlockmap: file.blockMapSize != null,
});
return {
version: result.latestVersion,
files: [file],
path: result.downloadUrl,
sha512: result.sha512,
releaseDate: result.releaseDate,
releaseNotes: result.changelog,
};
}
/**
* 将 UpdateInfo 中的相对路径解析为完整的下载 URL。
* electron-updater 在下载前调用此方法。
*/
resolveFiles(updateInfo: UpdateInfo): Array<ResolvedUpdateFileInfo> {
return updateInfo.files.map((f) => {
// 使用 downloadUrl 作为 base URL 来解析文件路径
const baseUrl = new URL(updateInfo.path);
const fileUrl = new URL(f.url, baseUrl.origin);
return {
url: fileUrl,
info: f,
};
});
}
}
// ============================================================
// 平台特定 Updater注入自定义 Provider
// ============================================================
/**
* 覆盖 getUpdateInfoAndProvider() 以注入 HeiXiuProvider。
* 其他行为NSIS 安装、签名校验等)完全由父类处理。
*/
class HeiXiuNsisUpdater extends NsisUpdater {
private heiXiuProvider: HeiXiuProvider;
constructor(heiXiuProvider: HeiXiuProvider) {
// 传 undefined 跳过内置 Provider 创建,由我们自己注入
super(undefined);
this.heiXiuProvider = heiXiuProvider;
this.autoDownload = false; // 由我们手动控制流程,保持 IPC 兼容
this.autoInstallOnAppQuit = false;
this.forceDevUpdateConfig = true; // 绕过 electron-updater 的 dev 模式检查
}
/** @internal — 注入自定义 Provider */
protected override async getUpdateInfoAndProvider(): Promise<{
info: UpdateInfo;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
provider: Provider<any>;
}> {
const info = await this.heiXiuProvider.getLatestVersion();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return {info, provider: this.heiXiuProvider as Provider<any>};
}
}
class HeiXiuMacUpdater extends MacUpdater {
private heiXiuProvider: HeiXiuProvider;
constructor(heiXiuProvider: HeiXiuProvider) {
super(undefined);
this.heiXiuProvider = heiXiuProvider;
this.autoDownload = false;
this.autoInstallOnAppQuit = false;
}
protected override async getUpdateInfoAndProvider(): Promise<{
info: UpdateInfo;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
provider: Provider<any>;
}> {
const info = await this.heiXiuProvider.getLatestVersion();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return {info, provider: this.heiXiuProvider as Provider<any>};
}
}
class HeiXiuAppImageUpdater extends AppImageUpdater {
private heiXiuProvider: HeiXiuProvider;
constructor(heiXiuProvider: HeiXiuProvider) {
super(null);
this.heiXiuProvider = heiXiuProvider;
this.autoDownload = false;
this.autoInstallOnAppQuit = false;
}
protected override async getUpdateInfoAndProvider(): Promise<{
info: UpdateInfo;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
provider: Provider<any>;
}> {
const info = await this.heiXiuProvider.getLatestVersion();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return {info, provider: this.heiXiuProvider as Provider<any>};
}
}
/** 工厂:按平台创建对应的 Updater 实例 */
function createPlatformUpdater(provider: HeiXiuProvider) {
switch (process.platform) {
case 'win32':
return new HeiXiuNsisUpdater(provider);
case 'darwin':
return new HeiXiuMacUpdater(provider);
default:
return new HeiXiuAppImageUpdater(provider);
}
}
// ---------- 状态 ----------
let updateWindow: BrowserWindow | null = null;
let provider: HeiXiuProvider | null = null;
let platformUpdater: NsisUpdater | MacUpdater | AppImageUpdater | null = null;
// ============================================================
// 初始化
// ============================================================
export function initUpdater(mainWindow: BrowserWindow): void {
updateWindow = mainWindow;
console.log('[updater] initUpdater 被调用API URL:', getApiBaseUrl());
// if (import.meta.env.DEV) {
// logger.info('updater', '开发模式,跳过自动检查更新', { apiUrl: getApiBaseUrl() });
// return;
// }
// 创建自定义 Provider
provider = new HeiXiuProvider({
isUseMultipleRangeRequest: true,
platform: process.platform as ProviderRuntimeOptions['platform'],
executor: null as unknown as ProviderRuntimeOptions['executor'],
});
// 创建平台 Updater
platformUpdater = createPlatformUpdater(provider);
// 将 electron-updater 事件桥接到现有 IPC 通道
platformUpdater.on('checking-for-update', () => {
logger.debug('updater', '正在检查更新');
});
platformUpdater.on('update-available', (info: UpdateInfo) => {
// releaseNotes 可能是 string | ReleaseNoteInfo[] | null统一转为 string
let notes = '';
if (typeof info.releaseNotes === 'string') {
notes = info.releaseNotes;
} else if (Array.isArray(info.releaseNotes)) {
notes = info.releaseNotes.map((n) => n.note).filter(Boolean).join('\n');
}
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_AVAILABLE, {
version: info.version,
releaseDate: info.releaseDate,
releaseNotes: notes,
forceUpdate: false,
});
logger.info('updater', '发现新版本', {
version: info.version,
releaseNotes: notes ? `${notes.slice(0, 50)}...` : '(空)',
});
});
platformUpdater.on('update-not-available', (info: UpdateInfo) => {
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_NOT_AVAILABLE);
logger.info('updater', '当前已是最新版本', {currentVersion: info.version});
});
platformUpdater.on('download-progress', (progress: {
percent: number;
bytesPerSecond: number;
transferred: number;
total: number
}) => {
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_PROGRESS, {
percent: Math.round(progress.percent),
bytesPerSecond: progress.bytesPerSecond,
transferred: progress.transferred,
total: progress.total,
});
});
platformUpdater.on('update-downloaded', () => {
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_DOWNLOADED);
logger.info('updater', '更新已下载完成');
});
platformUpdater.on('error', (error: Error) => {
logger.error('updater', '更新流程出错', error);
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_ERROR, {
message: error.message || '更新失败',
});
});
// 启动 5 秒后静默检查
setTimeout(() => {
checkForUpdates();
}, 5000);
}
// ============================================================
// 操作方法
// ============================================================
let updateChecked = false;
/** 静默检查更新(自动下载) */
export async function checkForUpdates(): Promise<void> {
if (updateChecked) {
logger.debug('updater', '已检查过更新,跳过');
return;
}
if (!platformUpdater || !provider) return;
try {
updateChecked = true;
// ① 先自行检查有无更新(不经过 electron-updater
const info = await provider.getLatestVersion();
if (info.version === APP_VERSION) {
// 无更新 → 直接通知 UI不触发 electron-updater 的任何事件
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_NOT_AVAILABLE);
logger.debug('updater', '当前已是最新版本', {version: APP_VERSION});
return;
}
// ② 有新版本 → 交给 electron-updater 触发 'update-available' 事件(不自动下载)
await platformUpdater.checkForUpdates();
} catch (error) {
updateChecked = false;
logger.error('updater', '更新流程失败', error as Error, {apiUrl: getApiBaseUrl()});
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_ERROR, {
message: (error as Error).message || '更新检查失败',
});
}
}
/** 手动检查更新(用户点击触发) */
export async function checkForUpdatesManual(): Promise<void> {
if (!platformUpdater || !provider) return;
try {
updateChecked = false;
// ① 先自行检查有无更新
const info = await provider.getLatestVersion();
if (info.version === APP_VERSION) {
// 无更新 → 直接通知 UI
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_NOT_AVAILABLE);
logger.debug('updater', '手动检查:当前已是最新版本', {version: APP_VERSION});
return;
}
// ② 有新版本 → 交给 electron-updater 触发 'update-available' 事件(不自动下载)
await platformUpdater.checkForUpdates();
} catch (error) {
logger.error('updater', '手动更新失败', error as Error);
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_ERROR, {
message: (error as Error).message || '检查更新失败',
});
}
}
/** 用户确认后开始下载更新 */
export async function downloadUpdate(): Promise<void> {
if (!platformUpdater) {
logger.warn('updater', 'downloadUpdate: Updater 未初始化');
return;
}
try {
await platformUpdater.downloadUpdate();
} catch (error) {
logger.error('updater', '下载更新失败', error as Error);
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_ERROR, {
message: (error as Error).message || '下载更新失败',
});
}
}
/** 安装已下载的更新并退出应用 */
export function installUpdateNow(): void {
if (!platformUpdater) {
logger.warn('updater', 'Updater 未初始化');
return;
}
logger.info('updater', '开始安装更新并退出应用');
setImmediate(() => {
platformUpdater!.quitAndInstall(false, true);
});
}
// ============================================================
// IPC 注册
// ============================================================
export function registerUpdateIpcHandlers(): void {
ipcMain.handle(UPDATE_CHANNELS.CHECK_FOR_UPDATE, async () => {
await checkForUpdatesManual();
});
ipcMain.handle(UPDATE_CHANNELS.DOWNLOAD_UPDATE, async () => {
await downloadUpdate();
});
ipcMain.on(UPDATE_CHANNELS.INSTALL_UPDATE, () => {
installUpdateNow();
});
}