most-box 0.1.1 → 0.1.3

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 (231) hide show
  1. package/README.md +39 -19
  2. package/electron/afterPack.cjs +87 -0
  3. package/electron/main.js +156 -8
  4. package/electron/preload.js +6 -0
  5. package/electron/updateChecker.js +97 -0
  6. package/electron/updateChecker.test.js +147 -0
  7. package/out/404/index.html +2 -2
  8. package/out/404.html +2 -2
  9. package/out/__next.__PAGE__.txt +6 -6
  10. package/out/__next._full.txt +16 -16
  11. package/out/__next._head.txt +3 -3
  12. package/out/__next._index.txt +8 -8
  13. package/out/__next._tree.txt +5 -5
  14. package/out/_next/static/chunks/04mo7rr..0_1q.js +1 -0
  15. package/out/_next/static/chunks/06rf3qq5ggs6v.js +1 -0
  16. package/out/_next/static/chunks/07td.jq7xff84.css +1 -0
  17. package/out/_next/static/chunks/0_0oph_z1az14.js +1 -0
  18. package/out/_next/static/chunks/{0qou.u2e2dy48.css → 0adx~d-j05c9d.css} +2 -2
  19. package/out/_next/static/chunks/0cl7d~7abnk_p.css +1 -0
  20. package/out/_next/static/chunks/0d306t1wvjpdx.js +1 -0
  21. package/out/_next/static/chunks/0g_a~e050bgzg.css +1 -0
  22. package/out/_next/static/chunks/{0o6lrkxy4jwag.js → 0gcsdf57gcm6h.js} +1 -1
  23. package/out/_next/static/chunks/0hpev4am9jpmu.css +1 -0
  24. package/out/_next/static/chunks/0m_5nb6x8qy._.js +1 -0
  25. package/out/_next/static/chunks/0n.ayxmsar6e5.js +1 -0
  26. package/out/_next/static/chunks/{0usvo~vu7r8np.js → 0o9ce4cyf76by.js} +1 -1
  27. package/out/_next/static/chunks/0olqjomda37-e.js +1 -0
  28. package/out/_next/static/chunks/{0o98f1yq..o.8.js → 0pt.5cg1t09qs.js} +1 -1
  29. package/out/_next/static/chunks/0qgx9t4jx16ua.css +1 -0
  30. package/out/_next/static/chunks/0s~g.l~x049o2.js +1 -0
  31. package/out/_next/static/chunks/0ukyg~tkm~h2m.css +1 -0
  32. package/out/_next/static/chunks/0voe1.ttrh84k.css +1 -0
  33. package/out/_next/static/chunks/0wtf0xsiicxx6.js +1 -0
  34. package/out/_next/static/chunks/0x.ky97owcxxs.js +1 -0
  35. package/out/_next/static/chunks/0xdwau5k2augv.css +4 -0
  36. package/out/_next/static/chunks/0ysj5b94vu4ri.js +1 -0
  37. package/out/_next/static/chunks/12nr19.nnn6s3.js +5 -0
  38. package/out/_next/static/chunks/{0qub_r0x_r-e9.css → 12pep-2t-qg4n.css} +1 -1
  39. package/out/_next/static/chunks/14_inksek_rth.js +2 -0
  40. package/out/_next/static/chunks/153-sz7s.qml2.js +1 -0
  41. package/out/_next/static/chunks/17cwkb2yn_akx.js +1 -0
  42. package/out/_next/static/chunks/184hxsuf-5c84.js +1 -0
  43. package/out/_next/static/chunks/{turbopack-0xs6mybc~5t_3.js → turbopack-0xta0kqwzkf28.js} +1 -1
  44. package/out/_not-found/__next._full.txt +13 -13
  45. package/out/_not-found/__next._head.txt +3 -3
  46. package/out/_not-found/__next._index.txt +8 -8
  47. package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
  48. package/out/_not-found/__next._not-found.txt +3 -3
  49. package/out/_not-found/__next._tree.txt +3 -3
  50. package/out/_not-found/index.html +2 -2
  51. package/out/_not-found/index.txt +13 -13
  52. package/out/admin/__next._full.txt +15 -15
  53. package/out/admin/__next._head.txt +3 -3
  54. package/out/admin/__next._index.txt +8 -8
  55. package/out/admin/__next._tree.txt +4 -4
  56. package/out/admin/__next.admin.__PAGE__.txt +4 -4
  57. package/out/admin/__next.admin.txt +4 -4
  58. package/out/admin/index.html +2 -2
  59. package/out/admin/index.txt +15 -15
  60. package/out/app/__next._full.txt +14 -14
  61. package/out/app/__next._head.txt +3 -3
  62. package/out/app/__next._index.txt +8 -8
  63. package/out/app/__next._tree.txt +3 -3
  64. package/out/app/__next.app.__PAGE__.txt +4 -4
  65. package/out/app/__next.app.txt +3 -3
  66. package/out/app/index.html +2 -2
  67. package/out/app/index.txt +14 -14
  68. package/out/chat/__next._full.txt +15 -15
  69. package/out/chat/__next._head.txt +3 -3
  70. package/out/chat/__next._index.txt +8 -8
  71. package/out/chat/__next._tree.txt +4 -4
  72. package/out/chat/__next.chat.__PAGE__.txt +4 -4
  73. package/out/chat/__next.chat.txt +4 -4
  74. package/out/chat/index.html +2 -2
  75. package/out/chat/index.txt +15 -15
  76. package/out/chat/join/__next._full.txt +25 -0
  77. package/out/chat/join/__next._head.txt +5 -0
  78. package/out/{changelog → chat/join}/__next._index.txt +8 -8
  79. package/out/chat/join/__next._tree.txt +5 -0
  80. package/out/chat/join/__next.chat.join.__PAGE__.txt +9 -0
  81. package/out/chat/join/__next.chat.join.txt +5 -0
  82. package/out/chat/join/__next.chat.txt +5 -0
  83. package/out/chat/join/index.html +15 -0
  84. package/out/chat/join/index.txt +25 -0
  85. package/out/download/__next._full.txt +37 -33
  86. package/out/download/__next._head.txt +3 -3
  87. package/out/download/__next._index.txt +8 -8
  88. package/out/download/__next._tree.txt +5 -5
  89. package/out/download/__next.download.__PAGE__.txt +9 -14
  90. package/out/download/__next.download.txt +3 -3
  91. package/out/download/index.html +2 -2
  92. package/out/download/index.txt +37 -33
  93. package/out/favicon.ico +0 -0
  94. package/out/gandengyan/__next._full.txt +25 -0
  95. package/out/{changelog → gandengyan}/__next._head.txt +3 -3
  96. package/out/{docs → gandengyan}/__next._index.txt +8 -8
  97. package/out/gandengyan/__next._tree.txt +5 -0
  98. package/out/gandengyan/__next.gandengyan.__PAGE__.txt +10 -0
  99. package/out/gandengyan/__next.gandengyan.txt +5 -0
  100. package/out/gandengyan/index.html +15 -0
  101. package/out/gandengyan/index.txt +25 -0
  102. package/out/index.html +2 -2
  103. package/out/index.txt +16 -16
  104. package/out/note/__next._full.txt +14 -14
  105. package/out/note/__next._head.txt +3 -3
  106. package/out/note/__next._index.txt +8 -8
  107. package/out/note/__next._tree.txt +3 -3
  108. package/out/note/__next.note.__PAGE__.txt +4 -4
  109. package/out/note/__next.note.txt +3 -3
  110. package/out/note/index.html +2 -2
  111. package/out/note/index.txt +14 -14
  112. package/out/ping/__next._full.txt +16 -16
  113. package/out/ping/__next._head.txt +3 -3
  114. package/out/ping/__next._index.txt +8 -8
  115. package/out/ping/__next._tree.txt +5 -5
  116. package/out/ping/__next.ping.__PAGE__.txt +5 -5
  117. package/out/ping/__next.ping.txt +4 -4
  118. package/out/ping/index.html +2 -2
  119. package/out/ping/index.txt +16 -16
  120. package/out/web3/__next._full.txt +15 -15
  121. package/out/web3/__next._head.txt +3 -3
  122. package/out/web3/__next._index.txt +8 -8
  123. package/out/web3/__next._tree.txt +4 -4
  124. package/out/web3/__next.web3.__PAGE__.txt +4 -4
  125. package/out/web3/__next.web3.txt +4 -4
  126. package/out/web3/ed25519/__next._full.txt +13 -13
  127. package/out/web3/ed25519/__next._head.txt +3 -3
  128. package/out/web3/ed25519/__next._index.txt +8 -8
  129. package/out/web3/ed25519/__next._tree.txt +4 -4
  130. package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
  131. package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
  132. package/out/web3/ed25519/__next.web3.txt +4 -4
  133. package/out/web3/ed25519/index.html +1 -1
  134. package/out/web3/ed25519/index.txt +13 -13
  135. package/out/web3/index.html +2 -2
  136. package/out/web3/index.txt +15 -15
  137. package/out/web3/tools/__next._full.txt +13 -13
  138. package/out/web3/tools/__next._head.txt +3 -3
  139. package/out/web3/tools/__next._index.txt +8 -8
  140. package/out/web3/tools/__next._tree.txt +4 -4
  141. package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
  142. package/out/web3/tools/__next.web3.tools.txt +3 -3
  143. package/out/web3/tools/__next.web3.txt +4 -4
  144. package/out/web3/tools/index.html +1 -1
  145. package/out/web3/tools/index.txt +13 -13
  146. package/package.json +44 -35
  147. package/public/favicon.ico +0 -0
  148. package/server/index.js +142 -1304
  149. package/server/src/config.js +1 -1
  150. package/server/src/core/channelAttachment.js +68 -0
  151. package/server/src/core/cid.js +2 -88
  152. package/server/src/core/cidTopic.js +29 -0
  153. package/server/src/core/mostLink.js +88 -0
  154. package/server/src/games/gandengyan.js +675 -0
  155. package/server/src/http/access.js +127 -0
  156. package/server/src/http/app.js +1102 -0
  157. package/server/src/http/errors.js +35 -0
  158. package/server/src/http/nodeLogs.js +53 -0
  159. package/server/src/http/nodeStatus.js +146 -0
  160. package/server/src/http/staticFiles.js +84 -0
  161. package/server/src/http/uploads.js +114 -0
  162. package/server/src/index.js +799 -211
  163. package/server/src/node/config.js +38 -6
  164. package/server/src/utils/api.js +305 -14
  165. package/server/src/utils/auth.js +63 -0
  166. package/server/src/utils/dateTime.js +30 -0
  167. package/server/src/utils/downloadMessages.js +89 -0
  168. package/server/src/utils/errors.js +7 -0
  169. package/server/src/utils/mostWallet.js +151 -0
  170. package/server/src/utils/mp.js +2 -26
  171. package/server/src/utils/noteBackup.js +2 -5
  172. package/server/src/utils/noteUtils.js +11 -3
  173. package/server/src/utils/userIdentity.js +0 -1
  174. package/out/_next/static/chunks/00-u5nq76f0.j.js +0 -1
  175. package/out/_next/static/chunks/00fm8lijienf1.js +0 -1
  176. package/out/_next/static/chunks/00o9ht.f2qm00.css +0 -4
  177. package/out/_next/static/chunks/00zi-erhjrny2.js +0 -2
  178. package/out/_next/static/chunks/084xf0edl9sfo.js +0 -1
  179. package/out/_next/static/chunks/09f1gfke9m5wg.css +0 -1
  180. package/out/_next/static/chunks/09xyi6fpro_d-.css +0 -1
  181. package/out/_next/static/chunks/0_npg_pcoywti.js +0 -5
  182. package/out/_next/static/chunks/0_r_mk1~6bosc.js +0 -1
  183. package/out/_next/static/chunks/0arm0a6adt7cc.css +0 -1
  184. package/out/_next/static/chunks/0c9j3eq_14vv2.css +0 -1
  185. package/out/_next/static/chunks/0d4bueddmcnca.js +0 -1
  186. package/out/_next/static/chunks/0gtwvy1z9ksa7.css +0 -1
  187. package/out/_next/static/chunks/0ho~log~~-jwp.css +0 -1
  188. package/out/_next/static/chunks/0j27tcmtt4ly7.js +0 -1
  189. package/out/_next/static/chunks/0j3v4mq67wtnh.js +0 -1
  190. package/out/_next/static/chunks/0lkmf5ry.s_7w.js +0 -1
  191. package/out/_next/static/chunks/0p486m03-zfoi.js +0 -1
  192. package/out/_next/static/chunks/0r1~k82nji8sf.js +0 -1
  193. package/out/_next/static/chunks/0v7qp4hv-_._r.js +0 -1
  194. package/out/_next/static/chunks/0wuwlgcn6gxqt.js +0 -1
  195. package/out/_next/static/chunks/0xl5_avhu._i8.js +0 -1
  196. package/out/_next/static/chunks/10kvl8vj_plm-.js +0 -1
  197. package/out/_next/static/chunks/16m27azcs4k6w.js +0 -1
  198. package/out/changelog/__next._full.txt +0 -25
  199. package/out/changelog/__next._tree.txt +0 -5
  200. package/out/changelog/__next.changelog.__PAGE__.txt +0 -10
  201. package/out/changelog/__next.changelog.txt +0 -5
  202. package/out/changelog/index.html +0 -15
  203. package/out/changelog/index.txt +0 -25
  204. package/out/docs/__next._full.txt +0 -25
  205. package/out/docs/__next._head.txt +0 -5
  206. package/out/docs/__next._tree.txt +0 -5
  207. package/out/docs/__next.docs.__PAGE__.txt +0 -10
  208. package/out/docs/__next.docs.txt +0 -5
  209. package/out/docs/getting-started/__next._full.txt +0 -25
  210. package/out/docs/getting-started/__next._head.txt +0 -5
  211. package/out/docs/getting-started/__next._index.txt +0 -9
  212. package/out/docs/getting-started/__next._tree.txt +0 -5
  213. package/out/docs/getting-started/__next.docs.getting-started.__PAGE__.txt +0 -10
  214. package/out/docs/getting-started/__next.docs.getting-started.txt +0 -5
  215. package/out/docs/getting-started/__next.docs.txt +0 -5
  216. package/out/docs/getting-started/index.html +0 -15
  217. package/out/docs/getting-started/index.txt +0 -25
  218. package/out/docs/index.html +0 -15
  219. package/out/docs/index.txt +0 -25
  220. package/out/note/edit/__next._full.txt +0 -24
  221. package/out/note/edit/__next._head.txt +0 -5
  222. package/out/note/edit/__next._index.txt +0 -9
  223. package/out/note/edit/__next._tree.txt +0 -4
  224. package/out/note/edit/__next.note.edit.__PAGE__.txt +0 -9
  225. package/out/note/edit/__next.note.edit.txt +0 -5
  226. package/out/note/edit/__next.note.txt +0 -5
  227. package/out/note/edit/index.html +0 -15
  228. package/out/note/edit/index.txt +0 -24
  229. /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → aPEZ4zaaR5W3WpSZ0dFsa}/_buildManifest.js +0 -0
  230. /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → aPEZ4zaaR5W3WpSZ0dFsa}/_clientMiddlewareManifest.js +0 -0
  231. /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → aPEZ4zaaR5W3WpSZ0dFsa}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  | 🚀 P2P直连,不限速 | ✅ | ❌ | 限流 |
