most-box 0.2.0 → 0.2.1

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 (136) hide show
  1. package/README.md +3 -3
  2. package/out/admin/index.html +0 -0
  3. package/out/app/index.html +0 -0
  4. package/out/assets/{AppShell-CQhg6DJU.js → AppShell-OiOEqXPr.js} +1 -1
  5. package/out/assets/ChatUi-Cif5LRF3.js +1 -0
  6. package/out/assets/CopyButton-Dm7krgbq.js +1 -0
  7. package/out/assets/LanguageToggle-B4ZNuBCV.js +1 -0
  8. package/out/assets/LogoIcon-B2fFe0l1.js +1 -0
  9. package/out/assets/MarketingHeader-yIZuQP7m.js +1 -0
  10. package/out/assets/MarketingLayout-DVH0Nx7S.js +1 -0
  11. package/out/assets/{MarketingThemeToggle-DBaC9bjz.js → MarketingThemeToggle-qlwCZU1o.js} +1 -1
  12. package/out/assets/{MilkdownEditor-BqJzntYE.js → MilkdownEditor-_TGlDyA_.js} +1 -1
  13. package/out/assets/MoveModal-BVr4Q7-b.js +1 -0
  14. package/out/assets/Nav-5xeettNJ.js +1 -0
  15. package/out/assets/NoteSidebar-DpniUKmy.js +1 -0
  16. package/out/assets/OpenSidebarButton-BfgG2HIT.js +1 -0
  17. package/out/assets/PemBlock-CxwIepth.js +1 -0
  18. package/out/assets/SidebarAccount-Zg5DZblE.js +1 -0
  19. package/out/assets/arrow-right-CL9YSDVS.js +1 -0
  20. package/out/assets/channelApi-DNdJfsJ-.js +1 -0
  21. package/out/assets/chevron-down-CnLh_-aO.js +1 -0
  22. package/out/assets/{circle-alert-W0iyN4sC.js → circle-alert-oiiRDvhx.js} +1 -1
  23. package/out/assets/cloud-BEe2N89j.js +1 -0
  24. package/out/assets/code-9LB8QqxL.js +1 -0
  25. package/out/assets/{copy-C1MttOli.js → copy-giX4rmFJ.js} +1 -1
  26. package/out/assets/{download-y7SZXu6E.js → download-D0oMEYQZ.js} +1 -1
  27. package/out/assets/{downloadValidation-B0p9Ai_9.js → downloadValidation-Bk1VsBBo.js} +1 -1
  28. package/out/assets/external-link-Cm2WCUxv.js +1 -0
  29. package/out/assets/filePreview-BZ50vZZf.js +1 -0
  30. package/out/assets/game-Bvz4dspe.js +1 -0
  31. package/out/assets/{hard-drive-D13Qbobu.js → hard-drive-B3CQbcp2.js} +1 -1
  32. package/out/assets/index-BkZvz4WA.css +1 -0
  33. package/out/assets/index-WCK14Vja.js +34 -0
  34. package/out/assets/index.lazy-5Q6GuMNT.js +1 -0
  35. package/out/assets/index.lazy-5jq6EFXa.js +3 -0
  36. package/out/assets/index.lazy-7n1Q-NrA.js +3 -0
  37. package/out/assets/index.lazy-BFnOyQFj.js +1 -0
  38. package/out/assets/index.lazy-B_oPp6qK.js +1 -0
  39. package/out/assets/index.lazy-BvY50KVz.js +1 -0
  40. package/out/assets/index.lazy-C0Kn_amZ.js +1 -0
  41. package/out/assets/index.lazy-C3cek3Gn.js +1 -0
  42. package/out/assets/index.lazy-CLpPkdy1.js +1 -0
  43. package/out/assets/index.lazy-Cpr1kApf.js +2 -0
  44. package/out/assets/index.lazy-CuwLZiUK.js +1 -0
  45. package/out/assets/index.lazy-DDc3Ylgf.js +2 -0
  46. package/out/assets/index.lazy-Dg3aqOss.js +1 -0
  47. package/out/assets/{key-round-CZniN9lv.js → key-round-CzuljhND.js} +1 -1
  48. package/out/assets/{lock-D5OSNhep.js → lock-D2NhNoJW.js} +1 -1
  49. package/out/assets/message-square-DwBq_Go5.js +1 -0
  50. package/out/assets/mp-Bln2MB9G.js +1 -0
  51. package/out/assets/{music-CbUskKgg.js → music-CB73K5Gz.js} +1 -1
  52. package/out/assets/{notebook-pen-DqKDQ6MJ.js → notebook-pen-Up7r5zoI.js} +1 -1
  53. package/out/assets/play-OszVgROb.js +1 -0
  54. package/out/assets/plus-BbxQG_Ai.js +1 -0
  55. package/out/assets/{save-DkH1n_Ov.js → save-CiqyiifY.js} +1 -1
  56. package/out/assets/search-gqAPOsgS.js +1 -0
  57. package/out/assets/{send-Cl6NtD2T.js → send-vwCWsZGP.js} +1 -1
  58. package/out/assets/shield-check-CxWxsNLc.js +1 -0
  59. package/out/assets/{trash-2-BBjpgK_f.js → trash-2-DNGr8IgF.js} +1 -1
  60. package/out/assets/{triangle-alert-l98G8u9O.js → triangle-alert-B_1BlX1b.js} +1 -1
  61. package/out/assets/{upload-ByP6Ydde.js → upload-Dxl7GUzb.js} +1 -1
  62. package/out/assets/useChannelMessages-7bYKXU_R.js +3 -0
  63. package/out/assets/useGameRoom-DqA1mkfk.js +1 -0
  64. package/out/assets/wallet-DlkawdPJ.js +1 -0
  65. package/out/assets/{wifi-Bm4biAjc.js → wifi-sBOKcPFM.js} +1 -1
  66. package/out/avatars/default/LICENSE.md +7 -0
  67. package/out/avatars/default/dusk.svg +100 -0
  68. package/out/avatars/default/ember.svg +55 -0
  69. package/out/avatars/default/mint.svg +29 -0
  70. package/out/avatars/default/ocean.svg +25 -0
  71. package/out/avatars/default/sage.svg +34 -0
  72. package/out/avatars/default/violet.svg +60 -0
  73. package/out/chat/index.html +0 -0
  74. package/out/chat/join/index.html +0 -0
  75. package/out/demo/index.html +0 -0
  76. package/out/download/index.html +4 -3
  77. package/out/game/gandengyan/index.html +0 -0
  78. package/out/game/index.html +0 -0
  79. package/out/game/zhajinhua/index.html +0 -0
  80. package/out/index.html +4 -3
  81. package/out/note/index.html +0 -0
  82. package/out/ping/index.html +4 -3
  83. package/out/profile/index.html +0 -0
  84. package/out/web3/index.html +0 -0
  85. package/package.json +1 -1
  86. package/public/avatars/default/LICENSE.md +7 -0
  87. package/public/avatars/default/dusk.svg +100 -0
  88. package/public/avatars/default/ember.svg +55 -0
  89. package/public/avatars/default/mint.svg +29 -0
  90. package/public/avatars/default/ocean.svg +25 -0
  91. package/public/avatars/default/sage.svg +34 -0
  92. package/public/avatars/default/violet.svg +60 -0
  93. package/server/src/core/channelIdentity.js +2 -13
  94. package/server/src/http/app.js +1 -3
  95. package/server/src/index.js +286 -249
  96. package/server/src/utils/avatar.js +44 -3
  97. package/out/assets/ChatUi-BepWs-ZU.js +0 -1
  98. package/out/assets/LanguageToggle-CtzCCAYv.js +0 -1
  99. package/out/assets/LogoIcon-Dxto3Sb4.js +0 -1
  100. package/out/assets/MarketingLayout-BQw0IS2i.js +0 -1
  101. package/out/assets/MoveModal-4D9n11Kw.js +0 -1
  102. package/out/assets/Nav-9MDdvgNs.js +0 -1
  103. package/out/assets/NoteSidebar-C-rIt32H.js +0 -1
  104. package/out/assets/OpenSidebarButton-Dd0JmKuE.js +0 -1
  105. package/out/assets/PemBlock-C8dEIzu-.js +0 -1
  106. package/out/assets/SidebarAccount-ClS-N0lq.js +0 -1
  107. package/out/assets/arrow-right-urE9Rd7j.js +0 -1
  108. package/out/assets/channelApi-BwQU0-h1.js +0 -1
  109. package/out/assets/check-DUNsD2t6.js +0 -1
  110. package/out/assets/chevron-down-D6mpsfv4.js +0 -1
  111. package/out/assets/cloud-BMyOoC2x.js +0 -1
  112. package/out/assets/code-B1Cb_Icm.js +0 -1
  113. package/out/assets/filePreview-UI9NH34f.js +0 -1
  114. package/out/assets/game-CdU3xnZo.js +0 -1
  115. package/out/assets/image-DJCA16l_.js +0 -1
  116. package/out/assets/index-BdaFEQG-.css +0 -1
  117. package/out/assets/index-QxXZzOUL.js +0 -33
  118. package/out/assets/index.lazy-BBTTFanX.js +0 -1
  119. package/out/assets/index.lazy-BG4ZylHD.js +0 -2
  120. package/out/assets/index.lazy-Bi-6ZXZX.js +0 -1
  121. package/out/assets/index.lazy-BixWVr0B.js +0 -1
  122. package/out/assets/index.lazy-BjFwNYy5.js +0 -3
  123. package/out/assets/index.lazy-C8EIQsXY.js +0 -2
  124. package/out/assets/index.lazy-CarNe2uu.js +0 -1
  125. package/out/assets/index.lazy-DEuGu3H3.js +0 -1
  126. package/out/assets/index.lazy-GPyILCA7.js +0 -3
  127. package/out/assets/index.lazy-I8ofndXl.js +0 -1
  128. package/out/assets/index.lazy-TxhWsA7y.js +0 -1
  129. package/out/assets/index.lazy-azfky8k7.js +0 -1
  130. package/out/assets/log-out-B6phyZ5z.js +0 -1
  131. package/out/assets/play-BIl8q9eU.js +0 -1
  132. package/out/assets/plus-BxxbpH6Q.js +0 -1
  133. package/out/assets/search-BQi5Z0E-.js +0 -1
  134. package/out/assets/useChannelMessages-BgbYfF2c.js +0 -3
  135. package/out/assets/useGameRoom-DPmweWwe.js +0 -1
  136. package/out/assets/wallet-c7zIhNSM.js +0 -1
