most-box 0.1.3 → 0.1.5

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 (205) hide show
  1. package/README.md +7 -4
  2. package/electron/main.js +31 -10
  3. package/out/404/index.html +2 -2
  4. package/out/404.html +2 -2
  5. package/out/__next.__PAGE__.txt +6 -6
  6. package/out/__next._full.txt +15 -15
  7. package/out/__next._head.txt +3 -3
  8. package/out/__next._index.txt +7 -7
  9. package/out/__next._tree.txt +4 -4
  10. package/out/_next/static/chunks/{0hpev4am9jpmu.css → 0.4ov9319fecg.css} +1 -1
  11. package/out/_next/static/chunks/0._8s6pbfw.xk.js +1 -0
  12. package/out/_next/static/chunks/02jvyg27pp0mz.js +1 -0
  13. package/out/_next/static/chunks/02zzxfop_k6tl.css +1 -0
  14. package/out/_next/static/chunks/06lttvu7563zo.css +1 -0
  15. package/out/_next/static/chunks/{0pt.5cg1t09qs.js → 08qqf0rsi3oot.js} +1 -1
  16. package/out/_next/static/chunks/{0adx~d-j05c9d.css → 09h7l4i38xc7q.css} +1 -1
  17. package/out/_next/static/chunks/09hqx79-jkvm_.css +1 -0
  18. package/out/_next/static/chunks/09vcm1ku9k7o8.js +1 -0
  19. package/out/_next/static/chunks/0c6e_d2jq179x.js +1 -0
  20. package/out/_next/static/chunks/0ccalho416.d7.js +1 -0
  21. package/out/_next/static/chunks/0exj_tg.ew-t3.js +1 -0
  22. package/out/_next/static/chunks/0f-wz5d~tv-r4.js +1 -0
  23. package/out/_next/static/chunks/0f0jhsujtf-61.js +1 -0
  24. package/out/_next/static/chunks/0hyds~bp.auvh.js +1 -0
  25. package/out/_next/static/chunks/0ig8a4sazk3~2.css +1 -0
  26. package/out/_next/static/chunks/0iq1h7g4dudg8.js +1 -0
  27. package/out/_next/static/chunks/0knnzo9aih48r.js +1 -0
  28. package/out/_next/static/chunks/0nzyk~sg_tn._.js +1 -0
  29. package/out/_next/static/chunks/{12nr19.nnn6s3.js → 0t_3xxx4zkerp.js} +2 -2
  30. package/out/_next/static/chunks/0w~2fjq86t7c7.css +1 -0
  31. package/out/_next/static/chunks/0xrqerhosrp9~.js +1 -0
  32. package/out/_next/static/chunks/0xx_10jns1.s7.css +1 -0
  33. package/out/_next/static/chunks/14-fm9r_mom81.js +1 -0
  34. package/out/_next/static/chunks/14jhy~xia8lh8.js +1 -0
  35. package/out/_next/static/chunks/{turbopack-0xta0kqwzkf28.js → turbopack-05qngmxam3ar~.js} +1 -1
  36. package/out/_not-found/__next._full.txt +12 -12
  37. package/out/_not-found/__next._head.txt +3 -3
  38. package/out/_not-found/__next._index.txt +7 -7
  39. package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
  40. package/out/_not-found/__next._not-found.txt +3 -3
  41. package/out/_not-found/__next._tree.txt +2 -2
  42. package/out/_not-found/index.html +2 -2
  43. package/out/_not-found/index.txt +12 -12
  44. package/out/admin/__next._full.txt +14 -14
  45. package/out/admin/__next._head.txt +3 -3
  46. package/out/admin/__next._index.txt +7 -7
  47. package/out/admin/__next._tree.txt +3 -3
  48. package/out/admin/__next.admin.__PAGE__.txt +4 -4
  49. package/out/admin/__next.admin.txt +4 -4
  50. package/out/admin/index.html +2 -2
  51. package/out/admin/index.txt +14 -14
  52. package/out/app/__next._full.txt +13 -13
  53. package/out/app/__next._head.txt +3 -3
  54. package/out/app/__next._index.txt +7 -7
  55. package/out/app/__next._tree.txt +2 -2
  56. package/out/app/__next.app.__PAGE__.txt +4 -4
  57. package/out/app/__next.app.txt +3 -3
  58. package/out/app/index.html +2 -2
  59. package/out/app/index.txt +13 -13
  60. package/out/chat/__next._full.txt +14 -14
  61. package/out/chat/__next._head.txt +3 -3
  62. package/out/chat/__next._index.txt +7 -7
  63. package/out/chat/__next._tree.txt +3 -3
  64. package/out/chat/__next.chat.__PAGE__.txt +4 -4
  65. package/out/chat/__next.chat.txt +4 -4
  66. package/out/chat/index.html +2 -2
  67. package/out/chat/index.txt +14 -14
  68. package/out/chat/join/__next._full.txt +14 -14
  69. package/out/chat/join/__next._head.txt +3 -3
  70. package/out/chat/join/__next._index.txt +7 -7
  71. package/out/chat/join/__next._tree.txt +3 -3
  72. package/out/chat/join/__next.chat.join.__PAGE__.txt +4 -4
  73. package/out/chat/join/__next.chat.join.txt +3 -3
  74. package/out/chat/join/__next.chat.txt +4 -4
  75. package/out/chat/join/index.html +2 -2
  76. package/out/chat/join/index.txt +14 -14
  77. package/out/download/__next._full.txt +34 -36
  78. package/out/download/__next._head.txt +3 -3
  79. package/out/download/__next._index.txt +7 -7
  80. package/out/download/__next._tree.txt +4 -4
  81. package/out/download/__next.download.__PAGE__.txt +7 -7
  82. package/out/download/__next.download.txt +3 -3
  83. package/out/download/index.html +2 -2
  84. package/out/download/index.txt +34 -36
  85. package/out/favicon.ico +0 -0
  86. package/out/game/__next._full.txt +20 -0
  87. package/out/game/__next._head.txt +5 -0
  88. package/out/game/__next._index.txt +9 -0
  89. package/out/game/__next._tree.txt +5 -0
  90. package/out/game/__next.game.__PAGE__.txt +6 -0
  91. package/out/game/__next.game.txt +5 -0
  92. package/out/game/gandengyan/__next._full.txt +26 -0
  93. package/out/game/gandengyan/__next._head.txt +5 -0
  94. package/out/game/gandengyan/__next._index.txt +9 -0
  95. package/out/game/gandengyan/__next._tree.txt +6 -0
  96. package/out/game/gandengyan/__next.game.gandengyan.__PAGE__.txt +10 -0
  97. package/out/game/gandengyan/__next.game.gandengyan.txt +5 -0
  98. package/out/game/gandengyan/__next.game.txt +5 -0
  99. package/out/game/gandengyan/index.html +15 -0
  100. package/out/game/gandengyan/index.txt +26 -0
  101. package/out/game/index.html +1 -0
  102. package/out/game/index.txt +20 -0
  103. package/out/game/zhajinhua/__next._full.txt +25 -0
  104. package/out/game/zhajinhua/__next._head.txt +5 -0
  105. package/out/game/zhajinhua/__next._index.txt +9 -0
  106. package/out/game/zhajinhua/__next._tree.txt +5 -0
  107. package/out/game/zhajinhua/__next.game.txt +5 -0
  108. package/out/game/zhajinhua/__next.game.zhajinhua.__PAGE__.txt +9 -0
  109. package/out/game/zhajinhua/__next.game.zhajinhua.txt +5 -0
  110. package/out/game/zhajinhua/index.html +15 -0
  111. package/out/game/zhajinhua/index.txt +25 -0
  112. package/out/index.html +2 -2
  113. package/out/index.txt +15 -15
  114. package/out/logo-512.png +0 -0
  115. package/out/logo.ico +0 -0
  116. package/out/logo.svg +12 -0
  117. package/out/note/__next._full.txt +13 -13
  118. package/out/note/__next._head.txt +3 -3
  119. package/out/note/__next._index.txt +7 -7
  120. package/out/note/__next._tree.txt +2 -2
  121. package/out/note/__next.note.__PAGE__.txt +4 -4
  122. package/out/note/__next.note.txt +3 -3
  123. package/out/note/index.html +2 -2
  124. package/out/note/index.txt +13 -13
  125. package/out/ping/__next._full.txt +14 -14
  126. package/out/ping/__next._head.txt +3 -3
  127. package/out/ping/__next._index.txt +7 -7
  128. package/out/ping/__next._tree.txt +3 -3
  129. package/out/ping/__next.ping.__PAGE__.txt +5 -5
  130. package/out/ping/__next.ping.txt +3 -3
  131. package/out/ping/index.html +2 -2
  132. package/out/ping/index.txt +14 -14
  133. package/out/web3/__next._full.txt +13 -13
  134. package/out/web3/__next._head.txt +3 -3
  135. package/out/web3/__next._index.txt +7 -7
  136. package/out/web3/__next._tree.txt +2 -2
  137. package/out/web3/__next.web3.__PAGE__.txt +4 -4
  138. package/out/web3/__next.web3.txt +3 -3
  139. package/out/web3/ed25519/__next._full.txt +11 -11
  140. package/out/web3/ed25519/__next._head.txt +3 -3
  141. package/out/web3/ed25519/__next._index.txt +7 -7
  142. package/out/web3/ed25519/__next._tree.txt +2 -2
  143. package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
  144. package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
  145. package/out/web3/ed25519/__next.web3.txt +3 -3
  146. package/out/web3/ed25519/index.html +1 -1
  147. package/out/web3/ed25519/index.txt +11 -11
  148. package/out/web3/index.html +2 -2
  149. package/out/web3/index.txt +13 -13
  150. package/out/web3/tools/__next._full.txt +11 -11
  151. package/out/web3/tools/__next._head.txt +3 -3
  152. package/out/web3/tools/__next._index.txt +7 -7
  153. package/out/web3/tools/__next._tree.txt +2 -2
  154. package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
  155. package/out/web3/tools/__next.web3.tools.txt +3 -3
  156. package/out/web3/tools/__next.web3.txt +3 -3
  157. package/out/web3/tools/index.html +1 -1
  158. package/out/web3/tools/index.txt +11 -11
  159. package/package.json +6 -5
  160. package/public/favicon.ico +0 -0
  161. package/public/logo-512.png +0 -0
  162. package/public/logo.ico +0 -0
  163. package/public/logo.svg +12 -0
  164. package/server/index.js +0 -8
  165. package/server/src/config.js +1 -1
  166. package/server/src/core/gameRoom.js +222 -0
  167. package/server/src/core/zhajinhua.js +563 -0
  168. package/server/src/games/gandengyan.js +354 -413
  169. package/server/src/http/app.js +22 -14
  170. package/server/src/http/uploads.js +1 -0
  171. package/server/src/index.js +275 -90
  172. package/server/src/utils/avatar.js +3 -2
  173. package/out/_next/static/chunks/04mo7rr..0_1q.js +0 -1
  174. package/out/_next/static/chunks/06rf3qq5ggs6v.js +0 -1
  175. package/out/_next/static/chunks/07td.jq7xff84.css +0 -1
  176. package/out/_next/static/chunks/0_0oph_z1az14.js +0 -1
  177. package/out/_next/static/chunks/0ao1lbi4b.sfa.js +0 -1
  178. package/out/_next/static/chunks/0cl7d~7abnk_p.css +0 -1
  179. package/out/_next/static/chunks/0d306t1wvjpdx.js +0 -1
  180. package/out/_next/static/chunks/0g_a~e050bgzg.css +0 -1
  181. package/out/_next/static/chunks/0m_5nb6x8qy._.js +0 -1
  182. package/out/_next/static/chunks/0n.ayxmsar6e5.js +0 -1
  183. package/out/_next/static/chunks/0olqjomda37-e.js +0 -1
  184. package/out/_next/static/chunks/0qgx9t4jx16ua.css +0 -1
  185. package/out/_next/static/chunks/0s~g.l~x049o2.js +0 -1
  186. package/out/_next/static/chunks/0voe1.ttrh84k.css +0 -1
  187. package/out/_next/static/chunks/0wtf0xsiicxx6.js +0 -1
  188. package/out/_next/static/chunks/0x.ky97owcxxs.js +0 -1
  189. package/out/_next/static/chunks/0ysj5b94vu4ri.js +0 -1
  190. package/out/_next/static/chunks/153-sz7s.qml2.js +0 -1
  191. package/out/_next/static/chunks/17cwkb2yn_akx.js +0 -1
  192. package/out/_next/static/chunks/184hxsuf-5c84.js +0 -1
  193. package/out/gandengyan/__next._full.txt +0 -25
  194. package/out/gandengyan/__next._head.txt +0 -5
  195. package/out/gandengyan/__next._index.txt +0 -9
  196. package/out/gandengyan/__next._tree.txt +0 -5
  197. package/out/gandengyan/__next.gandengyan.__PAGE__.txt +0 -10
  198. package/out/gandengyan/__next.gandengyan.txt +0 -5
  199. package/out/gandengyan/index.html +0 -15
  200. package/out/gandengyan/index.txt +0 -25
  201. /package/out/_next/static/{aPEZ4zaaR5W3WpSZ0dFsa → 2smv1H9Y4Z2Ri-SL-UFgR}/_buildManifest.js +0 -0
  202. /package/out/_next/static/{aPEZ4zaaR5W3WpSZ0dFsa → 2smv1H9Y4Z2Ri-SL-UFgR}/_clientMiddlewareManifest.js +0 -0
  203. /package/out/_next/static/{aPEZ4zaaR5W3WpSZ0dFsa → 2smv1H9Y4Z2Ri-SL-UFgR}/_ssgManifest.js +0 -0
  204. /package/out/{pwa-512x512.png → avatar.png} +0 -0
  205. /package/public/{pwa-512x512.png → avatar.png} +0 -0
