most-box 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +15 -1
  2. package/electron/main.js +30 -420
  3. package/electron/preload.js +1 -16
  4. package/electron/updateChecker.js +12 -30
  5. package/electron/updateChecker.test.js +14 -55
  6. package/out/admin/index.html +0 -0
  7. package/out/app/index.html +0 -0
  8. package/out/assets/{FilePreviewOverlay-DqH7uKb7.js → FilePreviewOverlay-D8H61qbp.js} +1 -1
  9. package/out/assets/LanguageToggle-CSQtRZ3A.js +1 -0
  10. package/out/assets/MarketingHeader-CrYsQsQ5.js +1 -0
  11. package/out/assets/MarketingLayout-CQurMOSD.js +1 -0
  12. package/out/assets/{MarketingThemeToggle-BLuQ_uE-.js → MarketingThemeToggle-CIKP8x9W.js} +1 -1
  13. package/out/assets/{MilkdownEditor-f2vRIY21.js → MilkdownEditor-DrsyaR4h.js} +1 -1
  14. package/out/assets/{OpenSidebarButton-BKX4B_yG.js → OpenSidebarButton-DnEgqP0S.js} +1 -1
  15. package/out/assets/{SidebarAccount-D6wj_ijc.js → SidebarAccount-B4OAl0as.js} +1 -1
  16. package/out/assets/SidebarHomeLink-BdsUM4Kt.js +1 -0
  17. package/out/assets/arrow-right-BU9c_SL4.js +1 -0
  18. package/out/assets/{circle-alert-Dyc-4wje.js → circle-alert-iuHzciph.js} +1 -1
  19. package/out/assets/{copy-D8HSH5Ki.js → copy-DEzFUzeX.js} +1 -1
  20. package/out/assets/{download-Q6jKMPIG.js → download-jEPn2njL.js} +1 -1
  21. package/out/assets/{downloadValidation-B-w8D_0W.js → downloadValidation-DbVOwdmC.js} +1 -1
  22. package/out/assets/{external-link-GQcZwDTT.js → external-link-BFaoZeK0.js} +1 -1
  23. package/out/assets/{file-text-CVl5SZPJ.js → file-text-DPA1riJ_.js} +1 -1
  24. package/out/assets/{folder-BtIdk-YN.js → folder-Cfgc70_F.js} +1 -1
  25. package/out/assets/{hard-drive-Cew_LD0N.js → hard-drive-Dar_AZEq.js} +1 -1
  26. package/out/assets/i18n-NjvOSuV6.js +1 -0
  27. package/out/assets/index-BZ-f0CZd.js +31 -0
  28. package/out/assets/index-Cde4g_dk.css +1 -0
  29. package/out/assets/index.lazy-B7AbDiMU.js +3 -0
  30. package/out/assets/{index.lazy-DujygWms.js → index.lazy-BsciBHf_.js} +1 -1
  31. package/out/assets/index.lazy-BxoGfWEW.js +1 -0
  32. package/out/assets/index.lazy-CKbrHko5.js +1 -0
  33. package/out/assets/index.lazy-CgKPe02l.js +1 -0
  34. package/out/assets/index.lazy-Citucq1G.js +1 -0
  35. package/out/assets/index.lazy-CvKEcWlH.js +2 -0
  36. package/out/assets/{index.lazy-WXDn5r_l.js → index.lazy-DB9Ht9LJ.js} +1 -1
  37. package/out/assets/index.lazy-DVeqmqaA.js +1 -0
  38. package/out/assets/index.lazy-hF807bh0.js +1 -0
  39. package/out/assets/index.lazy-oFaoa4qT.js +1 -0
  40. package/out/assets/index.lazy-rfgjFZNH.js +1 -0
  41. package/out/assets/index.lazy-zpQXhgmn.js +2 -0
  42. package/out/assets/{key-round-D1zfZzvP.js → key-round-DNcpSuMw.js} +1 -1
  43. package/out/assets/{loader-BSclU8WE.js → loader-BXQtdens.js} +1 -1
  44. package/out/assets/{lock-C9VP0ezG.js → lock-CLIE8cct.js} +1 -1
  45. package/out/assets/{message-square-Dg3YjjmO.js → message-square-Dp09WY1I.js} +1 -1
  46. package/out/assets/{mp-FQvFEcYM.js → mp-B6kdA94c.js} +1 -1
  47. package/out/assets/{music-BeCPak70.js → music-B19Tg557.js} +1 -1
  48. package/out/assets/{notebook-pen-DO1CJiqn.js → notebook-pen-C8TlK5sB.js} +1 -1
  49. package/out/assets/play-B-BjsTZz.js +1 -0
  50. package/out/assets/plus-4hygj-1Z.js +1 -0
  51. package/out/assets/{save-BVeMjaW1.js → save-BjrbPwhJ.js} +1 -1
  52. package/out/assets/search-DSkiWdvX.js +1 -0
  53. package/out/assets/{send-C1J-eGNs.js → send-BL4d5iOt.js} +1 -1
  54. package/out/assets/{shield-check-CZYDVJoh.js → shield-check-DzDuWO8r.js} +1 -1
  55. package/out/assets/smartphone-tRRFFO0a.js +1 -0
  56. package/out/assets/{trash-2-C_LtCiNh.js → trash-2-V12tJ8lI.js} +1 -1
  57. package/out/assets/triangle-alert-DGWc8ZHu.js +1 -0
  58. package/out/assets/{ui-PnUvY9zL.js → ui-BpDGD3IR.js} +2 -2
  59. package/out/assets/{upload-DgxxldtV.js → upload-CoCptKya.js} +1 -1
  60. package/out/assets/{useChannelMessages-CZHmH5n-.js → useChannelMessages-B6RMaNDH.js} +1 -1
  61. package/out/assets/useGameRoom-CRJL0kcm.js +1 -0
  62. package/out/assets/userProfile-DBkH4s29.js +1 -0
  63. package/out/assets/{wallet-DBTWBi6K.js → wallet-T7ONZ3Kg.js} +1 -1
  64. package/out/assets/{wifi-pP6SJLcu.js → wifi-Bgo8eHY0.js} +1 -1
  65. package/out/chat/index.html +0 -0
  66. package/out/chat/join/index.html +0 -0
  67. package/out/download/index.html +2 -2
  68. package/out/game/gandengyan/index.html +0 -0
  69. package/out/game/index.html +0 -0
  70. package/out/game/zhajinhua/index.html +0 -0
  71. package/out/index.html +2 -2
  72. package/out/note/index.html +0 -0
  73. package/out/ping/index.html +2 -2
  74. package/out/profile/index.html +0 -0
  75. package/out/web3/index.html +0 -0
  76. package/package.json +5 -8
  77. package/server/src/core/channelIdentity.js +3 -5
  78. package/server/src/core/gameRoom.js +18 -43
  79. package/server/src/core/ownerMetadata.js +3 -4
  80. package/server/src/core/releaseManifest.js +91 -0
  81. package/server/src/core/shared.js +50 -0
  82. package/server/src/core/zhajinhua.js +12 -33
  83. package/server/src/games/gandengyan.js +7 -14
  84. package/server/src/http/nodeStatus.js +0 -6
  85. package/server/src/http/routes/channelRoutes.js +1 -13
  86. package/server/src/http/routes/nodeRoutes.js +1 -1
  87. package/server/src/index.js +44 -298
  88. package/server/src/utils/api.js +47 -0
  89. package/server/src/utils/auth.js +2 -5
  90. package/server/src/utils/dateTime.js +1 -3
  91. package/out/assets/LanguageToggle-BoBNesNU.js +0 -1
  92. package/out/assets/MarketingHeader-Derv67Vs.js +0 -1
  93. package/out/assets/MarketingLayout-fc2mymJm.js +0 -1
  94. package/out/assets/SidebarHomeLink-ByOHwlH8.js +0 -1
  95. package/out/assets/arrow-right-BCi9KXDX.js +0 -1
  96. package/out/assets/cloud-DfaFxhLj.js +0 -1
  97. package/out/assets/i18n--maTEG0d.js +0 -1
  98. package/out/assets/index-B2-kpfu5.css +0 -1
  99. package/out/assets/index-Cg4kEvx8.js +0 -31
  100. package/out/assets/index.lazy-BAq__8ON.js +0 -2
  101. package/out/assets/index.lazy-BPh8D_H9.js +0 -1
  102. package/out/assets/index.lazy-Bn4kDToA.js +0 -1
  103. package/out/assets/index.lazy-BqHgW5fE.js +0 -1
  104. package/out/assets/index.lazy-CLYRM_kh.js +0 -1
  105. package/out/assets/index.lazy-CMSCQucz.js +0 -1
  106. package/out/assets/index.lazy-Cut4XkPH.js +0 -1
  107. package/out/assets/index.lazy-DKOSJyaQ.js +0 -2
  108. package/out/assets/index.lazy-DjPbRI_L.js +0 -1
  109. package/out/assets/index.lazy-DxAGUwlf.js +0 -3
  110. package/out/assets/index.lazy-ghLI7p1x.js +0 -1
  111. package/out/assets/play-CnhsLC5C.js +0 -1
  112. package/out/assets/plus-BpjwXzbO.js +0 -1
  113. package/out/assets/search-CsAnnJyH.js +0 -1
  114. package/out/assets/triangle-alert-DOwc-41Y.js +0 -1
  115. package/out/assets/useGameRoom-B8Ds_PKe.js +0 -1
  116. package/out/assets/userProfile-CRGZ8PUF.js +0 -1
