/** * UpdateService — 轻量自动更新检查(v0.6.4 P4-2) * * 选型说明:完整方案为 electron-updater(需签名/发布通道配套);本轮依赖冻结 * 且尚无发布基础设施,落地"feed 比对"最小闭环: * - 配置 app.updateFeedUrl 指向版本清单 JSON: * { "version": "0.7.0", "url": "https://…/MetonaAI-Setup.exe", "notes": "…" } * - 用户点击"检查更新"→ 主进程拉取清单 → 语义化版本比较 → 结果经 toast/UI 反馈, * available 时提供下载直链(shell.openExternal 由既有 app:openExternal 白名单承接)。 * 未来接入 electron-updater 时,本服务的 IPC 面与 UI 不变,仅替换 check() 实现。 */ export interface UpdateFeedInfo { version: string; url?: string; notes?: string; } export type UpdateCheckResult = | { status: 'disabled'; message: string } | { status: 'error'; message: string } | { status: 'up-to-date'; latestVersion: string } | { status: 'available'; latestVersion: string; downloadUrl?: string; notes?: string }; /** * 语义化版本比较(仅支持 Major.Minor.Patch 与可选的预发布后缀忽略策略: * 带预发布标签视为小于同号正式版)。非法输入返回 false(宁可不提示升级)。 */ export function isNewerVersion(candidate: string, current: string): boolean { const parse = (v: string): number[] | null => { if (typeof v !== 'string') return null; const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v.trim()); if (!m) return null; return [Number(m[1]), Number(m[2]), Number(m[3])]; }; const a = parse(candidate); const b = parse(current); if (!a || !b) return false; // 预发布标记(如 1.2.3-beta)小于同号正式版 const preA = /-/.test(candidate.trim()); const preB = /-/.test(current.trim()); for (let i = 0; i < 3; i++) { if (a[i] !== b[i]) return a[i] > b[i]; } if (a.join('.') === b.join('.')) { // 同号:pre-release < stable return !preA && preB; } return false; } export class UpdateService { constructor( private readonly getCurrentVersion: () => string, private readonly getFeedUrl: () => string | null, ) {} async check(): Promise { const feedUrl = this.getFeedUrl(); if (!feedUrl) { return { status: 'disabled', message: '未配置更新源(app.updateFeedUrl)' }; } try { const response = await fetch(feedUrl, { signal: AbortSignal.timeout(10_000), headers: { Accept: 'application/json' }, }); if (!response.ok) { return { status: 'error', message: `更新源响应异常(HTTP ${response.status})` }; } const feed = (await response.json()) as Partial; const latestVersion = typeof feed.version === 'string' ? feed.version : ''; if (!latestVersion) { return { status: 'error', message: '更新源缺少 version 字段' }; } const current = this.getCurrentVersion(); if (!isNewerVersion(latestVersion, current)) { return { status: 'up-to-date', latestVersion }; } return { status: 'available', latestVersion, downloadUrl: typeof feed.url === 'string' && /^https?:\/\//i.test(feed.url) ? feed.url : undefined, notes: typeof feed.notes === 'string' ? feed.notes : undefined, }; } catch (err) { return { status: 'error', message: `检查失败:${(err as Error).message}` }; } } }