@@ -672,16 +672,16 @@ export function createApp(engine, options = {}) {
672
672
  return c.json({ error: parsed.error }, 400)
673
673
  }
674
674
 
675
- const existingFile = engine
676
- .getPublishedFiles({ ownerAddress: c.get('userAddress') })
677
- .find(f => f.cid === parsed.cid)
678
- if (existingFile) {
675
+ const localAvailability = await engine.getLocalCidAvailability(body.link, {
676
+ ownerAddress: c.get('userAddress'),
677
+ })
678
+ if (localAvailability) {
679
679
  return c.json({
680
680
  success: true,
681
681
  available: true,
682
682
  cid: parsed.cid,
683
- fileName: existingFile.fileName,
684
- size: Number(existingFile.size) || null,
683
+ fileName: localAvailability.fileName,
684
+ size: Number(localAvailability.size) || null,
685
685
  alreadyExists: true,
686
686
  })
687
687
  }
@@ -719,11 +719,13 @@ export function createApp(engine, options = {}) {
719
719
  return c.json({ error: parsed.error }, 400)
720
720
  }
721
721
 
722
- const existingFile = engine
723
- .getPublishedFiles({ ownerAddress: c.get('userAddress') })
724
- .find(f => f.cid === parsed.cid)
725
- if (existingFile) {
726
- console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
722
+ const localAvailability = await engine.getLocalCidAvailability(body.link, {
723
+ ownerAddress: c.get('userAddress'),
724
+ })
725
+ if (localAvailability) {
726
+ console.log(
727
+ `[MostBox] CID content already exists locally: ${parsed.cid}`
728
+ )
727
729
  try {
728
730
  const result = await engine.downloadFile(body.link, taskId, {
729
731
  ownerAddress: c.get('userAddress'),
@@ -801,7 +803,7 @@ export function createApp(engine, options = {}) {
801
803
  })
802
804
  return c.json({ success: true, ...result })
803
805
  } catch (err) {
804
- return c.json({ error: err.message }, 400)
806
+ return badRequestOrAppError(c, err)
805
807
  }
806
808
  })
807
809
 
@@ -959,7 +961,13 @@ export function createApp(engine, options = {}) {
959
961
  })
960
962
 
961
963
  app.get('/api/channels', c => {
962
- return c.json(engine.listChannels({ ownerAddress: c.get('userAddress') }))
964
+ return c.json(
965
+ engine.listChannels({
966
+ ownerAddress: c.get('userAddress'),
967
+ type: c.req.query('type'),
968
+ excludeType: c.req.query('excludeType'),
969
+ })
970
+ )
963
971
  })
964
972
 
965
973
  app.delete('/api/channels/:name', async c => {
@@ -1065,7 +1073,7 @@ export function createApp(engine, options = {}) {
1065
1073
  })
1066
1074
  return c.json({ success: true, ...result })
1067
1075
  } catch (err) {
1068
- return c.json({ error: err.message }, 400)
1076
+ return badRequestOrAppError(c, err)
1069
1077
  }
1070
1078
  })
