most-box 0.3.4 → 0.3.6
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.
- package/out/about/index.html +20 -0
- package/out/admin/index.html +0 -0
- package/out/app/index.html +0 -0
- package/out/assets/AccountMenu-B26qus-Q.js +1 -0
- package/out/assets/AppShell-BiYQsNO8.js +1 -0
- package/out/assets/AppTop-D-qYC0ep.js +1 -0
- package/out/assets/ChatRestoringIndicator-CLV78gNW.js +1 -0
- package/out/assets/CopyButton-BrEkzqEq.js +1 -0
- package/out/assets/MarketingHeader-CJXpogPY.js +1 -0
- package/out/assets/MarketingLayout-Ch4sw5Qm.js +1 -0
- package/out/assets/{MilkdownEditor-BzbYt-xb.js → MilkdownEditor-DfhuT9eU.js} +1 -1
- package/out/assets/OpenSidebarButton-Bvwr7B6k.js +1 -0
- package/out/assets/arrow-right-BsHzC0ZL.js +1 -0
- package/out/assets/{arrow-up-down-B-677ip8.js → arrow-up-down-DvLSz6Ma.js} +1 -1
- package/out/assets/{userProfile-BZvkCuet.js → channelApi-C5eHetoJ.js} +1 -1
- package/out/assets/chatJoinInvite-7tuD-STm.js +1 -0
- package/out/assets/{chatNoteDraft-CYbvcEvC.js → chatNoteDraft-DTy6In5R.js} +1 -1
- package/out/assets/{circle-alert-dfN4T6Nq.js → circle-alert-DXVkx8p8.js} +1 -1
- package/out/assets/{copy-B7pPZ2MP.js → copy-OwXDsstV.js} +1 -1
- package/out/assets/{download-COQOcPqI.js → download-Cy2BlfKM.js} +1 -1
- package/out/assets/downloadValidation-Dd43i9_9.js +1 -0
- package/out/assets/{external-link-D6tHlYgS.js → external-link-D_AQSgxW.js} +1 -1
- package/out/assets/{file-text-DiyIGL1I.js → file-text-zvTER2mN.js} +1 -1
- package/out/assets/fingerprint-pattern-BT73ASEB.js +1 -0
- package/out/assets/{folder-BS_GbpPQ.js → folder-6pYYnS2-.js} +1 -1
- package/out/assets/folder-open-DOJ4bB58.js +1 -0
- package/out/assets/format-CAKfypjK.js +1 -0
- package/out/assets/gamepad-2-BYqNgCQr.js +1 -0
- package/out/assets/{hard-drive-Cqa7Rstj.js → hard-drive-DL1KJHmG.js} +1 -1
- package/out/assets/i18n-DKsg67mH.js +1 -0
- package/out/assets/index-FEwag7ej.css +1 -0
- package/out/assets/index-mjFWLwue.js +31 -0
- package/out/assets/index.lazy-8GSHLIGt.js +3 -0
- package/out/assets/index.lazy-B3jH9yBA.js +1 -0
- package/out/assets/index.lazy-B5JMiv3z.js +1 -0
- package/out/assets/index.lazy-BAr7-dhf.js +1 -0
- package/out/assets/index.lazy-BMFwyE_Z.js +3 -0
- package/out/assets/index.lazy-BbA_1LnY.js +1 -0
- package/out/assets/index.lazy-BmpOpQqg.js +1 -0
- package/out/assets/index.lazy-CB3C6kMj.js +1 -0
- package/out/assets/index.lazy-CVOAOO0_.js +1 -0
- package/out/assets/index.lazy-CyFSwJuo.js +1 -0
- package/out/assets/index.lazy-D3iEq2ZW.js +2 -0
- package/out/assets/index.lazy-F3iHN_Ao.js +1 -0
- package/out/assets/index.lazy-PnZR3z1V.js +5 -0
- package/out/assets/index.lazy-qDpRLqXu.js +1 -0
- package/out/assets/index.lazy-s6Ok77GI.js +1 -0
- package/out/assets/{loader-CwyHYZdX.js → loader-C4pED_QP.js} +1 -1
- package/out/assets/lock-yG7M7GkB.js +1 -0
- package/out/assets/{message-square-BgsNXPft.js → message-square-CfApq8Ou.js} +1 -1
- package/out/assets/mp-cEQteKUC.js +1 -0
- package/out/assets/{music-Dn9nm9Qb.js → music-Br2x8TKX.js} +1 -1
- package/out/assets/notebook-pen-Bcd3MrtD.js +1 -0
- package/out/assets/play-DSHtNcOe.js +1 -0
- package/out/assets/{refresh-cw-D4BsHvhi.js → refresh-cw-CfkUlTK9.js} +1 -1
- package/out/assets/{save-B02sbO9Y.js → save-CfYzXcxH.js} +1 -1
- package/out/assets/saveLocalFile-BLTn3Z18.js +1 -0
- package/out/assets/search-DLBOUfny.js +1 -0
- package/out/assets/{send-4dhnJGuS.js → send-Bie9hgHI.js} +1 -1
- package/out/assets/{shield-check-Boae7Nc6.js → shield-check-2-h_eEXp.js} +1 -1
- package/out/assets/{smartphone-JXH-zQXB.js → smartphone-CrsocYJo.js} +1 -1
- package/out/assets/trash-2-B9gno5q6.js +1 -0
- package/out/assets/{triangle-alert-B9sMiA9l.js → triangle-alert-BhNtjKm2.js} +1 -1
- package/out/assets/{upload-DVuXJt_o.js → upload-BmufnMFu.js} +1 -1
- package/out/assets/{useAppStore-DOnk608w.js → useAppStore-DNhyhD20.js} +1 -1
- package/out/assets/useChannelMessages-DKlO_2oW.js +4 -0
- package/out/assets/useGameRoom-DLN636mz.js +1 -0
- package/out/assets/{wallet-D9Iw4YWE.js → wallet-D6HZocla.js} +1 -1
- package/out/assets/{wifi-ljEI_ySA.js → wifi-CILQyKk0.js} +1 -1
- package/out/chat/index.html +0 -0
- package/out/chat/join/demo/index.html +0 -0
- package/out/chat/join/index.html +0 -0
- package/out/download/index.html +2 -2
- package/out/game/gandengyan/index.html +0 -0
- package/out/game/index.html +0 -0
- package/out/game/zhajinhua/index.html +0 -0
- package/out/index.html +2 -2
- package/out/note/index.html +0 -0
- package/out/ping/index.html +2 -2
- package/out/profile/index.html +0 -0
- package/out/web3/index.html +0 -0
- package/package.json +1 -1
- package/server/index.js +18 -1
- package/server/src/core/channelVoice.js +164 -0
- package/server/src/core/mostLink.js +30 -13
- package/server/src/http/nodeStatus.js +2 -2
- package/server/src/http/routes/fileRoutes.js +10 -8
- package/server/src/http/uploads.js +53 -15
- package/server/src/index.js +317 -133
- package/server/src/node/config.js +4 -4
- package/server/src/utils/api.js +9 -1
- package/server/src/utils/avatar.js +1 -10
- package/server/src/utils/downloadMessages.js +7 -7
- package/server/src/utils/errors.js +11 -1
- package/server/src/utils/userIdentity.js +0 -1
- package/out/assets/AccountMenu-ptQklqvD.js +0 -1
- package/out/assets/AppTop-Cy0ZOCxO.js +0 -1
- package/out/assets/CopyButton-C8PIDSJZ.js +0 -1
- package/out/assets/FilePreviewOverlay-faaTnmlN.js +0 -1
- package/out/assets/MarketingHeader-CxJ_OVOW.js +0 -1
- package/out/assets/MarketingLayout-CiN5KYcE.js +0 -1
- package/out/assets/OpenSidebarButton-CGhXxQ5Z.js +0 -1
- package/out/assets/arrow-right-CXzRt0Fp.js +0 -1
- package/out/assets/chatJoinInvite-B-ucvYdg.js +0 -1
- package/out/assets/check-iBCQl0yr.js +0 -1
- package/out/assets/chevron-right-D75NRNNs.js +0 -1
- package/out/assets/downloadValidation-UPgpJbKz.js +0 -1
- package/out/assets/format-CR8oUWq6.js +0 -1
- package/out/assets/index-D0Uk9-58.js +0 -31
- package/out/assets/index-FV6RZlVI.css +0 -1
- package/out/assets/index.lazy-B6XDJ8fg.js +0 -1
- package/out/assets/index.lazy-BFdWbm3R.js +0 -1
- package/out/assets/index.lazy-BzIt7fP4.js +0 -1
- package/out/assets/index.lazy-CLkb32nv.js +0 -1
- package/out/assets/index.lazy-CNmqW4-r.js +0 -1
- package/out/assets/index.lazy-Cnkj-gAd.js +0 -3
- package/out/assets/index.lazy-Cx1bp84f.js +0 -5
- package/out/assets/index.lazy-CxLzR674.js +0 -2
- package/out/assets/index.lazy-D4RjTGu-.js +0 -3
- package/out/assets/index.lazy-D9xLqvqW.js +0 -1
- package/out/assets/index.lazy-DNCF6MTJ.js +0 -1
- package/out/assets/index.lazy-MY-mUG5Z.js +0 -1
- package/out/assets/index.lazy-erUKA2rI.js +0 -1
- package/out/assets/index.lazy-qTjgLKhY.js +0 -1
- package/out/assets/key-round-JR8M3aHl.js +0 -1
- package/out/assets/lock-CuwwlXlx.js +0 -1
- package/out/assets/mp-Dv12gDTx.js +0 -1
- package/out/assets/notebook-pen-BzrCXQJE.js +0 -1
- package/out/assets/play-CvWKSMwY.js +0 -1
- package/out/assets/search-HW4oPOBP.js +0 -1
- package/out/assets/trash-2-BLwoM0ME.js +0 -1
- package/out/assets/useChannelMessages-CDvw-f_x.js +0 -4
- package/out/assets/useGameRoom-CS1w4IqP.js +0 -1
package/server/src/index.js
CHANGED
|
@@ -20,9 +20,9 @@ import { Duplex } from 'node:stream'
|
|
|
20
20
|
|
|
21
21
|
import { calculateCid, parseMostLink, buildMostLink } from './core/cid.js'
|
|
22
22
|
import { normalizeChannelAttachment } from './core/channelAttachment.js'
|
|
23
|
+
import { normalizeChannelVoiceEvent } from './core/channelVoice.js'
|
|
23
24
|
import { getCidInfo } from './core/cidTopic.js'
|
|
24
25
|
import {
|
|
25
|
-
CHAT_FILE_ROOT,
|
|
26
26
|
TRANSIENT_CHANNEL_TYPES,
|
|
27
27
|
CHANNEL_DISCOVERY_TIMEOUT,
|
|
28
28
|
CHANNEL_CANDIDATE_TTL,
|
|
@@ -95,7 +95,11 @@ import {
|
|
|
95
95
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
96
96
|
const CHANNEL_PRESENCE_HEARTBEAT_MS = 15 * 1000
|
|
97
97
|
const CHANNEL_PRESENCE_TIMEOUT_MS = 45 * 1000
|
|
98
|
-
const
|
|
98
|
+
const CHANNEL_MEMBER_JOINED_EVENT = 'channel.member.joined'
|
|
99
|
+
|
|
100
|
+
function isChannelHistoryEntry(entry) {
|
|
101
|
+
return entry?.type === 'message' || entry?.type === 'system'
|
|
102
|
+
}
|
|
99
103
|
|
|
100
104
|
function createMemoryDuplexPair() {
|
|
101
105
|
let left
|
|
@@ -203,6 +207,16 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
203
207
|
return this.#getChannelPresenceList(channel.channelKey)
|
|
204
208
|
}
|
|
205
209
|
|
|
210
|
+
sendChannelVoiceEvent(channelKeyInput, input = {}, options = {}) {
|
|
211
|
+
this.#ensureInitialized()
|
|
212
|
+
this.#assertChannelMember(channelKeyInput, options.ownerAddress)
|
|
213
|
+
const channel = this.#resolveChannel(channelKeyInput, options.ownerAddress)
|
|
214
|
+
const event = normalizeChannelVoiceEvent(channel.channelKey, input, options)
|
|
215
|
+
this.#broadcastChannelVoice(event)
|
|
216
|
+
this.emit('channel:voice', event)
|
|
217
|
+
return event
|
|
218
|
+
}
|
|
219
|
+
|
|
206
220
|
joinChannelPresence(channelKeyInput, options = {}) {
|
|
207
221
|
this.#ensureInitialized()
|
|
208
222
|
this.#assertChannelMember(channelKeyInput, options.ownerAddress)
|
|
@@ -279,6 +293,42 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
279
293
|
return this.#pruneStaleChannelPresence()
|
|
280
294
|
}
|
|
281
295
|
|
|
296
|
+
#isClosedSessionError(err) {
|
|
297
|
+
if (!err || typeof err !== 'object') return false
|
|
298
|
+
const code = String(err.code || '')
|
|
299
|
+
const message = String(err.message || '')
|
|
300
|
+
return (
|
|
301
|
+
code === 'SESSION_CLOSED' ||
|
|
302
|
+
message.includes('SESSION_CLOSED') ||
|
|
303
|
+
message.includes('closed session')
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async #reopenChannelLocalWriter(channel) {
|
|
308
|
+
const channelKey = channel.channelKey
|
|
309
|
+
const localKeyHex =
|
|
310
|
+
this.#channelLocalCoreKey.get(channelKey) || channel.localWriterCoreKey
|
|
311
|
+
const coresMap = this.#channelCores.get(channelKey)
|
|
312
|
+
const staleCore = localKeyHex && coresMap ? coresMap.get(localKeyHex) : null
|
|
313
|
+
|
|
314
|
+
if (staleCore) {
|
|
315
|
+
coresMap.delete(localKeyHex)
|
|
316
|
+
await staleCore.close().catch(() => {})
|
|
317
|
+
}
|
|
318
|
+
this.#channelLocalCoreKey.delete(channelKey)
|
|
319
|
+
|
|
320
|
+
await this.#openChannelRuntime(channel)
|
|
321
|
+
|
|
322
|
+
const reopenedKeyHex = this.#channelLocalCoreKey.get(channelKey)
|
|
323
|
+
const reopenedCore = reopenedKeyHex
|
|
324
|
+
? this.#channelCores.get(channelKey)?.get(reopenedKeyHex)
|
|
325
|
+
: null
|
|
326
|
+
if (!reopenedCore) {
|
|
327
|
+
throw new Error('频道未初始化或无可写 core')
|
|
328
|
+
}
|
|
329
|
+
return reopenedCore
|
|
330
|
+
}
|
|
331
|
+
|
|
282
332
|
/**
|
|
283
333
|
* 初始化引擎 — 必须在调用其他方法之前调用
|
|
284
334
|
*/
|
|
@@ -610,29 +660,38 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
610
660
|
const publishedBucket = this.#getPublishedBucket(ownerAddress, true)
|
|
611
661
|
// 检查相同内容是否已存在
|
|
612
662
|
const existingIndex = publishedBucket.findIndex(f => f.cid === cidString)
|
|
663
|
+
const repairingMissingContent = existingIndex !== -1
|
|
613
664
|
if (existingIndex !== -1) {
|
|
614
665
|
const existing = publishedBucket[existingIndex]
|
|
615
|
-
await this.#
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
this.#upsertHolding({
|
|
620
|
-
cid: cidString,
|
|
621
|
-
fileName: existing.fileName,
|
|
622
|
-
size: fileSize,
|
|
623
|
-
driveName: name,
|
|
624
|
-
source: 'published',
|
|
666
|
+
const existingContent = await this.#getLocalCidContent(cidString, {
|
|
667
|
+
ownerAddress,
|
|
668
|
+
public: true,
|
|
669
|
+
allowHoldingFallback: true,
|
|
625
670
|
})
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
671
|
+
if (existingContent) {
|
|
672
|
+
await this.#joinCidTopicInternal(cidString, {
|
|
673
|
+
server: true,
|
|
674
|
+
client: false,
|
|
675
|
+
})
|
|
676
|
+
this.#upsertHolding({
|
|
677
|
+
cid: cidString,
|
|
678
|
+
fileName: existing.fileName,
|
|
679
|
+
size: fileSize,
|
|
680
|
+
driveName: name,
|
|
681
|
+
source: 'published',
|
|
682
|
+
})
|
|
683
|
+
return {
|
|
684
|
+
cid: cidString,
|
|
685
|
+
link: buildMostLink(cidString, existing.fileName),
|
|
686
|
+
fileName: existing.fileName,
|
|
687
|
+
alreadyExists: true,
|
|
688
|
+
}
|
|
631
689
|
}
|
|
632
690
|
}
|
|
633
691
|
|
|
634
692
|
this.#assertDisplayNameAvailable(safeFileName, {
|
|
635
693
|
ownerAddress,
|
|
694
|
+
excludeCid: repairingMissingContent ? cidString : undefined,
|
|
636
695
|
})
|
|
637
696
|
|
|
638
697
|
// 获取或创建该 CID 对应的 drive
|
|
@@ -653,40 +712,17 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
653
712
|
|
|
654
713
|
// Hyperdrive 中用 CID 作为 key 存储(解耦目录结构)
|
|
655
714
|
const driveKey = '/' + cidString
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
let offset = 0
|
|
661
|
-
const waitForDrain = () =>
|
|
662
|
-
new Promise(resolve => ws.once('drain', resolve))
|
|
663
|
-
|
|
664
|
-
try {
|
|
665
|
-
while (offset < content.length) {
|
|
666
|
-
const chunk = content.slice(offset, offset + FILE_WRITE_CHUNK_SIZE)
|
|
667
|
-
const canContinue = ws.write(chunk)
|
|
668
|
-
offset += chunk.length
|
|
669
|
-
if (!canContinue && offset < content.length) {
|
|
670
|
-
await waitForDrain()
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
ws.end()
|
|
674
|
-
await new Promise((resolve, reject) => {
|
|
675
|
-
ws.on('finish', resolve)
|
|
676
|
-
ws.on('error', reject)
|
|
677
|
-
})
|
|
678
|
-
} catch (err) {
|
|
679
|
-
ws.destroy()
|
|
715
|
+
try {
|
|
716
|
+
await this.#writeDriveFile(drive, driveKey, content, cleanPath)
|
|
717
|
+
} catch (err) {
|
|
718
|
+
if (!this.#isClosedSessionError(err)) {
|
|
680
719
|
throw err
|
|
681
720
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
rs.pipe(ws)
|
|
686
|
-
ws.on('finish', resolve)
|
|
687
|
-
ws.on('error', reject)
|
|
688
|
-
rs.on('error', reject)
|
|
721
|
+
drive = await this.#reopenDrive(name, {
|
|
722
|
+
server: true,
|
|
723
|
+
client: false,
|
|
689
724
|
})
|
|
725
|
+
await this.#writeDriveFile(drive, driveKey, content, cleanPath)
|
|
690
726
|
}
|
|
691
727
|
|
|
692
728
|
// 存储 displayName(用户看到的文件夹路径),不存储 drivePath
|
|
@@ -701,7 +737,11 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
701
737
|
starred: false,
|
|
702
738
|
syncUpdatedAt: now,
|
|
703
739
|
}
|
|
704
|
-
|
|
740
|
+
if (repairingMissingContent) {
|
|
741
|
+
publishedBucket[existingIndex] = fileRecord
|
|
742
|
+
} else {
|
|
743
|
+
publishedBucket.push(fileRecord)
|
|
744
|
+
}
|
|
705
745
|
this.#savePublishedMetadata()
|
|
706
746
|
this.#upsertHolding({
|
|
707
747
|
cid: cidString,
|
|
@@ -728,7 +768,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
728
768
|
* @param {object} [options] - 下载选项
|
|
729
769
|
* @param {number} [options.timeout] - 等待 P2P 内容的超时时间(毫秒)
|
|
730
770
|
* @param {number} [options.streamReadTimeout] - 下载流无进度超时时间(毫秒)
|
|
731
|
-
* @returns {Promise<{ taskId: string, fileName: string, savedPath
|
|
771
|
+
* @returns {Promise<{ taskId: string, fileName: string, savedPath?: string, localAvailable?: boolean, alreadyExists?: boolean }>}
|
|
732
772
|
*/
|
|
733
773
|
async downloadFile(link, taskId = null, options = {}) {
|
|
734
774
|
this.#ensureInitialized()
|
|
@@ -767,12 +807,38 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
767
807
|
})
|
|
768
808
|
if (localContent) {
|
|
769
809
|
const existingFile = localContent.fileRecord
|
|
810
|
+
const publishedBucket = this.#getPublishedBucket(ownerAddress, true)
|
|
811
|
+
const existingIndex = publishedBucket.findIndex(
|
|
812
|
+
f => f.cid === cidString
|
|
813
|
+
)
|
|
814
|
+
const alreadyInOwnerLibrary = existingIndex !== -1
|
|
770
815
|
console.log(
|
|
771
816
|
`[MostBox] CID content already exists locally: ${cidString}`
|
|
772
817
|
)
|
|
773
818
|
const existingHolding = this.#holdings.find(
|
|
774
819
|
item => item.cid === cidString
|
|
775
820
|
)
|
|
821
|
+
const localSize =
|
|
822
|
+
Number(existingHolding?.size) ||
|
|
823
|
+
(Number.isFinite(localContent.size) ? localContent.size : 0)
|
|
824
|
+
if (!alreadyInOwnerLibrary) {
|
|
825
|
+
this.#assertDisplayNameAvailable(linkFileName, {
|
|
826
|
+
ownerAddress,
|
|
827
|
+
excludeCid: cidString,
|
|
828
|
+
})
|
|
829
|
+
const syncUpdatedAt = Date.now()
|
|
830
|
+
publishedBucket.push({
|
|
831
|
+
fileName: linkFileName,
|
|
832
|
+
cid: cidString,
|
|
833
|
+
driveName: existingFile?.driveName || name,
|
|
834
|
+
size: localSize,
|
|
835
|
+
source: 'downloaded',
|
|
836
|
+
publishedAt: new Date(syncUpdatedAt).toISOString(),
|
|
837
|
+
starred: false,
|
|
838
|
+
syncUpdatedAt,
|
|
839
|
+
})
|
|
840
|
+
this.#savePublishedMetadata()
|
|
841
|
+
}
|
|
776
842
|
await this.#joinCidTopicInternal(cidString, {
|
|
777
843
|
server: true,
|
|
778
844
|
client: false,
|
|
@@ -780,17 +846,21 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
780
846
|
this.#upsertHolding({
|
|
781
847
|
cid: cidString,
|
|
782
848
|
fileName:
|
|
783
|
-
existingHolding?.fileName ||
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
849
|
+
existingHolding?.fileName ||
|
|
850
|
+
(alreadyInOwnerLibrary ? existingFile?.fileName : linkFileName) ||
|
|
851
|
+
linkFileName,
|
|
852
|
+
size: localSize,
|
|
787
853
|
driveName: existingFile?.driveName || name,
|
|
788
|
-
source:
|
|
854
|
+
source:
|
|
855
|
+
existingHolding?.source ||
|
|
856
|
+
(alreadyInOwnerLibrary ? existingFile?.source : 'downloaded') ||
|
|
857
|
+
'downloaded',
|
|
789
858
|
})
|
|
790
859
|
return {
|
|
791
860
|
taskId,
|
|
792
861
|
fileName: linkFileName,
|
|
793
|
-
|
|
862
|
+
localAvailable: true,
|
|
863
|
+
alreadyExists: alreadyInOwnerLibrary,
|
|
794
864
|
}
|
|
795
865
|
}
|
|
796
866
|
|
|
@@ -1120,7 +1190,10 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1120
1190
|
cid: parsed.cid,
|
|
1121
1191
|
fileName: sanitizeFilename(parsed.fileName),
|
|
1122
1192
|
size: localContent.size,
|
|
1123
|
-
|
|
1193
|
+
localAvailable: true,
|
|
1194
|
+
alreadyExists: this.#getPublishedBucket(ownerAddress).some(
|
|
1195
|
+
f => f.cid === parsed.cid
|
|
1196
|
+
),
|
|
1124
1197
|
}
|
|
1125
1198
|
}
|
|
1126
1199
|
|
|
@@ -1129,7 +1202,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1129
1202
|
* @param {string} link - most:// 链接
|
|
1130
1203
|
* @param {object} [options] - 检测选项
|
|
1131
1204
|
* @param {number} [options.timeout] - 等待 P2P 内容的超时时间(毫秒)
|
|
1132
|
-
* @returns {Promise<{ available: boolean, cid: string, fileName: string, size: number|null, alreadyExists?: boolean }>}
|
|
1205
|
+
* @returns {Promise<{ available: boolean, cid: string, fileName: string, size: number|null, localAvailable?: boolean, alreadyExists?: boolean }>}
|
|
1133
1206
|
*/
|
|
1134
1207
|
async checkDownloadAvailability(link, options = {}) {
|
|
1135
1208
|
this.#ensureInitialized()
|
|
@@ -1158,7 +1231,10 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1158
1231
|
cid: cidString,
|
|
1159
1232
|
fileName: sanitizeFilename(parsed.fileName),
|
|
1160
1233
|
size: localContent.size,
|
|
1161
|
-
|
|
1234
|
+
localAvailable: true,
|
|
1235
|
+
alreadyExists: this.#getPublishedBucket(ownerAddress).some(
|
|
1236
|
+
f => f.cid === cidString
|
|
1237
|
+
),
|
|
1162
1238
|
}
|
|
1163
1239
|
}
|
|
1164
1240
|
|
|
@@ -2164,7 +2240,10 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2164
2240
|
async #hasLocalDriveContent(drive, key) {
|
|
2165
2241
|
try {
|
|
2166
2242
|
return await drive.has(key)
|
|
2167
|
-
} catch {
|
|
2243
|
+
} catch (err) {
|
|
2244
|
+
if (this.#isClosedSessionError(err)) {
|
|
2245
|
+
throw err
|
|
2246
|
+
}
|
|
2168
2247
|
return false
|
|
2169
2248
|
}
|
|
2170
2249
|
}
|
|
@@ -2184,42 +2263,53 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2184
2263
|
}
|
|
2185
2264
|
const holding = this.#holdings.find(item => item.cid === cid)
|
|
2186
2265
|
const { driveName } = this.#getCidInfo(cid)
|
|
2187
|
-
|
|
2266
|
+
let drive = await this.#getOrCreateDrive(
|
|
2188
2267
|
fileRecord?.driveName || holding?.driveName || driveName,
|
|
2189
2268
|
{ server: true, client: false }
|
|
2190
2269
|
)
|
|
2191
2270
|
const driveKey = '/' + cid
|
|
2192
2271
|
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2272
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
2273
|
+
try {
|
|
2274
|
+
const entry = await drive.entry(driveKey, { wait: false })
|
|
2275
|
+
if (!entry?.value?.blob) {
|
|
2276
|
+
return null
|
|
2277
|
+
}
|
|
2278
|
+
const hasContent = await this.#hasLocalDriveContent(drive, driveKey)
|
|
2279
|
+
if (!hasContent) {
|
|
2280
|
+
return null
|
|
2281
|
+
}
|
|
2202
2282
|
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
size,
|
|
2212
|
-
fileRecord: fileRecord || {
|
|
2213
|
-
cid,
|
|
2214
|
-
fileName: holding?.fileName || cid,
|
|
2215
|
-
driveName: holding?.driveName || driveName,
|
|
2283
|
+
const size =
|
|
2284
|
+
Number(entry.value.blob.byteLength) ||
|
|
2285
|
+
Number(fileRecord?.size) ||
|
|
2286
|
+
Number(holding?.size) ||
|
|
2287
|
+
0
|
|
2288
|
+
return {
|
|
2289
|
+
drive,
|
|
2290
|
+
entry,
|
|
2216
2291
|
size,
|
|
2217
|
-
|
|
2218
|
-
|
|
2292
|
+
fileRecord: fileRecord || {
|
|
2293
|
+
cid,
|
|
2294
|
+
fileName: holding?.fileName || cid,
|
|
2295
|
+
driveName: holding?.driveName || driveName,
|
|
2296
|
+
size,
|
|
2297
|
+
ownerAddress,
|
|
2298
|
+
},
|
|
2299
|
+
}
|
|
2300
|
+
} catch (err) {
|
|
2301
|
+
if (attempt === 0 && this.#isClosedSessionError(err)) {
|
|
2302
|
+
drive = await this.#reopenDrive(
|
|
2303
|
+
fileRecord?.driveName || holding?.driveName || driveName,
|
|
2304
|
+
{ server: true, client: false }
|
|
2305
|
+
)
|
|
2306
|
+
continue
|
|
2307
|
+
}
|
|
2308
|
+
return null
|
|
2219
2309
|
}
|
|
2220
|
-
} catch {
|
|
2221
|
-
return null
|
|
2222
2310
|
}
|
|
2311
|
+
|
|
2312
|
+
return null
|
|
2223
2313
|
}
|
|
2224
2314
|
|
|
2225
2315
|
// --- 频道管理 ---
|
|
@@ -2567,7 +2657,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2567
2657
|
for (let i = 0; i < core.length; i++) {
|
|
2568
2658
|
try {
|
|
2569
2659
|
const entry = await core.get(i)
|
|
2570
|
-
if (entry
|
|
2660
|
+
if (isChannelHistoryEntry(entry)) {
|
|
2571
2661
|
allMessages.push({
|
|
2572
2662
|
...entry,
|
|
2573
2663
|
_coreKey: coreKeyHex,
|
|
@@ -2582,7 +2672,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2582
2672
|
|
|
2583
2673
|
const seen = new Set()
|
|
2584
2674
|
const unique = allMessages.filter(m => {
|
|
2585
|
-
const key = `${m._coreKey}:${m.author}:${m.timestamp}:${m.content}`
|
|
2675
|
+
const key = `${m._coreKey}:${m.type}:${m.event || ''}:${m.author}:${m.timestamp}:${m.content}`
|
|
2586
2676
|
if (seen.has(key)) return false
|
|
2587
2677
|
seen.add(key)
|
|
2588
2678
|
return true
|
|
@@ -2649,7 +2739,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2649
2739
|
|
|
2650
2740
|
const normalizedAvatar = normalizeChannelAvatar(options.avatar)
|
|
2651
2741
|
const message = {
|
|
2652
|
-
type: 'message',
|
|
2742
|
+
type: options.type === 'system' ? 'system' : 'message',
|
|
2653
2743
|
author,
|
|
2654
2744
|
authorName: normalizeChannelDisplayName(
|
|
2655
2745
|
authorName,
|
|
@@ -2666,8 +2756,20 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2666
2756
|
if (attachment) {
|
|
2667
2757
|
message.attachment = attachment
|
|
2668
2758
|
}
|
|
2759
|
+
if (message.type === 'system') {
|
|
2760
|
+
const event = String(options.event || '').trim()
|
|
2761
|
+
if (event) message.event = event
|
|
2762
|
+
}
|
|
2669
2763
|
|
|
2670
|
-
|
|
2764
|
+
try {
|
|
2765
|
+
await core.append(message)
|
|
2766
|
+
} catch (err) {
|
|
2767
|
+
if (!this.#isClosedSessionError(err)) {
|
|
2768
|
+
throw err
|
|
2769
|
+
}
|
|
2770
|
+
const reopenedCore = await this.#reopenChannelLocalWriter(channel)
|
|
2771
|
+
await reopenedCore.append(message)
|
|
2772
|
+
}
|
|
2671
2773
|
if (channel) {
|
|
2672
2774
|
channel.lastMessageAt = new Date(message.timestamp).toISOString()
|
|
2673
2775
|
this.#saveChannelsMetadata()
|
|
@@ -3158,11 +3260,13 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3158
3260
|
|
|
3159
3261
|
await this.sendMessage(
|
|
3160
3262
|
channel.channelKey,
|
|
3161
|
-
|
|
3263
|
+
CHANNEL_MEMBER_JOINED_EVENT,
|
|
3162
3264
|
ownerAddress,
|
|
3163
3265
|
normalizeChannelDisplayName(options.displayName, ownerAddress),
|
|
3164
3266
|
{
|
|
3165
3267
|
ownerAddress,
|
|
3268
|
+
type: 'system',
|
|
3269
|
+
event: CHANNEL_MEMBER_JOINED_EVENT,
|
|
3166
3270
|
...(Object.prototype.hasOwnProperty.call(options, 'avatar')
|
|
3167
3271
|
? { avatar: options.avatar }
|
|
3168
3272
|
: {}),
|
|
@@ -3517,7 +3621,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3517
3621
|
for (let i = 0; i < core.length; i++) {
|
|
3518
3622
|
try {
|
|
3519
3623
|
const entry = await core.get(i)
|
|
3520
|
-
if (entry
|
|
3624
|
+
if (isChannelHistoryEntry(entry)) {
|
|
3521
3625
|
maxTimestamp = Math.max(
|
|
3522
3626
|
maxTimestamp,
|
|
3523
3627
|
Number(entry.timestamp) || 0
|
|
@@ -3555,42 +3659,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3555
3659
|
baseMessage = { ...baseMessage, avatar }
|
|
3556
3660
|
}
|
|
3557
3661
|
}
|
|
3558
|
-
|
|
3559
|
-
if (!attachment?.cid || !attachment.fileName) {
|
|
3560
|
-
return baseMessage
|
|
3561
|
-
}
|
|
3562
|
-
|
|
3563
|
-
const oldFileName = sanitizeFilename(String(attachment.fileName))
|
|
3564
|
-
const channelPathName = channel?.channelId || channelKey
|
|
3565
|
-
const channelPrefix = `${CHAT_FILE_ROOT}/${channelPathName}/`
|
|
3566
|
-
const fileName = oldFileName.startsWith(channelPrefix)
|
|
3567
|
-
? oldFileName
|
|
3568
|
-
: `${channelPrefix}${getPathBaseName(oldFileName)}`
|
|
3569
|
-
const link = buildMostLink(attachment.cid, fileName)
|
|
3570
|
-
const content =
|
|
3571
|
-
typeof baseMessage.content === 'string' &&
|
|
3572
|
-
(baseMessage.content === attachment.link ||
|
|
3573
|
-
parseMostLink(baseMessage.content).cid === attachment.cid)
|
|
3574
|
-
? link
|
|
3575
|
-
: baseMessage.content
|
|
3576
|
-
|
|
3577
|
-
if (
|
|
3578
|
-
fileName === attachment.fileName &&
|
|
3579
|
-
link === attachment.link &&
|
|
3580
|
-
content === baseMessage.content
|
|
3581
|
-
) {
|
|
3582
|
-
return baseMessage
|
|
3583
|
-
}
|
|
3584
|
-
|
|
3585
|
-
return {
|
|
3586
|
-
...baseMessage,
|
|
3587
|
-
content,
|
|
3588
|
-
attachment: {
|
|
3589
|
-
...attachment,
|
|
3590
|
-
fileName,
|
|
3591
|
-
link,
|
|
3592
|
-
},
|
|
3593
|
-
}
|
|
3662
|
+
return baseMessage
|
|
3594
3663
|
}
|
|
3595
3664
|
|
|
3596
3665
|
#getCidInfo(cid) {
|
|
@@ -4064,6 +4133,19 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4064
4133
|
if (!this.#holdings.some(current => current.cid === holding.cid)) {
|
|
4065
4134
|
return
|
|
4066
4135
|
}
|
|
4136
|
+
const localContent = await this.#getLocalCidContent(holding.cid, {
|
|
4137
|
+
public: true,
|
|
4138
|
+
allowHoldingFallback: true,
|
|
4139
|
+
})
|
|
4140
|
+
if (!localContent) {
|
|
4141
|
+
this.#setSeedState(holding.cid, {
|
|
4142
|
+
status: 'error',
|
|
4143
|
+
topic: holding.topic,
|
|
4144
|
+
driveName: holding.driveName,
|
|
4145
|
+
error: 'Local CID content missing',
|
|
4146
|
+
})
|
|
4147
|
+
return
|
|
4148
|
+
}
|
|
4067
4149
|
await this.#joinCidTopicInternal(holding.cid, {
|
|
4068
4150
|
server: true,
|
|
4069
4151
|
client: false,
|
|
@@ -4291,6 +4373,60 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4291
4373
|
return drive
|
|
4292
4374
|
}
|
|
4293
4375
|
|
|
4376
|
+
async #reopenDrive(name, options = { server: true, client: false }) {
|
|
4377
|
+
const staleDrive = this.#drives.get(name)
|
|
4378
|
+
if (staleDrive) {
|
|
4379
|
+
this.#drives.delete(name)
|
|
4380
|
+
await staleDrive.close().catch(() => {})
|
|
4381
|
+
}
|
|
4382
|
+
this.#drivePromises.delete(name)
|
|
4383
|
+
return this.#getOrCreateDrive(name, options)
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
async #writeDriveFile(drive, driveKey, content, cleanPath) {
|
|
4387
|
+
const ws = drive.createWriteStream(driveKey)
|
|
4388
|
+
|
|
4389
|
+
if (Buffer.isBuffer(content)) {
|
|
4390
|
+
let offset = 0
|
|
4391
|
+
const waitForDrain = () =>
|
|
4392
|
+
new Promise(resolve => ws.once('drain', resolve))
|
|
4393
|
+
|
|
4394
|
+
try {
|
|
4395
|
+
while (offset < content.length) {
|
|
4396
|
+
const chunk = content.slice(offset, offset + FILE_WRITE_CHUNK_SIZE)
|
|
4397
|
+
const canContinue = ws.write(chunk)
|
|
4398
|
+
offset += chunk.length
|
|
4399
|
+
if (!canContinue && offset < content.length) {
|
|
4400
|
+
await waitForDrain()
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
ws.end()
|
|
4404
|
+
await new Promise((resolve, reject) => {
|
|
4405
|
+
ws.on('finish', resolve)
|
|
4406
|
+
ws.on('error', reject)
|
|
4407
|
+
})
|
|
4408
|
+
} catch (err) {
|
|
4409
|
+
ws.destroy()
|
|
4410
|
+
throw err
|
|
4411
|
+
}
|
|
4412
|
+
return
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4415
|
+
const rs = fs.createReadStream(cleanPath)
|
|
4416
|
+
try {
|
|
4417
|
+
await new Promise((resolve, reject) => {
|
|
4418
|
+
rs.pipe(ws)
|
|
4419
|
+
ws.on('finish', resolve)
|
|
4420
|
+
ws.on('error', reject)
|
|
4421
|
+
rs.on('error', reject)
|
|
4422
|
+
})
|
|
4423
|
+
} catch (err) {
|
|
4424
|
+
ws.destroy()
|
|
4425
|
+
rs.destroy()
|
|
4426
|
+
throw err
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4294
4430
|
#getOwnerKey(ownerAddress) {
|
|
4295
4431
|
return getOwnerBucketKey(ownerAddress)
|
|
4296
4432
|
}
|
|
@@ -4549,7 +4685,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4549
4685
|
err.message
|
|
4550
4686
|
)
|
|
4551
4687
|
}
|
|
4552
|
-
return
|
|
4688
|
+
return {}
|
|
4553
4689
|
}
|
|
4554
4690
|
|
|
4555
4691
|
#savePublishedMetadata() {
|
|
@@ -4603,7 +4739,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4603
4739
|
err.message
|
|
4604
4740
|
)
|
|
4605
4741
|
}
|
|
4606
|
-
return
|
|
4742
|
+
return {}
|
|
4607
4743
|
}
|
|
4608
4744
|
|
|
4609
4745
|
#saveTrashMetadata() {
|
|
@@ -4760,7 +4896,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4760
4896
|
for (let i = lastCoreLength; i < core.length; i++) {
|
|
4761
4897
|
try {
|
|
4762
4898
|
const entry = await core.get(i)
|
|
4763
|
-
if (entry
|
|
4899
|
+
if (isChannelHistoryEntry(entry)) {
|
|
4764
4900
|
const channel = this.#channels.find(
|
|
4765
4901
|
c => c.channelKey === channelKey
|
|
4766
4902
|
)
|
|
@@ -4913,6 +5049,32 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4913
5049
|
}
|
|
4914
5050
|
}
|
|
4915
5051
|
|
|
5052
|
+
#sendChannelVoice(stream, event) {
|
|
5053
|
+
if (!stream || stream.destroyed || stream.writableEnded || !event) {
|
|
5054
|
+
this.#channelStreams.delete(stream)
|
|
5055
|
+
return false
|
|
5056
|
+
}
|
|
5057
|
+
try {
|
|
5058
|
+
stream.write(
|
|
5059
|
+
`${JSON.stringify({
|
|
5060
|
+
type: 'channel-voice',
|
|
5061
|
+
peerId: this.getNodeId(),
|
|
5062
|
+
...event,
|
|
5063
|
+
})}\n`
|
|
5064
|
+
)
|
|
5065
|
+
return true
|
|
5066
|
+
} catch {
|
|
5067
|
+
this.#channelStreams.delete(stream)
|
|
5068
|
+
return false
|
|
5069
|
+
}
|
|
5070
|
+
}
|
|
5071
|
+
|
|
5072
|
+
#broadcastChannelVoice(event) {
|
|
5073
|
+
for (const stream of [...this.#channelStreams]) {
|
|
5074
|
+
this.#sendChannelVoice(stream, event)
|
|
5075
|
+
}
|
|
5076
|
+
}
|
|
5077
|
+
|
|
4916
5078
|
async #processChannelHelloMessage(msg) {
|
|
4917
5079
|
if (msg.type !== 'channel-hello') return null
|
|
4918
5080
|
|
|
@@ -5033,6 +5195,26 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
5033
5195
|
return peerId
|
|
5034
5196
|
}
|
|
5035
5197
|
|
|
5198
|
+
#processChannelVoiceMessage(msg) {
|
|
5199
|
+
if (msg.type !== 'channel-voice') return null
|
|
5200
|
+
const peerId = String(msg.peerId || '').trim()
|
|
5201
|
+
if (!peerId || peerId === this.getNodeId()) return null
|
|
5202
|
+
const channelId = normalizeChannelId(msg.channelId || msg.channelKey)
|
|
5203
|
+
const channelKey = buildChannelKey(channelId)
|
|
5204
|
+
const localChannel = this.#channels.find(
|
|
5205
|
+
channel => channel.channelKey === channelKey
|
|
5206
|
+
)
|
|
5207
|
+
if (!localChannel) return peerId
|
|
5208
|
+
|
|
5209
|
+
try {
|
|
5210
|
+
const event = normalizeChannelVoiceEvent(channelKey, msg, {
|
|
5211
|
+
timestamp: msg.timestamp,
|
|
5212
|
+
})
|
|
5213
|
+
this.emit('channel:voice', event)
|
|
5214
|
+
} catch {}
|
|
5215
|
+
return peerId
|
|
5216
|
+
}
|
|
5217
|
+
|
|
5036
5218
|
async #handleChannelConnection(conn) {
|
|
5037
5219
|
const stream = conn
|
|
5038
5220
|
let connectedPeerId = null
|
|
@@ -5055,7 +5237,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
5055
5237
|
const peerId =
|
|
5056
5238
|
message.type === 'channel-presence'
|
|
5057
5239
|
? this.#processChannelPresenceMessage(message)
|
|
5058
|
-
:
|
|
5240
|
+
: message.type === 'channel-voice'
|
|
5241
|
+
? this.#processChannelVoiceMessage(message)
|
|
5242
|
+
: await this.#processChannelHelloMessage(message)
|
|
5059
5243
|
if (peerId) connectedPeerId = peerId
|
|
5060
5244
|
} catch (err) {
|
|
5061
5245
|
console.warn(`[MostBox] Failed to process channel data:`, err.message)
|