// ============================================================ // provider.ts — 自定义更新 Provider // // 封装与自有版本检查 API 的交互逻辑, // 返回 electron-updater 格式的 UpdateInfo。 // ============================================================ import { app } from 'electron'; import { createHash } from 'node:crypto'; import { URL } from 'node:url'; import { 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'; // ---------- 构建时注入的渠道信息 ---------- /** 构建渠道(由 build.mjs 通过 VITE_BUILD_CHANNEL 注入,Vite 编译时内联) */ export const BUILD_CHANNEL: string = (typeof import.meta !== 'undefined' && (import.meta as unknown as Record).env ? ((import.meta as unknown as Record).env as Record).VITE_BUILD_CHANNEL : undefined) || process.env.VITE_BUILD_CHANNEL || 'stable'; /** 构建版本类型(由 build.mjs 通过 VITE_BUILD_EDITION 注入) */ export const BUILD_EDITION: string = (typeof import.meta !== 'undefined' && (import.meta as unknown as Record).env ? ((import.meta as unknown as Record).env as Record).VITE_BUILD_EDITION : undefined) || process.env.VITE_BUILD_EDITION || 'personal'; // ---------- 设备指纹(稳定的机器标识,用于灰度分组)---------- let _deviceFingerprint: string | null = null; /** 获取设备指纹(基于机器特定信息的 SHA-256 前 16 位) */ export function getDeviceFingerprint(): string { if (_deviceFingerprint) return _deviceFingerprint; try { const parts = [ process.platform, process.arch, app.getPath('home'), ]; _deviceFingerprint = createHash('sha256') .update(parts.join('|')) .digest('hex') .slice(0, 16); } catch { _deviceFingerprint = 'unknown-device'; } return _deviceFingerprint; } // ---------- 配置 ---------- export 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; } // ============================================================ // HeiXiuProvider // ============================================================ export class HeiXiuProvider extends Provider { 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 { // 短时缓存(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 { const { data } = await netRequest.get<{ code: number; data?: UpdateCheckResult } & UpdateCheckResult>( '/api/v1/update/check', { baseURL: this.apiBaseUrl, params: { platform: process.platform, version: APP_VERSION, channel: BUILD_CHANNEL, edition: BUILD_EDITION, deviceFingerprint: getDeviceFingerprint(), }, }, ); // 兼容两种 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 { 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, }; }); } }