1071
1079
 
@@ -46,6 +46,7 @@ export async function parseMultipartBusboy(req, maxUploadSize = MAX_FILE_SIZE) {
46
46
 
47
47
  const busboy = Busboy({
48
48
  headers: req.headers,
49
+ preservePath: true,
49
50
  limits: {
50
51
  fileSize: maxUploadSize,
51
52
  files: 1,
@@ -48,7 +48,6 @@ import {
48
48
  SWARM_KEEP_ALIVE_INTERVAL,
49
49
  SWARM_RANDOM_PUNCH_INTERVAL,
50
50
  DRIVE_ENTRY_TIMEOUT,
51
- DRIVE_SYNC_TIMEOUT,
52
51
  STREAM_READ_TIMEOUT,
53
52
  FILE_WRITE_CHUNK_SIZE,
54
53
  DOWNLOAD_POLL_INTERVAL_MIN,
@@ -67,12 +66,28 @@ import {
67
66
  } from './config.js'
68
67
 
69
68
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
69
+ const CHAT_FILE_ROOT = 'chat-file'
70
70
 
71
71
  function normalizeOwnerAddress(address) {
72
72
  const value = String(address || '').trim()
73
73
  return /^0x[a-fA-F0-9]{40}$/.test(value) ? value.toLowerCase() : ''
74
74
  }
75
75
 
76
+ function getPathBaseName(fileName) {
77
+ const parts = String(fileName || '').split('/').filter(Boolean)
78
+ return parts[parts.length - 1] || 'unnamed_file'
79
+ }
80
+
81
+ function getDisplayPathFolder(fileName) {
82
+ const parts = String(fileName || '').split('/').filter(Boolean)
83
+ parts.pop()
84
+ return parts.join('/')
85
+ }
86
+
87
+ function buildMostLink(cid, fileName) {
88
+ return `most://${cid}?filename=${encodeURIComponent(fileName)}`
89
+ }
90
+
76
91
  function createOfflineSwarm() {
77
92
  return {
78
93
  connections: new Set(),
@@ -528,6 +543,10 @@ export class MostBoxEngine extends EventEmitter {
528
543
  }
529
544
  }
530
545
 
546
+ this.#assertDisplayNameAvailable(safeFileName, {
547
+ ownerAddress,
548
+ })
549
+
531
550
  // 获取或创建该 CID 对应的 drive
532
551
  let drive = this.#drives.get(name)
533
552
 
@@ -644,39 +663,48 @@ export class MostBoxEngine extends EventEmitter {
644
663
  const cidString = parsed.cid
645
664
  console.log(`[MostBox] Parsed CID: ${cidString}`)
646
665
  const { driveName: name } = this.#getCidInfo(cidString)
666
+ const linkFileName = sanitizeFilename(parsed.fileName)
647
667
 
648
- const existingFile = this.#publishedFiles.find(
649
- f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
650
- )
651
- if (existingFile) {
652
- console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
668
+ const localContent = await this.#getLocalCidContent(cidString, {
669
+ ownerAddress,
670
+ public: true,
671
+ allowHoldingFallback: true,
672
+ })
673
+ if (localContent) {
674
+ const existingFile = localContent.fileRecord
675
+ console.log(
676
+ `[MostBox] CID content already exists locally: ${cidString}`
677
+ )
653
678
  const existingHolding = this.#holdings.find(
654
679
  item => item.cid === cidString
655
680
  )
656
- const existingSize = Number(existingFile.size)
657
681
  await this.#joinCidTopicInternal(cidString, {
658
682
  server: true,
659
683
  client: false,
660
684
  })
661
685
  this.#upsertHolding({
662
686
  cid: cidString,
663
- fileName: existingFile.fileName,
687
+ fileName:
688
+ existingHolding?.fileName || existingFile?.fileName || linkFileName,
664
689
  size:
665
690
  existingHolding?.size ??
666
- (Number.isFinite(existingSize) ? existingSize : 0),
691
+ (Number.isFinite(localContent.size) ? localContent.size : 0),
667
692
  localPath:
668
- existingHolding?.localPath || existingFile.localPath || null,
669
- driveName: existingFile.driveName || name,
693
+ existingHolding?.localPath || existingFile?.localPath || null,
694
+ driveName: existingFile?.driveName || name,
670
695
  source: existingHolding?.source || 'published',
671
696
  })