15
15
  | 💾 去中心化存储 | ✅ | ❌ | ❌ |
16
16
  | 🌐 开源免费,自托管 | ✅ | ❌ | |
17
- | 📦 无限文件大小 | ✅ | ❌ | 限流 |
17
+ | 📦 大文件分享 | ✅ | ❌ | 限流 |
18
18
 
19
19
  ## 演示
20
20
 
@@ -48,7 +48,8 @@ npx most-box@latest
48
48
  git clone <your-repo-url>
49
49
  cd most
50
50
  npm i
51
- npm start
51
+ npm run dev
52
+ node server/index.js
52
53
  ```
53
54
 
54
55
  ## 测试
@@ -60,19 +61,24 @@ npm run test:unit # 只运行单元测试
60
61
 
61
62
  ## 访问场景
62
63
 
63
- | 场景 | 命令 | 访问地址 |
64
- | ---- | ------------------------------------------ | ------------------------- |
65
- | 本地 | `npx most-box` | `http://localhost:1976` |
66
- | 内网 | `set MOSTBOX_HOST=0.0.0.0 && npx most-box` | `http://<IP>:1976` |
67
- | 外网 | Caddy 反向代理 | `https://your-domain.com` |
64
+ | 场景 | 方式 | 访问地址 |
65
+ | -------- | ------------------------------------- | ----------------------- |
66
+ | 本地 | `npx most-box` | `http://localhost:1976` |
67
+ | 远程管理 | SSH 隧道 + `/admin/` | `http://localhost:1976` |
68
+ | 外网 | Caddy 反向代理 | `https://your-domain` |
68
69
 
