most-box 0.2.6 → 0.2.8

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 (116) hide show
  1. package/README.md +15 -1
  2. package/electron/main.js +30 -420
  3. package/electron/preload.js +1 -16
  4. package/electron/updateChecker.js +12 -30
  5. package/electron/updateChecker.test.js +14 -55
  6. package/out/admin/index.html +0 -0
  7. package/out/app/index.html +0 -0
  8. package/out/assets/{FilePreviewOverlay-DqH7uKb7.js → FilePreviewOverlay-D8H61qbp.js} +1 -1
  9. package/out/assets/LanguageToggle-CSQtRZ3A.js +1 -0
  10. package/out/assets/MarketingHeader-CrYsQsQ5.js +1 -0
  11. package/out/assets/MarketingLayout-CQurMOSD.js +1 -0
  12. package/out/assets/{MarketingThemeToggle-BLuQ_uE-.js → MarketingThemeToggle-CIKP8x9W.js} +1 -1
  13. package/out/assets/{MilkdownEditor-f2vRIY21.js → MilkdownEditor-DrsyaR4h.js} +1 -1
  14. package/out/assets/{OpenSidebarButton-BKX4B_yG.js → OpenSidebarButton-DnEgqP0S.js} +1 -1
  15. package/out/assets/{SidebarAccount-D6wj_ijc.js → SidebarAccount-B4OAl0as.js} +1 -1
  16. package/out/assets/SidebarHomeLink-BdsUM4Kt.js +1 -0
  17. package/out/assets/arrow-right-BU9c_SL4.js +1 -0
  18. package/out/assets/{circle-alert-Dyc-4wje.js → circle-alert-iuHzciph.js} +1 -1
  19. package/out/assets/{copy-D8HSH5Ki.js → copy-DEzFUzeX.js} +1 -1
  20. package/out/assets/{download-Q6jKMPIG.js → download-jEPn2njL.js} +1 -1
  21. package/out/assets/{downloadValidation-B-w8D_0W.js → downloadValidation-DbVOwdmC.js} +1 -1
  22. package/out/assets/{external-link-GQcZwDTT.js → external-link-BFaoZeK0.js} +1 -1
  23. package/out/assets/{file-text-CVl5SZPJ.js → file-text-DPA1riJ_.js} +1 -1
  24. package/out/assets/{folder-BtIdk-YN.js → folder-Cfgc70_F.js} +1 -1
  25. package/out/assets/{hard-drive-Cew_LD0N.js → hard-drive-Dar_AZEq.js} +1 -1
  26. package/out/assets/i18n-NjvOSuV6.js +1 -0
  27. package/out/assets/index-BZ-f0CZd.js +31 -0
  28. package/out/assets/index-Cde4g_dk.css +1 -0
  29. package/out/assets/index.lazy-B7AbDiMU.js +3 -0
  30. package/out/assets/{index.lazy-DujygWms.js → index.lazy-BsciBHf_.js} +1 -1
  31. package/out/assets/index.lazy-BxoGfWEW.js +1 -0
  32. package/out/assets/index.lazy-CKbrHko5.js +1 -0
  33. package/out/assets/index.lazy-CgKPe02l.js +1 -0
  34. package/out/assets/index.lazy-Citucq1G.js +1 -0
  35. package/out/assets/index.lazy-CvKEcWlH.js +2 -0
  36. package/out/assets/{index.lazy-WXDn5r_l.js → index.lazy-DB9Ht9LJ.js} +1 -1
  37. package/out/assets/index.lazy-DVeqmqaA.js +1 -0
  38. package/out/assets/index.lazy-hF807bh0.js +1 -0
  39. package/out/assets/index.lazy-oFaoa4qT.js +1 -0
  40. package/out/assets/index.lazy-rfgjFZNH.js +1 -0
  41. package/out/assets/index.lazy-zpQXhgmn.js +2 -0
  42. package/out/assets/{key-round-D1zfZzvP.js → key-round-DNcpSuMw.js} +1 -1
  43. package/out/assets/{loader-BSclU8WE.js → loader-BXQtdens.js} +1 -1
  44. package/out/assets/{lock-C9VP0ezG.js → lock-CLIE8cct.js} +1 -1
  45. package/out/assets/{message-square-Dg3YjjmO.js → message-square-Dp09WY1I.js} +1 -1
  46. package/out/assets/{mp-FQvFEcYM.js → mp-B6kdA94c.js} +1 -1
  47. package/out/assets/{music-BeCPak70.js → music-B19Tg557.js} +1 -1
  48. package/out/assets/{notebook-pen-DO1CJiqn.js → notebook-pen-C8TlK5sB.js} +1 -1
  49. package/out/assets/play-B-BjsTZz.js +1 -0
  50. package/out/assets/plus-4hygj-1Z.js +1 -0
  51. package/out/assets/{save-BVeMjaW1.js → save-BjrbPwhJ.js} +1 -1
  52. package/out/assets/search-DSkiWdvX.js +1 -0
  53. package/out/assets/{send-C1J-eGNs.js → send-BL4d5iOt.js} +1 -1
  54. package/out/assets/{shield-check-CZYDVJoh.js → shield-check-DzDuWO8r.js} +1 -1
  55. package/out/assets/smartphone-tRRFFO0a.js +1 -0
  56. package/out/assets/{trash-2-C_LtCiNh.js → trash-2-V12tJ8lI.js} +1 -1
  57. package/out/assets/triangle-alert-DGWc8ZHu.js +1 -0
  58. package/out/assets/{ui-PnUvY9zL.js → ui-BpDGD3IR.js} +2 -2
  59. package/out/assets/{upload-DgxxldtV.js → upload-CoCptKya.js} +1 -1
  60. package/out/assets/{useChannelMessages-CZHmH5n-.js → useChannelMessages-B6RMaNDH.js} +1 -1
  61. package/out/assets/useGameRoom-CRJL0kcm.js +1 -0
  62. package/out/assets/userProfile-DBkH4s29.js +1 -0
  63. package/out/assets/{wallet-DBTWBi6K.js → wallet-T7ONZ3Kg.js} +1 -1
  64. package/out/assets/{wifi-pP6SJLcu.js → wifi-Bgo8eHY0.js} +1 -1
  65. package/out/chat/index.html +0 -0
  66. package/out/chat/join/index.html +0 -0
  67. package/out/download/index.html +2 -2
  68. package/out/game/gandengyan/index.html +0 -0
  69. package/out/game/index.html +0 -0
  70. package/out/game/zhajinhua/index.html +0 -0
  71. package/out/index.html +2 -2
  72. package/out/note/index.html +0 -0
  73. package/out/ping/index.html +2 -2
  74. package/out/profile/index.html +0 -0
  75. package/out/web3/index.html +0 -0
  76. package/package.json +5 -8
  77. package/server/src/core/channelIdentity.js +3 -5
  78. package/server/src/core/gameRoom.js +18 -43
  79. package/server/src/core/ownerMetadata.js +3 -4
  80. package/server/src/core/releaseManifest.js +91 -0
  81. package/server/src/core/shared.js +50 -0
  82. package/server/src/core/zhajinhua.js +12 -33
  83. package/server/src/games/gandengyan.js +7 -14
  84. package/server/src/http/nodeStatus.js +0 -6
  85. package/server/src/http/routes/channelRoutes.js +1 -13
  86. package/server/src/http/routes/nodeRoutes.js +1 -1
  87. package/server/src/index.js +44 -298
  88. package/server/src/utils/api.js +47 -0
  89. package/server/src/utils/auth.js +2 -5
  90. package/server/src/utils/dateTime.js +1 -3
  91. package/out/assets/LanguageToggle-BoBNesNU.js +0 -1
  92. package/out/assets/MarketingHeader-Derv67Vs.js +0 -1
  93. package/out/assets/MarketingLayout-fc2mymJm.js +0 -1
  94. package/out/assets/SidebarHomeLink-ByOHwlH8.js +0 -1
  95. package/out/assets/arrow-right-BCi9KXDX.js +0 -1
  96. package/out/assets/cloud-DfaFxhLj.js +0 -1
  97. package/out/assets/i18n--maTEG0d.js +0 -1
  98. package/out/assets/index-B2-kpfu5.css +0 -1
  99. package/out/assets/index-Cg4kEvx8.js +0 -31
  100. package/out/assets/index.lazy-BAq__8ON.js +0 -2
  101. package/out/assets/index.lazy-BPh8D_H9.js +0 -1
  102. package/out/assets/index.lazy-Bn4kDToA.js +0 -1
  103. package/out/assets/index.lazy-BqHgW5fE.js +0 -1
  104. package/out/assets/index.lazy-CLYRM_kh.js +0 -1
  105. package/out/assets/index.lazy-CMSCQucz.js +0 -1
  106. package/out/assets/index.lazy-Cut4XkPH.js +0 -1
  107. package/out/assets/index.lazy-DKOSJyaQ.js +0 -2
  108. package/out/assets/index.lazy-DjPbRI_L.js +0 -1
  109. package/out/assets/index.lazy-DxAGUwlf.js +0 -3
  110. package/out/assets/index.lazy-ghLI7p1x.js +0 -1
  111. package/out/assets/play-CnhsLC5C.js +0 -1
  112. package/out/assets/plus-BpjwXzbO.js +0 -1
  113. package/out/assets/search-CsAnnJyH.js +0 -1
  114. package/out/assets/triangle-alert-DOwc-41Y.js +0 -1
  115. package/out/assets/useGameRoom-B8Ds_PKe.js +0 -1
  116. package/out/assets/userProfile-CRGZ8PUF.js +0 -1
