updateService.cjs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. const https = require('node:https');
  2. const LATEST_RELEASE_API = 'https://api.github.com/repos/FB208/OpenBidKit_Yibiao/releases/latest';
  3. let autoUpdaterInstance = null;
  4. let downloadedUpdateVersion = '';
  5. let activeUpdateCheckPromise = null;
  6. function compareVersions(a, b) {
  7. const pa = String(a || '').replace(/^v/, '').split('.').map(Number);
  8. const pb = String(b || '').replace(/^v/, '').split('.').map(Number);
  9. for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
  10. const na = Number.isFinite(pa[i]) ? pa[i] : 0;
  11. const nb = Number.isFinite(pb[i]) ? pb[i] : 0;
  12. if (na > nb) return 1;
  13. if (na < nb) return -1;
  14. }
  15. return 0;
  16. }
  17. function fetchLatestRelease() {
  18. return new Promise((resolve, reject) => {
  19. const request = https.get(LATEST_RELEASE_API, { headers: { 'User-Agent': 'yibiao-client' } }, (response) => {
  20. let data = '';
  21. response.on('data', (chunk) => { data += chunk; });
  22. response.on('end', () => {
  23. if (response.statusCode < 200 || response.statusCode >= 300) {
  24. reject(new Error(`GitHub API 请求失败:${response.statusCode}`));
  25. return;
  26. }
  27. try {
  28. const release = JSON.parse(data);
  29. resolve({
  30. version: release.tag_name?.replace(/^v/, '') || '',
  31. body: release.body || '',
  32. });
  33. } catch {
  34. reject(new Error('解析 GitHub API 响应失败'));
  35. }
  36. });
  37. });
  38. request.on('error', (error) => reject(error));
  39. request.setTimeout(10000, () => {
  40. request.destroy();
  41. reject(new Error('请求超时'));
  42. });
  43. });
  44. }
  45. function formatErrorMessage(error) {
  46. return error instanceof Error ? error.message : String(error || '未知错误');
  47. }
  48. function setProgressBar(mainWindow, progress) {
  49. if (!mainWindow || mainWindow.isDestroyed()) {
  50. return;
  51. }
  52. mainWindow.setProgressBar(progress);
  53. }
  54. function getDisabledResult() {
  55. return { enabled: false, updateAvailable: false };
  56. }
  57. async function runUpdateCheck({ app, mainWindow, onProgress, onDownloaded, onError }) {
  58. const release = await fetchLatestRelease();
  59. if (!release.version || compareVersions(release.version, app.getVersion()) <= 0) {
  60. return { enabled: true, updateAvailable: false };
  61. }
  62. let downloadedVersion = release.version;
  63. let downloadedNotified = false;
  64. let errorNotified = false;
  65. const notifyError = (message) => {
  66. if (errorNotified) {
  67. return;
  68. }
  69. errorNotified = true;
  70. onError?.(message);
  71. };
  72. const handleProgress = (progress) => {
  73. const percent = Number(progress?.percent || 0);
  74. setProgressBar(mainWindow, Math.max(0, Math.min(1, percent / 100)));
  75. onProgress?.(percent);
  76. };
  77. const handleDownloaded = (info) => {
  78. downloadedVersion = info?.version || release.version;
  79. downloadedUpdateVersion = downloadedVersion;
  80. downloadedNotified = true;
  81. setProgressBar(mainWindow, -1);
  82. onDownloaded?.(downloadedVersion);
  83. };
  84. const handleError = (error) => {
  85. setProgressBar(mainWindow, -1);
  86. notifyError(formatErrorMessage(error));
  87. };
  88. autoUpdaterInstance.on('download-progress', handleProgress);
  89. autoUpdaterInstance.on('update-downloaded', handleDownloaded);
  90. autoUpdaterInstance.on('error', handleError);
  91. try {
  92. const result = await autoUpdaterInstance.checkForUpdates();
  93. if (!result) {
  94. throw new Error('未找到可下载的更新包');
  95. }
  96. await autoUpdaterInstance.downloadUpdate();
  97. downloadedUpdateVersion = downloadedVersion;
  98. setProgressBar(mainWindow, -1);
  99. if (!downloadedNotified) {
  100. onDownloaded?.(downloadedVersion);
  101. }
  102. return { enabled: true, updateAvailable: true, version: downloadedVersion, downloaded: true };
  103. } catch (error) {
  104. const message = formatErrorMessage(error);
  105. notifyError(message);
  106. return { enabled: true, updateAvailable: true, version: release.version, failed: true, message };
  107. } finally {
  108. autoUpdaterInstance.removeListener('download-progress', handleProgress);
  109. autoUpdaterInstance.removeListener('update-downloaded', handleDownloaded);
  110. autoUpdaterInstance.removeListener('error', handleError);
  111. setProgressBar(mainWindow, -1);
  112. }
  113. }
  114. async function checkAndDownloadUpdate(options = {}) {
  115. const { app } = options;
  116. if (!app?.isPackaged) {
  117. return getDisabledResult();
  118. }
  119. if (!autoUpdaterInstance) {
  120. return { enabled: true, updateAvailable: false, failed: true, message: '自动更新未初始化' };
  121. }
  122. if (downloadedUpdateVersion) {
  123. return { enabled: true, updateAvailable: true, version: downloadedUpdateVersion, downloaded: true };
  124. }
  125. if (activeUpdateCheckPromise) {
  126. return activeUpdateCheckPromise;
  127. }
  128. activeUpdateCheckPromise = runUpdateCheck(options)
  129. .catch((error) => {
  130. const message = formatErrorMessage(error);
  131. options.onError?.(message);
  132. return { enabled: true, updateAvailable: false, failed: true, message };
  133. })
  134. .finally(() => {
  135. activeUpdateCheckPromise = null;
  136. });
  137. return activeUpdateCheckPromise;
  138. }
  139. function triggerUpdateDownload(options) {
  140. return checkAndDownloadUpdate(options);
  141. }
  142. function quitAndInstall() {
  143. if (autoUpdaterInstance && downloadedUpdateVersion) {
  144. autoUpdaterInstance.quitAndInstall(false, true);
  145. }
  146. }
  147. function setupAutoUpdate({ app, mainWindow }) {
  148. if (!app.isPackaged) {
  149. return;
  150. }
  151. const { autoUpdater } = require('electron-updater');
  152. autoUpdaterInstance = autoUpdater;
  153. autoUpdater.autoDownload = false;
  154. autoUpdater.autoInstallOnAppQuit = false;
  155. autoUpdater.on('download-progress', (progress) => {
  156. const percent = Number(progress?.percent || 0);
  157. setProgressBar(mainWindow, Math.max(0, Math.min(1, percent / 100)));
  158. });
  159. autoUpdater.on('update-downloaded', (info) => {
  160. downloadedUpdateVersion = info?.version || downloadedUpdateVersion;
  161. setProgressBar(mainWindow, -1);
  162. });
  163. autoUpdater.on('error', (error) => {
  164. setProgressBar(mainWindow, -1);
  165. console.warn('自动更新检查失败', error);
  166. });
  167. }
  168. module.exports = {
  169. setupAutoUpdate,
  170. checkAndDownloadUpdate,
  171. triggerUpdateDownload,
  172. quitAndInstall,
  173. };