koishi-plugin-kkk 1.0.0

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 (702) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/LICENSE +674 -0
  3. package/README.md +98 -0
  4. package/assets/web/assets/CronEditor-CYgFpX-g.js +1 -0
  5. package/assets/web/assets/DesktopLayout-iI43ij11.js +1 -0
  6. package/assets/web/assets/MobileLayout-CziU8MCR.js +1 -0
  7. package/assets/web/assets/ThemeSwitch-DFohhwd9.js +1 -0
  8. package/assets/web/assets/index-BRROK0vh.js +47 -0
  9. package/assets/web/assets/index-Dj-StEiS.css +2 -0
  10. package/assets/web/assets/separator-CxcFlCzz.js +1 -0
  11. package/assets/web/favicon.svg +1 -0
  12. package/assets/web/icons.svg +24 -0
  13. package/assets/web/index.html +15 -0
  14. package/assets/webui.html +115 -0
  15. package/client/index.ts +63 -0
  16. package/config/default_config/app.yaml +64 -0
  17. package/config/default_config/bilibili.yaml +116 -0
  18. package/config/default_config/config.json +157 -0
  19. package/config/default_config/cookies.yaml +11 -0
  20. package/config/default_config/douyin.yaml +99 -0
  21. package/config/default_config/kuaishou.yaml +8 -0
  22. package/config/default_config/pushlist.yaml +37 -0
  23. package/config/default_config/request.yaml +22 -0
  24. package/config/default_config/upload.yaml +50 -0
  25. package/config/default_config/xiaohongshu.yaml +23 -0
  26. package/dist/index.js +1 -0
  27. package/lib/compat/axios.js +23 -0
  28. package/lib/compat/cron.js +77 -0
  29. package/lib/compat/esm.js +15 -0
  30. package/lib/compat/express.js +23 -0
  31. package/lib/compat/heroui.js +12 -0
  32. package/lib/compat/lodash.js +33 -0
  33. package/lib/compat/logger.js +118 -0
  34. package/lib/compat/node-karin.js +1053 -0
  35. package/lib/compat/phosphor-icons.js +27 -0
  36. package/lib/compat/qqcard.js +256 -0
  37. package/lib/compat/root.js +44 -0
  38. package/lib/compat/runtime.js +87 -0
  39. package/lib/compat/segment.js +167 -0
  40. package/lib/compat/sqlite3.js +117 -0
  41. package/lib/compat/start.js +7 -0
  42. package/lib/compat/syncConfig.js +65 -0
  43. package/lib/compat/template-react.js +30 -0
  44. package/lib/compat/text.js +65 -0
  45. package/lib/compat/yaml.js +13 -0
  46. package/lib/configBridge.js +91 -0
  47. package/lib/index.js +775 -0
  48. package/lib/karin/app.js +34 -0
  49. package/lib/karin/apps/__ready.js +9 -0
  50. package/lib/karin/apps/admin.js +136 -0
  51. package/lib/karin/apps/help.js +233 -0
  52. package/lib/karin/apps/push.js +291 -0
  53. package/lib/karin/apps/qrlogin.js +176 -0
  54. package/lib/karin/apps/statistics.js +209 -0
  55. package/lib/karin/apps/testPush.js +247 -0
  56. package/lib/karin/apps/tools.js +473 -0
  57. package/lib/karin/apps/update.js +252 -0
  58. package/lib/karin/index.js +37 -0
  59. package/lib/karin/module/db/bilibili.js +831 -0
  60. package/lib/karin/module/db/douyin.js +918 -0
  61. package/lib/karin/module/db/index.js +165 -0
  62. package/lib/karin/module/db/migration.js +199 -0
  63. package/lib/karin/module/db/statistics.js +559 -0
  64. package/lib/karin/module/index.js +94 -0
  65. package/lib/karin/module/server/constants/routes.js +48 -0
  66. package/lib/karin/module/server/controllers/bots.js +230 -0
  67. package/lib/karin/module/server/controllers/config.js +75 -0
  68. package/lib/karin/module/server/controllers/video.js +208 -0
  69. package/lib/karin/module/server/index.js +26 -0
  70. package/lib/karin/module/server/middlewares/auth.js +133 -0
  71. package/lib/karin/module/server/routes/amagi.js +57 -0
  72. package/lib/karin/module/server/routes/api.js +40 -0
  73. package/lib/karin/module/server/routes/index.js +97 -0
  74. package/lib/karin/module/server/routes/ssr.js +16 -0
  75. package/lib/karin/module/server/routes/static.js +43 -0
  76. package/lib/karin/module/utils/Base.js +403 -0
  77. package/lib/karin/module/utils/CardParser.js +487 -0
  78. package/lib/karin/module/utils/Common.js +418 -0
  79. package/lib/karin/module/utils/Config.js +434 -0
  80. package/lib/karin/module/utils/EmojiReaction.js +242 -0
  81. package/lib/karin/module/utils/ErrorHandler/handler.js +114 -0
  82. package/lib/karin/module/utils/ErrorHandler/index.js +55 -0
  83. package/lib/karin/module/utils/ErrorHandler/render.js +182 -0
  84. package/lib/karin/module/utils/ErrorHandler/sender.js +206 -0
  85. package/lib/karin/module/utils/ErrorHandler/strategy.js +41 -0
  86. package/lib/karin/module/utils/ErrorHandler/types.js +2 -0
  87. package/lib/karin/module/utils/ErrorHandler/utils.js +108 -0
  88. package/lib/karin/module/utils/FFmpeg.js +298 -0
  89. package/lib/karin/module/utils/ImageHelper.js +91 -0
  90. package/lib/karin/module/utils/ImageSlice.js +429 -0
  91. package/lib/karin/module/utils/MotionPhoto.js +250 -0
  92. package/lib/karin/module/utils/Network/Downloader.js +413 -0
  93. package/lib/karin/module/utils/Network/ImageDownloader.js +250 -0
  94. package/lib/karin/module/utils/Network/Network.js +332 -0
  95. package/lib/karin/module/utils/Network/ThrottleStream.js +71 -0
  96. package/lib/karin/module/utils/Network/constants.js +129 -0
  97. package/lib/karin/module/utils/Network/helpers.js +174 -0
  98. package/lib/karin/module/utils/Network/index.js +34 -0
  99. package/lib/karin/module/utils/Network/types.js +12 -0
  100. package/lib/karin/module/utils/Networks.js +17 -0
  101. package/lib/karin/module/utils/ParseLock.js +34 -0
  102. package/lib/karin/module/utils/ParseOverride.js +88 -0
  103. package/lib/karin/module/utils/QqCardResolve.js +114 -0
  104. package/lib/karin/module/utils/QqPanel.js +879 -0
  105. package/lib/karin/module/utils/Render/coverTheme.js +256 -0
  106. package/lib/karin/module/utils/Render/index.js +582 -0
  107. package/lib/karin/module/utils/amagiClient.js +338 -0
  108. package/lib/karin/module/utils/avatar.js +201 -0
  109. package/lib/karin/module/utils/bot.js +141 -0
  110. package/lib/karin/module/utils/build-metadata.js +50 -0
  111. package/lib/karin/module/utils/changelog.js +166 -0
  112. package/lib/karin/module/utils/index.js +26 -0
  113. package/lib/karin/module/utils/loginSession.js +259 -0
  114. package/lib/karin/module/utils/parseTip.js +93 -0
  115. package/lib/karin/module/utils/runtime-report.js +140 -0
  116. package/lib/karin/module/utils/semver.js +72 -0
  117. package/lib/karin/module/utils/statisticsAggregate.js +415 -0
  118. package/lib/karin/platform/bilibili/bilibili.js +1805 -0
  119. package/lib/karin/platform/bilibili/comments.js +294 -0
  120. package/lib/karin/platform/bilibili/danmaku.js +555 -0
  121. package/lib/karin/platform/bilibili/dynamic-text.js +827 -0
  122. package/lib/karin/platform/bilibili/genParams.js +37 -0
  123. package/lib/karin/platform/bilibili/getID.js +159 -0
  124. package/lib/karin/platform/bilibili/index.js +24 -0
  125. package/lib/karin/platform/bilibili/live-status.js +40 -0
  126. package/lib/karin/platform/bilibili/login.js +132 -0
  127. package/lib/karin/platform/bilibili/push.js +1397 -0
  128. package/lib/karin/platform/bilibili/riskControl.js +133 -0
  129. package/lib/karin/platform/bilibili/search.js +299 -0
  130. package/lib/karin/platform/common/danmaku-types.js +12 -0
  131. package/lib/karin/platform/douyin/comments.js +310 -0
  132. package/lib/karin/platform/douyin/danmaku.js +893 -0
  133. package/lib/karin/platform/douyin/douyin.js +1145 -0
  134. package/lib/karin/platform/douyin/getID.js +94 -0
  135. package/lib/karin/platform/douyin/index.js +24 -0
  136. package/lib/karin/platform/douyin/login.js +230 -0
  137. package/lib/karin/platform/douyin/push/favorite.js +75 -0
  138. package/lib/karin/platform/douyin/push/index.js +21 -0
  139. package/lib/karin/platform/douyin/push/live.js +69 -0
  140. package/lib/karin/platform/douyin/push/post.js +60 -0
  141. package/lib/karin/platform/douyin/push/recommend.js +74 -0
  142. package/lib/karin/platform/douyin/push/render.js +621 -0
  143. package/lib/karin/platform/douyin/push/types.js +2 -0
  144. package/lib/karin/platform/douyin/push.js +1128 -0
  145. package/lib/karin/platform/douyin/types.js +13 -0
  146. package/lib/karin/platform/douyin/videoQuality.js +222 -0
  147. package/lib/karin/platform/douyin/workType.js +182 -0
  148. package/lib/karin/platform/index.js +20 -0
  149. package/lib/karin/platform/kuaishou/comments.js +91 -0
  150. package/lib/karin/platform/kuaishou/getID.js +43 -0
  151. package/lib/karin/platform/kuaishou/getdata.js +41 -0
  152. package/lib/karin/platform/kuaishou/index.js +20 -0
  153. package/lib/karin/platform/kuaishou/kuaishou.js +122 -0
  154. package/lib/karin/platform/xiaohongshu/comments.js +162 -0
  155. package/lib/karin/platform/xiaohongshu/getID.js +129 -0
  156. package/lib/karin/platform/xiaohongshu/index.js +18 -0
  157. package/lib/karin/platform/xiaohongshu/xiaohongshu.js +643 -0
  158. package/lib/karin/root.js +73 -0
  159. package/lib/karin/setup.js +99 -0
  160. package/lib/karin/types/config/amagi.js +2 -0
  161. package/lib/karin/types/config/app.js +2 -0
  162. package/lib/karin/types/config/bilibili.js +2 -0
  163. package/lib/karin/types/config/cookies.js +2 -0
  164. package/lib/karin/types/config/douyin.js +2 -0
  165. package/lib/karin/types/config/index.js +2 -0
  166. package/lib/karin/types/config/kuaishou.js +2 -0
  167. package/lib/karin/types/config/pushlist.js +2 -0
  168. package/lib/karin/types/config/request.js +2 -0
  169. package/lib/karin/types/config/upload.js +2 -0
  170. package/lib/karin/types/config/xiaohongshu.js +2 -0
  171. package/lib/karin/types/datatypes.js +2 -0
  172. package/lib/karin/types/index.js +18 -0
  173. package/lib/karin/types/netrorks/NetworksConfigType.js +2 -0
  174. package/lib/karin/types/netrorks/index.js +17 -0
  175. package/lib/karin/web.config.js +49 -0
  176. package/lib/ktr/registry.js +82 -0
  177. package/lib/ktr/template/_preview/components/PreviewInfoCard.js +10 -0
  178. package/lib/ktr/template/_preview/components/PreviewLayout.js +10 -0
  179. package/lib/ktr/template/_preview/components/PreviewVideoCard.js +8 -0
  180. package/lib/ktr/template/_preview/render.js +142 -0
  181. package/lib/ktr/template/_preview/types.js +2 -0
  182. package/lib/ktr/template/_preview/utils/status.js +31 -0
  183. package/lib/ktr/template/_preview/utils/time.js +12 -0
  184. package/lib/ktr/template/bilibili/bangumi/components/bangumi.js +127 -0
  185. package/lib/ktr/template/bilibili/bangumi/components/types.js +2 -0
  186. package/lib/ktr/template/bilibili/bangumi/index.js +12 -0
  187. package/lib/ktr/template/bilibili/comment/components/Comment.js +193 -0
  188. package/lib/ktr/template/bilibili/comment/components/types.js +2 -0
  189. package/lib/ktr/template/bilibili/comment/index.js +10 -0
  190. package/lib/ktr/template/bilibili/components/Icons.js +66 -0
  191. package/lib/ktr/template/bilibili/components/shared.js +93 -0
  192. package/lib/ktr/template/bilibili/components/types.js +2 -0
  193. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_ARTICLE/components/DYNAMIC_TYPE_ARTICLE.js +66 -0
  194. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_ARTICLE/components/types.js +2 -0
  195. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_ARTICLE/index.js +10 -0
  196. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_AV/components/DYNAMIC_TYPE_AV.js +87 -0
  197. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_AV/components/types.js +2 -0
  198. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_AV/index.js +10 -0
  199. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_DRAW/components/DYNAMIC_TYPE_DRAW.js +93 -0
  200. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_DRAW/components/types.js +2 -0
  201. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_DRAW/index.js +10 -0
  202. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_FORWARD/components/DYNAMIC_TYPE_FORWARD.js +132 -0
  203. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_FORWARD/components/types.js +2 -0
  204. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_FORWARD/index.js +10 -0
  205. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD/components/DYNAMIC_TYPE_LIVE_RCMD.js +84 -0
  206. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD/components/types.js +2 -0
  207. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD/index.js +10 -0
  208. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_WORD/components/DYNAMIC_TYPE_WORD.js +38 -0
  209. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_WORD/components/types.js +2 -0
  210. package/lib/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_WORD/index.js +10 -0
  211. package/lib/ktr/template/bilibili/dynamic/components/AdditionalCard.js +59 -0
  212. package/lib/ktr/template/bilibili/dynamic/components/CommonComponents.js +30 -0
  213. package/lib/ktr/template/bilibili/dynamic/types.js +2 -0
  214. package/lib/ktr/template/bilibili/qrcodeImg/components/qrcodeImg.js +48 -0
  215. package/lib/ktr/template/bilibili/qrcodeImg/components/types.js +3 -0
  216. package/lib/ktr/template/bilibili/qrcodeImg/index.js +10 -0
  217. package/lib/ktr/template/bilibili/userlist/components/UserList.js +53 -0
  218. package/lib/ktr/template/bilibili/userlist/components/types.js +3 -0
  219. package/lib/ktr/template/bilibili/userlist/index.js +13 -0
  220. package/lib/ktr/template/bilibili/videoInfo/components/types.js +2 -0
  221. package/lib/ktr/template/bilibili/videoInfo/components/videoInfo.js +62 -0
  222. package/lib/ktr/template/bilibili/videoInfo/index.js +10 -0
  223. package/lib/ktr/template/components/AmbientCover.js +36 -0
  224. package/lib/ktr/template/components/DefaultLayout.js +33 -0
  225. package/lib/ktr/template/components/GlowImage.js +276 -0
  226. package/lib/ktr/template/components/QRCodeWithAvatar.js +34 -0
  227. package/lib/ktr/template/components/RolldownLogo.js +6 -0
  228. package/lib/ktr/template/components/ViteLogo.js +6 -0
  229. package/lib/ktr/template/components/iconRegistry.js +173 -0
  230. package/lib/ktr/template/douyin/article-work/components/ArticleWork.js +99 -0
  231. package/lib/ktr/template/douyin/article-work/components/types.js +3 -0
  232. package/lib/ktr/template/douyin/article-work/index.js +10 -0
  233. package/lib/ktr/template/douyin/comment/components/Comment.js +189 -0
  234. package/lib/ktr/template/douyin/comment/components/types.js +2 -0
  235. package/lib/ktr/template/douyin/comment/index.js +10 -0
  236. package/lib/ktr/template/douyin/components/Icons.js +29 -0
  237. package/lib/ktr/template/douyin/components/types.js +3 -0
  238. package/lib/ktr/template/douyin/dynamic/components/Dynamic.js +90 -0
  239. package/lib/ktr/template/douyin/dynamic/components/types.js +3 -0
  240. package/lib/ktr/template/douyin/dynamic/index.js +10 -0
  241. package/lib/ktr/template/douyin/favorite-list/components/FavoriteList.js +30 -0
  242. package/lib/ktr/template/douyin/favorite-list/components/types.js +3 -0
  243. package/lib/ktr/template/douyin/favorite-list/index.js +10 -0
  244. package/lib/ktr/template/douyin/image-work/components/ImageWork.js +136 -0
  245. package/lib/ktr/template/douyin/image-work/components/types.js +2 -0
  246. package/lib/ktr/template/douyin/image-work/index.js +10 -0
  247. package/lib/ktr/template/douyin/live/components/Live.js +51 -0
  248. package/lib/ktr/template/douyin/live/components/types.js +3 -0
  249. package/lib/ktr/template/douyin/live/index.js +10 -0
  250. package/lib/ktr/template/douyin/musicinfo/components/MusicInfo.js +60 -0
  251. package/lib/ktr/template/douyin/musicinfo/components/types.js +3 -0
  252. package/lib/ktr/template/douyin/musicinfo/index.js +10 -0
  253. package/lib/ktr/template/douyin/qrcodeImg/components/qrcodeImg.js +39 -0
  254. package/lib/ktr/template/douyin/qrcodeImg/components/types.js +3 -0
  255. package/lib/ktr/template/douyin/qrcodeImg/index.js +9 -0
  256. package/lib/ktr/template/douyin/recommend-list/components/RecommendList.js +30 -0
  257. package/lib/ktr/template/douyin/recommend-list/components/types.js +3 -0
  258. package/lib/ktr/template/douyin/recommend-list/index.js +10 -0
  259. package/lib/ktr/template/douyin/user_profile/components/UserVideoList.js +54 -0
  260. package/lib/ktr/template/douyin/user_profile/components/types.js +3 -0
  261. package/lib/ktr/template/douyin/user_profile/index.js +9 -0
  262. package/lib/ktr/template/douyin/userlist/components/UserList.js +50 -0
  263. package/lib/ktr/template/douyin/userlist/components/types.js +3 -0
  264. package/lib/ktr/template/douyin/userlist/index.js +13 -0
  265. package/lib/ktr/template/douyin/video-work/components/VideoWork.js +127 -0
  266. package/lib/ktr/template/douyin/video-work/components/types.js +2 -0
  267. package/lib/ktr/template/douyin/video-work/index.js +10 -0
  268. package/lib/ktr/template/kuaishou/comment/components/Comment.js +88 -0
  269. package/lib/ktr/template/kuaishou/comment/components/types.js +2 -0
  270. package/lib/ktr/template/kuaishou/comment/index.js +10 -0
  271. package/lib/ktr/template/other/changelog/components/changelog.js +117 -0
  272. package/lib/ktr/template/other/changelog/components/types.js +3 -0
  273. package/lib/ktr/template/other/changelog/index.js +9 -0
  274. package/lib/ktr/template/other/handlerError/components/errorTitles.js +227 -0
  275. package/lib/ktr/template/other/handlerError/components/handlerError.js +392 -0
  276. package/lib/ktr/template/other/handlerError/components/stackHighlight.js +109 -0
  277. package/lib/ktr/template/other/handlerError/components/types.js +2 -0
  278. package/lib/ktr/template/other/handlerError/index.js +9 -0
  279. package/lib/ktr/template/other/help/components/Help.js +79 -0
  280. package/lib/ktr/template/other/help/components/types.js +3 -0
  281. package/lib/ktr/template/other/help/index.js +12 -0
  282. package/lib/ktr/template/other/live-photo-tip/components/LivePhotoTip.js +80 -0
  283. package/lib/ktr/template/other/live-photo-tip/components/types.js +3 -0
  284. package/lib/ktr/template/other/live-photo-tip/index.js +9 -0
  285. package/lib/ktr/template/other/qrlogin/components/qrlogin.js +58 -0
  286. package/lib/ktr/template/other/qrlogin/components/types.js +3 -0
  287. package/lib/ktr/template/other/qrlogin/index.js +9 -0
  288. package/lib/ktr/template/other/runtime/components/runtime.js +101 -0
  289. package/lib/ktr/template/other/runtime/components/types.js +3 -0
  290. package/lib/ktr/template/other/runtime/index.js +9 -0
  291. package/lib/ktr/template/other/version_warning/components/VersionWarning.js +38 -0
  292. package/lib/ktr/template/other/version_warning/components/types.js +3 -0
  293. package/lib/ktr/template/other/version_warning/index.js +9 -0
  294. package/lib/ktr/template/statistics/components/BoxplotGuide.js +17 -0
  295. package/lib/ktr/template/statistics/components/ChartCard.js +15 -0
  296. package/lib/ktr/template/statistics/components/KpiCard.js +14 -0
  297. package/lib/ktr/template/statistics/components/PlatformTable.js +20 -0
  298. package/lib/ktr/template/statistics/components/RankList.js +61 -0
  299. package/lib/ktr/template/statistics/components/SSRChart.js +22 -0
  300. package/lib/ktr/template/statistics/components/chartPresets.js +630 -0
  301. package/lib/ktr/template/statistics/components/chartTheme.js +226 -0
  302. package/lib/ktr/template/statistics/components/format.js +38 -0
  303. package/lib/ktr/template/statistics/components/types.js +9 -0
  304. package/lib/ktr/template/statistics/global/components/GlobalStatistics.js +246 -0
  305. package/lib/ktr/template/statistics/global/components/types.js +3 -0
  306. package/lib/ktr/template/statistics/global/index.js +9 -0
  307. package/lib/ktr/template/statistics/group/components/GroupStatistics.js +181 -0
  308. package/lib/ktr/template/statistics/group/components/types.js +3 -0
  309. package/lib/ktr/template/statistics/group/index.js +9 -0
  310. package/lib/ktr/template/types/ctx.js +2 -0
  311. package/lib/ktr/template/xiaohongshu/comment/components/Comment.js +99 -0
  312. package/lib/ktr/template/xiaohongshu/comment/components/types.js +3 -0
  313. package/lib/ktr/template/xiaohongshu/comment/index.js +9 -0
  314. package/lib/ktr/template/xiaohongshu/noteInfo/components/noteInfo.js +93 -0
  315. package/lib/ktr/template/xiaohongshu/noteInfo/components/types.js +2 -0
  316. package/lib/ktr/template/xiaohongshu/noteInfo/index.js +9 -0
  317. package/lib/ktr/utils/QRCodeAvatar.js +99 -0
  318. package/lib/ktr/utils/QRcode.js +56 -0
  319. package/lib/ktr/utils/avatarProxy.js +45 -0
  320. package/lib/ktr/utils/cn.js +11 -0
  321. package/lib/ktr/utils/theme.js +12 -0
  322. package/lib/qqFields.json +75 -0
  323. package/lib/qqOptions.js +44 -0
  324. package/lib/richtext/index.js +25 -0
  325. package/lib/richtext/parse/index.js +232 -0
  326. package/lib/richtext/react/cn.js +11 -0
  327. package/lib/richtext/react/index.js +221 -0
  328. package/lib/richtext/types.js +2 -0
  329. package/lib/schema.js +352 -0
  330. package/lib/webui.js +341 -0
  331. package/package.json +88 -0
  332. package/resources/image/bilibili/RICH_TEXT_NODE_TYPE/RICH_TEXT_NODE_TYPE_BV.svg +1 -0
  333. package/resources/image/bilibili/RICH_TEXT_NODE_TYPE/RICH_TEXT_NODE_TYPE_GOODS.svg +1 -0
  334. package/resources/image/bilibili/RICH_TEXT_NODE_TYPE/RICH_TEXT_NODE_TYPE_LOTTERY.svg +1 -0
  335. package/resources/image/bilibili/RICH_TEXT_NODE_TYPE/RICH_TEXT_NODE_TYPE_VIEW_PICTURE.svg +1 -0
  336. package/resources/image/bilibili/RICH_TEXT_NODE_TYPE/RICH_TEXT_NODE_TYPE_VOTE.svg +1 -0
  337. package/resources/image/bilibili/RICH_TEXT_NODE_TYPE/RICH_TEXT_NODE_TYPE_WEB.svg +1 -0
  338. package/resources/image/bilibili/bilibili-dark.png +0 -0
  339. package/resources/image/bilibili/bilibili-light.png +0 -0
  340. package/resources/image/bilibili/bilibili.png +0 -0
  341. package/resources/image/bilibili/id-dark.svg +1 -0
  342. package/resources/image/bilibili/id-light.svg +1 -0
  343. package/resources/image/bilibili/level/lv0.svg +1 -0
  344. package/resources/image/bilibili/level/lv1.svg +1 -0
  345. package/resources/image/bilibili/level/lv2.svg +1 -0
  346. package/resources/image/bilibili/level/lv3.svg +1 -0
  347. package/resources/image/bilibili/level/lv4.svg +1 -0
  348. package/resources/image/bilibili/level/lv5.svg +1 -0
  349. package/resources/image/bilibili/level/lv6.svg +1 -0
  350. package/resources/image/bilibili/level/lv7.svg +1 -0
  351. package/resources/image/bilibili/like-dark.svg +6 -0
  352. package/resources/image/bilibili/like-light.svg +6 -0
  353. package/resources/image/bilibili/play.svg +28 -0
  354. package/resources/image/bilibili/res-local1.png +0 -0
  355. package/resources/image/bilibili/up_pb.svg +1 -0
  356. package/resources/image/bilibili//347/233/264/346/222/255/344/270/255.png +0 -0
  357. package/resources/image/douyin/douyin.png +0 -0
  358. package/resources/image/douyin/dylogo-dark.svg +23 -0
  359. package/resources/image/douyin/dylogo-light.svg +38 -0
  360. package/resources/image/douyin/search-dark.svg +1 -0
  361. package/resources/image/douyin/search-light.svg +3 -0
  362. package/resources/image/douyin//346/212/226/351/237/263-/347/233/264/346/222/255/344/270/255.png +0 -0
  363. package/resources/image/frame-logo.png +0 -0
  364. package/resources/image/kuaishou/logo.png +0 -0
  365. package/resources/image/other/handlerError/Milky.png +0 -0
  366. package/resources/image/other/handlerError/chronocat.svg +43 -0
  367. package/resources/image/other/handlerError/conwechat.webp +0 -0
  368. package/resources/image/other/handlerError/gocq-http.webp +0 -0
  369. package/resources/image/other/handlerError/lagrange.webp +0 -0
  370. package/resources/image/other/handlerError/llonebot.webp +0 -0
  371. package/resources/image/other/handlerError/napcat.webp +0 -0
  372. package/resources/image/other/handlerError/onebot.png +0 -0
  373. package/resources/image/other/handlerError/satori.png +0 -0
  374. package/resources/image/xiaohongshu/logo.png +0 -0
  375. package/resources/template/style.css +23708 -0
  376. package/resources/vite.svg +130 -0
  377. package/scripts/build.mjs +129 -0
  378. package/scripts/check-schema.cjs +55 -0
  379. package/scripts/deploy.mjs +78 -0
  380. package/scripts/make-demo-placeholders.mjs +112 -0
  381. package/scripts/patch-deps.mjs +46 -0
  382. package/scripts/patch-webui.mjs +112 -0
  383. package/scripts/smoke-commands.cjs +39 -0
  384. package/scripts/smoke-config.cjs +122 -0
  385. package/scripts/smoke-douyin.cjs +170 -0
  386. package/scripts/smoke-koishi-commands.cjs +210 -0
  387. package/scripts/smoke-login.cjs +110 -0
  388. package/scripts/smoke-parse.cjs +54 -0
  389. package/scripts/smoke-push-mock.cjs +114 -0
  390. package/scripts/smoke-push.cjs +104 -0
  391. package/scripts/smoke-qq.cjs +147 -0
  392. package/scripts/smoke-qqcard.cjs +108 -0
  393. package/scripts/smoke-qqpanel.cjs +205 -0
  394. package/scripts/smoke-render.cjs +106 -0
  395. package/scripts/smoke-server.cjs +94 -0
  396. package/src/compat/axios.ts +5 -0
  397. package/src/compat/cron.ts +86 -0
  398. package/src/compat/esm.ts +13 -0
  399. package/src/compat/express.ts +5 -0
  400. package/src/compat/heroui.tsx +53 -0
  401. package/src/compat/lodash.ts +32 -0
  402. package/src/compat/logger.ts +121 -0
  403. package/src/compat/node-karin.ts +1064 -0
  404. package/src/compat/phosphor-icons.tsx +25 -0
  405. package/src/compat/qqcard.ts +251 -0
  406. package/src/compat/root.ts +44 -0
  407. package/src/compat/runtime.ts +120 -0
  408. package/src/compat/segment.ts +161 -0
  409. package/src/compat/sqlite3.ts +123 -0
  410. package/src/compat/start.ts +3 -0
  411. package/src/compat/syncConfig.ts +64 -0
  412. package/src/compat/template-react.ts +54 -0
  413. package/src/compat/text.ts +58 -0
  414. package/src/compat/yaml.ts +6 -0
  415. package/src/configBridge.ts +94 -0
  416. package/src/index.ts +769 -0
  417. package/src/karin/app.ts +1 -0
  418. package/src/karin/apps/__ready.ts +14 -0
  419. package/src/karin/apps/admin.ts +127 -0
  420. package/src/karin/apps/help.ts +228 -0
  421. package/src/karin/apps/push.ts +327 -0
  422. package/src/karin/apps/qrlogin.ts +160 -0
  423. package/src/karin/apps/statistics.ts +203 -0
  424. package/src/karin/apps/testPush.ts +226 -0
  425. package/src/karin/apps/tools.ts +510 -0
  426. package/src/karin/apps/update.ts +239 -0
  427. package/src/karin/index.ts +9 -0
  428. package/src/karin/module/db/bilibili.ts +1089 -0
  429. package/src/karin/module/db/douyin.ts +1218 -0
  430. package/src/karin/module/db/index.ts +159 -0
  431. package/src/karin/module/db/migration.ts +249 -0
  432. package/src/karin/module/db/statistics.ts +817 -0
  433. package/src/karin/module/index.ts +37 -0
  434. package/src/karin/module/server/constants/routes.ts +53 -0
  435. package/src/karin/module/server/controllers/bots.ts +289 -0
  436. package/src/karin/module/server/controllers/config.ts +85 -0
  437. package/src/karin/module/server/controllers/video.ts +212 -0
  438. package/src/karin/module/server/index.ts +11 -0
  439. package/src/karin/module/server/middlewares/auth.ts +147 -0
  440. package/src/karin/module/server/routes/amagi.ts +57 -0
  441. package/src/karin/module/server/routes/api.ts +42 -0
  442. package/src/karin/module/server/routes/index.ts +68 -0
  443. package/src/karin/module/server/routes/ssr.ts +14 -0
  444. package/src/karin/module/server/routes/static.ts +43 -0
  445. package/src/karin/module/utils/Base.ts +497 -0
  446. package/src/karin/module/utils/CardParser.ts +503 -0
  447. package/src/karin/module/utils/Common.ts +448 -0
  448. package/src/karin/module/utils/Config.ts +443 -0
  449. package/src/karin/module/utils/EmojiReaction.ts +268 -0
  450. package/src/karin/module/utils/ErrorHandler/handler.ts +124 -0
  451. package/src/karin/module/utils/ErrorHandler/index.ts +48 -0
  452. package/src/karin/module/utils/ErrorHandler/render.ts +180 -0
  453. package/src/karin/module/utils/ErrorHandler/sender.ts +192 -0
  454. package/src/karin/module/utils/ErrorHandler/strategy.ts +41 -0
  455. package/src/karin/module/utils/ErrorHandler/types.ts +122 -0
  456. package/src/karin/module/utils/ErrorHandler/utils.ts +108 -0
  457. package/src/karin/module/utils/FFmpeg.ts +357 -0
  458. package/src/karin/module/utils/ImageHelper.ts +90 -0
  459. package/src/karin/module/utils/ImageSlice.ts +383 -0
  460. package/src/karin/module/utils/MotionPhoto.ts +297 -0
  461. package/src/karin/module/utils/Network/Downloader.ts +481 -0
  462. package/src/karin/module/utils/Network/ImageDownloader.ts +291 -0
  463. package/src/karin/module/utils/Network/Network.ts +344 -0
  464. package/src/karin/module/utils/Network/ThrottleStream.ts +76 -0
  465. package/src/karin/module/utils/Network/constants.ts +135 -0
  466. package/src/karin/module/utils/Network/helpers.ts +183 -0
  467. package/src/karin/module/utils/Network/index.ts +28 -0
  468. package/src/karin/module/utils/Network/types.ts +46 -0
  469. package/src/karin/module/utils/Networks.ts +20 -0
  470. package/src/karin/module/utils/ParseLock.ts +33 -0
  471. package/src/karin/module/utils/ParseOverride.ts +128 -0
  472. package/src/karin/module/utils/QqCardResolve.ts +116 -0
  473. package/src/karin/module/utils/QqPanel.ts +880 -0
  474. package/src/karin/module/utils/Render/coverTheme.ts +294 -0
  475. package/src/karin/module/utils/Render/index.ts +574 -0
  476. package/src/karin/module/utils/amagiClient.ts +368 -0
  477. package/src/karin/module/utils/avatar.ts +220 -0
  478. package/src/karin/module/utils/bot.ts +115 -0
  479. package/src/karin/module/utils/build-metadata.ts +60 -0
  480. package/src/karin/module/utils/changelog.ts +175 -0
  481. package/src/karin/module/utils/index.ts +10 -0
  482. package/src/karin/module/utils/loginSession.ts +291 -0
  483. package/src/karin/module/utils/parseTip.ts +59 -0
  484. package/src/karin/module/utils/runtime-report.ts +142 -0
  485. package/src/karin/module/utils/semver.ts +60 -0
  486. package/src/karin/module/utils/statisticsAggregate.ts +507 -0
  487. package/src/karin/platform/bilibili/bilibili.ts +2004 -0
  488. package/src/karin/platform/bilibili/comments.ts +397 -0
  489. package/src/karin/platform/bilibili/danmaku.ts +653 -0
  490. package/src/karin/platform/bilibili/dynamic-text.ts +926 -0
  491. package/src/karin/platform/bilibili/genParams.ts +30 -0
  492. package/src/karin/platform/bilibili/getID.ts +163 -0
  493. package/src/karin/platform/bilibili/index.ts +8 -0
  494. package/src/karin/platform/bilibili/live-status.ts +36 -0
  495. package/src/karin/platform/bilibili/login.ts +130 -0
  496. package/src/karin/platform/bilibili/push.ts +1615 -0
  497. package/src/karin/platform/bilibili/riskControl.ts +114 -0
  498. package/src/karin/platform/bilibili/search.ts +305 -0
  499. package/src/karin/platform/common/danmaku-types.ts +18 -0
  500. package/src/karin/platform/douyin/comments.ts +404 -0
  501. package/src/karin/platform/douyin/danmaku.ts +1187 -0
  502. package/src/karin/platform/douyin/douyin.ts +1244 -0
  503. package/src/karin/platform/douyin/getID.ts +101 -0
  504. package/src/karin/platform/douyin/index.ts +8 -0
  505. package/src/karin/platform/douyin/login.ts +251 -0
  506. package/src/karin/platform/douyin/push/favorite.ts +97 -0
  507. package/src/karin/platform/douyin/push/index.ts +5 -0
  508. package/src/karin/platform/douyin/push/live.ts +86 -0
  509. package/src/karin/platform/douyin/push/post.ts +75 -0
  510. package/src/karin/platform/douyin/push/recommend.ts +96 -0
  511. package/src/karin/platform/douyin/push/render.ts +768 -0
  512. package/src/karin/platform/douyin/push/types.ts +41 -0
  513. package/src/karin/platform/douyin/push.ts +1241 -0
  514. package/src/karin/platform/douyin/types.ts +151 -0
  515. package/src/karin/platform/douyin/videoQuality.ts +234 -0
  516. package/src/karin/platform/douyin/workType.ts +238 -0
  517. package/src/karin/platform/index.ts +4 -0
  518. package/src/karin/platform/kuaishou/comments.ts +114 -0
  519. package/src/karin/platform/kuaishou/getID.ts +48 -0
  520. package/src/karin/platform/kuaishou/getdata.ts +78 -0
  521. package/src/karin/platform/kuaishou/index.ts +4 -0
  522. package/src/karin/platform/kuaishou/kuaishou.ts +130 -0
  523. package/src/karin/platform/xiaohongshu/comments.ts +227 -0
  524. package/src/karin/platform/xiaohongshu/getID.ts +127 -0
  525. package/src/karin/platform/xiaohongshu/index.ts +2 -0
  526. package/src/karin/platform/xiaohongshu/xiaohongshu.ts +733 -0
  527. package/src/karin/root.ts +78 -0
  528. package/src/karin/setup.ts +77 -0
  529. package/src/karin/types/config/amagi.ts +46 -0
  530. package/src/karin/types/config/app.ts +129 -0
  531. package/src/karin/types/config/bilibili.ts +121 -0
  532. package/src/karin/types/config/cookies.ts +11 -0
  533. package/src/karin/types/config/douyin.ts +93 -0
  534. package/src/karin/types/config/index.ts +41 -0
  535. package/src/karin/types/config/kuaishou.ts +10 -0
  536. package/src/karin/types/config/pushlist.ts +55 -0
  537. package/src/karin/types/config/request.ts +27 -0
  538. package/src/karin/types/config/upload.ts +49 -0
  539. package/src/karin/types/config/xiaohongshu.ts +24 -0
  540. package/src/karin/types/datatypes.ts +42 -0
  541. package/src/karin/types/index.ts +4 -0
  542. package/src/karin/types/netrorks/NetworksConfigType.ts +61 -0
  543. package/src/karin/types/netrorks/index.ts +1 -0
  544. package/src/karin/web.config.ts +48 -0
  545. package/src/ktr/registry.ts +55 -0
  546. package/src/ktr/template/_preview/components/PreviewInfoCard.tsx +33 -0
  547. package/src/ktr/template/_preview/components/PreviewLayout.tsx +65 -0
  548. package/src/ktr/template/_preview/components/PreviewVideoCard.tsx +22 -0
  549. package/src/ktr/template/_preview/render.ts +158 -0
  550. package/src/ktr/template/_preview/types.ts +15 -0
  551. package/src/ktr/template/_preview/utils/status.ts +29 -0
  552. package/src/ktr/template/_preview/utils/time.ts +8 -0
  553. package/src/ktr/template/bilibili/bangumi/components/bangumi.tsx +462 -0
  554. package/src/ktr/template/bilibili/bangumi/components/types.ts +36 -0
  555. package/src/ktr/template/bilibili/bangumi/index.tsx +9 -0
  556. package/src/ktr/template/bilibili/comment/components/Comment.tsx +657 -0
  557. package/src/ktr/template/bilibili/comment/components/types.ts +26 -0
  558. package/src/ktr/template/bilibili/comment/index.tsx +11 -0
  559. package/src/ktr/template/bilibili/components/Icons.tsx +209 -0
  560. package/src/ktr/template/bilibili/components/shared.tsx +162 -0
  561. package/src/ktr/template/bilibili/components/types.ts +388 -0
  562. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_ARTICLE/components/DYNAMIC_TYPE_ARTICLE.tsx +251 -0
  563. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_ARTICLE/components/types.ts +67 -0
  564. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_ARTICLE/index.tsx +12 -0
  565. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_AV/components/DYNAMIC_TYPE_AV.tsx +238 -0
  566. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_AV/components/types.ts +39 -0
  567. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_AV/index.tsx +12 -0
  568. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_DRAW/components/DYNAMIC_TYPE_DRAW.tsx +205 -0
  569. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_DRAW/components/types.ts +20 -0
  570. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_DRAW/index.tsx +11 -0
  571. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_FORWARD/components/DYNAMIC_TYPE_FORWARD.tsx +348 -0
  572. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_FORWARD/components/types.ts +16 -0
  573. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_FORWARD/index.tsx +12 -0
  574. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD/components/DYNAMIC_TYPE_LIVE_RCMD.tsx +264 -0
  575. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD/components/types.ts +32 -0
  576. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD/index.tsx +12 -0
  577. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_WORD/components/DYNAMIC_TYPE_WORD.tsx +68 -0
  578. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_WORD/components/types.ts +14 -0
  579. package/src/ktr/template/bilibili/dynamic/DYNAMIC_TYPE_WORD/index.tsx +12 -0
  580. package/src/ktr/template/bilibili/dynamic/components/AdditionalCard.tsx +165 -0
  581. package/src/ktr/template/bilibili/dynamic/components/CommonComponents.tsx +142 -0
  582. package/src/ktr/template/bilibili/dynamic/types.ts +382 -0
  583. package/src/ktr/template/bilibili/qrcodeImg/components/qrcodeImg.tsx +179 -0
  584. package/src/ktr/template/bilibili/qrcodeImg/components/types.ts +9 -0
  585. package/src/ktr/template/bilibili/qrcodeImg/index.tsx +12 -0
  586. package/src/ktr/template/bilibili/userlist/components/UserList.tsx +246 -0
  587. package/src/ktr/template/bilibili/userlist/components/types.ts +35 -0
  588. package/src/ktr/template/bilibili/userlist/index.tsx +11 -0
  589. package/src/ktr/template/bilibili/videoInfo/components/types.ts +28 -0
  590. package/src/ktr/template/bilibili/videoInfo/components/videoInfo.tsx +217 -0
  591. package/src/ktr/template/bilibili/videoInfo/index.tsx +12 -0
  592. package/src/ktr/template/components/AmbientCover.tsx +50 -0
  593. package/src/ktr/template/components/DefaultLayout.tsx +166 -0
  594. package/src/ktr/template/components/GlowImage.tsx +572 -0
  595. package/src/ktr/template/components/QRCodeWithAvatar.tsx +52 -0
  596. package/src/ktr/template/components/RolldownLogo.tsx +110 -0
  597. package/src/ktr/template/components/ViteLogo.tsx +134 -0
  598. package/src/ktr/template/components/iconRegistry.ts +294 -0
  599. package/src/ktr/template/douyin/article-work/components/ArticleWork.tsx +301 -0
  600. package/src/ktr/template/douyin/article-work/components/types.ts +53 -0
  601. package/src/ktr/template/douyin/article-work/index.tsx +12 -0
  602. package/src/ktr/template/douyin/comment/components/Comment.tsx +612 -0
  603. package/src/ktr/template/douyin/comment/components/types.ts +85 -0
  604. package/src/ktr/template/douyin/comment/index.tsx +11 -0
  605. package/src/ktr/template/douyin/components/Icons.tsx +75 -0
  606. package/src/ktr/template/douyin/components/types.ts +83 -0
  607. package/src/ktr/template/douyin/dynamic/components/Dynamic.tsx +283 -0
  608. package/src/ktr/template/douyin/dynamic/components/types.ts +46 -0
  609. package/src/ktr/template/douyin/dynamic/index.tsx +11 -0
  610. package/src/ktr/template/douyin/favorite-list/components/FavoriteList.tsx +201 -0
  611. package/src/ktr/template/douyin/favorite-list/components/types.ts +38 -0
  612. package/src/ktr/template/douyin/favorite-list/index.tsx +12 -0
  613. package/src/ktr/template/douyin/image-work/components/ImageWork.tsx +437 -0
  614. package/src/ktr/template/douyin/image-work/components/types.ts +78 -0
  615. package/src/ktr/template/douyin/image-work/index.tsx +11 -0
  616. package/src/ktr/template/douyin/live/components/Live.tsx +229 -0
  617. package/src/ktr/template/douyin/live/components/types.ts +47 -0
  618. package/src/ktr/template/douyin/live/index.tsx +11 -0
  619. package/src/ktr/template/douyin/musicinfo/components/MusicInfo.tsx +187 -0
  620. package/src/ktr/template/douyin/musicinfo/components/types.ts +31 -0
  621. package/src/ktr/template/douyin/musicinfo/index.tsx +11 -0
  622. package/src/ktr/template/douyin/qrcodeImg/components/qrcodeImg.tsx +170 -0
  623. package/src/ktr/template/douyin/qrcodeImg/components/types.ts +9 -0
  624. package/src/ktr/template/douyin/qrcodeImg/index.tsx +9 -0
  625. package/src/ktr/template/douyin/recommend-list/components/RecommendList.tsx +201 -0
  626. package/src/ktr/template/douyin/recommend-list/components/types.ts +37 -0
  627. package/src/ktr/template/douyin/recommend-list/index.tsx +12 -0
  628. package/src/ktr/template/douyin/user_profile/components/UserVideoList.tsx +225 -0
  629. package/src/ktr/template/douyin/user_profile/components/types.ts +71 -0
  630. package/src/ktr/template/douyin/user_profile/index.tsx +9 -0
  631. package/src/ktr/template/douyin/userlist/components/UserList.tsx +229 -0
  632. package/src/ktr/template/douyin/userlist/components/types.ts +35 -0
  633. package/src/ktr/template/douyin/userlist/index.tsx +11 -0
  634. package/src/ktr/template/douyin/video-work/components/VideoWork.tsx +398 -0
  635. package/src/ktr/template/douyin/video-work/components/types.ts +80 -0
  636. package/src/ktr/template/douyin/video-work/index.tsx +11 -0
  637. package/src/ktr/template/font.css +6071 -0
  638. package/src/ktr/template/global.d.ts +69 -0
  639. package/src/ktr/template/kuaishou/comment/components/Comment.tsx +207 -0
  640. package/src/ktr/template/kuaishou/comment/components/types.ts +47 -0
  641. package/src/ktr/template/kuaishou/comment/index.tsx +11 -0
  642. package/src/ktr/template/other/changelog/components/changelog.tsx +411 -0
  643. package/src/ktr/template/other/changelog/components/types.ts +21 -0
  644. package/src/ktr/template/other/changelog/index.tsx +9 -0
  645. package/src/ktr/template/other/handlerError/components/errorTitles.ts +224 -0
  646. package/src/ktr/template/other/handlerError/components/handlerError.tsx +1076 -0
  647. package/src/ktr/template/other/handlerError/components/stackHighlight.ts +127 -0
  648. package/src/ktr/template/other/handlerError/components/types.ts +124 -0
  649. package/src/ktr/template/other/handlerError/index.tsx +9 -0
  650. package/src/ktr/template/other/help/components/Help.tsx +249 -0
  651. package/src/ktr/template/other/help/components/types.ts +47 -0
  652. package/src/ktr/template/other/help/index.tsx +9 -0
  653. package/src/ktr/template/other/live-photo-tip/components/LivePhotoTip.tsx +295 -0
  654. package/src/ktr/template/other/live-photo-tip/components/types.ts +8 -0
  655. package/src/ktr/template/other/live-photo-tip/index.tsx +9 -0
  656. package/src/ktr/template/other/qrlogin/components/qrlogin.tsx +295 -0
  657. package/src/ktr/template/other/qrlogin/components/types.ts +11 -0
  658. package/src/ktr/template/other/qrlogin/index.tsx +9 -0
  659. package/src/ktr/template/other/runtime/components/runtime.tsx +347 -0
  660. package/src/ktr/template/other/runtime/components/types.ts +75 -0
  661. package/src/ktr/template/other/runtime/index.tsx +9 -0
  662. package/src/ktr/template/other/version_warning/components/VersionWarning.tsx +392 -0
  663. package/src/ktr/template/other/version_warning/components/types.ts +8 -0
  664. package/src/ktr/template/other/version_warning/index.tsx +9 -0
  665. package/src/ktr/template/statistics/components/BoxplotGuide.tsx +57 -0
  666. package/src/ktr/template/statistics/components/ChartCard.tsx +40 -0
  667. package/src/ktr/template/statistics/components/KpiCard.tsx +39 -0
  668. package/src/ktr/template/statistics/components/PlatformTable.tsx +83 -0
  669. package/src/ktr/template/statistics/components/RankList.tsx +127 -0
  670. package/src/ktr/template/statistics/components/SSRChart.tsx +41 -0
  671. package/src/ktr/template/statistics/components/chartPresets.ts +700 -0
  672. package/src/ktr/template/statistics/components/chartTheme.ts +234 -0
  673. package/src/ktr/template/statistics/components/format.ts +33 -0
  674. package/src/ktr/template/statistics/components/types.ts +92 -0
  675. package/src/ktr/template/statistics/global/components/GlobalStatistics.tsx +597 -0
  676. package/src/ktr/template/statistics/global/components/types.ts +138 -0
  677. package/src/ktr/template/statistics/global/index.tsx +9 -0
  678. package/src/ktr/template/statistics/group/components/GroupStatistics.tsx +468 -0
  679. package/src/ktr/template/statistics/group/components/types.ts +113 -0
  680. package/src/ktr/template/statistics/group/index.tsx +9 -0
  681. package/src/ktr/template/style.css +65 -0
  682. package/src/ktr/template/types/ctx.ts +42 -0
  683. package/src/ktr/template/xiaohongshu/comment/components/Comment.tsx +264 -0
  684. package/src/ktr/template/xiaohongshu/comment/components/types.ts +119 -0
  685. package/src/ktr/template/xiaohongshu/comment/index.tsx +9 -0
  686. package/src/ktr/template/xiaohongshu/noteInfo/components/noteInfo.tsx +271 -0
  687. package/src/ktr/template/xiaohongshu/noteInfo/components/types.ts +66 -0
  688. package/src/ktr/template/xiaohongshu/noteInfo/index.tsx +9 -0
  689. package/src/ktr/utils/QRCodeAvatar.ts +102 -0
  690. package/src/ktr/utils/QRcode.ts +65 -0
  691. package/src/ktr/utils/avatarProxy.ts +42 -0
  692. package/src/ktr/utils/cn.ts +6 -0
  693. package/src/ktr/utils/theme.ts +10 -0
  694. package/src/qqFields.json +75 -0
  695. package/src/qqOptions.ts +64 -0
  696. package/src/richtext/index.ts +9 -0
  697. package/src/richtext/parse/index.ts +271 -0
  698. package/src/richtext/react/cn.ts +6 -0
  699. package/src/richtext/react/index.tsx +582 -0
  700. package/src/richtext/types.ts +320 -0
  701. package/src/schema.ts +372 -0
  702. package/src/webui.ts +352 -0