@@ -93,6 +93,7 @@ import {
93
93
  } from './config.js'
94
94
 
95
95
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
96
+ const CHANNEL_WELCOME_MESSAGE = '我加入了群聊'
96
97
 
97
98
  function createMemoryDuplexPair() {
98
99
  let left
@@ -1939,290 +1940,6 @@ export class MostBoxEngine extends EventEmitter {
1939
1940
  return result
1940
1941
  }
1941
1942
 
1942
- /**
1943
- * 按 CID 将内容下载到指定路径,不写入用户文件列表。
1944
- * 用于桌面客户端更新等内部缓存场景。
1945
- * @param {object} input - 下载参数
1946
- * @param {string} input.cid - 文件 CID
1947
- * @param {string} input.targetPath - 目标文件路径
1948
- * @param {string} [input.fileName] - 持有记录展示名
1949
- * @param {string} [input.taskId] - 下载任务 ID
1950
- * @param {number} [input.timeout] - 等待 P2P 内容的超时时间
1951
- * @param {number} [input.streamReadTimeout] - 下载流无进度超时时间
1952
- * @param {boolean} [input.overwrite=true] - 是否覆盖已存在目标文件
1953
- * @param {string} [input.source='update'] - holding 来源
1954
- */
1955
- async downloadCidToPath(input = {}) {
1956
- this.#ensureInitialized()
1957
-
1958
- const cid = input.cid
1959
- const targetPath = input.targetPath
1960
- if (!cid) throw new ValidationError('cid is required')
1961
- if (!targetPath) throw new ValidationError('targetPath is required')
1962
-
1963
- this.#getCidInfo(cid)
1964
- const taskId =
1965
- input.taskId || `cid_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
1966
- const taskState = { aborted: false, readStream: null, writeStream: null }
1967
- const timeout = input.timeout || this.#options.downloadTimeout
1968
- const streamReadTimeout =
1969
- input.streamReadTimeout ?? STREAM_READ_TIMEOUT
1970
- const fileName = sanitizeFilename(input.fileName || path.basename(targetPath))
1971
- const overwrite = input.overwrite !== false
1972
- const resolvedTargetPath = path.resolve(targetPath)
1973
- const targetDir = path.dirname(resolvedTargetPath)
1974
- const tempPath = `${resolvedTargetPath}.part`
1975
-
1976
- this.#activeDownloads.set(taskId, taskState)
1977
-
1978
- try {
1979
- const writableCheck = await checkDirectoryWritable(targetDir)
1980
- if (!writableCheck.writable) {
1981
- throw new PermissionError(writableCheck.error)
1982
- }
1983
- if (!overwrite && fs.existsSync(resolvedTargetPath)) {
1984
- throw new ConflictError(`File already exists: ${resolvedTargetPath}`)
1985
- }
1986
- fs.rmSync(tempPath, { force: true })
1987
-
1988
- const { driveName } = this.#getCidInfo(cid)
1989
- let drive = this.#drives.get(driveName)
1990
- if (!drive) {
1991
- drive = await this.#getOrCreateDrive(driveName, {
1992
- server: true,
1993
- client: true,
1994
- })
1995
- this.emit('download:status', { taskId, status: 'connecting' })
1996
- }
1997
-
1998
- await this.#joinCidTopicInternal(cid, {
1999
- server: false,
2000
- client: true,
2001
- })
2002
-
2003
- if (taskState.aborted) throw new Error('Download cancelled')
2004
- this.emit('download:status', { taskId, status: 'finding-peers' })
2005
-
2006
- const driveKey = '/' + cid
2007
- const entry = await this.#waitForDriveEntry(
2008
- drive,
2009
- driveKey,
2010
- timeout,
2011
- taskId,
2012
- taskState
2013
- )
2014
-
2015
- if (!entry) {
2016
- throw new PeerNotFoundError(
2017
- '当前没有发现可下载的在线种子,请稍后重试或确认发布者在线'
2018
- )
2019
- }
2020
-
2021
- let totalBytes = 0
2022
- try {
2023
- const stat = await drive.entry(entry.key)
2024
- totalBytes = Number(stat?.value?.blob?.byteLength) || 0
2025
- } catch {}
2026
-
2027
- this.emit('download:status', {
2028
- taskId,
2029
- status: 'downloading',
2030
- file: fileName,
2031
- size: totalBytes ? formatFileSize(totalBytes) : null,
2032
- })
2033
-
2034
- const rs = drive.createReadStream(entry.key)
2035
- const ws = fs.createWriteStream(tempPath)
2036
- taskState.readStream = rs
2037
- taskState.writeStream = ws
2038
-
2039
- let loadedBytes = 0
2040
- let lastProgressUpdate = 0
2041
-
2042
- await new Promise((resolve, reject) => {
2043
- let settled = false
2044
- let readTimer = null
2045
-
2046
- const clearReadTimer = () => {
2047
- if (readTimer) {
2048
- clearTimeout(readTimer)
2049
- readTimer = null
2050
- }
2051
- }
2052
-
2053
- const fail = err => {
2054
- if (settled) return
2055
- settled = true
2056
- clearReadTimer()
2057
- rs.destroy(err)
2058
- ws.destroy()
2059
- fs.unlink(tempPath, () => {})
2060
- reject(err)
2061
- }
2062
-
2063
- const complete = () => {
2064
- if (settled) return
2065
- settled = true
2066
- clearReadTimer()
2067
- resolve()
2068
- }
2069
-
2070
- const resetReadTimer = () => {
2071
- clearReadTimer()
2072
- if (streamReadTimeout > 0) {
2073
- readTimer = setTimeout(() => {
2074
- fail(
2075
- new Error(
2076
- `Download stalled: no data received for ${streamReadTimeout / 1000}s`
2077
- )
2078
- )
2079
- }, streamReadTimeout)
2080
- }
2081
- }
2082
-
2083
- resetReadTimer()
2084
-
2085
- rs.on('data', chunk => {
2086
- if (taskState.aborted) {
2087
- fail(new Error('Download cancelled'))
2088
- return
2089
- }
2090
- resetReadTimer()
2091
- loadedBytes += chunk.length
2092
- const now = Date.now()
2093
- if (
2094
- totalBytes > 0 &&
2095
- now - lastProgressUpdate > PROGRESS_THROTTLE
2096
- ) {
2097
- lastProgressUpdate = now
2098
- this.emit('download:progress', {
2099
- taskId,
2100
- loaded: loadedBytes,
2101
- total: totalBytes,
2102
- percent: Math.round((loadedBytes / totalBytes) * 100),
2103
- })
2104
- }
2105
- })
2106
-
2107
- rs.pipe(ws)
2108
- ws.on('finish', complete)
2109
- ws.on('error', fail)
2110
- rs.on('error', fail)
2111
- rs.on('close', () => {
2112
- if (taskState.aborted) fail(new Error('Download cancelled'))
2113
- })
2114
- ws.on('close', () => {
2115
- if (taskState.aborted) fail(new Error('Download cancelled'))
2116
- })
2117
- })
2118
-
2119
- if (taskState.aborted) throw new Error('Download cancelled')
2120
-
2121
- this.emit('download:status', { taskId, status: 'verifying' })
2122
- const { cid: downloadedCid, size } = await calculateCid(tempPath)
2123
- if (downloadedCid.toString() !== cid) {
2124
- fs.unlinkSync(tempPath)
2125
- throw new IntegrityError(
2126
- `File content CID mismatch. Expected ${cid}, got ${downloadedCid.toString()}.`
2127
- )
2128
- }
2129
-
2130
- if (fs.existsSync(resolvedTargetPath)) {
2131
- fs.rmSync(resolvedTargetPath, { force: true })
2132
- }
2133
- fs.renameSync(tempPath, resolvedTargetPath)
2134
-
2135
- await this.seedCidFileFromPath(cid, resolvedTargetPath, {
2136
- fileName,
2137
- source: input.source || 'update',
2138
- })
2139
-
2140
- const result = {
2141
- taskId,
2142
- cid,
2143
- fileName,
2144
- savedPath: resolvedTargetPath,
2145
- size,
2146
- }
2147
- this.emit('download:success', result)
2148
- return result
2149
- } finally {
2150
- this.#activeDownloads.delete(taskId)
2151
- fs.rmSync(tempPath, { force: true })
2152
- }
2153
- }
2154
-
2155
- /**
2156
- * 将本地文件按既有 CID 写入 Hyperdrive 并记录 holding,不写入用户文件列表。
2157
- * @param {string} cid - 文件 CID
2158
- * @param {string} filePath - 本地文件路径
2159
- * @param {object} [options] - 选项
2160
- * @param {string} [options.fileName] - 持有记录展示名
2161
- * @param {string} [options.source='update'] - holding 来源
2162
- */
2163
- async seedCidFileFromPath(cid, filePath, options = {}) {
2164
- this.#ensureInitialized()
2165
- if (!cid) throw new ValidationError('cid is required')
2166
- if (!filePath) throw new ValidationError('filePath is required')
2167
-
2168
- this.#getCidInfo(cid)
2169
- const sizeValidation = await validateFileSize(
2170
- filePath,
2171
- this.#options.maxFileSize
2172
- )
2173
- if (!sizeValidation.valid) {
2174
- throw new FileSizeError(sizeValidation.error, sizeValidation.size)
2175
- }
2176
-
2177
- const { cid: actualCid } = await calculateCid(filePath)
2178
- const actualCidString = actualCid.toString()
2179
- if (actualCidString !== cid) {
2180
- throw new IntegrityError(
2181
- `File content CID mismatch. Expected ${cid}, got ${actualCidString}.`
2182
- )
2183
- }
2184
-
2185
- const existingHolding = this.#holdings.find(holding => holding.cid === cid)
2186
- if (!existingHolding) {
2187
- this.#checkCapacity(sizeValidation.size || 0)
2188
- }
2189
-
2190
- const { driveName } = this.#getCidInfo(cid)
2191
- const drive = await this.#getOrCreateDrive(driveName, {
2192
- server: true,
2193
- client: false,
2194
- })
2195
- const driveKey = '/' + cid
2196
- const localContent = await this.#getLocalCidContent(cid, {
2197
- public: true,
2198
- allowHoldingFallback: true,
2199
- })
2200
-
2201
- if (!localContent) {
2202
- const readStream = fs.createReadStream(filePath)
2203
- const writeStream = drive.createWriteStream(driveKey)
2204
- await new Promise((resolve, reject) => {
2205
- readStream.pipe(writeStream)
2206
- writeStream.on('finish', resolve)
2207
- writeStream.on('error', reject)
2208
- readStream.on('error', reject)
2209
- })
2210
- }
2211
-
2212
- await this.#joinCidTopicInternal(cid, {
2213
- server: true,
2214
- client: false,
2215
- })
2216
-
2217
- return this.#upsertHolding({
2218
- cid,
2219
- fileName: sanitizeFilename(options.fileName || path.basename(filePath)),
2220
- size: sizeValidation.size || 0,
2221
- driveName,
2222
- source: options.source || 'update',
2223
- })
2224
- }
2225
-
2226
1943
  /**
2227
1944
  * 读取已发布文件的内容(用于预览)
2228
1945
  * Hyperdrive 中用 CID 作为 key 存储
@@ -2462,6 +2179,7 @@ export class MostBoxEngine extends EventEmitter {
2462
2179
  channel => channel.channelKey === candidate.channelKey
2463
2180
  )
2464
2181
  if (existing) {
2182
+ const wasMember = this.#channelHasMember(existing, ownerAddress)
2465
2183
  const writerKeysChanged = await this.#mergeChannelWriterCoreKeys(
2466
2184
  existing,
2467
2185
  candidate.writerCoreKeys
@@ -2472,6 +2190,7 @@ export class MostBoxEngine extends EventEmitter {
2472
2190
  this.#saveChannelsMetadata()
2473
2191
  this.#broadcastChannelHello()
2474
2192
  }
2193
+ await this.#appendChannelWelcomeMessage(existing, options, wasMember)
2475
2194
  return this.#formatChannelForResponse(existing, ownerAddress)
2476
2195
  }
2477
2196
  const joined = await this.#joinChannelFromCandidate(
@@ -2528,6 +2247,7 @@ export class MostBoxEngine extends EventEmitter {
2528
2247
  const channelKey = buildChannelKey(channelId)
2529
2248
  const existing = this.#channels.find(c => c.channelKey === channelKey)
2530
2249
  if (existing) {
2250
+ const wasMember = this.#channelHasMember(existing, options.ownerAddress)
2531
2251
  const writerKeysChanged = await this.#mergeChannelWriterCoreKeys(
2532
2252
  existing,
2533
2253
  candidate.writerCoreKeys
@@ -2538,6 +2258,7 @@ export class MostBoxEngine extends EventEmitter {
2538
2258
  this.#saveChannelsMetadata()
2539
2259
  this.#broadcastChannelHello()
2540
2260
  }
2261
+ await this.#appendChannelWelcomeMessage(existing, options, wasMember)
2541
2262
  return this.#formatChannelForResponse(existing, options.ownerAddress)
2542
2263
  }
2543
2264
 
@@ -2730,14 +2451,6 @@ export class MostBoxEngine extends EventEmitter {
2730
2451
  .map(c => this.#formatChannelForResponse(c, ownerAddress))
2731
2452
  }
2732
2453
 
2733
- getChannelMembers(channelKeyInput, options = {}) {
2734
- this.#ensureInitialized()
2735
- this.#assertChannelMember(channelKeyInput, options.ownerAddress)
2736
- const channel = this.#resolveChannel(channelKeyInput, options.ownerAddress)
2737
-
2738
- return this.#getChannelMembers(channel)
2739
- }
2740
-
2741
2454
  /**
2742
2455
  * 获取频道消息
2743
2456
  * @param {string} channelKeyInput - 内部频道 key,或本地唯一短频道 ID
@@ -2843,15 +2556,22 @@ export class MostBoxEngine extends EventEmitter {
2843
2556
  this.#saveChannelsMetadata()
2844
2557
  }
2845
2558
 
2559
+ const normalizedAvatar = normalizeChannelAvatar(options.avatar)
2846
2560
  const message = {
2847
2561
  type: 'message',
2848
2562
  author,
2849
- authorName,
2563
+ authorName: normalizeChannelDisplayName(
2564
+ authorName,
2565
+ normalizeOwnerAddress(author)
2566
+ ),
2850
2567
  content: trimmed,
2851
2568
  timestamp: await this.#getNextChannelMessageTimestamp(
2852
2569
  channel.channelKey
2853
2570
  ),
2854
2571
  }
2572
+ if (normalizedAvatar) {
2573
+ message.avatar = normalizedAvatar
2574
+ }
2855
2575
  if (attachment) {
2856
2576
  message.attachment = attachment
2857
2577
  }
@@ -2977,6 +2697,7 @@ export class MostBoxEngine extends EventEmitter {
2977
2697
  await this.#joinChannelDiscoveryTopics(channelInfo)
2978
2698
  this.#cacheChannelCandidate(this.#channelToCandidate(channelInfo, true))
2979
2699
  this.#saveChannelsMetadata()
2700
+ await this.#appendChannelWelcomeMessage(channelInfo, options, false)
2980
2701
  this.#broadcastChannelHello()
2981
2702
  return channelInfo
2982
2703
  }
@@ -2994,10 +2715,12 @@ export class MostBoxEngine extends EventEmitter {
2994
2715
  channel => channel.channelKey === channelKey
2995
2716
  )
2996
2717
  if (existing) {
2718
+ const wasMember = this.#channelHasMember(existing, options.ownerAddress)
2997
2719
  if (this.#upsertChannelMember(existing, options)) {
2998
2720
  this.#saveChannelsMetadata()
2999
2721
  this.#broadcastChannelHello()
3000
2722
  }
2723
+ await this.#appendChannelWelcomeMessage(existing, options, wasMember)
3001
2724
  return this.#formatChannelForResponse(existing, options.ownerAddress)
3002
2725
  }
3003
2726
 
@@ -3328,6 +3051,32 @@ export class MostBoxEngine extends EventEmitter {
3328
3051
  return true
3329
3052
  }
3330
3053
 
3054
+ async #appendChannelWelcomeMessage(channel, options = {}, wasMember = false) {
3055
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
3056
+ if (
3057
+ wasMember ||
3058
+ !ownerAddress ||
3059
+ !channel ||
3060
+ TRANSIENT_CHANNEL_TYPES.has(channel.type)
3061
+ ) {
3062
+ return false
3063
+ }
3064
+
3065
+ await this.sendMessage(
3066
+ channel.channelKey,
3067
+ CHANNEL_WELCOME_MESSAGE,
3068
+ ownerAddress,
3069
+ normalizeChannelDisplayName(options.displayName, ownerAddress),
3070
+ {
3071
+ ownerAddress,
3072
+ ...(Object.prototype.hasOwnProperty.call(options, 'avatar')
3073
+ ? { avatar: options.avatar }
3074
+ : {}),
3075
+ }
3076
+ )
3077
+ return true
3078
+ }
3079
+
3331
3080
  #getChannelMembers(channel) {
3332
3081
  const members = Array.isArray(channel?.members) ? channel.members : []
3333
3082
  return members
@@ -3392,14 +3141,11 @@ export class MostBoxEngine extends EventEmitter {
3392
3141
  authorAddress
3393
3142
  )
3394
3143
  const avatar = normalizeChannelAvatar(member.avatar)
3395
- if (displayName && baseMessage?.authorName !== displayName) {
3144
+ if (displayName && !String(baseMessage?.authorName || '').trim()) {
3396
3145
  baseMessage = { ...baseMessage, authorName: displayName }
3397
3146
  }
3398
- if (avatar && baseMessage?.avatar !== avatar) {
3147
+ if (avatar && !normalizeChannelAvatar(baseMessage?.avatar)) {
3399
3148
  baseMessage = { ...baseMessage, avatar }
3400
- } else if (!avatar && baseMessage?.avatar) {
3401
- baseMessage = { ...baseMessage }
3402
- delete baseMessage.avatar
3403
3149
  }
3404
3150
  }
3405
3151
  const attachment = baseMessage?.attachment
@@ -164,6 +164,49 @@ function clearActiveRemoteNode() {
164
164
  )
165
165
  }
166
166
 
167
+ function getLocalNodeHistoryItem(activeBackendUrl = getBackendUrl()) {
168
+ const normalizedActiveUrl = normalizeBackendUrl(activeBackendUrl)
169
+ const fallbackLocalUrl = getDefaultBackendUrl()
170
+ const localUrl = isLocalBackendUrl(normalizedActiveUrl)
171
+ ? normalizedActiveUrl
172
+ : fallbackLocalUrl
173
+
174
+ if (!isLocalBackendUrl(localUrl)) return null
175
+
176
+ const url = normalizeBackendUrl(localUrl)
177
+ return {
178
+ url,
179
+ invite: '',
180
+ active: url === normalizedActiveUrl,
181
+ local: true,
182
+ updatedAt: Number.MAX_SAFE_INTEGER,
183
+ }
184
+ }
185
+
186
+ function getNodeHistory() {
187
+ if (typeof window === 'undefined') return []
188
+
189
+ const activeBackendUrl = normalizeBackendUrl(getBackendUrl())
190
+ const localNode = getLocalNodeHistoryItem(activeBackendUrl)
191
+ const remoteNodes = getRemoteNodes().map((node, index) => ({
192
+ ...node,
193
+ active: normalizeBackendUrl(node.url) === activeBackendUrl,
194
+ local: false,
195
+ order: index + 1,
196
+ }))
197
+ const nodes = localNode
198
+ ? [{ ...localNode, order: 0 }, ...remoteNodes]
199
+ : remoteNodes
200
+
201
+ return nodes
202
+ .sort((a, b) => {
203
+ if (a.active !== b.active) return a.active ? -1 : 1
204
+ if (a.local !== b.local) return a.local ? -1 : 1
205
+ return a.order - b.order
206
+ })
207
+ .map(({ order: _order, ...node }) => node)
208
+ }
209
+
167
210
  function normalizeBackendUrl(url) {
168
211
  return (url || '').trim().replace(/\/+$/, '')
169
212
  }
@@ -390,6 +433,10 @@ export function getRemoteNodesExport() {
390
433
  return getRemoteNodes()
391
434
  }
392
435
 
436
+ export function getNodeHistoryExport() {
437
+ return getNodeHistory()
438
+ }
439
+
393
440
  export function getBackendInviteExport() {
394
441
  return getBackendInvite()
395
442
  }
@@ -1,12 +1,9 @@
1
1
  import { verifyMessage } from 'ethers'
2
+ import { normalizeAddress } from '../core/shared.js'
2
3
  import { mostSignMessage } from './mostWallet.js'
3
4
 
4
5
  export const AUTH_MAX_AGE_MS = 5 * 60 * 1000
5
-
6
- export function normalizeAddress(address) {
7
- const value = String(address || '').trim()
8
- return /^0x[a-fA-F0-9]{40}$/.test(value) ? value.toLowerCase() : ''
9
- }
6
+ export { normalizeAddress }
10
7
 
11
8
  export function buildAuthMessage(timestamp, method, path) {
12
9
  return `${timestamp}:${String(method || 'GET').toUpperCase()}:${normalizeAuthPath(path)}`
@@ -1,6 +1,6 @@
1
1
  import dayjs from 'dayjs'
2
2
 
3
- export function formatDate(time) {
3
+ export function formatTime(time) {
4
4
  if (!time) return ''
5
5
  const date = dayjs(Number(time))
6
6
  const hour = date.hour()
@@ -26,5 +26,3 @@ export function formatDate(time) {
26
26
 
27
27
  return date.format(`YYYY年M月D日 ${timeOfDay}h:m`)
28
28
  }
29
-
30
- export const formatTime = formatDate
@@ -1 +0,0 @@
1
- import{t as e}from"./jsx-runtime-Bt-cYkS5.js";import{s as t,t as n,w as r}from"./ui-PnUvY9zL.js";import{a as i,c as a,o}from"./i18n--maTEG0d.js";var s=r(`check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]),c=r(`languages`,[[`path`,{d:`m5 8 6 6`,key:`1wu5hv`}],[`path`,{d:`m4 14 6-6 2-3`,key:`1k1g8d`}],[`path`,{d:`M2 5h12`,key:`or177f`}],[`path`,{d:`M7 2h1`,key:`1t2jsx`}],[`path`,{d:`m22 22-5-10-5 10`,key:`don7ne`}],[`path`,{d:`M14 18h6`,key:`1m8k6r`}]]),l=r(`moon`,[[`path`,{d:`M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401`,key:`kfwtm`}]]),u=r(`refresh-cw`,[[`path`,{d:`M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8`,key:`v9h5vc`}],[`path`,{d:`M21 3v5h-5`,key:`1q7to0`}],[`path`,{d:`M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16`,key:`3uifl3`}],[`path`,{d:`M8 16H3v5`,key:`1cv678`}]]),d=r(`sun`,[[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}],[`path`,{d:`M12 2v2`,key:`tus03m`}],[`path`,{d:`M12 20v2`,key:`1lh1kg`}],[`path`,{d:`m4.93 4.93 1.41 1.41`,key:`149t6j`}],[`path`,{d:`m17.66 17.66 1.41 1.41`,key:`ptbguv`}],[`path`,{d:`M2 12h2`,key:`1t8f8n`}],[`path`,{d:`M20 12h2`,key:`1q8mjw`}],[`path`,{d:`m6.34 17.66-1.41 1.41`,key:`1m8zz5`}],[`path`,{d:`m19.07 4.93-1.41 1.41`,key:`1shlcs`}]]),f=e();function p(){let{t:e}=i(),{state:n,installAndRestart:r}=t(),a=n.status===`checking`,o=n.status===`downloading`,s=n.status===`downloaded`,c=n.status===`installing`,l=a||o||c;if(!a&&!o&&!s&&!c)return null;let d=e(`desktopUpdate.install`);a?d=e(`desktopUpdate.checking`):o?d=n.progress>0?e(`desktopUpdate.downloadingProgress`,{progress:n.progress}):e(`desktopUpdate.downloading`):c&&(d=e(`desktopUpdate.installing`));let p=a||o?e(`desktopUpdate.downloadingTitle`):e(`desktopUpdate.readyTitle`,{version:n.version||n.filename});return(0,f.jsxs)(`button`,{type:`button`,className:`btn ${s||c?`btn-primary`:`btn-secondary`} desktop-update-button`,disabled:!s||c,title:p,"aria-live":l?`polite`:void 0,onClick:()=>{s&&r()},children:[(0,f.jsx)(u,{size:16,className:l?`spin`:void 0}),d]})}function m({className:e=``}){let{locale:t,localeName:r,setLocale:l,t:u}=i(),d=u(`common.locale.choose`),p=`${d} (${u(`common.locale.current`)}: ${r})`;return(0,f.jsx)(n,{ariaLabel:d,placement:`bottom-end`,items:o.map(e=>({key:e,label:a[e],icon:e===t?(0,f.jsx)(s,{size:16}):(0,f.jsx)(`span`,{"aria-hidden":`true`}),onSelect:()=>{e!==t&&l(e)}})),renderTrigger:t=>(0,f.jsx)(`button`,{...t,className:[`language-toggle`,e].filter(Boolean).join(` `),"aria-label":d,title:p,children:(0,f.jsx)(c,{size:16})})})}export{l as a,u as i,p as n,s as o,d as r,m as t};
@@ -1 +0,0 @@
1
- import{t as e}from"./jsx-runtime-Bt-cYkS5.js";import{n as t,t as n}from"./LanguageToggle-BoBNesNU.js";import{k as r,w as i}from"./index-Cg4kEvx8.js";import{t as a}from"./MarketingThemeToggle-BLuQ_uE-.js";var o=e();function s(){return(0,o.jsx)(`nav`,{className:`mkt-nav`,children:(0,o.jsxs)(`div`,{className:`mkt-nav-inner`,children:[(0,o.jsxs)(`button`,{type:`button`,className:`mkt-nav-logo`,onClick:i(),children:[(0,o.jsx)(r,{size:18}),(0,o.jsx)(`span`,{children:`MOST PEOPLE`})]}),(0,o.jsxs)(`div`,{className:`mkt-nav-cta`,children:[(0,o.jsx)(a,{}),(0,o.jsx)(n,{className:`mkt-theme-toggle`}),(0,o.jsx)(t,{})]})]})})}export{s as t};
@@ -1 +0,0 @@
1
- import{t as e}from"./jsx-runtime-Bt-cYkS5.js";import{O as t,c as n,o as r}from"./ui-PnUvY9zL.js";import{a as i}from"./i18n--maTEG0d.js";import{n as a,t as o}from"./LanguageToggle-BoBNesNU.js";import{t as s}from"./download-Q6jKMPIG.js";import{i as c}from"./index-Cg4kEvx8.js";import{t as l}from"./MarketingThemeToggle-BLuQ_uE-.js";import{t as u}from"./LogoIcon-B2fFe0l1.js";var d={name:`most-box`,version:`0.2.6`,description:`MostBox - P2P file sharing application`,type:`module`,main:`electron/main.js`,bin:{"most-box":`server/cli.js`},files:[`out`,`public`,`electron`,`server/src`,`server/index.js`,`server/cli.js`],scripts:{start:`vite`,server:`node --watch server/index.js`,dev:`vite`,build:`vite build && node scripts/prepare-start-static.mjs`,preview:`vite preview`,serve:`npm run build && node server/index.js`,test:`node --test server/tests/**/*.test.js`,"test:unit":`node --test server/tests/unit/*.test.js`,"test:frontend":`node --test src/tests/*.test.js`,"test:protocol":`node --test --test-name-pattern "extracts CID and filename|matches golden CID samples|publishes a file from Buffer|lists published files|pulls through local seed nodes" server/tests/unit/cid.test.js server/tests/integration/cid.test.js server/tests/integration/engine.test.js`,typecheck:`tsc -p tsconfig.typecheck.json`,"typecheck:strict-router":`tsc -p tsconfig.strict-router.json`,"check:static-output":`node scripts/check-static-output.mjs`,format:`npx prettier --write .`,lint:`npx eslint .`,"r2:cors":`node scripts/configure-r2-cors.mjs`,"electron:dev":`concurrently "npm run build" "wait-on out/index.html && electron ."`,"electron:rebuild":`electron-rebuild -f -w sodium-native`,"electron:build":`npm run build && npm run electron:rebuild && electron-builder`,"electron:build:win":`npm run build && npm run electron:rebuild && electron-builder --win`,"electron:build:win:x64":`npm run electron:build:win -- --x64`,"electron:build:win:arm64":`npm run electron:build:win -- --arm64`,"electron:build:mac":`npm run build && npm run electron:rebuild && electron-builder --mac`,"electron:build:linux":`npm run build && npm run electron:rebuild && electron-builder --linux`},keywords:[`p2p`,`file-sharing`,`hyperswarm`,`hyperdrive`],license:`MIT`,dependencies:{"@dicebear/collection":`^9.4.2`,"@dicebear/core":`^9.4.2`,"@hono/node-server":`^2.0.4`,"@tanstack/react-router":`^1.170.15`,"@tanstack/react-start":`^1.168.25`,b4a:`^1.8.1`,busboy:`^1.6.0`,corestore:`^7.10.0`,dayjs:`^1.11.21`,ethers:`^6.16.0`,eventemitter3:`^5.0.4`,hono:`^4.12.23`,hypercore:`^11.33.1`,hyperdrive:`^13.3.2`,hyperswarm:`^4.17.0`,"ipfs-unixfs-importer":`^17.0.1`,multiformats:`^14.0.0`,tweetnacl:`^1.0.3`,ws:`^8.21.0`},devDependencies:{"@electron/rebuild":`^4.0.4`,"@eslint/js":`^10.0.1`,"@iconify/react":`^6.0.2`,"@mantine/hooks":`^9.3.0`,"@milkdown/crepe":`^7.21.2`,"@milkdown/utils":`^7.21.2`,"@types/react":`^19.2.16`,"@vitejs/plugin-react":`^6.0.2`,concurrently:`^10.0.3`,electron:`^42.3.3`,"electron-builder":`^26.8.1`,esbuild:`^0.28.0`,eslint:`^10.4.1`,globals:`^17.6.0`,ky:`^2.0.2`,"lucide-react":`^1.17.0`,prettier:`^3.8.3`,"qrcode.react":`^4.2.0`,react:`^19.2.7`,"react-dom":`^19.2.7`,typescript:`6.0.3`,"typescript-eslint":`^8.60.1`,vite:`^8.0.16`,"wait-on":`^9.0.10`,zustand:`^5.0.14`},overrides:{"brace-expansion":`5.0.6`,postcss:`8.5.15`,ws:`$ws`},build:{appId:`com.mostbox.app`,productName:`MostBox`,publish:[{provider:`github`,releaseType:`release`}],directories:{output:`dist`},afterPack:`electron/afterPack.cjs`,files:[`out/**/*`,`electron/**/*`,`server/src/**/*`,`server/index.js`,`server/cli.js`,`package.json`,`!**/*.map`,`!electron/**/*.test.js`,`!server/tests/**/*`],asarUnpack:[`**/*.node`,`**/sodium-native/**`],protocols:[{name:`MostBox Link`,schemes:[`most`]}],win:{target:[`nsis`],artifactName:"${productName}-${version}-win-${arch}-setup.${ext}",icon:`public/logo.ico`},nsis:{oneClick:!1,allowToChangeInstallationDirectory:!0,createDesktopShortcut:!0,createStartMenuShortcut:!0,shortcutName:`MostBox`},mac:{artifactName:"${productName}-${version}-mac-${arch}.${ext}",target:[{target:`dmg`,arch:[`x64`,`arm64`]},{target:`zip`,arch:[`x64`,`arm64`]}],icon:`public/logo-512.png`,category:`public.app-category.utilities`},dmg:{contents:[{x:130,y:220},{x:410,y:220,type:`link`,path:`/Applications`}]},linux:{artifactName:"${productName}-${version}-linux-${arch}.${ext}",target:[{target:`AppImage`,arch:[`x64`,`arm64`]}],icon:`public/logo-512.png`,category:`Utility`}}},f=e(),p=[{to:`/`,labelKey:`footer.about`},{to:`/ping/`,labelKey:`footer.network`},{href:`https://github.com/most-people/most`,labelKey:null,label:`GitHub`,external:!0}],m=d.version;function h(){let{t:e}=i();return(0,f.jsx)(`footer`,{className:`mkt-footer`,children:(0,f.jsx)(`div`,{className:`mkt-container`,children:(0,f.jsxs)(`div`,{className:`mkt-footer-inner`,children:[(0,f.jsx)(`div`,{className:`mkt-footer-links`,children:p.map(n=>`external`in n?(0,f.jsx)(`a`,{href:n.href,target:`_blank`,rel:`noopener noreferrer`,children:n.label},n.href):(0,f.jsx)(t,{to:n.to,children:e(n.labelKey)},n.to))}),(0,f.jsxs)(`span`,{className:`mkt-footer-copy`,children:[`© `,new Date().getFullYear(),` MOST PEOPLE · MIT License`]}),(0,f.jsxs)(`span`,{className:`mkt-footer-build`,translate:`no`,children:[`v`,m]})]})})})}function g({children:e,header:t}){return(0,f.jsxs)(`div`,{className:`mkt-layout`,children:[t??(0,f.jsx)(_,{}),(0,f.jsx)(`main`,{className:`mkt-layout-main`,children:e}),(0,f.jsx)(h,{})]})}function _(){let e=n(e=>e.identity),d=n(e=>e.openLoginModal),{t:p}=i(),m=r(),h=e?.displayName||e?.username||p(`nav.openWeb`);return(0,f.jsx)(`nav`,{className:`mkt-nav`,children:(0,f.jsxs)(`div`,{className:`mkt-nav-inner`,children:[(0,f.jsxs)(t,{to:`/`,className:`mkt-nav-logo`,children:[(0,f.jsx)(u,{}),`MOST PEOPLE`]}),(0,f.jsxs)(`div`,{className:`mkt-nav-cta`,children:[(0,f.jsx)(l,{}),(0,f.jsx)(o,{className:`mkt-theme-toggle`}),(0,f.jsx)(a,{}),!m&&(0,f.jsxs)(t,{to:`/download/`,className:`btn btn-primary mkt-nav-preview`,children:[(0,f.jsx)(s,{size:16}),p(`nav.downloadClient`)]}),e?(0,f.jsx)(t,{to:`/profile/`,className:`mkt-nav-avatar-trigger`,"aria-label":p(`nav.profile`),title:h,children:(0,f.jsx)(`img`,{className:`mkt-nav-avatar`,src:c(e.address,e.avatar),alt:``,"aria-hidden":`true`})}):(0,f.jsx)(`button`,{type:`button`,className:`btn btn-secondary`,onClick:d,children:p(`nav.getStarted`)})]})]})})}export{g as t};
@@ -1 +0,0 @@
1
- import{n as e,o as t,t as n}from"./jsx-runtime-Bt-cYkS5.js";import{O as r,w as i}from"./ui-PnUvY9zL.js";import{a}from"./i18n--maTEG0d.js";import{n as o,t as s}from"./LanguageToggle-BoBNesNU.js";import{k as c}from"./index-Cg4kEvx8.js";var l=i(`menu`,[[`path`,{d:`M4 5h16`,key:`1tepv9`}],[`path`,{d:`M4 12h16`,key:`1lakjw`}],[`path`,{d:`M4 19h16`,key:`1djgab`}]]),u=t(e(),1);function d(e,t){return typeof t==`boolean`?t:typeof window<`u`&&`matchMedia`in window?window.matchMedia(e).matches:!1}function f(e,t,{getInitialValueInEffect:n}={getInitialValueInEffect:!0}){let[r,i]=(0,u.useState)(n?t:d(e));return(0,u.useEffect)(()=>{try{if(`matchMedia`in window){let t=window.matchMedia(e);i(t.matches);let n=e=>i(e.matches);return t.addEventListener(`change`,n),()=>{t.removeEventListener(`change`,n)}}}catch{return}},[e]),r||!1}function p(e=!1,t={}){let[n,r]=(0,u.useState)(e),i=(0,u.useCallback)(()=>{r(e=>e||(t.onOpen?.(),!0))},[t.onOpen]),a=(0,u.useCallback)(()=>{r(e=>e&&(t.onClose?.(),!1))},[t.onClose]);return[n,{open:i,close:a,toggle:(0,u.useCallback)(()=>{n?a():i()},[a,i,n]),set:r}]}var m=n(),h=(0,u.createContext)(null);function g(){let e=(0,u.useContext)(h);if(!e)throw Error(`useAppShell must be used within AppShell`);return e}function _({sidebar:e,className:t=``,headerTitle:n,headerRight:r,sidebarToggleReplacement:i,defaultHide:c=!1,children:d}){let[g,_]=p(!1),[v,y]=(0,u.useState)(c),b=(0,u.useRef)(c),x=f(`(max-width: 768px)`),{t:S}=a(),C=(0,u.useCallback)((e={})=>{_.close(),e.collapse&&y(!0)},[_]);(0,u.useEffect)(()=>{c!==b.current&&(c?C({collapse:!0}):y(!1),b.current=c)},[c,C]);let w=()=>{x?_.toggle():y(!v)},T=()=>{x?_.open():y(!1)},E=x?g:!v;return(0,m.jsx)(h.Provider,{value:{closeSidebar:C,openSidebar:T,isSidebarVisible:E},children:(0,m.jsxs)(`div`,{className:[`app-layout`,t].filter(Boolean).join(` `),children:[(0,m.jsx)(`div`,{className:`sidebar-overlay ${g?`visible`:``}`,onClick:()=>C()}),(0,m.jsx)(`div`,{className:`sidebar ${g?`open`:``} ${v?`collapsed`:``}`,children:e({closeSidebar:C,openSidebar:T})}),(0,m.jsxs)(`div`,{className:`main-content`,children:[(0,m.jsxs)(`header`,{className:`app-header`,children:[(0,m.jsxs)(`div`,{className:`header-left`,children:[i??(0,m.jsx)(`button`,{onClick:w,className:`btn btn-icon sidebar-toggle-btn`,"aria-label":S(x?`appShell.openMenu`:v?`appShell.expandSidebar`:`appShell.collapseSidebar`),children:(0,m.jsx)(l,{size:16})}),n]}),(0,m.jsxs)(`div`,{className:`header-right`,children:[(0,m.jsx)(o,{}),(0,m.jsx)(s,{className:`btn btn-icon app-language-toggle`}),r]})]}),d]})]})})}function v({onNavigate:e}){let{t}=a(),n=t(`common.brand`);return(0,m.jsxs)(r,{to:`/`,className:`sidebar-header sidebar-header-link`,onClick:e,"aria-label":n,title:n,children:[(0,m.jsx)(c,{size:18}),(0,m.jsx)(`h1`,{children:n})]})}export{p as i,_ as n,g as r,v as t};
@@ -1 +0,0 @@
1
- import{w as e}from"./ui-PnUvY9zL.js";var t=e(`arrow-right`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]]);export{t};
@@ -1 +0,0 @@
1
- import{w as e}from"./ui-PnUvY9zL.js";var t=e(`cloud`,[[`path`,{d:`M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z`,key:`p7xjir`}]]);export{t};