package/README.md CHANGED
@@ -30,6 +30,12 @@
30
30
 
31
31
  前往 [Most.Box 下载页](https://Most.Box/download) 下载客户端,支持 Windows、macOS 和 Linux。桌面端内置本地 MostBox 节点,提供完整 P2P 文件分享、下载校验和持续做种能力,无需单独安装 Node.js。
32
32
 
33
+ ### 移动端 App 计划
34
+
35
+ 移动端优先按 Android 前台完整种子 MVP 推进,参考 Keet/Pear 的“P2P 核心端 + 平台 UI 壳”分层:手机端先验证自己能发布、下载、校验并在前台继续做种,再扩展后台能力、iOS 和商店分发。详细范围见 [docs/mobile-android-plan.md](docs/mobile-android-plan.md)。
36
+
37
+ Android 工程入口在 `mobile/android/`。本地开发可运行 `npm run android` 启动 Expo Dev Client 并打开 Android 真机/模拟器;首次安装开发版或原生依赖变更后运行 `npm run android:build`。
38
+
33
39
  ### 方式二:npm 包
34
40
 
35
41
  适合开发、自托管或临时启动本机节点。请先安装 Node.js >= 22.12,然后运行:
@@ -272,7 +278,7 @@ git push origin vx.x.x
272
278
 
273
279
  1. **npm 包发布** — 发布 `most-box` 到 npm registry
274
280
  2. **Windows 打包** — 分别构建 `.exe` 安装包(x64 / arm64)并上传 Release
275
- 3. **macOS 打包** — 构建 `.dmg` 安装包和 `.zip` 更新包(x64 + arm64)并上传 Release
281
+ 3. **macOS 打包** — 构建 `.dmg` 安装包(x64 + arm64)并上传 Release
276
282
  4. **Linux 打包** — 构建 `.AppImage` 安装包(x64 + arm64)并上传 Release
277
283
  5. **下载镜像** — 将 Release 资产同步到 Cloudflare R2,并生成 `releases/latest.json`
278
284
 
@@ -282,6 +288,14 @@ GitHub Release 是可信备用源;下载页优先读取 R2 的 `releases/lates
282
288
 
283
289
  R2 发布资产使用独立公开桶,默认 bucket 为 `most-box-releases`,默认公开域名为
284
290
  `https://download.most.box`。不要复用 `api.most.box` 项目的 `most-box-backup` 备份桶。
291
+ Release workflow 不设置 Infrequent Access,R2 对象保持默认 Standard 存储层,并在上传后用
292
+ `head-object` 校验存储层与缓存头。版本化安装包使用
293
+ `public, max-age=31536000, immutable`;`releases/latest.json` 使用
294
+ `public, max-age=60, stale-while-revalidate=300`。
295
+ 新发版只在 manifest 和 R2 中发布当前系统可手动打开的 installer(Windows `.exe`、
296
+ macOS `.dmg`、Linux `.AppImage`),不再发布 updater / blockmap 资产。
297
+ 需要临时复查线上对象时,可手动运行 GitHub Actions 的 `Verify R2 Release` workflow;
298
+ 它复用仓库 R2 secrets 做只读 `head-object` 检查,不需要在本机输入 R2 密钥。
285
299
 
286
300
  在仓库 Settings → Secrets and variables → Actions 中添加:
287
301
 
package/electron/main.js CHANGED
@@ -3,17 +3,16 @@ import {
3
3
  BrowserWindow,
4
4
  Menu,
5
5
  Tray,
6
- ipcMain,
6
+ dialog,
7
7
  nativeImage,
8
8
  shell,
9
9
  } from 'electron'
10
- import { spawn } from 'node:child_process'
11
10
  import fs from 'node:fs'
12
11
  import path from 'node:path'
13
- import { Readable } from 'node:stream'
14
12
  import { fileURLToPath } from 'node:url'
15
13
 
16
14
  import {
15
+ formatBytes,
17
16
  getAvailableUpdate,
18
17
  getCurrentArch,
19
18
  getCurrentPlatform,
@@ -23,7 +22,6 @@ import {
23
22
  createMostDeepLinkTarget,
24
23
  findMostDeepLinkArg,
25
24
  } from './deepLink.js'
26
- import { calculateCid } from '../server/src/core/cid.js'
27
25
 
28
26
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
29
27
  const PORT = 1976
@@ -34,16 +32,6 @@ let tray = null
34
32
  let isQuitting = false
35
33
  let hasCheckedForUpdates = false
36
34
  let pendingDeepLinkUrl = ''
37
- let updateState = {
38
- status: 'idle',
39
- version: '',
40
- filename: '',
41
- source: '',
42
- progress: 0,
43
- error: '',
44
- path: '',
45
- cid: '',
46
- }
47
35
 
48
36
  const gotSingleInstanceLock = app.requestSingleInstanceLock()
49
37
 
@@ -101,57 +89,6 @@ function getInitialWindowUrl() {
101
89
  return initialUrl
102
90
  }
103
91
 
104
- function getPublicUpdateState() {
105
- return {
106
- status: updateState.status,
107
- version: updateState.version,
108
- filename: updateState.filename,
109
- source: updateState.source,
110
- progress: updateState.progress,
111
- error: updateState.error,
112
- cid: updateState.cid,
113
- }
114
- }
115
-
116
- function setUpdateState(patch) {
117
- updateState = {
118
- ...updateState,
119
- ...patch,
120
- }
121
- mainWindow?.webContents.send('updates:state', getPublicUpdateState())
122
- }
123
-
124
- function getUpdateAssetFilename(update) {
125
- return path.basename(
126
- update.asset.filename ||
127
- `MostBox-${update.version}-${getCurrentPlatform()}-${getCurrentArch()}`
128
- )
129
- }
130
-
131
- function getSafeVersionDir(version) {
132
- return String(version || 'unknown').replace(/[^a-zA-Z0-9._-]/g, '_')
133
- }
134
-
135
- function getUpdateCachePath(update) {
136
- const filename = getUpdateAssetFilename(update)
137
- return path.join(
138
- app.getPath('userData'),
139
- 'updates',
140
- getSafeVersionDir(update.version),
141
- filename
142
- )
143
- }
144
-
145
- async function verifyFileCid(filePath, expectedCid) {
146
- const { cid } = await calculateCid(filePath)
147
- const actualCid = cid.toString()
148
- if (actualCid !== expectedCid) {
149
- throw new Error(
150
- `Update CID mismatch. Expected ${expectedCid}, got ${actualCid}.`
151
- )
152
- }
153
- }
154
-
155
92
  function registerMostProtocolClient() {
156
93
  if (process.defaultApp && process.argv.length >= 2) {
157
94
  app.setAsDefaultProtocolClient('most', process.execPath, [
@@ -163,14 +100,6 @@ function registerMostProtocolClient() {
163
100
  app.setAsDefaultProtocolClient('most')
164
101
  }
165
102
 
166
- function registerUpdateIpc() {
167
- ipcMain.handle('updates:get-state', () => getPublicUpdateState())
168
- ipcMain.handle('updates:install-and-restart', async () => {
169
- await installDownloadedUpdate()
170
- return getPublicUpdateState()
171
- })
172
- }
173
-
174
103
  function openMostDeepLink(link) {
175
104
  const targetUrl = createMostDeepLinkTarget(
176
105
  link,
@@ -272,348 +201,14 @@ async function fetchReleaseManifest(manifestUrl) {
272
201
  }
273
202
  }
274
203
 
275
- async function downloadUpdateFromHttp(update, targetPath) {
276
- if (!update.downloadUrl) {
277
- throw new Error('No HTTP fallback URL is available for this update')
278
- }
279
-
280
- const tempPath = `${targetPath}.http.part`
281
- fs.rmSync(tempPath, { force: true })
282
- fs.mkdirSync(path.dirname(targetPath), { recursive: true })
283
-
284
- const response = await fetch(update.downloadUrl, { cache: 'no-store' })
285
- if (!response.ok || !response.body) {
286
- throw new Error(`HTTP update download failed: ${response.status}`)
287
- }
288
-
289
- const total = Number(response.headers.get('content-length')) || 0
290
- let loaded = 0
291
-
292
- await new Promise((resolve, reject) => {
293
- const rs = Readable.fromWeb(response.body)
294
- const ws = fs.createWriteStream(tempPath)
295
-
296
- const cleanup = err => {
297
- rs.destroy(err)
298
- ws.destroy()
299
- fs.unlink(tempPath, () => {})
300
- }
301
-
302
- rs.on('data', chunk => {
303
- loaded += chunk.length
304
- if (total > 0) {
305
- setUpdateState({
306
- status: 'downloading',
307
- source: 'http',
308
- progress: Math.min(99, Math.round((loaded / total) * 100)),
309
- })
310
- }
311
- })
312
-
313
- rs.on('error', err => {
314
- cleanup(err)
315
- reject(err)
316
- })
317
- ws.on('error', err => {
318
- cleanup(err)
319
- reject(err)
320
- })
321
- ws.on('finish', resolve)
322
- rs.pipe(ws)
323
- })
324
-
325
- try {
326
- await verifyFileCid(tempPath, update.cid)
327
- fs.rmSync(targetPath, { force: true })
328
- fs.renameSync(tempPath, targetPath)
329
- } catch (error) {
330
- fs.rmSync(tempPath, { force: true })
331
- throw error
332
- }
333
-
334
- try {
335
- await engine?.seedCidFileFromPath(update.cid, targetPath, {
336
- fileName: getUpdateAssetFilename(update),
337
- source: 'update',
338
- })
339
- } catch (error) {
340
- console.warn('[Electron] Update downloaded but seeding failed:', error)
341
- }
342
- }
343
-
344
- async function downloadUpdateByCid(update, targetPath) {
345
- if (!engine?.downloadCidToPath) {
346
- throw new Error('MostBox engine update download API is unavailable')
347
- }
348
-
349
- const taskId = `update_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
350
- const filename = getUpdateAssetFilename(update)
351
-
352
- const onProgress = data => {
353
- if (data?.taskId !== taskId) return
354
- setUpdateState({
355
- status: 'downloading',
356
- source: 'cid',
357
- progress: Math.min(99, Number(data.percent) || 0),
358
- })
359
- }
360
- const onStatus = data => {
361
- if (data?.taskId !== taskId) return
362
- setUpdateState({
363
- status: 'downloading',
364
- source: 'cid',
365
- filename,
366
- })
367
- }
368
-
369
- engine.on('download:progress', onProgress)
370
- engine.on('download:status', onStatus)
371
- try {
372
- await engine.downloadCidToPath({
373
- cid: update.cid,
374
- fileName: filename,
375
- targetPath,
376
- taskId,
377
- timeout: 30000,
378
- overwrite: true,
379
- source: 'update',
380
- })
381
- } finally {
382
- engine.off('download:progress', onProgress)
383
- engine.off('download:status', onStatus)
384
- }
385
- }
386
-
387
- async function downloadUpdate(update) {
388
- const targetPath = getUpdateCachePath(update)
389
- const filename = getUpdateAssetFilename(update)
390
-
391
- setUpdateState({
392
- status: 'downloading',
393
- version: update.version,
394
- filename,
395
- source: 'cid',
396
- progress: 0,
397
- error: '',
398
- path: '',
399
- cid: update.cid,
400
- })
401
-
402
- try {
403
- await downloadUpdateByCid(update, targetPath)
404
- } catch (cidError) {
405
- console.warn('[Electron] CID update download failed, trying fallback:', cidError)
406
- setUpdateState({
407
- status: 'downloading',
408
- source: 'http',
409
- progress: 0,
410
- error: '',
411
- })
412
- await downloadUpdateFromHttp(update, targetPath)
413
- }
414
-
415
- await verifyFileCid(targetPath, update.cid)
416
- setUpdateState({
417
- status: 'downloaded',
418
- version: update.version,
419
- filename,
420
- source: updateState.source || 'cid',
421
- progress: 100,
422
- error: '',
423
- path: targetPath,
424
- cid: update.cid,
425
- })
426
- }
427
-
428
- function quitForUpdate() {
429
- isQuitting = true
430
- if (tray) {
431
- tray.destroy()
432
- tray = null
433
- }
434
- app.quit()
435
- }
436
-
437
- function getPowerShellPath() {
438
- if (process.env.SystemRoot) {
439
- const candidate = path.join(
440
- process.env.SystemRoot,
441
- 'System32',
442
- 'WindowsPowerShell',
443
- 'v1.0',
444
- 'powershell.exe'
445
- )
446
- if (fs.existsSync(candidate)) return candidate
447
- }
448
-
449
- return 'powershell.exe'
450
- }
451
-
452
- function installWindowsUpdate(updatePath) {
453
- const scriptPath = path.join(
454
- app.getPath('userData'),
455
- 'updates',
456
- 'install-windows.ps1'
457
- )
458
- const exePath = app.getPath('exe')
459
- const script = `param(
460
- [Parameter(Mandatory=$true)][string]$UpdatePath,
461
- [Parameter(Mandatory=$true)][int]$AppPid,
462
- [Parameter(Mandatory=$true)][string]$ExePath
463
- )
464
-
465
- $ErrorActionPreference = "Stop"
466
- try {
467
- Wait-Process -Id $AppPid -ErrorAction SilentlyContinue
468
- } catch {}
469
-
470
- $installer = Start-Process -FilePath $UpdatePath -ArgumentList "/S" -Wait -PassThru
471
- if (($null -eq $installer.ExitCode -or $installer.ExitCode -eq 0) -and (Test-Path -LiteralPath $ExePath)) {
472
- Start-Process -FilePath $ExePath
473
- }
474
- `
475
-
476
- fs.mkdirSync(path.dirname(scriptPath), { recursive: true })
477
- fs.writeFileSync(scriptPath, script, 'utf8')
478
- const child = spawn(
479
- getPowerShellPath(),
480
- [
481
- '-NoProfile',
482
- '-ExecutionPolicy',
483
- 'Bypass',
484
- '-File',
485
- scriptPath,
486
- updatePath,
487
- String(process.pid),
488
- exePath,
489
- ],
490
- {
491
- detached: true,
492
- stdio: 'ignore',
493
- windowsHide: true,
494
- }
495
- )
496
- child.unref()
497
- quitForUpdate()
498
- }
499
-
500
- function getMacAppBundlePath() {
501
- return path.resolve(app.getPath('exe'), '../../..')
502
- }
503
-
504
- function installMacUpdate(updatePath) {
505
- const scriptPath = path.join(app.getPath('userData'), 'updates', 'install-mac.sh')
506
- const appBundlePath = getMacAppBundlePath()
507
- const script = `#!/bin/bash
508
- set -e
509
- APP_PATH="$1"
510
- ZIP_PATH="$2"
511
- TMP_DIR="$(mktemp -d)"
512
- cleanup() {
513
- rm -rf "$TMP_DIR"
514
- }
515
- trap cleanup EXIT
516
- while pgrep -f "$APP_PATH/Contents/MacOS" >/dev/null 2>&1; do
517
- sleep 1
518
- done
519
- ditto -x -k "$ZIP_PATH" "$TMP_DIR"
520
- NEW_APP="$(find "$TMP_DIR" -maxdepth 1 -name "*.app" -type d | head -n 1)"
521
- if [ -z "$NEW_APP" ]; then
522
- exit 1
523
- fi
524
- rm -rf "$APP_PATH"
525
- ditto "$NEW_APP" "$APP_PATH"
526
- open "$APP_PATH"
527
- `
528
-
529
- fs.mkdirSync(path.dirname(scriptPath), { recursive: true })
530
- fs.writeFileSync(scriptPath, script, { mode: 0o755 })
531
- const child = spawn('/bin/bash', [scriptPath, appBundlePath, updatePath], {
532
- detached: true,
533
- stdio: 'ignore',
534
- })
535
- child.unref()
536
- quitForUpdate()
537
- }
538
-
539
- function installLinuxUpdate(updatePath) {
540
- const currentAppImage = process.env.APPIMAGE
541
- if (!currentAppImage) {
542
- shell.openPath(updatePath).catch(() => {})
543
- throw new Error('当前 Linux 运行方式不支持自动替换 AppImage')
544
- }
545
-
546
- const scriptPath = path.join(app.getPath('userData'), 'updates', 'install-linux.sh')
547
- const script = `#!/bin/sh
548
- set -e
549
- APPIMAGE_PATH="$1"
550
- UPDATE_PATH="$2"
551
- APP_PID="$3"
552
- while kill -0 "$APP_PID" >/dev/null 2>&1; do
553
- sleep 1
554
- done
555
- cp "$UPDATE_PATH" "$APPIMAGE_PATH"
556
- chmod +x "$APPIMAGE_PATH"
557
- nohup "$APPIMAGE_PATH" >/dev/null 2>&1 &
558
- `
559
-
560
- fs.mkdirSync(path.dirname(scriptPath), { recursive: true })
561
- fs.writeFileSync(scriptPath, script, { mode: 0o755 })
562
- const child = spawn('/bin/sh', [scriptPath, currentAppImage, updatePath, String(process.pid)], {
563
- detached: true,
564
- stdio: 'ignore',
565
- })
566
- child.unref()
567
- quitForUpdate()
568
- }
569
-
570
- async function installDownloadedUpdate() {
571
- if (updateState.status !== 'downloaded' || !updateState.path) {
572
- throw new Error('No downloaded update is ready to install')
573
- }
574
-
575
- setUpdateState({ status: 'installing', error: '' })
576
-
577
- try {
578
- if (process.platform === 'win32') {
579
- installWindowsUpdate(updateState.path)
580
- return
581
- }
582
- if (process.platform === 'darwin') {
583
- installMacUpdate(updateState.path)
584
- return
585
- }
586
- if (process.platform === 'linux') {
587
- installLinuxUpdate(updateState.path)
588
- return
589
- }
590
-
591
- throw new Error('当前平台不支持自动安装更新')
592
- } catch (error) {
593
- setUpdateState({
594
- status: 'error',
595
- error: error?.message || 'Update install failed',
596
- })
597
- throw error
598
- }
599
- }
600
-
601
204
  async function checkForUpdates() {
602
205
  if (hasCheckedForUpdates) return
603
206
  hasCheckedForUpdates = true
604
207
 
605
208
  try {
606
- setUpdateState({
607
- status: 'checking',
608
- progress: 0,
609
- error: '',
610
- })
611
209
  const platform = getCurrentPlatform()
612
210
  const arch = getCurrentArch()
613
- if (!platform || !arch) {
614
- setUpdateState({ status: 'idle' })
615
- return
616
- }
211
+ if (!platform || !arch) return
617
212
 
618
213
  const manifest = await fetchReleaseManifest(getReleaseManifestUrl())
619
214
  const update = getAvailableUpdate(manifest, {
@@ -622,19 +217,35 @@ async function checkForUpdates() {
622
217
  arch,
623
218
  })
624
219
 
625
- if (!update) {
626
- setUpdateState({ status: 'idle' })
627
- return
628
- }
220
+ if (!update) return
221
+
222
+ const sizeText = formatBytes(update.asset.size)
223
+ const detail = [
224
+ `当前版本:${app.getVersion()}`,
225
+ `最新版本:${update.version}`,
226
+ sizeText ? `安装包大小:${sizeText}` : null,
227
+ '',
228
+ '是否现在下载当前系统版本的安装包?下载完成后请手动打开安装。',
229
+ ]
230
+ .filter(line => line !== null)
231
+ .join('\n')
232
+
233
+ const result = await dialog.showMessageBox(mainWindow, {
234
+ type: 'info',
235
+ buttons: ['立即下载', '稍后'],
236
+ defaultId: 0,
237
+ cancelId: 1,
238
+ title: '发现 MostBox 新版本',
239
+ message: `MostBox ${update.version} 已可下载`,
240
+ detail,
241
+ noLink: true,
242
+ })
629
243
 
630
- await downloadUpdate(update)
244
+ if (result.response === 0) {
245
+ await shell.openExternal(update.downloadUrl)
246
+ }
631
247
  } catch (error) {
632
- console.warn('[Electron] Update check failed:', error)
633
- setUpdateState({
634
- status: 'error',
635
- error: error?.message || 'Update check failed',
636
- progress: 0,
637
- })
248
+ console.warn('[Electron] Update check skipped:', error)
638
249
  }
639
250
  }
640
251
 
@@ -643,7 +254,6 @@ if (!gotSingleInstanceLock) {
643
254
  app.quit()
644
255
  } else {
645
256
  registerMostProtocolClient()
646
- registerUpdateIpc()
647
257
  openMostDeepLink(findMostDeepLinkArg(process.argv))
648
258
 
649
259
  app.on('second-instance', (_event, commandLine) => {
@@ -1,21 +1,6 @@
1
- import { contextBridge, ipcRenderer } from 'electron'
1
+ import { contextBridge } from 'electron'
2
2
 
3
3
  contextBridge.exposeInMainWorld('electronAPI', {
4
4
  platform: process.platform,
5
5
  isElectron: true,
6
- updates: {
7
- getState: () => ipcRenderer.invoke('updates:get-state'),
8
- installAndRestart: () => ipcRenderer.invoke('updates:install-and-restart'),
9
- onStateChange: callback => {
10
- if (typeof callback !== 'function') return () => {}
11
-
12
- const listener = (_event, state) => {
13
- callback(state)
14
- }
15
- ipcRenderer.on('updates:state', listener)
16
- return () => {
17
- ipcRenderer.off('updates:state', listener)
18
- }
19
- },
20
- },
21
6
  })
@@ -1,3 +1,9 @@
1
+ import {
2
+ getInstallerReleaseAssets,
3
+ isReleaseManifest,
4
+ resolveReleaseAssetDownload,
5
+ } from '../server/src/core/releaseManifest.js'
6
+
1
7
  export const DEFAULT_RELEASE_MANIFEST_URL =
2
8
  'https://download.most.box/releases/latest.json'
3
9
 
@@ -43,34 +49,11 @@ export function isNewerVersion(candidateVersion, currentVersion) {
43
49
  return false
44
50
  }
45
51
 
46
- function isRecord(value) {
47
- return typeof value === 'object' && value !== null
48
- }
49
-
50
- function hasDownloadUrl(asset) {
51
- return typeof asset.r2Url === 'string' || typeof asset.githubUrl === 'string'
52
- }
53
-
54
52
  export function findUpdateAsset(manifest, platform, arch) {
55
- if (!isRecord(manifest) || !Array.isArray(manifest.assets)) return null
56
-
57
- const compatibleAssets = manifest.assets.filter(
58
- asset =>
59
- isRecord(asset) &&
60
- asset.platform === platform &&
61
- asset.arch === arch &&
62
- (asset.kind === 'updater' || asset.kind === 'installer') &&
63
- typeof asset.cid === 'string' &&
64
- hasDownloadUrl(asset) &&
65
- (typeof asset.githubUrl === 'string' ||
66
- typeof asset.githubUrl === 'undefined') &&
67
- (typeof asset.r2Url === 'string' || typeof asset.r2Url === 'undefined')
68
- )
69
-
70
53
  return (
71
- compatibleAssets.find(asset => asset.kind === 'updater') ||
72
- compatibleAssets.find(asset => asset.kind === 'installer') ||
73
- null
54
+ getInstallerReleaseAssets(manifest).find(
55
+ asset => asset.platform === platform && asset.arch === arch
56
+ ) || null
74
57
  )
75
58
  }
76
59
 
@@ -87,8 +70,7 @@ export function getAvailableUpdate(manifest, options = {}) {
87
70
  const arch = options.arch
88
71
 
89
72
  if (
90
- !isRecord(manifest) ||
91
- typeof manifest.version !== 'string' ||
73
+ !isReleaseManifest(manifest) ||
92
74
  !isNewerVersion(manifest.version, currentVersion)
93
75
  ) {
94
76
  return null
@@ -97,12 +79,12 @@ export function getAvailableUpdate(manifest, options = {}) {
97
79
  const asset = findUpdateAsset(manifest, platform, arch)
98
80
  if (!asset) return null
99
81
 
100
- const downloadUrl = asset.r2Url || asset.githubUrl
82
+ const { url: downloadUrl } = resolveReleaseAssetDownload(asset, 'r2')
83
+ if (!downloadUrl) return null
101
84
 
102
85
  return {
103
86
  version: manifest.version,
104
87
  asset,
105
- cid: asset.cid,
106
88
  downloadUrl,
107
89
  }
108
90
  }