@@ -16,6 +16,7 @@ import b4a from 'b4a'
16
16
  import crypto from 'node:crypto'
17
17
  import fs from 'node:fs'
18
18
  import path from 'node:path'
19
+ import { Duplex } from 'node:stream'
19
20
 
20
21
  import { calculateCid, parseMostLink, buildMostLink } from './core/cid.js'
21
22
  import { normalizeChannelAttachment } from './core/channelAttachment.js'
@@ -28,11 +29,9 @@ import {
28
29
  normalizeChannelDisplayName,
29
30
  normalizeChannelAvatar,
30
31
  normalizeChannelId,
31
- createChannelFingerprint,
32
32
  createChannelWriterId,
33
33
  buildChannelKey,
34
34
  normalizeChannelKey,
35
- getChannelFingerprintFromKey,
36
35
  uniqueStrings,
37
36
  } from './core/channelIdentity.js'
38
37
  import { getPathBaseName, getDisplayPathFolder } from './core/displayPath.js'
@@ -99,6 +98,44 @@ import {
99
98
 
100
99
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
101
100
 
101
+ function createMemoryDuplexPair() {
102
+ let left
103
+ let right
104
+
105
+ left = new Duplex({
106
+ read() {},
107
+ write(chunk, _encoding, callback) {
108
+ if (!right.destroyed) right.push(chunk)
109
+ callback()
110
+ },
111
+ final(callback) {
112
+ if (!right.destroyed) right.push(null)
113
+ callback()
114
+ },
115
+ })
116
+
117
+ right = new Duplex({
118
+ read() {},
119
+ write(chunk, _encoding, callback) {
120
+ if (!left.destroyed) left.push(chunk)
121
+ callback()
122
+ },
123
+ final(callback) {
124
+ if (!left.destroyed) left.push(null)
125
+ callback()
126
+ },
127
+ })
128
+
129
+ left.on('close', () => {
130
+ if (!right.destroyed) right.destroy()
131
+ })
132
+ right.on('close', () => {
133
+ if (!left.destroyed) left.destroy()
134
+ })
135
+
136
+ return [left, right]
137
+ }
138
+
102
139
  export class MostBoxEngine extends EventEmitter {
103
140
  #store = null
104
141
  #swarm = null
@@ -123,6 +160,7 @@ export class MostBoxEngine extends EventEmitter {
123
160
  #channelIdDiscoveries = new Map()
124
161
  #channelPeers = new Map()
125
162
  #channelCandidateCache = new Map()
163
+ #channelStreams = new Set()
126
164
 
127
165
  #userSyncSessions = new Map()
128
166
  #userSyncCores = new Map()
@@ -387,6 +425,7 @@ export class MostBoxEngine extends EventEmitter {
387
425
  this.#channelIdDiscoveries.clear()
388
426
  this.#channelPeers.clear()
389
427
  this.#channelCandidateCache.clear()
428
+ this.#channelStreams.clear()
390
429
  this.#channels = []
391
430
 
392
431
  for (const [, coresMap] of this.#userSyncCores) {
@@ -1739,10 +1778,15 @@ export class MostBoxEngine extends EventEmitter {
1739
1778
 
1740
1779
  const left = this.#store.replicate(true, { live: true })
1741
1780
  const right = peerEngine.#store.replicate(false, { live: true })
1781
+ const [leftChat, rightChat] = createMemoryDuplexPair()
1742
1782
 
1743
1783
  left.on('error', () => {})
1744
1784
  right.on('error', () => {})
1785
+ leftChat.on('error', () => {})
1786
+ rightChat.on('error', () => {})
1745
1787
  left.pipe(right).pipe(left)
1788
+ this.#handleChannelConnection(leftChat).catch(() => {})
1789
+ peerEngine.#handleChannelConnection(rightChat).catch(() => {})
1746
1790
  this.#exchangeUserSyncSessions(peerEngine).catch(() => {})
1747
1791
  peerEngine.#exchangeUserSyncSessions(this).catch(() => {})
1748
1792
 
@@ -1750,6 +1794,8 @@ export class MostBoxEngine extends EventEmitter {
1750
1794
  close: () => {
1751
1795
  left.destroy()
1752
1796
  right.destroy()
1797
+ leftChat.destroy()
1798
+ rightChat.destroy()
1753
1799
  },
1754
1800
  }
1755
1801
  }
@@ -2050,7 +2096,7 @@ export class MostBoxEngine extends EventEmitter {
2050
2096
  // --- 频道管理 ---
2051
2097
 
2052
2098
  /**
2053
- * 创建或加入频道。channelId 是用户输入的短 ID,channelKey 是内部唯一身份。
2099
+ * 创建或加入频道。channelId 是用户输入的短 ID,channelKey 与频道名一致。
2054
2100
  * @param {string} channelIdInput - 用户可见短频道 ID
2055
2101
  * @param {string} [type='personal'] - 频道类型
2056
2102
  * @returns {Promise<object>}
@@ -2060,8 +2106,6 @@ export class MostBoxEngine extends EventEmitter {
2060
2106
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
2061
2107
  const channelId = normalizeChannelId(channelIdInput)
2062
2108
  const channelType = String(type || 'personal').trim() || 'personal'
2063
- const selectedChannelKey = normalizeChannelKey(options.channelKey)
2064
- const selectedFingerprint = String(options.fingerprint || '').trim()
2065
2109
 
2066
2110
  if (channelId.includes('.') && channelType !== 'game') {
2067
2111
  throw new Error('点号为系统保留,不能用于手动频道 ID')
@@ -2079,30 +2123,6 @@ export class MostBoxEngine extends EventEmitter {
2079
2123
  throw new Error(`频道名最多 ${CHANNEL_NAME_MAX_LENGTH} 个字符`)
2080
2124
  }
2081
2125
 
2082
- if (selectedChannelKey || selectedFingerprint) {
2083
- const channelKey =
2084
- selectedChannelKey || buildChannelKey(channelId, selectedFingerprint)
2085
- const existing = this.#channels.find(c => c.channelKey === channelKey)
2086
- if (existing) {
2087
- await this.#mergeChannelWriterCoreKeys(
2088
- existing,
2089
- options.writerCoreKeys
2090
- )
2091
- if (this.#upsertChannelMember(existing, options)) {
2092
- existing.syncUpdatedAt = getNextSyncTimestamp(existing.syncUpdatedAt)
2093
- this.#saveChannelsMetadata()
2094
- this.#appendUserSyncChannelUpsertSoon(existing, ownerAddress)
2095
- }
2096
- return this.#formatChannelForResponse(existing, ownerAddress)
2097
- }
2098
-
2099
- const candidate = this.#getCachedChannelCandidate(channelId, channelKey)
2100
- if (!candidate) {
2101
- throw new Error('未发现该频道候选,请重新搜索频道')
2102
- }
2103
- return this.#joinChannelFromCandidate(candidate, channelType, options)
2104
- }
2105
-
2106
2126
  const localCandidates = this.#getLocalChannelCandidates(channelId)
2107
2127
  const remoteCandidates = options.discover
2108
2128
  ? await this.#discoverChannelCandidates(channelId, {
@@ -2114,28 +2134,26 @@ export class MostBoxEngine extends EventEmitter {
2114
2134
  ...remoteCandidates,
2115
2135
  ])
2116
2136
 
2117
- if (candidates.length > 1) {
2118
- return {
2119
- conflict: true,
2120
- channelId,
2121
- candidates: candidates.map(candidate =>
2122
- this.#formatChannelCandidateForResponse(candidate, ownerAddress)
2123
- ),
2124
- }
2125
- }
2126
-
2127
- if (candidates.length === 1) {
2137
+ if (candidates.length > 0) {
2128
2138
  const candidate = candidates[0]
2129
2139
  if (candidate.local) {
2130
2140
  const existing = this.#channels.find(
2131
2141
  channel => channel.channelKey === candidate.channelKey
2132
2142
  )
2133
- if (existing && this.#upsertChannelMember(existing, options)) {
2134
- existing.syncUpdatedAt = getNextSyncTimestamp(existing.syncUpdatedAt)
2135
- this.#saveChannelsMetadata()
2136
- this.#appendUserSyncChannelUpsertSoon(existing, ownerAddress)
2143
+ if (existing) {
2144
+ const writerKeysChanged = await this.#mergeChannelWriterCoreKeys(
2145
+ existing,
2146
+ candidate.writerCoreKeys
2147
+ )
2148
+ const memberChanged = this.#upsertChannelMember(existing, options)
2149
+ if (writerKeysChanged || memberChanged) {
2150
+ existing.syncUpdatedAt = getNextSyncTimestamp(existing.syncUpdatedAt)
2151
+ this.#saveChannelsMetadata()
2152
+ this.#appendUserSyncChannelUpsertSoon(existing, ownerAddress)
2153
+ this.#broadcastChannelHello()
2154
+ }
2155
+ return this.#formatChannelForResponse(existing, ownerAddress)
2137
2156
  }
2138
- if (existing) return this.#formatChannelForResponse(existing, ownerAddress)
2139
2157
  const joined = await this.#joinChannelFromCandidate(
2140
2158
  candidate,
2141
2159
  channelType,
@@ -2188,25 +2206,32 @@ export class MostBoxEngine extends EventEmitter {
2188
2206
  ? { channelKey: String(candidateInput), channelId }
2189
2207
  : null
2190
2208
 
2191
- if (!candidate?.channelKey && !candidate?.fingerprint) {
2209
+ if (!candidate?.channelKey) {
2192
2210
  return this.createChannel(channelId, options.type || 'group', options)
2193
2211
  }
2194
2212
 
2195
- const channelKey =
2196
- normalizeChannelKey(candidate.channelKey) ||
2197
- buildChannelKey(channelId, String(candidate.fingerprint || '').trim())
2213
+ const channelKey = buildChannelKey(channelId)
2198
2214
  const existing = this.#channels.find(c => c.channelKey === channelKey)
2199
2215
  if (existing) {
2200
- await this.#mergeChannelWriterCoreKeys(existing, candidate.writerCoreKeys)
2201
- if (this.#upsertChannelMember(existing, options)) {
2216
+ const writerKeysChanged = await this.#mergeChannelWriterCoreKeys(
2217
+ existing,
2218
+ candidate.writerCoreKeys
2219
+ )
2220
+ const memberChanged = this.#upsertChannelMember(existing, options)
2221
+ if (writerKeysChanged || memberChanged) {
2202
2222
  existing.syncUpdatedAt = getNextSyncTimestamp(existing.syncUpdatedAt)
2203
2223
  this.#saveChannelsMetadata()
2204
2224
  this.#appendUserSyncChannelUpsertSoon(existing, options.ownerAddress)
2225
+ this.#broadcastChannelHello()
2205
2226
  }
2206
2227
  return this.#formatChannelForResponse(existing, options.ownerAddress)
2207
2228
  }
2208
2229
 
2209
- const cached = this.#getCachedChannelCandidate(channelId, channelKey)
2230
+ const cached =
2231
+ this.#getCachedChannelCandidate(
2232
+ channelId,
2233
+ normalizeChannelKey(candidate.channelKey)
2234
+ ) || this.#getCachedChannelCandidate(channelId, channelKey)
2210
2235
  const joined = await this.#joinChannelFromCandidate(cached || candidate, 'group', {
2211
2236
  ...options,
2212
2237
  channelKey,
@@ -2517,7 +2542,9 @@ export class MostBoxEngine extends EventEmitter {
2517
2542
  author,
2518
2543
  authorName,
2519
2544
  content: trimmed,
2520
- timestamp: Date.now(),
2545
+ timestamp: await this.#getNextChannelMessageTimestamp(
2546
+ channel.channelKey
2547
+ ),
2521
2548
  }
2522
2549
  if (attachment) {
2523
2550
  message.attachment = attachment
@@ -2597,21 +2624,15 @@ export class MostBoxEngine extends EventEmitter {
2597
2624
  let channel = this.#channels.find(c => c.channelKey === value)
2598
2625
  if (channel) return channel
2599
2626
 
2600
- const matches = this.#channels.filter(c => c.channelId === value)
2601
- const visibleMatches = owner
2602
- ? matches.filter(c => this.#channelHasMember(c, owner))
2603
- : matches
2604
- if (visibleMatches.length === 1) return visibleMatches[0]
2605
- if (visibleMatches.length > 1) {
2606
- throw new Error('频道 ID 存在多个候选,请使用 channelKey')
2627
+ channel = this.#channels.find(c => c.channelId === value)
2628
+ if (channel && (!owner || this.#channelHasMember(channel, owner))) {
2629
+ return channel
2607
2630
  }
2608
2631
  throw new Error('频道不存在')
2609
2632
  }
2610
2633
 
2611
2634
  async #createLocalChannel(channelId, type = 'personal', options = {}) {
2612
- const fingerprint =
2613
- String(options.fingerprint || '').trim() || createChannelFingerprint()
2614
- const channelKey = buildChannelKey(channelId, fingerprint)
2635
+ const channelKey = buildChannelKey(channelId)
2615
2636
  const writerId = String(options.writerId || '').trim() || createChannelWriterId()
2616
2637
  const ns = this.#store.namespace(`channel-${channelKey}`)
2617
2638
  const localCore = ns.get({
@@ -2626,7 +2647,6 @@ export class MostBoxEngine extends EventEmitter {
2626
2647
  ])
2627
2648
  const channelInfo = {
2628
2649
  channelId,
2629
- fingerprint,
2630
2650
  channelKey,
2631
2651
  name: channelId,
2632
2652
  type: String(type || 'personal').trim() || 'personal',
@@ -2651,6 +2671,7 @@ export class MostBoxEngine extends EventEmitter {
2651
2671
  await this.#joinChannelDiscoveryTopics(channelInfo)
2652
2672
  this.#cacheChannelCandidate(this.#channelToCandidate(channelInfo, true))
2653
2673
  this.#saveChannelsMetadata()
2674
+ this.#broadcastChannelHello()
2654
2675
  return channelInfo
2655
2676
  }
2656
2677
 
@@ -2658,24 +2679,18 @@ export class MostBoxEngine extends EventEmitter {
2658
2679
  const channelId = normalizeChannelId(
2659
2680
  candidateInput.channelId || options.channelId
2660
2681
  )
2661
- const channelKey = normalizeChannelKey(candidateInput.channelKey)
2662
- const fingerprint =
2663
- String(candidateInput.fingerprint || '').trim() ||
2664
- getChannelFingerprintFromKey(channelId, channelKey)
2665
- if (!channelId || !fingerprint) {
2682
+ const channelKey = buildChannelKey(channelId)
2683
+ if (!channelId || !channelKey) {
2666
2684
  throw new Error('频道候选缺少身份信息')
2667
2685
  }
2668
- const expectedChannelKey = buildChannelKey(channelId, fingerprint)
2669
- if (channelKey && channelKey !== expectedChannelKey) {
2670
- throw new Error('频道候选身份格式不匹配')
2671
- }
2672
2686
 
2673
2687
  const existing = this.#channels.find(
2674
- channel => channel.channelKey === expectedChannelKey
2688
+ channel => channel.channelKey === channelKey
2675
2689
  )
2676
2690
  if (existing) {
2677
2691
  if (this.#upsertChannelMember(existing, options)) {
2678
2692
  this.#saveChannelsMetadata()
2693
+ this.#broadcastChannelHello()
2679
2694
  }
2680
2695
  return this.#formatChannelForResponse(existing, options.ownerAddress)
2681
2696
  }
@@ -2691,7 +2706,6 @@ export class MostBoxEngine extends EventEmitter {
2691
2706
  const channelInfo = await this.#createLocalChannel(channelId, candidateInput.type || type, {
2692
2707
  ...options,
2693
2708
  ownerAddress,
2694
- fingerprint,
2695
2709
  createdAt: candidateInput.createdAt,
2696
2710
  lastMessageAt: candidateInput.lastMessageAt,
2697
2711
  writerCoreKeys: candidateInput.writerCoreKeys,
@@ -2805,7 +2819,18 @@ export class MostBoxEngine extends EventEmitter {
2805
2819
  }
2806
2820
 
2807
2821
  async #discoverChannelCandidates(channelId, options = {}) {
2808
- if (this.#options.disableNetwork) return []
2822
+ const getCachedCandidates = () => {
2823
+ const now = Date.now()
2824
+ return [
2825
+ ...(this.#channelCandidateCache.get(channelId)?.values() || []),
2826
+ ].filter(
2827
+ candidate =>
2828
+ candidate.local ||
2829
+ !candidate.lastSeen ||
2830
+ now - candidate.lastSeen <= CHANNEL_CANDIDATE_TTL
2831
+ )
2832
+ }
2833
+ if (this.#options.disableNetwork) return getCachedCandidates()
2809
2834
  const timeout =
2810
2835
  Number(options.timeout) >= 0
2811
2836
  ? Number(options.timeout)
@@ -2819,15 +2844,7 @@ export class MostBoxEngine extends EventEmitter {
2819
2844
  this.#channelIdDiscoveries.set(channelId, discovery)
2820
2845
  }
2821
2846
  await sleep(timeout)
2822
- const now = Date.now()
2823
- const candidates = [
2824
- ...(this.#channelCandidateCache.get(channelId)?.values() || []),
2825
- ].filter(
2826
- candidate =>
2827
- candidate.local ||
2828
- !candidate.lastSeen ||
2829
- now - candidate.lastSeen <= CHANNEL_CANDIDATE_TTL
2830
- )
2847
+ const candidates = getCachedCandidates()
2831
2848
  if (!hadDiscovery && !this.#channels.some(c => c.channelId === channelId)) {
2832
2849
  this.#channelIdDiscoveries.delete(channelId)
2833
2850
  this.#chatSwarm
@@ -2846,7 +2863,6 @@ export class MostBoxEngine extends EventEmitter {
2846
2863
  byKey.set(candidate.channelKey, {
2847
2864
  ...candidate,
2848
2865
  writerCoreKeys: uniqueStrings(candidate.writerCoreKeys),
2849
- onlineCount: Number(candidate.onlineCount) || (candidate.local ? 0 : 1),
2850
2866
  })
2851
2867
  continue
2852
2868
  }
@@ -2858,9 +2874,6 @@ export class MostBoxEngine extends EventEmitter {
2858
2874
  ...existing.writerCoreKeys,
2859
2875
  ...(candidate.writerCoreKeys || []),
2860
2876
  ]),
2861
- onlineCount:
2862
- Math.max(Number(existing.onlineCount) || 0, 0) +
2863
- (candidate.local ? 0 : 1),
2864
2877
  })
2865
2878
  }
2866
2879
  return [...byKey.values()]
@@ -2869,66 +2882,47 @@ export class MostBoxEngine extends EventEmitter {
2869
2882
  #channelToCandidate(channel, local = false) {
2870
2883
  return {
2871
2884
  channelId: channel.channelId,
2872
- fingerprint: channel.fingerprint,
2873
2885
  channelKey: channel.channelKey,
2874
2886
  type: channel.type,
2875
2887
  createdAt: channel.createdAt,
2876
2888
  lastMessageAt: channel.lastMessageAt || '',
2877
2889
  writerCoreKeys: uniqueStrings(channel.writerCoreKeys),
2878
2890
  local,
2879
- onlineCount: local ? 0 : 1,
2880
2891
  }
2881
2892
  }
2882
2893
 
2883
2894
  #cacheChannelCandidate(candidate) {
2884
- if (!candidate?.channelId || !candidate?.channelKey) return
2885
- if (!this.#channelCandidateCache.has(candidate.channelId)) {
2886
- this.#channelCandidateCache.set(candidate.channelId, new Map())
2887
- }
2888
- const cache = this.#channelCandidateCache.get(candidate.channelId)
2889
- const existing = cache.get(candidate.channelKey)
2890
- cache.set(candidate.channelKey, {
2895
+ const channelId = normalizeChannelId(candidate?.channelId)
2896
+ const channelKey = buildChannelKey(channelId)
2897
+ if (!channelId || !channelKey) return
2898
+ if (!this.#channelCandidateCache.has(channelId)) {
2899
+ this.#channelCandidateCache.set(channelId, new Map())
2900
+ }
2901
+ const cache = this.#channelCandidateCache.get(channelId)
2902
+ const existing = cache.get(channelKey)
2903
+ cache.set(channelKey, {
2891
2904
  ...existing,
2892
2905
  ...candidate,
2906
+ channelId,
2907
+ channelKey,
2893
2908
  writerCoreKeys: uniqueStrings([
2894
2909
  ...(existing?.writerCoreKeys || []),
2895
2910
  ...(candidate.writerCoreKeys || []),
2896
2911
  ]),
2897
- onlineCount: Math.max(Number(existing?.onlineCount) || 0, 0) + 1,
2898
2912
  lastSeen: Date.now(),
2899
2913
  })
2900
2914
  }
2901
2915
 
2902
2916
  #getCachedChannelCandidate(channelId, channelKey) {
2903
- const candidate = this.#channelCandidateCache.get(channelId)?.get(channelKey)
2917
+ const normalizedChannelId = normalizeChannelId(channelId)
2918
+ const normalizedChannelKey = buildChannelKey(normalizedChannelId)
2919
+ const cache = this.#channelCandidateCache.get(normalizedChannelId)
2920
+ const candidate = cache?.get(channelKey) || cache?.get(normalizedChannelKey)
2904
2921
  if (candidate) return candidate
2905
- const local = this.#channels.find(channel => channel.channelKey === channelKey)
2906
- return local ? this.#channelToCandidate(local, true) : null
2907
- }
2908
-
2909
- #formatChannelCandidateForResponse(candidate, ownerAddress = '') {
2910
- const owner = normalizeOwnerAddress(ownerAddress)
2911
- const localChannel = this.#channels.find(
2912
- channel => channel.channelKey === candidate.channelKey
2922
+ const local = this.#channels.find(
2923
+ channel => channel.channelKey === normalizedChannelKey
2913
2924
  )
2914
- const remark =
2915
- localChannel && owner
2916
- ? localChannel.remarks?.[owner] || ''
2917
- : candidate.local
2918
- ? ''
2919
- : `${candidate.channelId}-网络`
2920
- return {
2921
- channelId: candidate.channelId,
2922
- fingerprint: candidate.fingerprint,
2923
- channelKey: candidate.channelKey,
2924
- name: candidate.channelId,
2925
- type: candidate.type || 'public',
2926
- createdAt: candidate.createdAt || '',
2927
- lastMessageAt: candidate.lastMessageAt || '',
2928
- remark,
2929
- local: Boolean(candidate.local),
2930
- onlineCount: Number(candidate.onlineCount) || 0,
2931
- }
2925
+ return local ? this.#channelToCandidate(local, true) : null
2932
2926
  }
2933
2927
 
2934
2928
  #formatChannelForResponse(channel, ownerAddress = '') {
@@ -2936,7 +2930,6 @@ export class MostBoxEngine extends EventEmitter {
2936
2930
  return {
2937
2931
  name: channel.channelId,
2938
2932
  channelId: channel.channelId,
2939
- fingerprint: channel.fingerprint,
2940
2933
  channelKey: channel.channelKey,
2941
2934
  key: channel.channelKey,
2942
2935
  coreKey: channel.localWriterCoreKey,
@@ -3046,6 +3039,31 @@ export class MostBoxEngine extends EventEmitter {
3046
3039
  )
3047
3040
  }
3048
3041
 
3042
+ async #getNextChannelMessageTimestamp(channelKey) {
3043
+ const coresMap = this.#channelCores.get(channelKey)
3044
+ let maxTimestamp = 0
3045
+
3046
+ if (coresMap) {
3047
+ for (const [, core] of coresMap) {
3048
+ for (let i = 0; i < core.length; i++) {
3049
+ try {
3050
+ const entry = await core.get(i)
3051
+ if (entry?.type === 'message') {
3052
+ maxTimestamp = Math.max(
3053
+ maxTimestamp,
3054
+ Number(entry.timestamp) || 0
3055
+ )
3056
+ }
3057
+ } catch {
3058
+ break
3059
+ }
3060
+ }
3061
+ }
3062
+ }
3063
+
3064
+ return Math.max(Date.now(), maxTimestamp + 1)
3065
+ }
3066
+
3049
3067
  #normalizeChannelMessageForResponse(channelKey, message) {
3050
3068
  const channel = this.#channels.find(item => item.channelKey === channelKey)
3051
3069
  const authorAddress = normalizeOwnerAddress(message?.author)
@@ -3523,7 +3541,6 @@ export class MostBoxEngine extends EventEmitter {
3523
3541
  }
3524
3542
  return {
3525
3543
  channelId: channel.channelId,
3526
- fingerprint: channel.fingerprint,
3527
3544
  channelKey: channel.channelKey,
3528
3545
  type: channel.type,
3529
3546
  createdAt: channel.createdAt,
@@ -3701,15 +3718,9 @@ export class MostBoxEngine extends EventEmitter {
3701
3718
  async #applyUserSyncChannelRecord(ownerAddress, record, timestamp) {
3702
3719
  if (!record || typeof record !== 'object') return false
3703
3720
  const channelId = normalizeChannelId(record.channelId)
3704
- const fingerprint = String(record.fingerprint || '').trim()
3705
- const expectedChannelKey =
3706
- channelId && fingerprint ? buildChannelKey(channelId, fingerprint) : ''
3707
- const recordChannelKey = normalizeChannelKey(record.channelKey)
3708
- if (recordChannelKey && recordChannelKey !== expectedChannelKey) {
3709
- return false
3710
- }
3711
- const channelKey = recordChannelKey || expectedChannelKey
3712
- if (!channelId || !fingerprint || !channelKey) return false
3721
+ const expectedChannelKey = buildChannelKey(channelId)
3722
+ const channelKey = expectedChannelKey
3723
+ if (!channelId || !channelKey) return false
3713
3724
  const syncUpdatedAt = getSyncTimestamp(record.syncUpdatedAt, timestamp)
3714
3725
  const entityKey = `channel:${channelKey}`
3715
3726
  if (!this.#shouldApplyUserSyncEntity(ownerAddress, entityKey, syncUpdatedAt)) {
@@ -3721,7 +3732,6 @@ export class MostBoxEngine extends EventEmitter {
3721
3732
  if (!channel) {
3722
3733
  channel = {
3723
3734
  channelId,
3724
- fingerprint,
3725
3735
  channelKey,
3726
3736
  name: channelId,
3727
3737
  createdAt:
@@ -4668,16 +4678,11 @@ export class MostBoxEngine extends EventEmitter {
4668
4678
  .filter(channel => channel && typeof channel === 'object')
4669
4679
  .map(channel => {
4670
4680
  const channelId = normalizeChannelId(channel.channelId)
4671
- const fingerprint = String(channel.fingerprint || '').trim()
4672
- const expectedChannelKey =
4673
- channelId && fingerprint
4674
- ? buildChannelKey(channelId, fingerprint)
4675
- : ''
4676
4681
  const channelKey = normalizeChannelKey(channel.channelKey)
4682
+ const expectedChannelKey = buildChannelKey(channelId)
4677
4683
  return {
4678
4684
  ...channel,
4679
4685
  channelId,
4680
- fingerprint,
4681
4686
  channelKey,
4682
4687
  expectedChannelKey,
4683
4688
  name: channelId,
@@ -4687,7 +4692,6 @@ export class MostBoxEngine extends EventEmitter {
4687
4692
  .filter(
4688
4693
  channel =>
4689
4694
  CHANNEL_NAME_REGEX.test(channel.channelId) &&
4690
- channel.fingerprint &&
4691
4695
  channel.channelKey === channel.expectedChannelKey &&
4692
4696
  channel.writerId &&
4693
4697
  channel.localWriterCoreKey
@@ -4710,7 +4714,6 @@ export class MostBoxEngine extends EventEmitter {
4710
4714
  .filter(channel => !TRANSIENT_CHANNEL_TYPES.has(channel?.type))
4711
4715
  .map(channel => ({
4712
4716
  channelId: channel.channelId,
4713
- fingerprint: channel.fingerprint,
4714
4717
  channelKey: channel.channelKey,
4715
4718
  name: channel.channelId,
4716
4719
  type: channel.type,
@@ -4840,13 +4843,9 @@ export class MostBoxEngine extends EventEmitter {
4840
4843
  }
4841
4844
  }
4842
4845
 
4843
- async #handleChannelConnection(conn) {
4844
- const stream = conn
4845
- let connectedPeerId = null
4846
-
4846
+ #buildChannelHelloMessage() {
4847
4847
  const channels = this.#channels.map(channel => ({
4848
4848
  channelId: channel.channelId,
4849
- fingerprint: channel.fingerprint,
4850
4849
  channelKey: channel.channelKey,
4851
4850
  type: channel.type,
4852
4851
  createdAt: channel.createdAt,
@@ -4865,122 +4864,157 @@ export class MostBoxEngine extends EventEmitter {
4865
4864
  })
4866
4865
  )
4867
4866
 
4868
- const helloMessage = JSON.stringify({
4867
+ return {
4869
4868
  type: 'channel-hello',
4870
4869
  peerId: this.getNodeId(),
4871
4870
  authorName: this.getNodeId().slice(0, 4),
4872
4871
  channels,
4873
4872
  userSyncSessions,
4874
- })
4873
+ }
4874
+ }
4875
4875
 
4876
+ #sendChannelHello(stream) {
4877
+ if (!stream || stream.destroyed || stream.writableEnded) {
4878
+ this.#channelStreams.delete(stream)
4879
+ return false
4880
+ }
4876
4881
  try {
4877
- stream.write(helloMessage)
4882
+ stream.write(`${JSON.stringify(this.#buildChannelHelloMessage())}\n`)
4883
+ return true
4878
4884
  } catch {
4879
- return
4885
+ this.#channelStreams.delete(stream)
4886
+ return false
4880
4887
  }
4888
+ }
4881
4889
 
4882
- stream.on('data', async data => {
4883
- try {
4884
- const msg = JSON.parse(data.toString())
4885
- if (msg.type === 'channel-hello') {
4886
- connectedPeerId = msg.peerId
4887
-
4888
- const remoteChannels = Array.isArray(msg.channels)
4889
- ? msg.channels
4890
- .filter(channel => channel && typeof channel === 'object')
4891
- .map(channel => ({
4892
- channelId: normalizeChannelId(channel.channelId),
4893
- fingerprint: String(channel.fingerprint || '').trim(),
4894
- channelKey: normalizeChannelKey(channel.channelKey),
4895
- type: String(channel.type || 'public').trim() || 'public',
4896
- createdAt:
4897
- typeof channel.createdAt === 'string'
4898
- ? channel.createdAt
4899
- : '',
4900
- lastMessageAt:
4901
- typeof channel.lastMessageAt === 'string'
4902
- ? channel.lastMessageAt
4903
- : '',
4904
- writerCoreKeys: uniqueStrings(channel.writerCoreKeys),
4905
- }))
4906
- .filter(
4907
- channel =>
4908
- channel.channelId &&
4909
- channel.fingerprint &&
4910
- channel.channelKey
4911
- )
4912
- : []
4913
-
4914
- for (const remoteChannel of remoteChannels) {
4915
- this.#cacheChannelCandidate({
4916
- ...remoteChannel,
4917
- local: false,
4918
- peerId: msg.peerId,
4919
- onlineCount: 1,
4920
- })
4890
+ #broadcastChannelHello() {
4891
+ for (const stream of [...this.#channelStreams]) {
4892
+ this.#sendChannelHello(stream)
4893
+ }
4894
+ }
4921
4895
 
4922
- const localChannel = this.#channels.find(
4923
- channel => channel.channelKey === remoteChannel.channelKey
4924
- )
4925
- if (!localChannel) continue
4926
-
4927
- const peers = this.#channelPeers.get(localChannel.channelKey)
4928
- if (peers) {
4929
- peers.set(msg.peerId, {
4930
- peerId: msg.peerId,
4931
- authorName: msg.authorName,
4932
- lastSeen: Date.now(),
4933
- })
4934
- }
4896
+ async #processChannelHelloMessage(msg) {
4897
+ if (msg.type !== 'channel-hello') return null
4935
4898
 
4936
- for (const writerCoreKey of remoteChannel.writerCoreKeys) {
4937
- if (
4938
- writerCoreKey &&
4939
- writerCoreKey !== this.#channelLocalCoreKey.get(localChannel.channelKey)
4940
- ) {
4941
- await this.#openRemoteChannelCore(
4942
- localChannel.channelKey,
4943
- writerCoreKey
4944
- )
4945
- }
4899
+ const remoteChannels = Array.isArray(msg.channels)
4900
+ ? msg.channels
4901
+ .filter(channel => channel && typeof channel === 'object')
4902
+ .map(channel => {
4903
+ const channelId = normalizeChannelId(channel.channelId)
4904
+ return {
4905
+ channelId,
4906
+ channelKey: buildChannelKey(channelId),
4907
+ type: String(channel.type || 'public').trim() || 'public',
4908
+ createdAt:
4909
+ typeof channel.createdAt === 'string' ? channel.createdAt : '',
4910
+ lastMessageAt:
4911
+ typeof channel.lastMessageAt === 'string'
4912
+ ? channel.lastMessageAt
4913
+ : '',
4914
+ writerCoreKeys: uniqueStrings(channel.writerCoreKeys),
4946
4915
  }
4947
- }
4916
+ })
4917
+ .filter(channel => channel.channelId && channel.channelKey)
4918
+ : []
4948
4919
 
4949
- const remoteUserSyncSessions = Array.isArray(msg.userSyncSessions)
4950
- ? msg.userSyncSessions
4951
- .filter(session => session && typeof session === 'object')
4952
- .map(session => ({
4953
- ownerAddress: normalizeOwnerAddress(session.ownerAddress),
4954
- syncId: String(session.syncId || '').trim(),
4955
- writerCoreKeys: uniqueStrings(session.writerCoreKeys),
4956
- }))
4957
- .filter(session => session.ownerAddress && session.syncId)
4958
- : []
4959
-
4960
- for (const remoteSession of remoteUserSyncSessions) {
4961
- const localSession = this.#userSyncSessions.get(
4962
- remoteSession.ownerAddress
4963
- )
4964
- if (!localSession || localSession.syncId !== remoteSession.syncId) {
4965
- continue
4966
- }
4967
- await this.#mergeUserSyncWriterCoreKeys(
4968
- localSession,
4969
- remoteSession.writerCoreKeys
4970
- )
4971
- }
4920
+ for (const remoteChannel of remoteChannels) {
4921
+ this.#cacheChannelCandidate({
4922
+ ...remoteChannel,
4923
+ local: false,
4924
+ peerId: msg.peerId,
4925
+ })
4972
4926
 
4973
- this.emit('channel:peer:online', {
4974
- peerId: msg.peerId,
4975
- authorName: msg.authorName,
4976
- })
4927
+ const localChannel = this.#channels.find(
4928
+ channel => channel.channelKey === remoteChannel.channelKey
4929
+ )
4930
+ if (!localChannel) continue
4931
+
4932
+ const peers = this.#channelPeers.get(localChannel.channelKey)
4933
+ if (peers) {
4934
+ peers.set(msg.peerId, {
4935
+ peerId: msg.peerId,
4936
+ authorName: msg.authorName,
4937
+ lastSeen: Date.now(),
4938
+ })
4939
+ }
4940
+
4941
+ for (const writerCoreKey of remoteChannel.writerCoreKeys) {
4942
+ if (
4943
+ writerCoreKey &&
4944
+ writerCoreKey !== this.#channelLocalCoreKey.get(localChannel.channelKey)
4945
+ ) {
4946
+ await this.#openRemoteChannelCore(
4947
+ localChannel.channelKey,
4948
+ writerCoreKey
4949
+ )
4950
+ }
4951
+ }
4952
+ }
4953
+
4954
+ const remoteUserSyncSessions = Array.isArray(msg.userSyncSessions)
4955
+ ? msg.userSyncSessions
4956
+ .filter(session => session && typeof session === 'object')
4957
+ .map(session => ({
4958
+ ownerAddress: normalizeOwnerAddress(session.ownerAddress),
4959
+ syncId: String(session.syncId || '').trim(),
4960
+ writerCoreKeys: uniqueStrings(session.writerCoreKeys),
4961
+ }))
4962
+ .filter(session => session.ownerAddress && session.syncId)
4963
+ : []
4964
+
4965
+ for (const remoteSession of remoteUserSyncSessions) {
4966
+ const localSession = this.#userSyncSessions.get(
4967
+ remoteSession.ownerAddress
4968
+ )
4969
+ if (!localSession || localSession.syncId !== remoteSession.syncId) {
4970
+ continue
4971
+ }
4972
+ await this.#mergeUserSyncWriterCoreKeys(
4973
+ localSession,
4974
+ remoteSession.writerCoreKeys
4975
+ )
4976
+ }
4977
+
4978
+ this.emit('channel:peer:online', {
4979
+ peerId: msg.peerId,
4980
+ authorName: msg.authorName,
4981
+ })
4982
+
4983
+ return msg.peerId
4984
+ }
4985
+
4986
+ async #handleChannelConnection(conn) {
4987
+ const stream = conn
4988
+ let connectedPeerId = null
4989
+ let readBuffer = ''
4990
+ let closed = false
4991
+
4992
+ this.#channelStreams.add(stream)
4993
+ if (!this.#sendChannelHello(stream)) return
4994
+
4995
+ stream.on('data', async data => {
4996
+ readBuffer += data.toString()
4997
+ let newlineIndex = readBuffer.indexOf('\n')
4998
+ while (newlineIndex !== -1) {
4999
+ const line = readBuffer.slice(0, newlineIndex).trim()
5000
+ readBuffer = readBuffer.slice(newlineIndex + 1)
5001
+ newlineIndex = readBuffer.indexOf('\n')
5002
+ if (!line) continue
5003
+ try {
5004
+ const peerId = await this.#processChannelHelloMessage(
5005
+ JSON.parse(line)
5006
+ )
5007
+ if (peerId) connectedPeerId = peerId
5008
+ } catch (err) {
5009
+ console.warn(`[MostBox] Failed to process channel data:`, err.message)
4977
5010
  }
4978
- } catch (err) {
4979
- console.warn(`[MostBox] Failed to process channel data:`, err.message)
4980
5011
  }
4981
5012
  })
4982
5013
 
4983
- stream.on('close', () => {
5014
+ const cleanup = () => {
5015
+ if (closed) return
5016
+ closed = true
5017
+ this.#channelStreams.delete(stream)
4984
5018
  if (connectedPeerId) {
4985
5019
  for (const [, peers] of this.#channelPeers) {
4986
5020
  if (peers.has(connectedPeerId)) {
@@ -4993,7 +5027,10 @@ export class MostBoxEngine extends EventEmitter {
4993
5027
  }
4994
5028
  }
4995
5029
  }
4996
- })
5030
+ }
5031
+
5032
+ stream.on('close', cleanup)
5033
+ stream.on('error', cleanup)
4997
5034
  }
4998
5035
 
4999
5036
  /**