most-box 0.0.2 → 0.0.4
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/README.md +26 -0
- package/cli.js +2 -2
- package/out/404/index.html +15 -0
- package/out/404.html +15 -0
- package/out/__next.__PAGE__.txt +9 -0
- package/out/__next._full.txt +18 -0
- package/out/__next._head.txt +5 -0
- package/out/__next._index.txt +6 -0
- package/out/__next._tree.txt +2 -0
- package/out/_next/static/0h4f4QFk_KC9FlSRfQACk/_buildManifest.js +11 -0
- package/out/_next/static/0h4f4QFk_KC9FlSRfQACk/_clientMiddlewareManifest.js +1 -0
- package/out/_next/static/0h4f4QFk_KC9FlSRfQACk/_ssgManifest.js +1 -0
- package/out/_next/static/chunks/00l-yd3t8dvwz.js +5 -0
- package/out/_next/static/chunks/03k8t3tgym~8~.js +1 -0
- package/out/_next/static/chunks/03~yq9q893hmn.js +1 -0
- package/out/_next/static/chunks/09vfh8lfuacc0.css +1 -0
- package/out/_next/static/chunks/0bogtdbh.dcu1.js +1 -0
- package/out/_next/static/chunks/0dbhjjzl8qfwv.js +1 -0
- package/out/_next/static/chunks/0f73psqhr8dre.css +1 -0
- package/out/_next/static/chunks/0fbi7z4_.4j1j.js +1 -0
- package/out/_next/static/chunks/0ht900cau6_ur.js +31 -0
- package/out/_next/static/chunks/0ohm.ia-4ec60.js +1 -0
- package/out/_next/static/chunks/0u5ydb-f0.vxl.js +1 -0
- package/out/_next/static/chunks/14t2m1on-s5v~.js +1 -0
- package/out/_next/static/chunks/turbopack-076ce9exut_h3.js +1 -0
- package/out/_not-found/__next._full.txt +16 -0
- package/out/_not-found/__next._head.txt +5 -0
- package/out/_not-found/__next._index.txt +6 -0
- package/out/_not-found/__next._not-found/__PAGE__.txt +5 -0
- package/out/_not-found/__next._not-found.txt +5 -0
- package/out/_not-found/__next._tree.txt +2 -0
- package/out/_not-found/index.html +15 -0
- package/out/_not-found/index.txt +16 -0
- package/out/app.css +1535 -0
- package/out/bundle.js +107 -0
- package/out/bundle.js.map +7 -0
- package/out/chat/__next._full.txt +19 -0
- package/out/chat/__next._head.txt +5 -0
- package/out/chat/__next._index.txt +6 -0
- package/out/chat/__next._tree.txt +3 -0
- package/out/chat/__next.chat/__PAGE__.txt +9 -0
- package/out/chat/__next.chat.txt +5 -0
- package/out/chat/index.html +15 -0
- package/out/chat/index.txt +19 -0
- package/out/chat-page.js +112 -0
- package/out/chat.css +378 -0
- package/out/favicon.ico +0 -0
- package/out/index.html +15 -0
- package/out/index.js +148 -0
- package/out/index.txt +18 -0
- package/package.json +11 -6
- package/public/app.css +20 -4
- package/public/bundle.js +1 -1
- package/public/bundle.js.map +7 -0
- package/public/chat-page.js +112 -0
- package/public/chat.css +378 -0
- package/public/index.js +148 -0
- package/server.js +188 -6
- package/src/config.js +12 -1
- package/src/core/cid.js +7 -3
- package/src/index.js +475 -7
- package/src/utils/api.js +6 -0
- package/build.mjs +0 -40
- package/public/app.jsx +0 -1543
- package/public/bundle.css +0 -1
- package/public/error-boundary.jsx +0 -50
- package/public/index.html +0 -16
- package/public/index.jsx +0 -20
package/src/index.js
CHANGED
|
@@ -12,7 +12,9 @@ import EventEmitter from 'eventemitter3'
|
|
|
12
12
|
import Hyperswarm from 'hyperswarm'
|
|
13
13
|
import Corestore from 'corestore'
|
|
14
14
|
import Hyperdrive from 'hyperdrive'
|
|
15
|
+
import Hypercore from 'hypercore'
|
|
15
16
|
import b4a from 'b4a'
|
|
17
|
+
import crypto from 'node:crypto'
|
|
16
18
|
import { CID } from 'multiformats/cid'
|
|
17
19
|
import fs from 'node:fs'
|
|
18
20
|
import path from 'node:path'
|
|
@@ -20,7 +22,7 @@ import path from 'node:path'
|
|
|
20
22
|
import { calculateCid, parseMostLink } from './core/cid.js'
|
|
21
23
|
import { sanitizeFilename, validateAndSanitizePath, validateFileSize, checkDirectoryWritable, formatFileSize } from './utils/security.js'
|
|
22
24
|
import { ValidationError, PathSecurityError, FileSizeError, PeerNotFoundError, IntegrityError, PermissionError, EngineNotInitializedError } from './utils/errors.js'
|
|
23
|
-
import { GLOBAL_SHARED_SEED_STRING, MAX_FILE_SIZE, CONNECTION_TIMEOUT, DOWNLOAD_TIMEOUT, SWARM_BOOTSTRAP, MAX_PEERS, SWARM_KEEP_ALIVE_INTERVAL, SWARM_RANDOM_PUNCH_INTERVAL, DRIVE_ENTRY_TIMEOUT, DRIVE_SYNC_TIMEOUT, STREAM_READ_TIMEOUT, DOWNLOAD_POLL_INTERVAL, PROGRESS_THROTTLE, DEFAULT_READ_LIMIT } from './config.js'
|
|
25
|
+
import { GLOBAL_SHARED_SEED_STRING, MAX_FILE_SIZE, CONNECTION_TIMEOUT, DOWNLOAD_TIMEOUT, SWARM_BOOTSTRAP, MAX_PEERS, SWARM_KEEP_ALIVE_INTERVAL, SWARM_RANDOM_PUNCH_INTERVAL, DRIVE_ENTRY_TIMEOUT, DRIVE_SYNC_TIMEOUT, STREAM_READ_TIMEOUT, DOWNLOAD_POLL_INTERVAL, PROGRESS_THROTTLE, DEFAULT_READ_LIMIT, CHANNEL_NAME_MIN_LENGTH, CHANNEL_NAME_MAX_LENGTH, CHANNEL_NAME_REGEX, CHANNEL_NAME_PREFIX, CHANNEL_TOPIC_STRING, CHANNEL_MESSAGE_LIMIT, MAX_MESSAGE_LENGTH } from './config.js'
|
|
24
26
|
|
|
25
27
|
export class MostBoxEngine extends EventEmitter {
|
|
26
28
|
#store = null
|
|
@@ -33,6 +35,11 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
33
35
|
#activeDownloads = new Map()
|
|
34
36
|
#drivePromises = new Map()
|
|
35
37
|
|
|
38
|
+
#channels = []
|
|
39
|
+
#channelCores = new Map()
|
|
40
|
+
#channelDiscoveries = new Map()
|
|
41
|
+
#channelPeers = new Map()
|
|
42
|
+
|
|
36
43
|
/**
|
|
37
44
|
* 创建新的 MostBoxEngine 实例
|
|
38
45
|
* @param {object} options - 配置选项
|
|
@@ -114,16 +121,14 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
114
121
|
})
|
|
115
122
|
|
|
116
123
|
this.#swarm.on('connection', (conn, info) => {
|
|
117
|
-
console.log(`[MostBox] New peer connection established`)
|
|
118
124
|
conn.on('error', (err) => {
|
|
119
125
|
if (err.code === 'SSL_ERROR' || err.message?.includes('handshake')) {
|
|
120
|
-
console.warn('[MostBox] Connection warning:', err.message)
|
|
121
126
|
return
|
|
122
127
|
}
|
|
123
|
-
console.error('[MostBox] Connection error:', err.message)
|
|
124
128
|
})
|
|
125
129
|
|
|
126
130
|
this.#store.replicate(conn)
|
|
131
|
+
this.#handleChannelConnection(conn).catch(() => {})
|
|
127
132
|
this.emit('connection', conn)
|
|
128
133
|
})
|
|
129
134
|
|
|
@@ -133,6 +138,41 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
133
138
|
this.#trashFiles = this.#loadTrashMetadata()
|
|
134
139
|
console.log(`[MostBox] Loaded ${this.#trashFiles.length} trash files`)
|
|
135
140
|
|
|
141
|
+
this.#channels = this.#loadChannelsMetadata()
|
|
142
|
+
console.log(`[MostBox] Loaded ${this.#channels.length} channels`)
|
|
143
|
+
|
|
144
|
+
for (const channel of this.#channels) {
|
|
145
|
+
try {
|
|
146
|
+
const ns = this.#store.namespace(`channel-${channel.name}`)
|
|
147
|
+
const core = ns.get({ key: b4a.from(channel.coreKey, 'hex'), valueEncoding: 'json' })
|
|
148
|
+
await core.ready()
|
|
149
|
+
this.#channelCores.set(channel.name, core)
|
|
150
|
+
this.#channelPeers.set(channel.name, new Map())
|
|
151
|
+
|
|
152
|
+
let lastCoreLength = core.length
|
|
153
|
+
core.on('append', async () => {
|
|
154
|
+
if (core.length > lastCoreLength) {
|
|
155
|
+
for (let i = lastCoreLength; i < core.length; i++) {
|
|
156
|
+
try {
|
|
157
|
+
const entry = await core.get(i)
|
|
158
|
+
if (entry && entry.type === 'message') {
|
|
159
|
+
this.emit('channel:message', { channel: channel.name, message: entry })
|
|
160
|
+
}
|
|
161
|
+
} catch {}
|
|
162
|
+
}
|
|
163
|
+
lastCoreLength = core.length
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const discoveryKey = b4a.from(channel.discoveryKey, 'hex')
|
|
168
|
+
const discovery = this.#swarm.join(discoveryKey, { server: true, client: true })
|
|
169
|
+
this.#channelDiscoveries.set(channel.name, discovery)
|
|
170
|
+
console.log(`[MostBox] Rejoined channel: ${channel.name}`)
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.warn(`[MostBox] Failed to rejoin channel ${channel.name}:`, err.message)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
136
176
|
this.#initialized = true
|
|
137
177
|
console.log(`[MostBox] Engine initialized successfully`)
|
|
138
178
|
this.emit('ready')
|
|
@@ -158,6 +198,14 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
158
198
|
await Promise.allSettled([...this.#drives.values()].map(d => d.close()))
|
|
159
199
|
this.#drives.clear()
|
|
160
200
|
|
|
201
|
+
for (const core of this.#channelCores.values()) {
|
|
202
|
+
try { await core.close() } catch {}
|
|
203
|
+
}
|
|
204
|
+
this.#channelCores.clear()
|
|
205
|
+
this.#channelDiscoveries.clear()
|
|
206
|
+
this.#channelPeers.clear()
|
|
207
|
+
this.#channels = []
|
|
208
|
+
|
|
161
209
|
if (this.#swarm) {
|
|
162
210
|
await this.#swarm.destroy()
|
|
163
211
|
this.#swarm = null
|
|
@@ -316,7 +364,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
316
364
|
|
|
317
365
|
const result = {
|
|
318
366
|
cid: cidString,
|
|
319
|
-
link: `most://${cidString}`,
|
|
367
|
+
link: `most://${cidString}?filename=${encodeURIComponent(safeFileName)}`,
|
|
320
368
|
fileName: safeFileName
|
|
321
369
|
}
|
|
322
370
|
|
|
@@ -358,6 +406,8 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
358
406
|
}
|
|
359
407
|
}
|
|
360
408
|
|
|
409
|
+
const linkFileName = parsed.fileName
|
|
410
|
+
|
|
361
411
|
const parsedCid = CID.parse(cidString)
|
|
362
412
|
const hashHex = b4a.toString(parsedCid.multihash.digest, 'hex')
|
|
363
413
|
|
|
@@ -422,8 +472,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
422
472
|
// 下载文件
|
|
423
473
|
for (const entry of entries) {
|
|
424
474
|
const cleanKey = entry.key.replace(/^[\/\\]/, '')
|
|
425
|
-
|
|
426
|
-
|
|
475
|
+
const sanitizedFileName = linkFileName
|
|
476
|
+
? sanitizeFilename(linkFileName)
|
|
477
|
+
: sanitizeFilename(cleanKey)
|
|
427
478
|
|
|
428
479
|
let totalBytes = 0
|
|
429
480
|
try {
|
|
@@ -971,6 +1022,328 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
971
1022
|
return drive
|
|
972
1023
|
}
|
|
973
1024
|
|
|
1025
|
+
// --- 频道管理 ---
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* 创建或加入频道
|
|
1029
|
+
* @param {string} name - 频道名
|
|
1030
|
+
* @param {string} [type='personal'] - 频道类型
|
|
1031
|
+
* @returns {Promise<{ name: string, key: string }>}
|
|
1032
|
+
*/
|
|
1033
|
+
async createChannel(name, type = 'personal') {
|
|
1034
|
+
this.#ensureInitialized()
|
|
1035
|
+
|
|
1036
|
+
if (!CHANNEL_NAME_REGEX.test(name)) {
|
|
1037
|
+
throw new Error('频道名只能包含字母、数字、下划线和连字符')
|
|
1038
|
+
}
|
|
1039
|
+
if (name.length < CHANNEL_NAME_MIN_LENGTH) {
|
|
1040
|
+
throw new Error(`频道名至少 ${CHANNEL_NAME_MIN_LENGTH} 个字符`)
|
|
1041
|
+
}
|
|
1042
|
+
if (name.length > CHANNEL_NAME_MAX_LENGTH) {
|
|
1043
|
+
throw new Error(`频道名最多 ${CHANNEL_NAME_MAX_LENGTH} 个字符`)
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const existing = this.#channels.find(c => c.name === name)
|
|
1047
|
+
if (existing) {
|
|
1048
|
+
return { name: existing.name, key: existing.coreKey }
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const ns = this.#store.namespace(`channel-${name}`)
|
|
1052
|
+
const core = ns.get({ name: 'messages', valueEncoding: 'json' })
|
|
1053
|
+
await core.ready()
|
|
1054
|
+
|
|
1055
|
+
const discoveryKey = this.#generateChannelDiscoveryKey(name)
|
|
1056
|
+
const discovery = this.#swarm.join(discoveryKey, { server: true, client: true })
|
|
1057
|
+
await discovery.flushed()
|
|
1058
|
+
|
|
1059
|
+
let lastCoreLength = core.length
|
|
1060
|
+
core.on('append', async () => {
|
|
1061
|
+
if (core.length > lastCoreLength) {
|
|
1062
|
+
|
|
1063
|
+
for (let i = lastCoreLength; i < core.length; i++) {
|
|
1064
|
+
try {
|
|
1065
|
+
const entry = await core.get(i)
|
|
1066
|
+
if (entry && entry.type === 'message') {
|
|
1067
|
+
|
|
1068
|
+
this.emit('channel:message', { channel: name, message: entry })
|
|
1069
|
+
}
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
lastCoreLength = core.length
|
|
1075
|
+
}
|
|
1076
|
+
})
|
|
1077
|
+
|
|
1078
|
+
const channelInfo = {
|
|
1079
|
+
name,
|
|
1080
|
+
discoveryKey: b4a.toString(discoveryKey, 'hex'),
|
|
1081
|
+
coreKey: b4a.toString(core.key, 'hex'),
|
|
1082
|
+
createdAt: new Date().toISOString(),
|
|
1083
|
+
type
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
this.#channels.push(channelInfo)
|
|
1087
|
+
this.#channelCores.set(name, core)
|
|
1088
|
+
this.#channelPeers.set(name, new Map())
|
|
1089
|
+
this.#saveChannelsMetadata()
|
|
1090
|
+
|
|
1091
|
+
console.log(`[MostBox] Channel created: ${name}`)
|
|
1092
|
+
this.emit('channel:joined', { name, key: channelInfo.coreKey })
|
|
1093
|
+
|
|
1094
|
+
return { name, key: channelInfo.coreKey }
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* 加入已有频道(通过频道名和 coreKey)
|
|
1099
|
+
* @param {string} name - 频道名
|
|
1100
|
+
* @param {string} [coreKey] - 频道的 coreKey(加入他人创建的频道时必填)
|
|
1101
|
+
* @returns {Promise<{ name: string, key: string }>}
|
|
1102
|
+
*/
|
|
1103
|
+
async joinChannel(name, coreKey = null) {
|
|
1104
|
+
this.#ensureInitialized()
|
|
1105
|
+
|
|
1106
|
+
const existing = this.#channels.find(c => c.name === name)
|
|
1107
|
+
if (existing) {
|
|
1108
|
+
return { name: existing.name, key: existing.coreKey }
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (!coreKey) {
|
|
1112
|
+
throw new Error('加入已有频道需要提供 coreKey')
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const ns = this.#store.namespace(`channel-${name}`)
|
|
1116
|
+
const core = ns.get({ key: b4a.from(coreKey, 'hex'), valueEncoding: 'json' })
|
|
1117
|
+
await core.ready()
|
|
1118
|
+
|
|
1119
|
+
const discoveryKey = this.#generateChannelDiscoveryKey(name)
|
|
1120
|
+
const discovery = this.#swarm.join(discoveryKey, { server: true, client: true })
|
|
1121
|
+
await discovery.flushed()
|
|
1122
|
+
|
|
1123
|
+
let lastCoreLength = core.length
|
|
1124
|
+
core.on('append', async () => {
|
|
1125
|
+
if (core.length > lastCoreLength) {
|
|
1126
|
+
|
|
1127
|
+
for (let i = lastCoreLength; i < core.length; i++) {
|
|
1128
|
+
try {
|
|
1129
|
+
const entry = await core.get(i)
|
|
1130
|
+
if (entry && entry.type === 'message') {
|
|
1131
|
+
|
|
1132
|
+
this.emit('channel:message', { channel: name, message: entry })
|
|
1133
|
+
}
|
|
1134
|
+
} catch (err) {
|
|
1135
|
+
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
lastCoreLength = core.length
|
|
1139
|
+
}
|
|
1140
|
+
})
|
|
1141
|
+
|
|
1142
|
+
const channelInfo = {
|
|
1143
|
+
name,
|
|
1144
|
+
discoveryKey: b4a.toString(discoveryKey, 'hex'),
|
|
1145
|
+
coreKey,
|
|
1146
|
+
createdAt: new Date().toISOString(),
|
|
1147
|
+
type: 'group'
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
this.#channels.push(channelInfo)
|
|
1151
|
+
this.#channelCores.set(name, core)
|
|
1152
|
+
this.#channelPeers.set(name, new Map())
|
|
1153
|
+
this.#saveChannelsMetadata()
|
|
1154
|
+
|
|
1155
|
+
console.log(`[MostBox] Joined channel: ${name}`)
|
|
1156
|
+
this.emit('channel:joined', { name, key: coreKey })
|
|
1157
|
+
|
|
1158
|
+
return { name, key: coreKey }
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* 离开频道
|
|
1163
|
+
* @param {string} name - 频道名
|
|
1164
|
+
* @returns {Promise<string[]>} 剩余频道列表
|
|
1165
|
+
*/
|
|
1166
|
+
async leaveChannel(name) {
|
|
1167
|
+
this.#ensureInitialized()
|
|
1168
|
+
|
|
1169
|
+
const index = this.#channels.findIndex(c => c.name === name)
|
|
1170
|
+
if (index === -1) {
|
|
1171
|
+
throw new Error('频道不存在')
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const channel = this.#channels[index]
|
|
1175
|
+
|
|
1176
|
+
const discovery = this.#channelDiscoveries.get(name)
|
|
1177
|
+
if (discovery) {
|
|
1178
|
+
try {
|
|
1179
|
+
await this.#swarm.leave(b4a.from(channel.discoveryKey, 'hex'))
|
|
1180
|
+
} catch {}
|
|
1181
|
+
this.#channelDiscoveries.delete(name)
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const core = this.#channelCores.get(name)
|
|
1185
|
+
if (core) {
|
|
1186
|
+
try {
|
|
1187
|
+
await core.close()
|
|
1188
|
+
} catch {}
|
|
1189
|
+
this.#channelCores.delete(name)
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
this.#channelPeers.delete(name)
|
|
1193
|
+
this.#channels.splice(index, 1)
|
|
1194
|
+
this.#saveChannelsMetadata()
|
|
1195
|
+
|
|
1196
|
+
console.log(`[MostBox] Left channel: ${name}`)
|
|
1197
|
+
this.emit('channel:left', { name })
|
|
1198
|
+
|
|
1199
|
+
return this.listChannels()
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* 列出所有频道
|
|
1204
|
+
* @returns {Array<{ name: string, coreKey: string, createdAt: string, type: string, peerCount: number }>}
|
|
1205
|
+
*/
|
|
1206
|
+
listChannels() {
|
|
1207
|
+
this.#ensureInitialized()
|
|
1208
|
+
|
|
1209
|
+
return this.#channels.map(c => ({
|
|
1210
|
+
name: c.name,
|
|
1211
|
+
coreKey: c.coreKey,
|
|
1212
|
+
createdAt: c.createdAt,
|
|
1213
|
+
type: c.type,
|
|
1214
|
+
peerCount: (this.#channelPeers.get(c.name) || new Map()).size
|
|
1215
|
+
}))
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* 获取频道消息
|
|
1220
|
+
* @param {string} name - 频道名
|
|
1221
|
+
* @param {object} [options] - 选项
|
|
1222
|
+
* @param {number} [options.limit=100] - 消息数量
|
|
1223
|
+
* @param {number} [options.offset=0] - 偏移量
|
|
1224
|
+
* @returns {Promise<Array>}
|
|
1225
|
+
*/
|
|
1226
|
+
async getChannelMessages(name, options = {}) {
|
|
1227
|
+
this.#ensureInitialized()
|
|
1228
|
+
|
|
1229
|
+
const { limit = CHANNEL_MESSAGE_LIMIT, offset = 0 } = options
|
|
1230
|
+
|
|
1231
|
+
const core = this.#channelCores.get(name)
|
|
1232
|
+
if (!core) {
|
|
1233
|
+
throw new Error('频道未初始化')
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const messages = []
|
|
1237
|
+
const total = core.length
|
|
1238
|
+
const start = Math.max(0, total - offset - limit)
|
|
1239
|
+
const end = total - offset
|
|
1240
|
+
|
|
1241
|
+
for (let i = start; i < end; i++) {
|
|
1242
|
+
try {
|
|
1243
|
+
const entry = await core.get(i)
|
|
1244
|
+
messages.push(entry)
|
|
1245
|
+
} catch {
|
|
1246
|
+
break
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
return messages
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* 发送消息到频道
|
|
1255
|
+
* @param {string} name - 频道名
|
|
1256
|
+
* @param {string} content - 消息内容
|
|
1257
|
+
* @param {string} [authorName] - 作者名(可选,默认使用 displayName)
|
|
1258
|
+
* @returns {Promise<object>}
|
|
1259
|
+
*/
|
|
1260
|
+
async sendMessage(name, content, authorName = null) {
|
|
1261
|
+
this.#ensureInitialized()
|
|
1262
|
+
|
|
1263
|
+
const core = this.#channelCores.get(name)
|
|
1264
|
+
if (!core) {
|
|
1265
|
+
throw new Error('频道未初始化')
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
if (!content || !content.trim()) {
|
|
1269
|
+
throw new Error('消息内容不能为空')
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const trimmed = content.trim()
|
|
1273
|
+
if (trimmed.length > MAX_MESSAGE_LENGTH) {
|
|
1274
|
+
throw new Error(`消息内容不能超过 ${MAX_MESSAGE_LENGTH} 字符`)
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
const displayName = this.getNodeId().slice(0, 4)
|
|
1278
|
+
|
|
1279
|
+
const message = {
|
|
1280
|
+
type: 'message',
|
|
1281
|
+
author: this.getNodeId(),
|
|
1282
|
+
authorName: displayName,
|
|
1283
|
+
content: trimmed,
|
|
1284
|
+
timestamp: Date.now()
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
await core.append(message)
|
|
1288
|
+
|
|
1289
|
+
this.emit('channel:message', { channel: name, message })
|
|
1290
|
+
|
|
1291
|
+
return message
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* 获取频道内在线用户
|
|
1296
|
+
* @param {string} name - 频道名
|
|
1297
|
+
* @returns {Array<{ peerId: string, authorName: string, lastSeen: number }>}
|
|
1298
|
+
*/
|
|
1299
|
+
getChannelPeers(name) {
|
|
1300
|
+
this.#ensureInitialized()
|
|
1301
|
+
|
|
1302
|
+
const peers = this.#channelPeers.get(name)
|
|
1303
|
+
if (!peers) {
|
|
1304
|
+
throw new Error('频道未初始化')
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
return [...peers.values()].map(p => ({
|
|
1308
|
+
peerId: p.peerId,
|
|
1309
|
+
authorName: p.authorName,
|
|
1310
|
+
lastSeen: p.lastSeen
|
|
1311
|
+
}))
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* 获取显示名
|
|
1316
|
+
* @returns {string|null}
|
|
1317
|
+
*/
|
|
1318
|
+
getDisplayName() {
|
|
1319
|
+
try {
|
|
1320
|
+
const configPath = this.#getConfigPath()
|
|
1321
|
+
if (fs.existsSync(configPath)) {
|
|
1322
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
1323
|
+
return config.displayName || null
|
|
1324
|
+
}
|
|
1325
|
+
} catch {}
|
|
1326
|
+
return null
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* 设置显示名
|
|
1331
|
+
* @param {string} name - 显示名
|
|
1332
|
+
*/
|
|
1333
|
+
setDisplayName(name) {
|
|
1334
|
+
try {
|
|
1335
|
+
const configPath = this.#getConfigPath()
|
|
1336
|
+
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf-8')) : {}
|
|
1337
|
+
config.displayName = name.trim()
|
|
1338
|
+
const tmpPath = configPath + '.tmp'
|
|
1339
|
+
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), 'utf-8')
|
|
1340
|
+
fs.renameSync(tmpPath, configPath)
|
|
1341
|
+
return true
|
|
1342
|
+
} catch {
|
|
1343
|
+
return false
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
974
1347
|
// --- 私有方法 ---
|
|
975
1348
|
|
|
976
1349
|
#ensureInitialized() {
|
|
@@ -1073,6 +1446,101 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1073
1446
|
}
|
|
1074
1447
|
}
|
|
1075
1448
|
|
|
1449
|
+
#getChannelsMetadataPath() {
|
|
1450
|
+
return path.join(this.#options.dataPath, 'channels.json')
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
#getConfigPath() {
|
|
1454
|
+
return path.join(this.#options.dataPath, 'channel-config.json')
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
#loadChannelsMetadata() {
|
|
1458
|
+
try {
|
|
1459
|
+
const metadataPath = this.#getChannelsMetadataPath()
|
|
1460
|
+
if (fs.existsSync(metadataPath)) {
|
|
1461
|
+
const data = fs.readFileSync(metadataPath, 'utf-8')
|
|
1462
|
+
return JSON.parse(data)
|
|
1463
|
+
}
|
|
1464
|
+
} catch (err) {
|
|
1465
|
+
console.warn('Failed to load channels metadata, using empty list:', err.message)
|
|
1466
|
+
}
|
|
1467
|
+
return []
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
#saveChannelsMetadata() {
|
|
1471
|
+
try {
|
|
1472
|
+
const metadataPath = this.#getChannelsMetadataPath()
|
|
1473
|
+
this.#atomicWrite(metadataPath, JSON.stringify(this.#channels, null, 2))
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
console.error('Failed to save channels metadata:', err.message)
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
#generateChannelDiscoveryKey(name) {
|
|
1480
|
+
const hash = crypto.createHash('sha256')
|
|
1481
|
+
.update(`${CHANNEL_NAME_PREFIX}${name}`)
|
|
1482
|
+
.digest()
|
|
1483
|
+
return hash
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
async #handleChannelConnection(conn) {
|
|
1487
|
+
const stream = conn
|
|
1488
|
+
let connectedPeerId = null
|
|
1489
|
+
let connectedAuthorName = null
|
|
1490
|
+
|
|
1491
|
+
const helloMessage = JSON.stringify({
|
|
1492
|
+
type: 'channel-hello',
|
|
1493
|
+
peerId: this.getNodeId(),
|
|
1494
|
+
authorName: this.getNodeId().slice(0, 4),
|
|
1495
|
+
channels: this.#channels.map(c => c.name)
|
|
1496
|
+
})
|
|
1497
|
+
|
|
1498
|
+
try {
|
|
1499
|
+
stream.write(helloMessage)
|
|
1500
|
+
} catch {
|
|
1501
|
+
return
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
stream.on('data', async (data) => {
|
|
1505
|
+
try {
|
|
1506
|
+
const msg = JSON.parse(data.toString())
|
|
1507
|
+
if (msg.type === 'channel-hello') {
|
|
1508
|
+
connectedPeerId = msg.peerId
|
|
1509
|
+
connectedAuthorName = msg.authorName
|
|
1510
|
+
|
|
1511
|
+
const theirChannels = new Set(msg.channels || [])
|
|
1512
|
+
for (const [name, peers] of this.#channelPeers) {
|
|
1513
|
+
if (theirChannels.has(name)) {
|
|
1514
|
+
peers.set(msg.peerId, {
|
|
1515
|
+
peerId: msg.peerId,
|
|
1516
|
+
authorName: msg.authorName,
|
|
1517
|
+
lastSeen: Date.now()
|
|
1518
|
+
})
|
|
1519
|
+
const core = this.#channelCores.get(name)
|
|
1520
|
+
if (core) {
|
|
1521
|
+
const ns = this.#store.namespace(`channel-${name}`)
|
|
1522
|
+
ns.replicate(conn).catch(() => {})
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
this.emit('channel:peer:online', { peerId: msg.peerId, authorName: msg.authorName })
|
|
1527
|
+
}
|
|
1528
|
+
} catch {}
|
|
1529
|
+
})
|
|
1530
|
+
|
|
1531
|
+
stream.on('close', () => {
|
|
1532
|
+
if (connectedPeerId) {
|
|
1533
|
+
for (const [name, peers] of this.#channelPeers) {
|
|
1534
|
+
if (peers.has(connectedPeerId)) {
|
|
1535
|
+
const peer = peers.get(connectedPeerId)
|
|
1536
|
+
peers.delete(connectedPeerId)
|
|
1537
|
+
this.emit('channel:peer:offline', { peerId: connectedPeerId, authorName: peer?.authorName })
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
})
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1076
1544
|
/**
|
|
1077
1545
|
* 等待驱动器内容从对等节点或本地可用
|
|
1078
1546
|
* @param {Hyperdrive} drive - 要检查的驱动器
|
package/src/utils/api.js
ADDED
package/build.mjs
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import * as esbuild from 'esbuild'
|
|
2
|
-
import fs from 'fs'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import { fileURLToPath } from 'url'
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
-
const publicDir = path.join(__dirname, 'public')
|
|
8
|
-
const outFile = path.join(publicDir, 'bundle.js')
|
|
9
|
-
|
|
10
|
-
const isWatch = process.argv.includes('--watch')
|
|
11
|
-
const isDev = process.argv.includes('--dev')
|
|
12
|
-
|
|
13
|
-
const buildOptions = {
|
|
14
|
-
entryPoints: [path.join(publicDir, 'index.jsx')],
|
|
15
|
-
bundle: true,
|
|
16
|
-
outfile: outFile,
|
|
17
|
-
format: 'esm',
|
|
18
|
-
jsx: 'automatic',
|
|
19
|
-
jsxImportSource: 'react',
|
|
20
|
-
loader: {
|
|
21
|
-
'.js': 'jsx',
|
|
22
|
-
'.jsx': 'jsx',
|
|
23
|
-
},
|
|
24
|
-
define: {
|
|
25
|
-
'process.env.NODE_ENV': isDev ? '"development"' : '"production"'
|
|
26
|
-
},
|
|
27
|
-
minify: !isDev,
|
|
28
|
-
sourcemap: isDev,
|
|
29
|
-
target: ['es2020'],
|
|
30
|
-
logLevel: 'info',
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (isWatch) {
|
|
34
|
-
const ctx = await esbuild.context(buildOptions)
|
|
35
|
-
await ctx.watch()
|
|
36
|
-
console.log('[Build] Watching for changes...')
|
|
37
|
-
} else {
|
|
38
|
-
await esbuild.build(buildOptions)
|
|
39
|
-
console.log('[Build] Done.')
|
|
40
|
-
}
|