修复客户端更新诊断与错误展示

This commit is contained in:
2026-08-13 15:28:14 +08:00
parent a03043d048
commit f05b9d4cb5
5 changed files with 376 additions and 13 deletions

View File

@@ -14,6 +14,18 @@ import { setQuitting } from './app-state';
/** Base CDN URL (without trailing channel path) */
const OSS_BASE_URL = 'https://oss.intelli-spectrum.com';
const WORKS_SQUARE_UPDATE_BASE_URL = 'https://square.nianxx.cn/api/app-updates';
const STABLE_UPDATE_NOT_PUBLISHED_ERROR = '当前平台的正式更新包尚未发布,请稍后重试或从官网下载最新版';
export function normalizeUpdateError(error: unknown): string {
const message = error instanceof Error ? error.message : String(error);
const isMissingStableChannel = message.includes('Cannot find channel "latest.yml" update info')
|| message.includes('Cannot find channel "latest-mac.yml" update info');
const isMissingStableManifest = isMissingStableChannel
&& message.includes('HttpError: 404')
&& message.includes(WORKS_SQUARE_UPDATE_BASE_URL);
return isMissingStableManifest ? STABLE_UPDATE_NOT_PUBLISHED_ERROR : message;
}
export interface UpdateStatus {
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
@@ -69,6 +81,8 @@ export class AppUpdater extends EventEmitter {
private autoInstallTimer: NodeJS.Timeout | null = null;
private autoInstallCountdown = 0;
private readonly updateFeedError: string | null;
private readonly activeUpdateChecks = new Set<symbol>();
private readonly updaterEventErrorChecks = new Map<Error, Set<symbol>>();
/** Delay (in seconds) before auto-installing a downloaded update. */
private static readonly AUTO_INSTALL_DELAY_SECONDS = 5;
@@ -162,7 +176,10 @@ export class AppUpdater extends EventEmitter {
});
autoUpdater.on('error', (error: Error) => {
this.updateStatus({ status: 'error', error: error.message });
if (this.activeUpdateChecks.size > 0) {
this.updaterEventErrorChecks.set(error, new Set(this.activeUpdateChecks));
}
this.updateStatus({ status: 'error', error: normalizeUpdateError(error) });
this.emit('error', error);
});
}
@@ -198,6 +215,8 @@ export class AppUpdater extends EventEmitter {
* final status so the UI never gets stuck in 'checking'.
*/
async checkForUpdates(): Promise<UpdateInfo | null> {
const checkToken = Symbol('update-check');
this.activeUpdateChecks.add(checkToken);
try {
this.updateStatus({ status: 'checking' });
@@ -230,8 +249,22 @@ export class AppUpdater extends EventEmitter {
return result.updateInfo || null;
} catch (error) {
logger.error('[Updater] Check for updates failed:', error);
this.updateStatus({ status: 'error', error: (error as Error).message || String(error) });
const reportedChecks = error instanceof Error
? this.updaterEventErrorChecks.get(error)
: undefined;
const alreadyReported = reportedChecks?.has(checkToken) ?? false;
if (!alreadyReported) {
this.updateStatus({ status: 'error', error: normalizeUpdateError(error) });
}
throw error;
} finally {
this.activeUpdateChecks.delete(checkToken);
for (const [error, reportedChecks] of this.updaterEventErrorChecks) {
reportedChecks.delete(checkToken);
if (reportedChecks.size === 0) {
this.updaterEventErrorChecks.delete(error);
}
}
}
}