@@ -0,0 +1,2004 @@
1
+ import fs from 'node:fs'
2
+ import { buildMarkdownImageMessage } from '@/module/utils/QqPanel'
3
+ import { sendSlicedImage } from '@/module/utils/ImageSlice'
4
+
5
+ import {
6
+ AmagiSuccess,
7
+ BilibiliArticleContentResponse,
8
+ BilibiliBangumiInfoResponse,
9
+ BilibiliBangumiStreamResponse,
10
+ bilibiliApiUrls,
11
+ BiliBiliVideoPlayurlNoLogin,
12
+ BilibiliDynamicDetailResponse,
13
+ BilibiliVideoInfoResponse,
14
+ BilibiliVideoStreamResponse,
15
+ DynamicType,
16
+ DynamicTypeDraw
17
+ } from '@ikenxuan/amagi'
18
+ import type { BilibiliForwardOriginalContentProps } from '@template/template/bilibili/dynamic/types'
19
+ import { DecorationCardData } from '@template/template/bilibili/dynamic/types'
20
+ import { format, formatDistanceToNow, fromUnixTime } from 'date-fns'
21
+ import { zhCN } from 'date-fns/locale'
22
+ import karin, { common, ElementTypes, logger, Message, segment, SendMessage } from 'node-karin'
23
+
24
+ // 番剧在 QQ 上用「卡片 + 分集表格」面板(见 sendBangumiPanel 的说明)
25
+ import { buildDownloadTip, recallLastPanel, replyReplacing, sendBangumiPanel } from '../../module/utils/QqPanel'
26
+
27
+ import type { ParseWorkType } from '@/module/db'
28
+ import {
29
+ Base,
30
+ baseHeaders,
31
+ buildGoogleMotionPhoto,
32
+ Common,
33
+ Count,
34
+ downloadFile,
35
+ downloadVideo,
36
+ extractTotalBytesFromHeaders,
37
+ fileInfo,
38
+ fixM4sFile,
39
+ loopVideoWithTransition,
40
+ mergeVideoAudio,
41
+ Networks,
42
+ processLocalImageFile,
43
+ processImageUrl,
44
+ Render,
45
+ uploadFile
46
+ } from '@/module/utils'
47
+ import { bilibiliFetcher, isSoftFailure, SOFT_ERROR_CODES, softFetch } from '@/module/utils/amagiClient'
48
+ import { Config } from '@/module/utils/Config'
49
+ import { getParseOverride } from '@/module/utils/ParseOverride'
50
+ import { bilibiliComments, BilibiliId, checkCk, genParams } from '@/platform/bilibili'
51
+ import { type BiliDanmakuElem, burnBiliDanmaku, getHotDanmaku, mergeAndBurnBili } from '@/platform/bilibili/danmaku'
52
+ import {
53
+ buildBilibiliArticleRichText,
54
+ buildBilibiliDynamicRichText,
55
+ buildBilibiliRichTextForwardMessage,
56
+ buildBilibiliVideoDescRichText,
57
+ getUsernameMetadata
58
+ } from '@/platform/bilibili/dynamic-text'
59
+ import { BilibiliDataTypes } from '@/types'
60
+
61
+ let img: ElementTypes[]
62
+ type videoDownloadUrlList = BilibiliVideoStreamResponse['data']['dash']['video']
63
+
64
+ /** 评论请求统一使用匿名态,避免账号 Cookie 改变评论热度池结果。 */
65
+ const bilibiliAnonymousRequestConfig = {
66
+ headers: {
67
+ Cookie: ''
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 链接解析出的类型 → 统计用的内容形态。
73
+ * B站这边类型在构造时就定了(`data.type`),不像抖音要等作品详情回来才知道,
74
+ * 所以直接查表,命中不了说明不是可解析的类型,留空让调用方跳过计数。
75
+ */
76
+ const BILIBILI_WORK_TYPES: Record<string, ParseWorkType> = {
77
+ one_video: 'video',
78
+ bangumi_video_info: 'bangumi',
79
+ dynamic_info: 'dynamic',
80
+ live_room_detail: 'live'
81
+ }
82
+
83
+ export class Bilibili extends Base {
84
+ e: Message
85
+ type: any
86
+ STATUS: any
87
+ isVIP: boolean
88
+ Type: BilibiliDataTypes[keyof BilibiliDataTypes]
89
+ islogin: boolean
90
+ downloadfilename: string
91
+ /** 强制烧录弹幕(用于 #弹幕解析 命令) */
92
+ forceBurnDanmaku: boolean
93
+ /** 本次解析的内容形态,供统计埋点读取 */
94
+ workType?: ParseWorkType
95
+ get botadapter(): string {
96
+ return this.e.bot?.adapter?.name
97
+ }
98
+
99
+ constructor(e: Message, data: any, options?: { forceBurnDanmaku?: boolean }) {
100
+ super(e)
101
+ this.e = e
102
+ this.isVIP = false
103
+ this.Type = data?.type
104
+ this.workType = BILIBILI_WORK_TYPES[data?.type as string]
105
+ this.islogin = data?.USER?.STATUS === 'isLogin'
106
+ this.downloadfilename = ''
107
+ this.forceBurnDanmaku = options?.forceBurnDanmaku ?? false
108
+ this.headers!.Referer = 'https://www.bilibili.com/'
109
+ this.headers!.Cookie = Config.amagi.cookies.bilibili
110
+ }
111
+
112
+ async BilibiliHandler(iddata: BilibiliId): Promise<boolean | undefined> {
113
+ // 面板点进来的:卡片已经在面板里发过了,这里只回一句「收到请求」就开始下载
114
+ const fromPanel = getParseOverride()?.fromPanel === true
115
+ if (fromPanel) {
116
+ // 面板点进来的:只回一句「收到请求,开始下载」,并挂一个只查本次任务的进度按钮
117
+ // replyReplacing 会先撤掉上一条(也就是刚点的画质面板),群里只留这句提示
118
+ await replyReplacing(this.e, buildDownloadTip(String(iddata.bvid ?? ''), '收到请求,开始下载'))
119
+ } else if (Config.app.parseTip) {
120
+ // 同样:发这句话时把上一条机器人消息撤掉
121
+ await replyReplacing(this.e, '检测到B站链接,开始解析')
122
+ }
123
+ switch (this.Type) {
124
+ case 'one_video': {
125
+ const infoData = await this.amagi.bilibili.fetcher.fetchVideoInfo({ bvid: iddata.bvid })
126
+ const playUrlData = await this.amagi.bilibili.fetcher.fetchVideoStreamUrl({
127
+ avid: infoData.data.data.aid,
128
+ cid: iddata.p ? (infoData.data.data.pages[iddata.p - 1]?.cid ?? infoData.data.data.cid) : infoData.data.data.cid
129
+ })
130
+ // const playUrl = bilibiliApiUrls.视频流信息({ avid: infoData.data.aid, cid: infoData.data.cid })
131
+ /**
132
+ * 免登录(直链 durl)分支的判断。
133
+ *
134
+ * 上游只看「画质 < 64 就当免登录」——**登录状态下选 360P/480P 会被误判**,
135
+ * 于是走了形状完全不同的直链分支,一取就崩
136
+ * (日志里的 Cannot read properties of undefined (reading '0') 就是它)。
137
+ *
138
+ * 现在:先看 Cookie 到底有没有登录;没登录时才按低画质走直链;接口没给 dash 时也走直链。
139
+ */
140
+ const loginStatus = (await checkCk()).Status === 'isLogin'
141
+ this.islogin = loginStatus
142
+ /** 有没有 dash:无 CK 部署只会下发 durl,这时无论如何都得走直链分支 */
143
+ const hasDash = !!playUrlData.data?.data?.dash
144
+ /** 低画质(< 64):**只在没登录时**才因此走直链;登录了就用 dash 正常取流 */
145
+ const wantLowQuality = Config.bilibili.videoQuality !== 0 && Config.bilibili.videoQuality < 64
146
+ const useAnonymousQuality = loginStatus ? !hasDash : (!hasDash || wantLowQuality)
147
+
148
+ this.downloadfilename = infoData.data.data.title.substring(0, 50).replace(/[\\/:*?"<>|\r\n\s]/g, ' ')
149
+
150
+ /**
151
+ * 免登录 360P 那条不走 amagi(URL 上多一个 `platform=html5`),拿到的也不是登录态那种
152
+ * `dash` 形状,所以这里仍用手写快照树的 `BiliBiliVideoPlayurlNoLogin` —— 它不是任何端点的响应。
153
+ */
154
+ const nockData = (await new Networks({
155
+ url:
156
+ bilibiliApiUrls.getVideoStream({
157
+ avid: infoData.data.data.aid,
158
+ cid: iddata.p ? (infoData.data.data.pages[iddata.p - 1]?.cid ?? infoData.data.data.cid) : infoData.data.data.cid
159
+ }) + '&platform=html5',
160
+ headers: this.headers
161
+ }).getData()) as AmagiSuccess<BiliBiliVideoPlayurlNoLogin>
162
+
163
+ // 如果配置项不存在或长度为0,则不显示任何内容
164
+ // fromPanel:面板里已经发过这张卡片了,别再发一遍
165
+ if (!fromPanel && Config.bilibili.sendContent.some((content) => content === 'info')) {
166
+ if (Config.bilibili.videoInfoMode === 'text') {
167
+ // 构建回复内容数组
168
+ const replyContent: SendMessage = []
169
+ const { coin, like, share, view, favorite, danmaku } = infoData.data.data.stat
170
+ const coverUrl = await processImageUrl(infoData.data.data.pic, infoData.data.data.title)
171
+ const contentMap = {
172
+ cover: segment.image(coverUrl),
173
+ title: segment.text(`\n📺 标题: ${infoData.data.data.title}\n`),
174
+ author: segment.text(`\n👤 作者: ${infoData.data.data.owner.name}\n`),
175
+ stats: segment.text(formatVideoStats(view, danmaku, like, coin, share, favorite)),
176
+ desc: segment.text(`\n\n📝 简介: ${infoData.data.data.desc}`)
177
+ }
178
+ // 重新排序
179
+ const fixedOrder: (keyof typeof contentMap)[] = ['cover', 'title', 'author', 'stats', 'desc']
180
+ fixedOrder.forEach((item) => {
181
+ if (Config.bilibili.displayContent.includes(item) && contentMap[item]) {
182
+ replyContent.push(contentMap[item])
183
+ }
184
+ })
185
+ if (replyContent.length > 0) {
186
+ this.e.reply(replyContent)
187
+ }
188
+ } else {
189
+ // 渲染为图片
190
+ const userProfileData = await this.amagi.bilibili.fetcher.fetchUserCard({
191
+ host_mid: infoData.data.data.owner.mid
192
+ })
193
+ // 获取弹幕并统计出现次数最多的几条,用于模板展示(仅当配置开启时)
194
+ let hotDanmaku: ReturnType<typeof getHotDanmaku> | undefined
195
+ if (Config.bilibili.showDanmakuInVideoInfo) {
196
+ const danmakuCid = iddata.p ? (infoData.data.data.pages[iddata.p - 1]?.cid ?? infoData.data.data.cid) : infoData.data.data.cid
197
+ const danmakuDuration = iddata.p
198
+ ? (infoData.data.data.pages[iddata.p - 1]?.duration ?? infoData.data.data.duration)
199
+ : infoData.data.data.duration
200
+ const infoDanmakuList = await this.fetchVideoDanmakuList(danmakuCid, danmakuDuration)
201
+ hotDanmaku = getHotDanmaku(infoDanmakuList, 20)
202
+ }
203
+ const img = await Render(this.e, 'bilibili/videoInfo', {
204
+ share_url: 'https://b23.tv/' + infoData.data.data.bvid,
205
+ title: infoData.data.data.title,
206
+ desc: infoData.data.data.desc_v2?.length
207
+ ? buildBilibiliVideoDescRichText(infoData.data.data.desc_v2)
208
+ : buildBilibiliDynamicRichText(infoData.data.data.desc || '', []),
209
+ stat: infoData.data.data.stat,
210
+ bvid: infoData.data.data.bvid,
211
+ ctime: infoData.data.data.ctime,
212
+ pic: infoData.data.data.pic,
213
+ hotDanmaku,
214
+ owner: {
215
+ ...infoData.data.data.owner,
216
+ usernameMeta: getUsernameMetadata(userProfileData.data.data.card),
217
+ frame: userProfileData.data.data.card.pendant?.image || ''
218
+ }
219
+ })
220
+ this.e.reply(img)
221
+ }
222
+ }
223
+
224
+ let videoSize = ''
225
+ let correctList!: {
226
+ accept_description: string[]
227
+ videoList: videoDownloadUrlList
228
+ }
229
+
230
+ /**
231
+ * 选流:**已登录时一律按画质筛选**。
232
+ *
233
+ * 上游这里判断的是「画质 > 64 或自动」,于是请求低画质(360P/480P)时会掉进 else 分支 ——
234
+ * 那边不筛流,直接用 dash.video[0],而 B站返回的列表是**按画质从高到低**的,
235
+ * 结果就是要 360P 却下了个 4K:文件巨大、手机还解不出来,表现就是「只有声音没有画面」。
236
+ */
237
+ if (this.islogin && !useAnonymousQuality) {
238
+ /** 提取出视频流信息对象,并排除清晰度重复的视频流 */
239
+ const simplify = playUrlData.data.data.dash.video.filter((item: { id: number }, index: any, self: any[]) => {
240
+ return (
241
+ self.findIndex((t: { id: any }) => {
242
+ return t.id === item.id
243
+ }) === index
244
+ )
245
+ })
246
+ /** 替换原始的视频信息对象 */
247
+ playUrlData.data.data.dash.video = simplify
248
+ /** 没有音频流(如纯视频稿件)时拿不到音频地址,按只统计视频流大小处理 */
249
+ const audioUrl = playUrlData.data.data.dash.audio?.[0]?.base_url
250
+ /** 给视频信息对象删除不符合条件的视频流 */
251
+ correctList = await bilibiliProcessVideos(
252
+ {
253
+ accept_description: playUrlData.data.data.accept_description,
254
+ bvid: infoData.data.data.bvid,
255
+ qn: Config.bilibili.videoQuality
256
+ },
257
+ simplify,
258
+ audioUrl
259
+ )
260
+ playUrlData.data.data.dash.video = correctList.videoList
261
+ playUrlData.data.data.accept_description = correctList.accept_description
262
+ /** 获取第一个视频流的大小 */
263
+ videoSize = await getvideosize(correctList.videoList[0].base_url, audioUrl, infoData.data.data.bvid)
264
+ } else {
265
+ /**
266
+ * 免登录直链分支:体积取自 html5 播放接口的 \`durl[0].size\`。
267
+ * 这个接口会偶发拿不到 durl(风控、字段变化、超时),原来这里直接下标取值,
268
+ * 一旦为空就抛 TypeError,**整条解析直接失败**(用户侧表现就是「提示开始解析,然后没下文」)。
269
+ * 体积只是展示信息,取不到就按 0 处理,不要拖垮解析。
270
+ */
271
+ videoSize = ((nockData?.data?.durl?.[0]?.size ?? 0) / (1024 * 1024)).toFixed(2)
272
+ }
273
+ if (Config.bilibili.sendContent.some((content) => content === 'comment')) {
274
+ const commentsData = await softFetch(
275
+ () =>
276
+ this.amagi.bilibili.fetcher.fetchComments(
277
+ {
278
+ number: Config.bilibili.numcomment,
279
+ type: 1,
280
+ oid: infoData.data.data.aid.toString()
281
+ },
282
+ bilibiliAnonymousRequestConfig
283
+ ),
284
+ [SOFT_ERROR_CODES.BILIBILI_COMMENTS_DISABLED]
285
+ )
286
+ if (isSoftFailure(commentsData, SOFT_ERROR_CODES.BILIBILI_COMMENTS_DISABLED)) {
287
+ this.e.reply('UP主已关闭评论区,无法获取评论')
288
+ } else {
289
+ // 诊断:评论接口回来了但列表为空时,把真实结构打出来(历史上多次踩到「多包一层 data」)
290
+ logger.mark(
291
+ '[B站] 评论接口返回: 顶层键=' + JSON.stringify(Object.keys((commentsData as any)?.data ?? {}).slice(0, 8)) +
292
+ ' replies=' + ((commentsData as any)?.data?.replies?.length ?? (commentsData as any)?.data?.data?.replies?.length ?? 0) +
293
+ ' numcomment=' + Config.bilibili.numcomment
294
+ )
295
+ const { comments: commentsdata, image_urls } = bilibiliComments(commentsData.data, infoData.data.data.owner.mid.toString())
296
+ if (!commentsdata?.length) {
297
+ this.e.reply('这个视频没有评论 ~')
298
+ } else {
299
+ /**
300
+ * 评论区图片**不再单独发**(按需求关闭):评论卡片里本来就会展示这些图,
301
+ * 再发一遍合并转发既刷屏又慢。这里保留解析(image_urls 仍在用),只是不发出去。
302
+ */
303
+ const messageElements = []
304
+ if (false && Config.bilibili.commentImageCollection && image_urls.length > 0) {
305
+ for (const [index, v] of image_urls.entries()) {
306
+ const imageUrl = await processImageUrl(v, infoData.data.data.title, index)
307
+ messageElements.push(segment.image(imageUrl))
308
+ }
309
+ /**
310
+ * 评论图片收集:**合并成一条 markdown**(QQ 官方 bot 上合并转发经常发不出去)。
311
+ * md 里连续图片紧贴渲染,一条消息装完整套图;失败再退回转发。
312
+ */
313
+ const mdMessage = await buildMarkdownImageMessage(
314
+ messageElements.map((item: any) => String(item?.attrs?.src ?? '')).filter(Boolean)
315
+ )
316
+ if (mdMessage) {
317
+ await this.e.reply(mdMessage)
318
+ } else {
319
+ const res = common.makeForward(
320
+ messageElements,
321
+ Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId,
322
+ Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name
323
+ )
324
+ await this.e.bot.sendForwardMsg(this.e.contact, res, {
325
+ source: '评论图片收集',
326
+ summary: `查看${messageElements.length}张图片`,
327
+ prompt: 'B站评论解析结果',
328
+ news: [{ text: '点击查看解析结果' }]
329
+ })
330
+ }
331
+ }
332
+
333
+ img = await Render(this.e, 'bilibili/comment', {
334
+ Type: '视频',
335
+ CommentsData: commentsdata,
336
+ CommentLength: Config.bilibili.realCommentCount ? Count(infoData.data.data.stat.reply) : String(commentsdata.length),
337
+ share_url: 'https://b23.tv/' + infoData.data.data.bvid,
338
+ Clarity:
339
+ useAnonymousQuality
340
+ ? (nockData?.data?.accept_description?.slice(-1)[0] ?? '免登录 360P')
341
+ : (playUrlData.data?.data?.accept_description?.[0] ?? '未知画质'),
342
+ VideoSize:
343
+ useAnonymousQuality
344
+ ? Common.formatFileSize(((nockData?.data?.durl?.[0]?.size ?? 0) / (1024 * 1024)).toFixed(2))
345
+ : Common.formatFileSize(videoSize),
346
+ ImageLength: 0,
347
+ shareurl: 'https://b23.tv/' + infoData.data.data.bvid,
348
+ Resolution:
349
+ useAnonymousQuality
350
+ ? null
351
+ : `${playUrlData.data.data.dash.video[0].width} x ${playUrlData.data.data.dash.video[0].height}`
352
+ })
353
+ // 评论卡可能极长(实测 2880x40000),交给切片+md 拼接发送,避免 QQ 拒收
354
+ await sendSlicedImage(this.e, img)
355
+ }
356
+ }
357
+ }
358
+
359
+ if (Config.bilibili.sendContent.some((content) => content === 'video')) {
360
+ if (Config.app.usefilelimit && Number(videoSize) > Number(Config.app.filelimit) && !Config.app.compress) {
361
+ this.e.reply(
362
+ `设定的最大上传大小为 ${Config.app.filelimit}MB\n当前解析到的视频大小为 ${Number(videoSize)}MB\n` +
363
+ '视频太大了,还是去B站看吧~',
364
+ { reply: true }
365
+ )
366
+ } else {
367
+ if (useAnonymousQuality) {
368
+ this.islogin = false
369
+ }
370
+ // 获取弹幕数据
371
+ let danmakuList: BiliDanmakuElem[] = []
372
+ if (this.forceBurnDanmaku || Config.bilibili.burnDanmaku) {
373
+ const cid = iddata.p ? (infoData.data.data.pages[iddata.p - 1]?.cid ?? infoData.data.data.cid) : infoData.data.data.cid
374
+ const duration = iddata.p
375
+ ? (infoData.data.data.pages[iddata.p - 1]?.duration ?? infoData.data.data.duration)
376
+ : infoData.data.data.duration
377
+ danmakuList = await this.fetchVideoDanmakuList(cid, duration)
378
+ }
379
+ await this.getvideo(
380
+ // Koishi 移植修正:原实现这里传的是 \`nockData.data\`,但 getvideo() 读的是
381
+ // \`playUrlData.data.durl\`(与上面的 \`nockData.data.durl\` 同一层),传内层会取不到 durl。
382
+ useAnonymousQuality
383
+ // 免登录分支优先用 html5 播放接口的 durl;那个请求偶发失败(风控/超时),
384
+ // 此时退回 amagi 拿到的 durl,别让「提示开始解析然后没下文」再发生
385
+ ? { playUrlData: (nockData?.data?.durl?.length ? nockData : playUrlData) as any, danmakuList }
386
+ : { infoData: infoData.data, playUrlData: playUrlData.data, danmakuList }
387
+ )
388
+ }
389
+ }
390
+ break
391
+ }
392
+ case 'bangumi_video_info': {
393
+ const videoInfo = await this.amagi.bilibili.fetcher.fetchBangumiInfo({
394
+ [iddata.isEpid ? 'ep_id' : 'season_id']: iddata.realid
395
+ })
396
+ this.islogin = (await checkCk()).Status === 'isLogin'
397
+ this.isVIP = (await checkCk()).isVIP
398
+
399
+ /**
400
+ * QQ 平台:先发「卡片 + 分集表格」面板。
401
+ * 点某一集 → 带着那一集的链接回到画质面板 → 选清晰度 → 才真正解析。
402
+ * 好处是选集不再依赖「120 秒内发第 N 集」的文字匹配(群里别人说话就串了)。
403
+ * 面板发不出去(非 QQ / 没有 assets / 渲染失败)时继续走下面的原流程。
404
+ */
405
+ const bangumiEpisodes = (videoInfo.data.result.episodes ?? []).map((item: any) => ({
406
+ cover: item.cover,
407
+ bvid: item.bvid,
408
+ link: item.short_link,
409
+ long_title: item.long_title,
410
+ pub_time: item.pub_time,
411
+ badge: item.badge === '' ? '限免' : item.badge,
412
+ badge_info: item.badge_info
413
+ }))
414
+ // 面板上的「上一页 / 下一页」会把页码带过来(--bgp),这里直接出对应那一页
415
+ const bangumiPage = Math.max(1, Number(getParseOverride()?.bangumiPage) || 1)
416
+ if (bangumiEpisodes.length && await sendBangumiPanel(this.e, bangumiEpisodes, {
417
+ mainCover: videoInfo.data.result.cover,
418
+ Actors: videoInfo.data.result.actors,
419
+ Evaluate: videoInfo.data.result.evaluate,
420
+ Link: videoInfo.data.result.link,
421
+ newEP: videoInfo.data.result.new_ep,
422
+ Title: videoInfo.data.result.title,
423
+ Styles: videoInfo.data.result.styles,
424
+ seasonID: videoInfo.data.result.season_id,
425
+ subtitle: videoInfo.data.result.subtitle,
426
+ UPInfo: videoInfo.data.result.up_info,
427
+ Copyright: videoInfo.data.result.rights.copyright,
428
+ Stat: videoInfo.data.result.stat
429
+ }, bangumiPage)) {
430
+ return true
431
+ }
432
+
433
+ const Episodes = []
434
+ for (const item of videoInfo.data.result.episodes) {
435
+ Episodes.push({
436
+ cover: item.cover,
437
+ bvid: item.bvid,
438
+ link: item.short_link,
439
+ long_title: item.long_title,
440
+ pub_time: item.pub_time,
441
+ badge: item.badge === '' ? '限免' : item.badge,
442
+ badge_info: item.badge_info
443
+ })
444
+ }
445
+ img = await Render(this.e, 'bilibili/bangumi', {
446
+ mainCover: videoInfo.data.result.cover,
447
+ Actors: videoInfo.data.result.actors,
448
+ Evaluate: videoInfo.data.result.evaluate,
449
+ Link: videoInfo.data.result.link,
450
+ newEP: videoInfo.data.result.new_ep,
451
+ Title: videoInfo.data.result.title,
452
+ Styles: videoInfo.data.result.styles,
453
+ seasonID: videoInfo.data.result.season_id,
454
+ subtitle: videoInfo.data.result.subtitle,
455
+ UPInfo: videoInfo.data.result.up_info,
456
+ Copyright: videoInfo.data.result.rights.copyright,
457
+ Stat: videoInfo.data.result.stat,
458
+ Episodes,
459
+ length: videoInfo.data.result.episodes.length
460
+ })
461
+ this.e.reply([...img, segment.text('请在120秒内输入 第?集 选择集数')])
462
+ const context = await karin.ctx(this.e, { reply: true, throwOnTimeout: false })
463
+ if (!context) return true
464
+ const regex = /第([一二三四五六七八九十百千万0-9]+)集/.exec(context.msg)
465
+ let Episode
466
+ if (regex && regex[1]) {
467
+ Episode = regex[1]
468
+ // 检查是否为中文数字,如果是则转换为阿拉伯数字
469
+ if (/^[一二三四五六七八九十百千万]+$/.test(Episode)) {
470
+ Episode = Common.chineseToArabic(Episode).toString()
471
+ }
472
+ this.downloadfilename = videoInfo.data.result.episodes[Number(Episode) - 1].share_copy
473
+ .substring(0, 50)
474
+ .replace(/[\\/:*?"<>|\r\n\s]/g, ' ')
475
+ this.e.reply(`收到请求,第${Episode}集\n${this.downloadfilename}\n正在下载中`)
476
+ } else {
477
+ logger.debug(Episode)
478
+ this.e.reply('匹配内容失败,请重新发送链接再次解析')
479
+ return true
480
+ }
481
+ const bangumidataBASEURL = bilibiliApiUrls.getBangumiStream({
482
+ cid: videoInfo.data.result.episodes[Number(Episode) - 1].cid,
483
+ ep_id: videoInfo.data.result.episodes[Number(Episode) - 1].ep_id.toString()
484
+ })
485
+ const Params = await genParams(bangumidataBASEURL)
486
+ if (!this.islogin) this.e.reply('B站ck未配置或已失效,无法获取视频流,可尝试【#B站登录】以配置新ck')
487
+ const playUrlData = await new Networks({
488
+ url: bangumidataBASEURL + Params,
489
+ headers: this.headers
490
+ }).getData()
491
+ if (videoInfo.data.result.episodes[Number(Episode) - 1].badge === '会员' && !this.isVIP) {
492
+ logger.warn('该CK不是大会员,无法获取视频流')
493
+ return true
494
+ }
495
+ if (Config.bilibili.videoQuality === 0) {
496
+ /** 提取出视频流信息对象,并排除清晰度重复的视频流 */
497
+ const simplify = playUrlData.result.dash.video.filter((item: { id: number }, index: any, self: any[]) => {
498
+ return (
499
+ self.findIndex((t: { id: any }) => {
500
+ return t.id === item.id
501
+ }) === index
502
+ )
503
+ })
504
+ /** 替换原始的视频信息对象 */
505
+ playUrlData.result.dash.video = simplify
506
+ /** 给视频信息对象删除不符合条件的视频流 */
507
+ const correctList = await bilibiliProcessVideos(
508
+ {
509
+ accept_description: playUrlData.result.accept_description,
510
+ bvid: videoInfo.data.result.season_id.toString(),
511
+ qn: Config.bilibili.videoQuality
512
+ },
513
+ simplify,
514
+ playUrlData.result.dash.audio?.[0]?.base_url
515
+ )
516
+ playUrlData.result.dash.video = correctList.videoList
517
+ playUrlData.result.cept_description = correctList.accept_description
518
+ }
519
+ /**
520
+ * 番剧分支原来没有拉弹幕名单 —— 所以「弹幕解析」一部番剧,出来的是**没有弹幕的视频**。
521
+ * 这里和普通视频分支一样,按当前这一集的 cid 拉一份。
522
+ */
523
+ let bangumiDanmakuList: BiliDanmakuElem[] = []
524
+ if (this.forceBurnDanmaku || Config.bilibili.burnDanmaku) {
525
+ const currentEpisode = videoInfo.data.result.episodes[Number(Episode) - 1] as any
526
+ const epDuration = Number(currentEpisode?.duration ?? 0) || 0
527
+ bangumiDanmakuList = await this.fetchVideoDanmakuList(currentEpisode.cid, epDuration)
528
+ logger.debug('[番剧] 第' + Episode + '集弹幕: ' + bangumiDanmakuList.length + ' 条')
529
+ }
530
+ await this.getvideo({
531
+ infoData: videoInfo.data,
532
+ playUrlData,
533
+ danmakuList: bangumiDanmakuList
534
+ })
535
+ break
536
+ }
537
+ case 'dynamic_info': {
538
+ const dynamicInfo = await this.amagi.bilibili.fetcher.fetchDynamicDetail({
539
+ dynamic_id: iddata.dynamic_id
540
+ })
541
+ /**
542
+ * 动态类型取成枚举再用。
543
+ *
544
+ * 生成树的判别联合目前只录到 AV / DRAW / FORWARD 三种形状,其余(WORD / LIVE_RCMD /
545
+ * ARTICLE)落在兜底支上,判别字段声明成 `?: never` —— 拿 `item.type` 直接 switch 时,
546
+ * 枚举里那些没录到的取值会被判成「与判别字段无重叠」,连带把已知支一起收窄成 never。
547
+ * 转成枚举后各支照旧按运行时的 `type` 字符串走,字段读取仍走每层的索引签名。
548
+ */
549
+ const dynamicType = String(dynamicInfo.data.data.item.type) as DynamicType
550
+ const userProfileData = await this.amagi.bilibili.fetcher.fetchUserCard({
551
+ host_mid: dynamicInfo.data.data.item.modules.module_author.mid
552
+ })
553
+
554
+ switch (dynamicType) {
555
+ /** 图文、纯图 */
556
+ case DynamicType.DRAW: {
557
+ const imgArray = []
558
+ const temp: fileInfo[] = []
559
+ let hasGeneratedLivePhoto = false // 标记是否生成了实况图
560
+ const title = dynamicInfo.data.data.item.modules.module_dynamic.major.opus.title || 'bilibili_dynamic'
561
+ for (const [index, img] of dynamicInfo.data.data.item.modules.module_dynamic.major.opus.pics.entries()) {
562
+ if (img.url) {
563
+ // Check if this is a live image with live_url
564
+ if (img.live_url) {
565
+ // Process live image similar to douyin
566
+ const livePhoto = await downloadFile(img.live_url, {
567
+ title: `Bilibili_tmp_V_${Date.now()}_${index}.mp4`,
568
+ headers: baseHeaders
569
+ })
570
+
571
+ if (livePhoto.filepath) {
572
+ const outputPath = Common.tempDri.video + `Bilibili_Live_${Date.now()}_${index}.mp4`
573
+
574
+ // 下载原图用于静态显示
575
+ const staticImg = await downloadFile(img.url, {
576
+ title: `Bilibili_static_${Date.now()}_${index}.jpg`,
577
+ headers: baseHeaders,
578
+ filepath: Common.tempDri.images + `Bilibili_static_${Date.now()}_${index}.jpg`
579
+ })
580
+ if (staticImg.filepath) {
581
+ temp.push({ filepath: staticImg.filepath, totalBytes: 0 })
582
+ }
583
+
584
+ // 根据 livePhotoMode 配置决定处理方式
585
+ const livePhotoMode = Config.app.livePhotoMode ?? 'video_and_livephoto'
586
+ const shouldGenerateVideo = livePhotoMode === 'video_and_livephoto' || livePhotoMode === 'video_only'
587
+ const shouldGenerateLivePhoto = livePhotoMode === 'video_and_livephoto' || livePhotoMode === 'livephoto_only'
588
+
589
+ // Loop the live image 3 times with Live Photo effect
590
+ const loopCount = 3
591
+ if (!staticImg.filepath) {
592
+ await Common.removeFile(livePhoto.filepath, true)
593
+ continue
594
+ }
595
+
596
+ // 生成视频
597
+ if (shouldGenerateVideo) {
598
+ const result = await loopVideoWithTransition({
599
+ inputPath: livePhoto.filepath,
600
+ outputPath,
601
+ loopCount,
602
+ staticImagePath: staticImg.filepath,
603
+ transitionEnabled: loopCount > 1
604
+ })
605
+ const success = result.success
606
+
607
+ if (success) {
608
+ const filePath = Common.tempDri.video + `tmp_${Date.now()}.mp4`
609
+ fs.renameSync(outputPath, filePath)
610
+ logger.mark(`视频文件重命名完成: ${outputPath.split('/').pop()} -> ${filePath.split('/').pop()}`)
611
+ temp.push({ filepath: filePath, totalBytes: 0 })
612
+ const videoPath =
613
+ Config.app.videoSendMode === 'base64'
614
+ ? `base64://${fs.readFileSync(filePath).toString('base64')}`
615
+ : `file://${filePath}`
616
+ imgArray.push(segment.video(videoPath))
617
+ }
618
+ }
619
+
620
+ // 生成实况图
621
+ if (shouldGenerateLivePhoto) {
622
+ let hasPushedMotionPhotoCover = false
623
+ if (staticImg.filepath) {
624
+ const motionPhotoCoverPath =
625
+ Common.tempDri.images + `MVIMG_${format(new Date(), 'yyyyMMdd_HHmmss_SSS')}_${index}.jpg`
626
+ const motionPhotoCreated = await buildGoogleMotionPhoto({
627
+ imagePath: staticImg.filepath,
628
+ videoPath: livePhoto.filepath,
629
+ outputPath: motionPhotoCoverPath
630
+ })
631
+ if (motionPhotoCreated) {
632
+ temp.push({ filepath: motionPhotoCoverPath, totalBytes: 0 })
633
+ const motionPhotoCover = processLocalImageFile(motionPhotoCoverPath)
634
+ imgArray.push(segment.image(motionPhotoCover))
635
+ hasPushedMotionPhotoCover = true
636
+ }
637
+ }
638
+ if (!hasPushedMotionPhotoCover) {
639
+ const imageUrl = await processImageUrl(img.url, title, index)
640
+ imgArray.push(segment.image(imageUrl))
641
+ } else {
642
+ hasGeneratedLivePhoto = true // 标记已生成实况图
643
+ }
644
+ }
645
+
646
+ logger.mark('正在尝试删除缓存文件')
647
+ await Common.removeFile(livePhoto.filepath, true)
648
+ }
649
+ } else {
650
+ // Regular static image
651
+ const imageUrl = await processImageUrl(img.url, title, index)
652
+ imgArray.push(segment.image(imageUrl))
653
+ }
654
+ }
655
+ }
656
+
657
+ // 如果生成了实况图,添加提示文字
658
+ if (hasGeneratedLivePhoto) {
659
+ const tipImg = await Render(this.e, 'other/live-photo-tip', {
660
+ title: '实况照片已生成',
661
+ description: '保存原图到相册即可识别为实况图'
662
+ })
663
+ imgArray.push(...tipImg)
664
+ }
665
+
666
+ if (imgArray.length === 1) this.e.reply(imgArray[0])
667
+ if (imgArray.length > 1) {
668
+ const forwardMsg = common.makeForward(
669
+ imgArray,
670
+ Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId,
671
+ Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name
672
+ )
673
+ try {
674
+ await this.e.bot.sendForwardMsg(this.e.contact, forwardMsg, {
675
+ source: '图片合集',
676
+ summary: `查看${imgArray.length}张图片消息`,
677
+ prompt: 'B站图文动态解析结果',
678
+ news: [{ text: '点击查看解析结果' }]
679
+ })
680
+ } finally {
681
+ for (const item of temp) {
682
+ await Common.removeFile(item.filepath, true)
683
+ }
684
+ }
685
+ }
686
+
687
+ const md = dynamicInfo.data.data.item.modules.module_dynamic
688
+ if (md.topic) {
689
+ const { name } = md.topic
690
+ const summary = md.major.opus.summary
691
+ summary.rich_text_nodes ??= []
692
+ summary.rich_text_nodes.unshift({ orig_text: name, jump_url: '', text: name, type: 'topic' })
693
+ summary.text = summary.text ? `${name}\n${summary.text}` : name
694
+ }
695
+ this.e.reply(
696
+ await Render(this.e, 'bilibili/dynamic/DYNAMIC_TYPE_DRAW', {
697
+ // 生成类型在判别联合里把 `pics` 记成 `any`(各支索引签名),`Object.values` 于是推出
698
+ // `unknown[]` —— 谓词里先把元素当成「可能有 url 的对象」再判,形状仍然照旧收窄
699
+ image_url: Object.values(dynamicInfo.data.data.item.modules.module_dynamic.major.opus.pics)
700
+ .filter((item): item is { url: string } => typeof (item as { url?: unknown })?.url === 'string')
701
+ .map((item) => ({ image_src: item.url })),
702
+ // TIP: 2025/08/20, 动态卡片数据中,图文动态的描述文本在 major.opus.summary 中
703
+ title: dynamicInfo.data.data.item.modules.module_dynamic.major.opus.title ?? undefined,
704
+ text: dynamicInfo.data.data.item.modules.module_dynamic.major
705
+ ? buildBilibiliDynamicRichText(
706
+ dynamicInfo.data.data.item.modules.module_dynamic.major.opus?.summary?.text ?? '',
707
+ dynamicInfo.data.data.item.modules.module_dynamic.major.opus?.summary?.rich_text_nodes ?? []
708
+ )
709
+ : null,
710
+ dianzan: Count(dynamicInfo.data.data.item.modules.module_stat.like.count),
711
+ pinglun: Count(dynamicInfo.data.data.item.modules.module_stat.comment.count),
712
+ share: Count(dynamicInfo.data.data.item.modules.module_stat.forward.count),
713
+ create_time: TimeFormatter.toRelative(dynamicInfo.data.data.item.modules.module_author.pub_ts),
714
+ avatar_url: dynamicInfo.data.data.item.modules.module_author.face,
715
+ frame: dynamicInfo.data.data.item.modules.module_author.pendant.image,
716
+ share_url: 'https://t.bilibili.com/' + dynamicInfo.data.data.item.id_str,
717
+ usernameMeta: getUsernameMetadata(userProfileData.data.data.card),
718
+ fans: Count(userProfileData.data.data.follower),
719
+ user_shortid: dynamicInfo.data.data.item.modules.module_author.mid,
720
+ total_favorited: Count(userProfileData.data.data.like_num),
721
+ following_count: Count(userProfileData.data.data.card.attention),
722
+ decoration_card: generateDecorationCard(dynamicInfo.data.data.item.modules.module_author.decoration_card),
723
+ render_time: TimeFormatter.now(),
724
+ dynamicTYPE: '图文动态解析',
725
+ imageLayout: Config.bilibili.imageLayout,
726
+ additional: parseAdditionalCard(dynamicInfo.data.data.item.modules.module_dynamic.additional),
727
+ dynamic_id: dynamicInfo.data.data.item.id_str
728
+ })
729
+ )
730
+ break
731
+ }
732
+ /** 纯文 */
733
+ case DynamicType.WORD: {
734
+ // 处理话题
735
+ const md = dynamicInfo.data.data.item.modules.module_dynamic
736
+ if (md.topic) {
737
+ const { name } = md.topic
738
+ const summary = md.major.opus.summary
739
+ summary.rich_text_nodes ??= []
740
+ summary.rich_text_nodes.unshift({ orig_text: name, jump_url: '', text: name, type: 'topic' })
741
+ summary.text = summary.text ? `${name}\n\n${summary.text}` : name
742
+ }
743
+
744
+ const text = buildBilibiliDynamicRichText(
745
+ dynamicInfo.data.data.item.modules.module_dynamic.major.opus?.summary?.text ?? '',
746
+ dynamicInfo.data.data.item.modules.module_dynamic.major.opus?.summary?.rich_text_nodes ?? []
747
+ )
748
+
749
+ this.e.reply(
750
+ await Render(this.e, 'bilibili/dynamic/DYNAMIC_TYPE_WORD', {
751
+ text,
752
+ dianzan: Count(dynamicInfo.data.data.item.modules.module_stat.like.count),
753
+ pinglun: Count(dynamicInfo.data.data.item.modules.module_stat.comment.count),
754
+ share: Count(dynamicInfo.data.data.item.modules.module_stat.forward.count),
755
+ create_time: TimeFormatter.toRelative(dynamicInfo.data.data.item.modules.module_author.pub_ts),
756
+ avatar_url: dynamicInfo.data.data.item.modules.module_author.face,
757
+ frame: dynamicInfo.data.data.item.modules.module_author.pendant.image,
758
+ share_url: 'https://t.bilibili.com/' + dynamicInfo.data.data.item.id_str,
759
+ usernameMeta: getUsernameMetadata(userProfileData.data.data.card),
760
+ fans: Count(userProfileData.data.data.follower),
761
+ user_shortid: dynamicInfo.data.data.item.modules.module_author.mid,
762
+ total_favorited: Count(userProfileData.data.data.like_num),
763
+ following_count: Count(userProfileData.data.data.card.attention),
764
+ decoration_card: generateDecorationCard(dynamicInfo.data.data.item.modules.module_author.decoration_card),
765
+ render_time: TimeFormatter.now(),
766
+ dynamicTYPE: '纯文动态解析',
767
+ additional: parseAdditionalCard(dynamicInfo.data.data.item.modules.module_dynamic.additional),
768
+ dynamic_id: dynamicInfo.data.data.item.id_str
769
+ })
770
+ )
771
+ break
772
+ }
773
+ /** 转发动态 */
774
+ case DynamicType.FORWARD: {
775
+ // 处理话题
776
+ const md = dynamicInfo.data.data.item.modules.module_dynamic
777
+ if (md.topic) {
778
+ const { name } = md.topic
779
+ const desc = md.desc
780
+ desc.rich_text_nodes ??= []
781
+ desc.rich_text_nodes.unshift({ orig_text: name, jump_url: '', text: name, type: 'topic' })
782
+ desc.text = desc.text ? `${name}\n\n${desc.text}` : name
783
+ }
784
+
785
+ const text = buildBilibiliDynamicRichText(
786
+ dynamicInfo.data.data.item.modules.module_dynamic.desc.text,
787
+ dynamicInfo.data.data.item.modules.module_dynamic.desc.rich_text_nodes
788
+ )
789
+
790
+ // 富文本节点:查看图片
791
+ const imgList = []
792
+ for (const richTxtItem of dynamicInfo.data.data.item.modules.module_dynamic.desc.rich_text_nodes) {
793
+ if (richTxtItem.type === 'RICH_TEXT_NODE_TYPE_VIEW_PICTURE') {
794
+ for (const pic of richTxtItem.pics) {
795
+ imgList.push(pic.src)
796
+ }
797
+ }
798
+ }
799
+ let original_content: BilibiliForwardOriginalContentProps['original_content'] = {}
800
+ switch (dynamicInfo.data.data.item.orig.type) {
801
+ // 转发视频动态
802
+ case DynamicType.AV: {
803
+ const desc = dynamicInfo.data.data.item.orig.modules.module_dynamic?.desc || {
804
+ text: '',
805
+ rich_text_nodes: []
806
+ }
807
+
808
+ original_content = {
809
+ DYNAMIC_TYPE_AV: {
810
+ usernameMeta: getUsernameMetadata(dynamicInfo.data.data.item.orig.modules.module_author),
811
+ avatar_url: dynamicInfo.data.data.item.orig.modules.module_author.face,
812
+ duration_text: dynamicInfo.data.data.item.orig.modules.module_dynamic.major.archive.duration_text,
813
+ text: buildBilibiliDynamicRichText(desc.text, desc.rich_text_nodes),
814
+ title: buildBilibiliDynamicRichText(dynamicInfo.data.data.item.orig.modules.module_dynamic.major.archive.title, []),
815
+ danmaku: dynamicInfo.data.data.item.orig.modules.module_dynamic.major.archive.stat.danmaku,
816
+ play: dynamicInfo.data.data.item.orig.modules.module_dynamic.major.archive.stat.play,
817
+ cover: dynamicInfo.data.data.item.orig.modules.module_dynamic.major.archive.cover,
818
+ create_time: TimeFormatter.toDateTime(dynamicInfo.data.data.item.orig.modules.module_author.pub_ts),
819
+ decoration_card: generateDecorationCard(dynamicInfo.data.data.item.orig.modules.module_author.decoration_card),
820
+ frame: dynamicInfo.data.data.item.orig.modules.module_author.pendant.image
821
+ }
822
+ }
823
+ break
824
+ }
825
+ // 转发图文动态
826
+ case DynamicType.DRAW: {
827
+ // 处理话题
828
+ const origMd = dynamicInfo.data.data.item.orig.modules.module_dynamic
829
+ if (origMd.topic) {
830
+ const { name } = origMd.topic
831
+ const summary = origMd.major?.opus?.summary
832
+ if (summary) {
833
+ summary.rich_text_nodes ??= []
834
+ summary.rich_text_nodes.unshift({
835
+ orig_text: name,
836
+ jump_url: '',
837
+ text: name,
838
+ type: 'topic',
839
+ rid: '',
840
+ style: { '1114514': '1919810' }
841
+ })
842
+ summary.text = summary.text ? `${name}\n${summary.text}` : name
843
+ }
844
+ }
845
+
846
+ original_content = {
847
+ DYNAMIC_TYPE_DRAW: {
848
+ title: dynamicInfo.data.data.item.orig.modules.module_dynamic.major?.opus?.title ?? undefined,
849
+ usernameMeta: getUsernameMetadata(dynamicInfo.data.data.item.orig.modules.module_author),
850
+ create_time: TimeFormatter.toDateTime(dynamicInfo.data.data.item.orig.modules.module_author.pub_ts),
851
+ avatar_url: dynamicInfo.data.data.item.orig.modules.module_author.face,
852
+ text: buildBilibiliDynamicRichText(
853
+ dynamicInfo.data.data.item.orig.modules.module_dynamic.major.opus.summary.text,
854
+ dynamicInfo.data.data.item.orig.modules.module_dynamic.major.opus.summary.rich_text_nodes
855
+ ),
856
+ // 同上:生成类型的 `pics` 是 `any`,`Object.values` 出 `unknown[]`
857
+ image_url: Object.values(dynamicInfo.data.data.item.orig.modules.module_dynamic.major.opus.pics)
858
+ .filter((item): item is { url: string } => typeof (item as { url?: unknown })?.url === 'string')
859
+ .map((item) => ({ image_src: item.url })),
860
+ decoration_card: generateDecorationCard(dynamicInfo.data.data.item.orig.modules.module_author.decoration_card),
861
+ frame: dynamicInfo.data.data.item.orig.modules.module_author.pendant.image
862
+ }
863
+ }
864
+ break
865
+ }
866
+ // 转发纯文动态
867
+ case DynamicType.WORD: {
868
+ // 处理话题
869
+ const origMd = dynamicInfo.data.data.item.orig.modules.module_dynamic
870
+ if (origMd.topic) {
871
+ const { name } = origMd.topic
872
+ const summary = origMd.major?.opus?.summary
873
+ if (summary) {
874
+ summary.rich_text_nodes ??= []
875
+ summary.rich_text_nodes.unshift({ orig_text: name, jump_url: '', text: name, type: 'topic' })
876
+ summary.text = summary.text ? `${name}\n${summary.text}` : name
877
+ }
878
+ }
879
+
880
+ original_content = {
881
+ DYNAMIC_TYPE_WORD: {
882
+ usernameMeta: getUsernameMetadata(dynamicInfo.data.data.item.orig.modules.module_author),
883
+ create_time: TimeFormatter.toDateTime(dynamicInfo.data.data.item.orig.modules.module_author.pub_ts),
884
+ avatar_url: dynamicInfo.data.data.item.orig.modules.module_author.face,
885
+ text: buildBilibiliDynamicRichText(
886
+ dynamicInfo.data.data.item.orig.modules.module_dynamic.major.opus.summary.text,
887
+ dynamicInfo.data.data.item.orig.modules.module_dynamic.major.opus.summary.rich_text_nodes
888
+ ),
889
+ decoration_card: generateDecorationCard(dynamicInfo.data.data.item.orig.modules.module_author.decoration_card),
890
+ frame: dynamicInfo.data.data.item.orig.modules.module_author.pendant.image,
891
+ additional: parseAdditionalCard(dynamicInfo.data.data.item.orig.modules.module_dynamic.additional)
892
+ }
893
+ }
894
+ break
895
+ }
896
+ // 转发直播开始动态
897
+ case DynamicType.LIVE_RCMD: {
898
+ const liveData = JSON.parse(dynamicInfo.data.data.item.orig.modules.module_dynamic.major.live_rcmd.content)
899
+ original_content = {
900
+ DYNAMIC_TYPE_LIVE_RCMD: {
901
+ usernameMeta: getUsernameMetadata(dynamicInfo.data.data.item.orig.modules.module_author),
902
+ create_time: TimeFormatter.toDateTime(dynamicInfo.data.data.item.orig.modules.module_author.pub_ts),
903
+ avatar_url: dynamicInfo.data.data.item.orig.modules.module_author.face,
904
+ decoration_card: generateDecorationCard(dynamicInfo.data.data.item.orig.modules.module_author.decoration_card),
905
+ frame: dynamicInfo.data.data.item.orig.modules.module_author.pendant.image,
906
+ cover: liveData.live_play_info.cover,
907
+ text_large: liveData.live_play_info.watched_show.text_large,
908
+ area_name: liveData.live_play_info.area_name,
909
+ title: buildBilibiliDynamicRichText(liveData.live_play_info.title, []),
910
+ online: liveData.live_play_info.online
911
+ }
912
+ }
913
+ break
914
+ }
915
+ // 其他类型动态未适配
916
+ default: {
917
+ logger.warn(
918
+ `UP主:${userProfileData.data.data.card.name}的${logger.green('转发动态')}转发的原动态类型为「${logger.yellow(dynamicInfo.data.item.orig.type)}」暂未支持解析`
919
+ )
920
+ break
921
+ }
922
+ }
923
+ this.e.reply(
924
+ await Render(this.e, 'bilibili/dynamic/DYNAMIC_TYPE_FORWARD', {
925
+ text,
926
+ imgList: imgList.length > 0 ? imgList : null,
927
+ dianzan: Count(dynamicInfo.data.data.item.modules.module_stat.like.count),
928
+ pinglun: Count(dynamicInfo.data.data.item.modules.module_stat.comment.count),
929
+ share: Count(dynamicInfo.data.data.item.modules.module_stat.forward.count),
930
+ create_time: TimeFormatter.toRelative(dynamicInfo.data.data.item.modules.module_author.pub_ts),
931
+ avatar_url: dynamicInfo.data.data.item.modules.module_author.face,
932
+ frame: dynamicInfo.data.data.item.modules.module_author.pendant.image,
933
+ share_url: 'https://t.bilibili.com/' + dynamicInfo.data.data.item.id_str,
934
+ usernameMeta: getUsernameMetadata(userProfileData.data.data.card),
935
+ fans: Count(userProfileData.data.data.follower),
936
+ user_shortid: dynamicInfo.data.data.item.modules.module_author.mid,
937
+ total_favorited: Count(userProfileData.data.data.like_num),
938
+ following_count: Count(userProfileData.data.data.card.attention),
939
+ dynamicTYPE: '转发动态解析',
940
+ decoration_card: generateDecorationCard(dynamicInfo.data.data.item.modules.module_author.decoration_card),
941
+ render_time: TimeFormatter.now(),
942
+ original_content,
943
+ dynamic_id: dynamicInfo.data.data.item.id_str
944
+ })
945
+ )
946
+ break
947
+ }
948
+ /** 视频动态 */
949
+ case DynamicType.AV: {
950
+ if (dynamicInfo.data.data.item.modules.module_dynamic.major.type === 'MAJOR_TYPE_ARCHIVE') {
951
+ const bvid = dynamicInfo.data.data.item.modules.module_dynamic.major.archive.bvid
952
+ const INFODATA = await bilibiliFetcher.fetchVideoInfo({ bvid })
953
+
954
+ // 处理共创者信息
955
+ let staff = undefined
956
+ if (INFODATA.data.data.staff && Array.isArray(INFODATA.data.data.staff)) {
957
+ const currentMid = dynamicInfo.data.data.item.modules.module_author.mid
958
+ // 提取共创者信息
959
+ staff = INFODATA.data.data.staff.map((member: any) => ({
960
+ mid: member.mid,
961
+ title: member.title,
962
+ name: member.name,
963
+ face: member.face,
964
+ follower: member.follower
965
+ }))
966
+
967
+ // 如果当前动态发布者是共创者之一,将其排到最前面
968
+ const currentUserIndex = staff.findIndex((member: any) => member.mid === currentMid)
969
+ if (currentUserIndex > 0) {
970
+ const currentUser = staff.splice(currentUserIndex, 1)[0]
971
+ staff.unshift(currentUser)
972
+ }
973
+ }
974
+
975
+ // 处理话题
976
+ const md = dynamicInfo.data.data.item.modules.module_dynamic
977
+ if (md.topic) {
978
+ const { name } = md.topic
979
+ md.desc ??= { rich_text_nodes: [], text: '' }
980
+ md.desc.rich_text_nodes.unshift({ orig_text: name, jump_url: '', text: name, type: 'topic' })
981
+ md.desc.text = md.desc.text ? `${name}\n\n${md.desc.text}` : name
982
+ }
983
+
984
+ const dynamicText = buildBilibiliDynamicRichText(
985
+ dynamicInfo.data.data.item.modules.module_dynamic.desc?.text ?? '',
986
+ dynamicInfo.data.data.item.modules.module_dynamic.desc?.rich_text_nodes ?? []
987
+ )
988
+
989
+ img = await Render(this.e, 'bilibili/dynamic/DYNAMIC_TYPE_AV', {
990
+ image_url: INFODATA.data.data.pic,
991
+ text: buildBilibiliDynamicRichText(INFODATA.data.data.title, []),
992
+ desc: INFODATA.data.data.desc_v2?.length
993
+ ? buildBilibiliVideoDescRichText(INFODATA.data.data.desc_v2)
994
+ : buildBilibiliDynamicRichText(INFODATA.data.data.desc || '', []),
995
+ dynamic_text: dynamicText,
996
+ dianzan: Count(INFODATA.data.data.stat.like),
997
+ pinglun: Count(INFODATA.data.data.stat.reply),
998
+ share: Count(INFODATA.data.data.stat.share),
999
+ view: Count(INFODATA.data.data.stat.view),
1000
+ coin: Count(INFODATA.data.data.stat.coin),
1001
+ duration_text: dynamicInfo.data.data.item.modules.module_dynamic.major.archive.duration_text,
1002
+ page_length: INFODATA.data.data.pages.length,
1003
+ create_time: TimeFormatter.toRelative(dynamicInfo.data.data.item.modules.module_author.pub_ts),
1004
+ avatar_url: dynamicInfo.data.data.item.modules.module_author.face,
1005
+ frame: dynamicInfo.data.data.item.modules.module_author.pendant.image,
1006
+ share_url: 'https://www.bilibili.com/video/' + bvid,
1007
+ usernameMeta: getUsernameMetadata(userProfileData.data.data.card),
1008
+ fans: Count(userProfileData.data.data.follower),
1009
+ user_shortid: userProfileData.data.data.card.mid,
1010
+ total_favorited: Count(userProfileData.data.data.like_num),
1011
+ following_count: Count(userProfileData.data.data.card.attention),
1012
+ decoration_card: generateDecorationCard(dynamicInfo.data.data.item.modules.module_author.decoration_card),
1013
+ render_time: TimeFormatter.now(),
1014
+ dynamicTYPE: '视频动态解析',
1015
+ dynamic_id: dynamicInfo.data.data.item.id_str,
1016
+ staff
1017
+ })
1018
+ this.e.reply(img)
1019
+ }
1020
+ break
1021
+ }
1022
+ /** 直播动态 */
1023
+ case DynamicType.LIVE_RCMD: {
1024
+ const userINFO = await bilibiliFetcher.fetchUserCard({
1025
+ host_mid: dynamicInfo.data.data.item.modules.module_author.mid
1026
+ })
1027
+ const liveInfo = JSON.parse(dynamicInfo.data.data.item.modules.module_dynamic.major.live_rcmd.content)
1028
+ img = await Render(this.e, 'bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD', {
1029
+ image_url: liveInfo.live_play_info.cover,
1030
+ text: buildBilibiliDynamicRichText(liveInfo.live_play_info.title, []),
1031
+ liveinf: br(`${liveInfo.live_play_info.area_name} | 房间号: ${liveInfo.live_play_info.room_id}`),
1032
+ usernameMeta: getUsernameMetadata(userINFO.data.data.card),
1033
+ avatar_url: userINFO.data.data.card.face,
1034
+ frame: dynamicInfo.data.data.item.modules.module_author.pendant.image,
1035
+ fans: Count(userINFO.data.data.follower),
1036
+ create_time: TimeFormatter.toDateTime(dynamicInfo.data.data.item.modules.module_author.pub_ts),
1037
+ now_time: TimeFormatter.now(),
1038
+ share_url: 'https://live.bilibili.com/' + liveInfo.live_play_info.room_id,
1039
+ dynamicTYPE: '直播动态解析'
1040
+ })
1041
+ this.e.reply(img)
1042
+ break
1043
+ }
1044
+ /** 文章/专栏动态 */
1045
+ case DynamicType.ARTICLE: {
1046
+ const articleInfoBase = await this.amagi.bilibili.fetcher.fetchArticleInfo({
1047
+ id: dynamicInfo.data.data.item.basic.rid_str
1048
+ })
1049
+ const articleInfo = await this.amagi.bilibili.fetcher.fetchArticleContent({
1050
+ id: dynamicInfo.data.data.item.basic.rid_str
1051
+ })
1052
+
1053
+ // 提取专栏基本信息
1054
+ const articleData = articleInfoBase.data.data
1055
+ // 提取专栏正文内容(生成类型里 `data` 可空:样本里就有一条只有 code/message 的响应)
1056
+ const articleContent = articleInfo.data.data
1057
+ if (!articleContent) {
1058
+ this.e.reply('获取专栏正文失败,该专栏可能已被删除或设为私密')
1059
+ break
1060
+ }
1061
+
1062
+ // TODO: 还未完全支持B站的富文本格式,后续需要根据实际情况补充更多类型的节点解析
1063
+ // 构建富文本文档
1064
+ const body = buildBilibiliArticleRichText(articleContent.opus, articleContent.content, Common.useDarkTheme())
1065
+
1066
+ const title = articleData.title || 'bilibili_article'
1067
+ const shareUrl = articleContent.dyn_id_str
1068
+ ? `https://www.bilibili.com/opus/${articleContent.dyn_id_str}`
1069
+ : `https://www.bilibili.com/read/cv${articleContent.id}`
1070
+
1071
+ const messageElements = await buildBilibiliRichTextForwardMessage(body, {
1072
+ title: articleData.title,
1073
+ summary: articleData.summary,
1074
+ shareUrl,
1075
+ imageResolver: (src, index) => processImageUrl(src, title, index)
1076
+ })
1077
+ if (messageElements.length > 0) {
1078
+ const forwardMsg = common.makeForward(
1079
+ messageElements,
1080
+ Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId,
1081
+ Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name
1082
+ )
1083
+ await this.e.bot.sendForwardMsg(this.e.contact, forwardMsg, {
1084
+ source: '专栏内容',
1085
+ summary: `查看${messageElements.length}条专栏内容`,
1086
+ prompt: 'B站专栏动态解析结果',
1087
+ news: [{ text: '点击查看解析结果' }]
1088
+ })
1089
+ }
1090
+
1091
+ // 构建渲染数据
1092
+ const img = await Render(this.e, 'bilibili/dynamic/DYNAMIC_TYPE_ARTICLE', {
1093
+ // 用户信息
1094
+ usernameMeta: getUsernameMetadata(userProfileData.data.data.card),
1095
+ avatar_url: userProfileData.data.data.card.face,
1096
+ frame: dynamicInfo.data.data.item.modules.module_author.pendant.image,
1097
+ create_time: TimeFormatter.toDateTime(dynamicInfo.data.data.item.modules.module_author.pub_ts),
1098
+
1099
+ // 专栏内容信息
1100
+ title: articleData.title,
1101
+ summary: articleData.summary,
1102
+ banner_url: articleData.banner_url || (articleData.image_urls && articleData.image_urls[0]) || '',
1103
+ categories: articleData.categories || [],
1104
+ words: articleData.words || 0,
1105
+
1106
+ // 专栏正文内容(richtext 格式)
1107
+ body,
1108
+ // 统计信息
1109
+ stats: articleData.stats,
1110
+ render_time: TimeFormatter.now(),
1111
+ // 分享链接
1112
+ share_url: shareUrl,
1113
+ dynamicTYPE: '专栏动态解析',
1114
+
1115
+ // 用户统计信息
1116
+ user_shortid: userProfileData.data.data.card.mid,
1117
+ total_favorited: Count(userProfileData.data.data.like_num),
1118
+ following_count: Count(userProfileData.data.data.card.friend),
1119
+ fans: Count(userProfileData.data.data.card.fans)
1120
+ })
1121
+ this.e.reply(img)
1122
+ break
1123
+ }
1124
+ default: {
1125
+ const unknownItem = dynamicInfo.data.data.item as any
1126
+ this.e.reply(
1127
+ `该动态类型「${unknownItem.type}」暂未支持解析,可通过 https://github.com/ikenxuan/karin-plugin-kkk/issues/new/choose 提交反馈`
1128
+ )
1129
+ break
1130
+ }
1131
+ }
1132
+
1133
+ // 统一处理评论(直播动态除外)
1134
+ if (Config.bilibili.sendContent.some((content) => content === 'comment') && dynamicType !== DynamicType.LIVE_RCMD) {
1135
+ const commentsData = await softFetch(
1136
+ () =>
1137
+ this.amagi.bilibili.fetcher.fetchComments(
1138
+ {
1139
+ type: mapping_table(dynamicType),
1140
+ oid: oid(dynamicType, dynamicInfo.data),
1141
+ number: Config.bilibili.numcomment
1142
+ },
1143
+ bilibiliAnonymousRequestConfig
1144
+ ),
1145
+ [SOFT_ERROR_CODES.BILIBILI_COMMENTS_DISABLED]
1146
+ )
1147
+ if (isSoftFailure(commentsData, SOFT_ERROR_CODES.BILIBILI_COMMENTS_DISABLED)) {
1148
+ this.e.reply('UP主已关闭评论区,无法获取评论')
1149
+ } else {
1150
+ const { comments: commentsdata, image_urls } = bilibiliComments(
1151
+ commentsData.data,
1152
+ dynamicInfo.data.data.item.modules.module_author.mid.toString()
1153
+ )
1154
+
1155
+ if (commentsdata && commentsdata.length > 0) {
1156
+ // 收集评论区图片
1157
+ if (Config.bilibili.commentImageCollection && image_urls.length > 0) {
1158
+ const messageElements = []
1159
+ // 获取动态标题用于图片命名
1160
+ let title = 'bilibili_dynamic'
1161
+ if (dynamicType === DynamicType.DRAW) {
1162
+ title = dynamicInfo.data.data.item.modules.module_dynamic.major.opus.title || 'bilibili_dynamic'
1163
+ } else if (dynamicType === DynamicType.AV) {
1164
+ title = dynamicInfo.data.data.item.modules.module_dynamic.major.archive.title || 'bilibili_dynamic'
1165
+ }
1166
+
1167
+ for (const [index, v] of image_urls.entries()) {
1168
+ const imageUrl = await processImageUrl(v, title, index)
1169
+ messageElements.push(segment.image(imageUrl))
1170
+ }
1171
+ /**
1172
+ * 评论图片收集:**合并成一条 markdown**(QQ 官方 bot 上合并转发经常发不出去)。
1173
+ * md 里连续图片紧贴渲染,一条消息装完整套图;失败再退回转发。
1174
+ */
1175
+ const mdMessage = await buildMarkdownImageMessage(
1176
+ messageElements.map((item: any) => String(item?.attrs?.src ?? '')).filter(Boolean)
1177
+ )
1178
+ if (mdMessage) {
1179
+ await this.e.reply(mdMessage)
1180
+ } else {
1181
+ const res = common.makeForward(
1182
+ messageElements,
1183
+ Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId,
1184
+ Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name
1185
+ )
1186
+ await this.e.bot.sendForwardMsg(this.e.contact, res, {
1187
+ source: '评论图片收集',
1188
+ summary: `查看${messageElements.length}张图片`,
1189
+ prompt: 'B站评论解析结果',
1190
+ news: [{ text: '点击查看解析结果' }]
1191
+ })
1192
+ }
1193
+ }
1194
+
1195
+ // 渲染评论图
1196
+ const img = await Render(this.e, 'bilibili/comment', {
1197
+ Type: '动态',
1198
+ CommentsData: commentsdata,
1199
+ CommentLength: String(commentsdata.length),
1200
+ share_url:
1201
+ dynamicType === DynamicType.AV
1202
+ ? `https://www.bilibili.com/video/${dynamicInfo.data.data.item.modules.module_dynamic.major.archive.bvid}`
1203
+ : `https://t.bilibili.com/${dynamicInfo.data.data.item.id_str}`,
1204
+ ImageLength: dynamicInfo.data.data.item.modules?.module_dynamic?.major?.draw?.items?.length ?? 0,
1205
+ shareurl: '动态分享链接',
1206
+ Resolution: null
1207
+ })
1208
+ this.e.reply(img)
1209
+ } else {
1210
+ this.e.reply('这条动态暂时还没有评论~')
1211
+ }
1212
+ }
1213
+ }
1214
+
1215
+ break
1216
+ }
1217
+ case 'live_room_detail': {
1218
+ const liveInfo = await this.amagi.bilibili.fetcher.fetchLiveRoomInfo({
1219
+ room_id: iddata.room_id
1220
+ })
1221
+ const roomInitInfo = await this.amagi.bilibili.fetcher.fetchLiveRoomInitInfo({
1222
+ room_id: iddata.room_id
1223
+ })
1224
+ const userProfileData = await this.amagi.bilibili.fetcher.fetchUserCard({
1225
+ host_mid: roomInitInfo.data.data.uid
1226
+ })
1227
+
1228
+ if (roomInitInfo.data.data.live_status === 0) {
1229
+ this.e.reply(`「${userProfileData.data.data.card.name}」\n未开播,正在休息中~`)
1230
+ return true
1231
+ }
1232
+ const img = await Render(this.e, 'bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD', {
1233
+ image_url: liveInfo.data.data.user_cover,
1234
+ text: buildBilibiliDynamicRichText(liveInfo.data.data.title, []),
1235
+ liveinf: br(`${liveInfo.data.data.area_name} | 房间号: ${liveInfo.data.data.room_id}`),
1236
+ usernameMeta: getUsernameMetadata(userProfileData.data.data.card),
1237
+ avatar_url: userProfileData.data.data.card.face,
1238
+ frame: userProfileData.data.data.card.pendant.image,
1239
+ fans: Count(userProfileData.data.data.card.fans),
1240
+ create_time: liveInfo.data.data.live_time === '-62170012800' ? '获取失败' : liveInfo.data.data.live_time,
1241
+ now_time: TimeFormatter.now(),
1242
+ share_url: 'https://live.bilibili.com/' + liveInfo.data.data.room_id,
1243
+ dynamicTYPE: '直播动态解析'
1244
+ })
1245
+ this.e.reply(img)
1246
+ break
1247
+ }
1248
+ default:
1249
+ break
1250
+ }
1251
+ }
1252
+
1253
+ /**
1254
+ * 获取视频弹幕列表(按每 6 分钟一段并行拉取所有分段)
1255
+ * @param cid 视频分P的 cid
1256
+ * @param duration 视频时长(秒)
1257
+ * @returns 合并后的弹幕列表
1258
+ */
1259
+ async fetchVideoDanmakuList(cid: number, duration: number): Promise<BiliDanmakuElem[]> {
1260
+ try {
1261
+ const segmentCount = Math.ceil(duration / 360) // 360秒 = 6分钟
1262
+ logger.debug(`视频时长: ${duration}秒, 需要获取 ${segmentCount} 个弹幕分段`)
1263
+ const danmakuPromises = Array.from({ length: segmentCount }, (_, i) =>
1264
+ this.amagi.bilibili.fetcher
1265
+ .fetchVideoDanmaku({ cid, segment_index: i + 1 })
1266
+ .then((res) => res.data?.data?.elems || [])
1267
+ .catch(() => [] as BiliDanmakuElem[])
1268
+ )
1269
+ const danmakuSegments = await Promise.all(danmakuPromises)
1270
+ const danmakuList = danmakuSegments.flat()
1271
+ logger.debug(`获取到 ${danmakuList.length} 条弹幕(${segmentCount} 个分段)`)
1272
+ return danmakuList
1273
+ } catch (err) {
1274
+ logger.warn('获取弹幕失败', err)
1275
+ return []
1276
+ }
1277
+ }
1278
+
1279
+ async getvideo({
1280
+ infoData,
1281
+ playUrlData,
1282
+ danmakuList = []
1283
+ }: {
1284
+ infoData?: BilibiliBangumiInfoResponse | BilibiliVideoInfoResponse
1285
+ playUrlData: BilibiliVideoStreamResponse | BiliBiliVideoPlayurlNoLogin | BilibiliBangumiStreamResponse
1286
+ danmakuList?: BiliDanmakuElem[]
1287
+ }) {
1288
+ /** 获取视频 => FFmpeg合成 */
1289
+ logger.debug('是否登录:', this.islogin)
1290
+ switch (this.islogin) {
1291
+ case true: {
1292
+ logger.debug(
1293
+ '视频 URL:',
1294
+ this.Type === 'one_video' ? playUrlData.data?.dash?.video[0].base_url : playUrlData.result.dash.video[0].base_url
1295
+ )
1296
+
1297
+ // B站 CDN 需要正确的 Referer
1298
+ const downloadHeaders = {
1299
+ ...this.headers,
1300
+ Referer: 'https://www.bilibili.com'
1301
+ }
1302
+
1303
+ const bmp4Raw = await downloadFile(
1304
+ this.Type === 'one_video' ? playUrlData.data?.dash?.video[0].base_url : playUrlData.result.dash.video[0].base_url,
1305
+ {
1306
+ title: `Bil_V_${this.Type === 'one_video' ? infoData && infoData.data.bvid : infoData && infoData.result.season_id}.m4s`,
1307
+ headers: downloadHeaders
1308
+ }
1309
+ )
1310
+
1311
+ // 修复 m4s 文件为标准 MP4
1312
+ const videoPath =
1313
+ Common.tempDri.video +
1314
+ `Bil_V_${this.Type === 'one_video' ? infoData && infoData.data.bvid : infoData && infoData.result.season_id}.mp4`
1315
+ const videoFixed = await fixM4sFile(bmp4Raw.filepath, videoPath)
1316
+ if (!videoFixed) {
1317
+ logger.error('视频文件修复失败')
1318
+ return false
1319
+ }
1320
+ // 删除原始 m4s 文件
1321
+ await Common.removeFile(bmp4Raw.filepath, true)
1322
+
1323
+ const audioUrl =
1324
+ this.Type === 'one_video' ? playUrlData.data?.dash?.audio?.[0]?.base_url : playUrlData.result.dash.audio?.[0]?.base_url
1325
+ logger.debug('音频 URL:', audioUrl)
1326
+
1327
+ /** 没有音频流(如纯视频稿件)时为 undefined,此时无从合成,直接发视频流 */
1328
+ let bmp3: { filepath: string; totalBytes: number } | undefined
1329
+ if (audioUrl) {
1330
+ const bmp3Raw = await downloadFile(audioUrl, {
1331
+ title: `Bil_A_${this.Type === 'one_video' ? infoData && infoData.data.bvid : infoData && infoData.result.season_id}.m4s`,
1332
+ headers: downloadHeaders
1333
+ })
1334
+
1335
+ // 修复音频 m4s 文件为 m4a(AAC 音频不能直接转为 MP3 容器)
1336
+ const audioPath =
1337
+ Common.tempDri.video +
1338
+ `Bil_A_${this.Type === 'one_video' ? infoData && infoData.data.bvid : infoData && infoData.result.season_id}.m4a`
1339
+ const audioFixed = await fixM4sFile(bmp3Raw.filepath, audioPath)
1340
+ if (!audioFixed) {
1341
+ logger.error('音频文件修复失败')
1342
+ return false
1343
+ }
1344
+ // 删除原始 m4s 文件
1345
+ await Common.removeFile(bmp3Raw.filepath, true)
1346
+ bmp3 = { filepath: audioPath, totalBytes: bmp3Raw.totalBytes }
1347
+ }
1348
+
1349
+ const bmp4 = { filepath: videoPath, totalBytes: bmp4Raw.totalBytes }
1350
+
1351
+ if (bmp4.filepath) {
1352
+ // 根据是否有弹幕数据选择合成方式
1353
+ const hasDanmaku = (this.forceBurnDanmaku || Config.bilibili.burnDanmaku) && danmakuList.length > 0
1354
+ const resultPath =
1355
+ Common.tempDri.video +
1356
+ `Bil_Result_${this.Type === 'one_video' ? infoData && infoData.data.bvid : infoData && infoData.result.season_id}.mp4`
1357
+ let success: boolean
1358
+ /** 最终要上传的文件:合成/烧录的产物,或没有音频流时直接用的视频流 */
1359
+ let sourcePath = bmp4.filepath
1360
+
1361
+ /**
1362
+ * 弹幕解析的中间状态提示:下载已经完成、接下来是合成 + 烧录(要等一两分钟)。
1363
+ * 这时把「收到请求,开始下载」那条撤掉,换成「下载完成,正在添加弹幕」,
1364
+ * 让用户知道进度到哪了。
1365
+ */
1366
+ if (hasDanmaku) {
1367
+ // 用统一出口:它会撤掉上一条(「收到请求,开始下载」)**并把自己记下来**,
1368
+ // 等视频真正发出去时再被撤回(Base.ts 的发送流程会调 recallLastPanel)
1369
+ await replyReplacing(this.e, '下载完成,正在添加弹幕…')
1370
+ }
1371
+ if (!bmp3) {
1372
+ if (hasDanmaku) {
1373
+ logger.debug(`开始烧录 ${danmakuList.length} 条弹幕...`)
1374
+ success = await burnBiliDanmaku(bmp4.filepath, danmakuList, resultPath, {
1375
+ danmakuArea: Config.bilibili.danmakuArea,
1376
+ verticalMode: Config.bilibili.verticalMode,
1377
+ videoCodec: Config.bilibili.videoCodec,
1378
+ danmakuFontSize: Config.bilibili.danmakuFontSize,
1379
+ danmakuOpacity: Config.bilibili.danmakuOpacity
1380
+ })
1381
+ sourcePath = resultPath
1382
+ } else {
1383
+ success = true
1384
+ }
1385
+ } else if (hasDanmaku) {
1386
+ logger.debug(`开始合成视频并烧录 ${danmakuList.length} 条弹幕...`)
1387
+ success = await mergeAndBurnBili(bmp4.filepath, bmp3.filepath, danmakuList, resultPath, {
1388
+ danmakuArea: Config.bilibili.danmakuArea,
1389
+ verticalMode: Config.bilibili.verticalMode,
1390
+ videoCodec: Config.bilibili.videoCodec,
1391
+ danmakuFontSize: Config.bilibili.danmakuFontSize,
1392
+ danmakuOpacity: Config.bilibili.danmakuOpacity
1393
+ })
1394
+ sourcePath = resultPath
1395
+ } else {
1396
+ success = await mergeVideoAudio(bmp4.filepath, bmp3.filepath, resultPath)
1397
+ sourcePath = resultPath
1398
+ }
1399
+
1400
+ if (success) {
1401
+ const filePath = Common.tempDri.video + `${Config.app.removeCache ? 'tmp_' + Date.now() : this.downloadfilename}.mp4`
1402
+ fs.renameSync(sourcePath, filePath)
1403
+ logger.mark(`视频文件重命名完成: ${sourcePath.split('/').pop()} -> ${filePath.split('/').pop()}`)
1404
+ logger.mark('正在尝试删除缓存文件')
1405
+ if (fs.existsSync(bmp4.filepath)) await Common.removeFile(bmp4.filepath, true)
1406
+ if (bmp3 && fs.existsSync(bmp3.filepath)) await Common.removeFile(bmp3.filepath, true)
1407
+
1408
+ const stats = fs.statSync(filePath)
1409
+ const fileSizeInMB = Number((stats.size / (1024 * 1024)).toFixed(2))
1410
+ if (fileSizeInMB > Config.app.groupfilevalue) {
1411
+ // 使用文件上传
1412
+ await uploadFile(this.e, { filepath: filePath, totalBytes: fileSizeInMB, originTitle: this.downloadfilename }, '', {
1413
+ useGroupFile: true
1414
+ })
1415
+ } else {
1416
+ /** 因为本地合成,没有视频直链 */
1417
+ await uploadFile(this.e, { filepath: filePath, totalBytes: fileSizeInMB, originTitle: this.downloadfilename }, '')
1418
+ }
1419
+ } else {
1420
+ await Common.removeFile(bmp4.filepath, true)
1421
+ if (bmp3) await Common.removeFile(bmp3.filepath, true)
1422
+ }
1423
+ }
1424
+ break
1425
+ }
1426
+ case false: {
1427
+ /**
1428
+ * 没登录(没配置 ck)时直接发直链。
1429
+ *
1430
+ * 注意 `durl` 不一定在 `playUrlData.data` 下:html5 直链接口偶发失败时会退回 amagi 的形状,
1431
+ * 那时候对象层级不一样 —— 直接写 `playUrlData.data.durl[0].url` 会抛
1432
+ * `Cannot read properties of undefined (reading '0')`(登录状态下选 360P 就会中招)。
1433
+ */
1434
+ const anonymousInner = (playUrlData as any)?.data?.data ?? (playUrlData as any)?.data
1435
+ const directUrl: string | undefined =
1436
+ (playUrlData as any)?.data?.durl?.[0]?.url || anonymousInner?.durl?.[0]?.url
1437
+ logger.debug('视频 URL:', directUrl)
1438
+ if (!directUrl) {
1439
+ await this.e.reply('没有拿到可用的视频直链(未登录时部分稿件拿不到),可尝试【#B站登录】后再解析')
1440
+ return false
1441
+ }
1442
+ // 如果需要烧录弹幕,先下载视频再烧录
1443
+ if ((this.forceBurnDanmaku || Config.bilibili.burnDanmaku) && danmakuList.length > 0) {
1444
+ const videoFile = await downloadFile(directUrl, {
1445
+ title: `Bil_V_tmp_${Date.now()}.mp4`,
1446
+ headers: this.headers
1447
+ })
1448
+ if (videoFile.filepath) {
1449
+ const resultPath = Common.tempDri.video + `Bil_Result_${Date.now()}.mp4`
1450
+ logger.mark(`开始烧录 ${danmakuList.length} 条弹幕...`)
1451
+ const success = await burnBiliDanmaku(videoFile.filepath, danmakuList, resultPath, {
1452
+ danmakuArea: Config.bilibili.danmakuArea,
1453
+ verticalMode: Config.bilibili.verticalMode,
1454
+ videoCodec: Config.bilibili.videoCodec,
1455
+ danmakuFontSize: Config.bilibili.danmakuFontSize,
1456
+ danmakuOpacity: Config.bilibili.danmakuOpacity
1457
+ })
1458
+ if (success) {
1459
+ const filePath = Common.tempDri.video + `${Config.app.removeCache ? 'tmp_' + Date.now() : this.downloadfilename}.mp4`
1460
+ fs.renameSync(resultPath, filePath)
1461
+ await Common.removeFile(videoFile.filepath, true)
1462
+ const stats = fs.statSync(filePath)
1463
+ const fileSizeInMB = Number((stats.size / (1024 * 1024)).toFixed(2))
1464
+ if (fileSizeInMB > Config.app.groupfilevalue) {
1465
+ await uploadFile(this.e, { filepath: filePath, totalBytes: fileSizeInMB, originTitle: this.downloadfilename }, '', {
1466
+ useGroupFile: true
1467
+ })
1468
+ } else {
1469
+ await uploadFile(this.e, { filepath: filePath, totalBytes: fileSizeInMB, originTitle: this.downloadfilename }, '')
1470
+ }
1471
+ } else {
1472
+ await Common.removeFile(videoFile.filepath, true)
1473
+ }
1474
+ }
1475
+ } else {
1476
+ await downloadVideo(this.e, {
1477
+ video_url: directUrl,
1478
+ title: { timestampTitle: `tmp_${Date.now()}.mp4`, originTitle: `${this.downloadfilename}.mp4` }
1479
+ })
1480
+ }
1481
+ break
1482
+ }
1483
+ default:
1484
+ break
1485
+ }
1486
+ }
1487
+ }
1488
+
1489
+ const br = (data: string) => {
1490
+ return (data = data.replace(/\n/g, '<br>'))
1491
+ }
1492
+
1493
+ /**
1494
+ * 时间格式化工具函数集合
1495
+ */
1496
+ export const TimeFormatter = {
1497
+ /**
1498
+ * 格式化Unix时间戳为相对时间(如"2小时前")
1499
+ * @param timestamp Unix时间戳(秒)
1500
+ * @returns 格式化后的相对时间字符串
1501
+ */
1502
+ toRelative: (timestamp: number): string => {
1503
+ try {
1504
+ const date = fromUnixTime(timestamp)
1505
+ return formatDistanceToNow(date, {
1506
+ addSuffix: true,
1507
+ locale: zhCN
1508
+ })
1509
+ } catch (error) {
1510
+ logger.warn('相对时间格式化失败:', error)
1511
+ return TimeFormatter.toDateTime(timestamp)
1512
+ }
1513
+ },
1514
+
1515
+ /**
1516
+ * 格式化Unix时间戳为日期时间(yyyy-MM-dd HH:mm)
1517
+ * @param timestamp Unix时间戳(秒)
1518
+ * @returns 格式化后的日期时间字符串
1519
+ */
1520
+ toDateTime: (timestamp: number): string => {
1521
+ try {
1522
+ return format(fromUnixTime(timestamp), 'yyyy-MM-dd HH:mm')
1523
+ } catch (error) {
1524
+ logger.warn('日期时间格式化失败:', error)
1525
+ return '时间格式错误'
1526
+ }
1527
+ },
1528
+
1529
+ /**
1530
+ * 格式化当前时间为日期时间(yyyy-MM-dd HH:mm:ss)
1531
+ * @returns 格式化后的当前时间字符串
1532
+ */
1533
+ now: (): string => {
1534
+ try {
1535
+ return format(new Date(), 'yyyy-MM-dd HH:mm:ss')
1536
+ } catch (error) {
1537
+ logger.warn('当前时间格式化失败:', error)
1538
+ return new Date().toISOString()
1539
+ }
1540
+ }
1541
+ }
1542
+
1543
+ const qnd: Record<number, string> = {
1544
+ 6: '极速 240P',
1545
+ 16: '流畅 360P',
1546
+ 32: '清晰480P',
1547
+ 64: '高清720P',
1548
+ 74: '高帧率 720P60',
1549
+ 80: '高清 1080P',
1550
+ 112: '高码率 1080P+',
1551
+ 116: '高帧率 1080P60',
1552
+ 120: '超清 4K',
1553
+ 125: '真彩色 HDR ',
1554
+ 126: '杜比视界',
1555
+ 127: '超高清 8K'
1556
+ }
1557
+
1558
+ /**
1559
+ * 将给定的图片源数组转换为一个新的对象数组,每个对象包含单个图片源
1560
+ * @param pic 一个包含图片源字符串的数组
1561
+ * @returns 返回一个对象数组,每个对象包含单个图片源
1562
+ */
1563
+ export const cover = (pic: { img_src: string }[]) => {
1564
+ const imgArray = []
1565
+ for (const i of pic) {
1566
+ const obj = {
1567
+ image_src: i.img_src
1568
+ }
1569
+ imgArray.push(obj)
1570
+ }
1571
+ return imgArray
1572
+ }
1573
+
1574
+ /**
1575
+ * 生成装饰卡片数据
1576
+ * @param decorate 装饰对象,包含卡片的URL和颜色信息
1577
+ * @returns 返回装饰卡片数据对象或undefined
1578
+ */
1579
+ export const generateDecorationCard = (
1580
+ decorate: DynamicTypeDraw['data']['item']['modules']['module_author']['decoration_card']
1581
+ ): DecorationCardData | undefined => {
1582
+ if (!decorate) return undefined
1583
+ return {
1584
+ card_url: decorate.card_url,
1585
+ colors: decorate.fan?.color_format?.colors || [],
1586
+ text: decorate.fan?.num_str || decorate.fan?.num_desc || ''
1587
+ }
1588
+ }
1589
+
1590
+ /**
1591
+ * 处理B站动态中的相关内容卡片(additional)
1592
+ * @param additional 动态中的 additional 字段
1593
+ * @returns 处理后的卡片数据,如果不支持则返回 undefined
1594
+ * @see https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/dynamic/all.md
1595
+ */
1596
+ export const parseAdditionalCard = (additional: any) => {
1597
+ if (!additional) return undefined
1598
+
1599
+ switch (additional.type) {
1600
+ // 预约卡片(直播预约/视频预约)
1601
+ case 'ADDITIONAL_TYPE_RESERVE': {
1602
+ const reserve = additional.reserve
1603
+ if (!reserve) return undefined
1604
+
1605
+ // button.type: 1-直播预约 2-视频预约
1606
+ let buttonText = ''
1607
+ if (reserve.button.type === 1) {
1608
+ // 直播预约:使用 jump_style.text(如"去观看"、"已结束")
1609
+ buttonText = reserve.button.jump_style?.text ?? '预约'
1610
+ } else {
1611
+ // 视频预约:使用 uncheck.text(如"预约")或 check.text(如"已预约")
1612
+ buttonText = reserve.button.uncheck?.text ?? reserve.button.check?.text ?? '预约'
1613
+ }
1614
+
1615
+ return {
1616
+ type: 'ADDITIONAL_TYPE_RESERVE' as const,
1617
+ reserve: {
1618
+ title: reserve.title,
1619
+ desc1: reserve.desc1?.text ?? '',
1620
+ desc2: reserve.desc2?.text ?? '',
1621
+ desc3: reserve.desc3?.text,
1622
+ buttonText
1623
+ }
1624
+ }
1625
+ }
1626
+
1627
+ // 投票卡片
1628
+ case 'ADDITIONAL_TYPE_VOTE': {
1629
+ const vote = additional.vote
1630
+ if (!vote) return undefined
1631
+
1632
+ return {
1633
+ type: 'ADDITIONAL_TYPE_VOTE' as const,
1634
+ vote: {
1635
+ title: vote.title,
1636
+ desc: vote.desc,
1637
+ status: vote.status
1638
+ }
1639
+ }
1640
+ }
1641
+
1642
+ // 通用卡片(游戏、活动等)
1643
+ case 'ADDITIONAL_TYPE_COMMON': {
1644
+ if (!additional.common) return undefined
1645
+
1646
+ return {
1647
+ type: 'ADDITIONAL_TYPE_COMMON' as const,
1648
+ common: {
1649
+ cover: additional.common.cover,
1650
+ title: additional.common.title,
1651
+ desc1: additional.common.desc1,
1652
+ desc2: additional.common.desc2,
1653
+ button_text: additional.common.button?.jump_style?.text,
1654
+ head_text: additional.common.head_text,
1655
+ sub_type: additional.common.sub_type
1656
+ }
1657
+ }
1658
+ }
1659
+
1660
+ // 视频跳转卡片(UGC)
1661
+ case 'ADDITIONAL_TYPE_UGC': {
1662
+ const ugc = additional.ugc
1663
+ if (!ugc) return undefined
1664
+
1665
+ return {
1666
+ type: 'ADDITIONAL_TYPE_UGC' as const,
1667
+ ugc: {
1668
+ cover: ugc.cover,
1669
+ title: ugc.title,
1670
+ duration: ugc.duration,
1671
+ play: ugc.stat?.play ?? ugc.desc_second?.split(' ')[0]?.replace('观看', '播放') ?? '',
1672
+ danmaku: ugc.stat?.danmaku ?? ugc.desc_second?.split(' ')[1]?.replace('弹幕', '') ?? ''
1673
+ }
1674
+ }
1675
+ }
1676
+
1677
+ // 商品卡片
1678
+ case 'ADDITIONAL_TYPE_GOODS': {
1679
+ // TODO: 商品卡片暂未实现
1680
+ logger.error('商品卡片暂未实现,请将这次解析的内容反馈给开发者进行适配!')
1681
+ return undefined
1682
+ }
1683
+
1684
+ // 充电专属抽奖
1685
+ case 'ADDITIONAL_TYPE_UPOWER_LOTTERY': {
1686
+ // TODO: 充电专属抽奖暂未实现
1687
+ logger.error('充电专属抽奖暂未实现,请将这次解析的内容反馈给开发者进行适配!')
1688
+ return undefined
1689
+ }
1690
+
1691
+ default:
1692
+ logger.error('此卡片内容暂未实现,请将这次解析的内容反馈给开发者进行适配!')
1693
+ return undefined
1694
+ }
1695
+ }
1696
+
1697
+ const mapping_table = (type: any): number => {
1698
+ const Array: Record<string, string[]> = {
1699
+ 1: ['DYNAMIC_TYPE_AV', 'DYNAMIC_TYPE_PGC', 'DYNAMIC_TYPE_UGC_SEASON'],
1700
+ 11: ['DYNAMIC_TYPE_DRAW'],
1701
+ 12: ['DYNAMIC_TYPE_ARTICLE'],
1702
+ 17: ['DYNAMIC_TYPE_LIVE_RCMD', 'DYNAMIC_TYPE_FORWARD', 'DYNAMIC_TYPE_WORD', 'DYNAMIC_TYPE_COMMON_SQUARE'],
1703
+ 19: ['DYNAMIC_TYPE_MEDIALIST']
1704
+ }
1705
+ for (const key in Array) {
1706
+ if (Array[key].includes(type)) {
1707
+ return parseInt(key, 10)
1708
+ }
1709
+ }
1710
+ return 1
1711
+ }
1712
+
1713
+ /**
1714
+ * 根据动态类型获取对应的oid(对象ID),用于后续评论接口调用
1715
+ * @param dynamicType 动态类型
1716
+ * @param dynamicData 动态数据
1717
+ * @returns
1718
+ */
1719
+ const oid = (dynamicType: DynamicType, dynamicData: BilibiliDynamicDetailResponse) => {
1720
+ switch (dynamicType) {
1721
+ case DynamicType.WORD:
1722
+ case DynamicType.FORWARD: {
1723
+ return dynamicData.data.item.id_str
1724
+ }
1725
+ default: {
1726
+ return dynamicData.data.item.basic.rid_str.toString()
1727
+ }
1728
+ }
1729
+ }
1730
+
1731
+ type qualityOptions = {
1732
+ /**
1733
+ * qn值
1734
+ * @see https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/videostream_url.md#qn视频清晰度标识
1735
+ */
1736
+ qn?: number
1737
+ /** 可接受的最大视频文件单位:MB */
1738
+ maxAutoVideoSize?: number
1739
+ /** 视频BV号 */
1740
+ bvid: string
1741
+ /** 视频流清晰度列表 */
1742
+ accept_description: string[]
1743
+ }
1744
+ /**
1745
+ * 检出符合大小的视频流信息对象
1746
+ * @param accept_description 视频流清晰度列表
1747
+ * @param videoList 包含所有清晰度的视频流信息对象
1748
+ * @param audioUrl 音频流地址
1749
+ * @param bvid 视频bvid(BV号)
1750
+ * @returns
1751
+ */
1752
+ export const bilibiliProcessVideos = async (
1753
+ qualityOptions: qualityOptions,
1754
+ videoList: videoDownloadUrlList,
1755
+ audioUrl: string | undefined
1756
+ ) => {
1757
+ // 如果不是自动选择模式,直接根据配置的清晰度选择视频
1758
+ if (qualityOptions.qn !== 0 || Config.bilibili.videoQuality !== 0) {
1759
+ const targetQuality = qualityOptions.qn ?? Config.bilibili.videoQuality
1760
+
1761
+ // 尝试找到完全匹配的清晰度
1762
+ let matchedVideo = videoList.find((video) => video.id === targetQuality)
1763
+
1764
+ // 如果没有完全匹配的清晰度,找最接近的
1765
+ if (!matchedVideo) {
1766
+ // 按照清晰度ID排序
1767
+ const sortedVideos = [...videoList].sort((a, b) => a.id - b.id)
1768
+
1769
+ // 找到小于目标清晰度的最大值
1770
+ const lowerVideos = sortedVideos.filter((video) => video.id < targetQuality)
1771
+ const higherVideos = sortedVideos.filter((video) => video.id > targetQuality)
1772
+
1773
+ if (lowerVideos.length > 0) {
1774
+ // 有小于目标清晰度的,取最大的
1775
+ matchedVideo = lowerVideos[lowerVideos.length - 1]
1776
+ } else if (higherVideos.length > 0) {
1777
+ // 没有小于目标清晰度的,取最小的
1778
+ matchedVideo = higherVideos[0]
1779
+ } else {
1780
+ // 如果都没有,取第一个(应该不会发生)
1781
+ matchedVideo = sortedVideos[0]
1782
+ }
1783
+ }
1784
+
1785
+ // 更新视频列表和清晰度描述
1786
+ // accept_description 在免登录 / 只给 durl 的画质下可能是 undefined,
1787
+ // 直接 [0] 会抛 "Cannot read properties of undefined (reading '0')"(登录状态选 360P 就会中招)
1788
+ const matchedQuality = qnd[matchedVideo.id] || qualityOptions.accept_description?.[0] || ('qn' + matchedVideo.id)
1789
+ qualityOptions.accept_description = [matchedQuality]
1790
+ videoList = [matchedVideo]
1791
+
1792
+ return {
1793
+ accept_description: qualityOptions.accept_description,
1794
+ videoList
1795
+ }
1796
+ }
1797
+
1798
+ // 自动选择逻辑(videoQuality === 0)
1799
+ const results: Record<string, string> = {}
1800
+
1801
+ for (const video of videoList) {
1802
+ const size = await getvideosize(video.base_url, audioUrl, qualityOptions.bvid)
1803
+ results[video.id] = size
1804
+ }
1805
+
1806
+ // 将结果对象的值转换为数字,并找到最接近但不超过 qualityOptions.maxAutoVideoSize 或 Config.bilibili.maxAutoVideoSize 的值
1807
+ const sizes = Object.values(results).map((size) => parseFloat(size.replace('MB', '')))
1808
+ let closestId: string | null = null
1809
+ let smallestDifference = Infinity
1810
+
1811
+ sizes.forEach((size, index) => {
1812
+ if (size <= (qualityOptions?.maxAutoVideoSize ?? Config.bilibili.maxAutoVideoSize)) {
1813
+ const difference = Math.abs(size - (qualityOptions?.maxAutoVideoSize ?? Config.bilibili.maxAutoVideoSize))
1814
+ if (difference < smallestDifference) {
1815
+ smallestDifference = difference
1816
+ closestId = Object.keys(results)[index]
1817
+ }
1818
+ }
1819
+ })
1820
+
1821
+ if (closestId !== null) {
1822
+ // 找到最接近但不超过文件大小限制的视频清晰度
1823
+ const closestQuality = qnd[Number(closestId)]
1824
+ // 更新 OBJECT.DATA.data.accept_description
1825
+ qualityOptions.accept_description = qualityOptions.accept_description.filter((desc: any) => desc === closestQuality)
1826
+ if (qualityOptions.accept_description.length === 0) {
1827
+ qualityOptions.accept_description = [closestQuality]
1828
+ }
1829
+ // 找到对应的视频对象
1830
+ const video = videoList.find((video: { id: number }) => video.id === Number(closestId))!
1831
+ // 更新 OBJECT.DATA.data.dash.video 数组
1832
+ videoList = [video]
1833
+ } else {
1834
+ // 如果没有找到符合条件的视频,使用最低画质的视频对象
1835
+ videoList = [[...videoList].pop()!]
1836
+ // 更新 OBJECT.DATA.data.accept_description 为最低画质的描述
1837
+ qualityOptions.accept_description = [[...qualityOptions.accept_description].pop()!]
1838
+ }
1839
+ return {
1840
+ accept_description: qualityOptions.accept_description,
1841
+ videoList
1842
+ }
1843
+ }
1844
+
1845
+ /**
1846
+ * [bilibili] 获取视频和音频的总大小
1847
+ * @param videourl - 视频流URL
1848
+ * @param audiourl - 音频流URL,没有音频流(如纯视频稿件)时传 undefined,此时只统计视频流大小
1849
+ * @param bvid - 视频BV号
1850
+ * @returns 返回视频和音频总大小(MB),保留2位小数
1851
+ */
1852
+ export const getvideosize = async (videourl: string, audiourl: string | undefined, bvid: string) => {
1853
+ try {
1854
+ const videoheaders = await new Networks({
1855
+ url: videourl,
1856
+ headers: {
1857
+ ...baseHeaders,
1858
+ Referer: `https://www.bilibili.com/video/${bvid}`,
1859
+ Cookie: Config.amagi.cookies.bilibili
1860
+ }
1861
+ }).getHeaders()
1862
+ const audioheaders = audiourl
1863
+ ? await new Networks({
1864
+ url: audiourl,
1865
+ headers: {
1866
+ ...baseHeaders,
1867
+ Referer: `https://www.bilibili.com/video/${bvid}`,
1868
+ Cookie: Config.amagi.cookies.bilibili
1869
+ }
1870
+ }).getHeaders()
1871
+ : undefined
1872
+
1873
+ const videoSize = extractTotalBytesFromHeaders(videoheaders)
1874
+ const audioSize = audioheaders ? extractTotalBytesFromHeaders(audioheaders) : 0
1875
+
1876
+ const videoSizeInMB = (videoSize / (1024 * 1024)).toFixed(2)
1877
+ const audioSizeInMB = (audioSize / (1024 * 1024)).toFixed(2)
1878
+
1879
+ const totalSizeInMB = parseFloat(videoSizeInMB) + parseFloat(audioSizeInMB)
1880
+ return totalSizeInMB.toFixed(2)
1881
+ } catch (error) {
1882
+ logger.warn(`[koishi-plugin-kkk] 获取视频大小失败: ${error instanceof Error ? error.message : String(error)}`)
1883
+ return '0.00'
1884
+ }
1885
+ }
1886
+
1887
+ /**
1888
+ * 格式化视频统计信息为三行,每行两个数据项,并保持对齐
1889
+ */
1890
+ const formatVideoStats = (view: number, danmaku: number, like: number, coin: number, share: number, favorite: number): string => {
1891
+ // 计算每个数据项的文本
1892
+ const viewText = `📊 播放量: ${Count(view)}`
1893
+ const danmakuText = `💬 弹幕: ${Count(danmaku)}`
1894
+ const likeText = `👍 点赞: ${Count(like)}`
1895
+ const coinText = `🪙 投币: ${Count(coin)}`
1896
+ const shareText = `🔄 转发: ${Count(share)}`
1897
+ const favoriteText = `⭐ 收藏: ${Count(favorite)}`
1898
+
1899
+ // 找出第一列中最长的项的长度
1900
+ const firstColItems = [viewText, likeText, shareText]
1901
+ const maxFirstColLength = Math.max(...firstColItems.map((item) => getStringDisplayWidth(item)))
1902
+
1903
+ // 构建三行文本,确保第二列对齐
1904
+ const line1 = alignTwoColumns(viewText, danmakuText, maxFirstColLength)
1905
+ const line2 = alignTwoColumns(likeText, coinText, maxFirstColLength)
1906
+ const line3 = alignTwoColumns(shareText, favoriteText, maxFirstColLength)
1907
+
1908
+ return `${line1}\n${line2}\n${line3}`
1909
+ }
1910
+
1911
+ /**
1912
+ * 对齐两列文本
1913
+ */
1914
+ const alignTwoColumns = (col1: string, col2: string, targetLength: number): string => {
1915
+ // 计算需要添加的空格数量
1916
+ const col1Width = getStringDisplayWidth(col1)
1917
+ const spacesNeeded = targetLength - col1Width + 5 // 5是两列之间的固定间距
1918
+
1919
+ // 添加空格使两列对齐
1920
+ return col1 + ' '.repeat(spacesNeeded) + col2
1921
+ }
1922
+
1923
+ /**
1924
+ * 获取字符串在显示时的实际宽度
1925
+ * 考虑到不同字符的显示宽度不同(如中文、emoji等)
1926
+ */
1927
+ const getStringDisplayWidth = (str: string): number => {
1928
+ let width = 0
1929
+ for (let i = 0; i < str.length; i++) {
1930
+ const code = str.codePointAt(i)
1931
+ if (!code) continue
1932
+
1933
+ // 处理emoji和特殊Unicode字符
1934
+ if (code > 0xffff) {
1935
+ width += 2 // emoji通常占用2个字符宽度
1936
+ i++ // 跳过代理对的后半部分
1937
+ } else if (
1938
+ // 处理中文字符和其他全角字符
1939
+ (code >= 0x3000 && code <= 0x9fff) || // 中文字符范围
1940
+ (code >= 0xff00 && code <= 0xffef) || // 全角ASCII、全角标点
1941
+ code === 0x2026 || // 省略号
1942
+ code === 0x2014 || // 破折号
1943
+ (code >= 0x2e80 && code <= 0x2eff) || // CJK部首补充
1944
+ (code >= 0x3000 && code <= 0x303f) || // CJK符号和标点
1945
+ (code >= 0x31c0 && code <= 0x31ef) || // CJK笔画
1946
+ (code >= 0x3200 && code <= 0x32ff) || // 封闭式CJK字母和月份
1947
+ (code >= 0x3300 && code <= 0x33ff) || // CJK兼容
1948
+ (code >= 0xac00 && code <= 0xd7af) || // 朝鲜文音节
1949
+ (code >= 0xf900 && code <= 0xfaff) || // CJK兼容表意文字
1950
+ (code >= 0xfe30 && code <= 0xfe4f) // CJK兼容形式
1951
+ ) {
1952
+ width += 2
1953
+ } else if (code === 0x200d || (code >= 0xfe00 && code <= 0xfe0f) || (code >= 0x1f3fb && code <= 0x1f3ff)) {
1954
+ // emoji修饰符和连接符
1955
+ width += 0 // 这些字符不增加宽度,它们是修饰符
1956
+ } else {
1957
+ // 普通ASCII字符
1958
+ width += 1
1959
+ }
1960
+ }
1961
+ return width
1962
+ }
1963
+
1964
+ /**
1965
+ * 提取专栏中的所有图片URL
1966
+ * @param content
1967
+ * @returns
1968
+ */
1969
+ export const extractArticleImages = (content: NonNullable<BilibiliArticleContentResponse['data']>): string[] => {
1970
+ const images: string[] = []
1971
+
1972
+ // 处理 opus 格式(结构化数据)
1973
+ if (content.opus?.content?.paragraphs) {
1974
+ for (const paragraph of content.opus.content.paragraphs) {
1975
+ // para_type === 2 表示图片段落
1976
+ if (paragraph.para_type === 2 && paragraph.pic?.pics) {
1977
+ for (const pic of paragraph.pic.pics) {
1978
+ if (pic.url) {
1979
+ // 确保使用 https 协议
1980
+ const url = pic.url.startsWith('//') ? `https:${pic.url}` : pic.url
1981
+ images.push(url)
1982
+ }
1983
+ }
1984
+ }
1985
+ }
1986
+ }
1987
+
1988
+ // 处理 content 格式(HTML字符串)
1989
+ if (content.content && typeof content.content === 'string') {
1990
+ // 使用正则提取所有 img 标签的 src
1991
+ const imgRegex = /<img[^>]+src="([^"]+)"/gi
1992
+ let match
1993
+ while ((match = imgRegex.exec(content.content)) !== null) {
1994
+ let url = match[1]
1995
+ // 修复协议
1996
+ if (url.startsWith('//')) {
1997
+ url = `https:${url}`
1998
+ }
1999
+ images.push(url)
2000
+ }
2001
+ }
2002
+
2003
+ return images
2004
+ }