69
- ### 内网访问
70
+ ### 远程管理节点
71
+
72
+ MostBox 默认只监听 `127.0.0.1`,无需开放端口即可安全运行。
73
+
74
+ 要管理部署在远程服务器上的节点,使用 SSH 隧道将服务器的 1976 端口转发到本地:
70
75
 
71
76
  ```bash
72
- set MOSTBOX_HOST=0.0.0.0
73
- npx most-box
77
+ ssh -L 1976:127.0.0.1:1976 user@your-server
74
78
  ```
75
79
 
80
+ 然后在本地浏览器打开 `http://localhost:1976/admin/` 即可管理远程节点。
81
+
76
82
  ### 外网访问(Caddy)
77
83
 
78
84
  ```caddy
@@ -81,6 +87,8 @@ mostbox.example.com {
81
87
  }
82
88
  ```
83
89
 
90
+ 开放到局域网或公网时,在 `/admin/` 中配置邀请码,远程请求必须携带有效邀请码。
91
+
84
92
  ## 核心功能
85
93
 
86
94
  1. **确定性 P2P 文件发布**
@@ -109,7 +117,7 @@ mostbox.example.com {
109
117
 
110
118
  ### 文件存储在哪里?
111
119
 
112
- 文件以 **P2P 方式** 存储在分享者和接收者的设备上。当文件被分享时,内容会被分片存储在 P2P 网络中。**没有中心化服务器**,真正实现去中心化。
120
+ 文件以 **P2P 方式** 保存在分享者和接收者的设备上。每个做种节点都持有完整文件副本;MostBox 不会把文件集中上传到云端服务器。
113
121
 
114
122
  ### 如何分享文件给其他人?
115
123
 
@@ -124,7 +132,7 @@ mostbox.example.com {
124
132
 
125
133
  ### 支持大文件吗?
126
134
 
127
- 支持。目前已测试通过 **GB 级别**的大文件传输,采用流式处理,内存占用低。
135
+ 支持。目前默认单文件上限为 **10GB**,可在本地节点策略中调整;传输采用流式处理,内存占用低。
128
136
 
129
137
  ### 频道聊天是什么?
130
138
 
@@ -133,7 +141,7 @@ mostbox.example.com {
133
141
  - 创建一个频道(如 `alice` 或 `team-project`)
134
142
  - 将频道名称分享给朋友
135
143
  - 朋友加入后即可实时聊天
136
- - 消息通过 P2P 网络加密传输,服务器只负责建立连接
144
+ - 消息通过 P2P 通道复制,服务器或节点只负责连接与同步
137
145
 
138
146
  ### 如何使用频道聊天?
139
147
 
@@ -183,7 +191,6 @@ npx most-box
183
191
  - **前端**: React 19, Next.js 16, TypeScript, Zustand, Lucide React
184
192
  - **后端**: Hono + @hono/node-server + WebSocket
185
193
  - **P2P**: Hyperswarm 4.x, Hyperdrive 13.x, Corestore 7.x
186
- - **Web3**: ethers.js, Hardhat, Solidity, EIP-712
187
194
  - **桌面**: Electron 41, electron-builder
188
195
  - **测试**: Node.js built-in test runner
189
196
 
@@ -199,19 +206,32 @@ git push origin v0.0.7
199
206
  触发后自动执行:
200
207
 
201
208
  1. **npm 包发布** — 发布 `most-box` 到 npm registry
202
- 2. **Windows 打包** — 构建 `.exe` 安装包(x64 + arm64)并上传 Release
209
+ 2. **Windows 打包** — 分别构建 `.exe` 安装包(x64 / arm64)并上传 Release
203
210
  3. **macOS 打包** — 构建 `.dmg` 安装包(x64 + arm64)并上传 Release
204
211
  4. **Linux 打包** — 构建 `.AppImage` 安装包(x64 + arm64)并上传 Release
212
+ 5. **下载镜像** — 将 Release 资产同步到 Cloudflare R2,并生成 `releases/latest.json`
205
213
 
206
- electron-builder 自动创建 GitHub Release 并附加所有平台安装包。
214
+ GitHub Release 是可信备用源;下载页优先读取 R2 的 `releases/latest.json` 并使用 R2 下载链接。
207
215
 
208
216
  ### 配置 Secrets
209
217
 
218
+ R2 发布资产使用独立公开桶,默认 bucket 为 `most-box-releases`,默认公开域名为
219
+ `https://download.most.box`。不要复用 `api.most.box` 项目的 `most-box-backup` 备份桶。
220
+
210
221
  在仓库 Settings → Secrets and variables → Actions 中添加:
211
222
 
212
- | Secret | 说明 |
213
- | ----------- | --------------------------------------- |
214
- | `NPM_TOKEN` | npm 发布令牌(`npm token create` 生成) |
223
+ | Secret | 说明 |
224
+ | ---------------------- | --------------------------------------- |
225
+ | `NPM_TOKEN` | npm 发布令牌(`npm token create` 生成) |
226
+ | `R2_ACCOUNT_ID` | Cloudflare 账户 ID |
227
+ | `R2_ACCESS_KEY_ID` | R2 S3 API Access Key ID |
228
+ | `R2_SECRET_ACCESS_KEY` | R2 S3 API Secret Access Key |
229
+ | `R2_BUCKET` | 可选;默认 `most-box-releases` |
230
+ | `R2_PUBLIC_BASE_URL` | 可选;默认 `https://download.most.box` |
231
+
232
+ 下载页默认读取 `https://download.most.box/releases/latest.json`。部署环境可额外配置
233
+ `NEXT_PUBLIC_R2_PUBLIC_BASE_URL` 覆盖公开域名,或直接配置
234
+ `NEXT_PUBLIC_RELEASE_MANIFEST_URL` 指向指定的 `latest.json`。
215
235
 
216
236
  ## 社区
217
237
 
@@ -0,0 +1,87 @@
1
+ const fs = require('node:fs')
2
+ const path = require('node:path')
3
+
4
+ const ARCH_BY_VALUE = new Map([
5
+ [0, 'ia32'],
6
+ [1, 'x64'],
7
+ [2, 'armv7l'],
8
+ [3, 'arm64'],
9
+ [4, 'universal'],
10
+ ])
11
+
12
+ function normalizeArch(arch) {
13
+ if (typeof arch === 'string') return arch
14
+ if (typeof arch === 'number') return ARCH_BY_VALUE.get(arch) || String(arch)
15
+ return String(arch || '')
16
+ }
17
+
18
+ function findPrebuildDirs(root) {
19
+ const matches = []
20
+ const stack = [root]
21
+
22
+ while (stack.length > 0) {
23
+ const dir = stack.pop()
24
+ let entries
25
+ try {
26
+ entries = fs.readdirSync(dir, { withFileTypes: true })
27
+ } catch {
28
+ continue
29
+ }
30
+
31
+ for (const entry of entries) {
32
+ if (!entry.isDirectory()) continue
33
+
34
+ const fullPath = path.join(dir, entry.name)
35
+ if (entry.name === 'prebuilds') {
36
+ matches.push(fullPath)
37
+ continue
38
+ }
39
+ stack.push(fullPath)
40
+ }
41
+ }
42
+
43
+ return matches
44
+ }
45
+
46
+ function shouldKeepPrebuild(name, target) {
47
+ return (
48
+ name === target ||
49
+ name.startsWith(`${target}-`) ||
50
+ name.startsWith(`${target}+`)
51
+ )
52
+ }
53
+
54
+ module.exports = async function afterPack(context) {
55
+ const platform = context.electronPlatformName
56
+ const arch = normalizeArch(context.arch)
57
+ const target = `${platform}-${arch}`
58
+ const nodeModulesDir = path.join(
59
+ context.appOutDir,
60
+ 'resources',
61
+ 'app.asar.unpacked',
62
+ 'node_modules'
63
+ )
64
+
65
+ if (!platform || !arch || arch === 'universal' || !fs.existsSync(nodeModulesDir)) {
66
+ return
67
+ }
68
+
69
+ let removed = 0
70
+ for (const prebuildDir of findPrebuildDirs(nodeModulesDir)) {
71
+ const entries = fs.readdirSync(prebuildDir, { withFileTypes: true })
72
+ for (const entry of entries) {
73
+ if (!entry.isDirectory()) continue
74
+ if (shouldKeepPrebuild(entry.name, target)) continue
75
+
76
+ fs.rmSync(path.join(prebuildDir, entry.name), {
77
+ force: true,
78
+ recursive: true,
79
+ })
80
+ removed += 1
81
+ }
82
+ }
83
+
84
+ if (removed > 0) {
85
+ console.log(`[MostBox] Pruned ${removed} native prebuild directories for ${target}`)
86
+ }
87
+ }
package/electron/main.js CHANGED
@@ -1,12 +1,85 @@
1
- import { app, BrowserWindow, Menu } from 'electron'
1
+ import {
2
+ app,
3
+ BrowserWindow,
4
+ Menu,
5
+ Tray,
6
+ dialog,
7
+ nativeImage,
8
+ shell,
9
+ } from 'electron'
10
+ import fs from 'node:fs'
2
11
  import path from 'node:path'
3
12
  import { fileURLToPath } from 'node:url'
4
13
 
14
+ import {
15
+ formatBytes,
16
+ getAvailableUpdate,
17
+ getCurrentArch,
18
+ getCurrentPlatform,
19
+ getReleaseManifestUrl,
20
+ } from './updateChecker.js'
21
+
5
22
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
6
- const PORT = Number(process.env.MOSTBOX_PORT) || 1976
23
+ const PORT = 1976
7
24
 
8
25
  let mainWindow = null
9
26
  let engine = null
27
+ let tray = null
28
+ let isQuitting = false
29
+ let hasCheckedForUpdates = false
30
+
31
+ function getTrayIconPath() {
32
+ const candidates = [
33
+ path.join(__dirname, '..', 'out', 'favicon.ico'),
34
+ path.join(process.resourcesPath, 'app', 'out', 'favicon.ico'),
35
+ path.join(__dirname, '..', 'public', 'favicon.ico'),
36
+ ]
37
+
38
+ return candidates.find(candidate => fs.existsSync(candidate))
39
+ }
40
+
41
+ function showMainWindow() {
42
+ if (!mainWindow) {
43
+ createWindow()
44
+ return
45
+ }
46
+
47
+ if (mainWindow.isMinimized()) {
48
+ mainWindow.restore()
49
+ }
50
+ mainWindow.show()
51
+ mainWindow.focus()
52
+ }
53
+
54
+ function quitFromTray() {
55
+ isQuitting = true
56
+ if (tray) {
57
+ tray.destroy()
58
+ tray = null
59
+ }
60
+ app.quit()
61
+ }
62
+
63
+ function createTray() {
64
+ if (tray) {
65
+ return
66
+ }
67
+
68
+ const iconPath = getTrayIconPath()
69
+ const image = iconPath ? nativeImage.createFromPath(iconPath) : null
70
+
71
+ tray = new Tray(image && !image.isEmpty() ? image : nativeImage.createEmpty())
72
+ tray.setToolTip('MostBox')
73
+ tray.setContextMenu(
74
+ Menu.buildFromTemplate([
75
+ { label: '打开 MostBox', click: showMainWindow },
76
+ { type: 'separator' },
77
+ { label: '退出', click: quitFromTray },
78
+ ])
79
+ )
80
+ tray.on('click', showMainWindow)
81
+ tray.on('double-click', showMainWindow)
82
+ }
10
83
 
11
84
  function createWindow() {
12
85
  mainWindow = new BrowserWindow({
@@ -25,6 +98,15 @@ function createWindow() {
25
98
 
26
99
  mainWindow.loadURL(`http://localhost:${PORT}`)
27
100
 
101
+ mainWindow.on('close', event => {
102
+ if (isQuitting) {
103
+ return
104
+ }
105
+
106
+ event.preventDefault()
107
+ mainWindow.hide()
108
+ })
109
+
28
110
  mainWindow.on('closed', () => {
29
111
  mainWindow = null
30
112
  })
@@ -37,30 +119,96 @@ async function startServer() {
37
119
  engine = await main()
38
120
  }
39
121
 
122
+ async function fetchReleaseManifest(manifestUrl) {
123
+ const controller = new AbortController()
124
+ const timeout = setTimeout(() => controller.abort(), 8000)
125
+
126
+ try {
127
+ const response = await fetch(manifestUrl, {
128
+ cache: 'no-store',
129
+ signal: controller.signal,
130
+ })
131
+ if (!response.ok) return null
132
+ return await response.json()
133
+ } finally {
134
+ clearTimeout(timeout)
135
+ }
136
+ }
137
+
138
+ async function checkForUpdates() {
139
+ if (hasCheckedForUpdates) return
140
+ hasCheckedForUpdates = true
141
+
142
+ try {
143
+ const platform = getCurrentPlatform()
144
+ const arch = getCurrentArch()
145
+ if (!platform || !arch) return
146
+
147
+ const manifest = await fetchReleaseManifest(getReleaseManifestUrl())
148
+ const update = getAvailableUpdate(manifest, {
149
+ currentVersion: app.getVersion(),
150
+ platform,
151
+ arch,
152
+ })
153
+
154
+ if (!update) return
155
+
156
+ const sizeText = formatBytes(update.asset.size)
157
+ const detail = [
158
+ `当前版本:${app.getVersion()}`,
159
+ `最新版本:${update.version}`,
160
+ sizeText ? `安装包大小:${sizeText}` : null,
161
+ '',
162
+ '是否现在打开浏览器下载更新?',
163
+ ]
164
+ .filter(line => line !== null)
165
+ .join('\n')
166
+
167
+ const result = await dialog.showMessageBox(mainWindow, {
168
+ type: 'info',
169
+ buttons: ['立即下载', '稍后'],
170
+ defaultId: 0,
171
+ cancelId: 1,
172
+ title: '发现 MostBox 新版本',
173
+ message: `MostBox ${update.version} 已可下载`,
174
+ detail,
175
+ noLink: true,
176
+ })
177
+
178
+ if (result.response === 0) {
179
+ await shell.openExternal(update.downloadUrl)
180
+ }
181
+ } catch (error) {
182
+ console.warn('[Electron] Update check skipped:', error)
183
+ }
184
+ }
185
+
40
186
  app.whenReady().then(async () => {
41
187
  try {
42
188
  await startServer()
43
189
  createWindow()
190
+ createTray()
44
191
  Menu.setApplicationMenu(null)
192
+ setTimeout(() => {
193
+ checkForUpdates()
194
+ }, 3000)
45
195
 
46
196
  app.on('activate', () => {
47
- if (BrowserWindow.getAllWindows().length === 0) {
48
- createWindow()
49
- }
197
+ showMainWindow()
50
198
  })
51
199
  } catch (err) {
52
200
  console.error('[Electron] Failed to start server:', err)
201
+ isQuitting = true
53
202
  app.quit()
54
203
  }
55
204
  })
56
205
 
57
206
  app.on('window-all-closed', () => {
58
- if (process.platform !== 'darwin') {
59
- app.quit()
60
- }
207
+ // Keep the daemon alive; users exit from the tray menu.
61
208
  })
62
209
 
63
210
  app.on('before-quit', () => {
211
+ isQuitting = true
64
212
  if (engine && engine.stop) {
65
213
  engine.stop().catch(() => {})
66
214
  }
@@ -0,0 +1,6 @@
1
+ import { contextBridge } from 'electron'
2
+
3
+ contextBridge.exposeInMainWorld('electronAPI', {
4
+ platform: process.platform,
5
+ isElectron: true,
6
+ })
@@ -0,0 +1,97 @@
1
+ export const DEFAULT_RELEASE_MANIFEST_URL =
2
+ 'https://download.most.box/releases/latest.json'
3
+
4
+ const PLATFORM_BY_PROCESS = {
5
+ darwin: 'macos',
6
+ linux: 'linux',
7
+ win32: 'windows',
8
+ }
9
+
10
+ export function getReleaseManifestUrl(env = process.env) {
11
+ return env.MOSTBOX_RELEASE_MANIFEST_URL || DEFAULT_RELEASE_MANIFEST_URL
12
+ }
13
+
14
+ export function getCurrentPlatform(processPlatform = process.platform) {
15
+ return PLATFORM_BY_PROCESS[processPlatform] || null
16
+ }
17
+
18
+ export function getCurrentArch(processArch = process.arch) {
19
+ if (processArch === 'x64' || processArch === 'arm64') return processArch
20
+ return null
21
+ }
22
+
23
+ function parseVersion(version) {
24
+ if (typeof version !== 'string') return null
25
+
26
+ const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/)
27
+ if (!match) return null
28
+
29
+ return match.slice(1).map(part => Number(part))
30
+ }
31
+
32
+ export function isNewerVersion(candidateVersion, currentVersion) {
33
+ const candidate = parseVersion(candidateVersion)
34
+ const current = parseVersion(currentVersion)
35
+
36
+ if (!candidate || !current) return false
37
+
38
+ for (let index = 0; index < candidate.length; index += 1) {
39
+ if (candidate[index] > current[index]) return true
40
+ if (candidate[index] < current[index]) return false
41
+ }
42
+
43
+ return false
44
+ }
45
+
46
+ function isRecord(value) {
47
+ return typeof value === 'object' && value !== null
48
+ }
49
+
50
+ export function findUpdateAsset(manifest, platform, arch) {
51
+ if (!isRecord(manifest) || !Array.isArray(manifest.assets)) return null
52
+
53
+ return (
54
+ manifest.assets.find(
55
+ asset =>
56
+ isRecord(asset) &&
57
+ asset.platform === platform &&
58
+ asset.arch === arch &&
59
+ asset.kind === 'installer' &&
60
+ typeof asset.githubUrl === 'string' &&
61
+ (typeof asset.r2Url === 'string' || typeof asset.r2Url === 'undefined')
62
+ ) || null
63
+ )
64
+ }
65
+
66
+ export function formatBytes(size) {
67
+ if (!Number.isFinite(size) || size <= 0) return ''
68
+
69
+ const mb = size / 1024 / 1024
70
+ return `${mb.toFixed(mb >= 100 ? 0 : 1)} MB`
71
+ }
72
+
73
+ export function getAvailableUpdate(manifest, options = {}) {
74
+ const currentVersion = options.currentVersion
75
+ const platform = options.platform
76
+ const arch = options.arch
77
+
78
+ if (
79
+ !isRecord(manifest) ||
80
+ typeof manifest.version !== 'string' ||
81
+ !isNewerVersion(manifest.version, currentVersion)
82
+ ) {
83
+ return null
84
+ }
85
+
86
+ const asset = findUpdateAsset(manifest, platform, arch)
87
+ if (!asset) return null
88
+
89
+ const downloadUrl = asset.r2Url || asset.githubUrl
90
+ if (!downloadUrl) return null
91
+
92
+ return {
93
+ version: manifest.version,
94
+ asset,
95
+ downloadUrl,
96
+ }
97
+ }
@@ -0,0 +1,147 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ findUpdateAsset,
6
+ formatBytes,
7
+ getAvailableUpdate,
8
+ getCurrentArch,
9
+ getCurrentPlatform,
10
+ getReleaseManifestUrl,
11
+ isNewerVersion,
12
+ } from './updateChecker.js'
13
+
14
+ const manifest = {
15
+ version: '0.1.3',
16
+ publishedAt: '2026-06-01T00:00:00.000Z',
17
+ assets: [
18
+ {
19
+ platform: 'windows',
20
+ arch: 'x64',
21
+ kind: 'installer',
22
+ filename: 'MostBox-0.1.3-win-x64-setup.exe',
23
+ size: 112197632,
24
+ sha256: 'hash',
25
+ r2Url:
26
+ 'https://download.most.box/releases/v0.1.3/MostBox-0.1.3-win-x64-setup.exe',
27
+ githubUrl:
28
+ 'https://github.com/most-people/most/releases/download/v0.1.3/MostBox-0.1.3-win-x64-setup.exe',
29
+ },
30
+ {
31
+ platform: 'windows',
32
+ arch: 'arm64',
33
+ kind: 'installer',
34
+ filename: 'MostBox-0.1.3-win-arm64-setup.exe',
35
+ githubUrl:
36
+ 'https://github.com/most-people/most/releases/download/v0.1.3/MostBox-0.1.3-win-arm64-setup.exe',
37
+ },
38
+ {
39
+ platform: 'macos',
40
+ arch: 'arm64',
41
+ kind: 'installer',
42
+ filename: 'MostBox-0.1.3-mac-arm64.dmg',
43
+ githubUrl:
44
+ 'https://github.com/most-people/most/releases/download/v0.1.3/MostBox-0.1.3-mac-arm64.dmg',
45
+ },
46
+ ],
47
+ }
48
+
49
+ describe('desktop update checker', () => {
50
+ it('uses the public R2 manifest by default and allows env override', () => {
51
+ assert.equal(
52
+ getReleaseManifestUrl({}),
53
+ 'https://download.most.box/releases/latest.json'
54
+ )
55
+ assert.equal(
56
+ getReleaseManifestUrl({
57
+ MOSTBOX_RELEASE_MANIFEST_URL: 'http://localhost:9999/latest.json',
58
+ }),
59
+ 'http://localhost:9999/latest.json'
60
+ )
61
+ })
62
+
63
+ it('maps process platform and arch to release manifest values', () => {
64
+ assert.equal(getCurrentPlatform('win32'), 'windows')
65
+ assert.equal(getCurrentPlatform('darwin'), 'macos')
66
+ assert.equal(getCurrentPlatform('linux'), 'linux')
67
+ assert.equal(getCurrentPlatform('freebsd'), null)
68
+
69
+ assert.equal(getCurrentArch('x64'), 'x64')
70
+ assert.equal(getCurrentArch('arm64'), 'arm64')
71
+ assert.equal(getCurrentArch('ia32'), null)
72
+ })
73
+
74
+ it('compares stable and prefixed semver strings safely', () => {
75
+ assert.equal(isNewerVersion('0.1.3', '0.1.2'), true)
76
+ assert.equal(isNewerVersion('v0.2.0', '0.1.9'), true)
77
+ assert.equal(isNewerVersion('0.1.2', '0.1.2'), false)
78
+ assert.equal(isNewerVersion('0.1.1', '0.1.2'), false)
79
+ assert.equal(isNewerVersion('0.1.3-beta.1', '0.1.2'), true)
80
+ assert.equal(isNewerVersion('not-a-version', '0.1.2'), false)
81
+ assert.equal(isNewerVersion('0.1.3', 'not-a-version'), false)
82
+ })
83
+
84
+ it('matches the current platform installer asset', () => {
85
+ assert.equal(
86
+ findUpdateAsset(manifest, 'windows', 'x64')?.filename,
87
+ 'MostBox-0.1.3-win-x64-setup.exe'
88
+ )
89
+ assert.equal(
90
+ findUpdateAsset(manifest, 'windows', 'arm64')?.filename,
91
+ 'MostBox-0.1.3-win-arm64-setup.exe'
92
+ )
93
+ assert.equal(findUpdateAsset(manifest, 'linux', 'x64'), null)
94
+ })
95
+
96
+ it('returns an available update only for newer compatible manifests', () => {
97
+ const update = getAvailableUpdate(manifest, {
98
+ currentVersion: '0.1.2',
99
+ platform: 'windows',
100
+ arch: 'x64',
101
+ })
102
+
103
+ assert.equal(update?.version, '0.1.3')
104
+ assert.equal(update?.downloadUrl, manifest.assets[0].r2Url)
105
+
106
+ assert.equal(
107
+ getAvailableUpdate(manifest, {
108
+ currentVersion: '0.1.3',
109
+ platform: 'windows',
110
+ arch: 'x64',
111
+ }),
112
+ null
113
+ )
114
+ assert.equal(
115
+ getAvailableUpdate(manifest, {
116
+ currentVersion: '0.1.2',
117
+ platform: 'linux',
118
+ arch: 'x64',
119
+ }),
120
+ null
121
+ )
122
+ assert.equal(
123
+ getAvailableUpdate({ version: '0.1.3', assets: [] }, {
124
+ currentVersion: '0.1.2',
125
+ platform: 'windows',
126
+ arch: 'x64',
127
+ }),
128
+ null
129
+ )
130
+ })
131
+
132
+ it('falls back to GitHub when an R2 URL is absent', () => {
133
+ const update = getAvailableUpdate(manifest, {
134
+ currentVersion: '0.1.2',
135
+ platform: 'windows',
136
+ arch: 'arm64',
137
+ })
138
+
139
+ assert.equal(update?.downloadUrl, manifest.assets[1].githubUrl)
140
+ })
141
+
142
+ it('formats installer sizes for update prompts', () => {
143
+ assert.equal(formatBytes(112197632), '107 MB')
144
+ assert.equal(formatBytes(2.5 * 1024 * 1024), '2.5 MB')
145
+ assert.equal(formatBytes(0), '')
146
+ })
147
+ })