672
697
  return {
673
698
  taskId,
674
- fileName: existingFile.fileName,
699
+ fileName: linkFileName,
675
700
  alreadyExists: true,
676
701
  }
677
702
  }
678
703
 
679
- const linkFileName = parsed.fileName
704
+ this.#assertDisplayNameAvailable(linkFileName, {
705
+ ownerAddress,
706
+ excludeCid: cidString,
707
+ })
680
708
 
681
709
  if (taskState.aborted) throw new Error('Download cancelled')
682
710
 
@@ -924,11 +952,14 @@ export class MostBoxEngine extends EventEmitter {
924
952
  const existingIndex = this.#publishedFiles.findIndex(
925
953
  f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
926
954
  )
955
+ this.#assertDisplayNameAvailable(sanitizedFileName, {
956
+ ownerAddress,
957
+ excludeCid: cidString,
958
+ })
927
959
  if (existingIndex !== -1) {
928
960
  const existing = this.#publishedFiles[existingIndex]
929
- if (existing.fileName !== sanitizedFileName) {
930
- throw new Error(`文件已存在: ${existing.fileName}`)
931
- }
961
+ existing.fileName = sanitizedFileName
962
+ existing.driveName = name
932
963
  existing.publishedAt = new Date().toISOString()
933
964
  } else {
934
965
  this.#publishedFiles.push({
@@ -959,6 +990,35 @@ export class MostBoxEngine extends EventEmitter {
959
990
  }
960
991
  }
