most-box 0.2.5 → 0.2.7

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 (142) hide show
  1. package/README.md +6 -0
  2. package/electron/main.js +58 -4
  3. package/electron/updateChecker.js +10 -27
  4. package/electron/updateChecker.test.js +67 -0
  5. package/out/admin/index.html +0 -0
  6. package/out/app/index.html +0 -0
  7. package/out/assets/FilePreviewOverlay-CW8lKJCw.js +1 -0
  8. package/out/assets/LanguageToggle-Cfh33R2h.js +1 -0
  9. package/out/assets/{MarketingHeader-B3vYocUz.js → MarketingHeader-BKBngzf5.js} +1 -1
  10. package/out/assets/MarketingLayout-C3xrXmfU.js +1 -0
  11. package/out/assets/MarketingThemeToggle-Cl7PhFgv.js +1 -0
  12. package/out/assets/{MilkdownEditor-CRA9oxMp.js → MilkdownEditor-B5SKAyjg.js} +1 -1
  13. package/out/assets/OpenSidebarButton-Bdx86oG8.js +1 -0
  14. package/out/assets/SidebarAccount-c8jirEMF.js +1 -0
  15. package/out/assets/SidebarHomeLink-LzYXrJRr.js +1 -0
  16. package/out/assets/arrow-right-BC_GWahx.js +1 -0
  17. package/out/assets/circle-alert-Dkgyw_GE.js +1 -0
  18. package/out/assets/copy-KIwotKoP.js +1 -0
  19. package/out/assets/download-DPA-3LpR.js +1 -0
  20. package/out/assets/{downloadValidation-eInMiVSX.js → downloadValidation-CH8BeY7_.js} +1 -1
  21. package/out/assets/external-link-BmR2rZ80.js +1 -0
  22. package/out/assets/file-text-BJQfDW9r.js +1 -0
  23. package/out/assets/folder-Gb80aQnO.js +1 -0
  24. package/out/assets/hard-drive-P2dGA7FX.js +1 -0
  25. package/out/assets/i18n-Cqyi6NZb.js +1 -0
  26. package/out/assets/index-CSBg7dZN.css +1 -0
  27. package/out/assets/index-CiHRnvMj.js +31 -0
  28. package/out/assets/index.lazy-B7u3Ztoe.js +1 -0
  29. package/out/assets/index.lazy-BdOfbfjo.js +2 -0
  30. package/out/assets/index.lazy-Bh4jDrUl.js +1 -0
  31. package/out/assets/index.lazy-Bo6McUf1.js +1 -0
  32. package/out/assets/index.lazy-BxUje7fk.js +1 -0
  33. package/out/assets/index.lazy-CIqPn0cy.js +1 -0
  34. package/out/assets/index.lazy-CfuoMz6P.js +1 -0
  35. package/out/assets/index.lazy-DAKkwnV2.js +1 -0
  36. package/out/assets/index.lazy-DRaxm9OS.js +1 -0
  37. package/out/assets/index.lazy-DkrwcEBu.js +2 -0
  38. package/out/assets/index.lazy-K6IB-5kb.js +3 -0
  39. package/out/assets/index.lazy-Uu9DCtrI.js +1 -0
  40. package/out/assets/index.lazy-mXESMo49.js +1 -0
  41. package/out/assets/key-round-DvpIlAJY.js +1 -0
  42. package/out/assets/loader-8eVY8_SD.js +1 -0
  43. package/out/assets/lock-Cvtl60UJ.js +1 -0
  44. package/out/assets/message-square-JK7ABH3O.js +1 -0
  45. package/out/assets/mp-CMvgV1-c.js +1 -0
  46. package/out/assets/music-YRSH1Jq6.js +1 -0
  47. package/out/assets/notebook-pen-BjGx0coX.js +1 -0
  48. package/out/assets/play-Ds19moWN.js +1 -0
  49. package/out/assets/plus-CvGkRhJV.js +1 -0
  50. package/out/assets/save-BcI68ZgS.js +1 -0
  51. package/out/assets/search-BIa4RSPA.js +1 -0
  52. package/out/assets/send-_ups0_3f.js +1 -0
  53. package/out/assets/shield-check-Dzw5rdPe.js +1 -0
  54. package/out/assets/smartphone-DSQ5L_sr.js +1 -0
  55. package/out/assets/trash-2-BIrBn6Ce.js +1 -0
  56. package/out/assets/triangle-alert-CgWIEl4u.js +1 -0
  57. package/out/assets/ui-u9yJHkb-.js +4 -0
  58. package/out/assets/upload-1jmNeGjH.js +1 -0
  59. package/out/assets/{useChannelMessages-D_306Ymv.js → useChannelMessages-DNxfCew-.js} +1 -1
  60. package/out/assets/useGameRoom-Du07VEgb.js +1 -0
  61. package/out/assets/userProfile-CL1Ev2GK.js +1 -0
  62. package/out/assets/wallet-BvM_X5nW.js +1 -0
  63. package/out/assets/wifi-66t1dsgC.js +1 -0
  64. package/out/chat/index.html +0 -0
  65. package/out/chat/join/index.html +0 -0
  66. package/out/download/index.html +2 -2
  67. package/out/game/gandengyan/index.html +0 -0
  68. package/out/game/index.html +0 -0
  69. package/out/game/zhajinhua/index.html +0 -0
  70. package/out/index.html +2 -2
  71. package/out/note/index.html +0 -0
  72. package/out/ping/index.html +2 -2
  73. package/out/profile/index.html +0 -0
  74. package/out/web3/index.html +0 -0
  75. package/package.json +5 -1
  76. package/server/src/core/channelIdentity.js +3 -5
  77. package/server/src/core/gameRoom.js +18 -43
  78. package/server/src/core/ownerMetadata.js +3 -4
  79. package/server/src/core/releaseManifest.js +108 -0
  80. package/server/src/core/shared.js +50 -0
  81. package/server/src/core/zhajinhua.js +12 -33
  82. package/server/src/games/gandengyan.js +7 -14
  83. package/server/src/http/nodeStatus.js +0 -6
  84. package/server/src/http/routes/channelRoutes.js +1 -13
  85. package/server/src/http/routes/nodeRoutes.js +1 -1
  86. package/server/src/index.js +44 -14
  87. package/server/src/utils/api.js +47 -0
  88. package/server/src/utils/auth.js +2 -5
  89. package/server/src/utils/dateTime.js +1 -3
  90. package/out/assets/AppShell-oF8wPXde.js +0 -1
  91. package/out/assets/FilePreviewOverlay-DTBZt89J.js +0 -1
  92. package/out/assets/LanguageToggle-HAJ3D6Ne.js +0 -1
  93. package/out/assets/MarketingLayout-CapVVXTP.js +0 -1
  94. package/out/assets/MarketingThemeToggle-B47H68ro.js +0 -1
  95. package/out/assets/OpenSidebarButton-D3JRO-Hh.js +0 -1
  96. package/out/assets/SidebarAccount-8_hQAYXx.js +0 -1
  97. package/out/assets/arrow-right-mNO53HQi.js +0 -1
  98. package/out/assets/circle-alert-CofRTRHr.js +0 -1
  99. package/out/assets/cloud-DAGFCFIp.js +0 -1
  100. package/out/assets/copy-5aEv78Eq.js +0 -1
  101. package/out/assets/download-Bhqn6hL1.js +0 -1
  102. package/out/assets/external-link-CNR21yFA.js +0 -1
  103. package/out/assets/file-text-CFfvvCnM.js +0 -1
  104. package/out/assets/folder-BGJCjyba.js +0 -1
  105. package/out/assets/hard-drive-4vHpA5kz.js +0 -1
  106. package/out/assets/index--p9MKNEl.js +0 -31
  107. package/out/assets/index-DEu4udaa.css +0 -1
  108. package/out/assets/index.lazy-2K3f5PZe.js +0 -1
  109. package/out/assets/index.lazy-B-2etVwK.js +0 -1
  110. package/out/assets/index.lazy-B4WTQF5E.js +0 -1
  111. package/out/assets/index.lazy-BFfsQrRe.js +0 -1
  112. package/out/assets/index.lazy-C5udBnrO.js +0 -1
  113. package/out/assets/index.lazy-C84HrZpm.js +0 -1
  114. package/out/assets/index.lazy-CBC2N5p_.js +0 -3
  115. package/out/assets/index.lazy-CzANViGX.js +0 -1
  116. package/out/assets/index.lazy-D3cw7yyA.js +0 -2
  117. package/out/assets/index.lazy-DVVZzbCN.js +0 -1
  118. package/out/assets/index.lazy-DxZgywoV.js +0 -1
  119. package/out/assets/index.lazy-RBLN4SE9.js +0 -1
  120. package/out/assets/index.lazy-s4E2z9AY.js +0 -2
  121. package/out/assets/key-round-BpSdpOHi.js +0 -1
  122. package/out/assets/loader-ByMmV2_4.js +0 -1
  123. package/out/assets/lock-C0pdaery.js +0 -1
  124. package/out/assets/message-square-Eu86zZup.js +0 -1
  125. package/out/assets/mp-CJWipckT.js +0 -1
  126. package/out/assets/music-Btrhub6z.js +0 -1
  127. package/out/assets/notebook-pen-CfdFgaDE.js +0 -1
  128. package/out/assets/play-D5r9Yvbq.js +0 -1
  129. package/out/assets/plus-Mlh7r_bm.js +0 -1
  130. package/out/assets/refresh-cw--JB8k6mH.js +0 -1
  131. package/out/assets/save-DN4eprSt.js +0 -1
  132. package/out/assets/search-CbzMyENy.js +0 -1
  133. package/out/assets/send-D8p6f8OD.js +0 -1
  134. package/out/assets/shield-check-c0labdRH.js +0 -1
  135. package/out/assets/trash-2-zdhWec_U.js +0 -1
  136. package/out/assets/triangle-alert-CO_5uXy2.js +0 -1
  137. package/out/assets/upload-BD23c5Pz.js +0 -1
  138. package/out/assets/useGameRoom-CURx56m6.js +0 -1
  139. package/out/assets/userProfile-oeBvU6ch.js +0 -1
  140. package/out/assets/userStore-q0BBZsIV.js +0 -4
  141. package/out/assets/wallet-DmZMdK9A.js +0 -1
  142. package/out/assets/wifi-CVqaPDcv.js +0 -1
