@lf_maple/koishi-plugin-phi-plugin-full 0.1.80

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 (629) hide show
  1. package/LICENSE +22 -0
  2. package/core/.github/ISSUE_TEMPLATE/bug.yaml +133 -0
  3. package/core/.github/ISSUE_TEMPLATE/config.yml +1 -0
  4. package/core/.github/ISSUE_TEMPLATE/enhancement.yml +44 -0
  5. package/core/.vscode/c_cpp_properties.json +18 -0
  6. package/core/.vscode/launch.json +24 -0
  7. package/core/.vscode/settings.json +59 -0
  8. package/core/CHANGELOG.md +184 -0
  9. package/core/LICENSE +674 -0
  10. package/core/README.md +330 -0
  11. package/core/README_en.md +285 -0
  12. package/core/apps/Dan.js +129 -0
  13. package/core/apps/RankList.js +322 -0
  14. package/core/apps/aliasProposal.js +223 -0
  15. package/core/apps/apiSetting.js +592 -0
  16. package/core/apps/b19.js +1329 -0
  17. package/core/apps/botClient.js +64 -0
  18. package/core/apps/chart.js +692 -0
  19. package/core/apps/guessGame/guessIll.js +821 -0
  20. package/core/apps/guessGame/guessLetter.js +831 -0
  21. package/core/apps/guessGame/guessTips.js +254 -0
  22. package/core/apps/guessGame/letterGameUtils.js +74 -0
  23. package/core/apps/guessGame.js +214 -0
  24. package/core/apps/help.js +117 -0
  25. package/core/apps/manage.js +288 -0
  26. package/core/apps/market.js +243 -0
  27. package/core/apps/money.js +1064 -0
  28. package/core/apps/phisong.js +1418 -0
  29. package/core/apps/session.js +664 -0
  30. package/core/apps/setting.js +468 -0
  31. package/core/apps/update.js +541 -0
  32. package/core/apps/user.js +968 -0
  33. package/core/components/Config.js +249 -0
  34. package/core/components/Data.js +304 -0
  35. package/core/components/FileWatcherRegistry.js +102 -0
  36. package/core/components/Logger.js +11 -0
  37. package/core/components/ProcessCleanup.js +113 -0
  38. package/core/components/Version.js +56 -0
  39. package/core/components/YamlReader.js +157 -0
  40. package/core/components/baseClass.js +287 -0
  41. package/core/components/common.js +47 -0
  42. package/core/components/index.js +13 -0
  43. package/core/components/platform/index.js +79 -0
  44. package/core/components/platform/koishi.js +800 -0
  45. package/core/components/platform/koishiDatabaseRedis.js +391 -0
  46. package/core/components/platform/memoryRedis.js +209 -0
  47. package/core/components/platform/types.d.ts +272 -0
  48. package/core/components/platform/yunzai.js +604 -0
  49. package/core/components/segment.js +4 -0
  50. package/core/config/config//350/277/231/351/207/214/346/230/257/351/205/215/347/275/256/346/226/207/344/273/266/347/233/256/345/275/225.txt +0 -0
  51. package/core/config/default_config/banGroup.yaml +14 -0
  52. package/core/config/default_config/config.yaml +159 -0
  53. package/core/config/default_config/nickconfig.yaml +6 -0
  54. package/core/config/default_config/otherinfo.yaml +51 -0
  55. package/core/guoba.support.js +597 -0
  56. package/core/index.js +85 -0
  57. package/core/lib/AES.js +40 -0
  58. package/core/lib/Base64.js +22 -0
  59. package/core/lib/ByteReader.js +168 -0
  60. package/core/lib/GameProgress.js +37 -0
  61. package/core/lib/GameRecord.js +47 -0
  62. package/core/lib/GameSettings.js +24 -0
  63. package/core/lib/GameUser.js +19 -0
  64. package/core/lib/Level.js +6 -0
  65. package/core/lib/LevelRecord.js +9 -0
  66. package/core/lib/PhigrosUser.js +135 -0
  67. package/core/lib/SaveManager.js +117 -0
  68. package/core/lib/Summary.js +47 -0
  69. package/core/lib/TapTap/CompleteQRCodeData.js +27 -0
  70. package/core/lib/TapTap/LCHelper.js +81 -0
  71. package/core/lib/TapTap/TapTapHelper.js +170 -0
  72. package/core/lib/TapTap/getQRcode.js +61 -0
  73. package/core/lib/Util.js +30 -0
  74. package/core/lib/getQRcode.js +73 -0
  75. package/core/lib/http.js +105 -0
  76. package/core/model/README.md +3 -0
  77. package/core/model/api/aliasProposalService.js +151 -0
  78. package/core/model/api/apiVersion.js +67 -0
  79. package/core/model/api/autoSeekApi.js +165 -0
  80. package/core/model/api/botApiAuth.js +208 -0
  81. package/core/model/api/botSyncService.js +78 -0
  82. package/core/model/api/gameRecordPayload.js +24 -0
  83. package/core/model/api/makeRequest.js +1004 -0
  84. package/core/model/api/makeRequestFnc.js +17 -0
  85. package/core/model/api/phiApiClient.js +53 -0
  86. package/core/model/api/phiApiErrors.js +121 -0
  87. package/core/model/filesystem/getFile.js +256 -0
  88. package/core/model/filesystem/path.js +84 -0
  89. package/core/model/game/Chart.js +57 -0
  90. package/core/model/game/LevelRecordInfo.js +109 -0
  91. package/core/model/game/SongsInfo.js +46 -0
  92. package/core/model/game/b30Analysis.js +93 -0
  93. package/core/model/game/chartParser/Chart.js +23 -0
  94. package/core/model/game/chartParser/JudgeLine.js +38 -0
  95. package/core/model/game/chartParser/base/JudgeLineEvent.js +99 -0
  96. package/core/model/game/chartParser/base/Note.js +44 -0
  97. package/core/model/game/chartParser/base/SpeedEvent.js +25 -0
  98. package/core/model/game/chartParser/base/baseType.js +2 -0
  99. package/core/model/game/constNum.js +215 -0
  100. package/core/model/game/fCompute.js +711 -0
  101. package/core/model/game/getChartTag.js +140 -0
  102. package/core/model/game/getComment.js +132 -0
  103. package/core/model/game/getInfo.js +1069 -0
  104. package/core/model/game/getRksRank.js +77 -0
  105. package/core/model/game/markdown.js +77 -0
  106. package/core/model/integrations/Vika.js +103 -0
  107. package/core/model/integrations/getInfoFromTap.js +80 -0
  108. package/core/model/render/getPic.js +79 -0
  109. package/core/model/render/picmodle.js +496 -0
  110. package/core/model/render/progress-bar.js +44 -0
  111. package/core/model/render/puppeteer.js +431 -0
  112. package/core/model/render/renderPressureHistory.js +71 -0
  113. package/core/model/render/send.js +94 -0
  114. package/core/model/save/Save.js +953 -0
  115. package/core/model/save/analyzeSaveHistory.js +413 -0
  116. package/core/model/save/getBackup.js +328 -0
  117. package/core/model/save/getSave.js +90 -0
  118. package/core/model/save/getSaveFromApi.js +46 -0
  119. package/core/model/save/saveHistory.js +586 -0
  120. package/core/model/save/scoreHistory.js +101 -0
  121. package/core/model/theme/catalog.js +192 -0
  122. package/core/model/theme/installGuard.js +275 -0
  123. package/core/model/theme/installLock.js +174 -0
  124. package/core/model/theme/installer.js +398 -0
  125. package/core/model/theme/manager.js +408 -0
  126. package/core/model/theme/marketClient.js +294 -0
  127. package/core/model/theme/paths.js +42 -0
  128. package/core/model/theme/policy.js +54 -0
  129. package/core/model/theme/recovery.js +167 -0
  130. package/core/model/theme/useService.js +187 -0
  131. package/core/model/type/aliasProposal.d.ts +45 -0
  132. package/core/model/type/type.js +420 -0
  133. package/core/model/user/apiPermission.js +62 -0
  134. package/core/model/user/getBanGroup.js +120 -0
  135. package/core/model/user/getNotes.js +48 -0
  136. package/core/model/user/pluginData.js +101 -0
  137. package/core/model/user/userCredentialStore.js +154 -0
  138. package/core/model/user/userCredentials.js +860 -0
  139. package/core/package.json +60 -0
  140. package/core/resources/0608badSave/save.json +1 -0
  141. package/core/resources/LICENSE +201 -0
  142. package/core/resources/html/analyzeSaveHistory/analyzeSaveHistory.art +220 -0
  143. package/core/resources/html/analyzeSaveHistory/analyzeSaveHistory.css +345 -0
  144. package/core/resources/html/analyzeSaveHistory/analyzeSaveHistory.html +434 -0
  145. package/core/resources/html/arcgrosB19/arcgros.png +0 -0
  146. package/core/resources/html/arcgrosB19/arcgrosB19-orz.html +276 -0
  147. package/core/resources/html/arcgrosB19/arcgrosB19.art +168 -0
  148. package/core/resources/html/arcgrosB19/arcgrosB19.css +637 -0
  149. package/core/resources/html/atlas/atlas-orz.html +313 -0
  150. package/core/resources/html/atlas/atlas.art +194 -0
  151. package/core/resources/html/atlas/atlas.css +695 -0
  152. package/core/resources/html/avatar/-SURREALISM-.png +0 -0
  153. package/core/resources/html/avatar/Alkaid.png +0 -0
  154. package/core/resources/html/avatar/Aphasia.png +0 -0
  155. package/core/resources/html/avatar/Artificial Existence.png +0 -0
  156. package/core/resources/html/avatar/Bonus Time.png +0 -0
  157. package/core/resources/html/avatar/Burn.png +0 -0
  158. package/core/resources/html/avatar/Cereris1.png +0 -0
  159. package/core/resources/html/avatar/Cereris2.png +0 -0
  160. package/core/resources/html/avatar/Cereris3.png +0 -0
  161. package/core/resources/html/avatar/Christmas -2019.png +0 -0
  162. package/core/resources/html/avatar/Cipher1.png +0 -0
  163. package/core/resources/html/avatar/Class Memories.png +0 -0
  164. package/core/resources/html/avatar/Credits.png +0 -0
  165. package/core/resources/html/avatar/Dash.png +0 -0
  166. package/core/resources/html/avatar/Dlyrotz.png +0 -0
  167. package/core/resources/html/avatar/Doppelganger1.png +0 -0
  168. package/core/resources/html/avatar/Doppelganger2.png +0 -0
  169. package/core/resources/html/avatar/Drop it.png +0 -0
  170. package/core/resources/html/avatar/Energy Synergy Matrix1.png +0 -0
  171. package/core/resources/html/avatar/Energy Synergy Matrix2.png +0 -0
  172. package/core/resources/html/avatar/Energy Synergy Matrix3.png +0 -0
  173. package/core/resources/html/avatar/Engine x Start!! (melody mix).png +0 -0
  174. package/core/resources/html/avatar/Engine x Start!! extra 1 -by cycats.png +0 -0
  175. package/core/resources/html/avatar/Eradication Catastrophe.png +0 -0
  176. package/core/resources/html/avatar/FULi AUTO SHOOTER extra 1 -by cycats.png +0 -0
  177. package/core/resources/html/avatar/FULi AUTO SHOOTER.png +0 -0
  178. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/264-Special1.png +0 -0
  179. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/264-Special2.png +0 -0
  180. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2641.png +0 -0
  181. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2642.png +0 -0
  182. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2643.png +0 -0
  183. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2644.png +0 -0
  184. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2645.png +0 -0
  185. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2646.png +0 -0
  186. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2647.png +0 -0
  187. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2648.png +0 -0
  188. package/core/resources/html/avatar/Feast/350/277/234/344/270/234/344/271/213/345/256/2649.png +0 -0
  189. package/core/resources/html/avatar/Find_Me.png +0 -0
  190. package/core/resources/html/avatar/Geopelia 1 -by cycats.png +0 -0
  191. package/core/resources/html/avatar/Geopelia 2 -by cycats.png +0 -0
  192. package/core/resources/html/avatar/Geopelia 3 -by /344/275/225/345/221/265.png +0 -0
  193. package/core/resources/html/avatar/Geopelia in WEPLAY -by /346/265/267/344/272/247.png +0 -0
  194. package/core/resources/html/avatar/Gino-AprilFool.png +0 -0
  195. package/core/resources/html/avatar/Glaciaxion.png +0 -0
  196. package/core/resources/html/avatar/HumaN.png +0 -0
  197. package/core/resources/html/avatar/Indelible Scar.png +0 -0
  198. package/core/resources/html/avatar/Introduction.png +0 -0
  199. package/core/resources/html/avatar/Khronostasis Katharsis.png +0 -0
  200. package/core/resources/html/avatar/MARENOL.png +0 -0
  201. package/core/resources/html/avatar/Marenol extra 1 -by /345/226/265n/350/221/265.png +0 -0
  202. package/core/resources/html/avatar/Non-Melodic Ragez (MUG Edit).png +0 -0
  203. package/core/resources/html/avatar/Non-Melodic Ragez extra 1 -by cycats.png +0 -0
  204. package/core/resources/html/avatar/Oblivion +0 -0
  205. package/core/resources/html/avatar/OblivionPHIN.png +0 -0
  206. package/core/resources/html/avatar/Para.png +0 -0
  207. package/core/resources/html/avatar/Pixel Rebelz1.png +0 -0
  208. package/core/resources/html/avatar/Pixel Rebelz2.png +0 -0
  209. package/core/resources/html/avatar/Pixel Rebelz3.png +0 -0
  210. package/core/resources/html/avatar/RIPPER extra 1 -by cycats.png +0 -0
  211. package/core/resources/html/avatar/RIPPER extra 2 -by /344/275/225/345/221/265.png +0 -0
  212. package/core/resources/html/avatar/Sein.png +0 -0
  213. package/core/resources/html/avatar/ShineAfter-/346/260/264/351/235/222-Special.png +0 -0
  214. package/core/resources/html/avatar/ShineAfter-/346/260/264/351/235/222-Special2.png +0 -0
  215. package/core/resources/html/avatar/ShineAfter-/346/260/264/351/235/222-/346/231/256/351/200/232.png +0 -0
  216. package/core/resources/html/avatar/ShineAfter-/346/260/264/351/235/222-/346/233/262/347/273/230Ver.png +0 -0
  217. package/core/resources/html/avatar/ShineAfter-/351/270/240-Special.png +0 -0
  218. package/core/resources/html/avatar/ShineAfter-/351/270/240-/346/231/256/351/200/232.png +0 -0
  219. package/core/resources/html/avatar/ShineAfter-/351/270/240-/346/233/262/347/273/230Ver.png +0 -0
  220. package/core/resources/html/avatar/ShineAfter-/351/270/240/345/222/214/346/260/264/351/235/222-Special.png +0 -0
  221. package/core/resources/html/avatar/Snow Desert.png +0 -0
  222. package/core/resources/html/avatar/Sparkle New Life.png +0 -0
  223. package/core/resources/html/avatar/Sultan Rage.png +0 -0
  224. package/core/resources/html/avatar/Winter /342/206/221cube/342/206/223.png +0 -0
  225. package/core/resources/html/avatar/Xenolith.png +0 -0
  226. package/core/resources/html/avatar/[PRAW].png +0 -0
  227. package/core/resources/html/avatar/dB doll.png +0 -0
  228. package/core/resources/html/avatar/ripper.png +0 -0
  229. package/core/resources/html/avatar//343/202/202/343/201/272/343/202/202/343/201/2721.png +0 -0
  230. package/core/resources/html/avatar//343/202/202/343/201/272/343/202/202/343/201/2722.png +0 -0
  231. package/core/resources/html/avatar//344/270/207/345/220/250/345/214/277/345/220/215/344/277/241.png +0 -0
  232. package/core/resources/html/avatar//344/272/221/345/245/263/345/255/251 extra 1 -by /345/276/241/345/235/202/346/236/234/345/255/220.png +0 -0
  233. package/core/resources/html/avatar//344/272/221/345/245/263/345/255/251.png +0 -0
  234. package/core/resources/html/avatar//345/205/211.png +0 -0
  235. package/core/resources/html/avatar//345/274/200/345/277/203/347/227/2051.png +0 -0
  236. package/core/resources/html/avatar//345/274/200/345/277/203/347/227/2052.png +0 -0
  237. package/core/resources/html/avatar//346/260/264/351/235/222-/344/270/200/345/221/250/345/271/264/347/233/270/350/201/232.png +0 -0
  238. package/core/resources/html/avatar//346/267/267/344/271/261-Confusion.png +0 -0
  239. package/core/resources/html/avatar//347/213/202/345/226/234/350/230/255/350/210/2361.png +0 -0
  240. package/core/resources/html/avatar//347/213/202/345/226/234/350/230/255/350/210/2362.png +0 -0
  241. package/core/resources/html/avatar//347/223/267/345/262/201-Special.png +0 -0
  242. package/core/resources/html/avatar//347/223/267/345/262/2011.png +0 -0
  243. package/core/resources/html/avatar//347/223/267/345/262/2012.png +0 -0
  244. package/core/resources/html/avatar//347/223/267/345/262/2013.png +0 -0
  245. package/core/resources/html/avatar//347/223/267/345/262/2014.png +0 -0
  246. package/core/resources/html/avatar//347/223/267/345/262/2015.png +0 -0
  247. package/core/resources/html/avatar//347/223/267/345/262/2016.png +0 -0
  248. package/core/resources/html/avatar//347/223/267/345/262/2017.png +0 -0
  249. package/core/resources/html/avatar//347/233/217/350/214/227-Special.png +0 -0
  250. package/core/resources/html/avatar//347/233/217/350/214/2271.png +0 -0
  251. package/core/resources/html/avatar//347/233/217/350/214/2272.png +0 -0
  252. package/core/resources/html/avatar//347/233/217/350/214/2273.png +0 -0
  253. package/core/resources/html/avatar//350/217/257/347/201/257/347/210/2611.png +0 -0
  254. package/core/resources/html/avatar//350/217/257/347/201/257/347/210/2612.png +0 -0
  255. package/core/resources/html/avatar//350/220/244/347/201/253/350/231/253/343/201/256/346/200/2501.png +0 -0
  256. package/core/resources/html/avatar//350/220/244/347/201/253/350/231/253/343/201/256/346/200/2502.png +0 -0
  257. package/core/resources/html/avatar//351/235/222/350/212/275-Special.png +0 -0
  258. package/core/resources/html/avatar//351/235/222/350/212/275.png +0 -0
  259. package/core/resources/html/avatar//351/243/216/345/261/277.png +0 -0
  260. package/core/resources/html/avatar//351/270/240-AprilFool.png +0 -0
  261. package/core/resources/html/avatar//351/270/240-/344/270/200/345/221/250/345/271/264/347/233/270/350/201/232.png +0 -0
  262. package/core/resources/html/b19/b19.art +490 -0
  263. package/core/resources/html/b19/b19.css +1261 -0
  264. package/core/resources/html/b19/b19666.art +350 -0
  265. package/core/resources/html/b19/b19666.css +1409 -0
  266. package/core/resources/html/b19/b19666.html +481 -0
  267. package/core/resources/html/b19/b19title.css +120 -0
  268. package/core/resources/html/b19/best19.html +853 -0
  269. package/core/resources/html/b19/dss2-orz.html +428 -0
  270. package/core/resources/html/b19/dss2.art +298 -0
  271. package/core/resources/html/b19/dss2.css +776 -0
  272. package/core/resources/html/b19/res/666.jpeg +0 -0
  273. package/core/resources/html/chap/chap-orz.html +93 -0
  274. package/core/resources/html/chap/chap.art +87 -0
  275. package/core/resources/html/chap/chap.css +365 -0
  276. package/core/resources/html/chartImg/chartImg.art +169 -0
  277. package/core/resources/html/chartImg/chartImg.css +439 -0
  278. package/core/resources/html/chartImg/echarts-wordcloud.min.js +3 -0
  279. package/core/resources/html/chartImg/echarts.js +85796 -0
  280. package/core/resources/html/chartImg/img/Drag.png +0 -0
  281. package/core/resources/html/chartImg/img/DragHL.png +0 -0
  282. package/core/resources/html/chartImg/img/Flick.png +0 -0
  283. package/core/resources/html/chartImg/img/FlickHL.png +0 -0
  284. package/core/resources/html/chartImg/img/Hold.png +0 -0
  285. package/core/resources/html/chartImg/img/HoldHL.png +0 -0
  286. package/core/resources/html/chartImg/img/Hold_End.png +0 -0
  287. package/core/resources/html/chartImg/img/Hold_Head.png +0 -0
  288. package/core/resources/html/chartImg/img/Hold_HeadHL.png +0 -0
  289. package/core/resources/html/chartImg/img/Tap.png +0 -0
  290. package/core/resources/html/chartImg/img/TapHL.png +0 -0
  291. package/core/resources/html/chartInfo/chartInfo-orz.html +250 -0
  292. package/core/resources/html/chartInfo/chartInfo.art +222 -0
  293. package/core/resources/html/chartInfo/chartInfo.css +265 -0
  294. package/core/resources/html/chartInfo/echarts-wordcloud.min.js +3 -0
  295. package/core/resources/html/chartInfo/echarts.js +85796 -0
  296. package/core/resources/html/clg/clg-orz.html +218 -0
  297. package/core/resources/html/clg/clg.art +88 -0
  298. package/core/resources/html/clg/clg.css +230 -0
  299. package/core/resources/html/clg/demo.jpg +0 -0
  300. package/core/resources/html/common/common.css +319 -0
  301. package/core/resources/html/common/css/playerInfo.css +136 -0
  302. package/core/resources/html/common/font/Aldrich-Regular.ttf +0 -0
  303. package/core/resources/html/common/font/HIMALAYA.TTF +0 -0
  304. package/core/resources/html/common/font/NotoColorEmoji-Regular.ttf +0 -0
  305. package/core/resources/html/common/font/NotoSans-Regular.ttf +0 -0
  306. package/core/resources/html/common/font/NotoSansArabic.ttf +0 -0
  307. package/core/resources/html/common/font/NotoSansCanadianAboriginal.ttf +0 -0
  308. package/core/resources/html/common/font/NotoSansJP.ttf +0 -0
  309. package/core/resources/html/common/font/NotoSansKannada.ttf +0 -0
  310. package/core/resources/html/common/font/NotoSansMath-Regular.ttf +0 -0
  311. package/core/resources/html/common/font/NotoSansSymbols2.ttf +0 -0
  312. package/core/resources/html/common/font/phi.ttf +0 -0
  313. package/core/resources/html/common/font//345/220/236/345/274/245/346/201/260/344/277/212.ttf +0 -0
  314. package/core/resources/html/common/layout/default.art +324 -0
  315. package/core/resources/html/common/layout/elem.art +21 -0
  316. package/core/resources/html/common/theme/snow/snow.css +17 -0
  317. package/core/resources/html/common/theme/snow/snow.js +19 -0
  318. package/core/resources/html/common/theme/star/star.js +201 -0
  319. package/core/resources/html/common/theme/topText/topText.css +15 -0
  320. package/core/resources/html/common/theme/topText/topText.js +30 -0
  321. package/core/resources/html/difficultyHistory/difficultyHistory-orz.html +361 -0
  322. package/core/resources/html/difficultyHistory/difficultyHistory.art +102 -0
  323. package/core/resources/html/difficultyHistory/difficultyHistory.css +205 -0
  324. package/core/resources/html/guess/guess-orz.html +50 -0
  325. package/core/resources/html/guess/guess.art +51 -0
  326. package/core/resources/html/guess/guess.css +33 -0
  327. package/core/resources/html/help/help-orz.html +73 -0
  328. package/core/resources/html/help/help.art +84 -0
  329. package/core/resources/html/help/help.css +266 -0
  330. package/core/resources/html/historyB30/historyB30-orz.html +168 -0
  331. package/core/resources/html/historyB30/historyB30.art +143 -0
  332. package/core/resources/html/historyB30/historyB30.css +270 -0
  333. package/core/resources/html/ill/ill-orz.html +21 -0
  334. package/core/resources/html/ill/ill.art +23 -0
  335. package/core/resources/html/ill/ill.css +79 -0
  336. package/core/resources/html/jrrp/ShineAfter.removebg.png +0 -0
  337. package/core/resources/html/jrrp/jrrp-orz.html +68 -0
  338. package/core/resources/html/jrrp/jrrp.art +58 -0
  339. package/core/resources/html/jrrp/jrrp.css +251 -0
  340. package/core/resources/html/list/list-orz.html +35 -0
  341. package/core/resources/html/list/list.art +67 -0
  342. package/core/resources/html/list/list.css +259 -0
  343. package/core/resources/html/lvsco/demo.jpg +0 -0
  344. package/core/resources/html/lvsco/lvsco-orz.html +334 -0
  345. package/core/resources/html/lvsco/lvsco.art +314 -0
  346. package/core/resources/html/lvsco/lvsco.css +706 -0
  347. package/core/resources/html/market/detail.art +44 -0
  348. package/core/resources/html/market/market.art +86 -0
  349. package/core/resources/html/market/market.css +64 -0
  350. package/core/resources/html/newSong/newSong.art +34 -0
  351. package/core/resources/html/newSong/newSong.css +48 -0
  352. package/core/resources/html/newSong/newSong.html +23 -0
  353. package/core/resources/html/newnotice/newnotice.art +47 -0
  354. package/core/resources/html/newnotice/newnotice.css +83 -0
  355. package/core/resources/html/otherimg/0.png +0 -0
  356. package/core/resources/html/otherimg/1.png +0 -0
  357. package/core/resources/html/otherimg/2.png +0 -0
  358. package/core/resources/html/otherimg/3.png +0 -0
  359. package/core/resources/html/otherimg/4.png +0 -0
  360. package/core/resources/html/otherimg/5.png +0 -0
  361. package/core/resources/html/otherimg/A.png +0 -0
  362. package/core/resources/html/otherimg/B.png +0 -0
  363. package/core/resources/html/otherimg/C.png +0 -0
  364. package/core/resources/html/otherimg/F.png +0 -0
  365. package/core/resources/html/otherimg/FC.png +0 -0
  366. package/core/resources/html/otherimg/NEW.png +0 -0
  367. package/core/resources/html/otherimg/Phigros_Icon_3.0.0.png +0 -0
  368. package/core/resources/html/otherimg/S.png +0 -0
  369. package/core/resources/html/otherimg/Star1.png +0 -0
  370. package/core/resources/html/otherimg/Star2.png +0 -0
  371. package/core/resources/html/otherimg/V.png +0 -0
  372. package/core/resources/html/otherimg/callback.png +0 -0
  373. package/core/resources/html/otherimg/chapHelp.png +0 -0
  374. package/core/resources/html/otherimg/dan_code.png +0 -0
  375. package/core/resources/html/otherimg/data.png +0 -0
  376. package/core/resources/html/otherimg/helpDoc.png +0 -0
  377. package/core/resources/html/otherimg/nickSuggest.png +0 -0
  378. package/core/resources/html/otherimg/phi.png +0 -0
  379. package/core/resources/html/otherimg/phigros.png +0 -0
  380. package/core/resources/html/otherimg/snow1.png +0 -0
  381. package/core/resources/html/otherimg/snow2.png +0 -0
  382. package/core/resources/html/otherimg/snow3.png +0 -0
  383. package/core/resources/html/otherimg/snow4.png +0 -0
  384. package/core/resources/html/otherimg/title.png +0 -0
  385. package/core/resources/html/rand/rand-orz.html +62 -0
  386. package/core/resources/html/rand/rand.art +59 -0
  387. package/core/resources/html/rand/rand.css +257 -0
  388. package/core/resources/html/rankingList/rankingList-orz.html +456 -0
  389. package/core/resources/html/rankingList/rankingList.art +171 -0
  390. package/core/resources/html/rankingList/rankingList.css +570 -0
  391. package/core/resources/html/rankingList-old/rankingList-orz.html +300 -0
  392. package/core/resources/html/rankingList-old/rankingList.art +89 -0
  393. package/core/resources/html/rankingList-old/rankingList.css +265 -0
  394. package/core/resources/html/score/score-orz.html +625 -0
  395. package/core/resources/html/score/score.art +249 -0
  396. package/core/resources/html/score/score.css +435 -0
  397. package/core/resources/html/score/scoreOld-orz.html +128 -0
  398. package/core/resources/html/score/scoreOld.art +230 -0
  399. package/core/resources/html/score/scoreOld.css +475 -0
  400. package/core/resources/html/score/scoreRankList-orz.html +613 -0
  401. package/core/resources/html/score/scoreRankList.art +231 -0
  402. package/core/resources/html/score/scoreRankList.css +197 -0
  403. package/core/resources/html/setting/setting-orz.html +123 -0
  404. package/core/resources/html/setting/setting.art +72 -0
  405. package/core/resources/html/setting/setting.css +170 -0
  406. package/core/resources/html/setting/userSetting-orz.html +132 -0
  407. package/core/resources/html/setting/userSetting.art +48 -0
  408. package/core/resources/html/setting/userSetting.css +160 -0
  409. package/core/resources/html/sign/sign-orz.html +262 -0
  410. package/core/resources/html/sign/sign.art +163 -0
  411. package/core/resources/html/sign/sign.css +706 -0
  412. package/core/resources/html/suggest/suggest.art +271 -0
  413. package/core/resources/html/suggest/suggest.css +300 -0
  414. package/core/resources/html/table/table-orz.html +228 -0
  415. package/core/resources/html/table/table.art +163 -0
  416. package/core/resources/html/table/table.css +401 -0
  417. package/core/resources/html/tasks/tasks-orz.html +83 -0
  418. package/core/resources/html/tasks/tasks.art +118 -0
  419. package/core/resources/html/tasks/tasks.css +334 -0
  420. package/core/resources/html/update/update-orz.html +735 -0
  421. package/core/resources/html/update/update.art +259 -0
  422. package/core/resources/html/update/update.css +676 -0
  423. package/core/resources/html/userinfo/userinfo-old-orz.html +408 -0
  424. package/core/resources/html/userinfo/userinfo-old.art +233 -0
  425. package/core/resources/html/userinfo/userinfo-old.css +466 -0
  426. package/core/resources/html/userinfo/userinfo-orz.html +543 -0
  427. package/core/resources/html/userinfo/userinfo.art +288 -0
  428. package/core/resources/html/userinfo/userinfo.css +757 -0
  429. package/core/resources/info/DLC/arc.json +412 -0
  430. package/core/resources/info/DLC/chunithm.json +1172 -0
  431. package/core/resources/info/DLC/dynamix.json +455 -0
  432. package/core/resources/info/DLC/fanshi.json +132 -0
  433. package/core/resources/info/DLC/lanota.json +412 -0
  434. package/core/resources/info/DLC/maimai.json +1110 -0
  435. package/core/resources/info/DLC/orz.json +158 -0
  436. package/core/resources/info/DLC/rizline.json +40 -0
  437. package/core/resources/info/DLC/rotaeno.json +159 -0
  438. package/core/resources/info/DLC/ryceam.json +53 -0
  439. package/core/resources/info/avatar.txt +109 -0
  440. package/core/resources/info/chaplist.yaml +102 -0
  441. package/core/resources/info/help/api.json +83 -0
  442. package/core/resources/info/help.json +436 -0
  443. package/core/resources/info/info.csv +315 -0
  444. package/core/resources/info/infolist.json +1758 -0
  445. package/core/resources/info/jrrp.json +92 -0
  446. package/core/resources/info/nicklist.yaml +2373 -0
  447. package/core/resources/info/notesInfo.json +1 -0
  448. package/core/resources/info/notice.json +11 -0
  449. package/core/resources/info/oldInfo/100/change.csv +218 -0
  450. package/core/resources/info/oldInfo/100/info.json +7 -0
  451. package/core/resources/info/oldInfo/101/change.csv +218 -0
  452. package/core/resources/info/oldInfo/101/info.json +7 -0
  453. package/core/resources/info/oldInfo/102/change.csv +223 -0
  454. package/core/resources/info/oldInfo/102/info.json +7 -0
  455. package/core/resources/info/oldInfo/103/change.csv +224 -0
  456. package/core/resources/info/oldInfo/103/info.json +7 -0
  457. package/core/resources/info/oldInfo/104/change.csv +225 -0
  458. package/core/resources/info/oldInfo/104/info.json +7 -0
  459. package/core/resources/info/oldInfo/105/change.csv +228 -0
  460. package/core/resources/info/oldInfo/105/info.json +7 -0
  461. package/core/resources/info/oldInfo/106/change.csv +229 -0
  462. package/core/resources/info/oldInfo/106/info.json +7 -0
  463. package/core/resources/info/oldInfo/108/change.csv +233 -0
  464. package/core/resources/info/oldInfo/108/info.json +7 -0
  465. package/core/resources/info/oldInfo/109/change.csv +235 -0
  466. package/core/resources/info/oldInfo/109/info.json +7 -0
  467. package/core/resources/info/oldInfo/110/change.csv +235 -0
  468. package/core/resources/info/oldInfo/110/info.json +7 -0
  469. package/core/resources/info/oldInfo/111/change.csv +242 -0
  470. package/core/resources/info/oldInfo/111/info.json +7 -0
  471. package/core/resources/info/oldInfo/112/change.csv +242 -0
  472. package/core/resources/info/oldInfo/112/info.json +7 -0
  473. package/core/resources/info/oldInfo/113/change.csv +244 -0
  474. package/core/resources/info/oldInfo/113/info.json +7 -0
  475. package/core/resources/info/oldInfo/115/change.csv +247 -0
  476. package/core/resources/info/oldInfo/115/info.json +7 -0
  477. package/core/resources/info/oldInfo/116/change.csv +249 -0
  478. package/core/resources/info/oldInfo/116/info.json +7 -0
  479. package/core/resources/info/oldInfo/118/change.csv +252 -0
  480. package/core/resources/info/oldInfo/118/info.json +7 -0
  481. package/core/resources/info/oldInfo/119/change.csv +255 -0
  482. package/core/resources/info/oldInfo/119/info.json +7 -0
  483. package/core/resources/info/oldInfo/120/change.csv +255 -0
  484. package/core/resources/info/oldInfo/120/info.json +7 -0
  485. package/core/resources/info/oldInfo/121/change.csv +257 -0
  486. package/core/resources/info/oldInfo/121/info.json +7 -0
  487. package/core/resources/info/oldInfo/122/change.csv +260 -0
  488. package/core/resources/info/oldInfo/122/info.json +7 -0
  489. package/core/resources/info/oldInfo/123/change.csv +260 -0
  490. package/core/resources/info/oldInfo/123/info.json +7 -0
  491. package/core/resources/info/oldInfo/124/change.csv +262 -0
  492. package/core/resources/info/oldInfo/124/info.json +7 -0
  493. package/core/resources/info/oldInfo/125/change.csv +266 -0
  494. package/core/resources/info/oldInfo/125/info.json +7 -0
  495. package/core/resources/info/oldInfo/126/change.csv +269 -0
  496. package/core/resources/info/oldInfo/126/info.json +7 -0
  497. package/core/resources/info/oldInfo/127/change.csv +270 -0
  498. package/core/resources/info/oldInfo/127/info.json +7 -0
  499. package/core/resources/info/oldInfo/128/change.csv +273 -0
  500. package/core/resources/info/oldInfo/128/info.json +7 -0
  501. package/core/resources/info/oldInfo/129/change.csv +274 -0
  502. package/core/resources/info/oldInfo/129/info.json +7 -0
  503. package/core/resources/info/oldInfo/130/change.csv +275 -0
  504. package/core/resources/info/oldInfo/130/info.json +7 -0
  505. package/core/resources/info/oldInfo/131/change.csv +278 -0
  506. package/core/resources/info/oldInfo/131/info.json +7 -0
  507. package/core/resources/info/oldInfo/132/change.csv +280 -0
  508. package/core/resources/info/oldInfo/132/info.json +7 -0
  509. package/core/resources/info/oldInfo/133/change.csv +284 -0
  510. package/core/resources/info/oldInfo/133/info.json +7 -0
  511. package/core/resources/info/oldInfo/134/change.csv +286 -0
  512. package/core/resources/info/oldInfo/134/info.json +7 -0
  513. package/core/resources/info/oldInfo/135/change.csv +290 -0
  514. package/core/resources/info/oldInfo/135/info.json +7 -0
  515. package/core/resources/info/oldInfo/136/change.csv +290 -0
  516. package/core/resources/info/oldInfo/136/info.json +7 -0
  517. package/core/resources/info/oldInfo/137/change.csv +293 -0
  518. package/core/resources/info/oldInfo/137/info.json +7 -0
  519. package/core/resources/info/oldInfo/138/change.csv +295 -0
  520. package/core/resources/info/oldInfo/138/info.json +7 -0
  521. package/core/resources/info/oldInfo/139/change.csv +295 -0
  522. package/core/resources/info/oldInfo/139/info.json +7 -0
  523. package/core/resources/info/oldInfo/140/change.csv +298 -0
  524. package/core/resources/info/oldInfo/140/info.json +7 -0
  525. package/core/resources/info/oldInfo/141/change.csv +300 -0
  526. package/core/resources/info/oldInfo/141/info.json +7 -0
  527. package/core/resources/info/oldInfo/142/change.csv +302 -0
  528. package/core/resources/info/oldInfo/142/info.json +7 -0
  529. package/core/resources/info/oldInfo/143/change.csv +303 -0
  530. package/core/resources/info/oldInfo/143/info.json +7 -0
  531. package/core/resources/info/oldInfo/144/change.csv +303 -0
  532. package/core/resources/info/oldInfo/144/info.json +7 -0
  533. package/core/resources/info/oldInfo/145/change.csv +305 -0
  534. package/core/resources/info/oldInfo/145/info.json +7 -0
  535. package/core/resources/info/oldInfo/146/change.csv +305 -0
  536. package/core/resources/info/oldInfo/146/info.json +7 -0
  537. package/core/resources/info/oldInfo/148/change.csv +307 -0
  538. package/core/resources/info/oldInfo/148/info.json +7 -0
  539. package/core/resources/info/oldInfo/149/change.csv +307 -0
  540. package/core/resources/info/oldInfo/149/info.json +7 -0
  541. package/core/resources/info/oldInfo/151/change.csv +309 -0
  542. package/core/resources/info/oldInfo/151/info.json +7 -0
  543. package/core/resources/info/oldInfo/152/change.csv +312 -0
  544. package/core/resources/info/oldInfo/152/info.json +7 -0
  545. package/core/resources/info/oldInfo/153/change.csv +313 -0
  546. package/core/resources/info/oldInfo/153/info.json +7 -0
  547. package/core/resources/info/oldInfo/154/change.csv +315 -0
  548. package/core/resources/info/oldInfo/154/info.json +7 -0
  549. package/core/resources/info/oldInfo/92/change.csv +204 -0
  550. package/core/resources/info/oldInfo/92/info.json +7 -0
  551. package/core/resources/info/oldInfo/93/change.csv +206 -0
  552. package/core/resources/info/oldInfo/93/info.json +7 -0
  553. package/core/resources/info/oldInfo/94/change.csv +209 -0
  554. package/core/resources/info/oldInfo/94/info.json +7 -0
  555. package/core/resources/info/oldInfo/95/change.csv +211 -0
  556. package/core/resources/info/oldInfo/95/info.json +7 -0
  557. package/core/resources/info/oldInfo/96/change.csv +216 -0
  558. package/core/resources/info/oldInfo/96/info.json +7 -0
  559. package/core/resources/info/oldInfo/98/change.csv +216 -0
  560. package/core/resources/info/oldInfo/98/info.json +7 -0
  561. package/core/resources/info/oldInfo/99/change.csv +217 -0
  562. package/core/resources/info/oldInfo/99/info.json +7 -0
  563. package/core/resources/info/oldNotesInfo.json +1 -0
  564. package/core/resources/info/sentences.json +1 -0
  565. package/core/resources/info/spinfo.json +362 -0
  566. package/core/resources/info/tips.txt +278 -0
  567. package/core/resources/otherill//351/233/252/351/231/215SP.png +0 -0
  568. package/core/resources/song-select.html +535 -0
  569. package/core/resources/themes/README.md +24 -0
  570. package/core/resources/themes/milthm/A.png +0 -0
  571. package/core/resources/themes/milthm/B.png +0 -0
  572. package/core/resources/themes/milthm/C.png +0 -0
  573. package/core/resources/themes/milthm/F.png +0 -0
  574. package/core/resources/themes/milthm/FC.png +0 -0
  575. package/core/resources/themes/milthm/S.png +0 -0
  576. package/core/resources/themes/milthm/V.png +0 -0
  577. package/core/resources/themes/milthm/arcgrosB19.css +36 -0
  578. package/core/resources/themes/milthm/b19.art +464 -0
  579. package/core/resources/themes/milthm/b19.css +1314 -0
  580. package/core/resources/themes/milthm/bg.png +0 -0
  581. package/core/resources/themes/milthm/clg.css +37 -0
  582. package/core/resources/themes/milthm/difficultyHistory.css +42 -0
  583. package/core/resources/themes/milthm/font.ttf +0 -0
  584. package/core/resources/themes/milthm/help.css +36 -0
  585. package/core/resources/themes/milthm/historyB30.css +48 -0
  586. package/core/resources/themes/milthm/info.yaml +45 -0
  587. package/core/resources/themes/milthm/list.css +39 -0
  588. package/core/resources/themes/milthm/phi.png +0 -0
  589. package/core/resources/themes/milthm/setting.css +37 -0
  590. package/core/resources/themes/milthm/sign.css +81 -0
  591. package/core/resources/themes/milthm/suggest.css +40 -0
  592. package/core/resources/themes/milthm/table.css +54 -0
  593. package/core/resources/themes/milthm/update.css +68 -0
  594. package/core/tests/aliasProposalService.test.js +82 -0
  595. package/core/tests/apiAccountDeletion.test.js +125 -0
  596. package/core/tests/apiPermission.test.js +44 -0
  597. package/core/tests/apiVersion.test.js +55 -0
  598. package/core/tests/b30Analysis.test.js +38 -0
  599. package/core/tests/backgroundIllustration.test.js +21 -0
  600. package/core/tests/botApiAuthRecovery.test.js +148 -0
  601. package/core/tests/botSyncService.test.js +42 -0
  602. package/core/tests/fileWatcherRegistry.test.js +44 -0
  603. package/core/tests/gameRecordPayload.test.js +34 -0
  604. package/core/tests/guessLetter.test.js +79 -0
  605. package/core/tests/makeRequestHandling.test.js +118 -0
  606. package/core/tests/marketOverflow.test.js +31 -0
  607. package/core/tests/platformTemplateCache.test.js +92 -0
  608. package/core/tests/pluginDataThemePreference.test.js +36 -0
  609. package/core/tests/processCleanup.test.js +58 -0
  610. package/core/tests/puppeteerShutdown.test.js +17 -0
  611. package/core/tests/renderPressureHistory.test.js +59 -0
  612. package/core/tests/saveStats.test.js +26 -0
  613. package/core/tests/themeInstallGuard.test.js +289 -0
  614. package/core/tests/themeManager.test.js +625 -0
  615. package/core/tests/themeManagerInit.test.js +11 -0
  616. package/core/tests/themeMarket.test.js +855 -0
  617. package/core/tests/themePageRouting.test.js +431 -0
  618. package/core/tests/themePolicy.test.js +28 -0
  619. package/core/tests/themeStorageRecovery.test.js +192 -0
  620. package/core/tests/userCredentialModuleBoundaries.test.js +41 -0
  621. package/core/tests/userCredentials.test.js +285 -0
  622. package/core/tsconfig.json +32 -0
  623. package/core/types/vendor.d.ts +14 -0
  624. package/lib/index.cjs +358 -0
  625. package/lib/index.d.cts +182 -0
  626. package/lib/index.d.ts +182 -0
  627. package/lib/index.js +320 -0
  628. package/package.json +74 -0
  629. package/readme.md +3 -0