961
992
 
993
+ /**
994
+ * 快速检查 most:// 链接对应的 CID 内容是否已在本机可读。
995
+ */
996
+ async getLocalCidAvailability(link, options = {}) {
997
+ this.#ensureInitialized()
998
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
999
+ const parsed = parseMostLink(link)
1000
+ if (parsed.error) {
1001
+ throw new ValidationError(parsed.error)
1002
+ }
1003
+
1004
+ const localContent = await this.#getLocalCidContent(parsed.cid, {
1005
+ ownerAddress,
1006
+ public: true,
1007
+ allowHoldingFallback: true,
1008
+ })
1009
+ if (!localContent) {
1010
+ return null
1011
+ }
1012
+
1013
+ return {
1014
+ available: true,
1015
+ cid: parsed.cid,
1016
+ fileName: sanitizeFilename(parsed.fileName),
1017
+ size: localContent.size,
1018
+ alreadyExists: true,
1019
+ }
1020
+ }
1021
+
962
1022
  /**
963
1023
  * 检测 most:// 链接当前是否能找到可下载内容,但不读取文件内容。
964
1024
  * @param {string} link - most:// 链接
@@ -978,15 +1038,17 @@ export class MostBoxEngine extends EventEmitter {
978
1038
 
979
1039
  const cidString = parsed.cid
980
1040
  const { driveName: name } = this.#getCidInfo(cidString)
981
- const existingFile = this.#publishedFiles.find(
982
- f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
983
- )
984
- if (existingFile) {
1041
+ const localContent = await this.#getLocalCidContent(cidString, {
1042
+ ownerAddress,
1043
+ public: true,
1044
+ allowHoldingFallback: true,
1045
+ })
1046
+ if (localContent) {
985
1047
  return {
986
1048
  available: true,
987
1049
  cid: cidString,
988
- fileName: existingFile.fileName,
989
- size: Number(existingFile.size) || null,
1050
+ fileName: sanitizeFilename(parsed.fileName),
1051
+ size: localContent.size,
990
1052
  alreadyExists: true,
991
1053
  }
992
1054
  }
@@ -1172,6 +1234,20 @@ export class MostBoxEngine extends EventEmitter {
1172
1234
 
1173
1235
  const { driveName } = this.#getCidInfo(fileRecord.cid)
1174
1236
 
1237
+ const existingIndex = this.#publishedFiles.findIndex(
1238
+ f => f.cid === fileRecord.cid && this.#recordMatchesOwner(f, ownerAddress)
1239
+ )
1240
+ if (existingIndex !== -1) {
1241
+ this.#trashFiles.splice(index, 1)
1242
+ this.#saveTrashMetadata()
1243
+ return this.listPublishedFiles({ ownerAddress })
1244
+ }
1245
+
1246
+ this.#assertDisplayNameAvailable(fileRecord.fileName, {
1247
+ ownerAddress,
1248
+ excludeCid: fileRecord.cid,
1249
+ })
1250
+
1175
1251
  this.#publishedFiles.push({
1176
1252
  fileName: fileRecord.fileName,
1177
1253
  cid: fileRecord.cid,
@@ -1361,6 +1437,10 @@ export class MostBoxEngine extends EventEmitter {
1361
1437
  throw new Error('File not found')
1362
1438
  }
1363
1439
  const safeFileName = sanitizeFilename(newFileName)
1440
+ this.#assertDisplayNameAvailable(safeFileName, {
1441
+ ownerAddress,
1442
+ excludeCid: cid,
1443
+ })
1364
1444
  this.#publishedFiles[index].fileName = safeFileName
1365
1445
  this.#publishedFiles[index].publishedAt = new Date().toISOString()
1366
1446
  this.#savePublishedMetadata()
@@ -1382,7 +1462,7 @@ export class MostBoxEngine extends EventEmitter {
1382
1462
  this.#ensureInitialized()
1383
1463
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1384
1464
  const prefix = oldPath + '/'
1385
- const updatedFiles = []
1465
+ const updates = []
1386
1466
 
1387
1467
  for (const file of this.#publishedFiles) {
1388
1468
  if (
@@ -1393,16 +1473,27 @@ export class MostBoxEngine extends EventEmitter {
1393
1473
  const newFileName = sanitizeFilename(
1394
1474
  remainder ? newPath + '/' + remainder : newPath
1395
1475
  )
1396
- file.fileName = newFileName
1397
- file.publishedAt = new Date().toISOString()
1398
- updatedFiles.push({
1399
- cid: file.cid,
1400
- fileName: file.fileName,
1401
- link: `most://${file.cid}?filename=${encodeURIComponent(file.fileName)}`,
1402
- })
1476
+ updates.push({ file, newFileName })
1403
1477
  }
1404
1478
  }
1405
1479
 
1480
+ for (const { file, newFileName } of updates) {
1481
+ this.#assertDisplayNameAvailable(newFileName, {
1482
+ ownerAddress,
1483
+ excludeCid: file.cid,
1484
+ })
1485
+ }
1486
+
1487
+ const updatedFiles = updates.map(({ file, newFileName }) => {
1488
+ file.fileName = newFileName
1489
+ file.publishedAt = new Date().toISOString()
1490
+ return {
1491
+ cid: file.cid,
1492
+ fileName: file.fileName,
1493
+ link: `most://${file.cid}?filename=${encodeURIComponent(file.fileName)}`,
1494
+ }
1495
+ })
1496
+
1406
1497
  if (updatedFiles.length > 0) {
1407
1498
  this.#savePublishedMetadata()
1408
1499
  }
@@ -1678,28 +1769,23 @@ export class MostBoxEngine extends EventEmitter {
1678
1769
  options = {}
1679
1770
  ) {
1680
1771
  this.#ensureInitialized()
1772
+ if (typeof offset === 'object' && offset !== null) {
1773
+ options = offset
1774
+ offset = 0
1775
+ limit = DEFAULT_READ_LIMIT
1776
+ }
1681
1777
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1682
1778
 
1683
- const fileRecord = this.#publishedFiles.find(
1684
- f =>
1685
- f.cid === cid &&
1686
- (options.public || this.#recordMatchesOwner(f, ownerAddress))
1687
- )
1688
- if (!fileRecord) {
1779
+ const localContent = await this.#getLocalCidContent(cid, {
1780
+ ownerAddress,
1781
+ public: options.public,
1782
+ })
1783
+ if (!localContent) {
1689
1784
  throw new Error('File not found')
1690
1785
  }
1691
1786
 
1692
- const drive = await this.#getDriveForFile(fileRecord)
1693
-
1694
- // Hyperdrive 中 key 为 '/' + cid
1695
1787
  const driveKey = '/' + cid
1696
- const entry = await drive.entry(driveKey, {
1697
- wait: true,
1698
- timeout: DRIVE_ENTRY_TIMEOUT,
1699
- })
1700
- if (!entry || !entry.value) {
1701
- throw new Error('File content not available')
1702
- }
1788
+ const { drive } = localContent
1703
1789
 
1704
1790
  const chunks = []
1705
1791
  const stream = drive.createReadStream(driveKey, {
@@ -1743,25 +1829,16 @@ export class MostBoxEngine extends EventEmitter {
1743
1829
  this.#ensureInitialized()
1744
1830
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1745
1831
 
1746
- const fileRecord = this.#publishedFiles.find(
1747
- f =>
1748
- f.cid === cid &&
1749
- (options.public || this.#recordMatchesOwner(f, ownerAddress))
1750
- )
1751
- if (!fileRecord) {
1832
+ const localContent = await this.#getLocalCidContent(cid, {
1833
+ ownerAddress,
1834
+ public: options.public,
1835
+ })
1836
+ if (!localContent) {
1752
1837
  throw new Error('File not found')
1753
1838
  }
1754
1839
 
1755
- const drive = await this.#getDriveForFile(fileRecord)
1756
-
1757
1840
  const driveKey = '/' + cid
1758
- const entry = await drive.entry(driveKey, {
1759
- wait: true,
1760
- timeout: DRIVE_ENTRY_TIMEOUT,
1761
- })
1762
- if (!entry || !entry.value || !entry.value.blob) {
1763
- throw new Error('File content not available')
1764
- }
1841
+ const { drive, entry, fileRecord } = localContent
1765
1842
 
1766
1843
  const totalSize = entry.value.blob.byteLength || 0
1767
1844
 
@@ -1808,19 +1885,63 @@ export class MostBoxEngine extends EventEmitter {
1808
1885
  return { buffer, fileName: fileRecord.fileName, totalSize }
1809
1886
  }
1810
1887
 
1811
- /**
1812
- * 获取文件对应的 drive,如果不存在则创建并同步
1813
- */
1814
- async #getDriveForFile(fileRecord) {
1815
- let drive = this.#drives.get(fileRecord.driveName)
1816
- if (!drive) {
1817
- drive = await this.#getOrCreateDrive(fileRecord.driveName, {
1818
- server: true,
1819
- client: true,
1820
- })
1888
+ async #hasLocalDriveContent(drive, key) {
1889
+ try {
1890
+ return await drive.has(key)
1891
+ } catch {
1892
+ return false
1893
+ }
1894
+ }
1895
+
1896
+ async #getLocalCidContent(cid, options = {}) {
1897
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1898
+ const fileRecord = this.#publishedFiles.find(
1899
+ f =>
1900
+ f.cid === cid &&
1901
+ (options.public || this.#recordMatchesOwner(f, ownerAddress))
1902
+ )
1903
+ if (!options.allowHoldingFallback && !fileRecord) {
1904
+ return null
1905
+ }
1906
+ const holding = this.#holdings.find(item => item.cid === cid)
1907
+ const { driveName } = this.#getCidInfo(cid)
1908
+ const drive = await this.#getOrCreateDrive(
1909
+ fileRecord?.driveName || holding?.driveName || driveName,
1910
+ { server: true, client: false }
1911
+ )
1912
+ const driveKey = '/' + cid
1913
+
1914
+ try {
1915
+ const entry = await drive.entry(driveKey, { wait: false })
1916
+ if (!entry?.value?.blob) {
1917
+ return null
1918
+ }
1919
+ const hasContent = await this.#hasLocalDriveContent(drive, driveKey)
1920
+ if (!hasContent) {
1921
+ return null
1922
+ }
1923
+
1924
+ const size =
1925
+ Number(entry.value.blob.byteLength) ||
1926
+ Number(fileRecord?.size) ||
1927
+ Number(holding?.size) ||
1928
+ 0
1929
+ return {
1930
+ drive,
1931
+ entry,
1932
+ size,
1933
+ fileRecord: fileRecord || {
1934
+ cid,
1935
+ fileName: holding?.fileName || cid,
1936
+ driveName: holding?.driveName || driveName,
1937
+ localPath: holding?.localPath || null,
1938
+ size,
1939
+ ownerAddress,
1940
+ },
1941
+ }
1942
+ } catch {
1943
+ return null
1821
1944
  }