@@ -1,3 +1,15 @@
1
+ import {
2
+ createEventId,
3
+ normalizeAddress,
4
+ normalizeAvatar,
5
+ normalizeDisplayName,
6
+ normalizeRoomCode,
7
+ randomInt,
8
+ shortAddress,
9
+ } from './shared.js'
10
+
11
+ export { normalizeAddress } from './shared.js'
12
+
1
13
  export const GAME_CHANNEL_TYPE = 'game'
2
14
  export const GAME_EVENT_KIND = 'mostbox.game.event'
3
15
  export const GAME_EVENT_VERSION = 1
@@ -12,11 +24,7 @@ export function normalizeGameId(input) {
12
24
  }
13
25
 
14
26
  export function normalizeGameRoomCode(input) {
15
- return String(input || '')
16
- .trim()
17
- .toUpperCase()
18
- .replace(/[^A-Z0-9]/g, '')
19
- .slice(0, 8)
27
+ return normalizeRoomCode(input)
20
28
  }
21
29
 
22
30
  export function isValidGameRoomCode(input) {
@@ -166,10 +174,11 @@ export function createPlayerFromPayload(input, messageOrAuthor) {
166
174
  const author = message.author
167
175
  const address = normalizeAddress(input.address || author)
168
176
  if (!address || normalizeAddress(author) !== address) return null
169
- const name =
170
- String(message.authorName || input.name || '').trim().slice(0, 50) ||
177
+ const name = normalizeDisplayName(
178
+ message.authorName || input.name,
171
179
  shortAddress(address)
172
- const avatar = String(message.avatar || input.avatar || '').trim()
180
+ )
181
+ const avatar = normalizeAvatar(message.avatar || input.avatar)
173
182
  const publicKey = String(input.publicKey || '').trim()
174
183
  const player = {
175
184
  address,
@@ -177,7 +186,7 @@ export function createPlayerFromPayload(input, messageOrAuthor) {
177
186
  publicKey,
178
187
  joinedAt: Number(input.joinedAt || Date.now()),
179
188
  }
180
- if (avatar) player.avatar = avatar.slice(0, 4096)
189
+ if (avatar) player.avatar = avatar
181
190
  return player
182
191
  }
183
192
 
@@ -193,11 +202,6 @@ export function normalizeGamePlayer(input) {
193
202
  }
194
203
  }
195
204
 
196
- export function normalizeAddress(value) {
197
- const address = String(value || '').trim()
198
- return /^0x[a-fA-F0-9]{40}$/.test(address) ? address.toLowerCase() : ''
199
- }
200
-
201
205
  function upsertPlayer(players, playerMap, player) {
202
206
  const existing = playerMap.get(player.address)
203
207
  if (existing) {
@@ -213,32 +217,3 @@ function sortMessages(messages) {
213
217
  (a, b) => Number(a.timestamp || 0) - Number(b.timestamp || 0)
214
218
  )
215
219
  }
216
-
217
- function createEventId(prefix) {
218
- return `${prefix}_${Date.now()}_${randomHex(4)}`
219
- }
220
-
221
- function randomInt(max) {
222
- if (globalThis.crypto?.getRandomValues) {
223
- const value = new Uint32Array(1)
224
- globalThis.crypto.getRandomValues(value)
225
- return value[0] % max
226
- }
227
- return Math.floor(Math.random() * max)
228
- }
229
-
230
- function randomHex(byteLength) {
231
- const bytes = new Uint8Array(byteLength)
232
- if (globalThis.crypto?.getRandomValues) {
233
- globalThis.crypto.getRandomValues(bytes)
234
- } else {
235
- for (let i = 0; i < bytes.length; i++) {
236
- bytes[i] = Math.floor(Math.random() * 256)
237
- }
238
- }
239
- return [...bytes].map(byte => byte.toString(16).padStart(2, '0')).join('')
240
- }
241
-
242
- function shortAddress(address) {
243
- return address ? `${address.slice(0, 6)}...${address.slice(-4)}` : ''
244
- }
@@ -1,9 +1,8 @@
1
+ import { normalizeAddress } from './shared.js'
2
+
1
3
  export const DEFAULT_OWNER_BUCKET = '__local__'
2
4
 
3
- export function normalizeOwnerAddress(address) {
4
- const value = String(address || '').trim()
5
- return /^0x[a-fA-F0-9]{40}$/.test(value) ? value.toLowerCase() : ''
6
- }
5
+ export const normalizeOwnerAddress = normalizeAddress
7
6
 
8
7
  export function getOwnerBucketKey(address) {
9
8
  return normalizeOwnerAddress(address) || DEFAULT_OWNER_BUCKET
@@ -0,0 +1,108 @@
1
+ export const RELEASE_PLATFORMS = ['windows', 'macos', 'linux']
2
+ export const RELEASE_ARCHES = ['x64', 'arm64']
3
+ export const RELEASE_ASSET_KINDS = ['installer', 'updater']
4
+
5
+ export const RELEASE_TARGETS = Object.freeze([
6
+ { platform: 'windows', arch: 'x64' },
7
+ { platform: 'windows', arch: 'arm64' },
8
+ { platform: 'macos', arch: 'x64' },
9
+ { platform: 'macos', arch: 'arm64' },
10
+ { platform: 'linux', arch: 'x64' },
11
+ { platform: 'linux', arch: 'arm64' },
12
+ ])
13
+
14
+ const RELEASE_PLATFORM_SET = new Set(RELEASE_PLATFORMS)
15
+ const RELEASE_ARCH_SET = new Set(RELEASE_ARCHES)
16
+ const RELEASE_ASSET_KIND_SET = new Set(RELEASE_ASSET_KINDS)
17
+
18
+ function isRecord(value) {
19
+ return typeof value === 'object' && value !== null
20
+ }
21
+
22
+ function isNonEmptyString(value) {
23
+ return typeof value === 'string' && value.trim().length > 0
24
+ }
25
+
26
+ function isPositiveFiniteNumber(value) {
27
+ return Number.isFinite(value) && value > 0
28
+ }
29
+
30
+ function isOptionalNonEmptyString(value) {
31
+ return typeof value === 'undefined' || isNonEmptyString(value)
32
+ }
33
+
34
+ export function isReleaseAsset(value) {
35
+ if (!isRecord(value)) return false
36
+
37
+ return (
38
+ RELEASE_PLATFORM_SET.has(value.platform) &&
39
+ RELEASE_ARCH_SET.has(value.arch) &&
40
+ RELEASE_ASSET_KIND_SET.has(value.kind) &&
41
+ isNonEmptyString(value.filename) &&
42
+ isPositiveFiniteNumber(value.size) &&
43
+ isNonEmptyString(value.cid) &&
44
+ isNonEmptyString(value.githubUrl) &&
45
+ isOptionalNonEmptyString(value.r2Url)
46
+ )
47
+ }
48
+
49
+ export function isReleaseManifest(value) {
50
+ if (!isRecord(value)) return false
51
+ if (!isNonEmptyString(value.version)) return false
52
+ if (!isNonEmptyString(value.publishedAt)) return false
53
+ if (!Array.isArray(value.assets)) return false
54
+
55
+ return value.assets.every(isReleaseAsset)
56
+ }
57
+
58
+ export function getReleaseAssetKey(asset) {
59
+ return `${asset.platform}:${asset.arch}`
60
+ }
61
+
62
+ export function getInstallerReleaseAssets(manifest) {
63
+ if (!isReleaseManifest(manifest)) return []
64
+ return manifest.assets.filter(asset => asset.kind === 'installer')
65
+ }
66
+
67
+ export function hasR2ReleaseAssets(assets) {
68
+ if (!Array.isArray(assets)) return false
69
+ return assets.some(asset => isRecord(asset) && isNonEmptyString(asset.r2Url))
70
+ }
71
+
72
+ export function resolveReleaseAssetDownload(asset, preferredSource = 'r2') {
73
+ if (
74
+ preferredSource === 'r2' &&
75
+ isRecord(asset) &&
76
+ isNonEmptyString(asset.r2Url)
77
+ ) {
78
+ return {
79
+ source: 'r2',
80
+ url: asset.r2Url,
81
+ }
82
+ }
83
+
84
+ return {
85
+ source: 'github',
86
+ url:
87
+ isRecord(asset) && isNonEmptyString(asset.githubUrl)
88
+ ? asset.githubUrl
89
+ : '',
90
+ }
91
+ }
92
+
93
+ export function findCompatibleUpdateAsset(manifest, platform, arch) {
94
+ if (!isReleaseManifest(manifest)) return null
95
+
96
+ const compatibleAssets = manifest.assets.filter(
97
+ asset =>
98
+ asset.platform === platform &&
99
+ asset.arch === arch &&
100
+ (asset.kind === 'updater' || asset.kind === 'installer')
101
+ )
102
+
103
+ return (
104
+ compatibleAssets.find(asset => asset.kind === 'updater') ||
105
+ compatibleAssets.find(asset => asset.kind === 'installer') ||
106
+ null
107
+ )
108
+ }
@@ -0,0 +1,50 @@
1
+ export function normalizeAddress(value) {
2
+ const address = String(value || '').trim()
3
+ return /^0x[a-fA-F0-9]{40}$/.test(address) ? address.toLowerCase() : ''
4
+ }
5
+
6
+ export function shortAddress(address) {
7
+ return address ? `${address.slice(0, 6)}...${address.slice(-4)}` : ''
8
+ }
9
+
10
+ export function normalizeAvatar(value) {
11
+ return String(value || '').trim().slice(0, 4096)
12
+ }
13
+
14
+ export function normalizeDisplayName(value, fallback = '', maxLength = 50) {
15
+ const displayName = String(value || '').trim()
16
+ return (displayName || String(fallback || '')).slice(0, maxLength)
17
+ }
18
+
19
+ export function normalizeRoomCode(value) {
20
+ return String(value || '')
21
+ .trim()
22
+ .toUpperCase()
23
+ .replace(/[^A-Z0-9]/g, '')
24
+ .slice(0, 8)
25
+ }
26
+
27
+ export function createEventId(prefix) {
28
+ return `${prefix}_${Date.now()}_${randomHex(4)}`
29
+ }
30
+
31
+ export function randomInt(max) {
32
+ if (globalThis.crypto?.getRandomValues) {
33
+ const value = new Uint32Array(1)
34
+ globalThis.crypto.getRandomValues(value)
35
+ return value[0] % max
36
+ }
37
+ return Math.floor(Math.random() * max)
38
+ }
39
+
40
+ export function randomHex(byteLength) {
41
+ const bytes = new Uint8Array(byteLength)
42
+ if (globalThis.crypto?.getRandomValues) {
43
+ globalThis.crypto.getRandomValues(bytes)
44
+ } else {
45
+ for (let i = 0; i < bytes.length; i++) {
46
+ bytes[i] = Math.floor(Math.random() * 256)
47
+ }
48
+ }
49
+ return [...bytes].map(byte => byte.toString(16).padStart(2, '0')).join('')
50
+ }
@@ -1,3 +1,11 @@
1
+ import {
2
+ createEventId as createSharedEventId,
3
+ normalizeAddress,
4
+ normalizeAvatar,
5
+ normalizeRoomCode as normalizeSharedRoomCode,
6
+ shortAddress,
7
+ } from './shared.js'
8
+
1
9
  export const ZHJ_INITIAL_CHIPS = 1000
2
10
  export const ZHJ_ANTE = 10
3
11
  export const ZHJ_MIN_PLAYERS = 2
@@ -28,15 +36,11 @@ const CATEGORY_LABEL = {
28
36
  }
29
37
 
30
38
  export function normalizeRoomCode(input) {
31
- return String(input || '')
32
- .trim()
33
- .toUpperCase()
34
- .replace(/[^A-Z0-9]/g, '')
35
- .slice(0, 8)
39
+ return normalizeSharedRoomCode(input)
36
40
  }
37
41
 
38
42
  export function createEventId(prefix = 'zhajinhua') {
39
- return `${prefix}_${Date.now()}_${randomHex(4)}`
43
+ return createSharedEventId(prefix)
40
44
  }
41
45
 
42
46
  export function createRoundId() {
@@ -196,7 +200,7 @@ export function startRound({ roomCode, players, hostAddress, previousSeq = 0, pr
196
200
  return {
197
201
  address: player.address,
198
202
  name: player.name,
199
- avatar: cleanAvatar(player.avatar),
203
+ avatar: normalizeAvatar(player.avatar),
200
204
  publicKey: player.publicKey,
201
205
  chips: Number(player.chips) - ZHJ_ANTE,
202
206
  status: 'active',
@@ -598,7 +602,7 @@ function normalizeRoundPlayer(input) {
598
602
  return {
599
603
  address,
600
604
  name: String(input.name || shortAddress(address)).slice(0, 50),
601
- avatar: cleanAvatar(input.avatar),
605
+ avatar: normalizeAvatar(input.avatar),
602
606
  publicKey: String(input.publicKey || ''),
603
607
  chips: Math.max(0, Number(input.chips) || 0),
604
608
  status: input.status === 'folded' ? 'folded' : 'active',
@@ -607,31 +611,6 @@ function normalizeRoundPlayer(input) {
607
611
  }
608
612
  }
609
613
 
610
- function normalizeAddress(value) {
611
- const address = String(value || '').trim()
612
- return /^0x[a-fA-F0-9]{40}$/.test(address) ? address.toLowerCase() : ''
613
- }
614
-
615
- function shortAddress(address) {
616
- return address ? `${address.slice(0, 6)}...${address.slice(-4)}` : ''
617
- }
618
-
619
- function cleanAvatar(value) {
620
- return String(value || '').trim().slice(0, 4096)
621
- }
622
-
623
614
  function clone(value) {
624
615
  return JSON.parse(JSON.stringify(value))
625
616
  }
626
-
627
- function randomHex(byteLength) {
628
- const bytes = new Uint8Array(byteLength)
629
- if (globalThis.crypto?.getRandomValues) {
630
- globalThis.crypto.getRandomValues(bytes)
631
- } else {
632
- for (let i = 0; i < bytes.length; i++) {
633
- bytes[i] = Math.floor(Math.random() * 256)
634
- }
635
- }
636
- return [...bytes].map(byte => byte.toString(16).padStart(2, '0')).join('')
637
- }
@@ -1,3 +1,5 @@
1
+ import { normalizeAddress, normalizeAvatar } from '../core/shared.js'
2
+
1
3
  const SUITS = ['S', 'H', 'C', 'D']
2
4
  const RANKS = ['3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A', '2']
3
5
  const STRAIGHT_RANKS = RANKS.filter(rank => rank !== '2')
@@ -30,7 +32,7 @@ export function createGanDengYanRoom({
30
32
  players: roomPlayers.slice(0, 6).map((player, seat) => ({
31
33
  address: player.address,
32
34
  name: cleanName(player.name),
33
- avatar: cleanAvatar(player.avatar),
35
+ avatar: normalizeAvatar(player.avatar),
34
36
  seat,
35
37
  hand: [],
36
38
  handCount: 0,
@@ -64,7 +66,7 @@ export function syncGanDengYanLobby(room, players = []) {
64
66
  .map((player, seat) => ({
65
67
  address: player.address,
66
68
  name: cleanName(player.name),
67
- avatar: cleanAvatar(player.avatar),
69
+ avatar: normalizeAvatar(player.avatar),
68
70
  seat,
69
71
  hand: [],
70
72
  handCount: 0,
@@ -260,7 +262,7 @@ export function publicGanDengYanRoom(room) {
260
262
  players: orderedPlayers(room).map(player => ({
261
263
  address: player.address,
262
264
  name: player.name,
263
- avatar: cleanAvatar(player.avatar),
265
+ avatar: normalizeAvatar(player.avatar),
264
266
  seat: player.seat,
265
267
  handCount: player.hand?.length ?? player.handCount ?? 0,
266
268
  score: Number(player.score ?? INITIAL_SCORE),
@@ -435,7 +437,7 @@ function normalizePlayers(players) {
435
437
  .map(player => ({
436
438
  address: normalizeAddress(player.address),
437
439
  name: cleanName(player.name),
438
- avatar: cleanAvatar(player.avatar),
440
+ avatar: normalizeAvatar(player.avatar),
439
441
  publicKey: String(player.publicKey || ''),
440
442
  }))
441
443
  .filter(player => {
@@ -451,7 +453,7 @@ function normalizeRoundPlayer(input) {
451
453
  return {
452
454
  address,
453
455
  name: cleanName(input.name),
454
- avatar: cleanAvatar(input.avatar),
456
+ avatar: normalizeAvatar(input.avatar),
455
457
  seat: Number(input.seat || 0),
456
458
  hand: Array.isArray(input.hand) ? input.hand.map(normalizeCard).filter(Boolean) : [],
457
459
  handCount: Number(input.handCount || 0),
@@ -677,15 +679,6 @@ function rollDice(random) {
677
679
  return Math.floor(random() * 6) + 1
678
680
  }
679
681
 
680
- function normalizeAddress(value) {
681
- const address = String(value || '').trim()
682
- return /^0x[a-fA-F0-9]{40}$/.test(address) ? address.toLowerCase() : ''
683
- }
684
-
685
682
  function cleanName(name) {
686
683
  return String(name || '玩家').trim().slice(0, 16) || '玩家'
687
684
  }
688
-
689
- function cleanAvatar(avatar) {
690
- return String(avatar || '').trim().slice(0, 4096)
691
- }
@@ -260,12 +260,6 @@ export function buildOpenApiSpec(appPort) {
260
260
  responses: { 200: { description: 'Created channel message' } },
261
261
  },
262
262
  },
263
- '/api/channels/{name}/members': {
264
- get: {
265
- summary: 'List P2P channel members',
266
- responses: { 200: { description: 'Channel members' } },
267
- },
268
- },
269
263
  '/api/channels/{name}/peers': {
270
264
  get: {
271
265
  summary: 'List currently connected channel peers',
@@ -1,4 +1,4 @@
1
- import { normalizeAddress } from '../../utils/auth.js'
1
+ import { normalizeAddress } from '../../core/shared.js'
2
2
  import { badRequestOrAppError } from '../errors.js'
3
3
 
4
4
  export function registerChannelRoutes(app, { engine }) {
@@ -72,18 +72,6 @@ export function registerChannelRoutes(app, { engine }) {
72
72
  }
73
73
  })
74
74
 
75
- app.get('/api/channels/:name/members', c => {
76
- try {
77
- return c.json(
78
- engine.getChannelMembers(c.req.param('name'), {
79
- ownerAddress: c.get('userAddress'),
80
- })
81
- )
82
- } catch (err) {
83
- return badRequestOrAppError(c, err)
84
- }
85
- })
86
-
87
75
  app.post('/api/channels/:name/messages', async c => {
88
76
  const name = c.req.param('name')
89
77
  const body = await c.req.json()
@@ -1,4 +1,4 @@
1
- import { normalizeAddress } from '../../utils/auth.js'
1
+ import { normalizeAddress } from '../../core/shared.js'
2
2
  import { evaluateStorageLimits } from '../../node/config.js'
3
3
  import {
4
4
  isLoopbackRemoteAddress,
@@ -93,6 +93,7 @@ import {
93
93
  } from './config.js'
94
94
 
95
95
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
96
+ const CHANNEL_WELCOME_MESSAGE = '我加入了群聊'
96
97
 
97
98
  function createMemoryDuplexPair() {
98
99
  let left
@@ -2462,6 +2463,7 @@ export class MostBoxEngine extends EventEmitter {
2462
2463
  channel => channel.channelKey === candidate.channelKey
2463
2464
  )
2464
2465
  if (existing) {
2466
+ const wasMember = this.#channelHasMember(existing, ownerAddress)
2465
2467
  const writerKeysChanged = await this.#mergeChannelWriterCoreKeys(
2466
2468
  existing,
2467
2469
  candidate.writerCoreKeys
@@ -2472,6 +2474,7 @@ export class MostBoxEngine extends EventEmitter {
2472
2474
  this.#saveChannelsMetadata()
2473
2475
  this.#broadcastChannelHello()
2474
2476
  }
2477
+ await this.#appendChannelWelcomeMessage(existing, options, wasMember)
2475
2478
  return this.#formatChannelForResponse(existing, ownerAddress)
2476
2479
  }
2477
2480
  const joined = await this.#joinChannelFromCandidate(
@@ -2528,6 +2531,7 @@ export class MostBoxEngine extends EventEmitter {
2528
2531
  const channelKey = buildChannelKey(channelId)
2529
2532
  const existing = this.#channels.find(c => c.channelKey === channelKey)
2530
2533
  if (existing) {
2534
+ const wasMember = this.#channelHasMember(existing, options.ownerAddress)
2531
2535
  const writerKeysChanged = await this.#mergeChannelWriterCoreKeys(
2532
2536
  existing,
2533
2537
  candidate.writerCoreKeys
@@ -2538,6 +2542,7 @@ export class MostBoxEngine extends EventEmitter {
2538
2542
  this.#saveChannelsMetadata()
2539
2543
  this.#broadcastChannelHello()
2540
2544
  }
2545
+ await this.#appendChannelWelcomeMessage(existing, options, wasMember)
2541
2546
  return this.#formatChannelForResponse(existing, options.ownerAddress)
2542
2547
  }
2543
2548
 
@@ -2730,14 +2735,6 @@ export class MostBoxEngine extends EventEmitter {
2730
2735
  .map(c => this.#formatChannelForResponse(c, ownerAddress))
2731
2736
  }
2732
2737
 
2733
- getChannelMembers(channelKeyInput, options = {}) {
2734
- this.#ensureInitialized()
2735
- this.#assertChannelMember(channelKeyInput, options.ownerAddress)
2736
- const channel = this.#resolveChannel(channelKeyInput, options.ownerAddress)
2737
-
2738
- return this.#getChannelMembers(channel)
2739
- }
2740
-
2741
2738
  /**
2742
2739
  * 获取频道消息
2743
2740
  * @param {string} channelKeyInput - 内部频道 key,或本地唯一短频道 ID
@@ -2843,15 +2840,22 @@ export class MostBoxEngine extends EventEmitter {
2843
2840
  this.#saveChannelsMetadata()
2844
2841
  }
2845
2842
 
2843
+ const normalizedAvatar = normalizeChannelAvatar(options.avatar)
2846
2844
  const message = {
2847
2845
  type: 'message',
2848
2846
  author,
2849
- authorName,
2847
+ authorName: normalizeChannelDisplayName(
2848
+ authorName,
2849
+ normalizeOwnerAddress(author)
2850
+ ),
2850
2851
  content: trimmed,
2851
2852
  timestamp: await this.#getNextChannelMessageTimestamp(
2852
2853
  channel.channelKey
2853
2854
  ),
2854
2855
  }
2856
+ if (normalizedAvatar) {
2857
+ message.avatar = normalizedAvatar
2858
+ }
2855
2859
  if (attachment) {
2856
2860
  message.attachment = attachment
2857
2861
  }
@@ -2977,6 +2981,7 @@ export class MostBoxEngine extends EventEmitter {
2977
2981
  await this.#joinChannelDiscoveryTopics(channelInfo)
2978
2982
  this.#cacheChannelCandidate(this.#channelToCandidate(channelInfo, true))
2979
2983
  this.#saveChannelsMetadata()
2984
+ await this.#appendChannelWelcomeMessage(channelInfo, options, false)
2980
2985
  this.#broadcastChannelHello()
2981
2986
  return channelInfo
2982
2987
  }
@@ -2994,10 +2999,12 @@ export class MostBoxEngine extends EventEmitter {
2994
2999
  channel => channel.channelKey === channelKey
2995
3000
  )
2996
3001
  if (existing) {
3002
+ const wasMember = this.#channelHasMember(existing, options.ownerAddress)
2997
3003
  if (this.#upsertChannelMember(existing, options)) {
2998
3004
  this.#saveChannelsMetadata()
2999
3005
  this.#broadcastChannelHello()
3000
3006
  }
3007
+ await this.#appendChannelWelcomeMessage(existing, options, wasMember)
3001
3008
  return this.#formatChannelForResponse(existing, options.ownerAddress)
3002
3009
  }
3003
3010
 
@@ -3328,6 +3335,32 @@ export class MostBoxEngine extends EventEmitter {
3328
3335
  return true
3329
3336
  }
3330
3337
 
3338
+ async #appendChannelWelcomeMessage(channel, options = {}, wasMember = false) {
3339
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
3340
+ if (
3341
+ wasMember ||
3342
+ !ownerAddress ||
3343
+ !channel ||
3344
+ TRANSIENT_CHANNEL_TYPES.has(channel.type)
3345
+ ) {
3346
+ return false
3347
+ }
3348
+
3349
+ await this.sendMessage(
3350
+ channel.channelKey,
3351
+ CHANNEL_WELCOME_MESSAGE,
3352
+ ownerAddress,
3353
+ normalizeChannelDisplayName(options.displayName, ownerAddress),
3354
+ {
3355
+ ownerAddress,
3356
+ ...(Object.prototype.hasOwnProperty.call(options, 'avatar')
3357
+ ? { avatar: options.avatar }
3358
+ : {}),
3359
+ }
3360
+ )
3361
+ return true
3362
+ }
3363
+
3331
3364
  #getChannelMembers(channel) {
3332
3365
  const members = Array.isArray(channel?.members) ? channel.members : []
3333
3366
  return members
@@ -3392,14 +3425,11 @@ export class MostBoxEngine extends EventEmitter {
3392
3425
  authorAddress
3393
3426
  )
3394
3427
  const avatar = normalizeChannelAvatar(member.avatar)
3395
- if (displayName && baseMessage?.authorName !== displayName) {
3428
+ if (displayName && !String(baseMessage?.authorName || '').trim()) {
3396
3429
  baseMessage = { ...baseMessage, authorName: displayName }
3397
3430
  }
3398
- if (avatar && baseMessage?.avatar !== avatar) {
3431
+ if (avatar && !normalizeChannelAvatar(baseMessage?.avatar)) {
3399
3432
  baseMessage = { ...baseMessage, avatar }
3400
- } else if (!avatar && baseMessage?.avatar) {
3401
- baseMessage = { ...baseMessage }
3402
- delete baseMessage.avatar
3403
3433
  }
3404
3434
  }
3405
3435
  const attachment = baseMessage?.attachment
@@ -164,6 +164,49 @@ function clearActiveRemoteNode() {
164
164
  )
165
165
  }
166
166
 
167
+ function getLocalNodeHistoryItem(activeBackendUrl = getBackendUrl()) {
168
+ const normalizedActiveUrl = normalizeBackendUrl(activeBackendUrl)
169
+ const fallbackLocalUrl = getDefaultBackendUrl()
170
+ const localUrl = isLocalBackendUrl(normalizedActiveUrl)
171
+ ? normalizedActiveUrl
172
+ : fallbackLocalUrl
173
+
174
+ if (!isLocalBackendUrl(localUrl)) return null
175
+
176
+ const url = normalizeBackendUrl(localUrl)
177
+ return {
178
+ url,
179
+ invite: '',
180
+ active: url === normalizedActiveUrl,
181
+ local: true,
182
+ updatedAt: Number.MAX_SAFE_INTEGER,
183
+ }
184
+ }
185
+
186
+ function getNodeHistory() {
187
+ if (typeof window === 'undefined') return []
188
+
189
+ const activeBackendUrl = normalizeBackendUrl(getBackendUrl())
190
+ const localNode = getLocalNodeHistoryItem(activeBackendUrl)
191
+ const remoteNodes = getRemoteNodes().map((node, index) => ({
192
+ ...node,
193
+ active: normalizeBackendUrl(node.url) === activeBackendUrl,
194
+ local: false,
195
+ order: index + 1,
196
+ }))
197
+ const nodes = localNode
198
+ ? [{ ...localNode, order: 0 }, ...remoteNodes]
199
+ : remoteNodes
200
+
201
+ return nodes
202
+ .sort((a, b) => {
203
+ if (a.active !== b.active) return a.active ? -1 : 1
204
+ if (a.local !== b.local) return a.local ? -1 : 1
205
+ return a.order - b.order
206
+ })
207
+ .map(({ order: _order, ...node }) => node)
208
+ }
209
+
167
210
  function normalizeBackendUrl(url) {
168
211
  return (url || '').trim().replace(/\/+$/, '')
169
212
  }
@@ -390,6 +433,10 @@ export function getRemoteNodesExport() {
390
433
  return getRemoteNodes()
391
434
  }
392
435
 
436
+ export function getNodeHistoryExport() {
437
+ return getNodeHistory()
438
+ }
439
+
393
440
  export function getBackendInviteExport() {
394
441
  return getBackendInvite()
395
442
  }
@@ -1,12 +1,9 @@
1
1
  import { verifyMessage } from 'ethers'
2
+ import { normalizeAddress } from '../core/shared.js'
2
3
  import { mostSignMessage } from './mostWallet.js'
3
4
 
4
5
  export const AUTH_MAX_AGE_MS = 5 * 60 * 1000
5
-
6
- export function normalizeAddress(address) {
7
- const value = String(address || '').trim()
8
- return /^0x[a-fA-F0-9]{40}$/.test(value) ? value.toLowerCase() : ''
9
- }
6
+ export { normalizeAddress }
10
7
 
11
8
  export function buildAuthMessage(timestamp, method, path) {
12
9
  return `${timestamp}:${String(method || 'GET').toUpperCase()}:${normalizeAuthPath(path)}`