@@ -0,0 +1,192 @@
1
+ import { resolveBotDownloadAllowed, ThemeMarketClientError } from './marketClient.js'
2
+ import makeRequest from '../api/makeRequest.js'
3
+ import themeManager from './manager.js'
4
+
5
+ export const THEME_MARKET_API_ORIGIN = 'https://lyh.org.cn:18473'
6
+ const SLUG_RE = /^[a-z][a-z0-9_-]{0,119}$/
7
+ const PINNED_LOCAL_THEME_ID = 'milthm'
8
+ const MAX_MARKET_THEMES = 500
9
+ export const THEME_MARKET_PAGE_SIZE = 6
10
+
11
+ /** @param {unknown} value */
12
+ function isRecord(value) {
13
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
14
+ }
15
+
16
+ function invalidCatalogResponse() {
17
+ return new ThemeMarketClientError('theme_store_invalid_response', 502)
18
+ }
19
+
20
+ /** @param {unknown} value @param {number} limit */
21
+ function text(value, limit) {
22
+ return typeof value === 'string' ? value.trim().slice(0, limit) : ''
23
+ }
24
+
25
+ /** @param {unknown} value */
26
+ function safeCover(value) {
27
+ if (!value) return ''
28
+ try {
29
+ const url = new URL(String(value))
30
+ return url.protocol === 'https:' && url.origin === THEME_MARKET_API_ORIGIN ? url.toString() : ''
31
+ } catch {
32
+ return ''
33
+ }
34
+ }
35
+
36
+ /**
37
+ * @param {any} item
38
+ * @param {boolean|null} [inheritedBotDownloadAllowed]
39
+ */
40
+ export function normalizeMarketTheme(item, inheritedBotDownloadAllowed = null) {
41
+ const slug = text(item?.slug || item?.themeId, 120).toLowerCase()
42
+ const botDownloadAllowed = typeof item?.botDownloadAllowed === 'boolean'
43
+ ? item.botDownloadAllowed
44
+ : inheritedBotDownloadAllowed
45
+ return {
46
+ slug,
47
+ themeId: text(item?.themeId || slug, 120),
48
+ name: text(item?.name || slug, 100) || slug,
49
+ author: text(item?.author, 80),
50
+ summary: text(item?.summary, 180),
51
+ description: text(item?.description, 1200),
52
+ cover: safeCover(item?.cover),
53
+ tags: Array.isArray(item?.tags) ? item.tags.filter((/** @type {unknown} */ tag) => typeof tag === 'string').slice(0, 8).map((/** @type {string} */ tag) => tag.slice(0, 24)) : [],
54
+ version: text(item?.version, 64),
55
+ downloadPolicy: ['public', 'restricted', 'bot_only'].includes(item?.downloadPolicy) ? item.downloadPolicy : 'restricted',
56
+ compatibility: text(item?.compatibility, 100),
57
+ downloads: Number.isSafeInteger(item?.downloads) && item.downloads >= 0 ? item.downloads : 0,
58
+ updatedAt: text(item?.updatedAt, 40),
59
+ featured: item?.featured === true,
60
+ size: Number.isSafeInteger(item?.size) && item.size > 0 ? item.size : 0,
61
+ botDownloadAllowed,
62
+ local: false,
63
+ pinnedLocal: false,
64
+ }
65
+ }
66
+
67
+ /** @param {any[]} themes @param {string} query @param {number} page @param {boolean} localOnly */
68
+ function paginateThemes(themes, query, page, localOnly) {
69
+ const needle = text(query, 80).toLocaleLowerCase()
70
+ const filtered = needle
71
+ ? themes.filter((/** @type {any} */ theme) => [theme.slug, theme.name, theme.author, theme.summary, theme.description, ...theme.tags]
72
+ .some((/** @type {string} */ value) => value.toLocaleLowerCase().includes(needle)))
73
+ : themes
74
+ if (!localOnly) {
75
+ filtered.sort((/** @type {any} */ a, /** @type {any} */ b) => Number(b.pinnedLocal) - Number(a.pinnedLocal)
76
+ || Number(b.featured) - Number(a.featured)
77
+ || b.updatedAt.localeCompare(a.updatedAt))
78
+ }
79
+ const total = filtered.length
80
+ const pageCount = Math.max(1, Math.ceil(total / THEME_MARKET_PAGE_SIZE))
81
+ const currentPage = Number.isSafeInteger(page) ? Math.min(Math.max(page, 1), pageCount) : 1
82
+ const start = (currentPage - 1) * THEME_MARKET_PAGE_SIZE
83
+ return {
84
+ themes: filtered.slice(start, start + THEME_MARKET_PAGE_SIZE),
85
+ query: needle,
86
+ page: currentPage,
87
+ pageCount,
88
+ total,
89
+ localOnly,
90
+ }
91
+ }
92
+
93
+ /** @param {any} theme */
94
+ function normalizeLocalTheme(theme) {
95
+ return {
96
+ slug: theme.id,
97
+ themeId: theme.id,
98
+ name: theme.name || theme.id,
99
+ author: theme.author || '',
100
+ summary: theme.description || '',
101
+ description: theme.description || '',
102
+ cover: '',
103
+ tags: [theme.marketInstalled ? '已下载' : '本地主题'],
104
+ version: theme.marketVersion || '',
105
+ downloadPolicy: 'local',
106
+ compatibility: '本地已安装',
107
+ downloads: 0,
108
+ updatedAt: '',
109
+ featured: false,
110
+ size: 0,
111
+ botDownloadAllowed: themeManager.isThemeAvailable(theme.id),
112
+ local: true,
113
+ pinnedLocal: theme.id === PINNED_LOCAL_THEME_ID,
114
+ }
115
+ }
116
+
117
+ /**
118
+ * 返回当前进程已注册的本地自定义主题目录,不访问网络。
119
+ * @param {string} [query]
120
+ * @param {number} [page]
121
+ */
122
+ export function getLocalThemeCatalog(query = '', page = 1) {
123
+ const themes = themeManager.getCustomThemes().map(normalizeLocalTheme)
124
+ return { demo: false, ...paginateThemes(themes, query, page, true) }
125
+ }
126
+
127
+ /** @param {string} themeId */
128
+ export function getLocalThemeDetail(themeId) {
129
+ const theme = themeManager.getTheme(themeId)
130
+ if (!theme || !themeManager.isCustomTheme(themeId)) return null
131
+ return { ...normalizeLocalTheme(theme), releaseNotes: '' }
132
+ }
133
+
134
+ /** @param {any} response @param {any} item */
135
+ function validateCatalogTheme(response, item) {
136
+ if (!isRecord(item)
137
+ || typeof item.slug !== 'string' || !SLUG_RE.test(item.slug)
138
+ || typeof item.name !== 'string' || !item.name.trim() || item.name.length > 100
139
+ || Object.hasOwn(item, 'downloadPolicy') && !['public', 'restricted', 'bot_only'].includes(item.downloadPolicy)
140
+ || Object.hasOwn(item, 'tags') && (!Array.isArray(item.tags) || item.tags.some((/** @type {unknown} */ tag) => typeof tag !== 'string'))) {
141
+ throw invalidCatalogResponse()
142
+ }
143
+ const botDownloadAllowed = resolveBotDownloadAllowed(response, item)
144
+ return normalizeMarketTheme(item, botDownloadAllowed)
145
+ }
146
+
147
+ /**
148
+ * @param {string} [query]
149
+ * @param {number} [page]
150
+ */
151
+ export async function fetchThemeCatalog(query = '', page = 1) {
152
+ const data = /** @type {any} */ (await makeRequest.getThemeMarketList())
153
+ if (!isRecord(data)
154
+ || data.ok !== true
155
+ || !Array.isArray(data.themes)
156
+ || data.themes.length > MAX_MARKET_THEMES
157
+ || Object.hasOwn(data, 'demo') && typeof data.demo !== 'boolean') {
158
+ throw invalidCatalogResponse()
159
+ }
160
+ const seen = new Set()
161
+ const onlineThemes = []
162
+ for (const item of data.themes) {
163
+ const theme = validateCatalogTheme(data, item)
164
+ if (seen.has(theme.slug)) continue
165
+ seen.add(theme.slug)
166
+ onlineThemes.push(theme)
167
+ }
168
+ const pinnedLocalTheme = getLocalThemeDetail(PINNED_LOCAL_THEME_ID)
169
+ const themes = pinnedLocalTheme
170
+ ? [pinnedLocalTheme, ...onlineThemes.filter((/** @type {{slug:string}} */ theme) => theme.slug !== PINNED_LOCAL_THEME_ID)]
171
+ : onlineThemes
172
+ return {
173
+ demo: data?.demo === true,
174
+ ...paginateThemes(themes, query, page, false),
175
+ }
176
+ }
177
+
178
+ /** @param {string} slug */
179
+ export async function fetchThemeDetail(slug) {
180
+ if (!SLUG_RE.test(slug)) throw new ThemeMarketClientError('theme_slug_invalid', 400)
181
+ const data = /** @type {any} */ (await makeRequest.getThemeMarketDetail(slug))
182
+ if (!isRecord(data) || data.ok !== true || !isRecord(data.theme)) throw invalidCatalogResponse()
183
+ if (Object.hasOwn(data, 'releaseNotes') && typeof data.releaseNotes !== 'string'
184
+ || Object.hasOwn(data.theme, 'releaseNotes') && typeof data.theme.releaseNotes !== 'string') {
185
+ throw invalidCatalogResponse()
186
+ }
187
+ const theme = validateCatalogTheme(data, data.theme)
188
+ if (theme.slug !== slug) throw invalidCatalogResponse()
189
+ return { ...theme, releaseNotes: text(data?.releaseNotes || data?.theme?.releaseNotes, 3000) }
190
+ }
191
+
192
+ export const isThemeSlug = (/** @type {unknown} */ value) => typeof value === 'string' && SLUG_RE.test(value)
@@ -0,0 +1,275 @@
1
+ import crypto from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { ThemeMarketClientError } from './marketClient.js'
5
+ import { themesDir } from './paths.js'
6
+ import { getBackupThemeId, readMarketReceipt } from './recovery.js'
7
+
8
+ export const MARKET_INSTALL_LIMITS = Object.freeze({
9
+ maxPending: 8,
10
+ maxInstalled: 32,
11
+ maxDiskBytes: 512 * 1024 * 1024,
12
+ freshInstallsPerUser: 3,
13
+ freshInstallWindowMs: 60 * 60_000,
14
+ })
15
+
16
+ /** @param {string} directory */
17
+ async function directorySize(directory) {
18
+ let total = 0
19
+ const entries = await fs.promises.readdir(directory, { withFileTypes: true })
20
+ for (const entry of entries) {
21
+ const target = path.join(directory, entry.name)
22
+ if (entry.isDirectory()) total += await directorySize(target)
23
+ else total += (await fs.promises.lstat(target)).size
24
+ }
25
+ return total
26
+ }
27
+
28
+ /**
29
+ * @param {{root?:string}} [options]
30
+ * @returns {Promise<{count:number,totalBytes:number,byTheme:Map<string, number>}>}
31
+ */
32
+ export async function getMarketThemeStorageUsage(options = {}) {
33
+ const root = options.root || themesDir
34
+ const entries = await fs.promises.readdir(root, { withFileTypes: true }).catch(error => {
35
+ if (/** @type {any} */ (error)?.code === 'ENOENT') return []
36
+ throw error
37
+ })
38
+ /** @type {Map<string, number>} */
39
+ const byTheme = new Map()
40
+ let totalBytes = 0
41
+ for (const entry of entries) {
42
+ if (!entry.isDirectory()) continue
43
+ const themeId = entry.name.startsWith('.') ? getBackupThemeId(entry.name) : entry.name
44
+ if (!themeId) continue
45
+ const directory = path.join(root, entry.name)
46
+ const stat = await fs.promises.lstat(directory)
47
+ if (!stat.isDirectory() || stat.isSymbolicLink()) continue
48
+ const marker = await fs.promises.lstat(path.join(directory, '.phi-market.json')).catch(() => null)
49
+ const receipt = await readMarketReceipt(path.join(directory, '.phi-market.json'))
50
+ if (receipt?.slug !== themeId && !marker?.isFile()) continue
51
+ const bytes = await directorySize(directory)
52
+ byTheme.set(themeId, (byTheme.get(themeId) || 0) + bytes)
53
+ totalBytes += bytes
54
+ }
55
+ return { count: byTheme.size, totalBytes, byTheme }
56
+ }
57
+
58
+ /**
59
+ * Reject a new theme before downloading when the current installation already
60
+ * exhausts a hard quota. The staged directory is checked again before commit.
61
+ * @param {string} themeId
62
+ * @param {{root?:string,maxInstalled?:number,maxDiskBytes?:number}} [options]
63
+ */
64
+ export async function assertFreshMarketInstallCapacity(themeId, options = {}) {
65
+ const usage = await getMarketThemeStorageUsage(options)
66
+ if (usage.byTheme.has(themeId)) return { fresh: false, usage }
67
+ const maxInstalled = options.maxInstalled ?? MARKET_INSTALL_LIMITS.maxInstalled
68
+ const maxDiskBytes = options.maxDiskBytes ?? MARKET_INSTALL_LIMITS.maxDiskBytes
69
+ if (usage.count >= maxInstalled) throw new ThemeMarketClientError('theme_install_count_quota_exceeded')
70
+ if (usage.totalBytes >= maxDiskBytes) throw new ThemeMarketClientError('theme_install_disk_quota_exceeded')
71
+ return { fresh: true, usage }
72
+ }
73
+
74
+ /**
75
+ * Check the exact post-commit count and byte usage while the install lock is held.
76
+ * @param {string} themeId
77
+ * @param {string} stagedDirectory
78
+ * @param {{root?:string,maxInstalled?:number,maxDiskBytes?:number}} [options]
79
+ */
80
+ export async function assertMarketInstallQuota(themeId, stagedDirectory, options = {}) {
81
+ const usage = await getMarketThemeStorageUsage(options)
82
+ const incomingBytes = await directorySize(stagedDirectory)
83
+ const existingBytes = usage.byTheme.get(themeId) || 0
84
+ const nextCount = usage.count + (usage.byTheme.has(themeId) ? 0 : 1)
85
+ const nextBytes = usage.totalBytes - existingBytes + incomingBytes
86
+ const maxInstalled = options.maxInstalled ?? MARKET_INSTALL_LIMITS.maxInstalled
87
+ const maxDiskBytes = options.maxDiskBytes ?? MARKET_INSTALL_LIMITS.maxDiskBytes
88
+ if (nextCount > maxInstalled) throw new ThemeMarketClientError('theme_install_count_quota_exceeded')
89
+ if (nextBytes > maxDiskBytes) throw new ThemeMarketClientError('theme_install_disk_quota_exceeded')
90
+ return { count: nextCount, totalBytes: nextBytes }
91
+ }
92
+
93
+ export class FreshInstallRateLimiter {
94
+ /** @param {{limit?:number,windowMs?:number,now?:()=>number}} [options] */
95
+ constructor(options = {}) {
96
+ this.limit = options.limit ?? MARKET_INSTALL_LIMITS.freshInstallsPerUser
97
+ this.windowMs = options.windowMs ?? MARKET_INSTALL_LIMITS.freshInstallWindowMs
98
+ this.now = options.now || Date.now
99
+ /** @type {Map<string, number[]>} */
100
+ this.records = new Map()
101
+ }
102
+
103
+ /** @param {string} requesterId */
104
+ consume(requesterId) {
105
+ requesterId ||= 'unknown'
106
+ const now = this.now()
107
+ const cutoff = now - this.windowMs
108
+ const recent = (this.records.get(requesterId) || []).filter(timestamp => timestamp > cutoff)
109
+ if (recent.length >= this.limit) throw new ThemeMarketClientError('theme_install_rate_limited', 429)
110
+ recent.push(now)
111
+ this.records.set(requesterId, recent)
112
+ return true
113
+ }
114
+
115
+ /** @param {string} [requesterId] */
116
+ clear(requesterId) {
117
+ if (requesterId) this.records.delete(requesterId)
118
+ else this.records.clear()
119
+ }
120
+ }
121
+
122
+ export class PersistentFreshInstallRateLimiter {
123
+ /** @param {{filePath?:string,limit?:number,windowMs?:number,now?:()=>number}} [options] */
124
+ constructor(options = {}) {
125
+ this.filePath = options.filePath || path.join(themesDir, '.phi-market-rate-limits.json')
126
+ this.limit = options.limit ?? MARKET_INSTALL_LIMITS.freshInstallsPerUser
127
+ this.windowMs = options.windowMs ?? MARKET_INSTALL_LIMITS.freshInstallWindowMs
128
+ this.now = options.now || Date.now
129
+ }
130
+
131
+ /** @returns {Promise<Record<string, number[]>>} */
132
+ async readRecords() {
133
+ let value
134
+ try {
135
+ value = JSON.parse(await fs.promises.readFile(this.filePath, 'utf8'))
136
+ } catch (error) {
137
+ if (/** @type {any} */ (error)?.code === 'ENOENT') return {}
138
+ throw new ThemeMarketClientError('theme_install_rate_state_invalid')
139
+ }
140
+ if (value?.version !== 1 || !value.records || typeof value.records !== 'object' || Array.isArray(value.records)) {
141
+ throw new ThemeMarketClientError('theme_install_rate_state_invalid')
142
+ }
143
+ const entries = Object.entries(value.records)
144
+ if (entries.length > 10_000) throw new ThemeMarketClientError('theme_install_rate_state_invalid')
145
+ /** @type {Record<string, number[]>} */
146
+ const records = {}
147
+ for (const [key, timestamps] of entries) {
148
+ if (!/^[a-f0-9]{64}$/.test(key) || !Array.isArray(timestamps)
149
+ || timestamps.length > 1_000 || timestamps.some(item => !Number.isFinite(item))) {
150
+ throw new ThemeMarketClientError('theme_install_rate_state_invalid')
151
+ }
152
+ records[key] = timestamps
153
+ }
154
+ return records
155
+ }
156
+
157
+ /** @param {Record<string, number[]>} records */
158
+ async writeRecords(records) {
159
+ await fs.promises.mkdir(path.dirname(this.filePath), { recursive: true, mode: 0o700 })
160
+ const temporary = `${this.filePath}.${process.pid}.${crypto.randomUUID()}.tmp`
161
+ try {
162
+ await fs.promises.writeFile(temporary, `${JSON.stringify({ version: 1, records })}\n`, {
163
+ encoding: 'utf8', mode: 0o600, flag: 'wx',
164
+ })
165
+ await fs.promises.rename(temporary, this.filePath)
166
+ } finally {
167
+ await fs.promises.rm(temporary, { force: true }).catch(() => {})
168
+ }
169
+ }
170
+
171
+ /**
172
+ * The caller must hold the global market install lock so read-modify-write is
173
+ * atomic across Bot workers.
174
+ * @param {string} requesterId
175
+ */
176
+ async consume(requesterId) {
177
+ const requesterKey = crypto.createHash('sha256').update(requesterId || 'unknown').digest('hex')
178
+ const now = this.now()
179
+ const cutoff = now - this.windowMs
180
+ const records = await this.readRecords()
181
+ for (const [key, timestamps] of Object.entries(records)) {
182
+ const recent = timestamps.filter(timestamp => timestamp > cutoff)
183
+ if (recent.length) records[key] = recent
184
+ else delete records[key]
185
+ }
186
+ const recent = records[requesterKey] || []
187
+ if (recent.length >= this.limit) throw new ThemeMarketClientError('theme_install_rate_limited', 429)
188
+ if (!records[requesterKey] && Object.keys(records).length >= 10_000) {
189
+ throw new ThemeMarketClientError('theme_install_rate_limited', 429)
190
+ }
191
+ recent.push(now)
192
+ records[requesterKey] = recent
193
+ await this.writeRecords(records)
194
+ return true
195
+ }
196
+
197
+ /** @param {string} [requesterId] */
198
+ async clear(requesterId) {
199
+ if (requesterId === undefined) {
200
+ await fs.promises.rm(this.filePath, { force: true })
201
+ return
202
+ }
203
+ const records = await this.readRecords()
204
+ const requesterKey = crypto.createHash('sha256').update(requesterId || 'unknown').digest('hex')
205
+ delete records[requesterKey]
206
+ if (Object.keys(records).length) await this.writeRecords(records)
207
+ else await fs.promises.rm(this.filePath, { force: true })
208
+ }
209
+ }
210
+
211
+ export class ThemeInstallCoordinator {
212
+ /** @param {{maxPending?:number,concurrency?:number}} [options] */
213
+ constructor(options = {}) {
214
+ this.maxPending = options.maxPending ?? MARKET_INSTALL_LIMITS.maxPending
215
+ this.concurrency = options.concurrency ?? 1
216
+ this.active = 0
217
+ /** @type {Array<{key:string,operation:()=>Promise<any>,promise:Promise<any>,resolve:(value:any)=>void,reject:(error:unknown)=>void}>} */
218
+ this.queue = []
219
+ /** @type {Map<string, Promise<any>>} */
220
+ this.flights = new Map()
221
+ }
222
+
223
+ /** @template T @param {string} key @param {()=>Promise<T>} operation @returns {Promise<T>} */
224
+ schedule(key, operation) {
225
+ const existing = this.flights.get(key)
226
+ if (existing) return existing
227
+ if (this.active >= this.concurrency && this.queue.length >= this.maxPending) {
228
+ return Promise.reject(new ThemeMarketClientError('theme_install_queue_full', 429))
229
+ }
230
+
231
+ /** @type {(value:T)=>void} */ let resolve = () => {}
232
+ /** @type {(error:unknown)=>void} */ let reject = () => {}
233
+ const promise = new Promise((resolvePromise, rejectPromise) => {
234
+ resolve = resolvePromise
235
+ reject = rejectPromise
236
+ })
237
+ const task = { key, operation, promise, resolve, reject }
238
+ this.flights.set(key, promise)
239
+ if (this.active < this.concurrency) this.start(task)
240
+ else this.queue.push(task)
241
+ return promise
242
+ }
243
+
244
+ /** @param {{key:string,operation:()=>Promise<any>,promise:Promise<any>,resolve:(value:any)=>void,reject:(error:unknown)=>void}} task */
245
+ start(task) {
246
+ this.active++
247
+ Promise.resolve()
248
+ .then(task.operation)
249
+ .then(value => {
250
+ if (this.flights.get(task.key) === task.promise) this.flights.delete(task.key)
251
+ this.active--
252
+ const next = this.queue.shift()
253
+ if (next) this.start(next)
254
+ task.resolve(value)
255
+ }, error => {
256
+ if (this.flights.get(task.key) === task.promise) this.flights.delete(task.key)
257
+ this.active--
258
+ const next = this.queue.shift()
259
+ if (next) this.start(next)
260
+ task.reject(error)
261
+ })
262
+ }
263
+ }
264
+
265
+ export const freshInstallRateLimiter = new PersistentFreshInstallRateLimiter()
266
+ export const marketInstallCoordinator = new ThemeInstallCoordinator()
267
+
268
+ /** @param {any} event */
269
+ export function getThemeInstallRequesterId(event) {
270
+ const userId = event?.user_id ?? event?.userId
271
+ if (userId === undefined || userId === null || userId === '') return ''
272
+ const platform = event?.platform || event?.adapter || 'unknown'
273
+ const botId = event?.self_id ?? event?.bot_id ?? event?.bot?.self_id ?? ''
274
+ return `${platform}:${botId}:${userId}`
275
+ }
@@ -0,0 +1,174 @@
1
+ import crypto from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { AsyncLocalStorage } from 'node:async_hooks'
5
+ import { ThemeMarketClientError } from './marketClient.js'
6
+ import { migrateLegacyThemeDirectories, themesDir } from './paths.js'
7
+
8
+ const LOCK_PATH = path.join(themesDir, '.phi-market-install.lock')
9
+ const LOCK_REAP_PREFIX = `${LOCK_PATH}.reap-`
10
+ const LOCK_WAIT_MS = 120_000
11
+ const LOCK_STALE_MS = 5 * 60_000
12
+
13
+ /** @type {Promise<void>} */
14
+ let processQueue = Promise.resolve()
15
+ const installLockContext = new AsyncLocalStorage()
16
+
17
+ /** @param {number} ms */
18
+ const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
19
+
20
+ /** @returns {string | null} 本进程的调度器启动标识(clock ticks);非 Linux 或 /proc 不可用时返回 null */
21
+ function currentProcessStartIdentity() {
22
+ try {
23
+ const stat = fs.readFileSync(`/proc/${process.pid}/stat`, 'utf8')
24
+ // comm 字段可能含空格和括号,字段从最后一个 ')' 之后数起:其后首列是第 3 列 state,
25
+ // 因此第 22 列 starttime 的下标为 19。重启后 starttime 归零,可顺带识别跨重启的残留锁。
26
+ return stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19] || null
27
+ } catch {
28
+ return null
29
+ }
30
+ }
31
+
32
+ /**
33
+ * 核对锁记录的 pid 当前是否仍是当初那个进程:pid 被无关进程复用时启动标识不同,
34
+ * 崩溃持有者的锁才能被回收,而不是被永久判为存活。
35
+ * @param {number} pid @param {string | undefined} identity
36
+ * @returns {boolean | null} 无法判定(非 Linux / proc 不可读 / 旧锁未记录)时为 null
37
+ */
38
+ function processIdentityMatches(pid, identity) {
39
+ if (!identity) return null
40
+ try {
41
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8')
42
+ const current = stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19]
43
+ return current ? current === identity : null
44
+ } catch {
45
+ return null
46
+ }
47
+ }
48
+
49
+ /** @param {number} pid @param {string | undefined} identity */
50
+ function processAlive(pid, identity) {
51
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false
52
+ try {
53
+ process.kill(pid, 0)
54
+ } catch (error) {
55
+ if (/** @type {any} */ (error)?.code !== 'EPERM') return false
56
+ }
57
+ // 无法核对启动标识时保持保守的“存活”结论,避免误清他人的锁。
58
+ return processIdentityMatches(pid, identity) !== false
59
+ }
60
+
61
+ /** @returns {Promise<{stat:fs.Stats,owner:any,generation:string}|null>} */
62
+ async function readFileLockState() {
63
+ const stat = await fs.promises.stat(LOCK_PATH).catch(() => null)
64
+ if (!stat) return null
65
+ const raw = await fs.promises.readFile(LOCK_PATH, 'utf8').catch(() => '')
66
+ let owner = null
67
+ try { owner = JSON.parse(raw) } catch { }
68
+ const generation = typeof owner?.token === 'string' && /^[a-f0-9-]{36}$/i.test(owner.token)
69
+ ? owner.token.toLowerCase()
70
+ : `legacy-${crypto.createHash('sha256')
71
+ .update(`${raw}\0${stat.dev}\0${stat.ino}\0${stat.mtimeMs}\0${stat.size}`)
72
+ .digest('hex')}`
73
+ return { stat, owner, generation }
74
+ }
75
+
76
+ /** @param {{stat:fs.Stats,owner:any}} state */
77
+ function isStaleFileLock(state) {
78
+ return Date.now() - state.stat.mtimeMs > LOCK_STALE_MS
79
+ && !processAlive(Number(state.owner?.pid), state.owner?.identity)
80
+ }
81
+
82
+ /**
83
+ * A token-specific reap file elects one cleanup owner for a stale lock
84
+ * generation. A crashed reaper fails closed for that generation instead of
85
+ * letting a second process unlink a newly-created lock.
86
+ */
87
+ async function reapStaleFileLock() {
88
+ const observed = await readFileLockState()
89
+ if (!observed || !isStaleFileLock(observed)) return false
90
+ const reapPath = `${LOCK_REAP_PREFIX}${observed.generation}`
91
+ let reapHandle
92
+ try {
93
+ reapHandle = await fs.promises.open(reapPath, 'wx', 0o600)
94
+ await reapHandle.writeFile(JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }))
95
+ await reapHandle.sync()
96
+ } catch (error) {
97
+ if (reapHandle) {
98
+ await reapHandle.close().catch(() => {})
99
+ await fs.promises.rm(reapPath, { force: true }).catch(() => {})
100
+ }
101
+ if (/** @type {any} */ (error)?.code === 'EEXIST') return false
102
+ throw error
103
+ }
104
+
105
+ try {
106
+ const current = await readFileLockState()
107
+ if (!current || current.generation !== observed.generation || !isStaleFileLock(current)) return false
108
+ await fs.promises.unlink(LOCK_PATH).catch(error => {
109
+ if (/** @type {any} */ (error)?.code !== 'ENOENT') throw error
110
+ })
111
+ return true
112
+ } finally {
113
+ await reapHandle.close().catch(() => {})
114
+ await fs.promises.rm(reapPath, { force: true }).catch(() => {})
115
+ }
116
+ }
117
+
118
+ async function acquireFileLock() {
119
+ migrateLegacyThemeDirectories()
120
+ await fs.promises.mkdir(themesDir, { recursive: true, mode: 0o700 })
121
+ const started = Date.now()
122
+ while (Date.now() - started < LOCK_WAIT_MS) {
123
+ let handle
124
+ try {
125
+ handle = await fs.promises.open(LOCK_PATH, 'wx', 0o600)
126
+ const token = crypto.randomUUID()
127
+ try {
128
+ await handle.writeFile(JSON.stringify({
129
+ pid: process.pid,
130
+ identity: currentProcessStartIdentity(),
131
+ token,
132
+ createdAt: new Date().toISOString(),
133
+ }))
134
+ await handle.sync()
135
+ return { handle, token }
136
+ } catch (error) {
137
+ await handle.close().catch(() => {})
138
+ await fs.promises.rm(LOCK_PATH, { force: true }).catch(() => {})
139
+ throw error
140
+ }
141
+ } catch (error) {
142
+ if (/** @type {any} */ (error)?.code !== 'EEXIST') throw error
143
+ if (await reapStaleFileLock()) continue
144
+ await wait(200)
145
+ }
146
+ }
147
+ throw new ThemeMarketClientError('theme_install_busy')
148
+ }
149
+
150
+ /** @template T @param {() => Promise<T>} operation @returns {Promise<T>} */
151
+ export async function withMarketInstallLock(operation) {
152
+ if (installLockContext.getStore()) return operation()
153
+ const previous = processQueue.catch(() => {})
154
+ /** @type {() => void} */
155
+ let releaseQueue = () => {}
156
+ processQueue = new Promise(resolve => { releaseQueue = () => resolve() })
157
+ await previous
158
+ let lock
159
+ try {
160
+ lock = await acquireFileLock()
161
+ return await installLockContext.run(lock, operation)
162
+ } finally {
163
+ if (lock) {
164
+ await lock.handle.close().catch(() => {})
165
+ const current = await readFileLockState()
166
+ if (current?.owner?.token === lock.token) {
167
+ await fs.promises.rm(LOCK_PATH, { force: true }).catch(() => {})
168
+ }
169
+ }
170
+ releaseQueue()
171
+ }
172
+ }
173
+
174
+ export const marketInstallLockPath = LOCK_PATH