1822
- await this.#syncDrive(drive)
1823
- return drive
1824
1945
  }
1825
1946
 
1826
1947
  // --- 频道管理 ---
@@ -1834,6 +1955,7 @@ export class MostBoxEngine extends EventEmitter {
1834
1955
  async createChannel(name, type = 'personal', options = {}) {
1835
1956
  this.#ensureInitialized()
1836
1957
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1958
+ const channelType = String(type || 'personal').trim() || 'personal'
1837
1959
 
1838
1960
  if (!CHANNEL_NAME_REGEX.test(name)) {
1839
1961
  throw new Error('频道名只能包含字母、数字、下划线和连字符')
@@ -1879,7 +2001,7 @@ export class MostBoxEngine extends EventEmitter {
1879
2001
  discoveryKey: b4a.toString(discoveryKey, 'hex'),
1880
2002
  coreKey: b4a.toString(core.key, 'hex'),
1881
2003
  createdAt: new Date().toISOString(),
1882
- type,
2004
+ type: channelType,
1883
2005
  ownerAddress,
1884
2006
  members: ownerAddress ? [ownerAddress] : [],
1885
2007
  remoteCoreKeys: [],
@@ -2105,12 +2227,19 @@ export class MostBoxEngine extends EventEmitter {
2105
2227
  listChannels(options = {}) {
2106
2228
  this.#ensureInitialized()
2107
2229
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
2230
+ const type = String(options.type || '').trim()
2231
+ const excludeType = String(options.excludeType || '').trim()
2108
2232
 
2109
2233
  return this.#channels
2110
2234
  .filter(c => {
2111
2235
  if (!ownerAddress) return true
2112
2236
  return Array.isArray(c.members) && c.members.includes(ownerAddress)
2113
2237
  })
2238
+ .filter(c => {
2239
+ if (type) return c.type === type
2240
+ if (excludeType) return c.type !== excludeType
2241
+ return true
2242
+ })
2114
2243
  .map(c => ({
2115
2244
  name: c.name,
2116
2245
  coreKey: c.coreKey,
@@ -2172,7 +2301,11 @@ export class MostBoxEngine extends EventEmitter {
2172
2301
  const start = Math.max(0, total - offset - limit)
2173
2302
  const end = total - offset
2174
2303
 
2175
- return unique.slice(start, end).map(({ _coreKey, _index, ...msg }) => msg)
2304
+ return unique
2305
+ .slice(start, end)
2306
+ .map(({ _coreKey, _index, ...msg }) =>
2307
+ this.#normalizeChannelMessageForResponse(name, msg)
2308
+ )
2176
2309
  }
2177
2310
 
2178
2311
  /**
@@ -2304,6 +2437,44 @@ export class MostBoxEngine extends EventEmitter {
2304
2437
  }
2305
2438
  }
2306
2439
 
2440
+ #normalizeChannelMessageForResponse(channelName, message) {
2441
+ const attachment = message?.attachment
2442
+ if (!attachment?.cid || !attachment.fileName) {
2443
+ return message
2444
+ }
2445
+
2446
+ const oldFileName = sanitizeFilename(String(attachment.fileName))
2447
+ const channelPrefix = `${CHAT_FILE_ROOT}/${channelName}/`
2448
+ const fileName = oldFileName.startsWith(channelPrefix)
2449
+ ? oldFileName
2450
+ : `${channelPrefix}${getPathBaseName(oldFileName)}`
2451
+ const link = buildMostLink(attachment.cid, fileName)
2452
+ const content =
2453
+ typeof message.content === 'string' &&
2454
+ (message.content === attachment.link ||
2455
+ parseMostLink(message.content).cid === attachment.cid)
2456
+ ? link
2457
+ : message.content
2458
+
2459
+ if (
2460
+ fileName === attachment.fileName &&
2461
+ link === attachment.link &&
2462
+ content === message.content
2463
+ ) {
2464
+ return message
2465
+ }
2466
+
2467
+ return {
2468
+ ...message,
2469
+ content,
2470
+ attachment: {
2471
+ ...attachment,
2472
+ fileName,
2473
+ link,
2474
+ },
2475
+ }
2476
+ }
2477
+
2307
2478
  #getCidInfo(cid) {
2308
2479
  return getCidInfo(cid)
2309
2480
  }
@@ -2684,6 +2855,31 @@ export class MostBoxEngine extends EventEmitter {
2684
2855
  return normalizeOwnerAddress(record.ownerAddress) === normalizedOwner
2685
2856
  }
2686
2857
 
2858
+ #assertDisplayNameAvailable(fileName, options = {}) {
2859
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
2860
+ const safeFileName = sanitizeFilename(fileName)
2861
+ const folder = getDisplayPathFolder(safeFileName)
2862
+ const baseName = getPathBaseName(safeFileName)
2863
+ const conflict = this.#publishedFiles.find(file => {
2864
+ if (
2865
+ options.excludeCid &&
2866
+ file.cid === options.excludeCid &&
2867
+ this.#recordMatchesOwner(file, ownerAddress)
2868
+ ) {
2869
+ return false
2870
+ }
2871
+ if (!this.#recordMatchesOwner(file, ownerAddress)) return false
2872
+ const existingFileName = sanitizeFilename(file.fileName)
2873
+ return (
2874
+ getDisplayPathFolder(existingFileName) === folder &&
2875
+ getPathBaseName(existingFileName) === baseName
2876
+ )
2877
+ })
2878
+ if (conflict) {
2879
+ throw new ConflictError(`已有同名文件: ${safeFileName}`)
2880
+ }
2881
+ }
2882
+
2687
2883
  #hasPublishedReference(cid) {
2688
2884
  return this.#publishedFiles.some(file => file.cid === cid)
2689
2885
  }
@@ -2732,20 +2928,6 @@ export class MostBoxEngine extends EventEmitter {
2732
2928
  }
2733
2929
  }
2734
2930
 
2735
- async #syncDrive(drive, timeout = DRIVE_SYNC_TIMEOUT) {
2736
- try {
2737
- const updated = await Promise.race([
2738
- drive.update(),
2739
- new Promise((_, reject) =>
2740
- setTimeout(() => reject(new Error('Sync timeout')), timeout)
2741
- ),
2742
- ])
2743
- return updated
2744
- } catch {
2745
- return false
2746
- }
2747
- }
2748
-
2749
2931
  #getMetadataPath() {
2750
2932
  return path.join(this.#options.dataPath, 'published-files.json')
2751
2933
  }
@@ -2903,7 +3085,10 @@ export class MostBoxEngine extends EventEmitter {
2903
3085
  if (entry && entry.type === 'message') {
2904
3086
  this.emit('channel:message', {
2905
3087
  channel: channelName,
2906
- message: entry,
3088
+ message: this.#normalizeChannelMessageForResponse(
3089
+ channelName,
3090
+ entry
3091
+ ),
2907
3092
  })
2908
3093
  }
2909
3094
  } catch (err) {
@@ -1,8 +1,9 @@
1
1
  import { createAvatar } from '@dicebear/core'
2
2
  import { botttsNeutral } from '@dicebear/collection'
3
3
 
4
- export function generateAvatar(address) {
5
- if (!address) return '/pwa-512x512.png'
4
+ export function generateAvatar(address, avatar) {
5
+ if (avatar) return avatar
6
+ if (!address) return '/avatar.png'
6
7
  return createAvatar(botttsNeutral, {
7
8
  seed: 'most.box@' + address,
8
9
  flip: true,